import fs from "node:fs/promises"; 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 outputPath = path.resolve(args.get("output") ?? "outputs/packaged-live/builds-functional-acceptance.json"); const screenshotPath = path.resolve(args.get("screenshot") ?? "outputs/packaged-live/builds-functional-acceptance.png"); if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new Error("--port must be an integer between 1 and 65535."); } 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 renderer with CDP was 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 diagnostics = { exceptions: [], consoleErrors: [], logErrors: [] }; socket.addEventListener("message", (event) => { let message; try { message = JSON.parse(String(event.data)); } catch { return; } if (message.method === "Runtime.exceptionThrown") { diagnostics.exceptions.push(message.params?.exceptionDetails?.exception?.description ?? message.params?.exceptionDetails?.text ?? "Unknown renderer exception"); } else if (message.method === "Runtime.consoleAPICalled" && message.params?.type === "error") { diagnostics.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") { diagnostics.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); await call("Runtime.enable", {}, 10_000); await call("Log.enable", {}, 10_000); await call("Emulation.setDeviceMetricsOverride", { width: 1304, height: 821, deviceScaleFactor: 1, mobile: false, }, 10_000); await call("Emulation.setEmulatedMedia", { features: [{ name: "prefers-reduced-motion", value: "reduce" }], }, 10_000); await evaluate("localStorage.removeItem('gaa-ui-locale'); 'locale-cleared'", 10_000); await call("Page.reload", { ignoreCache: true }, 10_000); await new Promise((resolve) => setTimeout(resolve, 350)); const acceptance = await evaluate(` (async () => { const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const waitFor = async (selector, timeout = 10_000) => { const deadline = Date.now() + timeout; while (Date.now() < deadline) { const element = document.querySelector(selector); if (element) return element; await sleep(50); } throw new Error('Timed out waiting for ' + selector); }; const api = window.assistantApi; if (!api) throw new Error('Packaged preload bridge is unavailable.'); const initialLocale = document.documentElement.lang; if (initialLocale !== 'en' || document.documentElement.dataset.appLocale !== 'en') { throw new Error('Fresh packaged UI did not default to English.'); } const settingsTrigger = await waitFor('[data-app-settings-trigger]'); settingsTrigger.click(); const languageSelect = await waitFor('[data-app-language-select]'); languageSelect.value = 'de'; languageSelect.dispatchEvent(new Event('change', { bubbles: true })); await sleep(100); const germanLocaleApplied = document.documentElement.lang === 'de' && document.querySelector('[data-navigation-id="inventory"]')?.textContent?.trim() === 'Artefakte'; languageSelect.value = 'en'; languageSelect.dispatchEvent(new Event('change', { bubbles: true })); await sleep(100); const englishLocaleRestored = document.documentElement.lang === 'en' && document.querySelector('[data-navigation-id="inventory"]')?.textContent?.trim() === 'Artifacts'; document.querySelector('[data-app-settings-backdrop]')?.dispatchEvent(new MouseEvent('click', { bubbles: true })); const buildsNavigation = await waitFor('[data-navigation-id="builds"]'); buildsNavigation.focus(); const navigationFocusBeforeOpen = document.activeElement === buildsNavigation; buildsNavigation.click(); const view = await waitFor('.build-preview-page'); await sleep(250); const headingFocusAfterOpen = document.activeElement?.id === 'app-view-heading'; const results = await api.nativeScannerLoadResults({ limit: 2400 }); const contextInput = document.querySelector('.build-context-number input'); const contextCheckbox = document.querySelector('.build-context-confirm input[type="checkbox"]'); let contextInteraction = { available: false, accepted: false }; if (contextInput instanceof HTMLInputElement && contextCheckbox instanceof HTMLInputElement) { // React tracks controlled input values on the instance. Assigning the // property directly makes the browser value change, but React can // restore the old draft on the next checkbox render. Use the native // prototype setter, then dispatch a real input event so this CDP probe // exercises the same state transition as a typed value. const nativeValueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set; nativeValueSetter?.call(contextInput, '106.6'); contextInput.dispatchEvent(new Event('input', { bubbles: true })); await sleep(0); contextCheckbox.click(); await sleep(100); contextInteraction = { available: true, accepted: contextInput.value === '106.6' && contextCheckbox.checked }; } const viewStyle = getComputedStyle(view); const reducedMotionRespected = viewStyle.animationDuration === '0s' || viewStyle.animationDuration === '0ms' || matchMedia('(prefers-reduced-motion: reduce)').matches; const noDocumentOverflow = document.documentElement.scrollWidth <= document.documentElement.clientWidth && document.documentElement.scrollHeight <= document.documentElement.clientHeight; return { ok: Boolean( results.ok && germanLocaleApplied && englishLocaleRestored && navigationFocusBeforeOpen && headingFocusAfterOpen && contextInteraction.accepted && reducedMotionRespected && noDocumentOverflow ), locale: { initial: initialLocale, germanLocaleApplied, englishLocaleRestored }, focus: { navigationFocusBeforeOpen, headingFocusAfterOpen }, contextInteraction, results: { ok: results.ok, runDir: results.runDir, total: results.total, loaded: results.results.length, error: results.error ?? '' }, rendered: { bodyText: view.textContent?.slice(0, 6_000) ?? '', hasSuggestions: Boolean(document.querySelector('.build-suggestion-card')), deferredProfiles: document.querySelectorAll('.build-deferred-list li').length, overflow: { width: document.documentElement.scrollWidth, clientWidth: document.documentElement.clientWidth, height: document.documentElement.scrollHeight, clientHeight: document.documentElement.clientHeight }, reducedMotionRespected, }, }; })() `); const screenshot = await call("Page.captureScreenshot", { format: "png", captureBeyondViewport: false }, 30_000); await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.writeFile(screenshotPath, Buffer.from(screenshot.data, "base64")); const report = { version: "packaged-builds-functional-acceptance-v1", createdAt: new Date().toISOString(), target: { title: target.title, url: target.url }, acceptance, diagnostics, screenshotPath, ok: Boolean(acceptance?.ok) && diagnostics.exceptions.length === 0 && diagnostics.consoleErrors.length === 0 && diagnostics.logErrors.length === 0, }; await fs.writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`, "utf8"); socket.close(); console.log(`Packaged Builds acceptance: ${report.ok ? "PASS" : "FAIL"}`); console.log(`Report: ${outputPath}`); console.log(`Screenshot: ${screenshotPath}`); if (!report.ok) process.exitCode = 1;