Files
return-youtube-dislike/Extensions/e2e/shared-live-scenarios.spec.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

253 lines
9.3 KiB
JavaScript

const {
LIVE_RUNTIME_PROFILES,
createExtensionLiveRuntimeAdapter,
createUserscriptLiveRuntimeAdapter,
} = require("./live-runtime-adapter");
const {
SHARED_LIVE_SCENARIO_IDS,
assertSharedLiveScenarioAdapter,
createSharedLiveScenarioRunner,
} = require("./shared-live-scenarios");
const EXPECTED_SCENARIO_IDS = [
"channel-shorts-navigation",
"channel-watch-navigation",
"watch-render",
"reload",
"spa-navigation",
"sidebar-navigation-stress",
"shorts-render",
"responsive-visual",
"reaction-matrix",
];
const EXPECTED_BUILD_ID = "0123456789abcdef0123456789abcdef";
const ALL_DRIVER_METHODS = [
"assertCurrentShortsControl",
"assertCurrentVideo",
"assertRuntime",
"assertSignedIn",
"captureReactionStateVisual",
"captureNativeShortsVisual",
"captureSyntheticShortsVisual",
"captureWatchRatioVisual",
"clickAction",
"navigateFromColdChannelToShort",
"navigateFromColdChannelToWatch",
"navigateToNextShort",
"navigateToRelatedWatch",
"navigateWithinPlaylist",
"openPlaylist",
"openShort",
"openWatch",
"pausePlayback",
"readReactionState",
"readViewportSize",
"reload",
"setViewportSize",
"soakWatchRatioVisual",
"waitForDislikeText",
"waitForReactionState",
"withNoProductionInteractions",
];
function createDriver(overrides = {}) {
return {
...Object.fromEntries(ALL_DRIVER_METHODS.map((method) => [method, jest.fn()])),
...overrides,
};
}
function createAdapter(runtime, driver = createDriver(), expectedBuildId = EXPECTED_BUILD_ID) {
const options = { driver, expectedBuildId, expectedVersion: runtime === "userscript" ? "3.2.0" : "4.0.4" };
return runtime === "userscript"
? createUserscriptLiveRuntimeAdapter(options)
: createExtensionLiveRuntimeAdapter(options);
}
function createImplementations(events) {
const implementationNames = [
"runChannelShortsNavigationScenario",
"runChannelWatchNavigationScenario",
"runWatchRenderScenario",
"runReloadScenario",
"runSpaNavigationScenario",
"runSidebarStressScenario",
"runShortsRenderScenario",
"runResponsiveVisualScenario",
"runProductionReactionMatrixScenario",
];
return Object.fromEntries(
implementationNames.map((name) => [
name,
jest.fn(async (_driver, options) => {
events.push({ implementation: name, runtime: options.runtime });
return `${options.runtime}:${name}`;
}),
]),
);
}
describe("shared live runtime profiles", () => {
test("preserves each runtime's selectors and control-ownership capabilities", () => {
expect(LIVE_RUNTIME_PROFILES.userscript).toMatchObject({
capabilities: {
backgroundVoteTransport: false,
credentialStore: "gm-storage",
ownsShortsDislikeControl: true,
shortsControlModel: "synthetic-owned",
shortsVisualModel: "strict-synthetic",
},
buildMarkerAttribute: "data-ryd-userscript-build",
markerAttribute: "data-ryd-userscript-version",
selectors: {
rateBar: "#return-youtube-dislike-bar",
rateBarContainer: "#return-youtube-dislike-bar-container",
shortsDislikeControl: "[data-ryd-synthetic-shorts-dislike]",
},
});
expect(LIVE_RUNTIME_PROFILES.extension).toMatchObject({
capabilities: {
backgroundVoteTransport: true,
credentialStore: "browser.storage",
ownsShortsDislikeControl: false,
shortsControlModel: "native-youtube",
shortsVisualModel: "native-pair",
},
buildMarkerAttribute: "data-ryd-extension-build",
markerAttribute: "data-ryd-extension-version",
selectors: {
rateBar: "#ryd-bar",
rateBarContainer: "#ryd-bar-container",
shortsDislikeControl: null,
},
});
expect(Object.isFrozen(LIVE_RUNTIME_PROFILES.userscript.capabilities)).toBe(true);
expect(Object.isFrozen(LIVE_RUNTIME_PROFILES.extension.selectors)).toBe(true);
});
test.each(["userscript", "extension"])("binds runtime-sensitive driver calls for %s", async (runtime) => {
const driver = createDriver();
const adapter = createAdapter(runtime, driver);
await adapter.driver.assertRuntime(runtime, adapter.expectedVersion, adapter.expectedBuildId);
await adapter.driver.assertCurrentShortsControl("abcdefghijk", runtime);
await adapter.driver.captureWatchRatioVisual(runtime, "watch.png");
await adapter.driver.captureReactionStateVisual({
expectedState: "neutral",
isShort: false,
runtime,
screenshotPath: "reaction.png",
videoId: "abcdefghijk",
});
expect(driver.assertRuntime).toHaveBeenCalledWith(runtime, adapter.expectedVersion, adapter.expectedBuildId);
expect(driver.assertCurrentShortsControl).toHaveBeenCalledWith("abcdefghijk", runtime);
expect(driver.captureWatchRatioVisual).toHaveBeenCalledWith(runtime, "watch.png");
expect(driver.captureReactionStateVisual).toHaveBeenCalledWith(
expect.objectContaining({ runtime, videoId: "abcdefghijk" }),
);
});
test("rejects scenario options and calls for another runtime", async () => {
const adapter = createAdapter("userscript");
expect(() => adapter.createScenarioOptions({ runtime: "extension" })).toThrow(
"The configured scenario runtime does not match",
);
expect(() => adapter.driver.assertRuntime("extension", adapter.expectedVersion)).toThrow(
"The asserted runtime does not match",
);
});
test.each([undefined, "", "stale", "A".repeat(32)])(
"rejects a missing or malformed exact live build ID: %p",
(expectedBuildId) => {
expect(() =>
createUserscriptLiveRuntimeAdapter({
driver: createDriver(),
expectedBuildId,
expectedVersion: "3.2.0",
}),
).toThrow("expected live build ID must be a 32-character lowercase hexadecimal value");
},
);
test("binds the exact generated live build ID into runtime checks", async () => {
const driver = createDriver();
const expectedBuildId = "0123456789abcdef0123456789abcdef";
const adapter = createAdapter("userscript", driver, expectedBuildId);
await adapter.driver.assertRuntime("userscript", adapter.expectedVersion, expectedBuildId);
expect(driver.assertRuntime).toHaveBeenCalledWith("userscript", adapter.expectedVersion, expectedBuildId);
expect(adapter.createScenarioOptions()).toMatchObject({ expectedBuildId });
expect(() => adapter.driver.assertRuntime("userscript", adapter.expectedVersion, "f".repeat(32))).toThrow(
"live build ID does not match",
);
});
});
describe("shared live scenario contract", () => {
test("publishes one stable ordered scenario ID list", () => {
expect(SHARED_LIVE_SCENARIO_IDS).toEqual(EXPECTED_SCENARIO_IDS);
expect(Object.isFrozen(SHARED_LIVE_SCENARIO_IDS)).toBe(true);
});
test.each(["userscript", "extension"])("validates the complete contract for %s", (runtime) => {
const adapter = createAdapter(runtime);
expect(assertSharedLiveScenarioAdapter(adapter)).toEqual(EXPECTED_SCENARIO_IDS);
});
test("requires each runtime's Shorts visual model from the shared responsive scenario", () => {
const driver = createDriver();
delete driver.captureSyntheticShortsVisual;
expect(() => assertSharedLiveScenarioAdapter(createAdapter("userscript", driver), ["responsive-visual"])).toThrow(
"must implement captureSyntheticShortsVisual()",
);
expect(assertSharedLiveScenarioAdapter(createAdapter("extension", driver), ["responsive-visual"])).toEqual([
"responsive-visual",
]);
const extensionDriver = createDriver();
delete extensionDriver.captureNativeShortsVisual;
expect(() =>
assertSharedLiveScenarioAdapter(createAdapter("extension", extensionDriver), ["responsive-visual"]),
).toThrow("must implement captureNativeShortsVisual()");
});
test("executes the same scenario IDs through both runtime adapters", async () => {
const events = [];
const implementations = createImplementations(events);
const runner = createSharedLiveScenarioRunner({ implementations });
const services = {
consumeVoteApproval: jest.fn(),
createRecorder: jest.fn(),
visualOptions: { outputDirectory: "evidence" },
sidebarOptions: { outputDirectory: "sidebar-evidence" },
};
const userscriptResults = await runner.runAll(createAdapter("userscript"), {}, services);
const extensionResults = await runner.runAll(createAdapter("extension"), {}, services);
expect(userscriptResults.map(({ id }) => id)).toEqual(EXPECTED_SCENARIO_IDS);
expect(extensionResults.map(({ id }) => id)).toEqual(EXPECTED_SCENARIO_IDS);
expect(events.map(({ runtime }) => runtime)).toEqual([
...EXPECTED_SCENARIO_IDS.map(() => "userscript"),
...EXPECTED_SCENARIO_IDS.map(() => "extension"),
]);
expect(Object.values(implementations).every((implementation) => implementation.mock.calls.length === 2)).toBe(true);
});
test("rejects unknown IDs before invoking a scenario implementation", async () => {
const implementations = createImplementations([]);
const runner = createSharedLiveScenarioRunner({ implementations });
await expect(runner.run(createAdapter("userscript"), "not-a-scenario")).rejects.toThrow(
"Unknown shared live scenario: not-a-scenario",
);
expect(Object.values(implementations).every((implementation) => implementation.mock.calls.length === 0)).toBe(true);
});
});