Files
return-youtube-dislike/Extensions/e2e/hermetic-artifact-smoke.js
T
2026-09-07 19:10:02 +02:00

2266 lines
89 KiB
JavaScript

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,
};