const assert = require("node:assert/strict"); const fs = require("node:fs"); const http = require("node:http"); const os = require("node:os"); const path = require("node:path"); const { chromium } = require("@playwright/test"); const { VIDEO_A, VIDEO_B, createFakeBackend, injectGeneratedUserscript, installGmEnvironment, openNavigationFixture, openWatchFixture, } = require("../UserScript/e2e/harness"); const { assertInvariantContinuously, waitForStableInvariant } = require("./continuous-invariants"); const { WORKER_SIGNAL_PATH, assertExactSuccessfulVotesTraffic, isAllowedApiPreflight, } = require("./hermetic-api-contract"); const { LIVE_RUNTIME_PROFILES } = require("./live-runtime-adapter"); const { SHARED_LIVE_SCENARIO_IDS } = require("./shared-live-scenarios"); const { assertProductionJavaScriptOutput } = require("./verify-extension-artifact"); const { isVoteProtocolBodyPairValid } = require("./vote-protocol-contract"); const REPOSITORY_ROOT = path.resolve(__dirname, "../.."); const PRODUCTION_API_ORIGIN = "https://returnyoutubedislikeapi.com"; const ARTIFACT_SMOKE_SCENARIO_ID = "watch-render"; const ARTIFACT_WATCH_SPA_SCENARIO_ID = "watch-spa-side-panel"; const ARTIFACT_WATCH_SPA_VOTE_SCENARIO_ID = "watch-spa-dislike-activation"; const ARTIFACT_WATCH_SPA_CLONED_VOTE_SCENARIO_ID = "watch-spa-cloned-controls-immediate-dislike"; const ARTIFACT_EXTENSION_DELAYED_FAILURE_SCENARIO_ID = "extension-watch-spa-delayed-outgoing-failure"; const ARTIFACT_NATIVE_HYDRATION_DELAY_MS = 750; const SHARED_ARTIFACT_SCENARIO_IDS = Object.freeze([ ARTIFACT_SMOKE_SCENARIO_ID, ARTIFACT_WATCH_SPA_SCENARIO_ID, ARTIFACT_WATCH_SPA_VOTE_SCENARIO_ID, ARTIFACT_WATCH_SPA_CLONED_VOTE_SCENARIO_ID, ]); const SHARED_ARTIFACT_RUNTIMES = Object.freeze(["userscript", "extension"]); const EXTENSION_ONLY_ARTIFACT_CAPABILITIES = Object.freeze(["background", "premium", "settings"]); const ARTIFACT_BROWSER_SCENARIO_CATALOG = Object.freeze([ ...SHARED_ARTIFACT_SCENARIO_IDS.map((id) => Object.freeze({ id, runtimes: SHARED_ARTIFACT_RUNTIMES, shared: true })), Object.freeze({ capability: "background", id: ARTIFACT_EXTENSION_DELAYED_FAILURE_SCENARIO_ID, runtimes: Object.freeze(["extension"]), shared: false, }), ]); const SPA_COUNTS = Object.freeze({ [VIDEO_A]: Object.freeze({ dislikes: 10, likes: 90 }), [VIDEO_B]: Object.freeze({ dislikes: 65, likes: 35 }), }); const DEFAULT_EXTENSION_ARTIFACT = path.join(REPOSITORY_ROOT, "Extensions", "combined", "dist", "chrome"); const DEFAULT_USERSCRIPT_ARTIFACT = path.join( REPOSITORY_ROOT, "Extensions", "UserScript", "Return Youtube Dislike.user.js", ); const ZERO_DIFFICULTY_PUZZLE = { challenge: Buffer.alloc(16).toString("base64"), difficulty: 0, }; const ARTIFACT_UNHANDLED_REJECTION_BINDING = "__rydArtifactReportUnhandledRejection"; const ARTIFACT_WORKER_SIGNAL_STORE = "__rydArtifactWorkerSignals"; const CONSOLE_FAILURE_TYPES = new Set(["assert", "error", "warning"]); const SHARED_ARTIFACT_FIXTURE_DEFAULTS = Object.freeze({ nativeDislikeText: false, roleAttribute: "data-fixture-role", }); const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)); function assertArtifactBrowserScenarioCatalog(catalog = ARTIFACT_BROWSER_SCENARIO_CATALOG) { assert.ok(Array.isArray(catalog), "The hermetic artifact browser catalog must be an array."); assert.equal( new Set(catalog.map(({ id }) => id)).size, catalog.length, "Artifact browser scenario IDs must be unique.", ); const byId = new Map(catalog.map((entry) => [entry.id, entry])); for (const scenarioId of SHARED_ARTIFACT_SCENARIO_IDS) { const entry = byId.get(scenarioId); assert.ok(entry, `The shared artifact browser catalog is missing ${scenarioId}.`); assert.equal(entry.shared, true, `The core artifact browser scenario ${scenarioId} must remain shared.`); assert.deepEqual( entry.runtimes, SHARED_ARTIFACT_RUNTIMES, `The core artifact browser scenario ${scenarioId} must run for userscript and extension.`, ); assert.equal(entry.capability, undefined, `The shared artifact browser scenario ${scenarioId} is misclassified.`); } for (const entry of catalog) { assert.equal(typeof entry.id, "string", "Every artifact browser scenario must have a string ID."); assert.equal( typeof entry.shared, "boolean", `Artifact browser scenario ${entry.id} must declare shared explicitly.`, ); if (entry.shared) { assert.ok( SHARED_ARTIFACT_SCENARIO_IDS.includes(entry.id), `Unknown shared artifact browser scenario ${entry.id} must be added to the shared scenario contract.`, ); assert.deepEqual( entry.runtimes, SHARED_ARTIFACT_RUNTIMES, `The core artifact browser scenario ${entry.id} must run for userscript and extension.`, ); assert.equal(entry.capability, undefined, `The shared artifact browser scenario ${entry.id} is misclassified.`); } else { assert.deepEqual( entry.runtimes, ["extension"], `Extension-only scenario ${entry.id} must run only for extension.`, ); assert.ok( EXTENSION_ONLY_ARTIFACT_CAPABILITIES.includes(entry.capability), `Extension-only scenario ${entry.id} must declare a supported capability.`, ); } } return catalog; } function createArtifactBrowserScenarioPlan(catalog = ARTIFACT_BROWSER_SCENARIO_CATALOG) { return assertArtifactBrowserScenarioCatalog(catalog).flatMap(({ id: scenarioId, runtimes }) => runtimes.map((runtime) => Object.freeze({ runtime, scenarioId })), ); } function createSharedArtifactBackendOptions(backendOptions = {}) { if (!backendOptions || typeof backendOptions !== "object" || Array.isArray(backendOptions)) { throw new TypeError("Shared artifact backend options must be an object."); } const fixture = backendOptions.fixture ?? {}; if (!fixture || typeof fixture !== "object" || Array.isArray(fixture)) { throw new TypeError("Shared artifact fixture options must be an object."); } return { ...backendOptions, fixture: { ...SHARED_ARTIFACT_FIXTURE_DEFAULTS, ...fixture, }, }; } function serializeBrowserError(error) { return { message: error?.message ?? String(error), name: error?.name ?? "Error", stack: error?.stack ?? null, }; } async function createPageSignalCollector(page, runtime) { assert.ok(page && typeof page.on === "function", "A Playwright page is required to collect browser signals."); assert.ok(["extension", "userscript"].includes(runtime), "A supported runtime is required for page diagnostics."); const consoleErrors = []; const pageErrors = []; const unhandledRejections = []; page.on("console", (message) => { if (!CONSOLE_FAILURE_TYPES.has(message.type())) return; consoleErrors.push({ location: message.location(), text: message.text(), type: message.type(), }); }); page.on("pageerror", (error) => pageErrors.push(serializeBrowserError(error))); await page.exposeBinding(ARTIFACT_UNHANDLED_REJECTION_BINDING, (source, rejection) => { unhandledRejections.push({ ...rejection, frameUrl: source.frame?.url() ?? null, }); }); await page.addInitScript( ({ bindingName }) => { globalThis.addEventListener("unhandledrejection", (event) => { const reason = event.reason; let serialized; if (reason instanceof Error || (reason && typeof reason.message === "string")) { serialized = { message: reason.message, name: typeof reason.name === "string" ? reason.name : "Error", stack: reason.stack ?? null, }; } else { let value; try { value = JSON.stringify(reason); } catch { value = String(reason); } serialized = { message: value === undefined ? String(reason) : value, name: "UnhandledRejection", stack: null, }; } void Promise.resolve(globalThis[bindingName](serialized)).catch(() => {}); }); }, { bindingName: ARTIFACT_UNHANDLED_REJECTION_BINDING }, ); const snapshot = () => ({ consoleErrors: consoleErrors.map((signal) => ({ ...signal, location: { ...signal.location } })), pageErrors: pageErrors.map((signal) => ({ ...signal })), runtime, unhandledRejections: unhandledRejections.map((signal) => ({ ...signal })), }); return { async assertClean(scenarioId) { assert.equal(typeof scenarioId, "string", "A scenario id is required when checking page signals."); await page.evaluate(() => new Promise((resolve) => globalThis.setTimeout(resolve, 0))); const diagnostics = snapshot(); const failureCount = diagnostics.consoleErrors.length + diagnostics.pageErrors.length + diagnostics.unhandledRejections.length; assert.equal( failureCount, 0, `${runtime} emitted unexpected browser signals during ${scenarioId}: ${JSON.stringify(diagnostics, null, 2)}`, ); return diagnostics; }, snapshot, }; } function createWorkerRuntimeProbe(signalEndpoint) { const endpoint = JSON.stringify(signalEndpoint); const signalStore = JSON.stringify(ARTIFACT_WORKER_SIGNAL_STORE); return `;(() => { const endpoint = ${endpoint}; const signals = []; Object.defineProperty(globalThis, ${signalStore}, { configurable: false, value: signals, writable: false }); const serialize = (kind, value, details = {}) => { let message; let name = kind === "unhandledrejection" ? "UnhandledRejection" : "Error"; let stack = null; if (value instanceof Error || (value && typeof value.message === "string")) { message = value.message; name = typeof value.name === "string" ? value.name : name; stack = typeof value.stack === "string" ? value.stack : null; } else { try { message = typeof value === "string" ? value : JSON.stringify(value); } catch { message = String(value); } if (message === undefined) message = String(value); } return { at: Date.now(), details, kind, message, name, stack }; }; const report = (kind, value, details) => { const signal = serialize(kind, value, details); signals.push(signal); void fetch(endpoint, { body: JSON.stringify(signal), headers: { "content-type": "text/plain;charset=UTF-8" }, method: "POST", }).catch(() => {}); }; globalThis.addEventListener("error", (event) => { report("error", event.error || event.message, { column: event.colno || null, filename: event.filename || null, line: event.lineno || null, }); }); globalThis.addEventListener("unhandledrejection", (event) => report("unhandledrejection", event.reason)); for (const [method, kind] of [["error", "console-error"], ["warn", "console-warning"]]) { const original = console[method].bind(console); console[method] = (...values) => { report(kind, values.map((value) => (typeof value === "string" ? value : String(value))).join(" ")); return original(...values); }; } const originalAssert = console.assert.bind(console); console.assert = (condition, ...values) => { if (!condition) report("console-assert", values.join(" ") || "Assertion failed"); return originalAssert(condition, ...values); }; })();`; } function createWorkerSignalCollector(worker, apiServer) { assert.ok(worker && typeof worker.evaluate === "function", "A Playwright MV3 worker is required."); assert.ok(Array.isArray(apiServer?.workerSignals), "The hermetic server must expose worker signals."); const consoleFailures = []; let evaluatedSignals = []; let evaluationFailure = null; worker.on("console", (message) => { if (!CONSOLE_FAILURE_TYPES.has(message.type())) return; consoleFailures.push({ text: message.text(), type: message.type() }); }); async function refresh() { try { await worker.evaluate(() => new Promise((resolve) => globalThis.setTimeout(resolve, 0))); evaluatedSignals = await worker.evaluate((storeName) => { const signals = globalThis[storeName]; if (!Array.isArray(signals)) throw new Error(`Missing MV3 worker signal probe ${storeName}`); return signals.map((signal) => ({ ...signal })); }, ARTIFACT_WORKER_SIGNAL_STORE); evaluationFailure = null; } catch (error) { evaluationFailure = serializeBrowserError(error); } } const snapshot = () => ({ consoleFailures: consoleFailures.map((signal) => ({ ...signal })), evaluatedSignals: evaluatedSignals.map((signal) => ({ ...signal })), evaluationFailure: evaluationFailure ? { ...evaluationFailure } : null, reportedSignals: apiServer.workerSignals.map((signal) => ({ ...signal })), workerUrl: worker.url(), }); return { async assertClean(scenarioId) { await refresh(); const diagnostics = snapshot(); const signalCount = diagnostics.consoleFailures.length + diagnostics.evaluatedSignals.length + diagnostics.reportedSignals.length + Number(Boolean(diagnostics.evaluationFailure)); assert.equal( signalCount, 0, `extension MV3 worker emitted unexpected runtime signals during ${scenarioId}: ${JSON.stringify( diagnostics, null, 2, )}`, ); return diagnostics; }, refresh, snapshot, }; } function combineExtensionSignalCollectors(pageCollector, workerCollector) { return { async assertClean(scenarioId) { let pageFailure = null; let pageDiagnostics; try { pageDiagnostics = await pageCollector.assertClean(scenarioId); } catch (error) { pageFailure = error; pageDiagnostics = pageCollector.snapshot(); } let workerFailure = null; let workerDiagnostics; try { workerDiagnostics = await workerCollector.assertClean(scenarioId); } catch (error) { workerFailure = error; workerDiagnostics = workerCollector.snapshot(); } if (pageFailure || workerFailure) { throw new Error( `extension emitted unexpected runtime signals during ${scenarioId}: ${JSON.stringify( { page: pageDiagnostics, worker: workerDiagnostics }, null, 2, )}`, { cause: pageFailure ?? workerFailure }, ); } return { page: pageDiagnostics, worker: workerDiagnostics }; }, snapshot: () => ({ page: pageCollector.snapshot(), worker: workerCollector.snapshot() }), }; } function assertLoopbackOrigin(value) { const url = new URL(value); assert.ok(["http:", "https:"].includes(url.protocol), "The hermetic API origin must use HTTP or HTTPS."); assert.ok( ["127.0.0.1", "::1", "[::1]", "localhost"].includes(url.hostname), `Refusing to prepare a hermetic extension artifact for non-loopback origin ${url.origin}.`, ); assert.equal(url.pathname, "/", "The hermetic API value must be an origin without a path."); return url.origin; } function removeOwnedTemporaryDirectory(directory, prefix) { if (!directory) return; const resolvedDirectory = path.resolve(directory); const resolvedTemporaryRoot = path.resolve(os.tmpdir()); assert.equal(path.dirname(resolvedDirectory), resolvedTemporaryRoot, "Refusing to remove a non-temporary directory."); assert.ok(path.basename(resolvedDirectory).startsWith(prefix), "Refusing to remove an unowned temporary directory."); fs.rmSync(resolvedDirectory, { force: true, recursive: true }); } function readGeneratedMv3Contract(artifactDirectory) { const manifestPath = path.join(artifactDirectory, "manifest.json"); const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); assert.equal(manifest.manifest_version, 3, "The extension browser suite requires a generated MV3 artifact."); assert.equal( manifest.background?.service_worker, "ryd.background.js", "The generated MV3 artifact must declare the built background service worker.", ); const contentScript = manifest.content_scripts?.find( ({ css = [], js = [] }) => js.includes("ryd.content-script.js") && css.includes("content-style.css"), ); assert.ok(contentScript, "The generated MV3 artifact must declare its built content script and stylesheet."); for (const asset of [manifest.background.service_worker, "ryd.content-script.js", "content-style.css"]) { assert.ok( fs.statSync(path.join(artifactDirectory, asset)).isFile(), `The generated MV3 artifact is missing declared asset ${asset}.`, ); } for (const asset of new Set([manifest.background.service_worker, ...contentScript.js])) { assertProductionJavaScriptOutput(path.join(artifactDirectory, asset), asset); } return { serviceWorkerPath: `/${manifest.background.service_worker.replace(/^\/+/, "")}`, version: manifest.version, }; } function prepareHermeticExtensionArtifact(sourceDirectory, apiOrigin) { const origin = assertLoopbackOrigin(apiOrigin); const source = path.resolve(sourceDirectory); for (const requiredFile of ["manifest.json", "menu-fixer.js", "ryd.background.js", "ryd.content-script.js"]) { if (!fs.existsSync(path.join(source, requiredFile))) { throw new Error(`The extension artifact is missing ${requiredFile}: ${source}`); } } const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ryd-mv3-e2e-")); const extensionDirectory = path.join(temporaryRoot, "extension"); fs.cpSync(source, extensionDirectory, { recursive: true }); const backgroundBundlePath = path.join(extensionDirectory, "ryd.background.js"); const backgroundSource = fs.readFileSync(backgroundBundlePath, "utf8"); const replacementCount = backgroundSource.split(PRODUCTION_API_ORIGIN).length - 1; if (replacementCount < 1) { removeOwnedTemporaryDirectory(temporaryRoot, "ryd-mv3-e2e-"); throw new Error("ryd.background.js has no production API origin to replace; rebuild the extension before testing."); } const transformedBackground = backgroundSource.replaceAll(PRODUCTION_API_ORIGIN, origin); assert.equal( transformedBackground.includes(PRODUCTION_API_ORIGIN), false, "ryd.background.js still contains the production API origin after transformation.", ); const changelogListener = /api\.runtime\.onInstalled\.addListener\(\(details\) => \{\r?\n maybeShowChangelog\(details\);\r?\n\}\);/g; if ((transformedBackground.match(changelogListener) ?? []).length !== 1) { removeOwnedTemporaryDirectory(temporaryRoot, "ryd-mv3-e2e-"); throw new Error("ryd.background.js must have exactly one recognized first-install changelog listener to suppress."); } const hermeticBackground = transformedBackground.replace( changelogListener, "api.runtime.onInstalled.addListener(() => {});", ); const workerSignalEndpoint = `${origin}${WORKER_SIGNAL_PATH}`; fs.writeFileSync(backgroundBundlePath, `${createWorkerRuntimeProbe(workerSignalEndpoint)}\n${hermeticBackground}`); const contentScriptPath = path.join(extensionDirectory, "ryd.content-script.js"); const contentScriptSource = fs.readFileSync(contentScriptPath, "utf8"); if (!contentScriptSource.includes(PRODUCTION_API_ORIGIN)) { removeOwnedTemporaryDirectory(temporaryRoot, "ryd-mv3-e2e-"); throw new Error("ryd.content-script.js has no production API origin for the pre-navigation route to intercept."); } const manifestPath = path.join(extensionDirectory, "manifest.json"); const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); const apiUrl = new URL(origin); const loopbackPermission = `${apiUrl.protocol}//${apiUrl.hostname}/*`; manifest.host_permissions = [...new Set([...(manifest.host_permissions ?? []), loopbackPermission])]; fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); return { extensionDirectory, loopbackPermission, replacements: { "ryd.background.js": replacementCount, firstInstallChangelogListener: 1 }, routedBundles: ["ryd.content-script.js"], temporaryRoot, workerSignalEndpoint, }; } function readRequestBody(request) { return new Promise((resolve, reject) => { const chunks = []; request.on("data", (chunk) => chunks.push(chunk)); request.on("end", () => { const text = Buffer.concat(chunks).toString("utf8"); if (!text) { resolve(null); return; } try { resolve(JSON.parse(text)); } catch { resolve(text); } }); request.on("error", reject); }); } async function startHermeticApiServer({ dislikes = 25, likes = 100 } = {}) { const records = []; const unexpectedRequests = []; const workerSignals = []; const responsePlans = new Map(); const responseKey = (method, pathname) => `${method.toUpperCase()} ${pathname}`; const enqueue = (method, pathname, plannedResponse) => { const key = responseKey(method, pathname); const queue = responsePlans.get(key) ?? []; queue.push(plannedResponse); responsePlans.set(key, queue); }; const defer = (method, pathname) => { let releaseResponse; let resolveSeen; let released = false; const seen = new Promise((resolve) => { resolveSeen = resolve; }); enqueue(method, pathname, (record) => { resolveSeen(record); return new Promise((resolve) => { releaseResponse = resolve; }); }); return { get released() { return released; }, release(response) { if (released) return; if (!releaseResponse) { throw new Error(`Cannot release ${responseKey(method, pathname)} before its request is seen`); } released = true; releaseResponse(response); }, seen, }; }; const requestsFor = (method, pathname) => records.filter((record) => record.method === method.toUpperCase() && record.pathname === pathname); const takePlannedResponse = (record) => { const queue = responsePlans.get(responseKey(record.method, record.pathname)); if (!queue?.length) return null; const planned = queue.shift(); return typeof planned === "function" ? planned(record) : planned; }; const server = http.createServer(async (request, response) => { const origin = `http://${request.headers.host}`; const url = new URL(request.url, origin); const record = { at: Date.now(), body: await readRequestBody(request), method: request.method, pathname: url.pathname, query: Object.fromEntries(url.searchParams.entries()), }; records.push(record); const headers = { "access-control-allow-headers": "Accept, Content-Type", "access-control-allow-methods": "GET, POST, OPTIONS", "access-control-allow-origin": "*", "content-type": "application/json; charset=utf-8", }; if (request.method === "OPTIONS") { const requestedMethod = request.headers["access-control-request-method"]; if (!isAllowedApiPreflight(url.pathname, requestedMethod)) { unexpectedRequests.push(record); record.respondedAt = Date.now(); record.responseBody = { error: "unexpected hermetic preflight" }; record.responseStatus = 404; response.writeHead(404, headers); response.end(JSON.stringify(record.responseBody)); return; } record.respondedAt = Date.now(); record.responseBody = null; record.responseStatus = 204; response.writeHead(204, headers); response.end(); return; } if (request.method === "POST" && url.pathname === WORKER_SIGNAL_PATH) { workerSignals.push(record.body); record.respondedAt = Date.now(); record.responseBody = null; record.responseStatus = 204; response.writeHead(204, headers); response.end(); return; } let defaultBody; if (request.method === "GET" && url.pathname === "/configs/selectors") defaultBody = {}; else if (request.method === "GET" && url.pathname === "/votes") { defaultBody = { dislikes, likes, rating: 4.5 }; } else if (request.method === "GET" && url.pathname === "/puzzle/registration") { defaultBody = ZERO_DIFFICULTY_PUZZLE; } else if (request.method === "POST" && url.pathname === "/puzzle/registration") defaultBody = true; else if (request.method === "POST" && url.pathname === "/interact/vote") defaultBody = ZERO_DIFFICULTY_PUZZLE; else if (request.method === "POST" && url.pathname === "/interact/confirmVote") defaultBody = true; else { unexpectedRequests.push(record); record.respondedAt = Date.now(); record.responseBody = { error: "unexpected hermetic request" }; record.responseStatus = 404; response.writeHead(404, headers); response.end(JSON.stringify(record.responseBody)); return; } const planned = takePlannedResponse(record); const resolvedPlan = planned ? await planned : planned; const responsePlan = resolvedPlan ?? { body: defaultBody }; if (responsePlan.delayMs) await delay(responsePlan.delayMs); const body = responsePlan.body === undefined ? defaultBody : responsePlan.body; const status = responsePlan.status ?? 200; record.respondedAt = Date.now(); record.responseBody = body; record.responseStatus = status; response.writeHead(status, { ...headers, ...(responsePlan.headers ?? {}) }); response.end(typeof body === "string" ? body : JSON.stringify(body)); }); await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", resolve); }); const address = server.address(); assert.ok(address && typeof address === "object"); return { close: () => new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))), defer, enqueue, origin: `http://127.0.0.1:${address.port}`, records, requestsFor, unexpectedRequests, workerSignals, }; } async function installArtifactRoutes(context, backend, { passthroughOrigin = null } = {}) { await context.route("**/*", async (route) => { const url = new URL(route.request().url()); if (url.protocol === "chrome-extension:") { await route.continue(); return; } if (passthroughOrigin && url.origin === passthroughOrigin) { await route.continue(); return; } await backend.handle(route); }); } async function waitForWatchResult(page, runtime, videoId) { const profile = LIVE_RUNTIME_PROFILES[runtime]; const dislikeTextSelector = [ "dislike-button-view-model #text", "dislike-button-view-model [role='text']", "dislike-button-view-model .yt-spec-button-shape-next__button-text-content", "dislike-button-view-model .ytSpecButtonShapeNextButtonTextContent", ].join(", "); await page.waitForFunction( ({ dislikeTextSelector: expectedDislikeTextSelector, rateBarContainer, videoId: expectedVideoId }) => { const rendered = (element) => { if (!element?.isConnected || element.closest("[hidden], [aria-hidden='true'], [inert]")) return false; const box = element.getBoundingClientRect(); const style = getComputedStyle(element); return ( box.width > 0 && box.height > 0 && style.display !== "none" && style.visibility !== "hidden" && Number.parseFloat(style.opacity) !== 0 ); }; const watch = document.querySelector( `ytd-watch-flexy[video-id="${expectedVideoId}"], ytd-watch-grid[video-id="${expectedVideoId}"]`, ); const actionSurface = watch ? [...watch.querySelectorAll("#top-level-buttons-computed")].find( (candidate) => rendered(candidate) && rendered(candidate.querySelector(rateBarContainer)), ) : null; const dislikeText = actionSurface?.querySelector(expectedDislikeTextSelector) ?? null; const count = (dislikeText?.textContent ?? "").replace(/\s+/g, " ").trim(); return Boolean(actionSurface && rendered(dislikeText) && /\d/.test(count)); }, { dislikeTextSelector, rateBarContainer: profile.selectors.rateBarContainer, videoId }, ); return page.evaluate( ({ dislikeTextSelector: expectedDislikeTextSelector, rateBar, rateBarContainer, videoId: expectedVideoId }) => { const rendered = (element) => { if (!element?.isConnected || element.closest("[hidden], [aria-hidden='true'], [inert]")) return false; const box = element.getBoundingClientRect(); const style = getComputedStyle(element); return ( box.width > 0 && box.height > 0 && style.display !== "none" && style.visibility !== "hidden" && Number.parseFloat(style.opacity) !== 0 ); }; const watch = document.querySelector( `ytd-watch-flexy[video-id="${expectedVideoId}"], ytd-watch-grid[video-id="${expectedVideoId}"]`, ); const actionSurface = watch ? [...watch.querySelectorAll("#top-level-buttons-computed")].find( (candidate) => rendered(candidate) && rendered(candidate.querySelector(rateBarContainer)), ) : null; const dislikeText = actionSurface?.querySelector(expectedDislikeTextSelector) ?? null; const container = actionSurface?.querySelector(rateBarContainer); const fill = actionSurface?.querySelector(rateBar); const visible = (element) => { const box = element?.getBoundingClientRect(); return Boolean(box && box.width > 0 && box.height > 0); }; return { actionSurfaceVisible: rendered(actionSurface), count: (dislikeText?.textContent ?? "").replace(/\s+/g, " ").trim(), countVisible: rendered(dislikeText), fillRatio: container && fill && container.getBoundingClientRect().width > 0 ? fill.getBoundingClientRect().width / container.getBoundingClientRect().width : null, fillVisible: visible(fill), ownedByExpectedWatch: Boolean( actionSurface && actionSurface.closest("ytd-watch-flexy, ytd-watch-grid") === watch, ), rateBarVisible: visible(container), sameActionSurface: Boolean( actionSurface && dislikeText?.closest("#top-level-buttons-computed") === actionSurface && container?.closest("#top-level-buttons-computed") === actionSurface, ), videoId: watch?.getAttribute("video-id") ?? null, expectedVideoId, }; }, { ...profile.selectors, dislikeTextSelector, videoId }, ); } async function prepareSpaOutgoingControls(page, fromVideoId) { return page.evaluate((expectedVideoId) => { const fixturePage = document.querySelector("#fixture-page"); const currentSection = fixturePage?.querySelector( `[data-fixture-page-kind="watch"][data-fixture-video-id="${expectedVideoId}"]`, ); const outgoingTopRow = currentSection?.querySelector("#top-row"); if (!fixturePage || !currentSection || !outgoingTopRow) { throw new Error(`The outgoing watch fixture for ${expectedVideoId} is not ready.`); } if (!outgoingTopRow.querySelector(".ryd-tooltip")) { throw new Error(`The outgoing watch fixture for ${expectedVideoId} has no initialized ratio bar.`); } const beforeHolder = document.createElement("div"); beforeHolder.hidden = true; beforeHolder.setAttribute("data-artifact-outgoing-position", "before-current-root"); beforeHolder.setAttribute("data-artifact-outgoing-video-id", expectedVideoId); beforeHolder.appendChild(outgoingTopRow.cloneNode(true)); fixturePage.before(beforeHolder); globalThis.__artifactInsideOutgoingActions = outgoingTopRow.cloneNode(true); return { beforeBarCount: beforeHolder.querySelectorAll(".ryd-tooltip").length, fromVideoId: expectedVideoId, }; }, fromVideoId); } async function preparePendingSpaOutgoingControls(page, fromVideoId) { return page.evaluate((expectedVideoId) => { const fixturePage = document.querySelector("#fixture-page"); const currentSection = fixturePage?.querySelector( `[data-fixture-page-kind="watch"][data-fixture-video-id="${expectedVideoId}"]`, ); const outgoingTopRow = currentSection?.querySelector("#top-row"); if (!fixturePage || !currentSection || !outgoingTopRow) { throw new Error(`The pending outgoing watch fixture for ${expectedVideoId} is not ready.`); } const beforeHolder = document.createElement("div"); beforeHolder.hidden = true; beforeHolder.setAttribute("data-artifact-outgoing-position", "before-current-root"); beforeHolder.setAttribute("data-artifact-outgoing-video-id", expectedVideoId); beforeHolder.appendChild(outgoingTopRow.cloneNode(true)); fixturePage.before(beforeHolder); globalThis.__artifactInsideOutgoingActions = outgoingTopRow.cloneNode(true); return { beforeBarCount: beforeHolder.querySelectorAll(".ryd-tooltip").length, fromVideoId: expectedVideoId, }; }, fromVideoId); } function replaceSpaDestinationInPage({ expectedLikes, expectedVideoId, nativeHydrationDelay, beforeNavigation }) { const replace = () => { const currentSection = document.querySelector( `#fixture-page [data-fixture-page-kind="watch"][data-fixture-video-id="${expectedVideoId}"]`, ); if (!currentSection) throw new Error(`The destination watch fixture for ${expectedVideoId} is missing.`); if (!globalThis.__artifactInsideOutgoingActions) { throw new Error("The retained inside-current-root outgoing controls are missing."); } const insideHolder = document.createElement("div"); insideHolder.hidden = true; insideHolder.setAttribute("data-artifact-outgoing-position", "inside-current-root"); insideHolder.setAttribute( "data-artifact-outgoing-video-id", globalThis.__artifactInsideOutgoingActions .querySelector("[data-fixture-control-video-id]") ?.getAttribute("data-fixture-control-video-id") ?? "unknown", ); insideHolder.appendChild(globalThis.__artifactInsideOutgoingActions); const watchRoot = currentSection.querySelector( `ytd-watch-flexy[video-id="${expectedVideoId}"], ytd-watch-grid[video-id="${expectedVideoId}"]`, ); if (!watchRoot) throw new Error(`The destination Watch root for ${expectedVideoId} is missing.`); watchRoot.appendChild(insideHolder); delete globalThis.__artifactInsideOutgoingActions; const currentActions = watchRoot.querySelector(":scope > #top-row #top-level-buttons-computed"); const currentLikeButton = currentActions?.querySelector('[data-fixture-role="like"] button'); if (!currentLikeButton) throw new Error(`The destination controls for ${expectedVideoId} have no Like button.`); currentLikeButton.setAttribute("aria-label", `${expectedLikes} likes`); const currentLikeText = currentLikeButton.querySelector("#text, [role='text']"); if (currentLikeText) currentLikeText.textContent = String(expectedLikes); const nativeHydrationStartedAt = Date.now(); const replaced = globalThis.__navigationFixture.replaceCurrentWatchActions({ nativeHydrationDelay, retainOutgoing: true, }); if (!replaced) throw new Error(`The destination action container for ${expectedVideoId} was not replaced.`); const destinationActions = currentSection.querySelector( `#top-level-buttons-computed[data-fixture-watch-actions-replacement="${expectedVideoId}"]`, ); const likeButton = destinationActions?.querySelector('[data-fixture-role="like"] button'); if (nativeHydrationDelay === 0 && !likeButton) { throw new Error(`The replacement controls for ${expectedVideoId} have no Like button.`); } return { destinationReplaced: true, insideBarCount: insideHolder.querySelectorAll(".ryd-tooltip").length, nativeHydrationDelay, nativeHydrationStartedAt, }; }; if (!beforeNavigation) return replace(); delete globalThis.__artifactPreparedDestinationReplacement; document.addEventListener( "yt-navigate-finish", () => { // Capture runs before the extension's navigation listeners, so they never // see hydrated destination controls that the fixture removes afterwards. globalThis.__artifactPreparedDestinationReplacement = replace(); }, { capture: true, once: true }, ); } async function finishSpaDestinationReplacement( page, toVideoId, { nativeHydrationDelay = 0, beforeNavigation = false } = {}, ) { return page.evaluate(replaceSpaDestinationInPage, { expectedLikes: SPA_COUNTS[toVideoId].likes, expectedVideoId: toVideoId, nativeHydrationDelay, beforeNavigation, }); } async function observeSpaDestinationDislikeText(page, videoId) { await page.waitForFunction((expectedVideoId) => { const currentRoot = document.querySelector( `#fixture-page [data-fixture-page-kind="watch"][data-fixture-video-id="${expectedVideoId}"]`, ); return Boolean( currentRoot?.querySelector( `#top-level-buttons-computed[data-fixture-watch-actions-replacement="${expectedVideoId}"] ` + `[data-fixture-control-video-id="${expectedVideoId}"] [data-fixture-role="dislike"]`, ), ); }, videoId); await page.evaluate((expectedVideoId) => { globalThis.__artifactDestinationDislikeTextObserver?.disconnect(); const currentRoot = document.querySelector( `#fixture-page [data-fixture-page-kind="watch"][data-fixture-video-id="${expectedVideoId}"]`, ); const dislike = currentRoot?.querySelector( `#top-level-buttons-computed[data-fixture-watch-actions-replacement="${expectedVideoId}"] ` + `[data-fixture-control-video-id="${expectedVideoId}"] [data-fixture-role="dislike"]`, ); if (!dislike) throw new Error(`The destination Dislike control for ${expectedVideoId} is missing.`); const read = () => (dislike.querySelector("#text, [role='text']")?.textContent ?? "").replace(/\s+/g, " ").trim(); globalThis.__artifactDestinationDislikeTexts = [read()]; globalThis.__artifactDestinationDislikeTextObserver = new MutationObserver(() => { globalThis.__artifactDestinationDislikeTexts.push(read()); }); globalThis.__artifactDestinationDislikeTextObserver.observe(dislike, { characterData: true, childList: true, subtree: true, }); }, videoId); } async function readSpaWatchSnapshot(page, runtime, fromVideoId, toVideoId) { const profile = LIVE_RUNTIME_PROFILES[runtime]; return page.evaluate( ({ fromVideoId: outgoingVideoId, profile: runtimeProfile, toVideoId: destinationVideoId }) => { const normalizedText = (element) => (element?.textContent ?? "").replace(/\s+/g, " ").trim(); const visibleBox = (element) => { const box = element?.getBoundingClientRect(); return box && box.width > 0 && box.height > 0 ? { height: box.height, width: box.width, x: box.x, y: box.y } : null; }; const retainedState = (selector) => { const holder = document.querySelector(selector); return { barCount: holder?.querySelectorAll(runtimeProfile.selectors.rateBar).length ?? -1, containerCount: holder?.querySelectorAll(runtimeProfile.selectors.rateBarContainer).length ?? -1, controlVideoIds: [...(holder?.querySelectorAll("[data-fixture-control-video-id]") ?? [])].map((control) => control.getAttribute("data-fixture-control-video-id"), ), hidden: holder?.hidden === true, present: holder !== null, wrapperCount: holder?.querySelectorAll(".ryd-tooltip").length ?? -1, }; }; const currentRoot = document.querySelector( `#fixture-page [data-fixture-page-kind="watch"][data-fixture-video-id="${destinationVideoId}"]`, ); const actionHost = currentRoot?.querySelector( `#top-level-buttons-computed[data-fixture-watch-actions-replacement="${destinationVideoId}"]`, ); const controls = actionHost?.querySelector(`[data-fixture-control-video-id="${destinationVideoId}"]`); const countElement = controls?.querySelector( '[data-fixture-role="dislike"] #text, [data-fixture-role="dislike"] [role="text"]', ); const wrapper = actionHost?.querySelector(":scope > .ryd-tooltip"); const container = wrapper?.querySelector(runtimeProfile.selectors.rateBarContainer); const fill = container?.querySelector(runtimeProfile.selectors.rateBar); const tooltip = wrapper?.querySelector(runtimeProfile.selectors.tooltipContent); const containerBox = visibleBox(container); const fillBox = visibleBox(fill); const currentVideoId = currentRoot?.querySelector("ytd-watch-flexy")?.getAttribute("video-id") ?? null; const url = new URL(location.href); return { actionHostCount: currentRoot?.querySelectorAll( `#top-level-buttons-computed[data-fixture-watch-actions-replacement="${destinationVideoId}"]`, ).length ?? 0, barOwnedByDestination: Boolean(fill && fill.closest("#top-level-buttons-computed") === actionHost), containerOwnedByDestination: Boolean( container && container.closest("#top-level-buttons-computed") === actionHost, ), count: normalizedText(countElement), currentVideoId, destinationBarCount: actionHost?.querySelectorAll(runtimeProfile.selectors.rateBar).length ?? 0, destinationContainerCount: actionHost?.querySelectorAll(runtimeProfile.selectors.rateBarContainer).length ?? 0, destinationControlCount: actionHost?.querySelectorAll(`[data-fixture-control-video-id="${destinationVideoId}"]`).length ?? 0, destinationWrapperCount: actionHost?.querySelectorAll(":scope > .ryd-tooltip").length ?? 0, fillRatio: containerBox && fillBox ? fillBox.width / containerBox.width : null, globalBarCount: document.querySelectorAll(runtimeProfile.selectors.rateBar).length, globalContainerCount: document.querySelectorAll(runtimeProfile.selectors.rateBarContainer).length, globalWrapperCount: document.querySelectorAll(".ryd-tooltip").length, insideOutgoing: retainedState( `[data-artifact-outgoing-position="inside-current-root"][data-artifact-outgoing-video-id="${outgoingVideoId}"]`, ), retainedDestination: retainedState(`[data-fixture-retained-settling-watch-actions="${destinationVideoId}"]`), retainedBefore: retainedState( `[data-artifact-outgoing-position="before-current-root"][data-artifact-outgoing-video-id="${outgoingVideoId}"]`, ), tooltipText: normalizedText(tooltip), urlVideoId: url.pathname === "/watch" ? url.searchParams.get("v") : null, visibleContainer: containerBox !== null, visibleFill: fillBox !== null, }; }, { fromVideoId, profile, toVideoId }, ); } function isSpaDestinationValid(snapshot, { expectedCount, expectedRatio, fromVideoId, toVideoId }) { const outgoingRetained = [snapshot.retainedBefore, snapshot.insideOutgoing]; const hasNoRydBar = (state) => state.present === true && state.hidden === true && state.wrapperCount === 0 && state.containerCount === 0 && state.barCount === 0; return ( snapshot.urlVideoId === toVideoId && snapshot.currentVideoId === toVideoId && snapshot.actionHostCount === 1 && snapshot.destinationControlCount === 1 && snapshot.destinationWrapperCount === 1 && snapshot.destinationContainerCount === 1 && snapshot.destinationBarCount === 1 && snapshot.globalWrapperCount === 1 && snapshot.globalContainerCount === 1 && snapshot.globalBarCount === 1 && snapshot.barOwnedByDestination === true && snapshot.containerOwnedByDestination === true && snapshot.visibleContainer === true && snapshot.visibleFill === true && snapshot.count === String(expectedCount) && snapshot.tooltipText.includes(`${SPA_COUNTS[toVideoId].likes} / ${expectedCount}`) && Number.isFinite(snapshot.fillRatio) && Math.abs(snapshot.fillRatio - expectedRatio) <= 0.02 && outgoingRetained.every((state) => hasNoRydBar(state) && state.controlVideoIds.includes(fromVideoId)) && hasNoRydBar(snapshot.retainedDestination) && snapshot.retainedDestination.controlVideoIds.includes(toVideoId) ); } async function clickSpaDestinationDislike(page, videoId) { const selector = `#fixture-page [data-fixture-page-kind="watch"][data-fixture-video-id="${videoId}"] ` + `#top-level-buttons-computed[data-fixture-watch-actions-replacement="${videoId}"] ` + `[data-fixture-control-video-id="${videoId}"] [data-fixture-role="dislike"] button`; const buttons = page.locator(selector); assert.equal(await buttons.count(), 1, `Expected exactly one destination Dislike activation target for ${videoId}.`); const button = buttons.first(); assert.equal( await button.isVisible(), true, `The destination Dislike activation target for ${videoId} is not visible.`, ); const ariaPressedBefore = await button.getAttribute("aria-pressed"); await button.click(); return { ariaPressedBefore, selector, videoId }; } async function cloneRenderedSpaControlsAndActivateDislike(page, videoId) { return page.evaluate((expectedVideoId) => { const actionHost = document.querySelector( `#fixture-page [data-fixture-page-kind="watch"][data-fixture-video-id="${expectedVideoId}"] ` + `#top-level-buttons-computed[data-fixture-watch-actions-replacement="${expectedVideoId}"]`, ); if (!actionHost) throw new Error(`The rendered destination action host for ${expectedVideoId} is missing.`); const barSelector = "#ryd-bar, #return-youtube-dislike-bar"; const countSelector = '[data-fixture-role="dislike"] #text, [data-fixture-role="dislike"] [role="text"]'; const visible = (element) => { if (!element?.isConnected) return false; const bounds = element.getBoundingClientRect(); const style = getComputedStyle(element); return ( bounds.width > 0 && bounds.height > 0 && style.display !== "none" && style.visibility !== "hidden" && Number.parseFloat(style.opacity) !== 0 ); }; const countBefore = actionHost.querySelector(countSelector)?.textContent?.trim() ?? null; const barBefore = actionHost.querySelector(barSelector); const buttonBefore = actionHost.querySelector('[data-fixture-role="dislike"] button'); if (!visible(actionHost) || !visible(countBefore === null ? null : actionHost.querySelector(countSelector))) { throw new Error(`The destination count for ${expectedVideoId} is not visibly rendered before cloning.`); } if (!visible(barBefore) || !visible(buttonBefore)) { throw new Error( `The destination button and rate bar for ${expectedVideoId} are not visibly rendered before cloning.`, ); } const replacement = actionHost.cloneNode(true); replacement.setAttribute("data-artifact-cloned-rendered-actions", "true"); actionHost.replaceWith(replacement); const activationTarget = replacement.querySelector('[data-fixture-role="dislike"] button'); const count = replacement.querySelector(countSelector); const bar = replacement.querySelector(barSelector); if (!visible(replacement) || !visible(activationTarget) || !visible(count) || !visible(bar)) { throw new Error(`The cloned destination presentation for ${expectedVideoId} is not visibly complete.`); } const ariaPressedBefore = activationTarget.getAttribute("aria-pressed"); let clickObservedAtDocument = false; document.addEventListener( "click", (event) => { clickObservedAtDocument = event.composedPath().includes(activationTarget); }, { capture: true, once: true }, ); activationTarget.click(); return { ariaDisabled: activationTarget.getAttribute("aria-disabled"), ariaPressedAfterSynchronousClick: activationTarget.getAttribute("aria-pressed"), ariaPressedBefore, barCount: replacement.querySelectorAll(barSelector).length, buttonDisabled: activationTarget.disabled, clickObservedAtDocument, countAfterSynchronousClick: count.textContent?.trim() ?? null, countBefore, countContainerCount: replacement.querySelectorAll(countSelector).length, presentationCloned: replacement !== actionHost && !actionHost.isConnected, videoId: expectedVideoId, }; }, videoId); } function interactionRecordsSince(records, startIndex) { return records .slice(startIndex) .filter( (record) => record.method === "POST" && ["/interact/vote", "/interact/confirmVote"].includes(record.pathname), ); } function readArtifactVoteHandshake(records, startIndex, videoId, value) { const interactions = interactionRecordsSince(records, startIndex); const votes = interactions.filter((record) => record.pathname === "/interact/vote"); const confirmations = interactions.filter((record) => record.pathname === "/interact/confirmVote"); const vote = votes[0] ?? null; const confirmation = confirmations[0] ?? null; const userId = vote?.body?.userId ?? null; return { confirmation: confirmation ? { body: confirmation.body, responded: Number.isFinite(confirmation.respondedAt), responseBody: confirmation.responseBody, responseStatus: confirmation.responseStatus, } : null, confirmationCount: confirmations.length, expectedValue: value, expectedVideoId: videoId, interactionPaths: interactions.map((record) => record.pathname), interactionCount: interactions.length, sharedUserId: typeof userId === "string" && userId.length > 0 && confirmation?.body?.userId === userId ? userId : null, vote: vote ? { body: vote.body, responded: Number.isFinite(vote.respondedAt), responseBody: vote.responseBody, responseStatus: vote.responseStatus, } : null, voteCount: votes.length, }; } function enqueueRecordedSuccessfulVoteResponses(backend) { backend.enqueue("POST", "/interact/vote", (record) => { record.responseBody = ZERO_DIFFICULTY_PUZZLE; record.responseStatus = 200; return { body: record.responseBody }; }); backend.enqueue("POST", "/interact/confirmVote", (record) => { record.responseBody = true; record.responseStatus = 200; return { body: record.responseBody }; }); } function isArtifactVoteHandshakeValid(snapshot) { const isSuccessfulStatus = (status) => Number.isInteger(status) && status >= 200 && status < 300; if ( snapshot.interactionCount !== 2 || snapshot.voteCount !== 1 || snapshot.confirmationCount !== 1 || typeof snapshot.sharedUserId !== "string" || !/^[A-Za-z0-9]{36}$/.test(snapshot.sharedUserId) || snapshot.interactionPaths?.join(",") !== "/interact/vote,/interact/confirmVote" || snapshot.vote?.responded !== true || !isSuccessfulStatus(snapshot.vote?.responseStatus) || snapshot.confirmation?.responded !== true || !isSuccessfulStatus(snapshot.confirmation?.responseStatus) || snapshot.confirmation?.responseBody !== true ) { return false; } return isVoteProtocolBodyPairValid(snapshot.vote?.body, snapshot.confirmation?.body, { value: snapshot.expectedValue, videoId: snapshot.expectedVideoId, }); } function assertSpaStatsTraffic(backend, fromVideoId, toVideoId) { const votes = assertExactSuccessfulVotesTraffic( backend.requests, [fromVideoId, toVideoId], "The shared artifact SPA contract", ); assert.deepEqual( backend.blockedRequests, [], `The SPA scenario attempted unexpected network traffic: ${JSON.stringify(backend.blockedRequests)}`, ); return { fromVideoRequests: votes.filter((request) => request.query.videoId === fromVideoId).length, toVideoRequests: votes.filter((request) => request.query.videoId === toVideoId).length, }; } function assertSpaBackendTraffic(backend, fromVideoId, toVideoId) { const stats = assertSpaStatsTraffic(backend, fromVideoId, toVideoId); assert.equal(backend.requestsFor("POST", "/interact/vote").length, 0, "The read-only SPA scenario submitted a vote."); assert.equal( backend.requestsFor("POST", "/interact/confirmVote").length, 0, "The read-only SPA scenario confirmed a vote.", ); return { ...stats, interactionRequests: 0, }; } function readSpaTraffic(backend) { return backend.requests.map(({ method, pathname, query }) => ({ method, pathname, query })); } async function readWatchDiagnostics(page, runtime, videoId) { const profile = LIVE_RUNTIME_PROFILES[runtime]; return page.evaluate( ({ rateBar, rateBarContainer, videoId: expectedVideoId }) => ({ bodyText: (document.body?.innerText ?? "").replace(/\s+/g, " ").trim().slice(0, 500), currentVideoId: document.querySelector("ytd-watch-flexy")?.getAttribute("video-id") ?? null, dislikeText: document .querySelector("dislike-button-view-model #text, dislike-button-view-model [role='text']") ?.textContent?.trim() ?? null, expectedVideoId, fillPresent: document.querySelector(rateBar) !== null, rateBarPresent: document.querySelector(rateBarContainer) !== null, setStateCalls: globalThis.__rydSetStateCalls ?? 0, }), { ...profile.selectors, videoId }, ); } class HermeticUserscriptArtifactAdapter { constructor({ artifactPath = DEFAULT_USERSCRIPT_ARTIFACT, backendOptions = {}, browserType = chromium, disableVoteSubmission = false, headless = true, } = {}) { this.artifactPath = path.resolve(artifactPath); this.backendOptions = createSharedArtifactBackendOptions(backendOptions); this.browserType = browserType; this.disableVoteSubmission = disableVoteSubmission; this.headless = headless; this.profile = LIVE_RUNTIME_PROFILES.userscript; this.runtime = "userscript"; } async start() { if (!fs.existsSync(this.artifactPath)) throw new Error(`Generated userscript is missing: ${this.artifactPath}`); this.backend = createFakeBackend(this.backendOptions); this.browser = await this.browserType.launch({ headless: this.headless }); this.context = await this.browser.newContext({ serviceWorkers: "block" }); await installGmEnvironment(this.context); await installArtifactRoutes(this.context, this.backend); this.page = await this.context.newPage(); this.pageSignals = await createPageSignalCollector(this.page, this.runtime); } async openWatch(videoId) { await openWatchFixture(this.page, videoId); await injectGeneratedUserscript(this.page, { artifactPath: this.artifactPath, disableVoteSubmission: this.disableVoteSubmission, }); } async openSpaWatch(videoId) { await openNavigationFixture(this.page, { pageKind: "watch", videoId }); await injectGeneratedUserscript(this.page, { artifactPath: this.artifactPath, disableVoteSubmission: this.disableVoteSubmission, }); } async navigateSpaWatch(fromVideoId, toVideoId) { const outgoing = await prepareSpaOutgoingControls(this.page, fromVideoId); await this.page.locator("#watch-related").click(); const destination = await finishSpaDestinationReplacement(this.page, toVideoId); return { destination, outgoing }; } deferNextStatsRequest() { return this.backend.defer("GET", "/votes"); } async navigateSpaWatchWhilePending(fromVideoId, toVideoId) { const outgoing = await preparePendingSpaOutgoingControls(this.page, fromVideoId); await this.page.locator("#watch-related").click(); const destination = await finishSpaDestinationReplacement(this.page, toVideoId); await observeSpaDestinationDislikeText(this.page, toVideoId); return { destination, outgoing }; } async readDestinationDislikeTextHistory() { return this.page.evaluate(() => [...(globalThis.__artifactDestinationDislikeTexts ?? [])]); } deferInteractionResponse(pathname) { return this.backend.defer("POST", pathname); } enqueueInteractionResponse(pathname, response) { this.backend.enqueue("POST", pathname, response); } async setVoteSubmissionDisabled(disabled) { this.disableVoteSubmission = disabled === true; } readInteractionRecords() { return this.backend.requests; } readStatsRequestTimings() { return this.backend.requestsFor("GET", "/votes").map(({ at, query, respondedAt }) => ({ at, query: { ...query }, respondedAt, })); } async activateSpaDislike(videoId) { const interactionStartIndex = this.backend.requests.length; const activation = await clickSpaDestinationDislike(this.page, videoId); return { ...activation, interactionStartIndex }; } async activateClonedSpaDislike(videoId) { const interactionStartIndex = this.backend.requests.length; const activation = await cloneRenderedSpaControlsAndActivateDislike(this.page, videoId); return { ...activation, interactionStartIndex }; } async readSpaVoteHandshake(interactionStartIndex, videoId, value) { return readArtifactVoteHandshake(this.backend.requests, interactionStartIndex, videoId, value); } async assertSpaVoteNetwork(fromVideoId, toVideoId, interactionStartIndex) { const stats = assertSpaStatsTraffic(this.backend, fromVideoId, toVideoId); const requestsAfterActivation = this.backend.requests.slice(interactionStartIndex); assert.ok( requestsAfterActivation.every( (record) => record.method === "POST" && ["/interact/vote", "/interact/confirmVote"].includes(record.pathname), ), `The userscript made unexpected requests after activation: ${JSON.stringify( readSpaTraffic({ requests: requestsAfterActivation, }), )}`, ); return stats; } async readSpaWatchSnapshot(fromVideoId, toVideoId) { return readSpaWatchSnapshot(this.page, this.runtime, fromVideoId, toVideoId); } async assertSpaNetwork(fromVideoId, toVideoId) { return assertSpaBackendTraffic(this.backend, fromVideoId, toVideoId); } async readSpaTraffic() { return { routedRequests: readSpaTraffic(this.backend) }; } async waitForWatchResult(videoId) { let result; try { result = await waitForWatchResult(this.page, this.runtime, videoId); } catch (error) { const diagnostics = { page: await readWatchDiagnostics(this.page, this.runtime, videoId), pageSignals: this.pageSignals.snapshot(), productionOriginRequests: this.backend.requests, }; throw new Error(`${error.message}\nUserscript artifact diagnostics: ${JSON.stringify(diagnostics, null, 2)}`, { cause: error, }); } assert.equal(this.backend.blockedRequests.length, 0, "The userscript attempted unexpected network traffic."); return result; } async assertNoPageSignals(scenarioId) { return this.pageSignals.assertClean(scenarioId); } async close() { await this.context?.close(); await this.browser?.close(); } } class HermeticExtensionArtifactAdapter { constructor({ apiServer, artifactDirectory = DEFAULT_EXTENSION_ARTIFACT, backendOptions = {}, browserType = chromium, channel = "chromium", contextOptions = {}, headless = true, selectorResponse = { body: { rateBar: { oldDesignActions: ["#top-level-buttons-computed"] } } }, } = {}) { if (!apiServer?.origin) throw new TypeError("The extension adapter requires a running hermetic API server."); this.apiServer = apiServer; this.artifactDirectory = path.resolve(artifactDirectory); this.backendOptions = createSharedArtifactBackendOptions(backendOptions); this.browserType = browserType; this.channel = channel; this.contextOptions = contextOptions; this.headless = headless; this.selectorResponse = selectorResponse; this.profile = LIVE_RUNTIME_PROFILES.extension; this.runtime = "extension"; } async start() { this.artifactContract = readGeneratedMv3Contract(this.artifactDirectory); this.backend = createFakeBackend(this.backendOptions); if (this.selectorResponse !== null) { this.backend.enqueue("GET", "/configs/selectors", this.selectorResponse); } this.apiRecordStart = this.apiServer.records.length; this.preparedArtifact = prepareHermeticExtensionArtifact(this.artifactDirectory, this.apiServer.origin); this.profileDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "ryd-mv3-profile-")); const extensionPath = this.preparedArtifact.extensionDirectory; this.context = await this.browserType.launchPersistentContext(this.profileDirectory, { ...this.contextOptions, args: [ `--disable-extensions-except=${extensionPath}`, `--load-extension=${extensionPath}`, "--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE localhost, EXCLUDE 127.0.0.1", "--no-first-run", ], channel: this.channel, headless: this.headless, serviceWorkers: "allow", }); await installArtifactRoutes(this.context, this.backend, { passthroughOrigin: this.apiServer.origin }); const isArtifactWorker = (worker) => new URL(worker.url()).pathname === this.artifactContract.serviceWorkerPath; this.worker = this.context.serviceWorkers().find(isArtifactWorker); if (!this.worker) { this.worker = await this.context.waitForEvent("serviceworker", { predicate: isArtifactWorker, timeout: 15_000, }); } const workerUrl = new URL(this.worker.url()); assert.equal(workerUrl.protocol, "chrome-extension:", "The real MV3 background worker did not start."); assert.match(workerUrl.hostname, /^[a-p]{32}$/, "The MV3 worker has no valid generated extension ID."); assert.equal( workerUrl.pathname, this.artifactContract.serviceWorkerPath, "Chromium started a different extension worker than the generated artifact declares.", ); this.extensionId = workerUrl.hostname; this.workerSignals = createWorkerSignalCollector(this.worker, this.apiServer); this.page = await this.context.newPage(); const pageSignals = await createPageSignalCollector(this.page, this.runtime); this.pageSignals = combineExtensionSignalCollectors(pageSignals, this.workerSignals); } async openWatch(videoId) { await openWatchFixture(this.page, videoId); } async setPremiumTeaserHidden(hidden) { const stored = await this.worker.evaluate(async (value) => { // Wait for the background's initial default write before applying the // scenario preference; otherwise its pending write can replace our value. await new Promise((resolve, reject) => { const finish = (error) => { clearTimeout(timeout); chrome.storage.onChanged.removeListener(changed); if (error) reject(error); else resolve(); }; const changed = (changes, area) => { if (area === "sync" && changes.hidePremiumTeaser?.newValue !== undefined) finish(); }; const timeout = setTimeout( () => finish(new Error("The extension did not initialize hidePremiumTeaser.")), 2_000, ); chrome.storage.onChanged.addListener(changed); chrome.storage.sync.get(["hidePremiumTeaser"]).then((state) => { if (state.hidePremiumTeaser !== undefined) finish(); }, finish); }); await chrome.storage.sync.set({ hidePremiumTeaser: value }); return (await chrome.storage.sync.get(["hidePremiumTeaser"])).hidePremiumTeaser; }, hidden === true); assert.equal(stored, hidden === true, "The premium teaser preference was not stored."); } async openSpaWatch(videoId) { await openNavigationFixture(this.page, { pageKind: "watch", videoId }); } async navigateSpaWatch(fromVideoId, toVideoId) { const outgoing = await prepareSpaOutgoingControls(this.page, fromVideoId); await this.page.locator("#watch-related").click(); const destination = await finishSpaDestinationReplacement(this.page, toVideoId, { nativeHydrationDelay: ARTIFACT_NATIVE_HYDRATION_DELAY_MS, }); return { destination, outgoing }; } deferNextStatsRequest() { return this.backend.defer("GET", "/votes"); } async navigateSpaWatchWhilePending(fromVideoId, toVideoId) { const outgoing = await preparePendingSpaOutgoingControls(this.page, fromVideoId); await finishSpaDestinationReplacement(this.page, toVideoId, { beforeNavigation: true, nativeHydrationDelay: ARTIFACT_NATIVE_HYDRATION_DELAY_MS, }); await this.page.locator("#watch-related").click(); const destination = await this.page.evaluate(() => { const result = globalThis.__artifactPreparedDestinationReplacement; delete globalThis.__artifactPreparedDestinationReplacement; if (!result) throw new Error("The destination controls were not prepared during navigation."); return result; }); await observeSpaDestinationDislikeText(this.page, toVideoId); return { destination, outgoing }; } deferInteractionResponse(pathname) { return this.apiServer.defer("POST", pathname); } enqueueInteractionResponse(pathname, response) { this.apiServer.enqueue("POST", pathname, response); } readInteractionRecords() { return this.apiServer.records.slice(this.apiRecordStart); } async setVoteSubmissionDisabled(disabled) { await this.worker.evaluate(async (value) => { const deadline = Date.now() + 2_000; while ((await chrome.storage.sync.get(["disableVoteSubmission"])).disableVoteSubmission === undefined) { if (Date.now() >= deadline) throw new Error("The extension did not initialize disableVoteSubmission."); await new Promise((resolve) => setTimeout(resolve, 20)); } await chrome.storage.sync.set({ disableVoteSubmission: value }); const stored = (await chrome.storage.sync.get(["disableVoteSubmission"])).disableVoteSubmission; if (stored !== value) throw new Error("The extension did not persist disableVoteSubmission."); }, disabled === true); } async readDestinationDislikeTextHistory() { return this.page.evaluate(() => [...(globalThis.__artifactDestinationDislikeTexts ?? [])]); } readStatsRequestTimings() { return this.backend.requestsFor("GET", "/votes").map(({ at, query, respondedAt }) => ({ at, query: { ...query }, respondedAt, })); } async activateSpaDislike(videoId) { const interactionStartIndex = this.apiServer.records.length; const activation = await clickSpaDestinationDislike(this.page, videoId); return { ...activation, interactionStartIndex }; } async activateClonedSpaDislike(videoId) { const interactionStartIndex = this.apiServer.records.length; const activation = await cloneRenderedSpaControlsAndActivateDislike(this.page, videoId); return { ...activation, interactionStartIndex }; } async readSpaVoteHandshake(interactionStartIndex, videoId, value) { return readArtifactVoteHandshake(this.apiServer.records, interactionStartIndex, videoId, value); } async assertSpaVoteNetwork(fromVideoId, toVideoId, interactionStartIndex) { const stats = assertSpaStatsTraffic(this.backend, fromVideoId, toVideoId); assert.equal( this.backend.requestsFor("POST", "/interact/vote").length, 0, "The extension content script bypassed its background vote transport.", ); assert.equal( this.backend.requestsFor("POST", "/interact/confirmVote").length, 0, "The extension content script bypassed its background confirmation transport.", ); const backgroundAfterActivation = this.apiServer.records .slice(interactionStartIndex) .filter((record) => record.method !== "OPTIONS"); assert.ok( backgroundAfterActivation.every( (record) => record.method === "POST" && ["/interact/vote", "/interact/confirmVote"].includes(record.pathname), ), `The extension background made unexpected requests after activation: ${JSON.stringify(backgroundAfterActivation)}`, ); assert.equal(this.apiServer.unexpectedRequests.length, 0, "The extension made an unexpected test-server request."); return stats; } async readSpaWatchSnapshot(fromVideoId, toVideoId) { return readSpaWatchSnapshot(this.page, this.runtime, fromVideoId, toVideoId); } async assertSpaNetwork(fromVideoId, toVideoId) { const traffic = assertSpaBackendTraffic(this.backend, fromVideoId, toVideoId); assert.equal(this.apiServer.unexpectedRequests.length, 0, "The extension made an unexpected test-server request."); return traffic; } async readSpaTraffic() { return { backgroundRequests: this.apiServer.records.map(({ method, pathname, query }) => ({ method, pathname, query })), routedRequests: readSpaTraffic(this.backend), }; } async waitForWatchResult(videoId) { let result; try { result = await waitForWatchResult(this.page, this.runtime, videoId); } catch (error) { const diagnostics = { apiRecords: this.apiServer.records, page: await readWatchDiagnostics(this.page, this.runtime, videoId), pageSignals: this.pageSignals.snapshot(), productionOriginRequests: this.backend.requests, unexpectedRequests: this.apiServer.unexpectedRequests, workerUrl: this.worker.url(), }; throw new Error(`${error.message}\nExtension artifact diagnostics: ${JSON.stringify(diagnostics, null, 2)}`, { cause: error, }); } assert.deepEqual( this.backend.blockedRequests, [], `The extension attempted unexpected network traffic: ${JSON.stringify(this.backend.blockedRequests)}`, ); const unexpectedRoutedRequests = this.backend.requests.filter( (request) => request.method !== "GET" || !["/configs/selectors", "/votes"].includes(request.pathname), ); assert.deepEqual( unexpectedRoutedRequests, [], "The extension content script made an unexpected production-origin request.", ); assert.equal(this.apiServer.unexpectedRequests.length, 0, "The extension made an unexpected test-server request."); return { ...result, workerUrl: this.worker.url() }; } async assertNoPageSignals(scenarioId) { return this.pageSignals.assertClean(scenarioId); } async close() { await this.context?.close(); removeOwnedTemporaryDirectory(this.profileDirectory, "ryd-mv3-profile-"); removeOwnedTemporaryDirectory(this.preparedArtifact?.temporaryRoot, "ryd-mv3-e2e-"); } } async function runArtifactWatchRenderScenario(adapter, { videoId = VIDEO_A } = {}) { assert.ok( SHARED_LIVE_SCENARIO_IDS.includes(ARTIFACT_SMOKE_SCENARIO_ID), `${ARTIFACT_SMOKE_SCENARIO_ID} must remain in the shared scenario catalog.`, ); assert.ok( ["extension", "userscript"].includes(adapter?.runtime), "A supported artifact runtime adapter is required.", ); for (const method of ["start", "openWatch", "waitForWatchResult", "assertNoPageSignals", "close"]) { assert.equal( typeof adapter[method], "function", `The ${adapter.runtime} artifact adapter must implement ${method}().`, ); } try { await adapter.start(); await adapter.openWatch(videoId); const result = await adapter.waitForWatchResult(videoId); assert.equal(result.videoId, videoId); assert.equal( result.ownedByExpectedWatch, true, `${adapter.runtime} rendered the result outside the expected watch root.`, ); assert.equal( result.sameActionSurface, true, `${adapter.runtime} rendered the count and bar in different action surfaces.`, ); assert.equal(result.actionSurfaceVisible, true, `${adapter.runtime} rendered into a hidden watch action surface.`); assert.equal(result.countVisible, true, `${adapter.runtime} rendered a hidden dislike count.`); assert.equal(result.rateBarVisible, true, `${adapter.runtime} did not render a visible watch ratio bar.`); assert.equal(result.fillVisible, true, `${adapter.runtime} did not render a visible watch ratio fill.`); assert.match(result.count, /\d/, `${adapter.runtime} did not render a numeric dislike count.`); await adapter.assertNoPageSignals(ARTIFACT_SMOKE_SCENARIO_ID); return { ...result, runtime: adapter.runtime, scenarioId: ARTIFACT_SMOKE_SCENARIO_ID }; } finally { await adapter.close(); } } function assertArtifactRuntimeAdapter(adapter, methods) { assert.ok( ["extension", "userscript"].includes(adapter?.runtime), "A supported artifact runtime adapter is required.", ); for (const method of methods) { assert.equal( typeof adapter[method], "function", `The ${adapter.runtime} artifact adapter must implement ${method}().`, ); } } function createArtifactSpaScenarioConfiguration({ fromVideoId = VIDEO_A, intervalMs = 50, maxFirstValidMs = 1_000, stableForMs = 300, stabilityDurationMs = 1_000, timeoutMs = 15_000, toVideoId = VIDEO_B, } = {}) { const fromCounts = SPA_COUNTS[fromVideoId]; const toCounts = SPA_COUNTS[toVideoId]; if (!fromCounts || !toCounts) { throw new TypeError("The SPA scenario requires configured non-proportional video counts."); } if (!Number.isFinite(maxFirstValidMs) || maxFirstValidMs < 0) { throw new TypeError("The SPA first-valid latency budget must be a non-negative finite number."); } const fromRatio = fromCounts.likes / (fromCounts.likes + fromCounts.dislikes); const toRatio = toCounts.likes / (toCounts.likes + toCounts.dislikes); assert.notEqual(fromRatio, toRatio, "The A/B fixture ratios must not be proportional."); return { fromCounts, fromRatio, fromVideoId, intervalMs, maxFirstValidMs, stabilityDurationMs, stableForMs, timeoutMs, toCounts, toRatio, toVideoId, validityOptions: { expectedCount: toCounts.dislikes, expectedRatio: toRatio, fromVideoId, toVideoId, }, }; } async function runArtifactWatchSpaSetup(adapter, configuration) { const { fromCounts, fromRatio, fromVideoId, intervalMs, maxFirstValidMs, stabilityDurationMs, stableForMs, timeoutMs, toVideoId, validityOptions, } = configuration; await adapter.openSpaWatch(fromVideoId); const initial = await adapter.waitForWatchResult(fromVideoId); assert.equal(initial.videoId, fromVideoId, "The outgoing watch fixture reported the wrong video ID."); assert.equal(initial.count, String(fromCounts.dislikes), "The outgoing watch rendered the wrong dislike count."); assert.ok( Number.isFinite(initial.fillRatio) && Math.abs(initial.fillRatio - fromRatio) <= 0.02, `The outgoing watch rendered ratio ${initial.fillRatio}; expected ${fromRatio}.`, ); const mutation = await adapter.navigateSpaWatch(fromVideoId, toVideoId); assert.equal(mutation.outgoing.beforeBarCount, 1, "The retained outgoing fixture had no initialized A bar."); assert.equal(mutation.destination.destinationReplaced, true, "The destination actions were not replaced."); let readiness; try { readiness = await waitForStableInvariant({ intervalMs, isValid: (snapshot) => isSpaDestinationValid(snapshot, validityOptions), label: `${adapter.runtime} destination watch ownership`, read: () => adapter.readSpaWatchSnapshot(fromVideoId, toVideoId), stableForMs, timeoutMs, }); } catch (error) { if (typeof adapter.readSpaTraffic === "function") { error.message += ` Traffic: ${JSON.stringify(await adapter.readSpaTraffic())}`; } throw error; } assert.ok( readiness.firstValidMs <= maxFirstValidMs, `${adapter.runtime} destination watch first became valid after ${readiness.firstValidMs}ms; ` + `the budget is ${maxFirstValidMs}ms.`, ); if (mutation.destination.nativeHydrationDelay > 0) { assert.ok(readiness.invalidSamples > 0, `${adapter.runtime} became valid before delayed controls hydrated.`); assert.ok( readiness.firstValidMs >= mutation.destination.nativeHydrationDelay - intervalMs * 2, `${adapter.runtime} became valid after ${readiness.firstValidMs}ms, before the ` + `${mutation.destination.nativeHydrationDelay}ms native-control delay elapsed.`, ); } const stability = await assertInvariantContinuously({ durationMs: stabilityDurationMs, intervalMs, isValid: (snapshot) => isSpaDestinationValid(snapshot, validityOptions), label: `${adapter.runtime} settled watch SPA UI`, read: () => adapter.readSpaWatchSnapshot(fromVideoId, toVideoId), }); const destination = readiness.value; return { destination: { count: destination.count, fillRatio: destination.fillRatio, globalBarCount: destination.globalBarCount, retainedOutgoingBars: destination.retainedBefore.barCount + destination.insideOutgoing.barCount, tooltipText: destination.tooltipText, videoId: destination.currentVideoId, }, initial: { count: initial.count, fillRatio: initial.fillRatio, videoId: initial.videoId }, readiness: { firstValidMs: readiness.firstValidMs, invalidSamples: readiness.invalidSamples, maxFirstValidMs, sampleCount: readiness.sampleCount, stableForMs: readiness.stableForMs, }, stability: { elapsedMs: stability.elapsedMs, sampleCount: stability.sampleCount }, }; } async function runArtifactWatchSpaScenario(adapter, options = {}) { assert.ok( SHARED_ARTIFACT_SCENARIO_IDS.includes(ARTIFACT_WATCH_SPA_SCENARIO_ID), `${ARTIFACT_WATCH_SPA_SCENARIO_ID} must remain in the shared artifact scenario catalog.`, ); assertArtifactRuntimeAdapter(adapter, [ "assertSpaNetwork", "assertNoPageSignals", "close", "navigateSpaWatch", "openSpaWatch", "readSpaWatchSnapshot", "start", "waitForWatchResult", ]); const configuration = createArtifactSpaScenarioConfiguration(options); try { await adapter.start(); const result = await runArtifactWatchSpaSetup(adapter, configuration); const traffic = await adapter.assertSpaNetwork(configuration.fromVideoId, configuration.toVideoId); await adapter.assertNoPageSignals(ARTIFACT_WATCH_SPA_SCENARIO_ID); return { ...result, runtime: adapter.runtime, scenarioId: ARTIFACT_WATCH_SPA_SCENARIO_ID, traffic, }; } finally { await adapter.close(); } } async function runArtifactWatchSpaVoteContract( adapter, { activationMethod, expectClonedPresentation = false, handshakeStableForMs = 1_000, handshakeTimeoutMs = 10_000, scenarioId, voteValue = -1, ...spaOptions }, ) { assert.ok( SHARED_ARTIFACT_SCENARIO_IDS.includes(scenarioId), `${scenarioId} must remain in the shared artifact scenario catalog.`, ); assertArtifactRuntimeAdapter(adapter, [ activationMethod, "assertNoPageSignals", "assertSpaVoteNetwork", "close", "navigateSpaWatch", "openSpaWatch", "readSpaVoteHandshake", "readSpaWatchSnapshot", "start", "waitForWatchResult", ]); assert.equal(voteValue, -1, "The shared post-SPA activation scenario must submit a dislike value of -1."); const configuration = createArtifactSpaScenarioConfiguration(spaOptions); try { await adapter.start(); const result = await runArtifactWatchSpaSetup(adapter, configuration); const activation = await adapter[activationMethod](configuration.toVideoId); assert.ok( Number.isInteger(activation.interactionStartIndex) && activation.interactionStartIndex >= 0, "The adapter did not return a valid interaction record boundary.", ); assert.equal(activation.videoId, configuration.toVideoId, "The adapter activated the wrong destination video."); if (expectClonedPresentation) { assert.equal(activation.presentationCloned, true, "The initialized destination presentation was not cloned."); assert.equal(activation.barCount, 1, "The cloned destination did not visibly retain exactly one rate bar."); assert.equal( activation.countContainerCount, 1, "The cloned destination did not visibly retain exactly one Dislike count.", ); assert.equal( activation.countBefore, String(configuration.toCounts.dislikes), "The cloned destination did not start from the rendered destination Dislike count.", ); assert.equal( activation.countAfterSynchronousClick, String(configuration.toCounts.dislikes + 1), `The first immediate click on cloned initialized controls was lost: ${JSON.stringify(activation)}`, ); } let handshake; try { handshake = await waitForStableInvariant({ intervalMs: configuration.intervalMs, isValid: isArtifactVoteHandshakeValid, label: `${adapter.runtime} post-SPA dislike handshake`, read: () => adapter.readSpaVoteHandshake(activation.interactionStartIndex, configuration.toVideoId, voteValue), stableForMs: handshakeStableForMs, timeoutMs: handshakeTimeoutMs, }); } catch (error) { if (typeof adapter.readSpaTraffic === "function") { error.message += ` Traffic: ${JSON.stringify(await adapter.readSpaTraffic())}`; } throw error; } const handshakeSnapshot = handshake.value; const network = await adapter.assertSpaVoteNetwork( configuration.fromVideoId, configuration.toVideoId, activation.interactionStartIndex, ); await adapter.assertNoPageSignals(scenarioId); return { ...result, activation: { ariaPressedBefore: activation.ariaPressedBefore, presentationCloned: activation.presentationCloned === true, videoId: activation.videoId, }, handshake: { confirmationRequests: handshakeSnapshot.confirmationCount, confirmationStatus: handshakeSnapshot.confirmation.responseStatus, confirmed: handshakeSnapshot.confirmation.responseBody === true, firstValidMs: handshake.firstValidMs, interactionRequests: handshakeSnapshot.interactionCount, sampleCount: handshake.sampleCount, stableForMs: handshake.stableForMs, userId: handshakeSnapshot.sharedUserId, value: voteValue, videoId: configuration.toVideoId, voteResponded: handshakeSnapshot.vote.responded, voteRequests: handshakeSnapshot.voteCount, voteStatus: handshakeSnapshot.vote.responseStatus, }, runtime: adapter.runtime, scenarioId, traffic: { ...network, confirmationRequests: handshakeSnapshot.confirmationCount, interactionRequests: handshakeSnapshot.interactionCount, voteRequests: handshakeSnapshot.voteCount, }, }; } finally { await adapter.close(); } } async function runArtifactWatchSpaVoteScenario(adapter, options = {}) { return runArtifactWatchSpaVoteContract(adapter, { ...options, activationMethod: "activateSpaDislike", scenarioId: ARTIFACT_WATCH_SPA_VOTE_SCENARIO_ID, }); } async function runArtifactWatchSpaClonedVoteScenario(adapter, options = {}) { return runArtifactWatchSpaVoteContract(adapter, { ...options, activationMethod: "activateClonedSpaDislike", expectClonedPresentation: true, scenarioId: ARTIFACT_WATCH_SPA_CLONED_VOTE_SCENARIO_ID, }); } async function runExtensionDelayedOutgoingFailureScenario( adapter, { fromVideoId = VIDEO_A, maxDestinationRequestReadyDelayMs = 250, requestTimeoutMs = 5_000, toVideoId = VIDEO_B, } = {}, ) { assert.equal(adapter?.runtime, "extension", "The delayed outgoing failure scenario requires the extension runtime."); assertArtifactRuntimeAdapter(adapter, [ "assertNoPageSignals", "assertSpaNetwork", "close", "deferNextStatsRequest", "navigateSpaWatchWhilePending", "openSpaWatch", "readDestinationDislikeTextHistory", "readSpaWatchSnapshot", "readStatsRequestTimings", "setPremiumTeaserHidden", "start", "waitForWatchResult", ]); const configuration = createArtifactSpaScenarioConfiguration({ fromVideoId, toVideoId }); try { await adapter.start(); // This scenario times native-controls initialization. The optional teaser can // fetch counts before hydration and share that request with the main renderer, // so hide it before loading either video to keep /votes timing attributable. // The shared SPA scenarios retain the default teaser-enabled configuration. await adapter.setPremiumTeaserHidden(true); const outgoingRequest = adapter.deferNextStatsRequest(); await adapter.openSpaWatch(fromVideoId); let requestTimeout; try { await Promise.race([ outgoingRequest.seen, new Promise((resolve, reject) => { requestTimeout = setTimeout( () => reject(new Error(`The outgoing ${fromVideoId} stats request was not observed.`)), requestTimeoutMs, ); }), ]); } finally { clearTimeout(requestTimeout); } const mutation = await adapter.navigateSpaWatchWhilePending(fromVideoId, toVideoId); assert.equal(mutation.outgoing.beforeBarCount, 0, "The intentionally pending outgoing request rendered a bar."); assert.equal(mutation.destination.destinationReplaced, true, "The destination actions were not replaced."); const releasedAt = Date.now(); outgoingRequest.release({ body: "{", status: 200 }); const readiness = await waitForStableInvariant({ intervalMs: configuration.intervalMs, isValid: (snapshot) => isSpaDestinationValid(snapshot, configuration.validityOptions), label: "extension destination after delayed outgoing failure", read: () => adapter.readSpaWatchSnapshot(fromVideoId, toVideoId), stableForMs: configuration.stableForMs, timeoutMs: configuration.timeoutMs, }); const destination = readiness.value; const textHistory = await adapter.readDestinationDislikeTextHistory(); assert.ok(textHistory.length >= 1, "The destination dislike text history was not recorded."); assert.ok( textHistory.every((text) => text === "" || text === String(configuration.toCounts.dislikes)), `The outgoing failure wrote into the destination dislike UI: ${JSON.stringify(textHistory)}`, ); const timings = adapter.readStatsRequestTimings(); const destinationRequests = timings.filter((record) => record.query.videoId === toVideoId); assert.equal(destinationRequests.length, 1, `Expected one destination stats request for ${toVideoId}.`); const destinationRequestDelayMs = destinationRequests[0].at - releasedAt; const destinationControlsReadyAt = mutation.destination.nativeHydrationStartedAt + mutation.destination.nativeHydrationDelay; const destinationRequestReadyDelayMs = destinationRequests[0].at - destinationControlsReadyAt; assert.ok( destinationRequestReadyDelayMs >= 0, `The destination stats request started ${-destinationRequestReadyDelayMs}ms before its controls hydrated.`, ); assert.ok( destinationRequestReadyDelayMs <= maxDestinationRequestReadyDelayMs, `The queued destination initialization waited ${destinationRequestReadyDelayMs}ms after its controls hydrated; ` + `the budget is ${maxDestinationRequestReadyDelayMs}ms.`, ); const traffic = await adapter.assertSpaNetwork(fromVideoId, toVideoId); await adapter.assertNoPageSignals(ARTIFACT_EXTENSION_DELAYED_FAILURE_SCENARIO_ID); return { destination: { count: destination.count, fillRatio: destination.fillRatio, videoId: destination.currentVideoId, }, destinationRequestDelayMs, destinationRequestReadyDelayMs, runtime: adapter.runtime, scenarioId: ARTIFACT_EXTENSION_DELAYED_FAILURE_SCENARIO_ID, textHistory, traffic, }; } finally { await adapter.close(); } } function createCataloguedArtifactAdapter(runtime, scenarioId, apiServer) { const backendOptions = scenarioId === ARTIFACT_SMOKE_SCENARIO_ID ? undefined : { countsByVideo: SPA_COUNTS }; const options = backendOptions ? { backendOptions } : {}; if (runtime === "userscript") return new HermeticUserscriptArtifactAdapter(options); if (runtime === "extension") return new HermeticExtensionArtifactAdapter({ ...options, apiServer }); throw new TypeError(`Unsupported catalogued artifact runtime: ${runtime}`); } function runCataloguedArtifactScenario(scenarioId, adapter) { switch (scenarioId) { case ARTIFACT_SMOKE_SCENARIO_ID: return runArtifactWatchRenderScenario(adapter); case ARTIFACT_WATCH_SPA_SCENARIO_ID: return runArtifactWatchSpaScenario(adapter); case ARTIFACT_WATCH_SPA_VOTE_SCENARIO_ID: return runArtifactWatchSpaVoteScenario(adapter); case ARTIFACT_WATCH_SPA_CLONED_VOTE_SCENARIO_ID: return runArtifactWatchSpaClonedVoteScenario(adapter); case ARTIFACT_EXTENSION_DELAYED_FAILURE_SCENARIO_ID: return runExtensionDelayedOutgoingFailureScenario(adapter); default: throw new TypeError(`Unknown catalogued artifact scenario: ${scenarioId}`); } } async function runBothArtifactSmokes() { const apiServer = await startHermeticApiServer(); try { const results = []; for (const { runtime, scenarioId } of createArtifactBrowserScenarioPlan()) { const adapter = createCataloguedArtifactAdapter(runtime, scenarioId, apiServer); results.push(await runCataloguedArtifactScenario(scenarioId, adapter)); } return results; } finally { await apiServer.close(); } } if (require.main === module) { runBothArtifactSmokes() .then((results) => process.stdout.write(`${JSON.stringify(results, null, 2)}\n`)) .catch((error) => { process.stderr.write(`${error.stack ?? error.message}\n`); process.exitCode = 1; }); } module.exports = { ARTIFACT_BROWSER_SCENARIO_CATALOG, ARTIFACT_EXTENSION_DELAYED_FAILURE_SCENARIO_ID, ARTIFACT_SMOKE_SCENARIO_ID, ARTIFACT_WATCH_SPA_CLONED_VOTE_SCENARIO_ID, ARTIFACT_WATCH_SPA_SCENARIO_ID, ARTIFACT_WATCH_SPA_VOTE_SCENARIO_ID, HermeticExtensionArtifactAdapter, HermeticUserscriptArtifactAdapter, SHARED_ARTIFACT_SCENARIO_IDS, SHARED_ARTIFACT_RUNTIMES, SPA_COUNTS, assertLoopbackOrigin, assertArtifactBrowserScenarioCatalog, createArtifactBrowserScenarioPlan, createPageSignalCollector, createSharedArtifactBackendOptions, createWorkerSignalCollector, isArtifactVoteHandshakeValid, isSpaDestinationValid, prepareHermeticExtensionArtifact, readGeneratedMv3Contract, readArtifactVoteHandshake, runArtifactWatchRenderScenario, runArtifactWatchSpaClonedVoteScenario, runArtifactWatchSpaScenario, runArtifactWatchSpaVoteScenario, runBothArtifactSmokes, runExtensionDelayedOutgoingFailureScenario, startHermeticApiServer, };