feat(scanner): complete localized artifact quality checkpoint
This commit is contained in:
@@ -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();
|
||||
Reference in New Issue
Block a user