Files
return-youtube-dislike/Extensions/UserScript/live/live-interactive-readonly.spec.js
T

213 lines
8.3 KiB
JavaScript
Raw Normal View History

const {
INTERACTIVE_READ_ONLY_SCENARIOS,
createInteractiveReadOnlyStages,
runInteractiveReadOnlyAfterRuntimeReload,
} = require("../e2e/live/live-interactive-readonly");
const {
REQUIRED_INTERACTIVE_READ_ONLY_SCENARIO_IDS,
assertInteractiveReadOnlyScenarioCompletions,
assertInteractiveReadOnlyScenarioDefinitions,
} = require("../e2e/live/live-interactive-scenario-contract");
const OPTIONS = Object.freeze({
navigation: Object.freeze({ watch: "watch-target" }),
runtime: "extension",
short: "9LjMX9xeeok",
});
describe("interactive live read-only ordering", () => {
test("declares the exact independent required scenario set with the cold Short first", () => {
expect(REQUIRED_INTERACTIVE_READ_ONLY_SCENARIO_IDS).toEqual([
"shorts-render",
"channel-shorts-navigation",
"channel-watch-navigation",
"reload",
"responsive-visual",
"spa-navigation",
"watch-action-topology",
"sidebar-navigation-stress",
"watch-render",
]);
expect(new Set(REQUIRED_INTERACTIVE_READ_ONLY_SCENARIO_IDS)).toHaveProperty(
"size",
REQUIRED_INTERACTIVE_READ_ONLY_SCENARIO_IDS.length,
);
expect(REQUIRED_INTERACTIVE_READ_ONLY_SCENARIO_IDS[0]).toBe("shorts-render");
});
test("keeps the implementation aligned with the independent required order", () => {
const stages = createInteractiveReadOnlyStages({
diagnostics: { checkpoint: jest.fn() },
options: OPTIONS,
readOnly: {},
runtimeAdapter: {},
scenarioRunner: { run: jest.fn() },
});
expect(stages.map(({ scenarioId }) => scenarioId)).toEqual(REQUIRED_INTERACTIVE_READ_ONLY_SCENARIO_IDS);
expect(() => assertInteractiveReadOnlyScenarioDefinitions(INTERACTIVE_READ_ONLY_SCENARIOS)).not.toThrow();
expect(stages[0]).toMatchObject({ name: "read-only.short-render", scenarioId: "shorts-render" });
expect(stages.findIndex(({ scenarioId }) => scenarioId === "shorts-render")).toBeLessThan(
stages.findIndex(({ scenarioId }) => scenarioId === "channel-shorts-navigation"),
);
expect(stages.findIndex(({ scenarioId }) => scenarioId === "shorts-render")).toBeLessThan(
stages.findIndex(({ scenarioId }) => scenarioId === "channel-watch-navigation"),
);
});
test.each(REQUIRED_INTERACTIVE_READ_ONLY_SCENARIO_IDS)(
"fails the independent contract when %s is deleted from the implementation",
(deletedScenarioId) => {
const incompleteDefinitions = INTERACTIVE_READ_ONLY_SCENARIOS.filter(
({ scenarioId }) => scenarioId !== deletedScenarioId,
);
expect(() => assertInteractiveReadOnlyScenarioDefinitions(incompleteDefinitions)).toThrow(
new RegExp(`Missing: .*${deletedScenarioId}`),
);
},
);
test("rejects duplicate, unexpected, and reordered implementation scenarios", () => {
const duplicate = [
...INTERACTIVE_READ_ONLY_SCENARIOS,
{ key: "duplicate", name: "read-only.duplicate", scenarioId: "shorts-render" },
];
const unexpected = [
...INTERACTIVE_READ_ONLY_SCENARIOS.slice(0, -1),
{ key: "unexpected", name: "read-only.unexpected", scenarioId: "unexpected" },
];
const reordered = [...INTERACTIVE_READ_ONLY_SCENARIOS];
[reordered[0], reordered[1]] = [reordered[1], reordered[0]];
expect(() => assertInteractiveReadOnlyScenarioDefinitions(duplicate)).toThrow(/duplicate scenario IDs/);
expect(() => assertInteractiveReadOnlyScenarioDefinitions(unexpected)).toThrow(
/Missing: watch-render; unexpected: unexpected/,
);
expect(() => assertInteractiveReadOnlyScenarioDefinitions(reordered)).toThrow(
/required interactive read-only order/,
);
});
test("waits for the exact extension reload, then runs the configured Short cold before other scenarios", async () => {
const events = [];
let finishReload;
const reloadPending = new Promise((resolve) => {
finishReload = resolve;
});
const scenarioRunner = {
run: jest.fn(async (_adapter, scenarioId, receivedOptions) => {
events.push(`scenario:${scenarioId}:${receivedOptions.short}`);
return scenarioId;
}),
};
const execution = runInteractiveReadOnlyAfterRuntimeReload({
initializeRuntime: async (reloadResult) => {
events.push(`initialize:${reloadResult.extensionId}`);
return { diagnostics: {}, runtimeAdapter: { runtime: "extension" } };
},
options: OPTIONS,
reloadExtension: async () => {
events.push("reload:start");
const result = await reloadPending;
events.push(`reload:complete:${result.extensionId}`);
return result;
},
runStages: async (_diagnostics, stages) => {
for (const stage of stages) await stage.action();
},
scenarioRunner,
});
await Promise.resolve();
expect(events).toEqual(["reload:start"]);
finishReload({ extensionId: "installed-extension" });
await expect(execution).resolves.toMatchObject({
readOnly: { short: "shorts-render" },
reloadResult: { extensionId: "installed-extension" },
});
expect(events.slice(0, 4)).toEqual([
"reload:start",
"reload:complete:installed-extension",
"initialize:installed-extension",
`scenario:shorts-render:${OPTIONS.short}`,
]);
expect(scenarioRunner.run).toHaveBeenNthCalledWith(1, { runtime: "extension" }, "shorts-render", OPTIONS);
});
test("refuses to classify an unconfigured channel-to-Watch scenario as completed", () => {
expect(() =>
createInteractiveReadOnlyStages({
diagnostics: { checkpoint: jest.fn() },
options: { ...OPTIONS, navigation: { watch: null } },
readOnly: {},
runtimeAdapter: {},
scenarioRunner: { run: jest.fn() },
}),
).toThrow(/requires a configured channel Watch target/);
});
test("refuses reaction readiness when the stage executor silently omits any scenario", async () => {
const scenarioRunner = { run: jest.fn(async (_adapter, scenarioId) => scenarioId) };
await expect(
runInteractiveReadOnlyAfterRuntimeReload({
initializeRuntime: async () => ({ diagnostics: {}, runtimeAdapter: {} }),
options: OPTIONS,
reloadExtension: async () => ({ extensionId: "installed-extension" }),
runStages: async (_diagnostics, stages) => {
for (const stage of stages.slice(0, -1)) await stage.action();
},
scenarioRunner,
}),
).rejects.toThrow(/Missing: watch-render/);
expect(scenarioRunner.run).toHaveBeenCalledTimes(REQUIRED_INTERACTIVE_READ_ONLY_SCENARIO_IDS.length - 1);
});
test("refuses reaction readiness when a scenario resolves without a result", async () => {
await expect(
runInteractiveReadOnlyAfterRuntimeReload({
initializeRuntime: async () => ({ diagnostics: {}, runtimeAdapter: {} }),
options: OPTIONS,
reloadExtension: async () => ({ extensionId: "installed-extension" }),
runStages: async (_diagnostics, stages) => {
await stages[0].action();
},
scenarioRunner: { run: jest.fn().mockResolvedValue(undefined) },
}),
).rejects.toThrow(/shorts-render completed without a result/);
});
test("refuses reaction readiness when stages complete out of order or more than once", async () => {
const execute = (runStages) =>
runInteractiveReadOnlyAfterRuntimeReload({
initializeRuntime: async () => ({ diagnostics: {}, runtimeAdapter: {} }),
options: OPTIONS,
reloadExtension: async () => ({ extensionId: "installed-extension" }),
runStages,
scenarioRunner: { run: async (_adapter, scenarioId) => scenarioId },
});
await expect(
execute(async (_diagnostics, stages) => {
for (const stage of [...stages].reverse()) await stage.action();
}),
).rejects.toThrow(/required interactive read-only order/);
await expect(
execute(async (_diagnostics, stages) => {
for (const stage of stages) await stage.action();
await stages[0].action();
}),
).rejects.toThrow(/duplicate scenario IDs/);
});
test("completion validation independently rejects an empty or incomplete result set", () => {
expect(() => assertInteractiveReadOnlyScenarioCompletions([])).toThrow(/Missing:/);
expect(() =>
assertInteractiveReadOnlyScenarioCompletions(REQUIRED_INTERACTIVE_READ_ONLY_SCENARIO_IDS.slice(0, -1)),
).toThrow(/Missing: watch-render/);
});
});