#!/usr/bin/env node /** * End-to-end browser audit for the deployed img2threejs Space. * * This intentionally uses only Node.js built-ins and Chromium's raw Chrome * DevTools Protocol. It does not start a local server and does not use * Playwright, Puppeteer, Selenium, or sleep-based polling. * * Examples: * node scripts/live_browser_audit.mjs \ * --url https://mike0021-img2threejs.hf.space \ * --image tests/fixtures/mug_photo.png \ * --output-dir /tmp/img2threejs-browser-audit * * node scripts/live_browser_audit.mjs \ * --url https://mike0021-img2threejs.hf.space \ * --skip-submit --gallery-id 0123456789abcdef0123456789abcdef \ * --output-dir /tmp/img2threejs-browser-audit * * The Hugging Face parent page is derived from /health.space_id. Pass * --hub-url only when auditing a deployment that does not expose SPACE_ID. */ import { spawn } from 'node:child_process'; import { createHash } from 'node:crypto'; import { access, constants as fsConstants, mkdir, mkdtemp, readFile, rm, stat, writeFile, } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { basename, join, resolve } from 'node:path'; import process from 'node:process'; const DEFAULT_GENERATION_TIMEOUT_MS = 15 * 60 * 1000; const DEFAULT_VIEWER_TIMEOUT_MS = 75 * 1000; const DEFAULT_ACTION_TIMEOUT_MS = 30 * 1000; const GALLERY_ID_PATTERN = /^[a-f0-9]{32}$/i; const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); function usage() { return ` Usage: node scripts/live_browser_audit.mjs --url URL --image IMAGE --output-dir DIR [options] node scripts/live_browser_audit.mjs --url URL --skip-submit --gallery-id ID --output-dir DIR [options] Required: --url URL Direct deployed app URL (for example, *.hf.space) --output-dir DIR Parent directory for screenshots and downloaded files --image FILE Image submitted through #file-input (unless --skip-submit) Options: --hint TEXT Optional object hint entered before submission --skip-submit Audit an existing shared gallery item --gallery-id ID Existing 32-hex gallery ID (required with --skip-submit) --hub-url URL huggingface.co/spaces/... parent page (normally auto-detected) --chromium FILE Chromium executable (auto-detected by default) --timeout-ms N Generation timeout (default ${DEFAULT_GENERATION_TIMEOUT_MS}) --viewer-timeout-ms N Viewer-ready timeout (default ${DEFAULT_VIEWER_TIMEOUT_MS}) --help Show this help `.trim(); } function parseArgs(argv) { const options = { url: null, image: null, outputDir: null, hint: '', skipSubmit: false, galleryId: null, hubUrl: null, chromium: null, timeoutMs: DEFAULT_GENERATION_TIMEOUT_MS, viewerTimeoutMs: DEFAULT_VIEWER_TIMEOUT_MS, }; const valueOptions = new Map([ ['--url', 'url'], ['--image', 'image'], ['--output-dir', 'outputDir'], ['--hint', 'hint'], ['--gallery-id', 'galleryId'], ['--hub-url', 'hubUrl'], ['--chromium', 'chromium'], ['--timeout-ms', 'timeoutMs'], ['--viewer-timeout-ms', 'viewerTimeoutMs'], ]); for (let index = 0; index < argv.length; index += 1) { const raw = argv[index]; if (raw === '--help' || raw === '-h') { options.help = true; continue; } if (raw === '--skip-submit') { options.skipSubmit = true; continue; } const equals = raw.indexOf('='); const flag = equals === -1 ? raw : raw.slice(0, equals); if (!valueOptions.has(flag)) throw new Error(`Unknown argument: ${raw}`); const inlineValue = equals === -1 ? null : raw.slice(equals + 1); const value = inlineValue ?? argv[++index]; if (value === undefined || value === '') throw new Error(`${flag} requires a value.`); options[valueOptions.get(flag)] = value; } if (options.help) return options; if (!options.url) throw new Error('--url is required.'); if (!options.outputDir) throw new Error('--output-dir is required.'); if (options.skipSubmit && !options.galleryId) { throw new Error('--gallery-id is required with --skip-submit.'); } if (!options.skipSubmit && !options.image) { throw new Error('--image is required unless --skip-submit is used.'); } if (options.galleryId && !GALLERY_ID_PATTERN.test(options.galleryId)) { throw new Error('--gallery-id must be a 32-character hexadecimal ID.'); } for (const key of ['timeoutMs', 'viewerTimeoutMs']) { const value = Number(options[key]); if (!Number.isFinite(value) || value < 1000) { throw new Error(`--${key === 'timeoutMs' ? 'timeout-ms' : 'viewer-timeout-ms'} must be at least 1000.`); } options[key] = Math.floor(value); } return options; } function assert(condition, message) { if (!condition) throw new Error(`Assertion failed: ${message}`); } function delayTimeout(ms, callback) { const timer = setTimeout(callback, ms); timer.unref?.(); return timer; } async function findChromium(explicitPath) { const candidates = [ explicitPath, process.env.CHROMIUM_BIN, '/snap/bin/chromium', '/usr/bin/chromium', '/usr/bin/chromium-browser', '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', ].filter(Boolean); for (const candidate of candidates) { try { await access(candidate, fsConstants.X_OK); return candidate; } catch { // Try the next known location. } } throw new Error( `Could not find Chromium. Pass --chromium FILE. Checked: ${candidates.join(', ')}`, ); } function directAppRoot(rawUrl) { const parsed = new URL(rawUrl); assert(parsed.protocol === 'https:', 'the live audit URL must use HTTPS'); assert( !['localhost', '127.0.0.1', '::1'].includes(parsed.hostname), 'the audit must target a deployed app, not a local server', ); parsed.pathname = '/'; parsed.search = ''; parsed.hash = ''; return parsed.href; } async function resolveHubParentUrl(explicitUrl, appRoot) { if (explicitUrl) { const parsed = new URL(explicitUrl); assert(parsed.protocol === 'https:', '--hub-url must use HTTPS'); assert(parsed.hostname === 'huggingface.co', '--hub-url must use huggingface.co'); assert(/^\/spaces\/[^/]+\/[^/]+\/?$/.test(parsed.pathname), '--hub-url must identify a Space parent page'); parsed.search = ''; parsed.hash = ''; return parsed.href.replace(/\/$/, ''); } const response = await fetch(new URL('/health', appRoot), { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(30_000), }); assert(response.ok, `could not discover Space ID from /health (HTTP ${response.status})`); const health = await response.json(); const spaceId = String(health.space_id || ''); assert( /^[^/\s]+\/[^/\s]+$/.test(spaceId), 'the deployed /health response has no SPACE_ID; pass --hub-url explicitly', ); return `https://huggingface.co/spaces/${spaceId}`; } class CdpConnection { constructor(socket, commandTimeoutMs = DEFAULT_ACTION_TIMEOUT_MS) { this.socket = socket; this.commandTimeoutMs = commandTimeoutMs; this.nextId = 1; this.pending = new Map(); this.eventHandlers = new Set(); this.closed = false; socket.addEventListener('message', (event) => { void this.#handleMessage(event.data); }); socket.addEventListener('close', () => { this.closed = true; for (const pending of this.pending.values()) { clearTimeout(pending.timer); pending.reject(new Error('The Chromium DevTools connection closed.')); } this.pending.clear(); }); socket.addEventListener('error', () => { // The corresponding close or command timeout produces the actionable error. }); } async #handleMessage(raw) { let text; if (typeof raw === 'string') text = raw; else if (raw instanceof ArrayBuffer) text = Buffer.from(raw).toString('utf8'); else if (typeof raw?.text === 'function') text = await raw.text(); else text = Buffer.from(raw).toString('utf8'); const message = JSON.parse(text); if (message.id !== undefined) { const pending = this.pending.get(message.id); if (!pending) return; this.pending.delete(message.id); clearTimeout(pending.timer); if (message.error) { pending.reject( new Error(`${pending.method}: ${message.error.message} (${message.error.code})`), ); } else { pending.resolve(message.result || {}); } return; } for (const handler of [...this.eventHandlers]) { try { handler(message); } catch { // Event consumers own their asynchronous errors. } } } send(method, params = {}, sessionId = undefined, timeoutMs = this.commandTimeoutMs) { if (this.closed) return Promise.reject(new Error('The DevTools connection is closed.')); const id = this.nextId++; const payload = { id, method, params }; if (sessionId) payload.sessionId = sessionId; return new Promise((resolvePromise, rejectPromise) => { const timer = delayTimeout(timeoutMs, () => { this.pending.delete(id); rejectPromise(new Error(`${method} timed out after ${timeoutMs} ms.`)); }); this.pending.set(id, { method, resolve: resolvePromise, reject: rejectPromise, timer, }); this.socket.send(JSON.stringify(payload)); }); } onEvent(handler) { this.eventHandlers.add(handler); return () => this.eventHandlers.delete(handler); } waitForEvent(method, predicate = () => true, options = {}) { const { sessionId, timeoutMs = this.commandTimeoutMs } = options; return new Promise((resolvePromise, rejectPromise) => { let remove = () => {}; const timer = delayTimeout(timeoutMs, () => { remove(); rejectPromise(new Error(`CDP event ${method} timed out after ${timeoutMs} ms.`)); }); remove = this.onEvent((event) => { if (event.method !== method) return; if (sessionId !== undefined && event.sessionId !== sessionId) return; if (!predicate(event.params || {})) return; clearTimeout(timer); remove(); resolvePromise(event.params || {}); }); }); } close() { if (!this.closed) this.socket.close(); } } class TargetMonitor { constructor(cdp, rootSessionId, rootTargetId, issueSink) { this.cdp = cdp; this.rootSessionId = rootSessionId; this.issueSink = issueSink; this.records = new Map(); this.waiters = new Set(); this.removeListener = null; this.records.set(rootSessionId, { sessionId: rootSessionId, targetId: rootTargetId, targetInfo: { targetId: rootTargetId, type: 'page', url: 'about:blank' }, attached: true, ready: Promise.resolve(), }); } async start() { this.removeListener = this.cdp.onEvent((event) => this.#handle(event)); await this.#enableAutoAttach(this.rootSessionId); } async #enableAutoAttach(sessionId) { await this.cdp.send('Target.setAutoAttach', { autoAttach: true, // Pausing every target can deadlock a sandboxed viewer nested inside the // Hugging Face Space iframe before it can emit its shell-ready message. // Error/log domains still attach early without freezing target scripts. waitForDebuggerOnStart: false, flatten: true, filter: [ { type: 'iframe', exclude: false }, { exclude: true }, ], }, sessionId); } #recordIssue(event, kind, text) { const record = this.records.get(event.sessionId); const url = record?.targetInfo?.url || 'unknown target'; this.issueSink.push(`${kind} [${url}]: ${text}`); } #handle(event) { if (event.method === 'Target.attachedToTarget') { const params = event.params || {}; const sessionId = params.sessionId; if (!sessionId) return; const record = { sessionId, targetId: params.targetInfo?.targetId, targetInfo: params.targetInfo || {}, parentSessionId: event.sessionId || null, waitingForDebugger: Boolean(params.waitingForDebugger), mainFrameId: null, attached: true, ready: null, }; record.ready = (async () => { await Promise.all([ this.cdp.send('Runtime.enable', {}, sessionId), this.cdp.send('Log.enable', {}, sessionId), this.cdp.send('Page.enable', {}, sessionId), this.#enableAutoAttach(sessionId), ]); if (record.waitingForDebugger) { await this.cdp.send('Runtime.runIfWaitingForDebugger', {}, sessionId); } })().catch((error) => { this.issueSink.push( `CDP auto-attach failed [${record.targetInfo.url || record.targetId}]: ${error.message}`, ); }); this.records.set(sessionId, record); this.#notifyWaiters(); return; } if (event.method === 'Target.detachedFromTarget') { const record = this.records.get(event.params?.sessionId); if (record) record.attached = false; this.#notifyWaiters(); return; } if (event.method === 'Target.targetInfoChanged') { const targetInfo = event.params?.targetInfo; if (!targetInfo) return; for (const record of this.records.values()) { if (record.targetId === targetInfo.targetId) record.targetInfo = targetInfo; } this.#notifyWaiters(); return; } if (event.method === 'Page.frameNavigated') { const record = this.records.get(event.sessionId); const frame = event.params?.frame; if ( record && frame?.url && ( !record.mainFrameId || frame.id === record.mainFrameId || frame.id === record.targetId ) ) { record.mainFrameId ||= frame.id; record.targetInfo = { ...record.targetInfo, url: frame.url }; this.#notifyWaiters(); } return; } if (!this.records.has(event.sessionId)) return; if (event.method === 'Runtime.exceptionThrown') { const details = event.params?.exceptionDetails || {}; this.#recordIssue( event, 'Uncaught exception', details.exception?.description || details.text || 'unknown', ); } else if (event.method === 'Runtime.consoleAPICalled' && event.params?.type === 'error') { const text = (event.params.args || []) .map((arg) => arg.value ?? arg.description ?? arg.type) .join(' '); this.#recordIssue(event, 'console.error', text); } else if (event.method === 'Log.entryAdded' && event.params?.entry?.level === 'error') { this.#recordIssue(event, 'page log error', event.params.entry.text); } } #notifyWaiters() { for (const waiter of [...this.waiters]) { const match = [...this.records.values()].find( (record) => record.attached && waiter.predicate(record), ); if (!match) continue; clearTimeout(waiter.timer); this.waiters.delete(waiter); Promise.resolve(match.ready).then( () => waiter.resolve(match), waiter.reject, ); } } waitForTarget(predicate, timeoutMs = 60_000) { const existing = [...this.records.values()].find( (record) => record.attached && predicate(record), ); if (existing) return Promise.resolve(existing.ready).then(() => existing); return new Promise((resolvePromise, rejectPromise) => { const waiter = { predicate, resolve: resolvePromise, reject: rejectPromise, timer: null, }; waiter.timer = delayTimeout(timeoutMs, () => { this.waiters.delete(waiter); rejectPromise(new Error(`No matching iframe/OOPIF target appeared within ${timeoutMs} ms.`)); }); this.waiters.add(waiter); }); } attachedTargets() { return [...this.records.values()] .filter((record) => record.attached) .map((record) => ({ sessionId: record.sessionId, targetId: record.targetId, type: record.targetInfo?.type || '', url: record.targetInfo?.url || '', parentSessionId: record.parentSessionId || null, })); } close() { this.removeListener?.(); for (const waiter of this.waiters) { clearTimeout(waiter.timer); waiter.reject(new Error('Target monitor closed.')); } this.waiters.clear(); } } async function launchChromium(binary, userDataDir) { const argumentsList = [ '--headless=new', '--no-sandbox', '--disable-dev-shm-usage', '--disable-background-networking', '--disable-component-update', '--disable-default-apps', '--disable-features=Translate', '--no-first-run', '--remote-debugging-port=0', '--remote-allow-origins=*', `--user-data-dir=${userDataDir}`, '--window-size=1440,1000', '--use-gl=angle', '--use-angle=swiftshader', '--enable-webgl', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', 'about:blank', ]; const child = spawn(binary, argumentsList, { stdio: ['ignore', 'ignore', 'pipe'], }); const stderrTail = []; const webSocketUrl = await new Promise((resolvePromise, rejectPromise) => { let rollingText = ''; const timer = delayTimeout(30_000, () => { rejectPromise( new Error(`Chromium did not expose a DevTools endpoint.\n${stderrTail.join('')}`), ); }); const onExit = (code, signal) => { clearTimeout(timer); rejectPromise( new Error( `Chromium exited before DevTools was ready (code=${code}, signal=${signal}).\n` + stderrTail.join(''), ), ); }; child.once('exit', onExit); child.stderr.on('data', (chunk) => { const text = chunk.toString('utf8'); stderrTail.push(text); if (stderrTail.length > 40) stderrTail.shift(); rollingText = `${rollingText}${text}`.slice(-8192); const match = rollingText.match(/DevTools listening on (ws:\/\/[^\s]+)/); if (!match) return; clearTimeout(timer); child.off('exit', onExit); resolvePromise(match[1]); }); }); const socket = new WebSocket(webSocketUrl); await new Promise((resolvePromise, rejectPromise) => { const timer = delayTimeout(15_000, () => rejectPromise(new Error('DevTools WebSocket timed out.'))); socket.addEventListener('open', () => { clearTimeout(timer); resolvePromise(); }, { once: true }); socket.addEventListener('error', () => { clearTimeout(timer); rejectPromise(new Error('DevTools WebSocket could not be opened.')); }, { once: true }); }); return { child, cdp: new CdpConnection(socket), stderrTail }; } async function stopChromium(child) { if (!child || child.exitCode !== null || child.signalCode !== null) return; const exited = new Promise((resolvePromise) => child.once('exit', resolvePromise)); child.kill('SIGTERM'); const forceTimer = delayTimeout(5000, () => child.kill('SIGKILL')); await exited; clearTimeout(forceTimer); } class BrowserPage { constructor(cdp, sessionId) { this.cdp = cdp; this.sessionId = sessionId; } send(method, params = {}, timeoutMs = DEFAULT_ACTION_TIMEOUT_MS) { return this.cdp.send(method, params, this.sessionId, timeoutMs); } async evaluate(expression, options = {}) { const { awaitPromise = false, timeoutMs = DEFAULT_ACTION_TIMEOUT_MS, userGesture = false, } = options; const response = await this.send('Runtime.evaluate', { expression, awaitPromise, returnByValue: true, userGesture, }, timeoutMs); if (response.exceptionDetails) { const exception = response.exceptionDetails.exception; const description = exception?.description || exception?.value || response.exceptionDetails.text || 'JavaScript evaluation failed'; throw new Error(String(description)); } return response.result?.value; } waitForCondition(predicateSource, description, timeoutMs = DEFAULT_ACTION_TIMEOUT_MS) { const expression = `new Promise((resolve, reject) => { const description = ${JSON.stringify(description)}; let observer = null; let timer = null; const cleanup = () => { if (observer) observer.disconnect(); if (timer) clearTimeout(timer); window.removeEventListener('popstate', check); window.removeEventListener('hashchange', check); }; const check = () => { try { const value = (${predicateSource})(); if (value) { cleanup(); resolve(value); } } catch (error) { cleanup(); reject(error); } }; observer = new MutationObserver(check); observer.observe(document.documentElement, { subtree: true, childList: true, characterData: true, attributes: true }); window.addEventListener('popstate', check); window.addEventListener('hashchange', check); timer = setTimeout(() => { cleanup(); reject(new Error("Timed out waiting for " + description)); }, ${timeoutMs}); check(); })`; return this.evaluate(expression, { awaitPromise: true, timeoutMs: timeoutMs + 5000, }); } clickAndWait(selector, predicateSource, description, timeoutMs = DEFAULT_ACTION_TIMEOUT_MS) { const expression = `new Promise((resolve, reject) => { const selector = ${JSON.stringify(selector)}; const description = ${JSON.stringify(description)}; const target = document.querySelector(selector); if (!target) { reject(new Error("Missing click target " + selector)); return; } let observer = null; let timer = null; const cleanup = () => { if (observer) observer.disconnect(); if (timer) clearTimeout(timer); window.removeEventListener('popstate', check); window.removeEventListener('hashchange', check); }; const check = () => { try { const value = (${predicateSource})(); if (value) { cleanup(); resolve(value); } } catch (error) { cleanup(); reject(error); } }; observer = new MutationObserver(check); observer.observe(document.documentElement, { subtree: true, childList: true, characterData: true, attributes: true }); window.addEventListener('popstate', check); window.addEventListener('hashchange', check); timer = setTimeout(() => { cleanup(); reject(new Error("Timed out waiting for " + description)); }, ${timeoutMs}); target.click(); check(); })`; return this.evaluate(expression, { awaitPromise: true, timeoutMs: timeoutMs + 5000, userGesture: true, }); } async navigate(url, timeoutMs = DEFAULT_ACTION_TIMEOUT_MS) { const loaded = this.cdp.waitForEvent('Page.loadEventFired', () => true, { sessionId: this.sessionId, timeoutMs, }); const result = await this.send('Page.navigate', { url }, timeoutMs); if (result.errorText) throw new Error(`Navigation failed: ${result.errorText}`); await loaded; } async settleFrames() { await this.evaluate( 'new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))', { awaitPromise: true }, ); } async setDesktopViewport() { await this.send('Emulation.setDeviceMetricsOverride', { width: 1440, height: 1000, screenWidth: 1440, screenHeight: 1000, deviceScaleFactor: 1, mobile: false, }); await this.send('Emulation.setTouchEmulationEnabled', { enabled: false }); await this.settleFrames(); } async setMobileViewport() { await this.send('Emulation.setDeviceMetricsOverride', { width: 390, height: 844, screenWidth: 390, screenHeight: 844, deviceScaleFactor: 2, mobile: true, }); await this.send('Emulation.setTouchEmulationEnabled', { enabled: true, maxTouchPoints: 5, }); await this.settleFrames(); } async screenshot(filePath, selector = null) { if (selector) { await this.evaluate( `document.querySelector(${JSON.stringify(selector)})?.scrollIntoView({block: "center", inline: "center"})`, ); await this.settleFrames(); } const result = await this.send('Page.captureScreenshot', { format: 'png', fromSurface: true, captureBeyondViewport: false, }, 60_000); const bytes = Buffer.from(result.data, 'base64'); assert(bytes.subarray(0, 8).equals(PNG_SIGNATURE), `invalid PNG screenshot: ${filePath}`); await writeFile(filePath, bytes); return { path: filePath, bytes: bytes.length, sha256: sha256(bytes) }; } async clippedScreenshot(selector) { const rect = await this.evaluate(`(() => { const element = document.querySelector(${JSON.stringify(selector)}); if (!element) throw new Error("Missing screenshot target"); element.scrollIntoView({block: "center", inline: "center"}); const bounds = element.getBoundingClientRect(); return { x: bounds.left + window.scrollX, y: bounds.top + window.scrollY, width: bounds.width, height: bounds.height }; })()`); await this.settleFrames(); assert(rect.width > 10 && rect.height > 10, `${selector} has no visible screenshot area`); const result = await this.send('Page.captureScreenshot', { format: 'png', fromSurface: true, captureBeyondViewport: true, clip: { x: Math.max(0, rect.x), y: Math.max(0, rect.y), width: rect.width, height: rect.height, scale: 1, }, }, 60_000); return Buffer.from(result.data, 'base64'); } async elementCenter(selector) { return this.evaluate(`(() => { const element = document.querySelector(${JSON.stringify(selector)}); if (!element) throw new Error("Missing interaction target"); element.scrollIntoView({block: "center", inline: "center"}); const bounds = element.getBoundingClientRect(); if (bounds.width < 10 || bounds.height < 10) throw new Error("Interaction target is not visible"); return { x: bounds.left + bounds.width / 2, y: bounds.top + bounds.height / 2, width: bounds.width, height: bounds.height }; })()`); } } class DownloadMonitor { constructor(cdp, downloadDirectory, timeoutMs = 60_000) { this.cdp = cdp; this.downloadDirectory = downloadDirectory; this.timeoutMs = timeoutMs; this.waiting = []; this.records = new Map(); this.removeListener = cdp.onEvent((event) => this.#handle(event)); } #handle(event) { if (event.method === 'Browser.downloadWillBegin') { const waiter = this.waiting.shift(); if (!waiter) return; const params = event.params || {}; const record = { label: waiter.label, guid: params.guid, suggestedFilename: params.suggestedFilename, url: params.url, done: null, completionTimer: null, resolveDone: null, rejectDone: null, }; record.done = new Promise((resolvePromise, rejectPromise) => { record.resolveDone = resolvePromise; record.rejectDone = rejectPromise; }); record.completionTimer = delayTimeout(this.timeoutMs, () => { record.rejectDone(new Error(`${record.label} did not finish downloading.`)); }); this.records.set(record.guid, record); clearTimeout(waiter.timer); waiter.resolve(record); return; } if (event.method !== 'Browser.downloadProgress') return; const params = event.params || {}; const record = this.records.get(params.guid); if (!record) return; if (params.state === 'completed') { clearTimeout(record.completionTimer); record.receivedBytes = params.receivedBytes; record.totalBytes = params.totalBytes; record.filePath = params.filePath || join(this.downloadDirectory, record.guid); record.resolveDone(record); } else if (params.state === 'canceled') { clearTimeout(record.completionTimer); record.rejectDone(new Error(`${record.label} download was canceled.`)); } } expect(label) { return new Promise((resolvePromise, rejectPromise) => { const waiter = { label, resolve: resolvePromise, reject: rejectPromise, timer: null, }; waiter.timer = delayTimeout(this.timeoutMs, () => { const position = this.waiting.indexOf(waiter); if (position !== -1) this.waiting.splice(position, 1); rejectPromise(new Error(`${label} did not start downloading.`)); }); this.waiting.push(waiter); }); } close() { this.removeListener(); for (const waiter of this.waiting) { clearTimeout(waiter.timer); waiter.reject(new Error('Download monitor closed.')); } this.waiting.length = 0; for (const record of this.records.values()) clearTimeout(record.completionTimer); } } function sha256(bytes) { return createHash('sha256').update(bytes).digest('hex'); } function pageConditionVisible(selector) { return `() => { const element = document.querySelector(${JSON.stringify(selector)}); return element && !element.hidden && element.getClientRects().length > 0; }`; } async function installAuditRecorder(page, onUpdate) { const bindingName = '__i2tAuditEmit'; await page.send('Runtime.addBinding', { name: bindingName }); const removeListener = page.cdp.onEvent((event) => { if ( event.sessionId !== page.sessionId || event.method !== 'Runtime.bindingCalled' || event.params?.name !== bindingName ) return; try { onUpdate(JSON.parse(event.params.payload)); } catch { // The in-page recorder is diagnostic; malformed diagnostics never alter the app. } }); await page.evaluate(`(() => { window.__i2tAuditJobResponse = null; window.__i2tAuditProgress = []; const originalFetch = window.fetch.bind(window); window.fetch = async (...args) => { const response = await originalFetch(...args); try { const request = args[0]; const init = args[1] || {}; const url = new URL(typeof request === "string" ? request : request.url, location.href); const method = String(init.method || (typeof request !== "string" && request.method) || "GET").toUpperCase(); if (method === "POST" && url.pathname === "/api/jobs") { response.clone().json().then((value) => { window.__i2tAuditJobResponse = value; if (value?.job_id) { window.${bindingName}(JSON.stringify({ type: "job-accepted", jobId: value.job_id })); } }).catch(() => {}); } } catch { // The app still receives the untouched response. } return response; }; let previousKey = ""; const snapshot = () => { const panel = document.querySelector("#panel-progress"); if (!panel || panel.hidden) return; const stages = [...document.querySelectorAll("#stages .stage")].map((stage) => ({ stage: stage.dataset.stage, state: [...stage.classList].find((name) => name.startsWith("stage-") && name !== "stage") || "", note: stage.querySelector(".stage-note")?.textContent?.trim() || "" })); const logLines = (document.querySelector("#log")?.textContent || "").trim().split("\\n").filter(Boolean); const value = { at: Date.now(), elapsed: document.querySelector("#progress-elapsed")?.textContent?.trim() || "", activity: document.querySelector("#progress-activity")?.textContent?.trim() || "", activityTime: document.querySelector("#progress-activity-time")?.textContent?.trim() || "", attempt: document.querySelector("#progress-attempt")?.textContent?.trim() || "", connection: document.querySelector("#progress-connection")?.textContent?.trim() || "", stages, lastLogLine: logLines.at(-1) || "", logLineCount: logLines.length }; const key = JSON.stringify({ activity: value.activity, attempt: value.attempt, connection: value.connection, stages: value.stages, lastLogLine: value.lastLogLine }); if (key !== previousKey) { previousKey = key; window.__i2tAuditProgress.push(value); if (value.lastLogLine) { const active = [...value.stages].reverse().find((stage) => stage.state === "stage-active" || stage.state === "stage-done" ); window.${bindingName}(JSON.stringify({ type: "progress", elapsed: value.elapsed, stage: active?.stage || "pipeline", activity: value.activity, attempt: value.attempt, lastLogLine: value.lastLogLine })); } } }; window.__i2tAuditProgressObserver?.disconnect(); window.__i2tAuditProgressObserver = new MutationObserver(snapshot); window.__i2tAuditProgressObserver.observe(document.body, { subtree: true, childList: true, characterData: true, attributes: true }); snapshot(); })()`); return removeListener; } async function auditExternalLinks(page) { const result = await page.evaluate(`(() => { const links = [...document.querySelectorAll("a[href]")].map((link) => { const url = new URL(link.href, location.href); return { text: link.textContent.trim().replace(/\\s+/g, " "), href: url.href, external: url.origin !== location.origin, target: link.getAttribute("target") || "", rel: (link.getAttribute("rel") || "").split(/\\s+/).filter(Boolean) }; }); const external = links.filter((link) => link.external); return { count: external.length, links: external, violations: external.filter((link) => link.target !== "_blank" || !link.rel.includes("noopener") || !link.rel.includes("noreferrer") ) }; })()`); assert(result.count > 0, 'the page should expose at least one external project link'); assert( result.violations.length === 0, `external-link safety violations: ${JSON.stringify(result.violations)}`, ); return result; } async function selectFile(page, absoluteImagePath) { const { root } = await page.send('DOM.getDocument', { depth: 2, pierce: true }); const { nodeId } = await page.send('DOM.querySelector', { nodeId: root.nodeId, selector: '#file-input', }); assert(nodeId, 'missing #file-input'); await page.send('DOM.setFileInputFiles', { nodeId, files: [absoluteImagePath], }); await page.waitForCondition( `() => { const preview = document.querySelector("#preview-row"); const run = document.querySelector("#run-btn"); return preview && !preview.hidden && run && !run.disabled ? {name: document.querySelector("#preview-name")?.textContent || ""} : false; }`, 'the selected image preview', ); } async function inspectProgress(page) { const progress = await page.evaluate('window.__i2tAuditProgress || []'); assert(progress.length >= 3, `expected several server-backed progress updates, got ${progress.length}`); assert( progress.every((entry) => /^\d+:\d{2}(?::\d{2})?$/.test(entry.elapsed)), 'each recorded progress update must show a clock-style elapsed time', ); const logLines = Math.max(...progress.map((entry) => Number(entry.logLineCount) || 0)); assert(logLines > 0, 'the live server event log should contain at least one event'); const activities = [...new Set(progress.map((entry) => entry.activity).filter(Boolean))]; assert(activities.length >= 3, 'progress should contain multiple distinct server activities'); const allText = JSON.stringify(progress); assert(!/\b\d{1,3}\s*%/.test(allText), 'progress UI must not invent percentage completion'); const fakeProgress = await page.evaluate( 'document.querySelectorAll("progress, meter, [role=progressbar], [aria-valuenow]").length', ); assert(fakeProgress === 0, 'progress UI should not expose a fabricated numeric progress bar'); return { updates: progress.length, activities, finalElapsed: progress.at(-1)?.elapsed || '', maxLogLines: logLines, observedStages: [...new Set( progress.flatMap((entry) => entry.stages || []) .filter((stage) => stage.state === 'stage-active' || stage.state === 'stage-done') .map((stage) => stage.stage), )], }; } async function waitForViewer(page, prefix, timeoutMs) { const root = prefix ? `#${prefix}-viewer-wrap` : '#viewer-frame-wrap'; const wireframe = prefix ? `#${prefix}-viewer-wireframe` : '#viewer-wireframe'; const shadows = prefix ? `#${prefix}-viewer-shadows` : '#viewer-shadows'; const reset = prefix ? `#${prefix}-viewer-reset-camera` : '#viewer-reset-camera'; const overlay = prefix ? `#${prefix}-viewer-overlay` : '#viewer-overlay'; const stats = prefix ? `#${prefix}-viewer-stats` : '#viewer-stats'; return page.waitForCondition( `() => { const root = document.querySelector(${JSON.stringify(root)}); const overlay = document.querySelector(${JSON.stringify(overlay)}); const wireframe = document.querySelector(${JSON.stringify(wireframe)}); const shadows = document.querySelector(${JSON.stringify(shadows)}); const reset = document.querySelector(${JSON.stringify(reset)}); const stats = document.querySelector(${JSON.stringify(stats)}); return root?.getAttribute("aria-busy") === "false" && overlay?.hidden && wireframe && !wireframe.disabled && shadows && !shadows.disabled && reset && !reset.disabled ? { stats: stats?.textContent?.trim() || "", wireframe: wireframe.getAttribute("aria-pressed"), shadows: shadows.getAttribute("aria-pressed") } : false; }`, `${prefix || 'result'} viewer readiness`, timeoutMs, ); } async function toggleViewerControl(page, buttonSelector, statusSelector, label) { const before = await page.evaluate( `document.querySelector(${JSON.stringify(buttonSelector)})?.getAttribute("aria-pressed")`, ); assert(before === 'true' || before === 'false', `${label} must expose aria-pressed`); const expected = before === 'true' ? 'false' : 'true'; const result = await page.clickAndWait( buttonSelector, `() => { const button = document.querySelector(${JSON.stringify(buttonSelector)}); const status = document.querySelector(${JSON.stringify(statusSelector)}); return button?.getAttribute("aria-pressed") === ${JSON.stringify(expected)} && !button.disabled && status?.textContent?.trim() ? {pressed: button.getAttribute("aria-pressed"), status: status.textContent.trim()} : false; }`, `${label} acknowledgement`, ); assert(!/failed|error|not ready|timed out/i.test(result.status), `${label} failed: ${result.status}`); return result; } async function resetViewer(page, buttonSelector, statusSelector, label) { const result = await page.clickAndWait( buttonSelector, `() => { const button = document.querySelector(${JSON.stringify(buttonSelector)}); const status = document.querySelector(${JSON.stringify(statusSelector)})?.textContent?.trim() || ""; return !button?.disabled && status === "View reset" ? {status} : false; }`, `${label} camera reset acknowledgement`, ); return result.status; } async function drag(page, point, button, buttons, deltaX, deltaY) { await page.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: point.x, y: point.y, button, buttons, clickCount: 1, pointerType: 'mouse', }); for (let step = 1; step <= 6; step += 1) { await page.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: point.x + deltaX * (step / 6), y: point.y + deltaY * (step / 6), button: 'none', buttons, pointerType: 'mouse', }); } await page.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: point.x + deltaX, y: point.y + deltaY, button, buttons: 0, clickCount: 1, pointerType: 'mouse', }); await page.settleFrames(); } async function exerciseViewerGestures(page, frameWrapSelector) { const hashes = []; const captureHash = async () => { const bytes = await page.clippedScreenshot(frameWrapSelector); const digest = sha256(bytes); hashes.push(digest); return digest; }; await captureHash(); let point = await page.elementCenter(frameWrapSelector); await drag(page, point, 'left', 1, Math.min(110, point.width * 0.2), Math.min(55, point.height * 0.15)); await captureHash(); point = await page.elementCenter(frameWrapSelector); await page.send('Input.dispatchMouseEvent', { type: 'mouseWheel', x: point.x, y: point.y, deltaX: 0, deltaY: -260, pointerType: 'mouse', }); await page.settleFrames(); await captureHash(); point = await page.elementCenter(frameWrapSelector); await drag(page, point, 'right', 2, Math.min(70, point.width * 0.14), -Math.min(45, point.height * 0.12)); await captureHash(); const changed = { orbit: hashes[1] !== hashes[0], zoom: hashes[2] !== hashes[1], pan: hashes[3] !== hashes[2], }; for (const [gesture, visible] of Object.entries(changed)) { assert(visible, `${gesture} input did not produce its own visible viewer change`); } return { ...changed, visibleTransitions: 3, hashes }; } function validateArtifactBytes(name, bytes) { assert(Buffer.isBuffer(bytes) && bytes.length > 0, `${name} is empty`); const text = [ 'factory.ts', 'spec.json', 'model.bundle.js', 'standalone.html', ].includes(name) ? bytes.toString('utf8') : null; if (name === 'reference.png' || name === 'screenshot.png') { assert(bytes.subarray(0, 8).equals(PNG_SIGNATURE), `${name} is not a PNG`); } else if (name === 'factory.ts') { assert( /THREE|three|create[A-Za-z0-9_]*Model/.test(text), 'factory.ts does not look like generated TypeScript', ); } else if (name === 'spec.json') { const parsed = JSON.parse(text); assert(parsed && typeof parsed === 'object', 'spec.json must contain a JSON object'); } else if (name === 'model.bundle.js') { assert(bytes.length > 100 && /THREE|createScene|createModel/.test(text), 'model.bundle.js looks invalid'); } else if (name === 'standalone.html') { assert(/ 0, `${label} download is empty`); const bytes = await readFile(filePath); validateArtifactBytes(artifactName, bytes); return { report: { label, artifactName, suggestedFilename: record.suggestedFilename, path: filePath, bytes: fileStat.size, sha256: sha256(bytes), }, bytes, }; } async function downloadResultArtifacts(page, monitor) { const actions = [ ['factory', 'factory.ts', '#dl-ts'], ['spec', 'spec.json', '#dl-spec'], ['bundle', 'model.bundle.js', '#dl-bundle'], ['standalone', 'standalone.html', '#dl-standalone'], ['screenshot', 'screenshot.png', '#dl-shot'], ]; const readiness = await page.evaluate(`(() => { const selectors = ${JSON.stringify(actions.map(([, , selector]) => selector))}; return selectors.map((selector) => { const element = document.querySelector(selector); return { selector, href: element?.href || "", disabled: Boolean(element?.disabled), ariaDisabled: element?.getAttribute("aria-disabled") || "" }; }); })()`); for (const action of readiness) { assert(action.selector === '#dl-shot' ? !action.disabled : Boolean(action.href), `${action.selector} is not ready`); assert(action.ariaDisabled !== 'true', `${action.selector} is marked disabled`); } const downloaded = []; const bytesByArtifact = {}; for (const [label, artifactName, selector] of actions) { const start = monitor.expect(label); await page.evaluate( `document.querySelector(${JSON.stringify(selector)}).click()`, { userGesture: true }, ); const record = await start; await record.done; const inspected = await inspectDownloadedFile(record, label, artifactName); downloaded.push(inspected.report); bytesByArtifact[artifactName] = inspected.bytes; } const shotStatus = await page.evaluate('document.querySelector("#shot-status")?.textContent?.trim()'); assert(shotStatus === 'Screenshot saved', `screenshot UI did not confirm success: ${shotStatus}`); return { report: downloaded, bytesByArtifact }; } async function fetchAndValidateGalleryArtifacts( item, appRoot, { expectedResultBytes = {}, immutableBaseline = {} } = {}, ) { const names = [ 'reference.png', 'factory.ts', 'spec.json', 'model.bundle.js', 'standalone.html', ]; const appOrigin = new URL(appRoot).origin; const bytesByArtifact = {}; const artifactReport = []; for (const name of names) { const supplied = item.artifacts?.[name]; assert(supplied, `gallery item is missing ${name}`); const url = new URL(supplied, appRoot); assert(url.origin === appOrigin, `${name} gallery URL left the deployed Space origin`); assert( url.pathname === `/api/gallery/${item.id}/artifacts/${encodeURIComponent(name)}`, `${name} does not use its immutable gallery artifact route`, ); const response = await fetch(url, { headers: { Accept: '*/*' }, signal: AbortSignal.timeout(60_000), }); assert(response.ok, `${name} gallery fetch failed with HTTP ${response.status}`); const bytes = Buffer.from(await response.arrayBuffer()); validateArtifactBytes(name, bytes); bytesByArtifact[name] = bytes; const expected = expectedResultBytes[name]; if (expected) { assert( bytes.equals(expected), `${name} gallery bytes differ from the result-screen download`, ); } const baseline = immutableBaseline[name]; if (baseline) { assert( bytes.equals(baseline), `${name} changed between gallery fetches`, ); } artifactReport.push({ name, url: url.href, contentType: response.headers.get('content-type') || '', bytes: bytes.length, sha256: sha256(bytes), matchesResultDownload: expected ? true : null, matchesImmutableBaseline: baseline ? true : null, }); } return { report: artifactReport, bytesByArtifact }; } async function auditGalleryDetail( page, galleryId, viewerTimeoutMs, { downloadMonitor = null, exerciseControls = true, exerciseScreenshot = false } = {}, ) { assert(GALLERY_ID_PATTERN.test(galleryId), `invalid gallery ID in route: ${galleryId}`); const item = await page.evaluate(`fetch( "/api/gallery/${galleryId}" ).then(async (response) => { const payload = await response.json(); if (!response.ok) throw new Error(payload.detail || "Gallery API failed"); return payload; })`, { awaitPromise: true }); assert(item.id === galleryId, 'gallery API returned a different item ID'); for (const name of ['reference.png', 'factory.ts', 'spec.json', 'model.bundle.js', 'standalone.html']) { assert(item.artifacts?.[name], `gallery item is missing ${name}`); } const detail = await page.waitForCondition( `() => { const dialog = document.querySelector("#gallery-dialog"); const content = document.querySelector("#gallery-detail-content"); const error = document.querySelector("#gallery-detail-error"); if (error && !error.hidden) throw new Error( document.querySelector("#gallery-detail-error-message")?.textContent || "Gallery detail failed" ); return dialog?.open && content && !content.hidden ? { title: document.querySelector("#gallery-detail-title")?.textContent?.trim() || "", meta: document.querySelector("#gallery-detail-meta")?.textContent?.trim() || "" } : false; }`, 'gallery detail content', viewerTimeoutMs, ); assert(detail.title, 'gallery detail title is empty'); const viewer = await waitForViewer(page, 'gallery', viewerTimeoutMs); assert(viewer.stats && !/unavailable/i.test(viewer.stats), `gallery viewer stats are invalid: ${viewer.stats}`); const links = await page.evaluate(`(() => { const ids = [ "gallery-dl-reference", "gallery-dl-ts", "gallery-dl-spec", "gallery-dl-bundle", "gallery-dl-standalone" ]; return ids.map((id) => { const element = document.getElementById(id); return {id, href: element?.href || "", ariaDisabled: element?.getAttribute("aria-disabled") || ""}; }); })()`); assert(links.every((link) => link.href && link.ariaDisabled !== 'true'), 'gallery artifact links are not all enabled'); let controls = { verifiedEnabled: true }; if (exerciseControls) { controls = { wireframe: await toggleViewerControl( page, '#gallery-viewer-wireframe', '#gallery-viewer-control-status', 'gallery wireframe', ), shadows: await toggleViewerControl( page, '#gallery-viewer-shadows', '#gallery-viewer-control-status', 'gallery shadows', ), reset: await resetViewer( page, '#gallery-viewer-reset-camera', '#gallery-viewer-control-status', 'gallery', ), }; } let screenshot = null; if (exerciseScreenshot) { assert(downloadMonitor, 'gallery screenshot exercise requires a download monitor'); const start = downloadMonitor.expect('gallery screenshot'); await page.evaluate('document.querySelector("#gallery-dl-shot").click()', { userGesture: true, }); const record = await start; await record.done; const inspected = await inspectDownloadedFile(record, 'gallery screenshot', 'screenshot.png'); const status = await page.evaluate( 'document.querySelector("#gallery-shot-status")?.textContent?.trim()', ); assert(status === 'Screenshot saved', `gallery screenshot UI did not confirm success: ${status}`); screenshot = inspected.report; } return { report: { id: galleryId, title: detail.title, meta: detail.meta, viewer, controls, screenshot, artifactLinks: links.length, }, item, }; } async function closeGalleryDialog(page) { return page.clickAndWait( '#gallery-detail-close', `() => { const dialog = document.querySelector("#gallery-dialog"); return !dialog?.open && location.pathname === "/gallery" ? {path: location.pathname} : false; }`, 'gallery dialog close and gallery route restoration', ); } async function waitForGalleryIndex(page, galleryId, timeoutMs = DEFAULT_ACTION_TIMEOUT_MS) { const expectedPath = `/gallery/${galleryId}`; return page.waitForCondition( `() => { const panel = document.querySelector("#panel-gallery"); const error = document.querySelector("#gallery-error"); if (error && !error.hidden) { throw new Error( document.querySelector("#gallery-error-message")?.textContent || "Gallery index failed" ); } const card = [...document.querySelectorAll("#gallery-grid .gallery-card-button")] .find((candidate) => candidate.getAttribute("href") === ${JSON.stringify(expectedPath)}); const loading = document.querySelector("#gallery-loading"); return panel && !panel.hidden && card && loading?.hidden ? { path: location.pathname, cardCount: document.querySelectorAll("#gallery-grid .gallery-card").length, status: document.querySelector("#gallery-status")?.textContent?.trim() || "", href: card.getAttribute("href") } : false; }`, `gallery index card ${galleryId}`, timeoutMs, ); } async function resetGeneratedResult(page) { await page.clickAndWait( '#nav-create', pageConditionVisible('#panel-result'), 'return to the generated result', ); const reset = await page.clickAndWait( '#again-btn', `() => { const upload = document.querySelector("#panel-upload"); if (!upload || upload.hidden) return false; const downloads = ["dl-ts", "dl-spec", "dl-bundle", "dl-standalone"] .map((id) => document.getElementById(id)); return { path: location.pathname, shareChecked: document.querySelector("#share-toggle")?.checked, previewHidden: document.querySelector("#preview-row")?.hidden, fileEmpty: !document.querySelector("#file-input")?.value, viewerSource: document.querySelector("#viewer-frame")?.getAttribute("src") || "", downloadsCleared: downloads.every((link) => !link?.getAttribute("href") && link?.getAttribute("aria-disabled") === "true" ), screenshotDisabled: document.querySelector("#dl-shot")?.disabled, warningsHidden: document.querySelector("#warnings-box")?.hidden }; }`, 'Create another model reset', ); assert(reset.path === '/', `reset should return to /, got ${reset.path}`); assert(reset.shareChecked === true, 'reset did not restore default-on sharing'); assert(reset.previewHidden === true && reset.fileEmpty === true, 'reset retained the prior upload'); assert(reset.viewerSource === 'about:blank', `reset retained viewer source ${reset.viewerSource}`); assert(reset.downloadsCleared && reset.screenshotDisabled, 'reset retained result downloads'); assert(reset.warningsHidden, 'reset retained prior validation warnings'); return reset; } async function auditHubParentEmbed({ parentPage, targetMonitor, hubUrl, appRoot, galleryId, viewerTimeoutMs, screenshotPath, diagnostics = [], }) { const appOrigin = new URL(appRoot).origin; const phase = (name, details = {}) => { const entry = { phase: name, at: new Date().toISOString(), ...details }; diagnostics.push(entry); console.error(`[audit][hub] ${name}${details.url ? `: ${details.url}` : ''}`); return entry; }; phase('parent-navigation-start', { url: hubUrl }); await parentPage.setDesktopViewport(); console.error(`[audit] Navigating to Hugging Face parent page ${hubUrl}`); await parentPage.navigate(hubUrl, 90_000); phase('parent-document-loaded', { url: hubUrl }); const iframeInfo = await parentPage.waitForCondition( `() => { const expectedOrigin = ${JSON.stringify(appOrigin)}; const iframe = [...document.querySelectorAll("iframe[src]")].find((candidate) => { try { const url = new URL(candidate.src, location.href); return url.origin === expectedOrigin; } catch { return false; } }); if (!iframe) return false; iframe.scrollIntoView({block: "center", inline: "center"}); const bounds = iframe.getBoundingClientRect(); return bounds.width > 100 && bounds.height > 100 ? { src: iframe.src, title: iframe.title || "", sandbox: iframe.getAttribute("sandbox") || "", allow: iframe.getAttribute("allow") || "", width: Math.round(bounds.width), height: Math.round(bounds.height) } : false; }`, 'the actual Hugging Face Space iframe', 90_000, ); phase('space-iframe-visible', { url: iframeInfo.src, width: iframeInfo.width, height: iframeInfo.height, sandbox: iframeInfo.sandbox, }); const appTarget = await targetMonitor.waitForTarget((record) => { if (record.targetInfo?.type !== 'iframe') return false; try { const url = new URL(record.targetInfo.url); return url.origin === appOrigin && url.pathname !== '/static/viewer.html'; } catch { return false; } }, 90_000); phase('root-oopif-acquired', { url: appTarget.targetInfo.url, targetId: appTarget.targetId, sessionId: appTarget.sessionId, }); const embeddedPage = new BrowserPage(parentPage.cdp, appTarget.sessionId); const embeddedInitial = await embeddedPage.waitForCondition( `() => { const toggle = document.querySelector("#share-toggle"); const upload = document.querySelector("#panel-upload"); const badge = document.querySelector("#llm-badge"); return toggle && upload && !upload.hidden && badge?.textContent?.trim() === "Model ready" ? { shareChecked: toggle.checked, title: document.title, path: location.pathname, badge: badge.textContent.trim() } : false; }`, 'the img2threejs app inside the Hugging Face iframe', 60_000, ); assert(embeddedInitial.shareChecked === true, 'embedded app lost its default-on share choice'); phase('embedded-root-ready', { path: embeddedInitial.path, sessionId: appTarget.sessionId, }); phase('embedded-gallery-navigation-start'); const embeddedGalleryRoute = await embeddedPage.clickAndWait( '#nav-gallery', `() => { const panel = document.querySelector("#panel-gallery"); return location.pathname === "/gallery" && panel && !panel.hidden ? {path: location.pathname} : false; }`, 'the embedded community gallery route', 60_000, ); const embeddedIndex = await waitForGalleryIndex( embeddedPage, galleryId, viewerTimeoutMs, ); phase('embedded-gallery-index-ready', { path: embeddedGalleryRoute.path, cards: embeddedIndex.cardCount, status: embeddedIndex.status, }); phase('embedded-gallery-card-open-start', { url: new URL(embeddedIndex.href, appRoot).href }); const deepRoute = await embeddedPage.clickAndWait( `#gallery-grid .gallery-card-button[href=${JSON.stringify(`/gallery/${galleryId}`)}]`, `() => { const dialog = document.querySelector("#gallery-dialog"); return location.pathname === ${JSON.stringify(`/gallery/${galleryId}`)} && dialog?.open ? {path: location.pathname, title: document.title} : false; }`, 'the embedded gallery card deep route', 60_000, ); phase('embedded-deep-route-ready', { path: deepRoute.path, sessionId: appTarget.sessionId, }); const embeddedGallery = await auditGalleryDetail( embeddedPage, galleryId, viewerTimeoutMs, { exerciseControls: true, exerciseScreenshot: false }, ); phase('nested-gallery-viewer-ready', { sessionId: appTarget.sessionId, stats: embeddedGallery.report.viewer.stats, }); await parentPage.settleFrames(); const screenshot = await parentPage.screenshot(screenshotPath); phase('parent-embed-evidence-captured', { path: screenshot.path }); const attachedTargets = targetMonitor.attachedTargets(); const appFrames = attachedTargets.filter((target) => { try { return new URL(target.url).origin === appOrigin; } catch { return false; } }); assert(appFrames.length >= 1, 'the Hugging Face parent did not expose an attached Space iframe target'); return { hubUrl, parentIframe: iframeInfo, embeddedInitial, embeddedIndex, embeddedGallery: embeddedGallery.report, nestedViewerReady: true, attachedSpaceTargets: appFrames, screenshot, phases: diagnostics, }; } async function main() { let options; try { options = parseArgs(process.argv.slice(2)); } catch (error) { console.error(error.message); console.error(usage()); process.exitCode = 2; return; } if (options.help) { console.log(usage()); return; } const appRoot = directAppRoot(options.url); const hubUrl = await resolveHubParentUrl(options.hubUrl, appRoot); const imagePath = options.image ? resolve(options.image) : null; if (imagePath) { const imageStat = await stat(imagePath); assert(imageStat.isFile() && imageStat.size > 0, `image does not exist or is empty: ${imagePath}`); } const chromiumBinary = await findChromium(options.chromium); const outputRoot = resolve(options.outputDir); await mkdir(outputRoot, { recursive: true }); const runStamp = new Date().toISOString().replace(/[:.]/g, '-'); const runDirectory = join(outputRoot, `audit-${runStamp}`); const downloadDirectory = join(runDirectory, 'downloads'); await mkdir(downloadDirectory, { recursive: true }); const userDataDirectory = await mkdtemp(join(tmpdir(), 'img2threejs-cdp-')); let child = null; let cdp = null; let page = null; let targetId = null; let downloadMonitor = null; let targetMonitor = null; let removeProgressReporter = null; const pageIssues = []; const report = { url: appRoot, hubUrl, mode: options.skipSubmit ? 'existing-gallery' : 'submit-and-share', image: imagePath ? basename(imagePath) : null, jobId: null, galleryId: options.galleryId || null, runDirectory, screenshots: [], checks: {}, }; let resultBytesByArtifact = {}; let galleryBaselineBytes = {}; try { console.error(`[audit] Launching ${chromiumBinary}`); const launched = await launchChromium(chromiumBinary, userDataDirectory); child = launched.child; cdp = launched.cdp; const created = await cdp.send('Target.createTarget', { url: 'about:blank' }); targetId = created.targetId; const attached = await cdp.send('Target.attachToTarget', { targetId, flatten: true, }); page = new BrowserPage(cdp, attached.sessionId); await Promise.all([ page.send('Page.enable'), page.send('Runtime.enable'), page.send('DOM.enable'), page.send('Network.enable'), page.send('Log.enable'), ]); targetMonitor = new TargetMonitor(cdp, page.sessionId, targetId, pageIssues); await targetMonitor.start(); await cdp.send('Browser.setDownloadBehavior', { behavior: 'allowAndName', downloadPath: downloadDirectory, eventsEnabled: true, }); downloadMonitor = new DownloadMonitor(cdp, downloadDirectory); await page.setDesktopViewport(); console.error(`[audit] Navigating to ${appRoot}`); await page.navigate(appRoot, 60_000); await page.waitForCondition( `() => { const toggle = document.querySelector("#share-toggle"); const badge = document.querySelector("#llm-badge"); return toggle && badge && !/checking/i.test(badge.textContent) ? {checked: toggle.checked, badge: badge.textContent.trim()} : false; }`, 'application initialization', 45_000, ); const initial = await page.evaluate(`(() => ({ checked: document.querySelector("#share-toggle")?.checked, badge: document.querySelector("#llm-badge")?.textContent?.trim() || "", disclosure: document.querySelector(".share-control")?.textContent?.trim().replace(/\\s+/g, " ") || "" }))()`); assert(initial.checked === true, 'the public-gallery share toggle must be checked by default'); assert(/on by default/i.test(initial.disclosure), 'the default-sharing disclosure must be visible'); if (!options.skipSubmit) { assert(initial.badge === 'Model ready', `generation model is not ready: ${initial.badge}`); } report.checks.initial = initial; report.checks.externalLinks = await auditExternalLinks(page); report.screenshots.push( await page.screenshot(join(runDirectory, 'initial-desktop.png'), '#panel-upload'), ); await page.setMobileViewport(); report.screenshots.push( await page.screenshot(join(runDirectory, 'upload-mobile.png'), '#panel-upload'), ); await page.setDesktopViewport(); if (!options.skipSubmit) { let streamedProgressKey = ''; removeProgressReporter = await installAuditRecorder(page, (update) => { if (update.type === 'job-accepted' && update.jobId) { report.jobId = update.jobId; console.error(`[audit] Job accepted: ${update.jobId}`); return; } if (update.type === 'progress') { const progressKey = [ update.stage, update.activity, update.attempt, update.lastLogLine, ].join('\u0000'); if (progressKey === streamedProgressKey) return; streamedProgressKey = progressKey; const attempt = update.attempt && update.attempt !== '—' ? ` · attempt ${update.attempt}` : ''; console.error( `[progress ${update.elapsed || '--:--'}] ${update.stage}: ${update.activity}${attempt}`, ); } }); await selectFile(page, imagePath); assert( await page.evaluate('document.querySelector("#share-toggle")?.checked === true'), 'the share toggle changed before submission', ); if (options.hint) { await page.evaluate(`(() => { const input = document.querySelector("#hint"); input.value = ${JSON.stringify(options.hint)}; input.dispatchEvent(new Event("input", {bubbles: true})); })()`); } console.error('[audit] Submitting image; waiting on DOM/SSE events (no polling)'); const terminal = await page.clickAndWait( '#run-btn', `() => { const result = document.querySelector("#panel-result"); const error = document.querySelector("#panel-error"); if (result && !result.hidden) return {kind: "result"}; if (error && !error.hidden) { return { kind: "error", title: document.querySelector("#error-title")?.textContent?.trim() || "", message: document.querySelector("#error-message")?.textContent?.trim() || "", detail: document.querySelector("#error-detail")?.textContent?.trim() || "" }; } return false; }`, 'generation result or honest error', options.timeoutMs, ); report.jobId ||= await page.evaluate('window.__i2tAuditJobResponse?.job_id || null'); assert(terminal.kind === 'result', `${terminal.title}: ${terminal.message}\n${terminal.detail}`); assert(report.jobId && GALLERY_ID_PATTERN.test(report.jobId), `invalid or missing job ID: ${report.jobId}`); report.checks.progress = await inspectProgress(page); const sharing = await page.evaluate(`(() => ({ status: document.querySelector("#result-share-status")?.textContent?.trim() || "", galleryButtonVisible: !document.querySelector("#gallery-result-link")?.hidden, title: document.querySelector("#result-title")?.textContent?.trim() || "", subtitle: document.querySelector("#result-sub")?.textContent?.trim() || "" }))()`); assert(sharing.status === 'Shared publicly', `result was not shared: ${sharing.status}`); assert(sharing.galleryButtonVisible, 'shared result does not expose its gallery-detail action'); report.checks.result = sharing; const resultViewer = await waitForViewer(page, '', options.viewerTimeoutMs); assert(resultViewer.stats && !/unavailable/i.test(resultViewer.stats), 'result viewer did not report valid stats'); report.checks.resultViewer = { ready: resultViewer, wireframe: await toggleViewerControl( page, '#viewer-wireframe', '#viewer-control-status', 'result wireframe', ), shadows: await toggleViewerControl( page, '#viewer-shadows', '#viewer-control-status', 'result shadows', ), }; report.checks.resultViewer.gestures = await exerciseViewerGestures(page, '#viewer-frame-wrap'); report.checks.resultViewer.reset = await resetViewer( page, '#viewer-reset-camera', '#viewer-control-status', 'result', ); report.screenshots.push( await page.screenshot(join(runDirectory, 'result-desktop.png'), '#panel-result'), ); await page.setMobileViewport(); report.screenshots.push( await page.screenshot(join(runDirectory, 'result-mobile.png'), '#panel-result'), ); await page.setDesktopViewport(); console.error('[audit] Exercising all result downloads'); const resultDownloads = await downloadResultArtifacts(page, downloadMonitor); report.checks.downloads = resultDownloads.report; resultBytesByArtifact = resultDownloads.bytesByArtifact; console.error('[audit] Opening the newly shared gallery item'); const opened = await page.clickAndWait( '#gallery-result-link', `() => { const match = location.pathname.match(/^\\/gallery\\/([a-f0-9]{32})$/i); return match ? {id: match[1]} : false; }`, 'shared gallery route', ); report.galleryId = opened.id; } else { console.error(`[audit] Opening existing gallery item ${report.galleryId}`); await page.navigate(new URL(`/gallery/${report.galleryId}`, appRoot).href, 60_000); } const galleryAudit = await auditGalleryDetail( page, report.galleryId, options.viewerTimeoutMs, { downloadMonitor, exerciseControls: true, exerciseScreenshot: true, }, ); report.checks.gallery = galleryAudit.report; const galleryArtifacts = await fetchAndValidateGalleryArtifacts( galleryAudit.item, appRoot, { expectedResultBytes: resultBytesByArtifact }, ); galleryBaselineBytes = galleryArtifacts.bytesByArtifact; report.checks.galleryArtifacts = galleryArtifacts.report; report.checks.externalLinksAfterGallery = await auditExternalLinks(page); report.screenshots.push( await page.screenshot(join(runDirectory, 'gallery-desktop.png'), '#gallery-dialog'), ); await page.setMobileViewport(); report.screenshots.push( await page.screenshot(join(runDirectory, 'gallery-mobile.png'), '#gallery-dialog'), ); await page.setDesktopViewport(); report.checks.galleryDialogClose = await closeGalleryDialog(page); report.checks.galleryIndex = await waitForGalleryIndex( page, report.galleryId, options.viewerTimeoutMs, ); report.screenshots.push( await page.screenshot(join(runDirectory, 'gallery-index-desktop.png'), '#panel-gallery'), ); await page.setMobileViewport(); report.screenshots.push( await page.screenshot(join(runDirectory, 'gallery-index-mobile.png'), '#panel-gallery'), ); await page.setDesktopViewport(); if (!options.skipSubmit) { report.checks.createAnotherReset = await resetGeneratedResult(page); } const coldGalleryUrl = new URL(`/gallery/${report.galleryId}`, appRoot).href; console.error(`[audit] Cold-loading shared deep link ${coldGalleryUrl}`); await page.navigate(coldGalleryUrl, 60_000); const coldGalleryAudit = await auditGalleryDetail( page, report.galleryId, options.viewerTimeoutMs, { exerciseControls: false, exerciseScreenshot: false }, ); report.checks.coldGalleryDeepLink = { url: coldGalleryUrl, path: await page.evaluate('location.pathname'), ...coldGalleryAudit.report, }; assert( report.checks.coldGalleryDeepLink.path === `/gallery/${report.galleryId}`, 'cold gallery navigation did not preserve the deep-link path', ); const coldGalleryArtifacts = await fetchAndValidateGalleryArtifacts( coldGalleryAudit.item, appRoot, { expectedResultBytes: resultBytesByArtifact, immutableBaseline: galleryBaselineBytes, }, ); report.checks.coldGalleryArtifacts = coldGalleryArtifacts.report; report.screenshots.push( await page.screenshot(join(runDirectory, 'gallery-cold-deeplink-desktop.png'), '#gallery-dialog'), ); const hubEmbedPhases = []; report.checks.huggingFaceParentEmbedPhases = hubEmbedPhases; const hubEmbed = await auditHubParentEmbed({ parentPage: page, targetMonitor, hubUrl, appRoot, galleryId: report.galleryId, viewerTimeoutMs: options.viewerTimeoutMs, screenshotPath: join(runDirectory, 'huggingface-parent-embed-desktop.png'), diagnostics: hubEmbedPhases, }); report.checks.huggingFaceParentEmbed = hubEmbed; report.screenshots.push(hubEmbed.screenshot); report.checks.autoAttachedTargets = targetMonitor.attachedTargets(); const uniqueIssues = [...new Set(pageIssues)]; assert(uniqueIssues.length === 0, `page/console errors:\n${uniqueIssues.join('\n')}`); report.checks.pageErrors = 0; report.ok = true; await writeFile( join(runDirectory, 'report.json'), `${JSON.stringify(report, null, 2)}\n`, 'utf8', ); console.log(JSON.stringify(report, null, 2)); console.error( `[audit] PASS job=${report.jobId || 'skipped'} gallery=${report.galleryId} artifacts=${runDirectory}`, ); } catch (error) { report.ok = false; report.error = error.stack || error.message || String(error); report.pageIssues = [...new Set(pageIssues)]; if (page) { try { report.screenshots.push( await page.screenshot(join(runDirectory, 'failure.png')), ); } catch { // Preserve the original failure. } } try { await writeFile( join(runDirectory, 'report.json'), `${JSON.stringify(report, null, 2)}\n`, 'utf8', ); } catch { // Preserve the original failure. } console.error(`[audit] FAIL\n${report.error}`); if (report.pageIssues.length) { console.error(`[audit] Page issues:\n${report.pageIssues.join('\n')}`); } console.error(`[audit] Failure artifacts: ${runDirectory}`); process.exitCode = 1; } finally { removeProgressReporter?.(); downloadMonitor?.close(); targetMonitor?.close(); if (cdp && targetId) { try { await cdp.send('Target.closeTarget', { targetId }, undefined, 5000); } catch { // Chromium shutdown below is authoritative. } } cdp?.close(); await stopChromium(child); await rm(userDataDirectory, { recursive: true, force: true }); } } await main();