mirror of
https://github.com/Anarios/return-youtube-dislike.git
synced 2026-09-12 10:22:10 +02:00
3657de9b2c
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.
253 lines
9.3 KiB
JavaScript
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);
|
|
});
|
|
});
|