Files
return-youtube-dislike/Extensions/UserScript/e2e/userscript-voting.e2e.js
T
Anarios 3657de9b2c 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.
2026-09-01 23:41:28 +02:00

1029 lines
42 KiB
JavaScript

const { test, expect, devices } = require("@playwright/test");
const {
CREDENTIAL_KEY,
VIDEO_A,
VIDEO_B,
createFakeBackend,
injectGeneratedUserscript,
installGmEnvironment,
installHermeticRoutes,
openShortsFixture,
openWatchFixture,
readGmValue,
} = require("./harness");
const EXISTING_CREDENTIALS = {
userId: "ExistingUserscriptCredential000000000001",
registrationConfirmed: true,
};
const SYNTHETIC_STATE_KEY_PREFIX = "rydSyntheticDislikedShort:";
const PIXEL_5 = Object.fromEntries(Object.entries(devices["Pixel 5"]).filter(([key]) => key !== "defaultBrowserType"));
function monitorPage(page) {
const consoleErrors = [];
const pageErrors = [];
page.on("console", (message) => {
if (message.type() === "error") consoleErrors.push(message.text());
});
page.on("pageerror", (error) => pageErrors.push(error.message));
return { page, consoleErrors, pageErrors };
}
async function installUnhandledRejectionCapture(context) {
await context.addInitScript(() => {
globalThis.__unhandledRejections = [];
addEventListener("unhandledrejection", (event) => {
const reason = event.reason;
globalThis.__unhandledRejections.push(reason instanceof Error ? reason.message : String(reason));
});
});
}
async function launchHarness(
{ context, page },
{
backendOptions,
beforeInject,
coloredThumbs,
disableVoteSubmission = false,
gmValues,
hostname = "www.youtube.com",
pageKind = "watch",
videoId = VIDEO_A,
} = {},
) {
const backend = createFakeBackend(backendOptions);
const monitoredPages = [monitorPage(page)];
await installUnhandledRejectionCapture(context);
await installGmEnvironment(context, gmValues);
await installHermeticRoutes(context, backend);
if (pageKind === "shorts") await openShortsFixture(page, videoId, { hostname });
else await openWatchFixture(page, videoId, { hostname });
if (beforeInject) await beforeInject(page);
await injectGeneratedUserscript(page, { coloredThumbs, disableVoteSubmission });
return { backend, monitoredPages };
}
async function expectNoRuntimeFailures({ backend, monitoredPages }, { allowedConsoleErrors = [] } = {}) {
expect(backend.blockedRequests, "all network traffic must be served by the hermetic harness").toEqual([]);
for (const monitored of monitoredPages) {
const unexpectedConsoleErrors = monitored.consoleErrors.filter(
(message) => !allowedConsoleErrors.some((pattern) => pattern.test(message)),
);
expect(unexpectedConsoleErrors, "unexpected browser console errors").toEqual([]);
expect(monitored.pageErrors, "uncaught page errors").toEqual([]);
expect(
await monitored.page.evaluate(() => globalThis.__unhandledRejections || []),
"unhandled promise rejections",
).toEqual([]);
}
}
async function waitForCredentials(page) {
let credentials = null;
await expect
.poll(async () => {
credentials = await readGmValue(page, CREDENTIAL_KEY);
return credentials;
})
.toMatchObject({ registrationConfirmed: true });
return credentials;
}
function visibleVoteButton(page, role) {
return page.locator(`[data-ryd-role="${role}"]:visible button`);
}
async function waitForDislikeCount(page, value) {
await expect(page.locator('[data-ryd-role="dislike"]:visible #text')).toHaveText(String(value));
}
async function clickVoteAndWait(page, backend, role, expectedConfirmations) {
await visibleVoteButton(page, role).click();
await expect.poll(() => backend.requestsFor("POST", "/interact/confirmVote").length).toBe(expectedConfirmations);
}
test("fresh startup eagerly registers and submits a complete dislike handshake", async ({ context, page }) => {
const harness = await launchHarness({ context, page });
const { backend } = harness;
await waitForDislikeCount(page, 25);
const credentials = await waitForCredentials(page);
expect(credentials.userId).toHaveLength(36);
const registrationGet = backend.requestsFor("GET", "/puzzle/registration");
const registrationPost = backend.requestsFor("POST", "/puzzle/registration");
expect(registrationGet).toHaveLength(1);
expect(registrationPost).toHaveLength(1);
expect(registrationGet[0].query.userId).toBe(credentials.userId);
expect(Buffer.from(registrationPost[0].body.solution, "base64")).toHaveLength(4);
expect(backend.requestsFor("POST", "/interact/vote")).toHaveLength(0);
expect(backend.requestsFor("POST", "/interact/confirmVote")).toHaveLength(0);
await clickVoteAndWait(page, backend, "dislike", 1);
const vote = backend.requestsFor("POST", "/interact/vote")[0];
const confirmation = backend.requestsFor("POST", "/interact/confirmVote")[0];
expect(vote.body).toEqual({ userId: credentials.userId, videoId: VIDEO_A, value: -1 });
expect(confirmation.body.userId).toBe(credentials.userId);
expect(confirmation.body.videoId).toBe(VIDEO_A);
expect(Buffer.from(confirmation.body.solution, "base64")).toHaveLength(4);
await expectNoRuntimeFailures(harness);
});
test("credentials survive a page reload and prevent duplicate registration", async ({ context, page }) => {
const harness = await launchHarness({ context, page });
const { backend } = harness;
const firstCredentials = await waitForCredentials(page);
await waitForDislikeCount(page, 25);
const registrationCount = backend.requestsFor("GET", "/puzzle/registration").length;
await page.reload({ waitUntil: "domcontentloaded" });
await injectGeneratedUserscript(page);
await waitForDislikeCount(page, 25);
expect(await readGmValue(page, CREDENTIAL_KEY)).toEqual(firstCredentials);
expect(backend.requestsFor("GET", "/puzzle/registration")).toHaveLength(registrationCount);
await clickVoteAndWait(page, backend, "like", 1);
expect(backend.requestsFor("POST", "/interact/vote")[0].body).toEqual({
userId: firstCredentials.userId,
videoId: VIDEO_A,
value: 1,
});
await expectNoRuntimeFailures(harness);
});
test("a second page reuses the registered identity", async ({ context, page }) => {
const harness = await launchHarness({ context, page });
const { backend, monitoredPages } = harness;
const credentials = await waitForCredentials(page);
const secondPage = await context.newPage();
monitoredPages.push(monitorPage(secondPage));
await openWatchFixture(secondPage, VIDEO_B);
await injectGeneratedUserscript(secondPage);
await waitForDislikeCount(secondPage, 25);
expect(backend.requestsFor("GET", "/puzzle/registration")).toHaveLength(1);
await clickVoteAndWait(secondPage, backend, "like", 1);
expect(backend.requestsFor("POST", "/interact/vote")[0].body).toEqual({
userId: credentials.userId,
videoId: VIDEO_B,
value: 1,
});
await expectNoRuntimeFailures(harness);
});
const initialStateTransitions = [
{ initialState: "neutral", action: "like", expectedValue: 1, expectedDislikes: 25 },
{ initialState: "neutral", action: "dislike", expectedValue: -1, expectedDislikes: 26 },
{ initialState: "liked", action: "like", expectedValue: 0, expectedDislikes: 25 },
{ initialState: "liked", action: "dislike", expectedValue: -1, expectedDislikes: 26 },
{ initialState: "disliked", action: "like", expectedValue: 1, expectedDislikes: 24 },
{ initialState: "disliked", action: "dislike", expectedValue: 0, expectedDislikes: 24 },
];
for (const scenario of initialStateTransitions) {
test(`${scenario.initialState} plus ${scenario.action} submits ${scenario.expectedValue}`, async ({
context,
page,
}) => {
const harness = await launchHarness(
{ context, page },
{
backendOptions: { fixture: { initialState: scenario.initialState } },
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
},
);
const { backend } = harness;
await waitForDislikeCount(page, 25);
await clickVoteAndWait(page, backend, scenario.action, 1);
expect(backend.requestsFor("GET", "/puzzle/registration")).toHaveLength(0);
expect(backend.requestsFor("POST", "/interact/vote")[0].body.value).toBe(scenario.expectedValue);
await waitForDislikeCount(page, scenario.expectedDislikes);
await expectNoRuntimeFailures(harness);
});
}
test("disabled vote submission keeps local UI transitions without API interaction", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
disableVoteSubmission: true,
},
);
const { backend } = harness;
await waitForDislikeCount(page, 25);
await waitForCredentials(page);
await visibleVoteButton(page, "dislike").click();
await waitForDislikeCount(page, 26);
await page.waitForTimeout(100);
expect(backend.requestsFor("POST", "/interact/vote")).toHaveLength(0);
expect(backend.requestsFor("POST", "/interact/confirmVote")).toHaveLength(0);
expect(backend.requestsFor("GET", "/puzzle/registration")).toHaveLength(1);
await expectNoRuntimeFailures(harness);
});
test("signed-out controls never submit a vote", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
backendOptions: { fixture: { signedIn: false } },
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
},
);
const { backend } = harness;
await waitForDislikeCount(page, 25);
await visibleVoteButton(page, "dislike").click();
await page.waitForTimeout(100);
expect(backend.requestsFor("POST", "/interact/vote")).toHaveLength(0);
expect(backend.requestsFor("POST", "/interact/confirmVote")).toHaveLength(0);
await expectNoRuntimeFailures(harness);
});
test("late button insertion still initializes rendering and voting", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
backendOptions: { fixture: { initialButtons: false } },
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
},
);
const { backend } = harness;
await page.waitForTimeout(150);
await page.evaluate(() => window.__youtubeFixture.insertButtons("neutral"));
await waitForDislikeCount(page, 25);
await clickVoteAndWait(page, backend, "dislike", 1);
expect(backend.requestsFor("POST", "/interact/vote")[0].body.videoId).toBe(VIDEO_A);
await expectNoRuntimeFailures(harness);
});
test("an early vote is reconciled onto a delayed same-video count", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
backendOptions: {
countDelayByVideo: { [VIDEO_A]: 350 },
},
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
},
);
await expect.poll(() => harness.backend.requestsFor("GET", "/votes").length).toBe(1);
await visibleVoteButton(page, "dislike").click();
await expect.poll(() => harness.backend.requestsFor("POST", "/interact/confirmVote").length).toBe(1);
await waitForDislikeCount(page, 26);
await visibleVoteButton(page, "dislike").click();
await expect.poll(() => harness.backend.requestsFor("POST", "/interact/confirmVote").length).toBe(2);
await waitForDislikeCount(page, 25);
expect(harness.backend.requestsFor("POST", "/interact/vote").map((request) => request.body.value)).toEqual([-1, 0]);
await expectNoRuntimeFailures(harness);
});
test("SPA navigation ignores a stale count and submits only the current video id", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
backendOptions: {
countDelayByVideo: { [VIDEO_A]: 300 },
countsByVideo: {
[VIDEO_A]: { dislikes: 11, likes: 100 },
[VIDEO_B]: { dislikes: 22, likes: 200 },
},
},
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
},
);
const { backend } = harness;
await expect
.poll(() => backend.requestsFor("GET", "/votes").filter((request) => request.query.videoId === VIDEO_A).length)
.toBe(1);
await page.evaluate((videoId) => window.__youtubeFixture.navigate(videoId), VIDEO_B);
await expect
.poll(() => backend.requestsFor("GET", "/votes").filter((request) => request.query.videoId === VIDEO_B).length)
.toBe(1);
await waitForDislikeCount(page, 22);
await page.evaluate(() => {
document.dispatchEvent(new Event("yt-navigate-finish", { bubbles: true }));
document.dispatchEvent(new Event("yt-navigate-finish", { bubbles: true }));
});
await page.waitForTimeout(350);
await waitForDislikeCount(page, 22);
await clickVoteAndWait(page, backend, "dislike", 1);
const votes = backend.requestsFor("POST", "/interact/vote");
expect(votes).toHaveLength(1);
expect(votes[0].body.videoId).toBe(VIDEO_B);
await expectNoRuntimeFailures(harness);
});
test("navigation during an in-flight vote does not block the next video", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
backendOptions: {
countsByVideo: {
[VIDEO_A]: { dislikes: 11, likes: 100 },
[VIDEO_B]: { dislikes: 22, likes: 200 },
},
},
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
},
);
const { backend } = harness;
backend.enqueue("POST", "/interact/confirmVote", { body: true, delayMs: 1_000 });
await waitForDislikeCount(page, 11);
await visibleVoteButton(page, "dislike").click();
await expect.poll(() => backend.requestsFor("POST", "/interact/confirmVote").length).toBe(1);
await page.evaluate((videoId) => window.__youtubeFixture.navigate(videoId), VIDEO_B);
await waitForDislikeCount(page, 22);
await visibleVoteButton(page, "like").click();
await expect.poll(() => backend.requestsFor("POST", "/interact/confirmVote").length).toBe(2);
await expect
.poll(() => backend.requestsFor("POST", "/interact/confirmVote").every((request) => request.respondedAt))
.toBe(true);
const votes = backend.requestsFor("POST", "/interact/vote");
const confirmations = backend.requestsFor("POST", "/interact/confirmVote");
expect(votes.map((request) => request.body.videoId)).toEqual([VIDEO_A, VIDEO_B]);
expect(confirmations.map((request) => request.body.videoId)).toEqual([VIDEO_A, VIDEO_B]);
expect(votes[1].at).toBeLessThan(confirmations[0].respondedAt);
await expectNoRuntimeFailures(harness);
});
test("active Shorts controls switch video identity", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
backendOptions: {
countsByVideo: {
[VIDEO_A]: { dislikes: 11, likes: 100 },
[VIDEO_B]: { dislikes: 22, likes: 200 },
},
},
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
pageKind: "shorts",
},
);
const { backend } = harness;
await waitForDislikeCount(page, 11);
await page.evaluate((videoId) => window.__shortsFixture.activate(videoId), VIDEO_B);
await waitForDislikeCount(page, 22);
await expect(page.locator("[data-short-video]:not([hidden])")).toHaveCount(1);
await clickVoteAndWait(page, backend, "dislike", 1);
expect(backend.requestsFor("POST", "/interact/vote")[0].body.videoId).toBe(VIDEO_B);
expect(backend.requestsFor("GET", "/votes").filter((request) => request.query.videoId === VIDEO_A)).toHaveLength(1);
expect(backend.requestsFor("GET", "/votes").filter((request) => request.query.videoId === VIDEO_B)).toHaveLength(1);
await expectNoRuntimeFailures(harness);
});
test("a recycled Shorts renderer is retagged and initialized for its new video", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
backendOptions: {
countsByVideo: {
[VIDEO_A]: { dislikes: 11, likes: 100 },
[VIDEO_B]: { dislikes: 22, likes: 200 },
},
},
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
pageKind: "shorts",
},
);
await waitForDislikeCount(page, 11);
await page.evaluate((videoId) => window.__shortsFixture.recycleActiveRenderer(videoId), VIDEO_B);
await waitForDislikeCount(page, 22);
await expect(page.locator("[data-ryd-synthetic-shorts-dislike]:visible")).toHaveAttribute(
"data-ryd-video-id",
VIDEO_B,
);
await expect(visibleVoteButton(page, "dislike")).toHaveAttribute("aria-disabled", "false");
await expect(visibleVoteButton(page, "dislike")).toHaveAttribute("aria-pressed", "false");
await visibleVoteButton(page, "dislike").click();
await expect.poll(() => harness.backend.requestsFor("POST", "/interact/confirmVote").length).toBe(1);
expect(harness.backend.requestsFor("POST", "/interact/vote")[0].body.videoId).toBe(VIDEO_B);
await expectNoRuntimeFailures(harness);
});
test("current desktop Shorts UI gets one owned dislike control and covers all six transitions", async ({
context,
page,
}) => {
const harness = await launchHarness(
{ context, page },
{
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
pageKind: "shorts",
},
);
const { backend } = harness;
await waitForDislikeCount(page, 25);
const synthetic = page.locator("[data-ryd-synthetic-shorts-dislike]:visible");
await expect(synthetic).toHaveCount(1);
await expect(synthetic.locator("button")).toHaveAttribute("aria-label", "Dislike this video");
await expect(page.locator("dislike-button-view-model, #dislike-button")).toHaveCount(0);
await expect(page.locator('[data-fixture-control="comments"]:visible')).toContainText("12");
await expect(page.locator('[data-fixture-control="share"]:visible')).toContainText("Share");
await expect(page.locator('[data-fixture-control="remix"]:visible')).toContainText("Remix");
const actions = ["like", "like", "dislike", "like", "dislike", "dislike"];
const expectedStates = ["liked", "neutral", "disliked", "liked", "disliked", "neutral"];
const expectedValues = [1, 0, -1, 1, -1, 0];
for (let index = 0; index < actions.length; index += 1) {
await visibleVoteButton(page, actions[index]).click();
await expect.poll(() => backend.requestsFor("POST", "/interact/confirmVote").length).toBe(index + 1);
await expect
.poll(async () => {
const likePressed = await visibleVoteButton(page, "like").getAttribute("aria-pressed");
const dislikePressed = await visibleVoteButton(page, "dislike").getAttribute("aria-pressed");
if (likePressed === "true") return "liked";
if (dislikePressed === "true") return "disliked";
return "neutral";
})
.toBe(expectedStates[index]);
}
expect(backend.requestsFor("POST", "/interact/vote").map((request) => request.body.value)).toEqual(expectedValues);
await expect(synthetic).toHaveCount(1);
await expect(page.locator('[data-fixture-control="comments"]:visible')).toContainText("12");
await expectNoRuntimeFailures(harness);
});
test("modern Shorts supports the colored-thumbs option", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
coloredThumbs: true,
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
pageKind: "shorts",
},
);
await waitForDislikeCount(page, 25);
await visibleVoteButton(page, "dislike").click();
await expect.poll(() => harness.backend.requestsFor("POST", "/interact/confirmVote").length).toBe(1);
await expect.poll(() => visibleVoteButton(page, "dislike").evaluate((button) => button.style.color)).toBe("red");
await visibleVoteButton(page, "dislike").click();
await expect.poll(() => harness.backend.requestsFor("POST", "/interact/confirmVote").length).toBe(2);
await expect.poll(() => visibleVoteButton(page, "dislike").evaluate((button) => button.style.color)).toBe("unset");
await expectNoRuntimeFailures(harness);
});
test("a recreated synthetic Shorts control is restored and rebound exactly once", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
pageKind: "shorts",
},
);
await waitForDislikeCount(page, 25);
await page.evaluate(() => window.__shortsFixture.removeSyntheticDislike());
await expect(page.locator("[data-ryd-synthetic-shorts-dislike]:visible")).toHaveCount(1);
await waitForDislikeCount(page, 25);
await visibleVoteButton(page, "dislike").click();
await expect.poll(() => harness.backend.requestsFor("POST", "/interact/confirmVote").length).toBe(1);
expect(harness.backend.requestsFor("POST", "/interact/vote").map((request) => request.body.value)).toEqual([-1]);
await expect(page.locator("[data-ryd-synthetic-shorts-dislike]:visible")).toHaveCount(1);
await expectNoRuntimeFailures(harness);
});
test("a replaced Shorts action bar gets a fresh initialized control", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
pageKind: "shorts",
},
);
await waitForDislikeCount(page, 25);
await page.evaluate(() => window.__shortsFixture.replaceActionBar());
await expect(page.locator("[data-ryd-synthetic-shorts-dislike]:visible")).toHaveCount(1);
await waitForDislikeCount(page, 25);
await expect(visibleVoteButton(page, "dislike")).toHaveAttribute("aria-disabled", "false");
await visibleVoteButton(page, "dislike").click();
await expect.poll(() => harness.backend.requestsFor("POST", "/interact/confirmVote").length).toBe(1);
expect(harness.backend.requestsFor("POST", "/interact/vote").map((request) => request.body.value)).toEqual([-1]);
await expectNoRuntimeFailures(harness);
});
test("a pending count keeps its optimistic delta across same-video action-bar replacement", async ({
context,
page,
}) => {
const harness = await launchHarness(
{ context, page },
{
backendOptions: {
countDelayByVideo: { [VIDEO_A]: 350 },
},
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
pageKind: "shorts",
},
);
await expect.poll(() => harness.backend.requestsFor("GET", "/votes").length).toBe(1);
await visibleVoteButton(page, "dislike").click();
await expect.poll(() => harness.backend.requestsFor("POST", "/interact/confirmVote").length).toBe(1);
await page.evaluate(() => window.__shortsFixture.replaceActionBar());
await expect(page.locator("[data-ryd-synthetic-shorts-dislike]:visible")).toHaveCount(1);
await waitForDislikeCount(page, 26);
await expect(visibleVoteButton(page, "dislike")).toHaveAttribute("aria-pressed", "true");
expect(harness.backend.requestsFor("GET", "/votes")).toHaveLength(1);
await visibleVoteButton(page, "dislike").click();
await expect.poll(() => harness.backend.requestsFor("POST", "/interact/confirmVote").length).toBe(2);
await waitForDislikeCount(page, 25);
expect(harness.backend.requestsFor("POST", "/interact/vote").map((request) => request.body.value)).toEqual([-1, 0]);
await expectNoRuntimeFailures(harness);
});
test("a replaced synthetic inner button is rebound", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
pageKind: "shorts",
},
);
await waitForDislikeCount(page, 25);
await page.evaluate(() => window.__shortsFixture.replaceInnerButton("dislike"));
await expect(visibleVoteButton(page, "dislike")).toHaveAttribute("aria-disabled", "false");
await visibleVoteButton(page, "dislike").click();
await expect.poll(() => harness.backend.requestsFor("POST", "/interact/confirmVote").length).toBe(1);
expect(harness.backend.requestsFor("POST", "/interact/vote").map((request) => request.body.value)).toEqual([-1]);
await expectNoRuntimeFailures(harness);
});
test("Like and synthetic Dislike label-count clicks each submit once", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
pageKind: "shorts",
},
);
await waitForDislikeCount(page, 25);
await page.locator('[data-ryd-role="like"]:visible #text').click();
await expect.poll(() => harness.backend.requestsFor("POST", "/interact/confirmVote").length).toBe(1);
await expect(visibleVoteButton(page, "like")).toHaveAttribute("aria-pressed", "true");
await page.locator("[data-ryd-synthetic-shorts-dislike]:visible #text").click();
await expect.poll(() => harness.backend.requestsFor("POST", "/interact/confirmVote").length).toBe(2);
await page.waitForTimeout(200);
expect(harness.backend.requestsFor("POST", "/interact/vote").map((request) => request.body.value)).toEqual([1, -1]);
await expect(visibleVoteButton(page, "like")).toHaveAttribute("aria-pressed", "false");
await expect(visibleVoteButton(page, "dislike")).toHaveAttribute("aria-pressed", "true");
await expectNoRuntimeFailures(harness);
});
test("a native Shorts dislike arriving later replaces the owned control", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
pageKind: "shorts",
},
);
await waitForDislikeCount(page, 25);
await page.evaluate(() => window.__shortsFixture.installNativeDislike());
await expect(page.locator("[data-ryd-synthetic-shorts-dislike]")).toHaveCount(0);
await expect(page.locator("dislike-button-view-model:visible")).toHaveCount(1);
await waitForDislikeCount(page, 25);
await expect(page.locator('[data-fixture-control="comments"]:visible')).toContainText("12");
await visibleVoteButton(page, "dislike").click();
await expect.poll(() => harness.backend.requestsFor("POST", "/interact/confirmVote").length).toBe(1);
expect(harness.backend.requestsFor("POST", "/interact/vote").map((request) => request.body.value)).toEqual([-1]);
await expectNoRuntimeFailures(harness);
});
test("native Shorts takeover and return preserve the final per-video state", async ({ context, page }) => {
const stateKey = `${SYNTHETIC_STATE_KEY_PREFIX}${VIDEO_A}`;
const harness = await launchHarness(
{ context, page },
{
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
pageKind: "shorts",
},
);
await waitForDislikeCount(page, 25);
await visibleVoteButton(page, "dislike").click();
await expect.poll(() => harness.backend.requestsFor("POST", "/interact/confirmVote").length).toBe(1);
await expect.poll(() => readGmValue(page, stateKey)).toBe(true);
await page.evaluate(() => window.__shortsFixture.installNativeDislike());
await expect(page.locator("[data-ryd-synthetic-shorts-dislike]")).toHaveCount(0);
await page.evaluate(() => window.__shortsFixture.removeNativeDislike());
await waitForDislikeCount(page, 26);
await expect(visibleVoteButton(page, "dislike")).toHaveAttribute("aria-pressed", "true");
await page.evaluate(() => window.__shortsFixture.installNativeDislike());
await expect(page.locator("[data-ryd-synthetic-shorts-dislike]")).toHaveCount(0);
await waitForDislikeCount(page, 26);
await visibleVoteButton(page, "dislike").click();
await expect.poll(() => harness.backend.requestsFor("POST", "/interact/confirmVote").length).toBe(2);
await expect.poll(() => readGmValue(page, stateKey)).toBe(true);
await visibleVoteButton(page, "dislike").click();
await expect.poll(() => harness.backend.requestsFor("POST", "/interact/confirmVote").length).toBe(3);
await expect.poll(() => readGmValue(page, stateKey)).toBe(null);
await page.evaluate(() => window.__shortsFixture.removeNativeDislike());
await waitForDislikeCount(page, 25);
await expect(visibleVoteButton(page, "dislike")).toHaveAttribute("aria-pressed", "false");
expect(harness.backend.requestsFor("POST", "/interact/vote").map((request) => request.body.value)).toEqual([
-1, -1, 0,
]);
await expectNoRuntimeFailures(harness);
});
test("class-only active Like switches to synthetic Dislike with one backend vote", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
backendOptions: {
countsByVideo: {
[VIDEO_A]: { dislikes: 25, likes: 100 },
[VIDEO_B]: { dislikes: 30, likes: 200 },
},
},
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
pageKind: "shorts",
},
);
await waitForDislikeCount(page, 25);
await page.evaluate((videoId) => window.__shortsFixture.activate(videoId), VIDEO_B);
await waitForDislikeCount(page, 30);
await page.evaluate((videoId) => {
window.__shortsFixture.activate(videoId, { state: "liked" });
window.__shortsFixture.setClassOnlyLiked();
}, VIDEO_A);
await waitForDislikeCount(page, 25);
await expect(page.locator('[data-ryd-role="like"]:visible')).toHaveClass(/style-default-active/);
await expect(visibleVoteButton(page, "like")).toHaveAttribute("aria-pressed", "false");
await visibleVoteButton(page, "dislike").click();
await expect.poll(() => harness.backend.requestsFor("POST", "/interact/confirmVote").length).toBe(1);
await expect(visibleVoteButton(page, "like")).toHaveAttribute("aria-pressed", "false");
await expect(visibleVoteButton(page, "dislike")).toHaveAttribute("aria-pressed", "true");
expect(harness.backend.requestsFor("POST", "/interact/vote").map((request) => request.body.value)).toEqual([-1]);
await expectNoRuntimeFailures(harness);
});
test("a delayed state read cannot restore video A onto video B", async ({ context, page }) => {
const videoAStateKey = `${SYNTHETIC_STATE_KEY_PREFIX}${VIDEO_A}`;
const harness = await launchHarness(
{ context, page },
{
backendOptions: {
countsByVideo: {
[VIDEO_A]: { dislikes: 11, likes: 100 },
[VIDEO_B]: { dislikes: 22, likes: 200 },
},
},
beforeInject: async (fixturePage) => {
await fixturePage.evaluate((delayedKey) => {
const originalGetValue = globalThis.GM.getValue;
let releaseRead;
const readGate = new Promise((resolve) => {
releaseRead = resolve;
});
globalThis.__releaseSyntheticStateRead = releaseRead;
globalThis.GM.getValue = async (key, fallbackValue) => {
if (key === delayedKey) await readGate;
return originalGetValue(key, fallbackValue);
};
}, videoAStateKey);
},
gmValues: {
[CREDENTIAL_KEY]: EXISTING_CREDENTIALS,
[videoAStateKey]: true,
},
pageKind: "shorts",
},
);
const initialSyntheticButton = visibleVoteButton(page, "dislike");
await expect(initialSyntheticButton).toHaveAttribute("aria-disabled", "true");
await page.evaluate((videoId) => window.__shortsFixture.activate(videoId), VIDEO_B);
await page.evaluate(() => globalThis.__releaseSyntheticStateRead());
await waitForDislikeCount(page, 22);
await expect(page.locator("[data-ryd-synthetic-shorts-dislike]:visible")).toHaveAttribute(
"data-ryd-video-id",
VIDEO_B,
);
await expect(visibleVoteButton(page, "dislike")).toHaveAttribute("aria-pressed", "false");
await expect(visibleVoteButton(page, "dislike")).toHaveAttribute("aria-disabled", "false");
await expectNoRuntimeFailures(harness);
});
test("synthetic state storage failure falls back to an enabled neutral control", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
beforeInject: async (fixturePage) => {
await fixturePage.evaluate((keyPrefix) => {
const originalGetValue = globalThis.GM.getValue;
globalThis.GM.getValue = (key, fallbackValue) => {
if (key.startsWith(keyPrefix)) throw new Error("synthetic state read failed");
return originalGetValue(key, fallbackValue);
};
}, SYNTHETIC_STATE_KEY_PREFIX);
},
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
pageKind: "shorts",
},
);
await waitForDislikeCount(page, 25);
await expect(visibleVoteButton(page, "dislike")).toHaveAttribute("aria-pressed", "false");
await expect(visibleVoteButton(page, "dislike")).toHaveAttribute("aria-disabled", "false");
await expectNoRuntimeFailures(harness);
});
test("synthetic Shorts dislike state survives reload and toggles back to neutral", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
pageKind: "shorts",
},
);
const { backend } = harness;
await waitForDislikeCount(page, 25);
await visibleVoteButton(page, "dislike").click();
await expect.poll(() => backend.requestsFor("POST", "/interact/confirmVote").length).toBe(1);
await expect(visibleVoteButton(page, "dislike")).toHaveAttribute("aria-pressed", "true");
await page.reload({ waitUntil: "domcontentloaded" });
await injectGeneratedUserscript(page);
await waitForDislikeCount(page, 25);
await expect(page.locator("[data-ryd-synthetic-shorts-dislike]:visible")).toHaveCount(1);
await expect(visibleVoteButton(page, "dislike")).toHaveAttribute("aria-pressed", "true");
await visibleVoteButton(page, "dislike").click();
await expect.poll(() => backend.requestsFor("POST", "/interact/confirmVote").length).toBe(2);
await expect(visibleVoteButton(page, "dislike")).toHaveAttribute("aria-pressed", "false");
expect(backend.requestsFor("POST", "/interact/vote").map((request) => request.body.value)).toEqual([-1, 0]);
await expectNoRuntimeFailures(harness);
});
test("corrupt synthetic Shorts state falls back to neutral without runtime errors", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
gmValues: {
[CREDENTIAL_KEY]: EXISTING_CREDENTIALS,
rydSyntheticDislikedShorts: { invalid: true },
},
pageKind: "shorts",
},
);
await waitForDislikeCount(page, 25);
await expect(page.locator("[data-ryd-synthetic-shorts-dislike]:visible")).toHaveCount(1);
await expect(visibleVoteButton(page, "dislike")).toHaveAttribute("aria-pressed", "false");
await expectNoRuntimeFailures(harness);
});
test("synthetic Shorts control honors the disabled vote gate", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
disableVoteSubmission: true,
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
pageKind: "shorts",
},
);
await waitForDislikeCount(page, 25);
await visibleVoteButton(page, "dislike").click();
await waitForDislikeCount(page, 26);
expect(harness.backend.requestsFor("POST", "/interact/vote")).toHaveLength(0);
await expectNoRuntimeFailures(harness);
});
test("synthetic Shorts control honors the signed-out vote gate", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
backendOptions: { fixture: { signedIn: false } },
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
pageKind: "shorts",
},
);
await waitForDislikeCount(page, 25);
await visibleVoteButton(page, "dislike").click();
await page.waitForTimeout(150);
expect(harness.backend.requestsFor("POST", "/interact/vote")).toHaveLength(0);
await expect(visibleVoteButton(page, "dislike")).toHaveAttribute("aria-pressed", "false");
await expectNoRuntimeFailures(harness);
});
test("rapid synthetic Shorts toggles remain serialized", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
pageKind: "shorts",
},
);
const { backend } = harness;
backend.enqueue("POST", "/interact/confirmVote", { body: true, delayMs: 200 });
await waitForDislikeCount(page, 25);
const syntheticButton = visibleVoteButton(page, "dislike");
await syntheticButton.click();
await syntheticButton.click();
await expect.poll(() => backend.requestsFor("POST", "/interact/vote").length).toBe(1);
await page.waitForTimeout(75);
expect(backend.requestsFor("POST", "/interact/vote")).toHaveLength(1);
await expect.poll(() => backend.requestsFor("POST", "/interact/confirmVote").length).toBe(2);
expect(backend.requestsFor("POST", "/interact/vote").map((request) => request.body.value)).toEqual([-1, 0]);
await expect(syntheticButton).toHaveAttribute("aria-pressed", "false");
await expectNoRuntimeFailures(harness);
});
test("rapid votes for one video remain serialized and ordered", async ({ context, page }) => {
const harness = await launchHarness({ context, page }, { gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS } });
const { backend } = harness;
backend.enqueue("POST", "/interact/confirmVote", { body: true, delayMs: 200 });
await waitForDislikeCount(page, 25);
const likeButton = visibleVoteButton(page, "like");
await likeButton.click();
await likeButton.click();
await expect.poll(() => backend.requestsFor("POST", "/interact/vote").length).toBe(1);
await page.waitForTimeout(75);
expect(backend.requestsFor("POST", "/interact/vote")).toHaveLength(1);
await expect.poll(() => backend.requestsFor("POST", "/interact/confirmVote").length).toBe(2);
expect(backend.requestsFor("POST", "/interact/vote").map((request) => request.body.value)).toEqual([1, 0]);
await expectNoRuntimeFailures(harness);
});
test("a rejected vote does not poison the per-video queue", async ({ context, page }) => {
const harness = await launchHarness({ context, page }, { gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS } });
const { backend } = harness;
backend.enqueue("POST", "/interact/vote", { body: { invalidPuzzle: true } });
await waitForDislikeCount(page, 25);
const likeButton = visibleVoteButton(page, "like");
await likeButton.click();
await expect.poll(() => backend.requestsFor("POST", "/interact/vote").length).toBe(1);
await likeButton.click();
await expect.poll(() => backend.requestsFor("POST", "/interact/confirmVote").length).toBe(1);
expect(backend.requestsFor("POST", "/interact/vote").map((request) => request.body.value)).toEqual([1, 0]);
await expectNoRuntimeFailures(harness);
});
test("a rejected confirmation does not poison the per-video queue", async ({ context, page }) => {
const harness = await launchHarness({ context, page }, { gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS } });
const { backend } = harness;
backend.enqueue("POST", "/interact/confirmVote", { body: false });
await waitForDislikeCount(page, 25);
const dislikeButton = visibleVoteButton(page, "dislike");
await dislikeButton.click();
await expect.poll(() => backend.requestsFor("POST", "/interact/confirmVote").length).toBe(1);
await dislikeButton.click();
await expect.poll(() => backend.requestsFor("POST", "/interact/confirmVote").length).toBe(2);
expect(backend.requestsFor("POST", "/interact/vote").map((request) => request.body.value)).toEqual([-1, 0]);
await expectNoRuntimeFailures(harness);
});
test("a failed vote still allows later SPA navigation and voting", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
backendOptions: {
countsByVideo: {
[VIDEO_A]: { dislikes: 11, likes: 100 },
[VIDEO_B]: { dislikes: 22, likes: 200 },
},
},
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
},
);
const { backend } = harness;
backend.enqueue("POST", "/interact/vote", { status: 500, body: { error: "temporary failure" } });
await waitForDislikeCount(page, 11);
await visibleVoteButton(page, "dislike").click();
await expect.poll(() => backend.requestsFor("POST", "/interact/vote").length).toBe(1);
await page.evaluate((videoId) => window.__youtubeFixture.navigate(videoId), VIDEO_B);
await waitForDislikeCount(page, 22);
await clickVoteAndWait(page, backend, "like", 1);
expect(backend.requestsFor("POST", "/interact/vote").map((request) => request.body.videoId)).toEqual([
VIDEO_A,
VIDEO_B,
]);
await expectNoRuntimeFailures(harness, { allowedConsoleErrors: [/status of 500/] });
});
test("a 401 clears stale credentials, registers once, and retries the vote", async ({ context, page }) => {
const harness = await launchHarness({ context, page }, { gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS } });
const { backend } = harness;
backend.enqueue("POST", "/interact/vote", { status: 401, body: { error: "expired" } });
await waitForDislikeCount(page, 25);
await clickVoteAndWait(page, backend, "dislike", 1);
const votes = backend.requestsFor("POST", "/interact/vote");
expect(votes).toHaveLength(2);
expect(votes[0].body.userId).toBe(EXISTING_CREDENTIALS.userId);
expect(votes[1].body.userId).not.toBe(EXISTING_CREDENTIALS.userId);
expect(backend.requestsFor("GET", "/puzzle/registration")).toHaveLength(1);
expect(backend.requestsFor("POST", "/puzzle/registration")).toHaveLength(1);
const replacementCredentials = await readGmValue(page, CREDENTIAL_KEY);
expect(replacementCredentials).toMatchObject({
userId: votes[1].body.userId,
registrationConfirmed: true,
});
await expectNoRuntimeFailures(harness, { allowedConsoleErrors: [/status of 401/] });
});
test.describe("touch-enabled mobile fixture", () => {
test.use(PIXEL_5);
test("a real tap submits exactly one vote", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
hostname: "m.youtube.com",
},
);
const { backend } = harness;
await waitForDislikeCount(page, 25);
await visibleVoteButton(page, "dislike").tap();
await expect.poll(() => backend.requestsFor("POST", "/interact/confirmVote").length).toBe(1);
await page.waitForTimeout(150);
expect(backend.requestsFor("POST", "/interact/vote").map((request) => request.body.value)).toEqual([-1]);
await expectNoRuntimeFailures(harness);
});
test("mobile Shorts boot and vote through the active mobile controls", async ({ context, page }) => {
const harness = await launchHarness(
{ context, page },
{
gmValues: { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS },
hostname: "m.youtube.com",
pageKind: "shorts",
},
);
const { backend } = harness;
await waitForDislikeCount(page, 25);
await visibleVoteButton(page, "dislike").tap();
await expect.poll(() => backend.requestsFor("POST", "/interact/confirmVote").length).toBe(1);
expect(backend.requestsFor("POST", "/interact/vote")[0].body).toMatchObject({
videoId: VIDEO_A,
value: -1,
});
await expectNoRuntimeFailures(harness);
});
});