2026-09-06 12:51:12 +02:00
|
|
|
const fs = require("node:fs");
|
|
|
|
|
const path = require("node:path");
|
2026-09-01 23:41:28 +02:00
|
|
|
const { test, expect } = require("@playwright/test");
|
|
|
|
|
const {
|
|
|
|
|
CREDENTIAL_KEY,
|
|
|
|
|
VIDEO_A,
|
|
|
|
|
createFakeBackend,
|
|
|
|
|
injectGeneratedUserscript,
|
|
|
|
|
installGmEnvironment,
|
|
|
|
|
installHermeticRoutes,
|
|
|
|
|
openWatchFixture,
|
|
|
|
|
} = require("./harness");
|
2026-09-06 12:51:12 +02:00
|
|
|
const {
|
|
|
|
|
USERSCRIPT_WATCH_VISUAL_PROFILE,
|
|
|
|
|
attachVisualFailureScreenshot,
|
|
|
|
|
expectNoStructuralLayoutShift,
|
|
|
|
|
expectWatchVisualState,
|
|
|
|
|
readWatchVisualContract,
|
|
|
|
|
waitForBarRatio,
|
|
|
|
|
} = require("../../e2e/watch-visual-contract");
|
|
|
|
|
const { getWatchNeutralBaselineId, registerWatchVisualContract } = require("../../e2e/watch-visual-scenarios");
|
|
|
|
|
const { annotateVisualEvidence, captureOptionalVisualEvidence } = require("../../e2e/visual-evidence");
|
|
|
|
|
|
|
|
|
|
const REPOSITORY_ROOT = path.resolve(__dirname, "../../..");
|
|
|
|
|
const VISUAL_REVIEW_DIRECTORY = path.join(REPOSITORY_ROOT, "test-results", "visual-review", "userscript");
|
2026-09-01 23:41:28 +02:00
|
|
|
const EXISTING_CREDENTIALS = {
|
|
|
|
|
userId: "ExistingUserscriptCredential000000000001",
|
|
|
|
|
registrationConfirmed: true,
|
|
|
|
|
};
|
|
|
|
|
const BASE_COUNTS = { likes: 300, dislikes: 100 };
|
2026-09-06 12:51:12 +02:00
|
|
|
|
|
|
|
|
async function captureVisualReview(page, name) {
|
|
|
|
|
const outputPath = path.join(VISUAL_REVIEW_DIRECTORY, `${name}.png`);
|
|
|
|
|
return captureOptionalVisualEvidence({
|
|
|
|
|
capture: async (screenshotPath) => {
|
|
|
|
|
fs.mkdirSync(VISUAL_REVIEW_DIRECTORY, { recursive: true });
|
|
|
|
|
await page.locator("#top-row").screenshot({
|
|
|
|
|
animations: "disabled",
|
|
|
|
|
path: screenshotPath,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
outputPath,
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-09-01 23:41:28 +02:00
|
|
|
|
|
|
|
|
function monitorRuntime(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 { consoleErrors, pageErrors };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function launchVisualFixture({ context, page }, initialState) {
|
|
|
|
|
const runtime = monitorRuntime(page);
|
|
|
|
|
const backend = createFakeBackend({
|
|
|
|
|
countsByVideo: { [VIDEO_A]: BASE_COUNTS },
|
|
|
|
|
fixture: { initialState },
|
|
|
|
|
});
|
|
|
|
|
await installGmEnvironment(context, { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS });
|
|
|
|
|
await installHermeticRoutes(context, backend);
|
|
|
|
|
await openWatchFixture(page, VIDEO_A);
|
|
|
|
|
await injectGeneratedUserscript(page);
|
|
|
|
|
|
|
|
|
|
await expect(page.locator('[data-ryd-role="dislike"] #text')).toHaveText(String(BASE_COUNTS.dislikes));
|
2026-09-06 12:51:12 +02:00
|
|
|
await expect(page.locator(USERSCRIPT_WATCH_VISUAL_PROFILE.container)).toBeVisible();
|
2026-09-01 23:41:28 +02:00
|
|
|
return { backend, ...runtime };
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-06 12:51:12 +02:00
|
|
|
registerWatchVisualContract({
|
|
|
|
|
runtime: "userscript",
|
|
|
|
|
test,
|
|
|
|
|
async runScenario({ fixtures: { context, page }, scenario, testInfo }) {
|
|
|
|
|
const { transition, viewport } = scenario;
|
|
|
|
|
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
|
|
|
|
try {
|
|
|
|
|
const harness = await launchVisualFixture({ context, page }, transition.initialState);
|
|
|
|
|
const before = await readWatchVisualContract(page, USERSCRIPT_WATCH_VISUAL_PROFILE);
|
|
|
|
|
expectWatchVisualState(before, transition.initialState, BASE_COUNTS, viewport, USERSCRIPT_WATCH_VISUAL_PROFILE);
|
|
|
|
|
const neutralBaselineId = getWatchNeutralBaselineId(scenario);
|
|
|
|
|
if (neutralBaselineId) {
|
|
|
|
|
annotateVisualEvidence(testInfo, await captureVisualReview(page, neutralBaselineId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await page.locator(`[data-ryd-role="${transition.action}"] button`).click();
|
|
|
|
|
await expect.poll(() => harness.backend.requestsFor("POST", "/interact/confirmVote").length).toBe(1);
|
|
|
|
|
|
|
|
|
|
const nextCounts = {
|
|
|
|
|
likes: BASE_COUNTS.likes + transition.likesDelta,
|
|
|
|
|
dislikes: BASE_COUNTS.dislikes + transition.dislikesDelta,
|
2026-09-01 23:41:28 +02:00
|
|
|
};
|
2026-09-06 12:51:12 +02:00
|
|
|
await waitForBarRatio(page, nextCounts, USERSCRIPT_WATCH_VISUAL_PROFILE);
|
|
|
|
|
const after = await readWatchVisualContract(page, USERSCRIPT_WATCH_VISUAL_PROFILE);
|
|
|
|
|
expectWatchVisualState(after, transition.nextState, nextCounts, viewport, USERSCRIPT_WATCH_VISUAL_PROFILE);
|
|
|
|
|
expectNoStructuralLayoutShift(before, after);
|
|
|
|
|
|
|
|
|
|
expect(harness.backend.requestsFor("GET", "/votes")).toHaveLength(1);
|
|
|
|
|
expect(harness.backend.requestsFor("POST", "/interact/vote")).toHaveLength(1);
|
|
|
|
|
expect(harness.backend.requestsFor("POST", "/interact/vote")[0].body).toMatchObject({
|
|
|
|
|
userId: EXISTING_CREDENTIALS.userId,
|
|
|
|
|
value: transition.value,
|
|
|
|
|
videoId: VIDEO_A,
|
2026-09-01 23:41:28 +02:00
|
|
|
});
|
2026-09-06 12:51:12 +02:00
|
|
|
expect(harness.backend.requestsFor("POST", "/interact/confirmVote")).toHaveLength(1);
|
|
|
|
|
expect(harness.backend.blockedRequests).toEqual([]);
|
|
|
|
|
expect(harness.consoleErrors).toEqual([]);
|
|
|
|
|
expect(harness.pageErrors).toEqual([]);
|
|
|
|
|
annotateVisualEvidence(testInfo, await captureVisualReview(page, scenario.id));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
await attachVisualFailureScreenshot(testInfo, page, scenario.id);
|
|
|
|
|
throw error;
|
2026-09-01 23:41:28 +02:00
|
|
|
}
|
2026-09-06 12:51:12 +02:00
|
|
|
},
|
|
|
|
|
});
|