feat(scanner): complete localized artifact quality checkpoint

This commit is contained in:
AzuTear
2026-07-11 15:59:19 +02:00
parent 639b0b7f59
commit 8b9f948c6b
215 changed files with 35440 additions and 7273 deletions
+941
View File
@@ -0,0 +1,941 @@
import fs from "node:fs";
import path from "node:path";
const args = new Map(process.argv.slice(2).map((entry) => {
const [key, ...value] = entry.replace(/^--/, "").split("=");
return [key, value.join("=") || "true"];
}));
const port = Number(args.get("port") ?? 9223);
const mode = args.get("mode") ?? "inspect";
const view = args.get("view") ?? "scan";
const state = args.get("state") ?? "default";
const closeAfter = args.get("close") === "true";
const consoleCheck = args.get("console-check") === "true";
const viewportWidth = args.has("viewport-width") ? Number(args.get("viewport-width")) : null;
const viewportHeight = args.has("viewport-height") ? Number(args.get("viewport-height")) : null;
const reprocessRunDir = args.has("run-dir") ? path.resolve(args.get("run-dir")) : "";
const reprocessTarget = args.has("expected-target") ? Number(args.get("expected-target")) : null;
const reprocessMaxReviewRate = Number(args.get("max-review-rate") ?? 0.15);
const viewIds = new Set(["scan", "inventory", "triage", "builds", "overlay", "diagnose"]);
const output = path.resolve(args.get("output") ?? `outputs/packaged-live/packaged-${mode}-${view}.json`);
const screenshotPath = path.resolve(args.get("screenshot") ?? `outputs/packaged-live/packaged-${mode}-${view}.png`);
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid CDP port: ${port}`);
if ((viewportWidth === null) !== (viewportHeight === null)) throw new Error("Both --viewport-width and --viewport-height are required together.");
if (viewportWidth !== null && (!Number.isInteger(viewportWidth) || !Number.isInteger(viewportHeight) || viewportWidth < 480 || viewportHeight < 320)) {
throw new Error("Viewport dimensions must be integer CSS pixels of at least 480x320.");
}
if (!new Set(["inspect", "scan5", "ui-scan5", "ui-scan-row1", "ui-stream20", "ui-full-inventory", "reprocess-run"]).has(mode)) throw new Error(`Unsupported mode: ${mode}`);
if (!viewIds.has(view)) throw new Error(`Unsupported view: ${view}`);
if (mode === "reprocess-run" && (!reprocessRunDir || !Number.isInteger(reprocessTarget) || reprocessTarget < 1 || reprocessTarget > 2400)) {
throw new Error("reprocess-run requires --run-dir and --expected-target in 1..2400.");
}
if (mode === "reprocess-run" && (!Number.isFinite(reprocessMaxReviewRate) || reprocessMaxReviewRate < 0 || reprocessMaxReviewRate > 1)) {
throw new Error("--max-review-rate must be in 0..1.");
}
if (!new Set(["default", "scan-settings", "artifact-open", "artifact-probe", "post-scan-results", "review-deeplink", "keyboard-focus", "overlay-open", "inventory-confirm", "inventory-delete-confirm", "inventory-store-delete-confirm", "inventory-roving", "inventory-technical"]).has(state)) throw new Error(`Unsupported state: ${state}`);
if (state === "scan-settings" && view !== "scan") throw new Error("scan-settings state requires --view=scan.");
if (state === "artifact-open" && view !== "scan") throw new Error("artifact-open state requires --view=scan.");
if (state === "artifact-probe" && view !== "scan") throw new Error("artifact-probe state requires --view=scan.");
if (state === "post-scan-results" && view !== "scan") throw new Error("post-scan-results state requires --view=scan.");
if (state === "review-deeplink" && view !== "triage") throw new Error("review-deeplink state requires --view=triage.");
if (state === "overlay-open" && view !== "overlay") throw new Error("overlay-open state requires --view=overlay.");
if (state === "inventory-confirm" && view !== "inventory") throw new Error("inventory-confirm state requires --view=inventory.");
if (state === "inventory-delete-confirm" && view !== "inventory") throw new Error("inventory-delete-confirm state requires --view=inventory.");
if (state === "inventory-store-delete-confirm" && view !== "inventory") throw new Error("inventory-store-delete-confirm state requires --view=inventory.");
if (state === "inventory-roving" && view !== "inventory") throw new Error("inventory-roving state requires --view=inventory.");
if (state === "inventory-technical" && view !== "inventory") throw new Error("inventory-technical state requires --view=inventory.");
const uiScanConfig = mode === "ui-full-inventory"
? {
scopeMode: "all",
expectedTarget: null,
deadlineMs: 900000,
}
: mode === "ui-scan-row1"
? {
scopeMode: "limit",
limit: 1,
expectedTarget: 8,
unit: "rows",
deadlineMs: 240000,
}
: mode === "ui-stream20"
? {
scopeMode: "limit",
limit: 20,
expectedTarget: 20,
unit: "artifacts",
deadlineMs: 240000,
}
: {
scopeMode: "limit",
limit: 5,
expectedTarget: 5,
unit: "artifacts",
deadlineMs: 240000,
};
const targets = await fetch(`http://127.0.0.1:${port}/json/list`).then((response) => response.json());
const target = targets.find((candidate) => (
candidate.type === "page"
&& String(candidate.title).includes("Genshin Artifact Assistant")
&& !String(candidate.url).includes("overlay=1")
));
if (!target?.webSocketDebuggerUrl || !String(target.url).startsWith("file:")) {
throw new Error("No packaged Genshin Artifact Assistant file:// renderer target found.");
}
const socket = new WebSocket(target.webSocketDebuggerUrl);
await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("Timed out connecting to packaged Electron CDP.")), 10_000);
socket.addEventListener("open", () => {
clearTimeout(timer);
resolve();
}, { once: true });
socket.addEventListener("error", () => {
clearTimeout(timer);
reject(new Error("Packaged Electron CDP connection failed."));
}, { once: true });
});
let nextId = 0;
const pending = new Map();
const rendererDiagnostics = {
exceptions: [],
consoleErrors: [],
logErrors: [],
};
socket.addEventListener("message", (event) => {
let message;
try {
message = JSON.parse(String(event.data));
} catch {
return;
}
if (message.method === "Runtime.exceptionThrown") {
rendererDiagnostics.exceptions.push(message.params?.exceptionDetails?.exception?.description ?? message.params?.exceptionDetails?.text ?? "Unknown renderer exception");
} else if (message.method === "Runtime.consoleAPICalled" && message.params?.type === "error") {
rendererDiagnostics.consoleErrors.push((message.params.args ?? []).map((entry) => entry.value ?? entry.description ?? entry.type).join(" "));
} else if (message.method === "Log.entryAdded" && message.params?.entry?.level === "error") {
rendererDiagnostics.logErrors.push(message.params.entry.text ?? "Unknown renderer log error");
}
if (!message.id || !pending.has(message.id)) return;
const request = pending.get(message.id);
pending.delete(message.id);
clearTimeout(request.timer);
if (message.error) request.reject(new Error(JSON.stringify(message.error)));
else request.resolve(message.result);
});
function call(method, params = {}, timeoutMs = 30_000) {
const id = ++nextId;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pending.delete(id);
reject(new Error(`CDP ${method} timed out after ${timeoutMs} ms.`));
}, timeoutMs);
pending.set(id, { resolve, reject, timer });
socket.send(JSON.stringify({ id, method, params }));
});
}
async function evaluate(expression, timeoutMs = 30_000) {
const response = await call("Runtime.evaluate", {
expression,
awaitPromise: true,
returnByValue: true,
}, timeoutMs);
if (response.exceptionDetails) {
throw new Error(response.exceptionDetails.exception?.description ?? response.exceptionDetails.text ?? "Renderer evaluation failed.");
}
return response.result?.value;
}
await call("Page.bringToFront", {}, 10_000);
if (consoleCheck) {
await call("Runtime.enable", {}, 10_000);
await call("Log.enable", {}, 10_000);
}
if (viewportWidth !== null && viewportHeight !== null) {
await call("Emulation.setDeviceMetricsOverride", {
width: viewportWidth,
height: viewportHeight,
deviceScaleFactor: 1,
mobile: false,
}, 10_000);
}
await evaluate("new Promise((resolve) => setTimeout(resolve, 120))", 10_000);
const inspectExpression = `
(async () => {
const api = window.assistantApi;
if (!api) throw new Error("Packaged preload bridge is unavailable.");
const runtime = await api.getRuntimeInfo();
const guard = await api.getAutomationGuard();
const data = await api.nativeScannerDataStatus();
const results = await api.nativeScannerLoadResults({ limit: 3 });
const artifacts = await api.loadArtifacts();
const sources = await api.listCaptureSources();
const genshinSource = sources.find((source) => source.isGenshinCandidate) ?? null;
const readinessCapture = genshinSource
? await api.captureSource(genshinSource.id, 0, false, {
skipOcr: true,
omitFullFrame: true,
omitDetailPreview: true,
omitInventoryPreview: true,
omitCrops: true,
omitLockState: true,
})
: null;
return {
title: document.title,
url: location.href,
bridgeKeys: Object.keys(api).sort(),
runtime,
guard,
data,
latestResults: { ok: results.ok, runDir: results.runDir, total: results.total, loaded: results.results.length },
artifactStore: { ok: artifacts.ok, total: artifacts.total },
captureReadiness: readinessCapture ? {
source: { id: genshinSource.id, name: genshinSource.name },
captureTarget: readinessCapture.captureTarget,
layout: readinessCapture.layout,
artifactDetail: readinessCapture.artifactDetail,
inventoryGrid: readinessCapture.inventoryGrid ? {
rows: readinessCapture.inventoryGrid.rows,
cols: readinessCapture.inventoryGrid.cols,
confidence: readinessCapture.inventoryGrid.confidence,
source: readinessCapture.inventoryGrid.source,
} : null,
} : { source: null },
navigation: [...document.querySelectorAll("button,a")].map((element) => element.textContent.trim()).filter(Boolean).slice(0, 100),
bodyText: document.body.innerText.slice(0, 16000),
};
})()`;
const scanExpression = `
(async () => {
const api = window.assistantApi;
if (!api) throw new Error("Packaged preload bridge is unavailable.");
const preflight = await api.nativeScannerPreflight({ category: "artifacts" });
if (!preflight.ready) return { ok: false, phase: "preflight", preflight };
let status = await api.nativeScannerStart({ limit: 5, category: "artifacts" });
const deadline = Date.now() + 120000;
while (status.running && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 250));
status = await api.nativeScannerStatus();
}
if (status.running) {
status = await api.nativeScannerStop();
return { ok: false, phase: "timeout", status };
}
if (status.status !== "done" || status.captured !== 5 || !status.initialTopResetCompleted) {
return { ok: false, phase: "capture", status };
}
const processing = await api.nativeScannerProcessRun({ runDir: status.runDir, limit: 5, persist: false });
const results = await api.nativeScannerLoadResults({ runDir: status.runDir, limit: 5 });
return {
ok: Boolean(
processing.ok && processing.processed === 5 && processing.parsed === 5
&& processing.errors === 0 && processing.stored === 0 && !processing.persisted
&& results.ok && results.total === 5 && results.results.length === 5
&& results.results.every((result) => !result.persistedArtifact && !result.artifactRecordId)
),
phase: "complete",
preflight,
status,
processing: {
ok: processing.ok,
processed: processing.processed,
parsed: processing.parsed,
review: processing.review,
errors: processing.errors,
stored: processing.stored,
persisted: processing.persisted,
elapsedMs: processing.elapsedMs,
queueConcurrency: processing.queueConcurrency,
},
results: { ok: results.ok, runDir: results.runDir, total: results.total, loaded: results.results.length },
};
})()`;
const uiScanExpression = `
(async () => {
const config = ${JSON.stringify(uiScanConfig)};
const api = window.assistantApi;
if (!api) throw new Error("Packaged preload bridge is unavailable.");
const priorSummary = document.querySelector('.scan-summary-modal');
const priorSummaryClose = priorSummary?.querySelector('[data-scan-action="summary-close"]');
priorSummaryClose?.click();
if (priorSummaryClose) await new Promise((resolve) => setTimeout(resolve, 250));
const existingStatus = await api.nativeScannerStatus();
if (existingStatus.running || document.querySelector('.scan-progress-surface.is-busy')) {
return { ok: false, phase: 'precondition', error: 'A scanner run is already active.', existingStatus };
}
let dialog = document.querySelector('.scanner-settings-modal');
if (!dialog) {
const settingsButton = document.querySelector('[data-scan-action="open-settings"]');
if (!(settingsButton instanceof HTMLButtonElement)) return { ok: false, phase: 'settings', error: 'Scan settings action was not found.' };
settingsButton.click();
await new Promise((resolve) => setTimeout(resolve, 250));
dialog = document.querySelector('.scanner-settings-modal');
}
let configuredLimit = null;
let configuredScopeValid = false;
if (config.scopeMode === 'all') {
const fullScope = dialog?.querySelector('input[data-scan-scope="all"]');
if (!(fullScope instanceof HTMLInputElement)) return { ok: false, phase: 'settings', error: 'Full inventory scope option was not found.' };
if (!fullScope.checked) fullScope.click();
await new Promise((resolve) => setTimeout(resolve, 250));
configuredScopeValid = fullScope.checked;
} else {
const limitedScope = dialog?.querySelector('input[data-scan-scope="limit"]');
if (!(limitedScope instanceof HTMLInputElement)) return { ok: false, phase: 'settings', error: 'Limited scope option was not found.' };
if (!limitedScope.checked) limitedScope.click();
await new Promise((resolve) => setTimeout(resolve, 150));
const unitButton = dialog?.querySelector('[data-scan-limit-unit="' + config.unit + '"]');
if (!(unitButton instanceof HTMLButtonElement)) return { ok: false, phase: 'settings', error: 'Limit unit action was not found.', unit: config.unit };
unitButton.click();
await new Promise((resolve) => setTimeout(resolve, 150));
const inputSelector = 'input[data-scan-limit-input]';
const limitInput = document.querySelector(inputSelector);
if (!(limitInput instanceof HTMLInputElement)) return { ok: false, phase: 'settings', error: 'Limit input was not found.' };
for (let attempt = 0; attempt < 30 && Number(document.querySelector(inputSelector)?.value) !== config.limit; attempt += 1) {
const currentInput = document.querySelector(inputSelector);
const current = Number(currentInput?.value);
const action = current > config.limit ? 'decrease-limit' : 'increase-limit';
const stepper = dialog?.querySelector('[data-scan-action="' + action + '"]');
if (!(stepper instanceof HTMLButtonElement)) break;
stepper.click();
await new Promise((resolve) => setTimeout(resolve, 35));
}
await new Promise((resolve) => setTimeout(resolve, 250));
configuredLimit = Number(document.querySelector(inputSelector)?.value);
if (configuredLimit !== config.limit) return { ok: false, phase: 'settings', error: 'Scan limit could not be configured.', configuredLimit, config };
configuredScopeValid = limitedScope.checked
&& unitButton.getAttribute('aria-pressed') === 'true';
}
const configuredText = document.querySelector('[role="dialog"]')?.textContent ?? '';
if (!configuredScopeValid) return { ok: false, phase: 'settings', error: 'Scan scope was not configured.', configuredLimit, configuredText, config };
document.querySelector('[data-scan-action="close-settings"]')?.click();
await new Promise((resolve) => setTimeout(resolve, 200));
const startButton = document.querySelector('[data-scan-action="start-auto"]');
if (!(startButton instanceof HTMLButtonElement) || startButton.disabled) return { ok: false, phase: 'start', error: 'Auto-Scan action is unavailable.', configuredText };
startButton.click();
const deadline = Date.now() + config.deadlineMs;
let sawBusy = false;
let sawProcessing = false;
let sawCaptureRunning = false;
let sawLiveResultDuringCapture = false;
let sawEvaluationAdvanceDuringCapture = false;
let sawResultSlideIn = false;
const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
let maxResultRowsDuringCapture = 0;
let maxEvaluatedDuringCapture = 0;
const resultObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (!(node instanceof HTMLElement)) continue;
const resultNode = node.matches('.result-rail-row') ? node : node.querySelector('.result-rail-row');
if (!(resultNode instanceof HTMLElement)) continue;
requestAnimationFrame(() => {
if (getComputedStyle(resultNode).animationName.includes('scanner-result-slide-in')) {
sawResultSlideIn = true;
}
});
}
}
});
resultObserver.observe(document.body, { childList: true, subtree: true });
let lastProgressText = '';
while (Date.now() < deadline) {
const progress = document.querySelector('.scan-progress-surface');
const evaluation = document.querySelector('.scan-evaluation-progress');
const progressText = [progress?.textContent?.trim(), evaluation?.textContent?.trim()].filter(Boolean).join(' ');
if (progress?.classList.contains('is-busy')) sawBusy = true;
const liveCaptureStatus = await api.nativeScannerStatus();
const liveProcessingStatus = await api.nativeScannerProcessingStatus();
if (evaluation && (liveProcessingStatus.running || liveProcessingStatus.processed > 0)) sawProcessing = true;
if (progressText) lastProgressText = progressText;
const liveResultRows = document.querySelectorAll('.result-rail-row').length;
if (liveCaptureStatus.running) {
sawCaptureRunning = true;
maxResultRowsDuringCapture = Math.max(maxResultRowsDuringCapture, liveResultRows);
maxEvaluatedDuringCapture = Math.max(maxEvaluatedDuringCapture, liveProcessingStatus.processed);
if (liveResultRows > 0) sawLiveResultDuringCapture = true;
if (liveProcessingStatus.processed > 0) sawEvaluationAdvanceDuringCapture = true;
}
const summary = document.querySelector('.scan-summary-modal');
if (sawBusy && summary && !progress?.classList.contains('is-busy')) break;
await new Promise((resolve) => setTimeout(resolve, 250));
}
resultObserver.disconnect();
const status = await api.nativeScannerStatus();
const processing = await api.nativeScannerProcessingStatus();
const resolvedTarget = config.expectedTarget ?? status.target;
const targetValid = Number.isInteger(resolvedTarget) && resolvedTarget > 0 && resolvedTarget <= 2400;
const results = await api.nativeScannerLoadResults({ runDir: status.runDir, limit: targetValid ? resolvedTarget : 2400 });
const loadedEntries = Array.isArray(results.results) ? results.results : [];
const summaryText = document.querySelector('.scan-summary-modal')?.textContent?.trim() ?? '';
const resultRows = document.querySelectorAll('.result-rail-row').length;
const expectedPages = targetValid ? Math.ceil(resolvedTarget / 32) : 0;
const reviewRate = targetValid ? processing.review / resolvedTarget : 1;
const persistenceDisabled = processing.stored === 0
&& loadedEntries.every((result) => !result.persistedArtifact && !result.artifactRecordId);
const documentFits = document.documentElement.scrollWidth === document.documentElement.clientWidth
&& document.documentElement.scrollHeight === document.documentElement.clientHeight;
return {
ok: Boolean(
sawBusy && sawProcessing && sawCaptureRunning
&& sawLiveResultDuringCapture && sawEvaluationAdvanceDuringCapture && (sawResultSlideIn || reducedMotion)
&& configuredScopeValid
&& targetValid && status.status === 'done' && status.target === resolvedTarget && status.captured === resolvedTarget
&& status.pages === expectedPages && status.initialTopResetCompleted
&& !processing.running && processing.total === resolvedTarget && processing.processed === resolvedTarget
&& processing.parsed === resolvedTarget && processing.errors === 0 && reviewRate <= 0.15
&& results.ok && results.total === resolvedTarget && loadedEntries.length === resolvedTarget
&& resultRows === resolvedTarget && persistenceDisabled && documentFits
),
phase: 'complete',
configuredText,
configuredLimit,
configuredScopeValid,
config,
resolvedTarget,
expectedPages,
reviewRate,
persistenceDisabled,
documentFits,
sawBusy,
sawProcessing,
sawCaptureRunning,
sawLiveResultDuringCapture,
sawEvaluationAdvanceDuringCapture,
sawResultSlideIn,
reducedMotion,
maxResultRowsDuringCapture,
maxEvaluatedDuringCapture,
lastProgressText,
summaryText,
resultRows,
status,
processing,
results: { ok: results.ok, runDir: results.runDir, total: results.total, loaded: loadedEntries.length },
};
})()`;
const report = {
version: "packaged-live-acceptance-v1",
createdAt: new Date().toISOString(),
mode,
view,
state,
target: { title: target.title, url: target.url },
inspection: await evaluate(inspectExpression, 60_000),
};
if (mode === "scan5") {
report.scan = await evaluate(scanExpression, 180_000);
if (!report.scan?.ok) process.exitCode = 1;
}
report.navigation = await evaluate(`
(async () => {
const target = document.querySelector('[data-navigation-id=${JSON.stringify(view)}]');
if (!(target instanceof HTMLElement)) return { ok: false, error: ${JSON.stringify(`${view} navigation was not found.`)} };
target.click();
await new Promise((resolve) => setTimeout(resolve, 1500));
const activeNavigation = document.querySelector('.nav-item[aria-current="page"]');
return {
ok: activeNavigation?.getAttribute('data-navigation-id') === ${JSON.stringify(view)},
clickedNavigation: target.textContent.trim(),
activeNavigation: activeNavigation?.textContent?.trim() ?? null,
activeNavigationId: activeNavigation?.getAttribute('data-navigation-id') ?? null,
bodyText: document.body.innerText.slice(0, 16000),
viewport: { width: innerWidth, height: innerHeight },
document: {
clientWidth: document.documentElement.clientWidth,
clientHeight: document.documentElement.clientHeight,
scrollWidth: document.documentElement.scrollWidth,
scrollHeight: document.documentElement.scrollHeight,
},
};
})()
`, 30_000);
if (!report.navigation?.ok) process.exitCode = 1;
if (mode === "ui-scan5" || mode === "ui-scan-row1" || mode === "ui-stream20" || mode === "ui-full-inventory") {
report.scan = await evaluate(uiScanExpression, mode === "ui-full-inventory" ? 1_020_000 : 300_000);
if (!report.scan?.ok) process.exitCode = 1;
}
if (mode === "reprocess-run") {
report.reprocess = await evaluate(`
(async () => {
const runDir = ${JSON.stringify(reprocessRunDir)};
const expectedTarget = ${JSON.stringify(reprocessTarget)};
const maxReviewRate = ${JSON.stringify(reprocessMaxReviewRate)};
const processing = await window.assistantApi.nativeScannerProcessRun({
runDir,
persist: false,
limit: expectedTarget,
stream: false,
expectedTotal: expectedTarget,
});
const resultCount = Array.isArray(processing?.results) ? processing.results.length : 0;
const reviewRate = processing?.processed > 0 ? processing.review / processing.processed : 1;
return {
ok: Boolean(
processing?.ok
&& processing.processed === expectedTarget
&& processing.parsed === expectedTarget
&& resultCount === expectedTarget
&& processing.errors === 0
&& processing.stored === 0
&& processing.persisted === false
&& reviewRate <= maxReviewRate
),
runDir: processing?.runDir ?? runDir,
processed: processing?.processed ?? 0,
parsed: processing?.parsed ?? 0,
review: processing?.review ?? 0,
reviewRate,
stored: processing?.stored ?? 0,
errors: processing?.errors ?? 0,
elapsedMs: processing?.elapsedMs ?? 0,
resultCount,
persistenceDisabled: processing?.persisted === false,
maxReviewRate,
};
})()
`, 1_020_000);
if (!report.reprocess?.ok) process.exitCode = 1;
}
if (state === "scan-settings") {
report.interaction = await evaluate(`
(async () => {
const target = document.querySelector('[data-scan-action="open-settings"]');
if (!(target instanceof HTMLButtonElement)) return { ok: false, error: "Scan settings action was not found." };
target.click();
await new Promise((resolve) => setTimeout(resolve, 500));
const dialog = document.querySelector('[role="dialog"], .modal');
const visibleFocusable = dialog ? [...dialog.querySelectorAll('button,input,select,textarea,summary,[tabindex]:not([tabindex="-1"])')]
.filter((element) => element.getClientRects().length > 0 && getComputedStyle(element).visibility !== 'hidden') : [];
const firstFocusable = visibleFocusable[0];
const lastFocusable = visibleFocusable.at(-1);
lastFocusable?.focus();
lastFocusable?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true, cancelable: true }));
await new Promise((resolve) => requestAnimationFrame(resolve));
return {
ok: Boolean(dialog && firstFocusable && document.activeElement === firstFocusable),
bodyText: document.body.innerText.slice(0, 16000),
dialogText: dialog?.textContent?.trim() ?? null,
focusLooped: Boolean(dialog?.contains(document.activeElement) && document.activeElement === firstFocusable),
focusedLabel: document.activeElement?.getAttribute?.('aria-label') ?? document.activeElement?.textContent?.trim() ?? null,
};
})()
`, 30_000);
if (!report.interaction?.ok) process.exitCode = 1;
}
if (state === "artifact-open") {
report.interaction = await evaluate(`
(async () => {
const api = window.assistantApi;
const priorSummary = document.querySelector('.scan-summary-modal');
const priorSummaryClose = priorSummary?.querySelector('[data-scan-action="summary-close"]');
priorSummaryClose?.click();
if (priorSummaryClose) await new Promise((resolve) => setTimeout(resolve, 250));
const sources = await api.listCaptureSources();
const source = sources.find((candidate) => candidate.isGenshinCandidate);
if (!source) return { ok: false, inputSent: false, error: "Genshin source was not found." };
const options = {
skipOcr: true,
omitFullFrame: true,
omitInventoryPreview: true,
omitCrops: true,
omitLockState: true,
};
const before = await api.captureSource(source.id, 0, false, options);
const grid = before?.inventoryGrid;
const gridReady = before?.captureTarget === "genshin-client"
&& before?.width === 1920 && before?.height === 1080
&& grid?.source === "detected" && grid.confidence >= 60
&& grid.rows === 4 && grid.cols === 8 && grid.centers?.length >= 32;
if (!gridReady) {
return { ok: false, inputSent: false, error: "Accepted 1920x1080 4x8 Artifact grid was not detected.", before };
}
if (before.artifactDetail?.present && before.artifactDetail.confidence >= 45) {
return { ok: true, inputSent: false, alreadyOpen: true, artifactDetail: before.artifactDetail };
}
const target = grid.centers[0];
const focus = await api.focusGenshinForScanStart();
if (!focus.focused) return { ok: false, inputSent: false, error: "Genshin focus failed.", focus };
const click = await api.clickScreen(target.x, target.y);
await new Promise((resolve) => setTimeout(resolve, 650));
const after = await api.captureSource(source.id, 0, false, options);
await api.focusMainWindow();
return {
ok: Boolean(click.clicked && !click.inputBlocked && after?.artifactDetail?.present && after.artifactDetail.confidence >= 45),
inputSent: Boolean(click.clicked),
target,
click,
artifactDetail: after?.artifactDetail ?? null,
grid: after?.inventoryGrid ? { rows: after.inventoryGrid.rows, cols: after.inventoryGrid.cols, confidence: after.inventoryGrid.confidence } : null,
};
})()
`, 30_000);
if (!report.interaction?.ok) process.exitCode = 1;
}
if (state === "artifact-probe") {
report.interaction = await evaluate(`
(async () => {
const api = window.assistantApi;
const preflight = await api.nativeScannerPreflight({ category: 'artifacts' });
if (!preflight.ready || preflight.bounds?.width !== 1920 || preflight.bounds?.height !== 1080) {
return { ok: false, error: 'Artifact probe requires the accepted 1920x1080 preflight.', preflight };
}
const focus = await api.focusGenshinForScanStart();
if (!focus.focused) return { ok: false, error: 'Genshin focus failed.', focus, preflight };
const click = await api.clickScreen(179, 254);
await new Promise((resolve) => setTimeout(resolve, 450));
const sources = await api.listCaptureSources();
const source = sources.find((candidate) => candidate.isGenshinCandidate);
const capture = source
? await api.captureSource(source.id, 0, false, {
skipOcr: true,
omitFullFrame: true,
omitDetailPreview: true,
omitInventoryPreview: true,
omitCrops: true,
omitLockState: true,
})
: null;
await api.focusMainWindow();
return {
ok: Boolean(click.clicked && !click.inputBlocked && capture?.artifactDetail?.present),
focus,
click,
artifactDetail: capture?.artifactDetail ?? null,
inventoryGrid: capture?.inventoryGrid ?? null,
};
})()
`, 60_000);
if (!report.interaction?.ok) process.exitCode = 1;
}
if (state === "post-scan-results") {
report.interaction = await evaluate(`
(async () => {
const summary = document.querySelector('.scan-summary-modal');
const close = summary?.querySelector('[data-scan-action="summary-close"]');
close?.click();
await new Promise((resolve) => setTimeout(resolve, 350));
const panel = document.querySelector('.scanner-result-panel');
const evaluation = document.querySelector('.scan-evaluation-progress');
const rail = document.querySelector('.result-rail-list');
const rows = [...document.querySelectorAll('.result-rail-row')];
const panelRect = panel?.getBoundingClientRect();
return {
ok: Boolean(
!document.querySelector('.scan-summary-modal')
&& panel && evaluation && rail && rows.length > 0
&& document.documentElement.scrollWidth === document.documentElement.clientWidth
&& document.documentElement.scrollHeight === document.documentElement.clientHeight
),
resultRows: rows.length,
firstResult: rows[0]?.textContent?.trim() ?? null,
lastResult: rows.at(-1)?.textContent?.trim() ?? null,
evaluationText: evaluation?.textContent?.trim() ?? null,
panelRect: panelRect ? { x: panelRect.x, y: panelRect.y, width: panelRect.width, height: panelRect.height } : null,
rail: rail ? { clientHeight: rail.clientHeight, scrollHeight: rail.scrollHeight, scrollTop: rail.scrollTop } : null,
document: {
clientWidth: document.documentElement.clientWidth,
clientHeight: document.documentElement.clientHeight,
scrollWidth: document.documentElement.scrollWidth,
scrollHeight: document.documentElement.scrollHeight,
},
};
})()
`, 15_000);
if (!report.interaction?.ok) process.exitCode = 1;
}
if (state === "review-deeplink") {
report.interaction = await evaluate(`
(async () => {
const target = document.querySelector('.triage-primary-action');
if (!target) return { ok: false, error: "Review deep-link action was not found." };
target.click();
await new Promise((resolve) => setTimeout(resolve, 1200));
const activeNavigation = document.querySelector('.nav-item[aria-current="page"]');
const activeFilter = document.querySelector('.inventory-filter-control button.active')?.textContent?.trim() ?? null;
return {
ok: activeNavigation?.getAttribute('data-navigation-id') === 'inventory' && Boolean(activeFilter),
activeNavigation: activeNavigation?.textContent?.trim() ?? null,
activeNavigationId: activeNavigation?.getAttribute('data-navigation-id') ?? null,
activeFilter,
};
})()
`, 30_000);
if (!report.interaction?.ok) process.exitCode = 1;
}
if (state === "keyboard-focus") {
report.interaction = await evaluate(`
(() => {
const target = document.querySelector('.nav-item:not([aria-current="page"]):not(:disabled)');
if (!(target instanceof HTMLElement)) return { ok: false, error: "Focusable navigation item was not found." };
target.focus();
const styles = getComputedStyle(target);
return {
ok: document.activeElement === target && styles.outlineStyle !== "none" && Number.parseFloat(styles.outlineWidth) >= 2,
focusedText: target.textContent?.trim() ?? null,
outline: styles.outline,
boxShadow: styles.boxShadow,
};
})()
`, 10_000);
if (!report.interaction?.ok) process.exitCode = 1;
}
if (state === "overlay-open") {
report.interaction = await evaluate(`
(async () => {
const target = document.querySelector('.overlay-open-button');
if (!(target instanceof HTMLButtonElement)) return { ok: false, error: "Overlay action was not found." };
target.click();
await new Promise((resolve) => setTimeout(resolve, 650));
const toast = document.querySelector('.app-toast');
return {
ok: Boolean(toast),
toastText: toast?.textContent?.trim() ?? null,
};
})()
`, 15_000);
if (!report.interaction?.ok) process.exitCode = 1;
}
if (state === "inventory-confirm") {
report.interaction = await evaluate(`
(async () => {
let target = document.querySelector('[data-inventory-action="promote"]:not(:disabled), [data-inventory-action="review"]:not(:disabled)');
if (!target) {
const reviewFilter = document.querySelector('[data-inventory-filter="review"]');
reviewFilter?.click();
await new Promise((resolve) => setTimeout(resolve, 250));
const rows = [...document.querySelectorAll('.inventory-row')];
for (const row of rows) {
row.click();
await new Promise((resolve) => setTimeout(resolve, 150));
target = document.querySelector('[data-inventory-action="review"]:not(:disabled)');
if (target) break;
}
}
if (!target) return { ok: false, skipped: true, error: "No safe confirmation/editor action is available for the current corpus." };
target.click();
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
const confirmation = document.querySelector('.inventory-promotion-confirm, .inventory-review-editor');
return {
ok: Boolean(confirmation),
action: target.textContent.trim(),
confirmationText: confirmation?.textContent?.trim() ?? null,
focusedText: document.activeElement?.textContent?.trim() ?? null,
};
})()
`, 15_000);
if (!report.interaction?.ok && !report.interaction?.skipped) process.exitCode = 1;
}
if (state === "inventory-delete-confirm") {
report.interaction = await evaluate(`
(async () => {
const target = document.querySelector('[data-inventory-action="delete"]');
if (!(target instanceof HTMLButtonElement)) {
return { ok: false, error: "No local-only artifact deletion action is available for the selected row." };
}
target.click();
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
const confirmation = document.querySelector('.inventory-deletion-confirm');
const confirmButton = document.querySelector('[data-inventory-action="confirm-delete"]');
const cancelButton = document.querySelector('[data-inventory-action="cancel-delete"]');
const prompt = confirmation?.querySelector('strong');
const description = confirmation?.querySelector('small');
const visibleInViewport = (element) => {
if (!(element instanceof HTMLElement) || element.getClientRects().length === 0) return false;
const rect = element.getBoundingClientRect();
return rect.top >= 0 && rect.bottom <= innerHeight;
};
return {
ok: Boolean(
confirmation
&& visibleInViewport(prompt)
&& visibleInViewport(description)
&& confirmButton instanceof HTMLButtonElement
&& cancelButton instanceof HTMLButtonElement
&& document.activeElement === confirmButton
),
action: target.textContent?.trim() ?? null,
confirmationText: confirmation?.textContent?.trim() ?? null,
promptVisible: visibleInViewport(prompt),
descriptionVisible: visibleInViewport(description),
focusedAction: document.activeElement?.getAttribute?.('data-inventory-action') ?? null,
confirmDisabled: confirmButton instanceof HTMLButtonElement ? confirmButton.disabled : null,
};
})()
`, 15_000);
if (!report.interaction?.ok) process.exitCode = 1;
}
if (state === "inventory-store-delete-confirm") {
report.interaction = await evaluate(`
(async () => {
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const storeFilter = document.querySelector('[data-inventory-filter="stored"]');
if (!(storeFilter instanceof HTMLButtonElement)) {
return { ok: false, error: "The local Store filter is unavailable." };
}
storeFilter.click();
await sleep(160);
const storeRow = document.querySelector('.inventory-list [role="option"]');
if (!(storeRow instanceof HTMLElement)) {
return { ok: false, error: "No local Store row is available for a non-mutating confirmation probe." };
}
storeRow.click();
await sleep(120);
const target = document.querySelector('[data-inventory-action="delete"]');
if (!(target instanceof HTMLButtonElement)) {
return { ok: false, error: "The selected local Store row did not expose its delete action." };
}
target.click();
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
const confirmation = document.querySelector('.inventory-deletion-confirm');
const prompt = confirmation?.querySelector('strong');
const description = confirmation?.querySelector('small');
const confirmButton = document.querySelector('[data-inventory-action="confirm-delete"]');
const cancelButton = document.querySelector('[data-inventory-action="cancel-delete"]');
const linkedStoreToggle = document.querySelector('[data-inventory-action="toggle-linked-store"]');
const visibleInViewport = (element) => {
if (!(element instanceof HTMLElement) || element.getClientRects().length === 0) return false;
const rect = element.getBoundingClientRect();
return rect.top >= 0 && rect.bottom <= innerHeight;
};
return {
ok: Boolean(
confirmation
&& visibleInViewport(prompt)
&& visibleInViewport(description)
&& confirmButton instanceof HTMLButtonElement
&& cancelButton instanceof HTMLButtonElement
&& document.activeElement === confirmButton
&& !linkedStoreToggle
),
action: target.textContent?.trim() ?? null,
confirmationText: confirmation?.textContent?.trim() ?? null,
promptVisible: visibleInViewport(prompt),
descriptionVisible: visibleInViewport(description),
focusedAction: document.activeElement?.getAttribute?.('data-inventory-action') ?? null,
hasLinkedStoreToggle: Boolean(linkedStoreToggle),
};
})()
`, 15_000);
if (!report.interaction?.ok) process.exitCode = 1;
}
if (state === "inventory-roving") {
report.interaction = await evaluate(`
(async () => {
const listbox = document.querySelector('.inventory-list[role="listbox"]');
const before = listbox?.querySelector('[role="option"][aria-selected="true"]');
if (!(before instanceof HTMLElement)) return { ok: false, error: "Selected inventory row was not found." };
before.focus();
before.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true }));
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
const after = listbox.querySelector('[role="option"][aria-selected="true"]');
return {
ok: Boolean(after && after !== before && document.activeElement === after && after.getAttribute('tabindex') === '0'),
before: before.textContent.trim(),
after: after?.textContent?.trim() ?? null,
activeMatchesSelection: document.activeElement === after,
tabbableRows: listbox.querySelectorAll('[role="option"][tabindex="0"]').length,
};
})()
`, 15_000);
if (!report.interaction?.ok) process.exitCode = 1;
}
if (state === "inventory-technical") {
report.interaction = await evaluate(`
(async () => {
const target = document.querySelector('.inventory-technical-button');
if (!(target instanceof HTMLButtonElement)) return { ok: false, error: "Technical details action was not found." };
target.click();
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
const dialog = document.querySelector('.inventory-technical-modal[role="dialog"]');
const focusables = dialog ? [...dialog.querySelectorAll('button,input,select,textarea,[tabindex]:not([tabindex="-1"])')]
.filter((element) => element.getClientRects().length > 0 && getComputedStyle(element).visibility !== 'hidden') : [];
const firstFocusable = focusables[0];
const lastFocusable = focusables.at(-1);
lastFocusable?.focus();
lastFocusable?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true, cancelable: true }));
await new Promise((resolve) => requestAnimationFrame(resolve));
const rect = dialog?.getBoundingClientRect();
return {
ok: Boolean(
dialog && firstFocusable && document.activeElement === firstFocusable
&& rect && rect.top >= 0 && rect.bottom <= innerHeight
&& document.documentElement.scrollWidth === document.documentElement.clientWidth
&& document.documentElement.scrollHeight === document.documentElement.clientHeight
),
dialogText: dialog?.textContent?.trim() ?? null,
focusLooped: document.activeElement === firstFocusable,
dialogRect: rect ? { x: rect.x, y: rect.y, width: rect.width, height: rect.height } : null,
};
})()
`, 15_000);
if (!report.interaction?.ok) process.exitCode = 1;
}
await call("Page.bringToFront", {}, 10_000);
await evaluate("new Promise((resolve) => setTimeout(resolve, 120))", 10_000);
const screenshot = await call("Page.captureScreenshot", {
format: "png",
fromSurface: true,
captureBeyondViewport: false,
}, 30_000);
fs.mkdirSync(path.dirname(screenshotPath), { recursive: true });
fs.writeFileSync(screenshotPath, Buffer.from(screenshot.data, "base64"));
report.screenshotPath = screenshotPath;
report.rendererDiagnostics = { checked: consoleCheck, ...rendererDiagnostics };
if (consoleCheck && (rendererDiagnostics.exceptions.length > 0 || rendererDiagnostics.consoleErrors.length > 0 || rendererDiagnostics.logErrors.length > 0)) {
process.exitCode = 1;
}
fs.mkdirSync(path.dirname(output), { recursive: true });
fs.writeFileSync(output, JSON.stringify(report, null, 2), "utf8");
console.log(JSON.stringify(report, null, 2));
console.log(`Report: ${output}`);
console.log(`Screenshot: ${screenshotPath}`);
if (state === "scan-settings") await evaluate(`document.querySelector('[data-scan-action="close-settings"]')?.click(); true`, 5_000).catch(() => undefined);
if (state === "inventory-delete-confirm") await evaluate(`document.querySelector('[data-inventory-action="cancel-delete"]')?.click(); true`, 5_000).catch(() => undefined);
if (state === "inventory-store-delete-confirm") {
await evaluate(`
document.querySelector('[data-inventory-action="cancel-delete"]')?.click();
document.querySelector('[data-inventory-filter="all"]')?.click();
true
`, 5_000).catch(() => undefined);
}
if (state === "inventory-technical") await evaluate(`document.querySelector('.inventory-technical-modal .icon-button')?.click(); true`, 5_000).catch(() => undefined);
if (state === "overlay-open") await evaluate("window.assistantApi.hideOverlay(); true", 5_000).catch(() => undefined);
if (closeAfter) {
await evaluate("window.close(); true", 5_000).catch(() => undefined);
}
if (viewportWidth !== null && viewportHeight !== null) {
await call("Emulation.clearDeviceMetricsOverride", {}, 5_000).catch(() => undefined);
}
socket.close();