img2threejs / scripts /live_browser_audit.mjs
Mike0021's picture
Wait for embedded app readiness in browser audit
b456131 verified
Raw
History Blame Contribute Delete
75.3 kB
#!/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(/<!doctype html/i.test(text) && /<script/i.test(text), 'standalone.html looks invalid');
} else {
throw new Error(`No artifact validator exists for ${name}.`);
}
}
async function inspectDownloadedFile(record, label, artifactName) {
const filePath = record.filePath;
const fileStat = await stat(filePath);
assert(fileStat.isFile() && fileStat.size > 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();