Files
return-youtube-dislike/Extensions/UserScript/e2e/navigation-matrix.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

1418 lines
63 KiB
JavaScript

const { expect } = require("@playwright/test");
const { assertInvariantContinuously, waitForStableInvariant } = require("../../e2e/continuous-invariants");
const { VIDEO_A, VIDEO_B } = require("./harness");
const WATCH_SIDEBAR_MATRIX = [
{
coverage: {
destination: "shorts",
dom: ["preloaded-sibling", "active-reel-switch"],
origin: "shorts",
timing: ["settled"],
trigger: "next-control",
width: "wide-desktop",
},
destination: { counts: { dislikes: 300, likes: 100 }, kind: "shorts", videoId: VIDEO_B },
id: "short-next-short-active-reel",
origin: { counts: { dislikes: 100, likes: 300 }, kind: "shorts", state: "neutral", videoId: VIDEO_A },
viewport: { height: 720, width: 1280 },
},
{
coverage: {
destination: "watch",
dom: ["retain-hidden-outgoing", "replace-controls", "prune-current-bar"],
origin: "watch",
timing: ["finish-before-hydration", "destination-count-gated"],
trigger: "sidebar-link",
width: "wide-desktop",
},
id: "watch-sidebar-watch-retain-prune",
destination: {
counts: { dislikes: 300, likes: 100 },
kind: "watch",
videoId: VIDEO_B,
},
origin: {
counts: { dislikes: 100, likes: 300 },
kind: "watch",
state: "neutral",
videoId: VIDEO_A,
},
timing: {
destinationCount: "gated",
navigateFinish: "before-destination-controls",
},
transition: {
destinationControls: "replace",
outgoing: "retain-hidden-top-row",
postInit: "prune-current-rate-bar",
trigger: "persistent-sidebar-link",
},
viewport: { height: 720, width: 1280 },
},
{
coverage: {
destination: "watch",
dom: ["same-current-root", "reuse-exact-control-nodes", "no-useful-control-mutation"],
origin: "watch",
timing: ["navigate-finish", "destination-count-gated"],
trigger: "sidebar-link",
width: "wide-desktop",
},
destination: { counts: { dislikes: 300, likes: 100 }, kind: "watch", videoId: VIDEO_B },
id: "watch-sidebar-watch-same-node-route-complete",
origin: { counts: { dislikes: 100, likes: 300 }, kind: "watch", state: "neutral", videoId: VIDEO_A },
transition: { controls: "reuse-exact-nodes", nativeControlMutations: "none" },
viewport: { height: 720, width: 1280 },
},
{
coverage: {
destination: "watch",
dom: [
"connected-hidden-rate-bar",
"connected-collapsed-rate-bar",
"malformed-rate-bar",
"stripped-rate-bar-wrapper-class",
],
origin: "watch",
timing: ["same-video", "no-navigation-event"],
trigger: "dom-corruption",
width: "wide-desktop",
},
destination: { counts: { dislikes: 100, likes: 300 }, kind: "watch", videoId: VIDEO_A },
id: "watch-current-rate-bar-connected-corruption",
origin: { counts: { dislikes: 100, likes: 300 }, kind: "watch", state: "neutral", videoId: VIDEO_A },
transition: {
corruptions: ["hidden-wrapper", "collapsed-wrapper", "missing-fill", "stripped-wrapper-class"],
},
viewport: { height: 720, width: 1280 },
},
];
const NAVIGATION_MATRIX = [
...WATCH_SIDEBAR_MATRIX,
{
coverage: {
destination: "watch",
dom: ["same-current-root", "hidden-outgoing-first", "rendered-destination-second"],
origin: "watch",
timing: ["settled"],
trigger: "sidebar-link",
width: "wide-desktop",
},
destination: { counts: { dislikes: 300, likes: 100 }, kind: "watch", videoId: VIDEO_B },
id: "watch-sidebar-watch-same-root-hidden-first",
origin: { counts: { dislikes: 100, likes: 300 }, kind: "watch", state: "neutral", videoId: VIDEO_A },
transition: { outgoingPresentation: "hidden" },
viewport: { height: 720, width: 1280 },
},
{
coverage: {
destination: "watch",
dom: ["same-current-root", "positive-size-offscreen-outgoing-first", "rendered-destination-second"],
origin: "watch",
timing: ["settled"],
trigger: "sidebar-link",
width: "wide-desktop",
},
destination: { counts: { dislikes: 300, likes: 100 }, kind: "watch", videoId: VIDEO_B },
id: "watch-sidebar-watch-same-root-offscreen-first",
origin: { counts: { dislikes: 100, likes: 300 }, kind: "watch", state: "neutral", videoId: VIDEO_A },
transition: { outgoingPresentation: "offscreen" },
viewport: { height: 720, width: 1280 },
},
{
coverage: {
destination: "watch",
dom: [
"same-current-root",
"hidden-outgoing-first",
"rendered-destination-second",
"legacy-segmented-duplicate-ids",
],
origin: "watch",
timing: ["settled"],
trigger: "sidebar-link",
width: "wide-desktop",
},
destination: { counts: { dislikes: 300, likes: 100 }, kind: "watch", videoId: VIDEO_B },
id: "watch-sidebar-watch-legacy-segmented-duplicate-ids",
origin: { counts: { dislikes: 100, likes: 300 }, kind: "watch", state: "neutral", videoId: VIDEO_A },
transition: { controlMarkup: "legacy-segmented", outgoingPresentation: "hidden" },
viewport: { height: 720, width: 1280 },
},
{
coverage: {
destination: "watch",
dom: ["replace-controls"],
origin: "watch",
timing: ["settled"],
trigger: "history-back-forward",
width: "wide-desktop",
},
destination: { counts: { dislikes: 300, likes: 100 }, kind: "watch", videoId: VIDEO_B },
id: "watch-history-back-forward-replace",
origin: { counts: { dislikes: 100, likes: 300 }, kind: "watch", state: "neutral", videoId: VIDEO_A },
viewport: { height: 720, width: 1280 },
},
{
coverage: {
destination: "watch",
dom: ["replace-page-and-controls"],
origin: "watch",
timing: ["no-navigate-finish"],
trigger: "autoplay-ended",
width: "wide-desktop",
},
destination: { counts: { dislikes: 300, likes: 100 }, kind: "watch", videoId: VIDEO_B },
id: "watch-autoplay-watch-replace-no-finish",
origin: { counts: { dislikes: 100, likes: 300 }, kind: "watch", state: "neutral", videoId: VIDEO_A },
viewport: { height: 720, width: 1280 },
},
{
coverage: {
destination: "shorts",
dom: ["replace-page-and-controls", "delayed-hydration"],
origin: "watch",
timing: ["finish-before-hydration"],
trigger: "direct-link",
width: "wide-desktop",
},
destination: { counts: { dislikes: 300, likes: 100 }, kind: "shorts", videoId: VIDEO_B },
id: "watch-direct-short-delayed",
origin: { counts: { dislikes: 100, likes: 300 }, kind: "watch", state: "neutral", videoId: VIDEO_A },
viewport: { height: 720, width: 1280 },
},
{
coverage: {
destination: "watch",
dom: ["replace-page-and-controls", "delayed-hydration"],
origin: "shorts",
timing: ["finish-before-hydration"],
trigger: "direct-link",
width: "wide-desktop",
},
destination: { counts: { dislikes: 300, likes: 100 }, kind: "watch", videoId: VIDEO_B },
id: "short-direct-watch-delayed",
origin: { counts: { dislikes: 100, likes: 300 }, kind: "shorts", state: "neutral", videoId: VIDEO_A },
viewport: { height: 720, width: 1280 },
},
{
coverage: {
destination: "watch",
dom: ["replace-current-action-container"],
origin: "watch",
timing: ["same-video", "no-navigation-event"],
trigger: "dom-replacement",
width: "wide-desktop",
},
destination: { counts: { dislikes: 100, likes: 300 }, kind: "watch", videoId: VIDEO_A },
id: "watch-current-action-container-replace",
origin: { counts: { dislikes: 100, likes: 300 }, kind: "watch", state: "neutral", videoId: VIDEO_A },
viewport: { height: 720, width: 1280 },
},
];
const USERSCRIPT_MATRIX_RUNTIME = {
name: "userscript",
selectors: {
bar: "#return-youtube-dislike-bar",
container: "#return-youtube-dislike-bar-container",
tooltip: "#ryd-dislike-tooltip",
wrapper: ".ryd-tooltip",
shortsDislike: "[data-ryd-synthetic-shorts-dislike]",
shortsVideoAttribute: "data-ryd-video-id",
},
tooltipText({ dislikes, likes }) {
return `${likes} / ${dislikes}`;
},
};
async function installSidebarRetainPruneFixture(context, scenario) {
await context.addInitScript((matrixScenario) => {
if (!location.hostname.endsWith("youtube.com")) return;
addEventListener(
"DOMContentLoaded",
() => {
if (new URL(location.href).searchParams.get("rydNavigationFixture") !== "1") return;
const fixturePage = document.getElementById("fixture-page");
if (!fixturePage || !globalThis.__navigationFixture) {
throw new Error("The navigation matrix requires the navigation-page fixture.");
}
const retainedTrees = document.createElement("div");
retainedTrees.hidden = true;
retainedTrees.id = "fixture-matrix-retained-trees";
// Keep the retained outgoing tree before the live page in document
// order. This reproduces YouTube variants where a first-match query
// resolves stale connected controls before the current rendered tree.
fixturePage.before(retainedTrees);
const sidebar = document.createElement("aside");
sidebar.id = "fixture-matrix-sidebar";
sidebar.setAttribute("aria-label", "Fixture sidebar");
const destinationLink = document.createElement("a");
destinationLink.id = "fixture-matrix-sidebar-watch";
destinationLink.href = `/watch?v=${matrixScenario.destination.videoId}&rydNavigationFixture=1`;
destinationLink.textContent = "Open sidebar video";
sidebar.appendChild(destinationLink);
fixturePage.before(sidebar);
const transition = {
destinationTopRow: null,
documentIdentity: `matrix-${Date.now()}-${Math.random()}`,
phase: "origin",
timeline: [],
};
const record = (phase) => {
transition.phase = phase;
transition.timeline.push(phase);
};
destinationLink.addEventListener("click", (event) => {
event.preventDefault();
if (transition.phase !== "origin") throw new Error(`Cannot navigate from matrix phase ${transition.phase}.`);
const watchPage = fixturePage.querySelector('[data-fixture-page-kind="watch"]');
const outgoingTopRow = watchPage?.querySelector("#top-row");
const watchFlexy = watchPage?.querySelector("ytd-watch-flexy");
if (!watchPage || !outgoingTopRow || !watchFlexy) {
throw new Error("The origin watch fixture is incomplete.");
}
const destinationTopRow = outgoingTopRow.cloneNode(true);
destinationTopRow.removeAttribute("style");
destinationTopRow.querySelectorAll(".ryd-tooltip").forEach((element) => element.remove());
const destinationControls = destinationTopRow.querySelector("[data-fixture-control-video-id]");
destinationControls.setAttribute("data-fixture-control-video-id", matrixScenario.destination.videoId);
for (const role of ["like", "dislike"]) {
const control = destinationControls.querySelector(`[data-ryd-role="${role}"]`);
control.classList.remove("style-default-active");
control.classList.add("style-text");
control.querySelector("button")?.setAttribute("aria-pressed", "false");
}
const destinationDislikeText = destinationControls.querySelector('[data-ryd-role="dislike"] #text');
destinationDislikeText.textContent = "";
transition.destinationTopRow = destinationTopRow;
document.dispatchEvent(new Event("yt-navigate-start", { bubbles: true }));
record("navigate-start");
retainedTrees.appendChild(outgoingTopRow);
history.pushState({}, "", `/watch?v=${matrixScenario.destination.videoId}&rydNavigationFixture=1`);
watchPage.setAttribute("data-fixture-video-id", matrixScenario.destination.videoId);
watchFlexy.setAttribute("video-id", matrixScenario.destination.videoId);
record("route-and-shell");
document.dispatchEvent(new Event("yt-navigate-finish", { bubbles: true }));
record("navigate-finish");
});
globalThis.__navigationMatrixFixture = {
detachOutgoing() {
retainedTrees.replaceChildren();
record("detach-outgoing");
},
hydrateDestination() {
if (transition.phase !== "navigate-finish" || !transition.destinationTopRow) {
throw new Error(`Cannot hydrate the destination from matrix phase ${transition.phase}.`);
}
const watchPage = fixturePage.querySelector('[data-fixture-page-kind="watch"]');
const firstNavigationLink = watchPage?.querySelector("a[data-fixture-page-kind]");
watchPage.insertBefore(transition.destinationTopRow, firstNavigationLink ?? null);
transition.destinationTopRow = null;
record("hydrate-destination-controls");
},
snapshot() {
return {
currentControls: fixturePage.querySelectorAll("[data-fixture-control-video-id]").length,
documentIdentity: transition.documentIdentity,
phase: transition.phase,
retainedControls: retainedTrees.querySelectorAll("[data-fixture-control-video-id]").length,
retainedTreesConnected: retainedTrees.isConnected,
retainedTreesHidden: retainedTrees.hidden,
sidebarConnected: sidebar.isConnected,
timeline: [...transition.timeline],
};
},
};
},
{ once: true },
);
}, scenario);
}
async function installSameRootHiddenFirstFixture(context, scenario) {
await context.addInitScript((matrixScenario) => {
if (!location.hostname.endsWith("youtube.com")) return;
addEventListener(
"DOMContentLoaded",
() => {
if (new URL(location.href).searchParams.get("rydNavigationFixture") !== "1") return;
const fixturePage = document.getElementById("fixture-page");
const watchPage = fixturePage?.querySelector('[data-fixture-page-kind="watch"]');
const watchFlexy = watchPage?.querySelector("ytd-watch-flexy");
const originTopRow = watchPage?.querySelector("#top-row");
if (!fixturePage || !watchPage || !watchFlexy || !originTopRow) {
throw new Error("The same-root matrix requires a complete watch fixture.");
}
if (matrixScenario.transition?.controlMarkup === "legacy-segmented") {
const modernSegmented = originTopRow.querySelector("segmented-like-dislike-button-view-model");
if (!modernSegmented) {
throw new Error("The legacy segmented matrix could not find its source controls.");
}
const legacySegmented = document.createElement("ytd-segmented-like-dislike-button-renderer");
for (const attribute of modernSegmented.attributes) {
legacySegmented.setAttribute(attribute.name, attribute.value);
}
legacySegmented.style.cssText = "display:flex;gap:8px;min-height:48px;width:320px";
while (modernSegmented.firstChild) {
legacySegmented.appendChild(modernSegmented.firstChild);
}
const legacyLike = legacySegmented.querySelector('[data-ryd-role="like"]');
const legacyDislike = legacySegmented.querySelector('[data-ryd-role="dislike"]');
legacyLike.id = "segmented-like-button";
legacyDislike.id = "segmented-dislike-button";
for (const button of legacySegmented.querySelectorAll("button")) {
button.style.cssText = "min-height:36px;min-width:96px";
}
modernSegmented.replaceWith(legacySegmented);
}
watchFlexy.appendChild(originTopRow);
const sidebar = document.createElement("aside");
sidebar.id = "fixture-matrix-same-root-sidebar";
sidebar.setAttribute("aria-label", "Fixture same-root sidebar");
const destinationLink = document.createElement("a");
destinationLink.id = "fixture-matrix-same-root-watch";
destinationLink.href = `/watch?v=${matrixScenario.destination.videoId}&rydNavigationFixture=1`;
destinationLink.textContent = "Open same-root sidebar video";
sidebar.appendChild(destinationLink);
fixturePage.before(sidebar);
const documentIdentity = `matrix-${Date.now()}-${Math.random()}`;
destinationLink.addEventListener("click", (event) => {
event.preventDefault();
const outgoingTopRow = watchFlexy.querySelector("#top-row");
if (!outgoingTopRow || outgoingTopRow.hasAttribute("data-fixture-matrix-hidden-outgoing")) {
throw new Error("The same-root matrix origin is unavailable.");
}
const destinationTopRow = outgoingTopRow.cloneNode(true);
destinationTopRow.removeAttribute("style");
destinationTopRow.querySelectorAll(".ryd-tooltip").forEach((element) => element.remove());
const destinationControls = destinationTopRow.querySelector("[data-fixture-control-video-id]");
destinationControls.setAttribute("data-fixture-control-video-id", matrixScenario.destination.videoId);
for (const role of ["like", "dislike"]) {
const control = destinationControls.querySelector(`[data-ryd-role="${role}"]`);
control.classList.remove("style-default-active");
control.classList.add("style-text");
control.querySelector("button")?.setAttribute("aria-pressed", "false");
}
destinationControls.querySelector('[data-ryd-role="dislike"] #text').textContent = "";
destinationTopRow.setAttribute("data-fixture-matrix-live-destination", "true");
document.dispatchEvent(new Event("yt-navigate-start", { bubbles: true }));
if (matrixScenario.transition?.outgoingPresentation === "offscreen") {
outgoingTopRow.style.setProperty("left", "-10000px", "important");
outgoingTopRow.style.setProperty("position", "fixed", "important");
outgoingTopRow.style.setProperty("top", "0", "important");
} else {
outgoingTopRow.hidden = true;
}
outgoingTopRow.setAttribute("data-fixture-matrix-hidden-outgoing", "true");
history.pushState({}, "", `/watch?v=${matrixScenario.destination.videoId}&rydNavigationFixture=1`);
watchPage.setAttribute("data-fixture-video-id", matrixScenario.destination.videoId);
watchFlexy.setAttribute("video-id", matrixScenario.destination.videoId);
watchFlexy.appendChild(destinationTopRow);
document.dispatchEvent(new Event("yt-navigate-finish", { bubbles: true }));
});
globalThis.__navigationMatrixSameRootFixture = {
detachOutgoing() {
watchFlexy.querySelector('[data-fixture-matrix-hidden-outgoing="true"]')?.remove();
},
snapshot() {
const hiddenOutgoing = watchFlexy.querySelector('[data-fixture-matrix-hidden-outgoing="true"]');
const liveDestination = watchFlexy.querySelector('[data-fixture-matrix-live-destination="true"]');
const hiddenBox = hiddenOutgoing?.getBoundingClientRect();
return {
documentIdentity,
hiddenFirst: Boolean(
hiddenOutgoing &&
liveDestination &&
hiddenOutgoing.compareDocumentPosition(liveDestination) & Node.DOCUMENT_POSITION_FOLLOWING,
),
hiddenOutgoingConnected: Boolean(hiddenOutgoing?.isConnected),
hiddenOutgoingHeight: hiddenBox?.height ?? null,
hiddenOutgoingWidth: hiddenBox?.width ?? null,
outgoingIntersectsViewport: Boolean(
hiddenBox &&
hiddenBox.width > 0 &&
hiddenBox.height > 0 &&
hiddenBox.bottom > 0 &&
hiddenBox.right > 0 &&
hiddenBox.top < innerHeight &&
hiddenBox.left < innerWidth,
),
outgoingPresentation: matrixScenario.transition?.outgoingPresentation ?? "hidden",
liveDestinationConnected: Boolean(liveDestination?.isConnected),
rootVideoId: watchFlexy.getAttribute("video-id"),
sameRoot: Boolean(
hiddenOutgoing &&
liveDestination &&
hiddenOutgoing.closest("ytd-watch-flexy") === liveDestination.closest("ytd-watch-flexy"),
),
sidebarConnected: sidebar.isConnected,
};
},
};
},
{ once: true },
);
}, scenario);
}
async function installSameNodeRouteCompletionFixture(context, scenario) {
await context.addInitScript((matrixScenario) => {
if (!location.hostname.endsWith("youtube.com")) return;
addEventListener(
"DOMContentLoaded",
() => {
if (new URL(location.href).searchParams.get("rydNavigationFixture") !== "1") return;
const fixturePage = document.getElementById("fixture-page");
const watchPage = fixturePage?.querySelector('[data-fixture-page-kind="watch"]');
const watchFlexy = watchPage?.querySelector("ytd-watch-flexy");
const topRow = watchPage?.querySelector("#top-row");
const buttons = topRow?.querySelector("#top-level-buttons-computed");
const controls = buttons?.querySelector("[data-fixture-control-video-id]");
const like = controls?.querySelector('[data-ryd-role="like"]');
const dislike = controls?.querySelector('[data-ryd-role="dislike"]');
if (!fixturePage || !watchPage || !watchFlexy || !topRow || !buttons || !controls || !like || !dislike) {
throw new Error("The same-node matrix requires a complete watch fixture.");
}
watchFlexy.appendChild(topRow);
const sidebar = document.createElement("aside");
sidebar.id = "fixture-matrix-same-node-sidebar";
sidebar.setAttribute("aria-label", "Fixture same-node sidebar");
const destinationLink = document.createElement("a");
destinationLink.id = "fixture-matrix-same-node-watch";
destinationLink.href = `/watch?v=${matrixScenario.destination.videoId}&rydNavigationFixture=1`;
destinationLink.textContent = "Open same-node sidebar video";
sidebar.appendChild(destinationLink);
fixturePage.before(sidebar);
const documentIdentity = `matrix-${Date.now()}-${Math.random()}`;
const timeline = [];
destinationLink.addEventListener("click", (event) => {
event.preventDefault();
document.dispatchEvent(new Event("yt-navigate-start", { bubbles: true }));
timeline.push("navigate-start");
history.pushState({}, "", `/watch?v=${matrixScenario.destination.videoId}&rydNavigationFixture=1`);
watchPage.setAttribute("data-fixture-video-id", matrixScenario.destination.videoId);
watchFlexy.setAttribute("video-id", matrixScenario.destination.videoId);
// This fixture-only marker lets the test address B. It is deliberately
// outside the userscript observer's attribute filter and provides no
// runtime ownership evidence.
controls.setAttribute("data-fixture-control-video-id", matrixScenario.destination.videoId);
timeline.push("route-and-root-only");
document.dispatchEvent(new Event("yt-navigate-finish", { bubbles: true }));
timeline.push("navigate-finish");
});
globalThis.__navigationMatrixSameNodeFixture = {
snapshot() {
return {
buttonsReused: buttons === watchFlexy.querySelector("#top-level-buttons-computed"),
controlsReused: controls === watchFlexy.querySelector("[data-fixture-control-video-id]"),
dislikeReused: dislike === watchFlexy.querySelector('[data-ryd-role="dislike"]'),
documentIdentity,
likeReused: like === watchFlexy.querySelector('[data-ryd-role="like"]'),
rootVideoId: watchFlexy.getAttribute("video-id"),
sidebarConnected: sidebar.isConnected,
timeline: [...timeline],
};
},
};
},
{ once: true },
);
}, scenario);
}
async function installStandardNavigationMatrixFixture(context, scenario) {
await context.addInitScript((matrixScenario) => {
if (!location.hostname.endsWith("youtube.com")) return;
addEventListener(
"DOMContentLoaded",
() => {
if (new URL(location.href).searchParams.get("rydNavigationFixture") !== "1") return;
const fixturePage = document.getElementById("fixture-page");
if (!fixturePage || !globalThis.__navigationFixture) {
throw new Error("The navigation matrix requires the navigation-page fixture.");
}
const probe = {
documentIdentity: `matrix-${Date.now()}-${Math.random()}`,
historyRenders: [],
navigateFinishes: 0,
navigateStarts: 0,
};
document.addEventListener("yt-navigate-start", () => {
probe.navigateStarts += 1;
});
document.addEventListener("yt-navigate-finish", () => {
probe.navigateFinishes += 1;
});
const delayedLinkId = {
"short-direct-watch-delayed": "short-to-watch",
"watch-direct-short-delayed": "watch-to-short",
}[matrixScenario.id];
if (delayedLinkId) {
document.getElementById(delayedLinkId)?.setAttribute("data-fixture-control-delay", "600");
}
if (matrixScenario.id === "watch-history-back-forward-replace") {
addEventListener("popstate", () => {
const videoId = new URL(location.href).searchParams.get("v");
const watchPage = fixturePage.querySelector('[data-fixture-page-kind="watch"]');
const currentTopRow = watchPage?.querySelector("#top-row");
const watchFlexy = watchPage?.querySelector("ytd-watch-flexy");
if (!videoId || !watchPage || !currentTopRow || !watchFlexy) {
throw new Error("The history matrix could not render the current watch entry.");
}
const replacementTopRow = currentTopRow.cloneNode(true);
replacementTopRow.removeAttribute("style");
replacementTopRow.querySelectorAll(".ryd-tooltip").forEach((element) => element.remove());
const controls = replacementTopRow.querySelector("[data-fixture-control-video-id]");
controls.setAttribute("data-fixture-control-video-id", videoId);
for (const role of ["like", "dislike"]) {
const control = controls.querySelector(`[data-ryd-role="${role}"]`);
control.classList.remove("style-default-active");
control.classList.add("style-text");
control.querySelector("button")?.setAttribute("aria-pressed", "false");
}
controls.querySelector('[data-ryd-role="dislike"] #text').textContent = "";
currentTopRow.replaceWith(replacementTopRow);
watchPage.setAttribute("data-fixture-video-id", videoId);
watchFlexy.setAttribute("video-id", videoId);
probe.historyRenders.push(videoId);
document.dispatchEvent(new Event("yt-navigate-finish", { bubbles: true }));
});
}
globalThis.__navigationMatrixProbe = {
snapshot() {
return {
currentKind:
fixturePage.querySelector("[data-fixture-page-kind]")?.getAttribute("data-fixture-page-kind") ?? null,
currentVideoId:
fixturePage.querySelector("[data-fixture-video-id]")?.getAttribute("data-fixture-video-id") ?? null,
documentIdentity: probe.documentIdentity,
historyRenders: [...probe.historyRenders],
navigateFinishes: probe.navigateFinishes,
navigateStarts: probe.navigateStarts,
transitionPending: fixturePage.dataset.fixtureTransitionPending ?? null,
};
},
};
},
{ once: true },
);
}, scenario);
}
async function installNavigationMatrixFixture(context, scenario) {
if (!NAVIGATION_MATRIX.some((candidate) => candidate.id === scenario.id)) {
throw new Error(`No navigation matrix fixture is registered for ${scenario.id}.`);
}
if (scenario.id === "watch-sidebar-watch-retain-prune") {
await installSidebarRetainPruneFixture(context, scenario);
return;
}
if (
scenario.id === "watch-sidebar-watch-same-root-hidden-first" ||
scenario.id === "watch-sidebar-watch-same-root-offscreen-first" ||
scenario.id === "watch-sidebar-watch-legacy-segmented-duplicate-ids"
) {
await installSameRootHiddenFirstFixture(context, scenario);
return;
}
if (scenario.id === "watch-sidebar-watch-same-node-route-complete") {
await installSameNodeRouteCompletionFixture(context, scenario);
return;
}
await installStandardNavigationMatrixFixture(context, scenario);
}
function currentWatchLocators(page, runtime, videoId) {
const controls = page.locator(`[data-fixture-control-video-id="${videoId}"]`);
const reactionRegion = controls.locator("xpath=..");
const wrapper = reactionRegion.locator(`:scope > ${runtime.selectors.wrapper}`);
const container = wrapper.locator(runtime.selectors.container);
return {
bar: container.locator(runtime.selectors.bar),
container,
controls,
reactionRegion,
tooltip: wrapper.locator(runtime.selectors.tooltip),
wrapper,
};
}
async function expectOwnedWatchBar(page, runtime, videoId, counts) {
const locators = currentWatchLocators(page, runtime, videoId);
await expect(locators.controls).toHaveCount(1);
await expect(locators.controls.locator('[data-ryd-role="dislike"] #text')).toHaveText(String(counts.dislikes));
await expect(locators.wrapper).toHaveCount(1);
await expect(locators.wrapper).toBeVisible();
await expect(locators.container).toBeVisible();
await expect(locators.bar).toBeVisible();
await expect(locators.tooltip).toContainText(runtime.tooltipText(counts));
await expect(page.locator(runtime.selectors.wrapper)).toHaveCount(1);
await expect(page.locator(runtime.selectors.container)).toHaveCount(1);
await expect(page.locator(runtime.selectors.bar)).toHaveCount(1);
await expect(page.locator(runtime.selectors.tooltip)).toHaveCount(1);
const geometry = await locators.reactionRegion.evaluate((reactionRegion, selectors) => {
const box = (element) => {
const bounds = element.getBoundingClientRect();
return {
bottom: bounds.bottom,
left: bounds.left,
right: bounds.right,
top: bounds.top,
width: bounds.width,
};
};
const like = reactionRegion.querySelector('[data-ryd-role="like"] button');
const dislike = reactionRegion.querySelector('[data-ryd-role="dislike"] button');
const wrapper = reactionRegion.querySelector(`:scope > ${selectors.wrapper}`);
const container = wrapper.querySelector(selectors.container);
const bar = container.querySelector(selectors.bar);
return {
bar: box(bar),
container: box(container),
dislike: box(dislike),
like: box(like),
wrapper: box(wrapper),
};
}, runtime.selectors);
expect(geometry.wrapper.width).toBeCloseTo(geometry.like.width + geometry.dislike.width, 0);
expect(geometry.container.top).toBeGreaterThanOrEqual(Math.max(geometry.like.bottom, geometry.dislike.bottom) - 1);
expect(geometry.bar.left).toBeGreaterThanOrEqual(geometry.container.left - 1);
expect(geometry.bar.right).toBeLessThanOrEqual(geometry.container.right + 1);
expect(geometry.bar.width / geometry.container.width).toBeCloseTo(counts.likes / (counts.likes + counts.dislikes), 2);
return locators;
}
async function expectOwnedShortControl(page, runtime, videoId, counts) {
await expect(page).toHaveURL((url) => url.pathname === `/shorts/${videoId}`);
const activeRenderer = page.locator(`ytd-reel-video-renderer[video-id="${videoId}"][is-active]`);
const dislike = activeRenderer.locator(runtime.selectors.shortsDislike);
await expect(activeRenderer).toHaveCount(1);
await expect(activeRenderer).toBeVisible();
await expect(dislike).toHaveCount(1);
await expect(dislike).toBeVisible();
await expect(dislike.locator("#text")).toHaveText(String(counts.dislikes));
if (runtime.selectors.shortsVideoAttribute) {
await expect(dislike).toHaveAttribute(runtime.selectors.shortsVideoAttribute, videoId);
}
await expect(page.locator(`${runtime.selectors.shortsDislike}:visible`)).toHaveCount(1);
await expect(page.locator(runtime.selectors.wrapper)).toHaveCount(0);
await expect(page.locator(runtime.selectors.container)).toHaveCount(0);
await expect(page.locator(runtime.selectors.bar)).toHaveCount(0);
return { activeRenderer, dislike };
}
async function readOwnedSurfaceInvariant(page, runtime, videoId, counts, kind) {
return page.evaluate(
({ counts: expectedCounts, expectedTooltip, kind: expectedKind, selectors, videoId: expectedVideoId }) => {
const visible = (element) => {
if (!element?.isConnected || element.closest("[hidden], [aria-hidden='true']")) return false;
const style = getComputedStyle(element);
const bounds = element.getBoundingClientRect();
return style.display !== "none" && style.visibility !== "hidden" && bounds.width > 0 && bounds.height > 0;
};
const currentUrl = new URL(location.href);
const common = {
bars: document.querySelectorAll(selectors.bar).length,
containers: document.querySelectorAll(selectors.container).length,
pathname: currentUrl.pathname,
tooltips: document.querySelectorAll(selectors.tooltip).length,
wrappers: document.querySelectorAll(selectors.wrapper).length,
};
if (expectedKind === "shorts") {
const activeRenderers = Array.from(
document.querySelectorAll(`ytd-reel-video-renderer[video-id="${expectedVideoId}"][is-active]`),
);
const currentControls = activeRenderers.flatMap((renderer) =>
Array.from(renderer.querySelectorAll(selectors.shortsDislike)),
);
const currentActionButtons = activeRenderers.flatMap((renderer) =>
Array.from(renderer.querySelectorAll("reel-action-bar-view-model button")).filter(visible),
);
const visibleControls = Array.from(document.querySelectorAll(selectors.shortsDislike)).filter(visible);
return {
...common,
activeRenderers: activeRenderers.length,
count: currentControls[0]?.querySelector("#text")?.textContent?.trim() ?? null,
currentActionButtons: currentActionButtons.length,
currentControls: currentControls.length,
currentVisible: currentControls.length === 1 && visible(currentControls[0]),
expectedCount: String(expectedCounts.dislikes),
expectedPathname: `/shorts/${expectedVideoId}`,
kind: expectedKind,
visibleControls: visibleControls.length,
};
}
const controls = Array.from(document.querySelectorAll(`[data-fixture-control-video-id="${expectedVideoId}"]`));
const currentControls = controls.filter(visible);
const reactionRegion = currentControls[0]?.parentElement ?? null;
const wrapper = reactionRegion?.querySelector(`:scope > ${selectors.wrapper}`) ?? null;
const container = wrapper?.querySelector(selectors.container) ?? null;
const bar = container?.querySelector(selectors.bar) ?? null;
const containerBounds = container?.getBoundingClientRect();
const barBounds = bar?.getBoundingClientRect();
return {
...common,
count: currentControls[0]?.querySelector('[data-ryd-role="dislike"] #text')?.textContent?.trim() ?? null,
currentControls: currentControls.length,
expectedCount: String(expectedCounts.dislikes),
expectedPathname: "/watch",
expectedRatio: expectedCounts.likes / (expectedCounts.likes + expectedCounts.dislikes),
expectedTooltip,
expectedVideoId,
kind: expectedKind,
ownerBarVisible: visible(bar),
ownerContainerVisible: visible(container),
ownerTooltip: wrapper?.querySelector(selectors.tooltip)?.textContent?.replace(/\s+/g, " ").trim() ?? null,
ownerWrapperVisible: visible(wrapper),
ratio: containerBounds?.width > 0 && barBounds ? barBounds.width / containerBounds.width : null,
videoId: currentUrl.searchParams.get("v"),
};
},
{ counts, expectedTooltip: runtime.tooltipText(counts), kind, selectors: runtime.selectors, videoId },
);
}
function ownedSurfaceInvariantIsValid(sample) {
if (sample.pathname !== sample.expectedPathname || sample.count !== sample.expectedCount) return false;
if (sample.kind === "shorts") {
return (
sample.activeRenderers === 1 &&
sample.currentControls === 1 &&
sample.currentActionButtons >= 4 &&
sample.currentVisible &&
sample.visibleControls === 1 &&
sample.wrappers === 0 &&
sample.containers === 0 &&
sample.bars === 0
);
}
return (
sample.videoId === sample.expectedVideoId &&
sample.currentControls === 1 &&
sample.wrappers === 1 &&
sample.containers === 1 &&
sample.bars === 1 &&
sample.tooltips === 1 &&
sample.ownerWrapperVisible &&
sample.ownerContainerVisible &&
sample.ownerBarVisible &&
sample.ownerTooltip?.includes(sample.expectedTooltip) &&
Math.abs(sample.ratio - sample.expectedRatio) <= 0.015
);
}
async function waitForOwnedSurfaceStability(page, runtime, surface, timing = {}) {
return waitForStableInvariant({
intervalMs: 25,
isValid: ownedSurfaceInvariantIsValid,
label: `${runtime.name} ${surface.kind} ${surface.videoId} ownership`,
read: () => readOwnedSurfaceInvariant(page, runtime, surface.videoId, surface.counts, surface.kind),
stableForMs: timing.stableForMs ?? 250,
timeoutMs: timing.timeoutMs ?? 2_000,
});
}
async function waitForOwnedSurfaceWithinBudget(page, runtime, surface, maxFirstValidMs = 1_000) {
const readiness = await waitForOwnedSurfaceStability(page, runtime, surface, {
stableForMs: 250,
timeoutMs: maxFirstValidMs + 500,
});
expect(readiness.firstValidMs).toBeLessThanOrEqual(maxFirstValidMs);
return readiness;
}
async function assertOwnedSurfaceContinuously(page, runtime, surface, durationMs = 600) {
return assertInvariantContinuously({
durationMs,
intervalMs: 25,
isValid: ownedSurfaceInvariantIsValid,
label: `${runtime.name} settled ${surface.kind} ${surface.videoId}`,
read: () => readOwnedSurfaceInvariant(page, runtime, surface.videoId, surface.counts, surface.kind),
});
}
function expectCountRequestVideoIds(backend, expectedVideoIds) {
expect(backend.requestsFor("GET", "/votes").map((request) => request.query.videoId)).toEqual(expectedVideoIds);
}
async function readStandardProbe(page) {
return page.evaluate(() => globalThis.__navigationMatrixProbe.snapshot());
}
async function pruneCurrentWatchBar(page, runtime, locators, surface) {
await locators.reactionRegion.evaluate((reactionRegion, wrapperSelector) => {
const stats = { addedWrappers: 0, removedWrappers: 0 };
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof Element && node.matches(wrapperSelector)) stats.addedWrappers += 1;
}
for (const node of mutation.removedNodes) {
if (node instanceof Element && node.matches(wrapperSelector)) stats.removedWrappers += 1;
}
}
});
observer.observe(reactionRegion, { childList: true });
globalThis.__navigationMatrixBarObserver = observer;
globalThis.__navigationMatrixBarStats = stats;
const wrapper = reactionRegion.querySelector(`:scope > ${wrapperSelector}`);
if (!wrapper) throw new Error("The current matrix reaction tree has no owned rate bar to prune.");
wrapper.remove();
}, runtime.selectors.wrapper);
await waitForOwnedSurfaceWithinBudget(page, runtime, surface);
await assertOwnedSurfaceContinuously(page, runtime, surface);
return page.evaluate(() => {
globalThis.__navigationMatrixBarObserver.disconnect();
return globalThis.__navigationMatrixBarStats;
});
}
async function runWatchSidebarRetainPruneScenario({ backend, page, runtime, scenario }) {
const { destination, origin } = scenario;
const initialDocumentIdentity = await page.evaluate(
() => globalThis.__navigationMatrixFixture.snapshot().documentIdentity,
);
await expect(page).toHaveURL((url) => url.pathname === "/watch" && url.searchParams.get("v") === origin.videoId);
await expectOwnedWatchBar(page, runtime, origin.videoId, origin.counts);
expect(backend.requestsFor("GET", "/votes").map((request) => request.query.videoId)).toEqual([origin.videoId]);
const destinationCountGate = backend.defer("GET", "/votes");
await page.locator("#fixture-matrix-sidebar-watch").click();
await expect(page).toHaveURL((url) => url.pathname === "/watch" && url.searchParams.get("v") === destination.videoId);
expect(await page.evaluate(() => globalThis.__navigationMatrixFixture.snapshot())).toEqual({
currentControls: 0,
documentIdentity: initialDocumentIdentity,
phase: "navigate-finish",
retainedControls: 1,
retainedTreesConnected: true,
retainedTreesHidden: true,
sidebarConnected: true,
timeline: ["navigate-start", "route-and-shell", "navigate-finish"],
});
await expect(page.locator("#fixture-matrix-retained-trees").locator(runtime.selectors.wrapper)).toHaveCount(1);
expect(backend.requestsFor("GET", "/votes").map((request) => request.query.videoId)).toEqual([origin.videoId]);
await page.evaluate(() => globalThis.__navigationMatrixFixture.hydrateDestination());
const destinationRequest = await destinationCountGate.seen;
try {
expect(destinationRequest.query.videoId).toBe(destination.videoId);
expect(await page.evaluate(() => globalThis.__navigationMatrixFixture.snapshot())).toMatchObject({
currentControls: 1,
documentIdentity: initialDocumentIdentity,
phase: "hydrate-destination-controls",
retainedControls: 1,
retainedTreesConnected: true,
retainedTreesHidden: true,
sidebarConnected: true,
timeline: ["navigate-start", "route-and-shell", "navigate-finish", "hydrate-destination-controls"],
});
const gatedDestination = currentWatchLocators(page, runtime, destination.videoId);
await expect(gatedDestination.controls).toHaveCount(1);
await expect(gatedDestination.controls.locator('[data-ryd-role="dislike"] #text')).toHaveText("");
await expect(gatedDestination.wrapper).toHaveCount(0);
await expect(page.locator("#fixture-matrix-retained-trees").locator(runtime.selectors.wrapper)).toHaveCount(1);
} finally {
if (!destinationCountGate.released) {
destinationCountGate.release({ body: { ...destination.counts, rating: 4.5 } });
}
}
await waitForOwnedSurfaceWithinBudget(page, runtime, destination);
const destinationLocators = await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
await expect(page.locator("#fixture-matrix-retained-trees").locator(runtime.selectors.wrapper)).toHaveCount(0);
expect(backend.requestsFor("GET", "/votes").map((request) => request.query.videoId)).toEqual([
origin.videoId,
destination.videoId,
]);
const pruneStats = await pruneCurrentWatchBar(page, runtime, destinationLocators, destination);
expect(pruneStats).toEqual({ addedWrappers: 1, removedWrappers: 1 });
await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
expect(backend.requestsFor("GET", "/votes").map((request) => request.query.videoId)).toEqual([
origin.videoId,
destination.videoId,
]);
await page.evaluate(() => globalThis.__navigationMatrixFixture.detachOutgoing());
await assertOwnedSurfaceContinuously(page, runtime, destination, 550);
await expect(page.locator("#fixture-matrix-retained-trees").locator("[data-fixture-control-video-id]")).toHaveCount(
0,
);
await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
expect(await page.evaluate(() => globalThis.__navigationMatrixFixture.snapshot())).toMatchObject({
currentControls: 1,
documentIdentity: initialDocumentIdentity,
phase: "detach-outgoing",
retainedControls: 0,
retainedTreesConnected: true,
retainedTreesHidden: true,
sidebarConnected: true,
timeline: [
"navigate-start",
"route-and-shell",
"navigate-finish",
"hydrate-destination-controls",
"detach-outgoing",
],
});
expect(backend.requestsFor("POST", "/interact/vote")).toHaveLength(0);
expect(backend.requestsFor("POST", "/interact/confirmVote")).toHaveLength(0);
}
async function runWatchSameRootHiddenFirstScenario({ backend, page, runtime, scenario }) {
const { destination, origin } = scenario;
const initialDocumentIdentity = await page.evaluate(
() => globalThis.__navigationMatrixSameRootFixture.snapshot().documentIdentity,
);
await expectOwnedWatchBar(page, runtime, origin.videoId, origin.counts);
expectCountRequestVideoIds(backend, [origin.videoId]);
await page.locator("#fixture-matrix-same-root-watch").click();
await expect(page).toHaveURL((url) => url.pathname === "/watch" && url.searchParams.get("v") === destination.videoId);
await waitForOwnedSurfaceWithinBudget(page, runtime, destination);
await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
const outgoingIsOffscreen = scenario.transition.outgoingPresentation === "offscreen";
const settledTopology = await page.evaluate(() => globalThis.__navigationMatrixSameRootFixture.snapshot());
expect(settledTopology).toEqual({
documentIdentity: initialDocumentIdentity,
hiddenFirst: true,
hiddenOutgoingConnected: true,
hiddenOutgoingHeight: outgoingIsOffscreen ? expect.any(Number) : 0,
hiddenOutgoingWidth: outgoingIsOffscreen ? expect.any(Number) : 0,
liveDestinationConnected: true,
outgoingIntersectsViewport: false,
outgoingPresentation: scenario.transition.outgoingPresentation,
rootVideoId: destination.videoId,
sameRoot: true,
sidebarConnected: true,
});
if (outgoingIsOffscreen) {
expect(settledTopology.hiddenOutgoingHeight).toBeGreaterThan(0);
expect(settledTopology.hiddenOutgoingWidth).toBeGreaterThan(0);
}
const currentRoot = page.locator(`ytd-watch-flexy[video-id="${destination.videoId}"]`);
await expect(currentRoot.locator("ytd-menu-renderer.ytd-watch-metadata > div")).toHaveCount(2);
await expect(
currentRoot.locator('[data-fixture-matrix-hidden-outgoing="true"]').locator(runtime.selectors.wrapper),
).toHaveCount(0);
await expect(
currentRoot.locator('[data-fixture-matrix-live-destination="true"]').locator(runtime.selectors.wrapper),
).toHaveCount(1);
if (scenario.transition.controlMarkup === "legacy-segmented") {
await expect(currentRoot.locator("#segmented-like-button")).toHaveCount(2);
await expect(currentRoot.locator("#segmented-dislike-button")).toHaveCount(2);
await expect(
currentRoot.locator('[data-fixture-matrix-live-destination="true"]').locator("#segmented-dislike-button #text"),
).toHaveText(String(destination.counts.dislikes));
}
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
await assertOwnedSurfaceContinuously(page, runtime, destination);
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
expect(await page.evaluate(() => globalThis.__navigationMatrixSameRootFixture.snapshot())).toMatchObject({
documentIdentity: initialDocumentIdentity,
hiddenFirst: true,
hiddenOutgoingConnected: true,
hiddenOutgoingHeight: outgoingIsOffscreen ? expect.any(Number) : 0,
hiddenOutgoingWidth: outgoingIsOffscreen ? expect.any(Number) : 0,
liveDestinationConnected: true,
outgoingIntersectsViewport: false,
outgoingPresentation: scenario.transition.outgoingPresentation,
rootVideoId: destination.videoId,
sameRoot: true,
sidebarConnected: true,
});
await page.evaluate(() => globalThis.__navigationMatrixSameRootFixture.detachOutgoing());
await assertOwnedSurfaceContinuously(page, runtime, destination, 550);
await expect(currentRoot.locator("ytd-menu-renderer.ytd-watch-metadata > div")).toHaveCount(1);
await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
}
async function runWatchHistoryScenario({ backend, page, runtime, scenario }) {
const { destination, origin } = scenario;
const initialProbe = await readStandardProbe(page);
await expectOwnedWatchBar(page, runtime, origin.videoId, origin.counts);
expectCountRequestVideoIds(backend, [origin.videoId]);
await page.locator("#watch-next").click();
await waitForOwnedSurfaceWithinBudget(page, runtime, destination);
await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
expect(await readStandardProbe(page)).toMatchObject({
currentKind: "watch",
currentVideoId: destination.videoId,
documentIdentity: initialProbe.documentIdentity,
historyRenders: [],
navigateFinishes: 1,
navigateStarts: 0,
transitionPending: null,
});
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
await page.goBack();
await waitForOwnedSurfaceWithinBudget(page, runtime, origin);
await expectOwnedWatchBar(page, runtime, origin.videoId, origin.counts);
expect(await readStandardProbe(page)).toMatchObject({
currentKind: "watch",
currentVideoId: origin.videoId,
documentIdentity: initialProbe.documentIdentity,
historyRenders: [origin.videoId],
navigateFinishes: 2,
navigateStarts: 0,
transitionPending: null,
});
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId, origin.videoId]);
await page.goForward();
await waitForOwnedSurfaceWithinBudget(page, runtime, destination);
await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
expect(await readStandardProbe(page)).toMatchObject({
currentKind: "watch",
currentVideoId: destination.videoId,
documentIdentity: initialProbe.documentIdentity,
historyRenders: [origin.videoId, destination.videoId],
navigateFinishes: 3,
navigateStarts: 0,
transitionPending: null,
});
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId, origin.videoId, destination.videoId]);
await assertOwnedSurfaceContinuously(page, runtime, destination);
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId, origin.videoId, destination.videoId]);
}
async function runWatchAutoplayScenario({ backend, page, runtime, scenario }) {
const { destination, origin } = scenario;
const initialProbe = await readStandardProbe(page);
await expectOwnedWatchBar(page, runtime, origin.videoId, origin.counts);
expectCountRequestVideoIds(backend, [origin.videoId]);
await page.evaluate(() => globalThis.__navigationFixture.dispatchEnded());
await waitForOwnedSurfaceWithinBudget(page, runtime, destination);
await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
expect(await readStandardProbe(page)).toMatchObject({
currentKind: "watch",
currentVideoId: destination.videoId,
documentIdentity: initialProbe.documentIdentity,
historyRenders: [],
navigateFinishes: 0,
navigateStarts: 0,
transitionPending: null,
});
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
await assertOwnedSurfaceContinuously(page, runtime, destination);
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
}
async function runCrossSurfaceScenario({ backend, page, runtime, scenario }) {
const { destination, origin } = scenario;
const initialProbe = await readStandardProbe(page);
if (origin.kind === "watch") {
await expectOwnedWatchBar(page, runtime, origin.videoId, origin.counts);
} else {
await expectOwnedShortControl(page, runtime, origin.videoId, origin.counts);
}
expectCountRequestVideoIds(backend, [origin.videoId]);
const linkId = origin.kind === "watch" ? "watch-to-short" : "short-to-watch";
await page.locator(`#${linkId}`).click();
if (destination.kind === "watch") {
await expect(page).toHaveURL(
(url) => url.pathname === "/watch" && url.searchParams.get("v") === destination.videoId,
);
} else {
await expect(page).toHaveURL((url) => url.pathname === `/shorts/${destination.videoId}`);
}
expect(await readStandardProbe(page)).toMatchObject({
currentKind: origin.kind,
currentVideoId: origin.videoId,
documentIdentity: initialProbe.documentIdentity,
historyRenders: [],
navigateFinishes: 1,
navigateStarts: 0,
transitionPending: destination.kind,
});
expectCountRequestVideoIds(backend, [origin.videoId]);
await waitForOwnedSurfaceWithinBudget(page, runtime, destination);
if (destination.kind === "watch") {
await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
} else {
await expectOwnedShortControl(page, runtime, destination.videoId, destination.counts);
}
expect(await readStandardProbe(page)).toMatchObject({
currentKind: destination.kind,
currentVideoId: destination.videoId,
documentIdentity: initialProbe.documentIdentity,
historyRenders: [],
navigateFinishes: 1,
navigateStarts: 0,
transitionPending: null,
});
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
await assertOwnedSurfaceContinuously(page, runtime, destination);
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
}
async function runWatchActionContainerReplacementScenario({ backend, page, runtime, scenario }) {
const { origin } = scenario;
const initialProbe = await readStandardProbe(page);
await expectOwnedWatchBar(page, runtime, origin.videoId, origin.counts);
expectCountRequestVideoIds(backend, [origin.videoId]);
await page.evaluate((selectors) => {
const currentActions = document.querySelector(
'[data-fixture-page-kind="watch"] ytd-menu-renderer.ytd-watch-metadata > div',
);
if (!currentActions) throw new Error("The current watch fixture has no action container to replace.");
const replacement = currentActions.cloneNode(true);
replacement.setAttribute("data-fixture-matrix-action-replacement", "true");
replacement.querySelectorAll(selectors.wrapper).forEach((element) => element.remove());
const controls = replacement.querySelector("[data-fixture-control-video-id]");
for (const role of ["like", "dislike"]) {
const control = controls.querySelector(`[data-ryd-role="${role}"]`);
control.classList.remove("style-default-active");
control.classList.add("style-text");
control.querySelector("button")?.setAttribute("aria-pressed", "false");
}
controls.querySelector('[data-ryd-role="dislike"] #text').textContent = "";
const stats = { addedWrappers: 0 };
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof Element && node.matches(selectors.wrapper)) stats.addedWrappers += 1;
}
}
});
observer.observe(replacement, { childList: true });
globalThis.__navigationMatrixReplacementObserver = observer;
globalThis.__navigationMatrixReplacementStats = stats;
globalThis.__navigationMatrixReplacedActions = currentActions;
currentActions.replaceWith(replacement);
}, runtime.selectors);
await waitForOwnedSurfaceWithinBudget(page, runtime, origin);
await expectOwnedWatchBar(page, runtime, origin.videoId, origin.counts);
await expect(page.locator('[data-fixture-matrix-action-replacement="true"]')).toHaveCount(1);
expect(
await page.evaluate(() => ({
oldContainerConnected: globalThis.__navigationMatrixReplacedActions.isConnected,
probe: globalThis.__navigationMatrixProbe.snapshot(),
})),
).toMatchObject({
oldContainerConnected: false,
probe: {
currentKind: "watch",
currentVideoId: origin.videoId,
documentIdentity: initialProbe.documentIdentity,
historyRenders: [],
navigateFinishes: 0,
navigateStarts: 0,
transitionPending: null,
},
});
expectCountRequestVideoIds(backend, [origin.videoId]);
await assertOwnedSurfaceContinuously(page, runtime, origin);
const replacementStats = await page.evaluate(() => {
globalThis.__navigationMatrixReplacementObserver.disconnect();
return globalThis.__navigationMatrixReplacementStats;
});
expect(replacementStats).toEqual({ addedWrappers: 1 });
await expectOwnedWatchBar(page, runtime, origin.videoId, origin.counts);
expectCountRequestVideoIds(backend, [origin.videoId]);
}
async function runWatchSameNodeRouteCompletionScenario({ backend, page, runtime, scenario }) {
const { destination, origin } = scenario;
const initialTopology = await page.evaluate(() => globalThis.__navigationMatrixSameNodeFixture.snapshot());
await expectOwnedWatchBar(page, runtime, origin.videoId, origin.counts);
expectCountRequestVideoIds(backend, [origin.videoId]);
const destinationCountGate = backend.defer("GET", "/votes");
await page.locator("#fixture-matrix-same-node-watch").click();
await expect(page).toHaveURL((url) => url.pathname === "/watch" && url.searchParams.get("v") === destination.videoId);
const destinationRequest = await destinationCountGate.seen;
expect(destinationRequest.query.videoId).toBe(destination.videoId);
const pendingDestination = currentWatchLocators(page, runtime, destination.videoId);
await expect(pendingDestination.controls).toHaveCount(1);
await expect(pendingDestination.controls.locator('[data-ryd-role="dislike"] #text')).toHaveText("");
await expect(pendingDestination.wrapper).toHaveCount(0);
await expect(page.locator(`${runtime.selectors.wrapper}[data-ryd-video-id="${origin.videoId}"]`)).toHaveCount(0);
destinationCountGate.release({ body: { ...destination.counts, rating: 4.5 } });
await waitForOwnedSurfaceWithinBudget(page, runtime, destination);
const destinationLocators = await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
await expect(destinationLocators.wrapper).toHaveAttribute("data-ryd-video-id", destination.videoId);
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
expect(await page.evaluate(() => globalThis.__navigationMatrixSameNodeFixture.snapshot())).toEqual({
buttonsReused: true,
controlsReused: true,
dislikeReused: true,
documentIdentity: initialTopology.documentIdentity,
likeReused: true,
rootVideoId: destination.videoId,
sidebarConnected: true,
timeline: ["navigate-start", "route-and-root-only", "navigate-finish"],
});
await assertOwnedSurfaceContinuously(page, runtime, destination);
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
}
async function runWatchRateBarCorruptionScenario({ backend, page, runtime, scenario }) {
const { origin } = scenario;
await expectOwnedWatchBar(page, runtime, origin.videoId, origin.counts);
expectCountRequestVideoIds(backend, [origin.videoId]);
for (const corruption of scenario.transition.corruptions) {
const corrupted = await page.evaluate(
({ corruption: corruptionKind, selectors }) => {
const wrapper = document.querySelector(selectors.wrapper);
const container = wrapper?.querySelector(selectors.container);
const fill = container?.querySelector(selectors.bar);
if (!wrapper || !container || !fill) {
throw new Error(`Cannot apply ${corruptionKind}; the current rate bar is incomplete.`);
}
globalThis.__navigationMatrixCorruptedRateBar = wrapper;
if (corruptionKind === "hidden-wrapper") {
wrapper.hidden = true;
} else if (corruptionKind === "collapsed-wrapper") {
wrapper.style.width = "0px";
wrapper.style.overflow = "hidden";
} else if (corruptionKind === "missing-fill") {
fill.remove();
} else if (corruptionKind === "stripped-wrapper-class") {
wrapper.classList.remove("ryd-tooltip");
} else {
throw new Error(`Unknown rate-bar corruption ${corruptionKind}.`);
}
const bounds = wrapper.getBoundingClientRect();
return {
connected: wrapper.isConnected,
height: bounds.height,
hidden: wrapper.hidden,
width: bounds.width,
};
},
{ corruption, selectors: runtime.selectors },
);
expect(corrupted.connected).toBe(true);
if (corruption === "hidden-wrapper") {
expect(corrupted.hidden).toBe(true);
}
if (corruption === "collapsed-wrapper") {
expect(corrupted.width).toBe(0);
}
await waitForOwnedSurfaceWithinBudget(page, runtime, origin);
await expectOwnedWatchBar(page, runtime, origin.videoId, origin.counts);
expect(await page.evaluate(() => globalThis.__navigationMatrixCorruptedRateBar.isConnected)).toBe(false);
expectCountRequestVideoIds(backend, [origin.videoId]);
await assertOwnedSurfaceContinuously(page, runtime, origin, 300);
}
}
async function runShortNextScenario({ backend, page, runtime, scenario }) {
const { destination, origin } = scenario;
const initialProbe = await readStandardProbe(page);
await expectOwnedShortControl(page, runtime, origin.videoId, origin.counts);
expectCountRequestVideoIds(backend, [origin.videoId]);
await page.locator("#short-next").click();
await waitForOwnedSurfaceWithinBudget(page, runtime, destination);
await expectOwnedShortControl(page, runtime, destination.videoId, destination.counts);
expect(await readStandardProbe(page)).toMatchObject({
currentKind: "shorts",
currentVideoId: destination.videoId,
documentIdentity: initialProbe.documentIdentity,
historyRenders: [],
navigateFinishes: 1,
navigateStarts: 0,
transitionPending: null,
});
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
await assertOwnedSurfaceContinuously(page, runtime, destination);
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
}
const SCENARIO_RUNNERS = {
"short-next-short-active-reel": runShortNextScenario,
"short-direct-watch-delayed": runCrossSurfaceScenario,
"watch-autoplay-watch-replace-no-finish": runWatchAutoplayScenario,
"watch-current-action-container-replace": runWatchActionContainerReplacementScenario,
"watch-current-rate-bar-connected-corruption": runWatchRateBarCorruptionScenario,
"watch-direct-short-delayed": runCrossSurfaceScenario,
"watch-history-back-forward-replace": runWatchHistoryScenario,
"watch-sidebar-watch-retain-prune": runWatchSidebarRetainPruneScenario,
"watch-sidebar-watch-legacy-segmented-duplicate-ids": runWatchSameRootHiddenFirstScenario,
"watch-sidebar-watch-same-root-hidden-first": runWatchSameRootHiddenFirstScenario,
"watch-sidebar-watch-same-root-offscreen-first": runWatchSameRootHiddenFirstScenario,
"watch-sidebar-watch-same-node-route-complete": runWatchSameNodeRouteCompletionScenario,
};
async function runNavigationMatrixScenario(options) {
const runner = SCENARIO_RUNNERS[options.scenario.id];
if (!runner) throw new Error(`No navigation matrix runner is registered for ${options.scenario.id}.`);
await runner(options);
}
module.exports = {
NAVIGATION_MATRIX,
USERSCRIPT_MATRIX_RUNTIME,
WATCH_SIDEBAR_MATRIX,
installNavigationMatrixFixture,
runNavigationMatrixScenario,
};