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();