Fix Shorts support and add userscript voting

Harden Shorts controls, reaction state, and navigation; share voting logic
with the extension; improve ratio-bar rendering; and add comprehensive Jest,
Playwright, artifact, and live-browser regression coverage.
This commit is contained in:
Anarios
2026-09-01 23:41:28 +02:00
parent 4e6dfd1977
commit 3657de9b2c
74 changed files with 23328 additions and 588 deletions
@@ -0,0 +1,172 @@
/**
* @jest-environment jsdom
*/
const path = require("node:path");
const {
LiveRunDiagnostics,
UNHANDLED_REJECTION_PREFIX,
diagnosticApiUrl,
readLivePageState,
runLoggedStage,
} = require("../e2e/live/live-diagnostics");
class FakeEmitter {
constructor() {
this.listeners = new Map();
}
emit(event, value) {
for (const listener of this.listeners.get(event) ?? []) listener(value);
}
off(event, listener) {
this.listeners.get(event)?.delete(listener);
}
on(event, listener) {
if (!this.listeners.has(event)) this.listeners.set(event, new Set());
this.listeners.get(event).add(listener);
}
}
function visibleRect() {
return { bottom: 100, height: 90, left: 10, right: 60, top: 10, width: 50 };
}
describe("live interactive diagnostics", () => {
test("captures runtime and Shorts ownership state without dumping page HTML", () => {
document.documentElement.setAttribute("data-ryd-userscript-version", "3.2.0");
document.body.innerHTML = `
<ytd-reel-video-renderer video-id="abcdefghijk" is-active>
<a href="/shorts/abcdefghijk">Short</a>
<reel-action-bar-view-model>
<like-button-view-model></like-button-view-model>
<button-view-model data-ryd-synthetic-shorts-dislike data-ryd-video-id="abcdefghijk">
<button aria-pressed="false"></button><span>123</span>
</button-view-model>
</reel-action-bar-view-model>
</ytd-reel-video-renderer>
`;
for (const element of document.querySelectorAll("*")) element.getBoundingClientRect = visibleRect;
const state = readLivePageState();
expect(state.runtimeMarkers).toEqual({
extension: null,
extensionBuild: null,
userscript: "3.2.0",
userscriptBuild: null,
});
expect(state.renderers).toEqual([
expect.objectContaining({ actionBars: 1, syntheticControls: 1, videoId: "abcdefghijk", visible: true }),
]);
expect(state.renderers[0].links).toEqual([{ href: "/shorts/abcdefghijk", visible: true }]);
expect(state.actionBars).toEqual([
expect.objectContaining({ nativeLikes: 1, syntheticControls: 1, videoId: "abcdefghijk", visible: true }),
]);
expect(state.syntheticControls).toEqual([
expect.objectContaining({ ariaPressed: "false", text: "123", videoId: "abcdefghijk", visible: true }),
]);
expect(JSON.stringify(state)).not.toContain("outerHTML");
});
test("records browser failures and redacted recent API traffic in the persisted snapshot", async () => {
const context = new FakeEmitter();
const page = new FakeEmitter();
page.addInitScript = jest.fn().mockResolvedValue(undefined);
page.evaluate = jest.fn(async (callback) =>
callback.name === "readLivePageState" ? { runtimeMarkers: { userscript: "3.2.0" } } : undefined,
);
page.isClosed = jest.fn(() => false);
page.url = jest.fn(() => "https://www.youtube.com/shorts/abcdefghijk");
const fileSystem = { mkdirSync: jest.fn(), writeFileSync: jest.fn() };
const log = jest.fn();
const diagnostics = new LiveRunDiagnostics(page, context, {
clock: () => new Date("2026-08-18T12:34:56.000Z"),
fileSystem,
log,
outputDirectory: "diagnostics",
runtime: "userscript",
});
await diagnostics.start();
diagnostics.stageStarted("read-only.channel-to-shorts-and-next");
diagnostics.checkpoint("ryd-votes-response.waiting", { videoId: "abcdefghijk" });
const request = {
method: () => "GET",
resourceType: () => "fetch",
url: () => "https://returnyoutubedislikeapi.com/votes?videoId=abcdefghijk&userId=private-id",
};
context.emit("request", request);
context.emit("response", { request: () => request, status: () => 200 });
page.emit("pageerror", new Error("page exploded"));
page.emit("console", {
location: () => ({ lineNumber: 7, url: "https://www.youtube.com/shorts/abcdefghijk" }),
text: () => `${UNHANDLED_REJECTION_PREFIX}promise exploded`,
type: () => "error",
});
page.emit("console", {
location: () => ({}),
text: () => "console exploded",
type: () => "error",
});
const snapshotPath = await diagnostics.persistFailureSnapshot(new Error("scenario timed out"));
diagnostics.stop();
expect(snapshotPath).toBe(path.join("diagnostics", "failure-2026-08-18T12-34-56-000Z.json"));
expect(fileSystem.mkdirSync).toHaveBeenCalledWith("diagnostics", { recursive: true });
const snapshot = JSON.parse(fileSystem.writeFileSync.mock.calls[0][1]);
expect(snapshot.currentStage).toBe("read-only.channel-to-shorts-and-next");
expect(snapshot.currentCheckpoint).toBe("ryd-votes-response.waiting");
expect(snapshot.browserSignals.map(({ type }) => type)).toEqual([
"pageerror",
"unhandledrejection",
"console.error",
]);
expect(snapshot.recentApiRequests).toEqual([
expect.objectContaining({
method: "GET",
pathname: "/votes",
query: { userId: "<redacted>", videoId: "abcdefghijk" },
status: 200,
}),
]);
expect(snapshot.pageState).toEqual({ runtimeMarkers: { userscript: "3.2.0" } });
expect(snapshot.url).toBe("https://www.youtube.com/shorts/abcdefghijk");
expect(context.listeners.get("request").size).toBe(0);
expect(page.listeners.get("pageerror").size).toBe(0);
});
test("logs stage completion and failure boundaries", async () => {
const diagnostics = {
stageCompleted: jest.fn(),
stageFailed: jest.fn(),
stageStarted: jest.fn(),
};
await expect(runLoggedStage(diagnostics, "success", async () => 42)).resolves.toBe(42);
const failure = new Error("failed");
await expect(
runLoggedStage(diagnostics, "failure", async () => {
throw failure;
}),
).rejects.toBe(failure);
expect(diagnostics.stageStarted.mock.calls.map(([name]) => name)).toEqual(["success", "failure"]);
expect(diagnostics.stageCompleted).toHaveBeenCalledWith("success", expect.any(Number));
expect(diagnostics.stageFailed).toHaveBeenCalledWith("failure", expect.any(Number), failure);
});
test("redacts identity and proof material while retaining diagnostic video IDs", () => {
expect(
diagnosticApiUrl(
"https://returnyoutubedislikeapi.com/puzzle/registration?userId=private&videoId=abcdefghijk&solution=secret",
),
).toEqual({
pathname: "/puzzle/registration",
query: { solution: "<redacted>", userId: "<redacted>", videoId: "abcdefghijk" },
});
});
});
+237
View File
@@ -0,0 +1,237 @@
const crypto = require("node:crypto");
const fs = require("node:fs");
const path = require("node:path");
const extensionPackage = require("../../../package.json");
const userscriptVersion = require("../userscript-version.json");
const LIVE_VOTE_APPROVAL_WINDOW_SECONDS = 120;
const LIVE_VOTE_APPROVALS_DIRECTORY = path.resolve(__dirname, "../../../test-results/live-youtube-vote-approvals");
const DEFAULT_LIVE_NAV_CHANNEL_URL = "https://www.youtube.com/@SmashTrash";
const DEFAULT_LIVE_NAV_SHORT = "iKQhN7omLM4";
const DEFAULT_LIVE_SIDEBAR_HOPS = 3;
const MAX_LIVE_SIDEBAR_HOPS = 10;
const VIDEO_ID_PATTERN = /^[A-Za-z0-9_-]{11}$/;
const LIVE_BUILD_ID_PATTERN = /^[a-f0-9]{32}$/;
const SUPPORTED_RUNTIMES = new Set(["userscript", "extension"]);
const LIVE_BUILD_MARKER_PATHS = {
extension: path.resolve(__dirname, "../../combined/dist/chrome/live-build.json"),
userscript: path.resolve(__dirname, "../../../test-results/live-build/userscript/live-build.json"),
};
function requireValue(environment, name) {
const value = environment[name]?.trim();
if (!value) throw new Error(`${name} is required for the live YouTube smoke suite.`);
return value;
}
function requireVideoId(environment, name) {
const value = requireValue(environment, name);
if (!VIDEO_ID_PATTERN.test(value)) {
throw new Error(`${name} must be an 11-character YouTube video ID.`);
}
return value;
}
function optionalVideoId(environment, name) {
const value = environment[name]?.trim();
if (!value) return null;
if (!VIDEO_ID_PATTERN.test(value)) {
throw new Error(`${name} must be an 11-character YouTube video ID when provided.`);
}
return value;
}
function optionalBoundedInteger(environment, name, defaultValue, maximum) {
const rawValue = environment[name]?.trim();
if (!rawValue) return defaultValue;
if (!/^\d+$/.test(rawValue)) {
throw new Error(`${name} must be a whole number from 1 to ${maximum}.`);
}
const value = Number(rawValue);
if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
throw new Error(`${name} must be a whole number from 1 to ${maximum}.`);
}
return value;
}
function requireChannelHandle(environment) {
const value = requireValue(environment, "RYD_LIVE_EXPECTED_CHANNEL");
if (!/^@[A-Za-z0-9._-]{3,100}$/.test(value)) {
throw new Error("RYD_LIVE_EXPECTED_CHANNEL must be the public @handle of the signed-in YouTube test channel.");
}
return value;
}
function liveVoteApproval(runtime, videoId, unixSeconds) {
return `${runtime}:${videoId}:${unixSeconds}`;
}
function hasFreshVoteApproval(value, runtime, videoId, nowMilliseconds) {
if (!value) return false;
const [approvedRuntime, approvedVideoId, approvedAt, ...rest] = value.split(":");
if (rest.length || approvedRuntime !== runtime || approvedVideoId !== videoId) return false;
const approvedAtSeconds = Number(approvedAt);
if (!Number.isSafeInteger(approvedAtSeconds)) return false;
const ageSeconds = Math.floor(nowMilliseconds / 1000) - approvedAtSeconds;
return ageSeconds >= 0 && ageSeconds <= LIVE_VOTE_APPROVAL_WINDOW_SECONDS;
}
function consumeLiveVoteApproval(
value,
runtime,
videoId,
{ nowMilliseconds = Date.now(), usedApprovalsDirectory } = {},
) {
if (!hasFreshVoteApproval(value, runtime, videoId, nowMilliseconds)) return false;
const directory = usedApprovalsDirectory || LIVE_VOTE_APPROVALS_DIRECTORY;
fs.mkdirSync(directory, { recursive: true });
const approvalHash = crypto.createHash("sha256").update(value).digest("hex");
try {
fs.writeFileSync(path.join(directory, approvalHash), `${runtime}:${videoId}\n`, { flag: "wx" });
return true;
} catch (error) {
if (error.code === "EEXIST") return false;
throw error;
}
}
function parsePlaylistUrl(value, watchVideoId) {
let url;
try {
url = new URL(value);
} catch {
throw new Error("RYD_LIVE_PLAYLIST_URL must be a valid HTTPS YouTube watch URL.");
}
if (
url.protocol !== "https:" ||
!["www.youtube.com", "youtube.com"].includes(url.hostname) ||
url.pathname !== "/watch" ||
url.searchParams.get("v") !== watchVideoId ||
!url.searchParams.get("list")
) {
throw new Error(
"RYD_LIVE_PLAYLIST_URL must be an HTTPS YouTube watch URL for RYD_LIVE_WATCH_A with a playlist ID.",
);
}
return url.toString();
}
function parseChannelUrl(value) {
let url;
try {
url = new URL(value);
} catch {
throw new Error("RYD_LIVE_NAV_CHANNEL_URL must be a valid HTTPS YouTube channel URL.");
}
const isSafeChannelPath = /^\/@[A-Za-z0-9._-]{3,100}(?:\/(?:featured|shorts|videos))?\/?$/.test(url.pathname);
if (
url.protocol !== "https:" ||
!["www.youtube.com", "youtube.com"].includes(url.hostname) ||
url.port ||
url.username ||
url.password ||
url.search ||
url.hash ||
!isSafeChannelPath
) {
throw new Error(
"RYD_LIVE_NAV_CHANNEL_URL must be a plain HTTPS youtube.com /@handle channel, featured, Shorts, or videos URL.",
);
}
url.hostname = "www.youtube.com";
return url.toString();
}
function readExpectedBuildId(runtime, { markerPaths = LIVE_BUILD_MARKER_PATHS, readFileSync = fs.readFileSync } = {}) {
const markerPath = markerPaths[runtime];
let marker;
try {
marker = JSON.parse(readFileSync(markerPath, "utf8"));
} catch (error) {
throw new Error(
`Cannot read the ${runtime} live-build marker at ${markerPath}. Build that live artifact before running the smoke.`,
{ cause: error },
);
}
if (!LIVE_BUILD_ID_PATTERN.test(marker?.buildId)) {
throw new Error(`The ${runtime} live-build marker at ${markerPath} is malformed.`);
}
return marker.buildId;
}
function readLiveOptions(
environment = process.env,
nowMilliseconds = Date.now(),
{ readBuildId = readExpectedBuildId } = {},
) {
if (environment.RYD_LIVE_YOUTUBE !== "1") return null;
if (environment.RYD_LIVE_PRODUCTION_API !== "1") {
throw new Error(
"RYD_LIVE_PRODUCTION_API=1 is required because the installed runtime will contact the production RYD API.",
);
}
const runtime = requireValue(environment, "RYD_LIVE_RUNTIME");
if (!SUPPORTED_RUNTIMES.has(runtime)) {
throw new Error('RYD_LIVE_RUNTIME must be either "userscript" or "extension".');
}
const watchA = requireVideoId(environment, "RYD_LIVE_WATCH_A");
const watchB = requireVideoId(environment, "RYD_LIVE_WATCH_B");
const short = requireVideoId(environment, "RYD_LIVE_SHORT");
if (watchA === watchB) throw new Error("RYD_LIVE_WATCH_A and RYD_LIVE_WATCH_B must be different videos.");
const expectedVersion =
environment.RYD_LIVE_EXPECTED_VERSION?.trim() ||
(runtime === "userscript" ? userscriptVersion : extensionPackage.version);
return {
cdpEndpoint: environment.RYD_CDP_ENDPOINT?.trim() || "chrome",
expectedBuildId: readBuildId(runtime),
expectedChannel: requireChannelHandle(environment),
expectedVersion,
navigation: {
channelUrl: parseChannelUrl(environment.RYD_LIVE_NAV_CHANNEL_URL?.trim() || DEFAULT_LIVE_NAV_CHANNEL_URL),
short: requireVideoId(
{ RYD_LIVE_NAV_SHORT: environment.RYD_LIVE_NAV_SHORT?.trim() || DEFAULT_LIVE_NAV_SHORT },
"RYD_LIVE_NAV_SHORT",
),
watch: optionalVideoId(environment, "RYD_LIVE_NAV_WATCH"),
},
playlistUrl: parsePlaylistUrl(requireValue(environment, "RYD_LIVE_PLAYLIST_URL"), watchA),
productionApiApproved: true,
runtime,
sidebar: {
hopCount: optionalBoundedInteger(
environment,
"RYD_LIVE_SIDEBAR_HOPS",
DEFAULT_LIVE_SIDEBAR_HOPS,
MAX_LIVE_SIDEBAR_HOPS,
),
},
short,
watchA,
watchB,
};
}
module.exports = {
DEFAULT_LIVE_NAV_CHANNEL_URL,
DEFAULT_LIVE_NAV_SHORT,
DEFAULT_LIVE_SIDEBAR_HOPS,
LIVE_VOTE_APPROVALS_DIRECTORY,
LIVE_VOTE_APPROVAL_WINDOW_SECONDS,
consumeLiveVoteApproval,
hasFreshVoteApproval,
liveVoteApproval,
readExpectedBuildId,
readLiveOptions,
};
@@ -0,0 +1,223 @@
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const {
DEFAULT_LIVE_NAV_CHANNEL_URL,
DEFAULT_LIVE_NAV_SHORT,
DEFAULT_LIVE_SIDEBAR_HOPS,
LIVE_VOTE_APPROVALS_DIRECTORY,
LIVE_VOTE_APPROVAL_WINDOW_SECONDS,
consumeLiveVoteApproval,
hasFreshVoteApproval,
liveVoteApproval,
readExpectedBuildId,
readLiveOptions: readLiveOptionsFromEnvironment,
} = require("./live-options");
const NOW = Date.parse("2026-08-08T12:00:00.000Z");
const EXPECTED_BUILD_ID = "0123456789abcdef0123456789abcdef";
const VALID_ENVIRONMENT = {
RYD_LIVE_YOUTUBE: "1",
RYD_LIVE_PRODUCTION_API: "1",
RYD_LIVE_RUNTIME: "userscript",
RYD_LIVE_WATCH_A: "abcdefghijk",
RYD_LIVE_WATCH_B: "zyxwvutsrqp",
RYD_LIVE_SHORT: "shortsabcde",
RYD_LIVE_PLAYLIST_URL: "https://www.youtube.com/watch?v=abcdefghijk&list=PL-test",
RYD_LIVE_EXPECTED_CHANNEL: "@ryd-test",
};
function readLiveOptions(environment, nowMilliseconds = NOW) {
return readLiveOptionsFromEnvironment(environment, nowMilliseconds, {
readBuildId: () => EXPECTED_BUILD_ID,
});
}
describe("live YouTube options", () => {
test("reads the exact generated build ID from the selected runtime marker", () => {
const readFileSync = jest.fn(() => JSON.stringify({ buildId: EXPECTED_BUILD_ID }));
expect(
readExpectedBuildId("userscript", {
markerPaths: { userscript: "owned-live-build.json" },
readFileSync,
}),
).toBe(EXPECTED_BUILD_ID);
expect(readFileSync).toHaveBeenCalledWith("owned-live-build.json", "utf8");
});
test.each([
[
"missing",
() => {
throw Object.assign(new Error("missing"), { code: "ENOENT" });
},
/Cannot read/,
],
["invalid JSON", () => "not JSON", /Cannot read/],
["malformed ID", () => JSON.stringify({ buildId: "stale" }), /is malformed/],
])("rejects a %s generated live-build marker", (_label, readFileSync, expectedMessage) => {
expect(() =>
readExpectedBuildId("extension", {
markerPaths: { extension: "owned-live-build.json" },
readFileSync,
}),
).toThrow(expectedMessage);
});
test("stores consumed vote approvals outside Playwright's cleaned output directory", () => {
const playwrightOutputDirectory = path.resolve(__dirname, "../../../test-results/live-youtube");
expect(LIVE_VOTE_APPROVALS_DIRECTORY).not.toBe(playwrightOutputDirectory);
expect(LIVE_VOTE_APPROVALS_DIRECTORY.startsWith(`${playwrightOutputDirectory}${path.sep}`)).toBe(false);
});
test("stays disabled unless explicitly opted in", () => {
expect(readLiveOptions({})).toBeNull();
});
test("requires production API acknowledgement", () => {
expect(() => readLiveOptions({ ...VALID_ENVIRONMENT, RYD_LIVE_PRODUCTION_API: undefined })).toThrow(
"RYD_LIVE_PRODUCTION_API=1",
);
});
test.each(["", "tampermonkey", "chrome"])("rejects unsupported runtime %p", (runtime) => {
expect(() => readLiveOptions({ ...VALID_ENVIRONMENT, RYD_LIVE_RUNTIME: runtime })).toThrow(/RYD_LIVE_RUNTIME/);
});
test("requires distinct valid video IDs", () => {
expect(() => readLiveOptions({ ...VALID_ENVIRONMENT, RYD_LIVE_SHORT: "too-short" })).toThrow(
"11-character YouTube video ID",
);
expect(() =>
readLiveOptions({
...VALID_ENVIRONMENT,
RYD_LIVE_WATCH_B: VALID_ENVIRONMENT.RYD_LIVE_WATCH_A,
}),
).toThrow("must be different videos");
});
test("requires a playlist URL anchored at watch A", () => {
expect(() =>
readLiveOptions({
...VALID_ENVIRONMENT,
RYD_LIVE_PLAYLIST_URL: "https://www.youtube.com/watch?v=zyxwvutsrqp&list=PL-test",
}),
).toThrow("for RYD_LIVE_WATCH_A");
});
test("requires the expected signed-in channel handle", () => {
expect(() => readLiveOptions({ ...VALID_ENVIRONMENT, RYD_LIVE_EXPECTED_CHANNEL: "not-a-handle" })).toThrow(
"public @handle",
);
});
test.each(["userscript", "extension"])("accepts the %s runtime", (runtime) => {
expect(readLiveOptions({ ...VALID_ENVIRONMENT, RYD_LIVE_RUNTIME: runtime })).toMatchObject({
cdpEndpoint: "chrome",
expectedBuildId: EXPECTED_BUILD_ID,
navigation: {
channelUrl: DEFAULT_LIVE_NAV_CHANNEL_URL,
short: DEFAULT_LIVE_NAV_SHORT,
watch: null,
},
runtime,
sidebar: { hopCount: DEFAULT_LIVE_SIDEBAR_HOPS },
short: "shortsabcde",
watchA: "abcdefghijk",
watchB: "zyxwvutsrqp",
});
});
test("takes the exact build ID from the generated marker and ignores environment attempts to bless a stale build", () => {
const readBuildId = jest.fn(() => EXPECTED_BUILD_ID);
const result = readLiveOptionsFromEnvironment(
{ ...VALID_ENVIRONMENT, RYD_LIVE_EXPECTED_BUILD_ID: "f".repeat(32) },
NOW,
{ readBuildId },
);
expect(readBuildId).toHaveBeenCalledWith("userscript");
expect(result.expectedBuildId).toBe(EXPECTED_BUILD_ID);
});
test("accepts a bounded sidebar stress hop count", () => {
expect(readLiveOptions({ ...VALID_ENVIRONMENT, RYD_LIVE_SIDEBAR_HOPS: "5" })).toMatchObject({
sidebar: { hopCount: 5 },
});
});
test.each(["0", "11", "1.5", "three", "-1"])("rejects invalid sidebar stress hop count %p", (hopCount) => {
expect(() => readLiveOptions({ ...VALID_ENVIRONMENT, RYD_LIVE_SIDEBAR_HOPS: hopCount })).toThrow(
/RYD_LIVE_SIDEBAR_HOPS.*whole number from 1 to 10/,
);
});
test("accepts an exact, safely scoped channel-navigation dataset", () => {
expect(
readLiveOptions({
...VALID_ENVIRONMENT,
RYD_LIVE_NAV_CHANNEL_URL: "https://youtube.com/@ryd-test/shorts",
RYD_LIVE_NAV_SHORT: "navshort001",
RYD_LIVE_NAV_WATCH: "navwatch001",
}),
).toMatchObject({
navigation: {
channelUrl: "https://www.youtube.com/@ryd-test/shorts",
short: "navshort001",
watch: "navwatch001",
},
});
});
test.each([
"http://www.youtube.com/@SmashTrash",
"https://example.com/@SmashTrash",
"https://www.youtube.com.evil.example/@SmashTrash",
"https://www.youtube.com/channel/UC-not-a-handle",
"https://www.youtube.com/@SmashTrash/playlists",
"https://www.youtube.com/@SmashTrash?app=desktop",
])("rejects unsafe or non-deterministic navigation channel URL %p", (channelUrl) => {
expect(() => readLiveOptions({ ...VALID_ENVIRONMENT, RYD_LIVE_NAV_CHANNEL_URL: channelUrl })).toThrow(
/RYD_LIVE_NAV_CHANNEL_URL/,
);
});
test.each(["RYD_LIVE_NAV_SHORT", "RYD_LIVE_NAV_WATCH"])("validates optional navigation ID %s", (name) => {
expect(() => readLiveOptions({ ...VALID_ENVIRONMENT, [name]: "invalid" })).toThrow("11-character YouTube video ID");
});
test("accepts only a fresh runtime-and-video-specific vote approval", () => {
const nowSeconds = Math.floor(NOW / 1000);
const validApproval = liveVoteApproval("userscript", VALID_ENVIRONMENT.RYD_LIVE_WATCH_B, nowSeconds);
expect(hasFreshVoteApproval(validApproval, "userscript", VALID_ENVIRONMENT.RYD_LIVE_WATCH_B, NOW)).toBe(true);
for (const approval of [
liveVoteApproval("extension", VALID_ENVIRONMENT.RYD_LIVE_WATCH_B, nowSeconds),
liveVoteApproval("userscript", VALID_ENVIRONMENT.RYD_LIVE_WATCH_A, nowSeconds),
liveVoteApproval(
"userscript",
VALID_ENVIRONMENT.RYD_LIVE_WATCH_B,
nowSeconds - LIVE_VOTE_APPROVAL_WINDOW_SECONDS - 1,
),
]) {
expect(hasFreshVoteApproval(approval, "userscript", VALID_ENVIRONMENT.RYD_LIVE_WATCH_B, NOW)).toBe(false);
}
const futureApproval = liveVoteApproval("userscript", VALID_ENVIRONMENT.RYD_LIVE_WATCH_B, nowSeconds + 1);
expect(hasFreshVoteApproval(futureApproval, "userscript", VALID_ENVIRONMENT.RYD_LIVE_WATCH_B, NOW)).toBe(false);
});
test("consumes a fresh vote approval only once", () => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "ryd-live-approval-"));
const approval = liveVoteApproval("userscript", VALID_ENVIRONMENT.RYD_LIVE_WATCH_B, Math.floor(NOW / 1000));
try {
const settings = { nowMilliseconds: NOW, usedApprovalsDirectory: directory };
expect(consumeLiveVoteApproval(approval, "userscript", VALID_ENVIRONMENT.RYD_LIVE_WATCH_B, settings)).toBe(true);
expect(consumeLiveVoteApproval(approval, "userscript", VALID_ENVIRONMENT.RYD_LIVE_WATCH_B, settings)).toBe(false);
} finally {
fs.rmSync(directory, { recursive: true, force: true });
}
});
});
@@ -0,0 +1,119 @@
const { LiveYoutubeDriver } = require("../e2e/live/live-youtube-driver");
class FakeBrowserContext {
constructor() {
this.listeners = new Map();
this.routes = [];
this.routeCalls = 0;
this.unrouteCalls = 0;
}
on(event, handler) {
if (!this.listeners.has(event)) this.listeners.set(event, new Set());
this.listeners.get(event).add(handler);
}
off(event, handler) {
this.listeners.get(event)?.delete(handler);
}
async route(matcher, handler) {
this.routeCalls += 1;
this.routes.push({ handler, matcher });
}
async unroute(matcher, handler) {
this.unrouteCalls += 1;
this.routes = this.routes.filter((route) => route.matcher !== matcher || route.handler !== handler);
}
async dispatch(request) {
for (const listener of this.listeners.get("request") ?? []) listener(request);
const route = {
abort: jest.fn().mockResolvedValue(undefined),
fallback: jest.fn().mockResolvedValue(undefined),
request: () => request,
};
for (const registered of [...this.routes].reverse()) {
if (registered.matcher(new URL(request.url()))) {
await registered.handler(route);
break;
}
}
return route;
}
}
function createDriver(context) {
const page = {
setDefaultNavigationTimeout: jest.fn(),
setDefaultTimeout: jest.fn(),
};
return new LiveYoutubeDriver(page, context);
}
function request(method, url) {
return {
frame: jest.fn(() => {
throw new Error("Service-worker requests do not have a frame");
}),
method: () => method,
url: () => url,
};
}
describe("live read-only production-interaction guard", () => {
test("installs the route first, aborts a frame-less interaction POST, and reports the attempt", async () => {
const context = new FakeBrowserContext();
const driver = createDriver(context);
const frameLessRequest = request("POST", "https://returnyoutubedislikeapi.com/interact/vote?source=service-worker");
let interceptedRoute;
await expect(
driver.withNoProductionInteractions(async () => {
expect(context.routeCalls).toBe(1);
interceptedRoute = await context.dispatch(frameLessRequest);
}),
).rejects.toThrow("attempted a production interaction");
expect(interceptedRoute.abort).toHaveBeenCalledWith("blockedbyclient");
expect(interceptedRoute.fallback).not.toHaveBeenCalled();
expect(frameLessRequest.frame).not.toHaveBeenCalled();
expect(context.unrouteCalls).toBe(1);
expect(context.routes).toEqual([]);
});
test("shares one deny route across nested read-only guards", async () => {
const context = new FakeBrowserContext();
const driver = createDriver(context);
const frameLessRequest = request("POST", "https://returnyoutubedislikeapi.com/interact/confirmVote");
await expect(
driver.withNoProductionInteractions(() =>
driver.withNoProductionInteractions(async () => {
await context.dispatch(frameLessRequest);
}),
),
).rejects.toThrow("attempted a production interaction");
expect(context.routeCalls).toBe(1);
expect(context.unrouteCalls).toBe(1);
expect(frameLessRequest.frame).not.toHaveBeenCalled();
});
test("allows non-interaction traffic and removes the route after success", async () => {
const context = new FakeBrowserContext();
const driver = createDriver(context);
const result = await driver.withNoProductionInteractions(async () => {
const unrelatedRoute = await context.dispatch(request("POST", "https://www.youtube.com/youtubei/v1/player"));
expect(unrelatedRoute.abort).not.toHaveBeenCalled();
return "complete";
});
expect(result).toBe("complete");
expect(context.routeCalls).toBe(1);
expect(context.unrouteCalls).toBe(1);
expect(context.routes).toEqual([]);
});
});
@@ -0,0 +1,649 @@
const path = require("node:path");
const {
RESPONSIVE_VIEWPORTS,
runChannelShortsNavigationScenario,
runProductionReactionMatrixScenario,
runReactionCycle,
runResponsiveVisualScenario,
runSidebarStressScenario,
} = require("../e2e/live/live-scenarios");
const EXPECTED_ACTIONS = {
neutral: ["like", "like", "dislike", "like", "dislike", "dislike"],
liked: ["like", "dislike", "like", "dislike", "dislike", "like"],
disliked: ["like", "dislike", "dislike", "like", "like", "dislike"],
};
function nextState(state, action) {
if (action === "like") return state === "liked" ? "neutral" : "liked";
return state === "disliked" ? "neutral" : "disliked";
}
function valueForState(state) {
if (state === "liked") return 1;
if (state === "disliked") return -1;
return 0;
}
function createReactionHarness(
initialState,
{
clickThrowTransition = null,
failHandshake = null,
failHandshakes = failHandshake === null ? [] : [failHandshake],
failedVoteUserId = "shared-user-id",
rollbackOnClickThrow = false,
rollbackOnHandshakeFailure = false,
rollbackOnStateWaitFailure = false,
stateWaitFailTransition = null,
} = {},
) {
const events = [];
let currentState = initialState;
let currentVideo = null;
let handshakeNumber = 0;
let stateWaitNumber = 0;
const failedHandshakeNumbers = new Set(failHandshakes);
const driver = {
assertCurrentVideo: jest.fn((videoId) => expect(currentVideo).toBe(videoId)),
assertRuntime: jest.fn(),
assertSignedIn: jest.fn(),
clickAction: jest.fn(async (videoId, action) => {
expect(currentVideo).toBe(videoId);
currentState = nextState(currentState, action);
events.push({ action, state: currentState });
if (events.length === clickThrowTransition) {
if (rollbackOnClickThrow) currentState = initialState;
throw new Error("simulated post-dispatch click failure");
}
}),
openShort: jest.fn(async (videoId) => {
currentVideo = videoId;
}),
openWatch: jest.fn(async (videoId) => {
currentVideo = videoId;
}),
readReactionState: jest.fn(async () => currentState),
waitForDislikeText: jest.fn(async () => "123"),
waitForReactionState: jest.fn(async (expected) => {
stateWaitNumber += 1;
if (stateWaitNumber === stateWaitFailTransition) {
if (rollbackOnStateWaitFailure) currentState = initialState;
throw new Error("simulated post-click state-wait failure");
}
expect(currentState).toBe(expected);
}),
};
const recorder = {
mark: jest.fn(() => events.length),
waitForHandshake: jest.fn(async (value) => {
handshakeNumber += 1;
expect(value).toBe(valueForState(currentState));
if (failedHandshakeNumbers.has(handshakeNumber)) {
if (rollbackOnHandshakeFailure) currentState = initialState;
throw new Error("simulated handshake failure");
}
return "shared-user-id";
}),
voteUserId: jest.fn(() => failedVoteUserId),
};
return {
driver,
events,
getState: () => currentState,
recorder,
};
}
const OPTIONS = {
expectedBuildId: "0123456789abcdef0123456789abcdef",
expectedChannel: "@ryd-test",
expectedVersion: "3.2.0",
runtime: "userscript",
};
describe("live Shorts navigation stabilization", () => {
test("pauses the next Short only after its current control has rendered", async () => {
const events = [];
const driver = {
assertCurrentShortsControl: jest.fn(async (videoId) => {
events.push(`control:${videoId}`);
return { count: videoId === "abcdefghijk" ? "100" : "200", synthetic: true, videoId };
}),
assertRuntime: jest.fn(),
assertSignedIn: jest.fn(),
navigateFromColdChannelToShort: jest.fn(async () => events.push("channel-navigation")),
navigateToNextShort: jest.fn(async () => {
events.push("next-navigation");
return "lmnopqrstuv";
}),
pausePlayback: jest.fn(async () => events.push("pause")),
withNoProductionInteractions: jest.fn(async (action) => action()),
};
const options = {
...OPTIONS,
navigation: {
channelUrl: "https://www.youtube.com/@SmashTrash",
short: "abcdefghijk",
},
};
await expect(runChannelShortsNavigationScenario(driver, options)).resolves.toEqual({
initial: { count: "100", synthetic: true, videoId: "abcdefghijk" },
next: { count: "200", synthetic: true, videoId: "lmnopqrstuv" },
});
expect(events).toEqual([
"channel-navigation",
"control:abcdefghijk",
"next-navigation",
"control:lmnopqrstuv",
"pause",
]);
expect(driver.pausePlayback).toHaveBeenCalledTimes(1);
});
});
describe("live watch sidebar stress scenario", () => {
test.each(["userscript", "extension"])(
"takes consecutive unvisited related links and soaks one current %s bar after each exact response",
async (runtime) => {
const hopVideoIds = ["hopvideo001", "hopvideo002", "hopvideo003"];
const counts = ["101", "202", "303"];
let currentVideoId = null;
let hopIndex = 0;
const driver = {
assertCurrentVideo: jest.fn((videoId) => expect(currentVideoId).toBe(videoId)),
assertRuntime: jest.fn(),
assertSignedIn: jest.fn(),
captureWatchRatioVisual: jest.fn(async (_runtime, screenshotPath) => ({
count: counts[hopIndex - 1],
screenshotPath,
})),
navigateToRelatedWatch: jest.fn(async (excludedVideoIds) => {
expect(excludedVideoIds).toEqual(["abcdefghijk", ...hopVideoIds.slice(0, hopIndex)]);
const videoId = hopVideoIds[hopIndex];
const dislikes = (hopIndex + 1) * 1_000;
hopIndex += 1;
currentVideoId = videoId;
return { body: { dislikes }, videoId };
}),
openWatch: jest.fn(async (videoId) => {
currentVideoId = videoId;
}),
soakWatchRatioVisual: jest.fn(async (_runtime, settings) => ({
count: settings.expectedCount,
durationMs: settings.durationMs,
sampleCount: 9,
videoId: settings.videoId,
})),
withNoProductionInteractions: jest.fn(async (action) => action()),
};
const makeDirectory = jest.fn();
const options = { ...OPTIONS, runtime, sidebar: { hopCount: 3 }, watchA: "abcdefghijk" };
const result = await runSidebarStressScenario(driver, options, {
makeDirectory,
outputDirectory: "sidebar-evidence",
soakDurationMs: 25,
});
expect(makeDirectory).toHaveBeenCalledWith("sidebar-evidence");
expect(driver.openWatch).toHaveBeenCalledWith(options.watchA);
expect(driver.navigateToRelatedWatch).toHaveBeenCalledTimes(3);
expect(driver.captureWatchRatioVisual.mock.calls).toEqual(
hopVideoIds.map((_videoId, index) => [
runtime,
path.join("sidebar-evidence", `${runtime}-sidebar-hop-0${index + 1}.png`),
{ presenceTimeoutMs: 1_000 },
]),
);
expect(driver.soakWatchRatioVisual.mock.calls).toEqual(
hopVideoIds.map((videoId, index) => [runtime, { durationMs: 25, expectedCount: counts[index], videoId }]),
);
expect(driver.assertRuntime).toHaveBeenCalledTimes(4);
expect(driver.assertSignedIn).toHaveBeenCalledTimes(4);
expect(driver.withNoProductionInteractions).toHaveBeenCalledTimes(1);
expect(result).toEqual({
hopCount: 3,
hops: hopVideoIds.map((videoId, index) => ({
apiDislikes: (index + 1) * 1_000,
count: counts[index],
readyLatencyMs: expect.any(Number),
readyTimeoutMs: 1_000,
screenshotPath: path.join("sidebar-evidence", `${runtime}-sidebar-hop-0${index + 1}.png`),
soak: {
count: counts[index],
durationMs: 25,
sampleCount: 9,
videoId,
},
videoId,
})),
outputDirectory: "sidebar-evidence",
startVideoId: options.watchA,
});
},
);
});
describe("live reaction cycle", () => {
test.each(["neutral", "liked", "disliked"])(
"covers all six transitions and restores an initially %s watch video",
async (initialState) => {
const harness = createReactionHarness(initialState);
const beforeFirstAction = jest.fn();
const result = await runReactionCycle(harness.driver, harness.recorder, OPTIONS, {
beforeFirstAction,
videoId: "abcdefghijk",
});
expect(harness.driver.openWatch).toHaveBeenCalledWith("abcdefghijk");
expect(harness.driver.openShort).not.toHaveBeenCalled();
expect(harness.events.map(({ action }) => action)).toEqual(EXPECTED_ACTIONS[initialState]);
expect(
new Set(
harness.events.map(
({ action, state }, index) =>
`${index ? harness.events[index - 1].state : initialState}:${action}:${state}`,
),
).size,
).toBe(6);
expect(harness.getState()).toBe(initialState);
expect(harness.recorder.waitForHandshake).toHaveBeenCalledTimes(6);
expect(beforeFirstAction).toHaveBeenCalledTimes(1);
expect(result).toEqual({ evidencePaths: [], initialState, userId: "shared-user-id" });
},
);
test("targets Shorts through the same transition implementation", async () => {
const harness = createReactionHarness("neutral");
await runReactionCycle(harness.driver, harness.recorder, OPTIONS, {
isShort: true,
videoId: "shortsabcde",
});
expect(harness.driver.openShort).toHaveBeenCalledWith("shortsabcde");
expect(harness.driver.openWatch).not.toHaveBeenCalled();
});
test("restores the initial state when a production handshake fails mid-cycle", async () => {
const harness = createReactionHarness("neutral", { failHandshake: 3 });
await expect(
runReactionCycle(harness.driver, harness.recorder, OPTIONS, {
beforeFirstAction: jest.fn(),
videoId: "abcdefghijk",
}),
).rejects.toThrow("simulated handshake failure");
expect(harness.getState()).toBe("neutral");
expect(harness.events.at(-1)).toEqual({ action: "dislike", state: "neutral" });
expect(harness.recorder.waitForHandshake).toHaveBeenLastCalledWith(0, 3);
});
test.each([
[
"click dispatch throws",
{ clickThrowTransition: 1, rollbackOnClickThrow: true },
"simulated post-dispatch click failure",
],
[
"post-click state wait fails",
{ rollbackOnStateWaitFailure: true, stateWaitFailTransition: 1 },
"simulated post-click state-wait failure",
],
])(
"forces a verified away-and-back cleanup when %s after the UI returns to its initial state",
async (_failure, harnessOptions, expectedError) => {
const harness = createReactionHarness("neutral", harnessOptions);
await expect(
runReactionCycle(harness.driver, harness.recorder, OPTIONS, {
beforeFirstAction: jest.fn(),
videoId: "abcdefghijk",
}),
).rejects.toThrow(expectedError);
expect(harness.recorder.voteUserId).toHaveBeenCalledWith(1, 0);
expect(harness.events.slice(-2)).toEqual([
{ action: "dislike", state: "disliked" },
{ action: "dislike", state: "neutral" },
]);
expect(harness.recorder.waitForHandshake.mock.calls).toEqual([
[-1, 1],
[0, 2],
]);
expect(harness.getState()).toBe("neutral");
},
);
test("reports the manual-restore URL when cleanup after a post-dispatch click failure cannot be confirmed", async () => {
const harness = createReactionHarness("neutral", {
clickThrowTransition: 1,
failHandshakes: [1],
rollbackOnClickThrow: true,
});
await expect(
runReactionCycle(harness.driver, harness.recorder, OPTIONS, {
beforeFirstAction: jest.fn(),
videoId: "abcdefghijk",
}),
).rejects.toThrow(
/Automatic cleanup could not be verified\. Manually restore https:\/\/www\.youtube\.com\/watch\?v=abcdefghijk/,
);
});
test.each([
["first", "neutral", 1, true],
["middle", "liked", 3, false],
["final", "disliked", 6, false],
])(
"forces a verified away-and-back cleanup after a %s transition handshake fails with the UI at its initial state",
async (_position, initialState, failHandshake, rollbackOnHandshakeFailure) => {
const harness = createReactionHarness(initialState, { failHandshake, rollbackOnHandshakeFailure });
await expect(
runReactionCycle(harness.driver, harness.recorder, OPTIONS, {
beforeFirstAction: jest.fn(),
videoId: "abcdefghijk",
}),
).rejects.toThrow("simulated handshake failure");
let failedState = initialState;
for (const action of EXPECTED_ACTIONS[initialState].slice(0, failHandshake)) {
failedState = nextState(failedState, action);
}
const cleanupAction = initialState === "liked" ? "like" : "dislike";
const awayState = initialState === "neutral" ? "disliked" : "neutral";
expect(harness.recorder.voteUserId).toHaveBeenCalledWith(valueForState(failedState), failHandshake - 1);
expect(harness.events.slice(-2)).toEqual([
{ action: cleanupAction, state: awayState },
{ action: cleanupAction, state: initialState },
]);
expect(harness.recorder.waitForHandshake.mock.calls.slice(-2)).toEqual([
[valueForState(awayState), failHandshake],
[valueForState(initialState), failHandshake + 1],
]);
expect(harness.getState()).toBe(initialState);
},
);
test("reports the manual-restore URL when the verified cleanup round trip fails", async () => {
const harness = createReactionHarness("neutral", {
failHandshakes: [1, 2],
rollbackOnHandshakeFailure: true,
});
await expect(
runReactionCycle(harness.driver, harness.recorder, OPTIONS, {
beforeFirstAction: jest.fn(),
videoId: "abcdefghijk",
}),
).rejects.toThrow(
/Automatic cleanup could not be verified\. Manually restore https:\/\/www\.youtube\.com\/watch\?v=abcdefghijk/,
);
});
test("reports the manual-restore URL when cleanup cannot match the failed attempt identity", async () => {
const harness = createReactionHarness("neutral", {
failHandshake: 1,
failedVoteUserId: "failed-attempt-user-id",
});
await expect(
runReactionCycle(harness.driver, harness.recorder, OPTIONS, {
beforeFirstAction: jest.fn(),
videoId: "abcdefghijk",
}),
).rejects.toThrow(/cleanup reaction did not use the failed attempt's RYD identity/);
});
test("restores the initial state when a post-action visual assertion fails", async () => {
const harness = createReactionHarness("neutral");
const captureReactionVisual = jest.fn(async ({ index, state }) => {
if (index === 3) throw new Error("simulated visual failure");
return `watch-${index}-${state}.png`;
});
await expect(
runReactionCycle(harness.driver, harness.recorder, OPTIONS, {
beforeFirstAction: jest.fn(),
captureReactionVisual,
videoId: "abcdefghijk",
}),
).rejects.toThrow("simulated visual failure");
expect(captureReactionVisual.mock.calls.map(([capture]) => capture)).toEqual([
{ index: 0, state: "neutral" },
{ index: 1, state: "liked" },
{ index: 2, state: "neutral" },
{ index: 3, state: "disliked" },
]);
expect(harness.getState()).toBe("neutral");
expect(harness.events.at(-1)).toEqual({ action: "dislike", state: "neutral" });
expect(harness.recorder.waitForHandshake).toHaveBeenLastCalledWith(0, 3);
});
});
describe("live production reaction matrix visual evidence", () => {
test("captures and returns the initial state plus all six transitions for watch and Shorts", async () => {
const states = new Map([
["abcdefghijk", "neutral"],
["shortsabcde", "neutral"],
]);
let currentVideo;
const driver = {
assertCurrentVideo: jest.fn((videoId) => expect(currentVideo).toBe(videoId)),
assertRuntime: jest.fn(),
assertSignedIn: jest.fn(),
captureReactionStateVisual: jest.fn(async (capture) => {
expect(states.get(capture.videoId)).toBe(capture.expectedState);
return { screenshotPath: capture.screenshotPath };
}),
clickAction: jest.fn(async (videoId, action) => {
states.set(videoId, nextState(states.get(videoId), action));
}),
openShort: jest.fn(async (videoId) => {
currentVideo = videoId;
}),
openWatch: jest.fn(async (videoId) => {
currentVideo = videoId;
}),
readReactionState: jest.fn(async () => states.get(currentVideo)),
waitForDislikeText: jest.fn(async () => "123"),
waitForReactionState: jest.fn(async (expected) => expect(states.get(currentVideo)).toBe(expected)),
};
const recorders = [];
const createRecorder = jest.fn((videoId) => {
const recorder = {
mark: jest.fn(() => 0),
stop: jest.fn(),
waitForHandshake: jest.fn(async (value) => {
expect(value).toBe(valueForState(states.get(videoId)));
return "shared-user-id";
}),
};
recorders.push(recorder);
return recorder;
});
const consumeVoteApproval = jest.fn();
const makeDirectory = jest.fn();
const options = {
...OPTIONS,
short: "shortsabcde",
watchB: "abcdefghijk",
};
const outputDirectory = "reaction-evidence";
const result = await runProductionReactionMatrixScenario(driver, options, createRecorder, consumeVoteApproval, {
makeDirectory,
outputDirectory,
});
const stateSequence = ["neutral", "liked", "neutral", "disliked", "liked", "disliked", "neutral"];
const expectedWatch = stateSequence.map((state, index) =>
path.join(outputDirectory, `watch-${index}-${state}.png`),
);
const expectedShort = stateSequence.map((state, index) =>
path.join(outputDirectory, `short-${index}-${state}.png`),
);
expect(makeDirectory).toHaveBeenCalledWith(outputDirectory);
expect(consumeVoteApproval).toHaveBeenCalledTimes(1);
expect(driver.captureReactionStateVisual).toHaveBeenCalledTimes(14);
expect(driver.captureReactionStateVisual.mock.calls.slice(0, 7).map(([capture]) => capture)).toEqual(
expectedWatch.map((screenshotPath, index) => ({
expectedState: stateSequence[index],
isShort: false,
runtime: "userscript",
screenshotPath,
videoId: options.watchB,
})),
);
expect(driver.captureReactionStateVisual.mock.calls.slice(7).map(([capture]) => capture)).toEqual(
expectedShort.map((screenshotPath, index) => ({
expectedState: stateSequence[index],
isShort: true,
runtime: "userscript",
screenshotPath,
videoId: options.short,
})),
);
expect(result).toEqual({
evidencePaths: [...expectedWatch, ...expectedShort],
outputDirectory,
short: { evidencePaths: expectedShort, initialState: "neutral", userId: "shared-user-id" },
watch: { evidencePaths: expectedWatch, initialState: "neutral", userId: "shared-user-id" },
});
expect(recorders).toHaveLength(2);
expect(recorders.every((recorder) => recorder.stop.mock.calls.length === 1)).toBe(true);
});
});
describe("live responsive visual scenario", () => {
test("captures watch and userscript Shorts evidence at all responsive widths without actions", async () => {
const originalViewport = { height: 900, width: 1440 };
let currentViewport = originalViewport;
const driver = {
assertCurrentShortsControl: jest.fn(),
assertRuntime: jest.fn(),
assertSignedIn: jest.fn(),
captureSyntheticShortsVisual: jest.fn(async (_videoId, screenshotPath) => ({
screenshotPath,
viewport: currentViewport,
})),
captureWatchRatioVisual: jest.fn(async (_runtime, screenshotPath) => ({
screenshotPath,
viewport: currentViewport,
})),
openShort: jest.fn(),
openWatch: jest.fn(),
readViewportSize: jest.fn(async () => currentViewport),
setViewportSize: jest.fn(async (viewport) => {
currentViewport = viewport;
}),
waitForDislikeText: jest.fn(async () => "123"),
withNoProductionInteractions: jest.fn(async (action) => action()),
};
const makeDirectory = jest.fn();
const options = { ...OPTIONS, short: "shortsabcde", watchA: "abcdefghijk" };
const result = await runResponsiveVisualScenario(driver, options, {
makeDirectory,
outputDirectory: "responsive-evidence",
});
expect(makeDirectory).toHaveBeenCalledWith("responsive-evidence");
expect(driver.openWatch).toHaveBeenCalledWith(options.watchA);
expect(driver.openShort).toHaveBeenCalledWith(options.short);
expect(driver.assertCurrentShortsControl).toHaveBeenCalledTimes(RESPONSIVE_VIEWPORTS.length);
expect(driver.captureWatchRatioVisual).toHaveBeenCalledTimes(RESPONSIVE_VIEWPORTS.length);
expect(driver.captureSyntheticShortsVisual).toHaveBeenCalledTimes(RESPONSIVE_VIEWPORTS.length);
expect(driver.captureWatchRatioVisual.mock.calls.map(([, screenshotPath]) => screenshotPath)).toEqual(
RESPONSIVE_VIEWPORTS.map(({ width }) => expect.stringContaining(`userscript-watch-ratio-${width}.png`)),
);
expect(driver.captureSyntheticShortsVisual.mock.calls.map(([, screenshotPath]) => screenshotPath)).toEqual(
RESPONSIVE_VIEWPORTS.map(({ width }) => expect.stringContaining(`userscript-shorts-control-${width}.png`)),
);
expect(driver.setViewportSize).toHaveBeenLastCalledWith(originalViewport);
expect(driver.withNoProductionInteractions).toHaveBeenCalledTimes(1);
expect(driver.waitForDislikeText).toHaveBeenCalledTimes(RESPONSIVE_VIEWPORTS.length * 2);
expect(result.watch).toHaveLength(RESPONSIVE_VIEWPORTS.length);
expect(result.shorts).toHaveLength(RESPONSIVE_VIEWPORTS.length);
expect(result.shortsSkipped).toBeNull();
});
test("captures watch and native extension Shorts evidence at all responsive widths without actions", async () => {
const originalViewport = { height: 900, width: 1440 };
let currentViewport = originalViewport;
const driver = {
assertCurrentShortsControl: jest.fn(),
assertRuntime: jest.fn(),
assertSignedIn: jest.fn(),
captureNativeShortsVisual: jest.fn(async (_videoId, screenshotPath) => ({
screenshotPath,
viewport: currentViewport,
})),
captureWatchRatioVisual: jest.fn(async (_runtime, screenshotPath) => ({
screenshotPath,
viewport: currentViewport,
})),
openShort: jest.fn(),
openWatch: jest.fn(),
readViewportSize: jest.fn(async () => currentViewport),
setViewportSize: jest.fn(async (viewport) => {
currentViewport = viewport;
}),
waitForDislikeText: jest.fn(async () => "123"),
withNoProductionInteractions: jest.fn(async (action) => action()),
};
const result = await runResponsiveVisualScenario(
driver,
{ ...OPTIONS, runtime: "extension", short: "shortsabcde", watchA: "abcdefghijk" },
{ makeDirectory: jest.fn(), outputDirectory: "responsive-evidence" },
);
expect(driver.openShort).toHaveBeenCalledWith("shortsabcde");
expect(driver.assertCurrentShortsControl).toHaveBeenCalledTimes(RESPONSIVE_VIEWPORTS.length);
expect(driver.captureNativeShortsVisual).toHaveBeenCalledTimes(RESPONSIVE_VIEWPORTS.length);
expect(driver.captureNativeShortsVisual.mock.calls.map(([, screenshotPath]) => screenshotPath)).toEqual(
RESPONSIVE_VIEWPORTS.map(({ width }) => expect.stringContaining(`extension-shorts-control-${width}.png`)),
);
expect(result.shorts).toHaveLength(RESPONSIVE_VIEWPORTS.length);
expect(result.shortsSkipped).toBeNull();
expect(driver.setViewportSize).toHaveBeenLastCalledWith(originalViewport);
});
test("restores the original viewport when a visual assertion fails", async () => {
const originalViewport = { height: 800, width: 1200 };
const driver = {
assertRuntime: jest.fn(),
assertSignedIn: jest.fn(),
captureWatchRatioVisual: jest.fn(async () => Promise.reject(new Error("visual failed"))),
openWatch: jest.fn(),
readViewportSize: jest.fn(async () => originalViewport),
setViewportSize: jest.fn(),
waitForDislikeText: jest.fn(async () => "123"),
withNoProductionInteractions: jest.fn(async (action) => action()),
};
await expect(
runResponsiveVisualScenario(
driver,
{ ...OPTIONS, short: "shortsabcde", watchA: "abcdefghijk" },
{
makeDirectory: jest.fn(),
outputDirectory: "responsive-evidence",
},
),
).rejects.toThrow("visual failed");
expect(driver.setViewportSize).toHaveBeenLastCalledWith(originalViewport);
});
});
@@ -0,0 +1,76 @@
const { assertLogicalVoteHandshake } = require("../e2e/live/live-youtube-driver");
const VIDEO_ID = "abcdefghijk";
const USER_ID = "shared-user-id";
function vote({ status = 200, userId = USER_ID, value = 1, videoId = VIDEO_ID } = {}) {
return {
body: { userId, value, videoId },
pathname: "/interact/vote",
responseError: null,
status,
};
}
function confirmation({ confirmed = true, status = 200, userId = USER_ID, videoId = VIDEO_ID } = {}) {
return {
body: { solution: "proof", userId, videoId },
pathname: "/interact/confirmVote",
responseBody: confirmed,
responseError: null,
status,
};
}
describe("live logical vote handshake validation", () => {
test("accepts one vote puzzle followed by one true confirmation", () => {
expect(assertLogicalVoteHandshake([vote(), confirmation()], VIDEO_ID, 1)).toBe(USER_ID);
});
test("accepts two matching vote puzzle requests followed by one true confirmation", () => {
expect(assertLogicalVoteHandshake([vote(), vote(), confirmation()], VIDEO_ID, 1)).toBe(USER_ID);
});
test("accepts three matching vote puzzle requests followed by one true confirmation", () => {
expect(assertLogicalVoteHandshake([vote(), vote(), vote(), confirmation()], VIDEO_ID, 1)).toBe(USER_ID);
});
test("rejects a fourth vote puzzle request", () => {
expect(() => assertLogicalVoteHandshake([vote(), vote(), vote(), vote(), confirmation()], VIDEO_ID, 1)).toThrow(
/one to three vote puzzle requests/,
);
});
test.each([
["user", [vote(), vote({ userId: "another-user" }), confirmation()], /different user IDs/],
["video", [vote(), vote({ videoId: "lmnopqrstuv" }), confirmation()], /different video/],
["value", [vote(), vote({ value: 0 }), confirmation()], /changed the requested vote value/],
])("rejects a retry with a mismatched %s", (_field, records, message) => {
expect(() => assertLogicalVoteHandshake(records, VIDEO_ID, 1)).toThrow(message);
});
test.each([
["extra confirmation", [vote(), confirmation(), confirmation()]],
["other interaction", [vote(), { ...vote(), pathname: "/interact/other" }, confirmation()]],
["traffic after confirmation", [vote(), confirmation(), vote()]],
])("rejects %s traffic", (_case, records) => {
expect(() => assertLogicalVoteHandshake(records, VIDEO_ID, 1)).toThrow(
/exactly one confirmation|logical vote may/i,
);
});
test("rejects a false confirmation", () => {
expect(() => assertLogicalVoteHandshake([vote(), confirmation({ confirmed: false })], VIDEO_ID, 1)).toThrow(
/did not confirm the vote/,
);
});
test("rejects a failed vote or confirmation response", () => {
expect(() => assertLogicalVoteHandshake([vote({ status: 500 }), confirmation()], VIDEO_ID, 1)).toThrow(
/Vote request failed with HTTP 500/,
);
expect(() => assertLogicalVoteHandshake([vote(), confirmation({ status: 500 })], VIDEO_ID, 1)).toThrow(
/Vote confirmation failed with HTTP 500/,
);
});
});
@@ -0,0 +1,920 @@
/**
* @jest-environment jsdom
*/
const {
LiveYoutubeDriver,
assertNativeShortsPairGeometry,
assertReactionPressedStates,
assertShortsActionStackGeometry,
assertSyntheticShortsGeometry,
assertWatchRatioViewportAlignment,
clickWithSingleNavigationRetry,
croppedScreenshotClip,
firstVisibleRelatedWatchLink,
isShortCandidateEligible,
isShortsIconVisualReady,
readDislikeControlText,
relatedWatchVideoId,
} = require("../e2e/live/live-youtube-driver");
const VISIBLE_RECT = {
bottom: 200,
height: 100,
left: 10,
right: 110,
top: 100,
width: 100,
};
const ACTION_HOST_STYLE = {
marginBottom: 0,
marginLeft: 0,
marginRight: 0,
marginTop: 0,
paddingBottom: 8,
paddingLeft: 0,
paddingRight: 0,
paddingTop: 0,
};
const COUNT_STYLE = {
fontFamily: '"Roboto", "Arial", sans-serif',
fontSize: 12,
fontStyle: "normal",
fontWeight: "400",
lineHeight: 18,
};
function box(x, y, width, height) {
return { height, width, x, y };
}
function validSyntheticShortsGeometry() {
return {
like: {
button: box(100, 10, 48, 48),
count: box(110, 58, 28, 14),
countStyle: { ...COUNT_STYLE },
host: box(100, 10, 48, 78),
hostStyle: { ...ACTION_HOST_STYLE },
icon: box(112, 22, 24, 24),
label: box(100, 10, 48, 70),
svg: box(112, 22, 24, 24),
},
next: {
host: box(100, 166, 48, 78),
},
synthetic: {
button: box(100, 88, 48, 48),
count: box(109, 136, 30, 14),
countStyle: { ...COUNT_STYLE },
host: box(100, 88, 48, 78),
hostStyle: { ...ACTION_HOST_STYLE },
icon: box(112, 100, 24, 24),
label: box(100, 88, 48, 70),
svg: box(112, 100, 24, 24),
},
};
}
function validNativeShortsPairGeometry() {
const syntheticGeometry = validSyntheticShortsGeometry();
const enrich = (geometry, actionIndex) => ({
...geometry,
actionIndex,
reelIndex: 2,
videoMatches: true,
});
return {
dislike: enrich(syntheticGeometry.synthetic, 4),
like: enrich(syntheticGeometry.like, 3),
};
}
function renderShort({ href, rendererVideoId } = {}) {
document.body.innerHTML = `
<ytd-reel-video-renderer${rendererVideoId ? ` video-id="${rendererVideoId}"` : ""}>
${href ? `<a href="${href}"></a>` : ""}
<button type="button">Dislike</button>
</ytd-reel-video-renderer>
`;
const reel = document.querySelector("ytd-reel-video-renderer");
const button = reel.querySelector("button");
reel.getBoundingClientRect = jest.fn(() => VISIBLE_RECT);
button.getBoundingClientRect = jest.fn(() => VISIBLE_RECT);
return { button, reel };
}
const activeShortSettings = (videoId) => ({
activeShortRequired: true,
expectedShortVideoId: videoId,
});
beforeEach(() => {
Object.defineProperty(window, "innerHeight", { configurable: true, value: 768 });
Object.defineProperty(window, "innerWidth", { configurable: true, value: 1024 });
document.body.innerHTML = "";
});
describe("exact installed live-build identity", () => {
const buildId = "0123456789abcdef0123456789abcdef";
function createDriver(markers) {
const page = {
locator: jest.fn((selector) => {
expect(selector).toBe("html");
return { evaluate: jest.fn(async () => markers) };
}),
setDefaultNavigationTimeout: jest.fn(),
setDefaultTimeout: jest.fn(),
};
return new LiveYoutubeDriver(page, {});
}
test.each(["userscript", "extension"])("accepts only the exact installed %s live build", async (runtime) => {
const otherRuntime = runtime === "userscript" ? "extension" : "userscript";
const markers = {
extension: null,
extensionBuild: null,
userscript: null,
userscriptBuild: null,
[runtime]: runtime === "userscript" ? "3.2.0" : "4.0.4",
[`${runtime}Build`]: buildId,
};
const driver = createDriver(markers);
await expect(driver.assertRuntime(runtime, markers[runtime], buildId)).resolves.toBeUndefined();
expect(markers[otherRuntime]).toBeNull();
});
test("rejects a stale userscript build even when its version matches", async () => {
const driver = createDriver({
extension: null,
extensionBuild: null,
userscript: "3.2.0",
userscriptBuild: "f".repeat(32),
});
await expect(driver.assertRuntime("userscript", "3.2.0", buildId)).rejects.toThrow(
"match the freshly generated artifact",
);
});
test.each([undefined, "", "stale", "A".repeat(32)])(
"refuses to test without a valid exact build ID: %p",
async (expectedBuildId) => {
const driver = createDriver({
extension: null,
extensionBuild: null,
userscript: "3.2.0",
userscriptBuild: buildId,
});
await expect(driver.assertRuntime("userscript", "3.2.0", expectedBuildId)).rejects.toThrow(
"exact 32-character live build ID",
);
},
);
test("rejects an enabled opposite runtime even when the selected marker is exact", async () => {
const driver = createDriver({
extension: "4.0.4",
extensionBuild: "f".repeat(32),
userscript: "3.2.0",
userscriptBuild: buildId,
});
await expect(driver.assertRuntime("userscript", "3.2.0", buildId)).rejects.toThrow(
"Disable the extension before running the userscript smoke",
);
});
});
describe("isShortCandidateEligible", () => {
test("accepts an exact renderer video-id without requiring a Shorts link", () => {
const { button } = renderShort({ rendererVideoId: "current-video" });
expect(isShortCandidateEligible(button, activeShortSettings("current-video"))).toBe(true);
});
test("accepts an exact Shorts link when the renderer has no video-id", () => {
const { button } = renderShort({ href: "/shorts/current-video?feature=share" });
expect(isShortCandidateEligible(button, activeShortSettings("current-video"))).toBe(true);
});
test("rejects a matching descendant link when the renderer video-id identifies another Short", () => {
const { button } = renderShort({ href: "/shorts/current-video", rendererVideoId: "outgoing-video" });
expect(isShortCandidateEligible(button, activeShortSettings("current-video"))).toBe(false);
});
test.each(["/shorts/current-video-extra", "/shorts/current-video/related", "/shorts/other-video"])(
"rejects a non-exact Shorts link: %s",
(href) => {
const { button } = renderShort({ href });
expect(isShortCandidateEligible(button, activeShortSettings("current-video"))).toBe(false);
},
);
test("rejects an exact video outside the viewport", () => {
const { button, reel } = renderShort({ rendererVideoId: "current-video" });
reel.getBoundingClientRect = jest.fn(() => ({ ...VISIBLE_RECT, bottom: -1, top: -101 }));
expect(isShortCandidateEligible(button, activeShortSettings("current-video"))).toBe(false);
});
test("rejects an action button outside the viewport even when its renderer intersects", () => {
const { button } = renderShort({ rendererVideoId: "current-video" });
button.getBoundingClientRect = jest.fn(() => ({ ...VISIBLE_RECT, left: 1100, right: 1200 }));
expect(isShortCandidateEligible(button, activeShortSettings("current-video"))).toBe(false);
});
});
describe("readDislikeControlText", () => {
test("reads a synthetic control count from the sibling of its inner button", () => {
document.body.innerHTML = `
<div data-ryd-synthetic-shorts-dislike>
<div>
<button type="button" aria-pressed="false"><svg></svg></button>
<div><span id="text" role="text">1.2K</span></div>
</div>
</div>
`;
expect(readDislikeControlText(document.querySelector("button"))).toBe("1.2K");
});
test("continues to read native dislike text from the button", () => {
document.body.innerHTML = `<button type="button"><span role="text">456</span></button>`;
expect(readDislikeControlText(document.querySelector("button"))).toBe("456");
});
});
describe("relatedWatchVideoId", () => {
const settings = {
currentVideoId: "abcdefghijk",
excludedVideoIds: ["excluded001"],
origin: "https://www.youtube.com",
};
function read(href, overrides = {}) {
const link = document.createElement("a");
link.setAttribute("href", href);
return relatedWatchVideoId(link, { ...settings, ...overrides });
}
test("accepts an exact same-origin watch target while preserving harmless query parameters", () => {
expect(read("/watch?v=targetvid01&pp=sidebar")).toBe("targetvid01");
});
test.each([
["/watch?v=abcdefghijk", "current video"],
["/watch?v=excluded001", "previously visited video"],
["/watch?v=too-short", "invalid video ID"],
["/shorts/targetvid01", "Shorts URL"],
["https://example.com/watch?v=targetvid01", "cross-origin URL"],
["https://www.youtube.com.evil.example/watch?v=targetvid01", "lookalike host"],
])("rejects a %s (%s)", (href) => {
expect(read(href)).toBeNull();
});
});
describe("firstVisibleRelatedWatchLink", () => {
function relatedLink(href, { visible = true, label = href } = {}) {
const element = document.createElement("a");
element.setAttribute("href", href);
element.textContent = label;
return {
element,
evaluate: async (callback, settings) => callback(element, settings),
isVisible: async () => visible,
};
}
function relatedPage(candidates) {
const locator = {
count: async () => candidates.length,
nth: (index) => candidates[index],
};
return {
locator,
page: {
locator: jest.fn(() => locator),
url: jest.fn(() => "https://www.youtube.com/watch?v=abcdefghijk"),
},
};
}
test("selects the first visible exact generic related anchor without renderer or thumbnail markup", async () => {
const offscreenThumbnail = relatedLink("/watch?v=targetvid01&pp=thumbnail", {
label: "thumbnail duplicate",
visible: false,
});
const currentVideo = relatedLink("/watch?v=abcdefghijk", { label: "current video" });
const excludedVideo = relatedLink("/watch?v=excluded001", { label: "visited video" });
const genericTitle = relatedLink("/watch?v=targetvid01&pp=title", { label: "generic title link" });
const visibleThumbnailDuplicate = relatedLink("/watch?v=targetvid01&pp=thumbnail", {
label: "visible thumbnail duplicate",
});
const laterTarget = relatedLink("/watch?v=latervideo1", { label: "later target" });
const { page } = relatedPage([
offscreenThumbnail,
currentVideo,
excludedVideo,
genericTitle,
visibleThumbnailDuplicate,
laterTarget,
]);
await expect(firstVisibleRelatedWatchLink(page, "abcdefghijk", ["excluded001"], 100)).resolves.toEqual({
link: genericTitle,
videoId: "targetvid01",
});
expect(page.locator).toHaveBeenCalledTimes(1);
expect(page.locator).toHaveBeenCalledWith('#related a[href*="/watch"]');
});
});
describe("live playback diagnostics", () => {
test("reports that playback was intentionally paused", async () => {
const videos = [{ pause: jest.fn() }, { pause: jest.fn() }];
const page = {
locator: jest.fn(() => ({ evaluateAll: async (callback) => callback(videos) })),
setDefaultNavigationTimeout: jest.fn(),
setDefaultTimeout: jest.fn(),
url: jest.fn(() => "https://www.youtube.com/shorts/abcdefghijk"),
};
const reportProgress = jest.fn();
const driver = new LiveYoutubeDriver(page, {}, { reportProgress });
await expect(driver.pausePlayback()).resolves.toEqual({ pauseFailures: [], pausedVideos: 2 });
expect(videos.every(({ pause }) => pause.mock.calls.length === 1)).toBe(true);
expect(reportProgress).toHaveBeenCalledWith("playback.paused", {
explanation: "The live smoke pauses media intentionally while it validates the current page",
pauseFailures: [],
pausedVideos: 2,
url: "https://www.youtube.com/shorts/abcdefghijk",
});
});
test("catches individual synchronous pause failures without waiting on media playback", async () => {
const videos = [
{ pause: jest.fn(() => undefined) },
{
pause: jest.fn(() => {
throw new Error("media state unavailable");
}),
},
];
const page = {
locator: jest.fn(() => ({ evaluateAll: async (callback) => callback(videos) })),
setDefaultNavigationTimeout: jest.fn(),
setDefaultTimeout: jest.fn(),
url: jest.fn(() => "https://www.youtube.com/shorts/abcdefghijk"),
};
const driver = new LiveYoutubeDriver(page, {});
await expect(driver.pausePlayback()).resolves.toEqual({
pauseFailures: ["media state unavailable"],
pausedVideos: 1,
});
});
test("waits for a new video URL without pausing until the caller requests stable playback", async () => {
const videos = [{ pause: jest.fn() }];
const page = {
locator: jest.fn(() => ({ evaluateAll: async (callback) => callback(videos) })),
setDefaultNavigationTimeout: jest.fn(),
setDefaultTimeout: jest.fn(),
url: jest.fn(() => "https://www.youtube.com/shorts/abcdefghijk"),
};
const driver = new LiveYoutubeDriver(page, {});
await driver.waitForVideoUrl("abcdefghijk");
expect(page.locator).not.toHaveBeenCalled();
expect(videos[0].pause).not.toHaveBeenCalled();
await driver.waitForVideo("abcdefghijk");
expect(page.locator).toHaveBeenCalledWith("video");
expect(videos[0].pause).toHaveBeenCalledTimes(1);
});
});
describe("watch ratio-bar soak", () => {
test("rechecks the current video, bar geometry, and stable count for the requested interval", async () => {
const page = {
setDefaultNavigationTimeout: jest.fn(),
setDefaultTimeout: jest.fn(),
};
const reportProgress = jest.fn();
const driver = new LiveYoutubeDriver(page, {}, { reportProgress });
driver.assertCurrentVideo = jest.fn();
driver.assertWatchRatioVisual = jest.fn(async () => ({ count: "123" }));
const result = await driver.soakWatchRatioVisual("userscript", {
durationMs: 1,
expectedCount: "123",
intervalMs: 1,
videoId: "targetvid01",
});
expect(driver.assertCurrentVideo).toHaveBeenCalledWith("targetvid01");
expect(driver.assertWatchRatioVisual).toHaveBeenCalledWith("userscript", { expectedCount: "123" });
expect(result).toEqual({
count: "123",
durationMs: 1,
sampleCount: driver.assertWatchRatioVisual.mock.calls.length,
videoId: "targetvid01",
});
expect(reportProgress).toHaveBeenNthCalledWith(1, "watch-ratio-soak.start", {
durationMs: 1,
expectedCount: "123",
runtime: "userscript",
videoId: "targetvid01",
});
expect(reportProgress).toHaveBeenLastCalledWith("watch-ratio-soak.complete", {
durationMs: 1,
expectedCount: "123",
runtime: "userscript",
sampleCount: result.sampleCount,
videoId: "targetvid01",
});
});
});
describe("reaction visual capture hygiene", () => {
function createCapturePage(tooltip, videoBoxes = []) {
const tooltipList = {
count: jest.fn(async () => 1),
nth: jest.fn(() => tooltip),
};
const videos = videoBoxes.map((measurement) => ({
boundingBox: jest.fn(async () => measurement),
isVisible: jest.fn(async () => true),
}));
const videoList = {
count: jest.fn(async () => videos.length),
nth: jest.fn((index) => videos[index]),
};
return {
evaluate: jest.fn(async () => undefined),
locator: jest.fn((selector) => (selector === "video" ? videoList : tooltipList)),
mouse: { move: jest.fn(async () => undefined) },
screenshot: jest.fn(async () => undefined),
setDefaultNavigationTimeout: jest.fn(),
setDefaultTimeout: jest.fn(),
};
}
test("parks the pointer inside the largest visible video and outside controls before capturing", async () => {
const tooltip = {
innerText: jest.fn(async () => "I like this"),
isVisible: jest.fn().mockResolvedValueOnce(true).mockResolvedValue(false),
};
const preloadedVideo = box(0, 0, 320, 180);
const currentVideo = box(100, 40, 800, 640);
const page = createCapturePage(tooltip, [preloadedVideo, currentVideo]);
const driver = new LiveYoutubeDriver(page, {}, { visualTooltipTimeout: 1_000 });
const control = box(900, 500, 48, 78);
driver.readViewportSize = jest.fn(async () => ({ height: 720, width: 1280 }));
await expect(driver.captureCroppedScreenshot("short-liked.png", [control])).resolves.toEqual({
height: 102,
width: 72,
x: 888,
y: 488,
});
const [pointerX, pointerY] = page.mouse.move.mock.calls[0];
expect({ x: pointerX, y: pointerY }).toEqual({ x: 500, y: 360 });
expect(pointerX).toBeGreaterThan(currentVideo.x);
expect(pointerX).toBeLessThan(currentVideo.x + currentVideo.width);
expect(pointerY).toBeGreaterThan(currentVideo.y);
expect(pointerY).toBeLessThan(currentVideo.y + currentVideo.height);
expect(
pointerX < control.x ||
pointerX > control.x + control.width ||
pointerY < control.y ||
pointerY > control.y + control.height,
).toBe(true);
expect(tooltip.isVisible).toHaveBeenCalledTimes(2);
expect(page.mouse.move.mock.invocationCallOrder[0]).toBeLessThan(page.screenshot.mock.invocationCallOrder[0]);
expect(page.screenshot).toHaveBeenCalledWith(
expect.objectContaining({ path: "short-liked.png", clip: { height: 102, width: 72, x: 888, y: 488 } }),
);
});
test("fails within the configured bound instead of capturing a persistent native tooltip", async () => {
const tooltip = {
innerText: jest.fn(async () => "I like this"),
isVisible: jest.fn(async () => true),
};
const page = createCapturePage(tooltip);
const driver = new LiveYoutubeDriver(page, {}, { visualTooltipTimeout: 1 });
driver.readViewportSize = jest.fn(async () => ({ height: 720, width: 1280 }));
await expect(driver.captureCroppedScreenshot("obscured.png", [box(900, 500, 48, 78)])).rejects.toThrow(
/Timed out waiting for native YouTube tooltips.*I like this/,
);
expect(page.mouse.move).toHaveBeenCalledTimes(1);
expect(page.screenshot).not.toHaveBeenCalled();
});
});
describe("watch reaction screenshot bounds", () => {
function visibleBoxLocator(measurement) {
const locator = {
boundingBox: jest.fn(async () => measurement),
count: jest.fn(async () => 1),
isVisible: jest.fn(async () => true),
nth: jest.fn(() => locator),
scrollIntoViewIfNeeded: jest.fn(async () => undefined),
};
return locator;
}
test("passes the full ratio wrapper to the crop so its bottom label is retained", async () => {
const viewport = { height: 300, width: 500 };
const likeBox = box(100, 100, 80, 40);
const dislikeBox = box(180, 100, 80, 40);
const containerBox = box(100, 144, 160, 2);
const barBox = box(100, 144, 120, 2);
const wrapperBox = box(100, 140, 160, 62);
const like = visibleBoxLocator(likeBox);
const dislike = visibleBoxLocator(dislikeBox);
const bar = visibleBoxLocator(barBox);
const wrapper = visibleBoxLocator(wrapperBox);
const container = visibleBoxLocator(containerBox);
container.locator = jest.fn(() => wrapper);
const page = {
locator: jest.fn((selector) => (selector === "#return-youtube-dislike-bar-container" ? container : bar)),
setDefaultNavigationTimeout: jest.fn(),
setDefaultTimeout: jest.fn(),
};
const driver = new LiveYoutubeDriver(page, {});
driver.captureCroppedScreenshot = jest.fn(async (_path, boxes) => croppedScreenshotClip(boxes, viewport));
driver.readViewportSize = jest.fn(async () => viewport);
driver.visibleDislikeButton = jest.fn(async () => dislike);
driver.visibleLikeButton = jest.fn(async () => like);
driver.waitForDislikeText = jest.fn(async () => "123");
const result = await driver.captureWatchRatioVisual("userscript", "watch-liked.png");
expect(driver.captureCroppedScreenshot).toHaveBeenCalledWith("watch-liked.png", [
likeBox,
dislikeBox,
containerBox,
wrapperBox,
]);
expect(result.screenshotClip.y + result.screenshotClip.height).toBe(wrapperBox.y + wrapperBox.height + 12);
expect(result.screenshotClip.y + result.screenshotClip.height).toBeGreaterThan(
containerBox.y + containerBox.height + 12,
);
});
test("rejects duplicate visible runtime bars before taking evidence", async () => {
const candidates = [{ isVisible: jest.fn(async () => true) }, { isVisible: jest.fn(async () => true) }];
const duplicateContainers = {
count: jest.fn(async () => candidates.length),
nth: jest.fn((index) => candidates[index]),
};
const bar = visibleBoxLocator(box(100, 144, 120, 2));
const page = {
locator: jest.fn((selector) =>
selector === "#return-youtube-dislike-bar-container" ? duplicateContainers : bar,
),
setDefaultNavigationTimeout: jest.fn(),
setDefaultTimeout: jest.fn(),
};
const driver = new LiveYoutubeDriver(page, {});
await expect(driver.captureWatchRatioVisual("userscript", "duplicate.png")).rejects.toThrow(
"Expected exactly one visible userscript watch ratio bar; found 2.",
);
});
});
describe("live Shorts Next navigation retry", () => {
test("retries exactly once when YouTube ignores the first trusted click", async () => {
let navigated = false;
const click = jest.fn().mockResolvedValue(undefined);
const reportProgress = jest.fn();
const waitForNavigation = jest
.fn()
.mockRejectedValueOnce(new Error("first navigation timed out"))
.mockImplementationOnce(async () => {
navigated = true;
});
await expect(
clickWithSingleNavigationRetry({
click,
hasNavigated: () => navigated,
reportProgress,
retryDetails: { previousVideoId: "abcdefghijk" },
waitForNavigation,
}),
).resolves.toEqual({ retried: true });
expect(click.mock.calls).toEqual([
[1, 5_000],
[2, 25_000],
]);
expect(waitForNavigation.mock.calls.map(([timeout]) => timeout)).toEqual([5_000, 25_000]);
expect(reportProgress).toHaveBeenCalledTimes(1);
expect(reportProgress).toHaveBeenCalledWith("shorts-next-control.retrying", {
firstFailure: "first navigation timed out",
firstTimeoutMs: 5_000,
previousVideoId: "abcdefghijk",
retryTimeoutMs: 25_000,
});
});
test("fails after the single retry instead of clicking a third time", async () => {
const click = jest.fn().mockResolvedValue(undefined);
const reportProgress = jest.fn();
const waitForNavigation = jest
.fn()
.mockRejectedValueOnce(new Error("first navigation timed out"))
.mockRejectedValueOnce(new Error("retry navigation timed out"));
await expect(
clickWithSingleNavigationRetry({
click,
hasNavigated: () => false,
reportProgress,
retryDetails: { previousVideoId: "abcdefghijk" },
waitForNavigation,
}),
).rejects.toThrow(/first Shorts Next click or its single retry/);
expect(click).toHaveBeenCalledTimes(2);
expect(waitForNavigation).toHaveBeenCalledTimes(2);
expect(reportProgress).toHaveBeenCalledTimes(1);
});
test("does not retry when navigation completed at the first timeout boundary", async () => {
let navigated = false;
const click = jest.fn().mockResolvedValue(undefined);
const reportProgress = jest.fn();
const waitForNavigation = jest.fn(async () => {
navigated = true;
throw new Error("navigation event timed out after the URL changed");
});
await expect(
clickWithSingleNavigationRetry({
click,
hasNavigated: () => navigated,
reportProgress,
retryDetails: { previousVideoId: "abcdefghijk" },
waitForNavigation,
}),
).resolves.toEqual({ retried: false });
expect(click).toHaveBeenCalledTimes(1);
expect(reportProgress).not.toHaveBeenCalled();
});
});
describe("watch ratio viewport alignment", () => {
const viewport = { height: 844, width: 375 };
const like = box(-25.828, 700, 80.77, 40);
const dislike = box(54.942, 700, 84.658, 40);
test("allows the ratio bar to share YouTube's native horizontal clipping exactly", () => {
const container = box(-25.828, 744, 165.428, 2);
expect(assertWatchRatioViewportAlignment(container, like, dislike, viewport)).toEqual({
nativeControlsAreHorizontallyClipped: true,
nativeLeft: -25.828,
nativeRight: 139.6,
});
});
test("rejects ratio-bar clipping that extends beyond the native reaction controls", () => {
const container = box(-35.828, 744, 175.428, 2);
expect(() => assertWatchRatioViewportAlignment(container, like, dislike, viewport)).toThrow(
/left edge alignment with native reaction controls/,
);
});
test("retains the strict viewport assertion when native controls are in bounds", () => {
const inBoundsLike = box(10, 700, 80, 40);
const inBoundsDislike = box(90, 700, 85, 40);
const clippedContainer = box(-2, 744, 177, 2);
expect(() => assertWatchRatioViewportAlignment(clippedContainer, inBoundsLike, inBoundsDislike, viewport)).toThrow(
/Watch ratio bar is clipped past the viewport's left edge/,
);
});
});
describe("assertReactionPressedStates", () => {
test.each([
["neutral", { dislikeState: "false", likeState: "false" }],
["liked", { dislikeState: "false", likeState: "true" }],
["disliked", { dislikeState: "true", likeState: "false" }],
])("accepts the exact mutually-exclusive %s state", (expectedState, pressedStates) => {
expect(() => assertReactionPressedStates(pressedStates, expectedState)).not.toThrow();
});
test("rejects a valid but unexpected pressed state", () => {
expect(() => assertReactionPressedStates({ dislikeState: "false", likeState: "true" }, "neutral")).toThrow(
/Expected YouTube reaction state neutral/,
);
});
test("rejects Like and Dislike being selected together", () => {
expect(() => assertReactionPressedStates({ dislikeState: "true", likeState: "true" }, "liked")).toThrow(
/Like and Dislike as selected/,
);
});
test("rejects an invalid pressed-state value", () => {
expect(() => assertReactionPressedStates({ dislikeState: null, likeState: "false" }, "neutral")).toThrow(
/Unexpected YouTube dislike state/,
);
});
});
describe("assertSyntheticShortsGeometry", () => {
test("accepts the measured native Shorts action geometry", () => {
expect(() => assertSyntheticShortsGeometry(validSyntheticShortsGeometry())).not.toThrow();
});
test.each([
["icon container", "icon"],
["SVG", "svg"],
])("rejects an 8px-wide synthetic %s", (_label, property) => {
const measurement = validSyntheticShortsGeometry();
measurement.synthetic[property].width = 8;
expect(() => assertSyntheticShortsGeometry(measurement)).toThrow(
new RegExp(`Synthetic Shorts ${property === "svg" ? "SVG" : "icon container"} width`),
);
});
test("rejects a synthetic action host with a 16px margin", () => {
const measurement = validSyntheticShortsGeometry();
measurement.synthetic.hostStyle.marginTop = 16;
expect(() => assertSyntheticShortsGeometry(measurement)).toThrow(/Synthetic Shorts action host marginTop/);
});
test("rejects a gap between Like and the synthetic action", () => {
const measurement = validSyntheticShortsGeometry();
measurement.synthetic.host.y += 16;
measurement.next.host.y += 16;
expect(() => assertSyntheticShortsGeometry(measurement)).toThrow(
/Gap between native Like and synthetic Shorts action hosts/,
);
});
test("rejects a gap between the synthetic and following actions", () => {
const measurement = validSyntheticShortsGeometry();
measurement.next.host.y += 2;
expect(() => assertSyntheticShortsGeometry(measurement)).toThrow(
/Gap between synthetic Shorts and following action hosts/,
);
});
test.each([
["button", "width", 44, /Synthetic Shorts button width/],
["label", "height", 68, /Synthetic Shorts label height/],
["host", "height", 80, /Synthetic Shorts action host height/],
])("rejects a mismatched synthetic %s %s", (part, dimension, value, message) => {
const measurement = validSyntheticShortsGeometry();
measurement.synthetic[part][dimension] = value;
expect(() => assertSyntheticShortsGeometry(measurement)).toThrow(message);
});
test("rejects incorrect action-host padding", () => {
const measurement = validSyntheticShortsGeometry();
measurement.synthetic.hostStyle.paddingBottom = 0;
expect(() => assertSyntheticShortsGeometry(measurement)).toThrow(/Synthetic Shorts action host paddingBottom/);
});
test.each([
["fontSize", 14, /Synthetic Shorts count font-size/],
["lineHeight", 16, /Synthetic Shorts count line-height/],
["fontFamily", "Arial", /Synthetic Shorts count fontFamily/],
])("rejects mismatched count %s", (property, value, message) => {
const measurement = validSyntheticShortsGeometry();
measurement.synthetic.countStyle[property] = value;
expect(() => assertSyntheticShortsGeometry(measurement)).toThrow(message);
});
test("rejects a control that is off the native action-column center", () => {
const measurement = validSyntheticShortsGeometry();
measurement.synthetic.button.x += 2;
expect(() => assertSyntheticShortsGeometry(measurement)).toThrow(/Synthetic Shorts button horizontal center/);
});
test("allows a one-pixel fractional rendering difference", () => {
const measurement = validSyntheticShortsGeometry();
measurement.synthetic.button.width = 47;
measurement.synthetic.svg.height = 24.75;
measurement.next.host.x = 101;
expect(() => assertSyntheticShortsGeometry(measurement)).not.toThrow();
});
});
describe("assertNativeShortsPairGeometry", () => {
test("accepts an exact active native Like/Dislike action pair", () => {
expect(() => assertNativeShortsPairGeometry(validNativeShortsPairGeometry())).not.toThrow();
});
test("rejects controls selected from different retained Shorts reels", () => {
const measurement = validNativeShortsPairGeometry();
measurement.dislike.reelIndex = 1;
expect(() => assertNativeShortsPairGeometry(measurement)).toThrow(/belong to different reels/);
});
test("rejects a native Dislike that is not immediately after Like", () => {
const measurement = validNativeShortsPairGeometry();
measurement.dislike.actionIndex += 1;
expect(() => assertNativeShortsPairGeometry(measurement)).toThrow(/not immediately after Like/);
});
test("rejects a shrunken native Dislike icon", () => {
const measurement = validNativeShortsPairGeometry();
measurement.dislike.icon.width = 8;
expect(() => assertNativeShortsPairGeometry(measurement)).toThrow(/Native Shorts Dislike icon container width/);
});
test("rejects broken native action-stack spacing", () => {
const measurement = validNativeShortsPairGeometry();
measurement.dislike.host.y += 12;
expect(() => assertNativeShortsPairGeometry(measurement)).toThrow(
/Gap between native Shorts Like and Dislike action hosts/,
);
});
test("rejects native Dislike count typography that diverges from Like", () => {
const measurement = validNativeShortsPairGeometry();
measurement.dislike.countStyle.fontFamily = "Comic Sans MS";
expect(() => assertNativeShortsPairGeometry(measurement)).toThrow(/count fontFamily does not match/);
});
});
describe("live Shorts full-stack visual readiness", () => {
const viewport = { height: 844, width: 390 };
const validStack = () => [0, 1, 2, 3, 4].map((index) => box(330, 100 + index * 78, 48, 78));
test("accepts five aligned visible controls and painted icons", () => {
expect(() => assertShortsActionStackGeometry(validStack(), viewport)).not.toThrow();
expect(
isShortsIconVisualReady({
effectiveOpacity: 1,
paintedGraphicCount: 1,
rendered: true,
svgPresent: true,
}),
).toBe(true);
});
test("rejects an incomplete Shorts stack", () => {
expect(() => assertShortsActionStackGeometry(validStack().slice(0, 4), viewport)).toThrow(
/full Shorts action stack/,
);
});
test("rejects a misaligned or clipped control", () => {
const misaligned = validStack();
misaligned[3].x -= 8;
expect(() => assertShortsActionStackGeometry(misaligned, viewport)).toThrow(/horizontal center/);
const clipped = validStack();
clipped[4].y = 800;
expect(() => assertShortsActionStackGeometry(clipped, viewport)).toThrow(/bottom edge/);
});
test.each([
["unhydrated SVG", { effectiveOpacity: 1, paintedGraphicCount: 0, rendered: true, svgPresent: true }],
["missing SVG", { effectiveOpacity: 1, paintedGraphicCount: 0, rendered: true, svgPresent: false }],
["hidden ancestor", { effectiveOpacity: 0, paintedGraphicCount: 1, rendered: true, svgPresent: true }],
["non-rendered ancestor", { effectiveOpacity: 1, paintedGraphicCount: 1, rendered: false, svgPresent: true }],
])("does not screenshot a %s as a ready icon", (_label, state) => {
expect(isShortsIconVisualReady(state)).toBe(false);
});
});