2026-09-01 23:41:28 +02:00
|
|
|
const { expect } = require("@playwright/test");
|
|
|
|
|
const { assertInvariantContinuously, waitForStableInvariant } = require("../../e2e/continuous-invariants");
|
2026-09-06 12:51:12 +02:00
|
|
|
const { assertExactSuccessfulVotesTraffic } = require("../../e2e/hermetic-api-contract");
|
2026-09-01 23:41:28 +02:00
|
|
|
const { VIDEO_A, VIDEO_B } = require("./harness");
|
|
|
|
|
|
2026-09-06 12:51:12 +02:00
|
|
|
const SINGLE_DESTINATION_DISLIKE_POSTCONDITION = "single-destination-dislike";
|
|
|
|
|
const NO_DESTINATION_DISLIKE_POSTCONDITION = "no-destination-dislike";
|
|
|
|
|
|
|
|
|
|
function expectedDislikeText(counts, delta = 0) {
|
|
|
|
|
if (delta === 0 && typeof counts.displayedDislikes === "string") return counts.displayedDislikes;
|
|
|
|
|
if (delta === 1 && typeof counts.displayedDislikesAfterIncrement === "string") {
|
|
|
|
|
return counts.displayedDislikesAfterIncrement;
|
|
|
|
|
}
|
|
|
|
|
return String(counts.dislikes + delta);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function expectedTooltipText({ dislikes, likes }) {
|
|
|
|
|
return `${likes.toLocaleString("en-US")} / ${dislikes.toLocaleString("en-US")}`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-01 23:41:28 +02:00
|
|
|
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 },
|
2026-09-06 12:51:12 +02:00
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
|
|
|
viewport: { height: 720, width: 1280 },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
coverage: {
|
|
|
|
|
destination: "shorts",
|
|
|
|
|
dom: [
|
|
|
|
|
"reuse-exact-renderer",
|
|
|
|
|
"replace-action-root",
|
|
|
|
|
"exact-href-identity",
|
|
|
|
|
"no-is-active",
|
|
|
|
|
"no-video-id",
|
|
|
|
|
"incomplete-rendered-native-inventory",
|
|
|
|
|
"non-rendered-native-action",
|
|
|
|
|
"persistent-data-null-action-root",
|
|
|
|
|
"stable-action-root-geometry",
|
|
|
|
|
],
|
|
|
|
|
origin: "shorts",
|
|
|
|
|
timing: ["navigate-start-without-finish", "data-null-past-watchdog", "inert-beyond-fallback-window"],
|
|
|
|
|
trigger: "next-control",
|
|
|
|
|
width: "wide-desktop",
|
|
|
|
|
},
|
|
|
|
|
destination: {
|
|
|
|
|
counts: { dislikes: 300, likes: 100 },
|
|
|
|
|
kind: "shorts",
|
|
|
|
|
shortIdentity: "exact-href-without-active-or-video-id",
|
|
|
|
|
videoId: VIDEO_B,
|
|
|
|
|
},
|
|
|
|
|
id: "short-next-short-persistent-data-null-nonrendered-native-stays-inert",
|
|
|
|
|
origin: { counts: { dislikes: 100, likes: 300 }, kind: "shorts", state: "neutral", videoId: VIDEO_A },
|
|
|
|
|
postcondition: NO_DESTINATION_DISLIKE_POSTCONDITION,
|
|
|
|
|
timing: { inertForMs: 1_700 },
|
|
|
|
|
transition: {
|
|
|
|
|
actionRoot: "replace-incomplete-rendered-native-persistent-data-null",
|
|
|
|
|
navigateFinish: "none",
|
|
|
|
|
renderer: "reuse-exact-node",
|
|
|
|
|
},
|
|
|
|
|
viewport: { height: 720, width: 1280 },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
coverage: {
|
|
|
|
|
destination: "shorts",
|
|
|
|
|
dom: [
|
|
|
|
|
"reuse-exact-renderer",
|
|
|
|
|
"replace-action-root",
|
|
|
|
|
"exact-href-identity",
|
|
|
|
|
"no-is-active",
|
|
|
|
|
"no-video-id",
|
|
|
|
|
"native-dislike-present",
|
|
|
|
|
"no-synthetic-mutation",
|
|
|
|
|
"persistent-data-null-action-root",
|
|
|
|
|
],
|
|
|
|
|
origin: "shorts",
|
|
|
|
|
timing: [
|
|
|
|
|
"navigate-start-without-finish",
|
|
|
|
|
"native-dislike-after-stability",
|
|
|
|
|
"native-dislike-without-synthetic-mutation",
|
|
|
|
|
],
|
|
|
|
|
trigger: "next-control",
|
|
|
|
|
width: "wide-desktop",
|
|
|
|
|
},
|
|
|
|
|
destination: {
|
|
|
|
|
counts: { dislikes: 300, likes: 100 },
|
|
|
|
|
kind: "shorts",
|
|
|
|
|
shortIdentity: "exact-href-without-active-or-video-id",
|
|
|
|
|
shortsDislikeControl: "native",
|
|
|
|
|
videoId: VIDEO_B,
|
|
|
|
|
},
|
|
|
|
|
id: "short-next-short-persistent-data-null-native-dislike",
|
|
|
|
|
origin: { counts: { dislikes: 100, likes: 300 }, kind: "shorts", state: "neutral", videoId: VIDEO_A },
|
|
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
|
|
|
timing: { maxFirstValidMs: 2_500, unsafeWindowMs: 520 },
|
|
|
|
|
transition: {
|
|
|
|
|
actionRoot: "replace-native-dislike-persistent-data-null",
|
|
|
|
|
navigateFinish: "none",
|
|
|
|
|
renderer: "reuse-exact-node",
|
|
|
|
|
},
|
|
|
|
|
viewport: { height: 720, width: 1280 },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
coverage: {
|
|
|
|
|
destination: "shorts",
|
|
|
|
|
dom: ["preloaded-sibling", "active-reel-switch"],
|
|
|
|
|
origin: "shorts",
|
|
|
|
|
timing: ["settled"],
|
|
|
|
|
trigger: "next-control",
|
|
|
|
|
width: "medium-desktop",
|
|
|
|
|
},
|
|
|
|
|
destination: { counts: { dislikes: 300, likes: 100 }, kind: "shorts", videoId: VIDEO_B },
|
|
|
|
|
id: "short-next-short-active-reel-medium",
|
|
|
|
|
origin: { counts: { dislikes: 100, likes: 300 }, kind: "shorts", state: "neutral", videoId: VIDEO_A },
|
|
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
|
|
|
viewport: { height: 720, width: 768 },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
coverage: {
|
|
|
|
|
destination: "shorts",
|
|
|
|
|
dom: ["preloaded-sibling", "active-reel-switch"],
|
|
|
|
|
origin: "shorts",
|
|
|
|
|
timing: ["settled"],
|
|
|
|
|
trigger: "next-control",
|
|
|
|
|
width: "compact",
|
|
|
|
|
},
|
|
|
|
|
destination: { counts: { dislikes: 300, likes: 100 }, kind: "shorts", videoId: VIDEO_B },
|
|
|
|
|
id: "short-next-short-active-reel-compact",
|
|
|
|
|
origin: { counts: { dislikes: 100, likes: 300 }, kind: "shorts", state: "neutral", videoId: VIDEO_A },
|
|
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
|
|
|
viewport: { height: 844, width: 390 },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
coverage: {
|
|
|
|
|
destination: "shorts",
|
|
|
|
|
dom: ["reuse-exact-renderer", "reuse-exact-action-root", "exact-href-identity", "no-is-active"],
|
|
|
|
|
origin: "shorts",
|
|
|
|
|
timing: ["navigate-start-without-finish"],
|
|
|
|
|
trigger: "next-control",
|
|
|
|
|
width: "wide-desktop",
|
|
|
|
|
},
|
|
|
|
|
destination: {
|
|
|
|
|
counts: { dislikes: 300, likes: 100 },
|
|
|
|
|
kind: "shorts",
|
|
|
|
|
shortIdentity: "exact-href-without-active-or-video-id",
|
|
|
|
|
videoId: VIDEO_B,
|
|
|
|
|
},
|
|
|
|
|
id: "short-next-short-reuse-renderer-start-no-finish",
|
|
|
|
|
origin: { counts: { dislikes: 100, likes: 300 }, kind: "shorts", state: "neutral", videoId: VIDEO_A },
|
|
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
|
|
|
transition: { actionRoot: "reuse-exact-node", navigateFinish: "none", renderer: "reuse-exact-node" },
|
|
|
|
|
viewport: { height: 720, width: 1280 },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
coverage: {
|
|
|
|
|
destination: "shorts",
|
|
|
|
|
dom: [
|
|
|
|
|
"reuse-exact-renderer",
|
|
|
|
|
"reuse-exact-action-root",
|
|
|
|
|
"exact-href-identity",
|
|
|
|
|
"unrelated-description-short-link",
|
|
|
|
|
"no-is-active",
|
|
|
|
|
"no-video-id",
|
|
|
|
|
],
|
|
|
|
|
origin: "shorts",
|
|
|
|
|
timing: ["navigate-start-without-finish"],
|
|
|
|
|
trigger: "next-control",
|
|
|
|
|
width: "wide-desktop",
|
|
|
|
|
},
|
|
|
|
|
destination: {
|
|
|
|
|
counts: { dislikes: 300, likes: 100 },
|
|
|
|
|
kind: "shorts",
|
|
|
|
|
shortIdentity: "exact-href-without-active-or-video-id",
|
|
|
|
|
unrelatedDescriptionShortVideoId: "FupY92jTfho",
|
|
|
|
|
videoId: VIDEO_B,
|
|
|
|
|
},
|
|
|
|
|
id: "short-next-short-exact-href-with-description-crosslink",
|
|
|
|
|
origin: { counts: { dislikes: 100, likes: 300 }, kind: "shorts", state: "neutral", videoId: VIDEO_A },
|
|
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
|
|
|
transition: { actionRoot: "reuse-exact-node", navigateFinish: "none", renderer: "reuse-exact-node" },
|
|
|
|
|
viewport: { height: 720, width: 1280 },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
coverage: {
|
|
|
|
|
destination: "shorts",
|
|
|
|
|
dom: [
|
|
|
|
|
"reuse-exact-renderer",
|
|
|
|
|
"reuse-exact-action-root",
|
|
|
|
|
"remove-synthetic-before-route",
|
|
|
|
|
"exact-href-identity",
|
|
|
|
|
"no-is-active",
|
|
|
|
|
],
|
|
|
|
|
origin: "shorts",
|
|
|
|
|
timing: ["repeated-navigate-start", "navigate-start-without-finish"],
|
|
|
|
|
trigger: "next-control",
|
|
|
|
|
width: "wide-desktop",
|
|
|
|
|
},
|
|
|
|
|
destination: {
|
|
|
|
|
counts: { dislikes: 300, likes: 100 },
|
|
|
|
|
kind: "shorts",
|
|
|
|
|
shortIdentity: "exact-href-without-active-or-video-id",
|
|
|
|
|
videoId: VIDEO_B,
|
|
|
|
|
},
|
|
|
|
|
id: "short-next-short-reuse-renderer-repeated-start-no-finish",
|
|
|
|
|
origin: { counts: { dislikes: 100, likes: 300 }, kind: "shorts", state: "neutral", videoId: VIDEO_A },
|
|
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
|
|
|
transition: {
|
|
|
|
|
actionRoot: "reuse-exact-node",
|
|
|
|
|
navigateFinish: "none",
|
|
|
|
|
navigateStarts: "before-and-after-route",
|
|
|
|
|
renderer: "reuse-exact-node",
|
|
|
|
|
syntheticDislike: "remove-before-route",
|
|
|
|
|
},
|
|
|
|
|
viewport: { height: 720, width: 1280 },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
coverage: {
|
|
|
|
|
destination: "shorts",
|
|
|
|
|
dom: [
|
|
|
|
|
"reuse-exact-renderer",
|
|
|
|
|
"replace-action-root",
|
|
|
|
|
"exact-href-identity",
|
|
|
|
|
"no-is-active",
|
|
|
|
|
"no-video-id",
|
|
|
|
|
"complete-native-inventory",
|
|
|
|
|
"persistent-data-null-action-root",
|
|
|
|
|
],
|
|
|
|
|
origin: "shorts",
|
|
|
|
|
timing: ["navigate-start-without-finish", "data-null-past-watchdog", "stable-native-inventory"],
|
|
|
|
|
trigger: "next-control",
|
|
|
|
|
width: "wide-desktop",
|
|
|
|
|
},
|
|
|
|
|
destination: {
|
|
|
|
|
counts: { dislikes: 300, likes: 100 },
|
|
|
|
|
kind: "shorts",
|
|
|
|
|
shortIdentity: "exact-href-without-active-or-video-id",
|
|
|
|
|
videoId: VIDEO_B,
|
|
|
|
|
},
|
|
|
|
|
id: "short-next-short-reuse-renderer-replace-action-root-exact-href-persistent-data-null",
|
|
|
|
|
origin: { counts: { dislikes: 100, likes: 300 }, kind: "shorts", state: "neutral", videoId: VIDEO_A },
|
|
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
|
|
|
timing: { maxFirstValidMs: 2_500, unsafeWindowMs: 520 },
|
|
|
|
|
transition: {
|
|
|
|
|
actionRoot: "replace-complete-native-persistent-data-null",
|
|
|
|
|
navigateFinish: "none",
|
|
|
|
|
renderer: "reuse-exact-node",
|
|
|
|
|
},
|
|
|
|
|
viewport: { height: 720, width: 1280 },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
coverage: {
|
|
|
|
|
destination: "shorts",
|
|
|
|
|
dom: [
|
|
|
|
|
"replace-shorts-root",
|
|
|
|
|
"destination-action-root-absent",
|
|
|
|
|
"staged-data-null-action-root",
|
|
|
|
|
"complete-native-inventory-before-hydration",
|
|
|
|
|
],
|
|
|
|
|
origin: "shorts",
|
|
|
|
|
timing: ["navigate-start-without-finish", "no-controls-over-500ms", "data-null-over-stability-window"],
|
|
|
|
|
trigger: "next-control",
|
|
|
|
|
width: "wide-desktop",
|
|
|
|
|
},
|
|
|
|
|
destination: { counts: { dislikes: 300, likes: 100 }, kind: "shorts", videoId: VIDEO_B },
|
|
|
|
|
id: "short-next-short-replace-root-start-no-finish-staged-hydration",
|
|
|
|
|
origin: { counts: { dislikes: 100, likes: 300 }, kind: "shorts", state: "neutral", videoId: VIDEO_A },
|
|
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
|
|
|
transition: {
|
|
|
|
|
actionRoot: "absent-then-empty-then-native-then-hydrated",
|
|
|
|
|
navigateFinish: "none",
|
|
|
|
|
navigateStarts: "before-route",
|
|
|
|
|
root: "replace",
|
|
|
|
|
},
|
|
|
|
|
viewport: { height: 720, width: 1280 },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
coverage: {
|
|
|
|
|
destination: "shorts",
|
|
|
|
|
dom: [
|
|
|
|
|
"replace-shorts-root",
|
|
|
|
|
"destination-action-root-absent",
|
|
|
|
|
"staged-data-null-action-root",
|
|
|
|
|
"complete-native-inventory-before-hydration",
|
|
|
|
|
],
|
|
|
|
|
origin: "shorts",
|
|
|
|
|
timing: [
|
|
|
|
|
"repeated-navigate-start",
|
|
|
|
|
"navigate-start-without-finish",
|
|
|
|
|
"no-controls-over-500ms",
|
|
|
|
|
"data-null-over-stability-window",
|
|
|
|
|
],
|
|
|
|
|
trigger: "next-control",
|
|
|
|
|
width: "wide-desktop",
|
|
|
|
|
},
|
|
|
|
|
destination: { counts: { dislikes: 300, likes: 100 }, kind: "shorts", videoId: VIDEO_B },
|
|
|
|
|
id: "short-next-short-replace-root-repeated-start-no-finish-staged-hydration",
|
|
|
|
|
origin: { counts: { dislikes: 100, likes: 300 }, kind: "shorts", state: "neutral", videoId: VIDEO_A },
|
|
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
|
|
|
transition: {
|
|
|
|
|
actionRoot: "absent-then-empty-then-native-then-hydrated",
|
|
|
|
|
navigateFinish: "none",
|
|
|
|
|
navigateStarts: "before-and-after-route",
|
|
|
|
|
root: "replace",
|
|
|
|
|
},
|
2026-09-01 23:41:28 +02:00
|
|
|
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,
|
|
|
|
|
},
|
2026-09-06 12:51:12 +02:00
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
2026-09-01 23:41:28 +02:00
|
|
|
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",
|
2026-09-06 12:51:12 +02:00
|
|
|
dom: [
|
|
|
|
|
"same-current-root",
|
|
|
|
|
"reuse-exact-control-nodes",
|
|
|
|
|
"no-useful-control-mutation",
|
|
|
|
|
"same-compact-dislike-text",
|
|
|
|
|
"different-exact-dislike-count",
|
|
|
|
|
"different-rate-bar-ratio",
|
|
|
|
|
],
|
2026-09-01 23:41:28 +02:00
|
|
|
origin: "watch",
|
|
|
|
|
timing: ["navigate-finish", "destination-count-gated"],
|
|
|
|
|
trigger: "sidebar-link",
|
|
|
|
|
width: "wide-desktop",
|
|
|
|
|
},
|
2026-09-06 12:51:12 +02:00
|
|
|
destination: {
|
|
|
|
|
counts: {
|
|
|
|
|
dislikes: 1_409,
|
|
|
|
|
displayedDislikes: "1.4K",
|
|
|
|
|
displayedDislikesAfterIncrement: "1.4K",
|
|
|
|
|
likes: 3_591,
|
|
|
|
|
},
|
|
|
|
|
kind: "watch",
|
|
|
|
|
videoId: VIDEO_B,
|
|
|
|
|
},
|
2026-09-01 23:41:28 +02:00
|
|
|
id: "watch-sidebar-watch-same-node-route-complete",
|
2026-09-06 12:51:12 +02:00
|
|
|
origin: {
|
|
|
|
|
counts: { dislikes: 1_401, displayedDislikes: "1.4K", likes: 8_599 },
|
|
|
|
|
kind: "watch",
|
|
|
|
|
state: "neutral",
|
|
|
|
|
videoId: VIDEO_A,
|
|
|
|
|
},
|
|
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
2026-09-01 23:41:28 +02:00
|
|
|
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",
|
2026-09-06 12:51:12 +02:00
|
|
|
"missing-rate-bar-video-owner",
|
|
|
|
|
"stale-rate-bar-video-owner",
|
2026-09-01 23:41:28 +02:00
|
|
|
"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 },
|
2026-09-06 12:51:12 +02:00
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
2026-09-01 23:41:28 +02:00
|
|
|
transition: {
|
2026-09-06 12:51:12 +02:00
|
|
|
corruptions: [
|
|
|
|
|
"hidden-wrapper",
|
|
|
|
|
"collapsed-wrapper",
|
|
|
|
|
"missing-fill",
|
|
|
|
|
"missing-video-owner",
|
|
|
|
|
"stale-video-owner",
|
|
|
|
|
"stripped-wrapper-class",
|
|
|
|
|
],
|
2026-09-01 23:41:28 +02:00
|
|
|
},
|
|
|
|
|
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 },
|
2026-09-06 12:51:12 +02:00
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
2026-09-01 23:41:28 +02:00
|
|
|
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 },
|
2026-09-06 12:51:12 +02:00
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
2026-09-01 23:41:28 +02:00
|
|
|
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 },
|
2026-09-06 12:51:12 +02:00
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
2026-09-01 23:41:28 +02:00
|
|
|
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 },
|
2026-09-06 12:51:12 +02:00
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
2026-09-01 23:41:28 +02:00
|
|
|
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 },
|
2026-09-06 12:51:12 +02:00
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
|
|
|
viewport: { height: 720, width: 1280 },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
coverage: {
|
|
|
|
|
destination: "watch",
|
|
|
|
|
dom: ["replace-page-and-controls-after-start"],
|
|
|
|
|
origin: "watch",
|
|
|
|
|
timing: ["navigate-start-without-finish"],
|
|
|
|
|
trigger: "navigate-start-only",
|
|
|
|
|
width: "wide-desktop",
|
|
|
|
|
},
|
|
|
|
|
destination: { counts: { dislikes: 300, likes: 100 }, kind: "watch", videoId: VIDEO_B },
|
|
|
|
|
id: "watch-navigate-start-watch-replace-no-finish",
|
|
|
|
|
origin: { counts: { dislikes: 100, likes: 300 }, kind: "watch", state: "neutral", videoId: VIDEO_A },
|
|
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
2026-09-01 23:41:28 +02:00
|
|
|
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 },
|
2026-09-06 12:51:12 +02:00
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
|
|
|
// 600ms fixture hydration + one 111ms discovery sample + three 111ms
|
|
|
|
|
// samples to exceed 250ms native stability, with scheduler allowance.
|
|
|
|
|
timing: { controlDelayMs: 600, maxFirstValidMs: 1_250 },
|
2026-09-01 23:41:28 +02:00
|
|
|
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 },
|
2026-09-06 12:51:12 +02:00
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
|
|
|
timing: { controlDelayMs: 600 },
|
2026-09-01 23:41:28 +02:00
|
|
|
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 },
|
2026-09-06 12:51:12 +02:00
|
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
2026-09-01 23:41:28 +02:00
|
|
|
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",
|
2026-09-06 12:51:12 +02:00
|
|
|
watchDislikeCount: "#text",
|
|
|
|
|
wrapperVideoAttribute: "data-ryd-video-id",
|
2026-09-01 23:41:28 +02:00
|
|
|
},
|
2026-09-06 12:51:12 +02:00
|
|
|
tooltipText: expectedTooltipText,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const EXTENSION_MATRIX_RUNTIME = {
|
|
|
|
|
clearsOutgoingWatchPresentationOnNavigateStart: true,
|
|
|
|
|
name: "extension",
|
|
|
|
|
selectors: {
|
|
|
|
|
bar: "#ryd-bar",
|
|
|
|
|
container: "#ryd-bar-container",
|
|
|
|
|
tooltip: "#ryd-dislike-tooltip",
|
|
|
|
|
wrapper: ".ryd-tooltip",
|
|
|
|
|
shortsDislike: "[data-ryd-synthetic-shorts-dislike]",
|
|
|
|
|
shortsVideoAttribute: "data-ryd-video-id",
|
|
|
|
|
watchDislikeCount: "#text",
|
|
|
|
|
wrapperVideoAttribute: "data-ryd-video-id",
|
2026-09-01 23:41:28 +02:00
|
|
|
},
|
2026-09-06 12:51:12 +02:00
|
|
|
tooltipText: expectedTooltipText,
|
2026-09-01 23:41:28 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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);
|
2026-09-06 12:51:12 +02:00
|
|
|
globalThis.__navigationFixture.setNativeLikeCount(destinationControls, matrixScenario.destination.videoId);
|
2026-09-01 23:41:28 +02:00
|
|
|
for (const role of ["like", "dislike"]) {
|
2026-09-06 12:51:12 +02:00
|
|
|
const control = destinationControls.querySelector(`[data-fixture-role="${role}"]`);
|
2026-09-01 23:41:28 +02:00
|
|
|
control.classList.remove("style-default-active");
|
|
|
|
|
control.classList.add("style-text");
|
|
|
|
|
control.querySelector("button")?.setAttribute("aria-pressed", "false");
|
|
|
|
|
}
|
2026-09-06 12:51:12 +02:00
|
|
|
destinationControls
|
|
|
|
|
.querySelectorAll(
|
|
|
|
|
'[data-fixture-role="dislike"] .ytSpecButtonShapeNextButtonTextContent, [data-fixture-role="dislike"] #text',
|
|
|
|
|
)
|
|
|
|
|
.forEach((element) => element.remove());
|
2026-09-01 23:41:28 +02:00
|
|
|
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"]');
|
2026-09-06 12:51:12 +02:00
|
|
|
const watchFlexy = watchPage?.querySelector("ytd-watch-flexy");
|
|
|
|
|
if (!watchFlexy) throw new Error("The destination Watch root is unavailable during hydration.");
|
|
|
|
|
watchFlexy.appendChild(transition.destinationTopRow);
|
2026-09-01 23:41:28 +02:00
|
|
|
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);
|
|
|
|
|
}
|
2026-09-06 12:51:12 +02:00
|
|
|
const legacyLike = legacySegmented.querySelector('[data-fixture-role="like"]');
|
|
|
|
|
const legacyDislike = legacySegmented.querySelector('[data-fixture-role="dislike"]');
|
2026-09-01 23:41:28 +02:00
|
|
|
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);
|
2026-09-06 12:51:12 +02:00
|
|
|
globalThis.__navigationFixture.setNativeLikeCount(destinationControls, matrixScenario.destination.videoId);
|
2026-09-01 23:41:28 +02:00
|
|
|
for (const role of ["like", "dislike"]) {
|
2026-09-06 12:51:12 +02:00
|
|
|
const control = destinationControls.querySelector(`[data-fixture-role="${role}"]`);
|
2026-09-01 23:41:28 +02:00
|
|
|
control.classList.remove("style-default-active");
|
|
|
|
|
control.classList.add("style-text");
|
|
|
|
|
control.querySelector("button")?.setAttribute("aria-pressed", "false");
|
|
|
|
|
}
|
2026-09-06 12:51:12 +02:00
|
|
|
destinationControls
|
|
|
|
|
.querySelectorAll(
|
|
|
|
|
'[data-fixture-role="dislike"] .ytSpecButtonShapeNextButtonTextContent, [data-fixture-role="dislike"] #text',
|
|
|
|
|
)
|
|
|
|
|
.forEach((element) => element.remove());
|
2026-09-01 23:41:28 +02:00
|
|
|
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]");
|
2026-09-06 12:51:12 +02:00
|
|
|
const like = controls?.querySelector('[data-fixture-role="like"]');
|
|
|
|
|
const dislike = controls?.querySelector('[data-fixture-role="dislike"]');
|
2026-09-01 23:41:28 +02:00
|
|
|
if (!fixturePage || !watchPage || !watchFlexy || !topRow || !buttons || !controls || !like || !dislike) {
|
|
|
|
|
throw new Error("The same-node matrix requires a complete watch fixture.");
|
|
|
|
|
}
|
2026-09-06 12:51:12 +02:00
|
|
|
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 = [];
|
|
|
|
|
let countAfterNavigateStart = null;
|
|
|
|
|
let countAfterRouteAndRoot = null;
|
|
|
|
|
destinationLink.addEventListener("click", (event) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
document.dispatchEvent(new Event("yt-navigate-start", { bubbles: true }));
|
|
|
|
|
countAfterNavigateStart = dislike.querySelector("#text")?.textContent ?? null;
|
|
|
|
|
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);
|
|
|
|
|
countAfterRouteAndRoot = dislike.querySelector("#text")?.textContent ?? null;
|
|
|
|
|
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"),
|
|
|
|
|
countAfterNavigateStart,
|
|
|
|
|
countAfterRouteAndRoot,
|
|
|
|
|
controlsReused: controls === watchFlexy.querySelector("[data-fixture-control-video-id]"),
|
|
|
|
|
dislikeReused: dislike === watchFlexy.querySelector('[data-fixture-role="dislike"]'),
|
|
|
|
|
documentIdentity,
|
|
|
|
|
likeReused: like === watchFlexy.querySelector('[data-fixture-role="like"]'),
|
|
|
|
|
rootVideoId: watchFlexy.getAttribute("video-id"),
|
|
|
|
|
sidebarConnected: sidebar.isConnected,
|
|
|
|
|
timeline: [...timeline],
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
{ once: true },
|
|
|
|
|
);
|
|
|
|
|
}, scenario);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function installSameRendererShortStartOnlyFixture(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 shortsPage = fixturePage?.querySelector('[data-fixture-page-kind="shorts"]');
|
|
|
|
|
const renderer = shortsPage?.querySelector(
|
|
|
|
|
`ytd-reel-video-renderer[video-id="${matrixScenario.origin.videoId}"][is-active]`,
|
|
|
|
|
);
|
|
|
|
|
const actionBar = renderer?.querySelector("reel-action-bar-view-model");
|
|
|
|
|
const like = actionBar?.querySelector('[data-fixture-role="like"]');
|
|
|
|
|
const next = document.getElementById("short-next");
|
|
|
|
|
if (!fixturePage || !shortsPage || !renderer || !actionBar || !like || !next) {
|
|
|
|
|
throw new Error("The same-renderer Shorts matrix requires complete origin controls and a Next link.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const sibling of shortsPage.querySelectorAll("ytd-reel-video-renderer")) {
|
|
|
|
|
if (sibling !== renderer) sibling.remove();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const documentIdentity = `matrix-${Date.now()}-${Math.random()}`;
|
|
|
|
|
const originRenderer = renderer;
|
|
|
|
|
const originActionBar = actionBar;
|
|
|
|
|
const originLike = like;
|
|
|
|
|
const repeatedStart = matrixScenario.transition.navigateStarts === "before-and-after-route";
|
|
|
|
|
const timeline = [];
|
|
|
|
|
let immediatelyAfterRoute = null;
|
|
|
|
|
let originSyntheticDislike = null;
|
|
|
|
|
let originSyntheticNativeButton = null;
|
|
|
|
|
|
|
|
|
|
next.addEventListener(
|
|
|
|
|
"click",
|
|
|
|
|
(event) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
|
|
|
|
originSyntheticDislike = actionBar.querySelector("[data-ryd-synthetic-shorts-dislike]");
|
|
|
|
|
originSyntheticNativeButton = originSyntheticDislike?.querySelector("button") ?? null;
|
|
|
|
|
if (!originSyntheticDislike || !originSyntheticNativeButton) {
|
|
|
|
|
throw new Error("The same-renderer Shorts matrix requires an initialized synthetic Dislike.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.dispatchEvent(new Event("yt-navigate-start", { bubbles: true }));
|
|
|
|
|
timeline.push(repeatedStart ? "navigate-start-a" : "navigate-start");
|
|
|
|
|
|
|
|
|
|
if (repeatedStart) {
|
|
|
|
|
originSyntheticDislike.remove();
|
|
|
|
|
timeline.push("synthetic-removed-before-route");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
history.pushState({}, "", `/shorts/${matrixScenario.destination.videoId}?rydNavigationFixture=1`);
|
|
|
|
|
shortsPage.setAttribute("data-fixture-video-id", matrixScenario.destination.videoId);
|
|
|
|
|
renderer.removeAttribute("video-id");
|
|
|
|
|
renderer.removeAttribute("is-active");
|
|
|
|
|
renderer.hidden = false;
|
|
|
|
|
let identityLink = renderer.querySelector('a[href*="/shorts/"]');
|
|
|
|
|
if (repeatedStart) {
|
|
|
|
|
const destinationIdentityLink = identityLink?.cloneNode(true);
|
|
|
|
|
if (!destinationIdentityLink) {
|
|
|
|
|
throw new Error("The repeated-start Shorts matrix requires a canonical identity link.");
|
|
|
|
|
}
|
|
|
|
|
destinationIdentityLink.setAttribute("href", `/shorts/${matrixScenario.destination.videoId}`);
|
|
|
|
|
identityLink.replaceWith(destinationIdentityLink);
|
|
|
|
|
identityLink = destinationIdentityLink;
|
|
|
|
|
} else {
|
|
|
|
|
identityLink?.setAttribute("href", `/shorts/${matrixScenario.destination.videoId}`);
|
|
|
|
|
}
|
|
|
|
|
if (matrixScenario.destination.unrelatedDescriptionShortVideoId) {
|
|
|
|
|
const description = document.createElement("yt-attributed-string");
|
|
|
|
|
description.id = "description-text";
|
|
|
|
|
description.setAttribute("data-fixture-short-description", "true");
|
|
|
|
|
const descriptionLink = document.createElement("a");
|
|
|
|
|
descriptionLink.className =
|
|
|
|
|
"yt-core-attributed-string__link yt-core-attributed-string__link--call-to-action-color";
|
|
|
|
|
descriptionLink.href = `/shorts/${matrixScenario.destination.unrelatedDescriptionShortVideoId}`;
|
|
|
|
|
descriptionLink.textContent = "This knot is very useful";
|
|
|
|
|
description.append(descriptionLink);
|
|
|
|
|
actionBar.before(description);
|
|
|
|
|
}
|
|
|
|
|
globalThis.__navigationFixture.setNativeLikeCount(actionBar, matrixScenario.destination.videoId);
|
|
|
|
|
timeline.push("route-and-exact-href-only");
|
|
|
|
|
|
|
|
|
|
if (repeatedStart) {
|
|
|
|
|
document.dispatchEvent(new Event("yt-navigate-start", { bubbles: true }));
|
|
|
|
|
timeline.push("navigate-start-b");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const currentSyntheticDislike = actionBar.querySelector("[data-ryd-synthetic-shorts-dislike]");
|
|
|
|
|
immediatelyAfterRoute = {
|
|
|
|
|
actionBarReused: actionBar === originActionBar,
|
|
|
|
|
href: identityLink?.getAttribute("href") ?? null,
|
|
|
|
|
isActive: renderer.hasAttribute("is-active"),
|
|
|
|
|
likeReused: like === originLike,
|
|
|
|
|
nativeDislikeReused:
|
|
|
|
|
actionBar.querySelector("[data-ryd-synthetic-shorts-dislike] button") === originSyntheticNativeButton,
|
|
|
|
|
rendererReused: renderer === originRenderer,
|
|
|
|
|
rendererVideoId: renderer.getAttribute("video-id"),
|
|
|
|
|
shortHrefs: Array.from(renderer.querySelectorAll('a[href*="/shorts/"]')).map(
|
|
|
|
|
(link) => new URL(link.getAttribute("href"), location.origin).pathname,
|
|
|
|
|
),
|
|
|
|
|
syntheticDisabled: currentSyntheticDislike?.querySelector("button")?.disabled ?? null,
|
|
|
|
|
syntheticCount: actionBar.querySelectorAll("[data-ryd-synthetic-shorts-dislike]").length,
|
|
|
|
|
syntheticReused: currentSyntheticDislike === originSyntheticDislike,
|
|
|
|
|
syntheticText: currentSyntheticDislike?.querySelector("#text, [role='text']")?.textContent ?? null,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
globalThis.__navigationMatrixSameRendererShortFixture = {
|
|
|
|
|
snapshot() {
|
|
|
|
|
const identityLink = renderer.querySelector('a[href*="/shorts/"]');
|
|
|
|
|
return {
|
|
|
|
|
actionBarReused: actionBar === originActionBar,
|
|
|
|
|
documentIdentity,
|
|
|
|
|
href: identityLink?.getAttribute("href") ?? null,
|
|
|
|
|
immediatelyAfterRoute,
|
|
|
|
|
isActive: renderer.hasAttribute("is-active"),
|
|
|
|
|
likeReused: like === originLike,
|
|
|
|
|
nativeDislikeReused:
|
|
|
|
|
originSyntheticNativeButton !== null &&
|
|
|
|
|
actionBar.querySelector("[data-ryd-synthetic-shorts-dislike] button") === originSyntheticNativeButton,
|
|
|
|
|
navigateFinishes: 0,
|
|
|
|
|
rendererReused: renderer === originRenderer,
|
|
|
|
|
rendererVideoId: renderer.getAttribute("video-id"),
|
|
|
|
|
shortHrefs: Array.from(renderer.querySelectorAll('a[href*="/shorts/"]')).map(
|
|
|
|
|
(link) => new URL(link.getAttribute("href"), location.origin).pathname,
|
|
|
|
|
),
|
|
|
|
|
syntheticCount: actionBar.querySelectorAll("[data-ryd-synthetic-shorts-dislike]").length,
|
|
|
|
|
syntheticReused:
|
|
|
|
|
originSyntheticDislike !== null &&
|
|
|
|
|
actionBar.querySelector("[data-ryd-synthetic-shorts-dislike]") === originSyntheticDislike,
|
|
|
|
|
timeline: [...timeline],
|
|
|
|
|
visibleRendererCount: Array.from(shortsPage.querySelectorAll("ytd-reel-video-renderer")).filter(
|
|
|
|
|
(candidate) => {
|
|
|
|
|
const bounds = candidate.getBoundingClientRect();
|
|
|
|
|
return !candidate.hidden && bounds.width > 0 && bounds.height > 0;
|
|
|
|
|
},
|
|
|
|
|
).length,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
{ once: true },
|
|
|
|
|
);
|
|
|
|
|
}, scenario);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function installPersistentDataNullShortFixture(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 shortsPage = fixturePage?.querySelector('[data-fixture-page-kind="shorts"]');
|
|
|
|
|
const renderer = shortsPage?.querySelector(
|
|
|
|
|
`ytd-reel-video-renderer[video-id="${matrixScenario.origin.videoId}"][is-active]`,
|
|
|
|
|
);
|
|
|
|
|
const originActionBar = renderer?.querySelector("reel-action-bar-view-model");
|
|
|
|
|
const next = document.getElementById("short-next");
|
|
|
|
|
if (!fixturePage || !shortsPage || !renderer || !originActionBar || !next) {
|
|
|
|
|
throw new Error("The persistent data-null Shorts matrix requires complete origin controls and a Next link.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const sibling of shortsPage.querySelectorAll("ytd-reel-video-renderer")) {
|
|
|
|
|
if (sibling !== renderer) sibling.remove();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const documentIdentity = `matrix-${Date.now()}-${Math.random()}`;
|
|
|
|
|
const timeline = [];
|
|
|
|
|
let actionBarDataReadCount = 0;
|
|
|
|
|
let actionBarDataWriteCount = 0;
|
|
|
|
|
let destinationActionBar = null;
|
|
|
|
|
let destinationNativeChildren = [];
|
|
|
|
|
let originSyntheticDislike = null;
|
|
|
|
|
|
|
|
|
|
function visible(element) {
|
|
|
|
|
if (!element?.isConnected || element.closest("[hidden], [aria-hidden='true'], [inert]")) return false;
|
|
|
|
|
const bounds = element.getBoundingClientRect();
|
|
|
|
|
const style = getComputedStyle(element);
|
|
|
|
|
return (
|
|
|
|
|
style.display !== "none" &&
|
|
|
|
|
!["hidden", "collapse"].includes(style.visibility) &&
|
|
|
|
|
Number.parseFloat(style.opacity || "1") > 0.01 &&
|
|
|
|
|
bounds.width > 0 &&
|
|
|
|
|
bounds.height > 0 &&
|
|
|
|
|
bounds.bottom > 0 &&
|
|
|
|
|
bounds.right > 0 &&
|
|
|
|
|
bounds.top < innerHeight &&
|
|
|
|
|
bounds.left < innerWidth
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function activationTarget(host) {
|
|
|
|
|
return (
|
|
|
|
|
host?.querySelector("button, a[href], a[role='button'][tabindex='0'], tp-yt-paper-button#button") ?? null
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hitTested(element) {
|
|
|
|
|
if (!visible(element)) return false;
|
|
|
|
|
const bounds = element.getBoundingClientRect();
|
|
|
|
|
const x = Math.min(innerWidth - 1, Math.max(0, bounds.left + bounds.width / 2));
|
|
|
|
|
const y = Math.min(innerHeight - 1, Math.max(0, bounds.top + bounds.height / 2));
|
|
|
|
|
return document.elementsFromPoint(x, y).some((hit) => hit === element || element.contains(hit));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function actionName(host) {
|
|
|
|
|
if (host.matches("[data-ryd-synthetic-shorts-dislike]")) return "dislike";
|
|
|
|
|
if (host.getAttribute("data-fixture-role") === "like") return "like";
|
|
|
|
|
const fixtureControl = host.getAttribute("data-fixture-control");
|
|
|
|
|
return fixtureControl === "sound" ? "pivot" : fixtureControl;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function navigateToPersistentDataNullSurface() {
|
|
|
|
|
originSyntheticDislike = originActionBar.querySelector("[data-ryd-synthetic-shorts-dislike]");
|
|
|
|
|
const nativeChildTemplates = Array.from(originActionBar.children)
|
|
|
|
|
.filter((child) => !child.matches("[data-ryd-synthetic-shorts-dislike]"))
|
|
|
|
|
.map((child) => child.cloneNode(true));
|
|
|
|
|
if (!originSyntheticDislike || nativeChildTemplates.length !== 5) {
|
|
|
|
|
throw new Error("The persistent data-null Shorts matrix requires one synthetic and five native controls.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.dispatchEvent(new Event("yt-navigate-start", { bubbles: true }));
|
|
|
|
|
timeline.push("navigate-start");
|
|
|
|
|
history.pushState({}, "", `/shorts/${matrixScenario.destination.videoId}?rydNavigationFixture=1`);
|
|
|
|
|
shortsPage.setAttribute("data-fixture-video-id", matrixScenario.destination.videoId);
|
|
|
|
|
renderer.removeAttribute("video-id");
|
|
|
|
|
renderer.removeAttribute("is-active");
|
|
|
|
|
renderer.hidden = false;
|
|
|
|
|
const identityLink = renderer.querySelector('a[href*="/shorts/"]');
|
|
|
|
|
if (!identityLink) {
|
|
|
|
|
throw new Error("The persistent data-null Shorts matrix requires a canonical identity link.");
|
|
|
|
|
}
|
|
|
|
|
identityLink.setAttribute("href", `/shorts/${matrixScenario.destination.videoId}`);
|
|
|
|
|
|
|
|
|
|
destinationActionBar = document.createElement("reel-action-bar-view-model");
|
|
|
|
|
destinationActionBar.setAttribute("data-fixture-role", "buttons");
|
|
|
|
|
destinationActionBar.setAttribute("data-fixture-painted", "true");
|
|
|
|
|
destinationActionBar.setAttribute("data-fixture-persistent-data-null", matrixScenario.destination.videoId);
|
|
|
|
|
Object.defineProperty(destinationActionBar, "data", {
|
|
|
|
|
configurable: true,
|
|
|
|
|
get() {
|
|
|
|
|
actionBarDataReadCount += 1;
|
|
|
|
|
return null;
|
|
|
|
|
},
|
|
|
|
|
set() {
|
|
|
|
|
actionBarDataWriteCount += 1;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
destinationActionBar.append(...nativeChildTemplates);
|
|
|
|
|
globalThis.__navigationFixture.setNativeLikeCount(destinationActionBar, matrixScenario.destination.videoId);
|
|
|
|
|
const destinationLike = destinationActionBar.querySelector('[data-fixture-role="like"]');
|
|
|
|
|
destinationLike?.classList.remove("style-default-active");
|
|
|
|
|
destinationLike?.classList.add("style-text");
|
|
|
|
|
destinationLike?.querySelector("button")?.setAttribute("aria-pressed", "false");
|
|
|
|
|
destinationNativeChildren = Array.from(destinationActionBar.children);
|
|
|
|
|
originActionBar.replaceWith(destinationActionBar);
|
|
|
|
|
timeline.push("route-and-fresh-five-native-persistent-data-null-action-root");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
next.addEventListener(
|
|
|
|
|
"click",
|
|
|
|
|
(event) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopImmediatePropagation();
|
|
|
|
|
navigateToPersistentDataNullSurface();
|
|
|
|
|
},
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
globalThis.__navigationMatrixPersistentDataNullShortFixture = {
|
|
|
|
|
snapshot() {
|
|
|
|
|
const actionHosts = Array.from(destinationActionBar?.children ?? []);
|
|
|
|
|
const nativeChildren = actionHosts.filter((child) => !child.matches("[data-ryd-synthetic-shorts-dislike]"));
|
|
|
|
|
const activationTargets = actionHosts.map(activationTarget);
|
|
|
|
|
const visibleRenderers = Array.from(shortsPage.querySelectorAll("ytd-reel-video-renderer")).filter(visible);
|
|
|
|
|
const exactHrefRenderers = visibleRenderers.filter((candidate) =>
|
|
|
|
|
Array.from(candidate.querySelectorAll('a[href*="/shorts/"]')).some((link) => {
|
|
|
|
|
try {
|
|
|
|
|
return (
|
|
|
|
|
new URL(link.getAttribute("href"), location.origin).pathname ===
|
|
|
|
|
`/shorts/${matrixScenario.destination.videoId}`
|
|
|
|
|
);
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
const syntheticDislikes = actionHosts.filter((host) => host.matches("[data-ryd-synthetic-shorts-dislike]"));
|
|
|
|
|
return {
|
|
|
|
|
actionBarConnected: destinationActionBar?.isConnected ?? false,
|
|
|
|
|
actionBarDataReadCount,
|
|
|
|
|
actionBarDataReady: Boolean(destinationActionBar?.data),
|
|
|
|
|
actionBarDataWriteCount,
|
|
|
|
|
actionBarReplaced: destinationActionBar !== null && destinationActionBar !== originActionBar,
|
|
|
|
|
actionNames: actionHosts.map(actionName),
|
|
|
|
|
currentActionButtonCount: activationTargets.filter(Boolean).length,
|
|
|
|
|
currentNativeChildCount: nativeChildren.length,
|
|
|
|
|
currentSyntheticCount: syntheticDislikes.length,
|
|
|
|
|
currentSyntheticVideoIds: syntheticDislikes.map((control) => control.getAttribute("data-ryd-video-id")),
|
|
|
|
|
documentIdentity,
|
|
|
|
|
exactHrefRendererCount: exactHrefRenderers.length,
|
|
|
|
|
hitTestedActionButtonCount: activationTargets.filter(hitTested).length,
|
|
|
|
|
href: renderer.querySelector('a[href*="/shorts/"]')?.getAttribute("href") ?? null,
|
|
|
|
|
isActive: renderer.hasAttribute("is-active"),
|
|
|
|
|
nativeChildrenStable:
|
|
|
|
|
destinationNativeChildren.length === 5 &&
|
|
|
|
|
nativeChildren.every((child, index) => child === destinationNativeChildren[index]),
|
|
|
|
|
nativeDislikeCount:
|
|
|
|
|
destinationActionBar?.querySelectorAll("dislike-button-view-model, #dislike-button").length ?? 0,
|
|
|
|
|
navigateFinishes: 0,
|
|
|
|
|
navigateStarts: timeline.includes("navigate-start") ? 1 : 0,
|
|
|
|
|
originActionBarConnected: originActionBar.isConnected,
|
|
|
|
|
originSyntheticConnected: originSyntheticDislike?.isConnected ?? false,
|
|
|
|
|
rendererReused: renderer.isConnected,
|
|
|
|
|
rendererVideoId: renderer.getAttribute("video-id"),
|
|
|
|
|
syntheticEnabled: syntheticDislikes[0]
|
|
|
|
|
? !syntheticDislikes[0].querySelector("button")?.disabled &&
|
|
|
|
|
syntheticDislikes[0].querySelector("button")?.getAttribute("aria-disabled") !== "true"
|
|
|
|
|
: false,
|
|
|
|
|
timeline: [...timeline],
|
|
|
|
|
visibleActionButtonCount: activationTargets.filter(visible).length,
|
|
|
|
|
visibleRendererCount: visibleRenderers.length,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
{ once: true },
|
|
|
|
|
);
|
|
|
|
|
}, scenario);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function installPersistentDataNullVariantShortFixture(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 shortsPage = fixturePage?.querySelector('[data-fixture-page-kind="shorts"]');
|
|
|
|
|
const renderer = shortsPage?.querySelector(
|
|
|
|
|
`ytd-reel-video-renderer[video-id="${matrixScenario.origin.videoId}"][is-active]`,
|
|
|
|
|
);
|
|
|
|
|
const originActionBar = renderer?.querySelector("reel-action-bar-view-model");
|
|
|
|
|
const next = document.getElementById("short-next");
|
|
|
|
|
if (!fixturePage || !shortsPage || !renderer || !originActionBar || !next) {
|
|
|
|
|
throw new Error("The persistent data-null Shorts variant requires complete origin controls and a Next link.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const sibling of shortsPage.querySelectorAll("ytd-reel-video-renderer")) {
|
|
|
|
|
if (sibling !== renderer) sibling.remove();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const addsNativeDislike = matrixScenario.destination.shortsDislikeControl === "native";
|
|
|
|
|
const documentIdentity = `matrix-${Date.now()}-${Math.random()}`;
|
|
|
|
|
const timeline = [];
|
|
|
|
|
let actionBarDataReadCount = 0;
|
|
|
|
|
let actionBarDataWriteCount = 0;
|
|
|
|
|
let blockedActivationTarget = null;
|
|
|
|
|
let destinationActionBar = null;
|
|
|
|
|
let destinationNativeChildren = [];
|
|
|
|
|
let originSyntheticDislike = null;
|
|
|
|
|
|
|
|
|
|
function visible(element) {
|
|
|
|
|
if (!element?.isConnected || element.closest("[hidden], [aria-hidden='true'], [inert]")) return false;
|
|
|
|
|
const bounds = element.getBoundingClientRect();
|
|
|
|
|
const style = getComputedStyle(element);
|
|
|
|
|
return (
|
|
|
|
|
style.display !== "none" &&
|
|
|
|
|
!["hidden", "collapse"].includes(style.visibility) &&
|
|
|
|
|
Number.parseFloat(style.opacity || "1") > 0.01 &&
|
|
|
|
|
bounds.width > 0 &&
|
|
|
|
|
bounds.height > 0 &&
|
|
|
|
|
bounds.bottom > 0 &&
|
|
|
|
|
bounds.right > 0 &&
|
|
|
|
|
bounds.top < innerHeight &&
|
|
|
|
|
bounds.left < innerWidth
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function activationTarget(host) {
|
|
|
|
|
return (
|
|
|
|
|
host?.querySelector("button, a[href], a[role='button'][tabindex='0'], tp-yt-paper-button#button") ?? null
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hitTested(element) {
|
|
|
|
|
if (!visible(element)) return false;
|
|
|
|
|
const bounds = element.getBoundingClientRect();
|
|
|
|
|
const x = Math.min(innerWidth - 1, Math.max(0, bounds.left + bounds.width / 2));
|
|
|
|
|
const y = Math.min(innerHeight - 1, Math.max(0, bounds.top + bounds.height / 2));
|
|
|
|
|
return document.elementsFromPoint(x, y).some((hit) => hit === element || element.contains(hit));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function actionName(host) {
|
|
|
|
|
if (host.getAttribute("data-fixture-role") === "like") return "like";
|
|
|
|
|
if (host.getAttribute("data-fixture-role") === "dislike") return "dislike";
|
|
|
|
|
const fixtureControl = host.getAttribute("data-fixture-control");
|
|
|
|
|
return fixtureControl === "sound" ? "pivot" : fixtureControl;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bounds(element) {
|
|
|
|
|
if (!element?.isConnected) return null;
|
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
|
return {
|
|
|
|
|
bottom: rect.bottom,
|
|
|
|
|
height: rect.height,
|
|
|
|
|
left: rect.left,
|
|
|
|
|
right: rect.right,
|
|
|
|
|
top: rect.top,
|
|
|
|
|
width: rect.width,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createNativeDislike() {
|
|
|
|
|
const label = originSyntheticDislike?.querySelector("label")?.cloneNode(true);
|
|
|
|
|
if (!label)
|
|
|
|
|
throw new Error("The native-Dislike variant requires the initialized synthetic control template.");
|
|
|
|
|
const nativeDislike = document.createElement("dislike-button-view-model");
|
|
|
|
|
nativeDislike.className =
|
|
|
|
|
"ytDislikeButtonViewModelHost ytwReelActionBarViewModelHostDesktopActionButton style-text";
|
|
|
|
|
nativeDislike.setAttribute("data-fixture-role", "dislike");
|
|
|
|
|
nativeDislike.append(label);
|
|
|
|
|
const button = nativeDislike.querySelector("button");
|
|
|
|
|
if (!button) throw new Error("The native-Dislike variant requires an activation target.");
|
|
|
|
|
button.disabled = false;
|
|
|
|
|
button.setAttribute("aria-disabled", "false");
|
|
|
|
|
button.setAttribute("aria-pressed", "false");
|
|
|
|
|
const count = nativeDislike.querySelector("#text, [role='text']");
|
|
|
|
|
if (count) count.textContent = "";
|
|
|
|
|
return nativeDislike;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function navigateToVariantSurface() {
|
|
|
|
|
originSyntheticDislike = originActionBar.querySelector("[data-ryd-synthetic-shorts-dislike]");
|
|
|
|
|
const nativeChildTemplates = Array.from(originActionBar.children)
|
|
|
|
|
.filter((child) => !child.matches("[data-ryd-synthetic-shorts-dislike]"))
|
|
|
|
|
.map((child) => child.cloneNode(true));
|
|
|
|
|
if (!originSyntheticDislike || nativeChildTemplates.length !== 5) {
|
|
|
|
|
throw new Error("The persistent data-null Shorts variant requires one synthetic and five native controls.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.dispatchEvent(new Event("yt-navigate-start", { bubbles: true }));
|
|
|
|
|
timeline.push("navigate-start");
|
|
|
|
|
history.pushState({}, "", `/shorts/${matrixScenario.destination.videoId}?rydNavigationFixture=1`);
|
|
|
|
|
shortsPage.setAttribute("data-fixture-video-id", matrixScenario.destination.videoId);
|
|
|
|
|
renderer.removeAttribute("video-id");
|
|
|
|
|
renderer.removeAttribute("is-active");
|
|
|
|
|
renderer.hidden = false;
|
|
|
|
|
const identityLink = renderer.querySelector('a[href*="/shorts/"]');
|
|
|
|
|
if (!identityLink) {
|
|
|
|
|
throw new Error("The persistent data-null Shorts variant requires a canonical identity link.");
|
|
|
|
|
}
|
|
|
|
|
identityLink.setAttribute("href", `/shorts/${matrixScenario.destination.videoId}`);
|
|
|
|
|
|
|
|
|
|
destinationActionBar = document.createElement("reel-action-bar-view-model");
|
|
|
|
|
destinationActionBar.setAttribute("data-fixture-role", "buttons");
|
|
|
|
|
destinationActionBar.setAttribute("data-fixture-painted", "true");
|
|
|
|
|
destinationActionBar.setAttribute("data-fixture-persistent-data-null", matrixScenario.destination.videoId);
|
|
|
|
|
Object.defineProperty(destinationActionBar, "data", {
|
|
|
|
|
configurable: true,
|
|
|
|
|
get() {
|
|
|
|
|
actionBarDataReadCount += 1;
|
|
|
|
|
return null;
|
|
|
|
|
},
|
|
|
|
|
set() {
|
|
|
|
|
actionBarDataWriteCount += 1;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
destinationActionBar.append(...nativeChildTemplates);
|
|
|
|
|
globalThis.__navigationFixture.setNativeLikeCount(destinationActionBar, matrixScenario.destination.videoId);
|
|
|
|
|
const destinationLike = destinationActionBar.querySelector('[data-fixture-role="like"]');
|
|
|
|
|
destinationLike?.classList.remove("style-default-active");
|
|
|
|
|
destinationLike?.classList.add("style-text");
|
|
|
|
|
destinationLike?.querySelector("button")?.setAttribute("aria-pressed", "false");
|
|
|
|
|
|
|
|
|
|
if (addsNativeDislike) {
|
|
|
|
|
destinationLike?.insertAdjacentElement("afterend", createNativeDislike());
|
|
|
|
|
timeline.push("append-native-dislike");
|
|
|
|
|
} else {
|
|
|
|
|
blockedActivationTarget = activationTarget(destinationActionBar.lastElementChild);
|
|
|
|
|
if (!blockedActivationTarget) {
|
|
|
|
|
throw new Error("The inert persistent data-null variant requires a native activation target to hide.");
|
|
|
|
|
}
|
|
|
|
|
blockedActivationTarget.style.display = "none";
|
|
|
|
|
blockedActivationTarget.setAttribute("data-fixture-non-rendered", "true");
|
|
|
|
|
timeline.push("hide-one-native-activation-target");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
destinationNativeChildren = Array.from(destinationActionBar.children);
|
|
|
|
|
originActionBar.replaceWith(destinationActionBar);
|
|
|
|
|
timeline.push("route-and-fresh-persistent-data-null-action-root");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
next.addEventListener(
|
|
|
|
|
"click",
|
|
|
|
|
(event) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopImmediatePropagation();
|
|
|
|
|
navigateToVariantSurface();
|
|
|
|
|
},
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
globalThis.__navigationMatrixPersistentDataNullVariantFixture = {
|
|
|
|
|
snapshot() {
|
|
|
|
|
const actionHosts = Array.from(destinationActionBar?.children ?? []);
|
|
|
|
|
const nativeChildren = actionHosts.filter((child) => !child.matches("[data-ryd-synthetic-shorts-dislike]"));
|
|
|
|
|
const activationTargets = actionHosts.map(activationTarget);
|
|
|
|
|
const visibleRenderers = Array.from(shortsPage.querySelectorAll("ytd-reel-video-renderer")).filter(visible);
|
|
|
|
|
const exactHrefRenderers = visibleRenderers.filter((candidate) =>
|
|
|
|
|
Array.from(candidate.querySelectorAll('a[href*="/shorts/"]')).some((link) => {
|
|
|
|
|
try {
|
|
|
|
|
return (
|
|
|
|
|
new URL(link.getAttribute("href"), location.origin).pathname ===
|
|
|
|
|
`/shorts/${matrixScenario.destination.videoId}`
|
|
|
|
|
);
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
const nativeDislikes = actionHosts.filter((host) =>
|
|
|
|
|
host.matches("dislike-button-view-model, #dislike-button"),
|
|
|
|
|
);
|
|
|
|
|
const syntheticDislikes = actionHosts.filter((host) => host.matches("[data-ryd-synthetic-shorts-dislike]"));
|
|
|
|
|
return {
|
|
|
|
|
actionBarBounds: bounds(destinationActionBar),
|
|
|
|
|
actionBarConnected: destinationActionBar?.isConnected ?? false,
|
|
|
|
|
actionBarDataReadCount,
|
|
|
|
|
actionBarDataReady: Boolean(destinationActionBar?.data),
|
|
|
|
|
actionBarDataWriteCount,
|
|
|
|
|
actionBarReplaced: destinationActionBar !== null && destinationActionBar !== originActionBar,
|
|
|
|
|
actionNames: actionHosts.map(actionName),
|
|
|
|
|
blockedActivationConnected: blockedActivationTarget?.isConnected ?? false,
|
|
|
|
|
currentActionButtonCount: activationTargets.filter(Boolean).length,
|
|
|
|
|
currentNativeChildCount: nativeChildren.length,
|
|
|
|
|
currentSyntheticCount: syntheticDislikes.length,
|
|
|
|
|
documentIdentity,
|
|
|
|
|
exactHrefRendererCount: exactHrefRenderers.length,
|
|
|
|
|
hitTestedActionButtonCount: activationTargets.filter(hitTested).length,
|
|
|
|
|
href: renderer.querySelector('a[href*="/shorts/"]')?.getAttribute("href") ?? null,
|
|
|
|
|
isActive: renderer.hasAttribute("is-active"),
|
|
|
|
|
nativeChildrenStable:
|
|
|
|
|
destinationNativeChildren.length === nativeChildren.length &&
|
|
|
|
|
nativeChildren.every((child, index) => child === destinationNativeChildren[index]),
|
|
|
|
|
nativeDislikeCount: nativeDislikes.length,
|
|
|
|
|
nativeDislikeText: nativeDislikes[0]?.querySelector("#text, [role='text']")?.textContent?.trim() ?? null,
|
|
|
|
|
navigateFinishes: 0,
|
|
|
|
|
navigateStarts: timeline.includes("navigate-start") ? 1 : 0,
|
|
|
|
|
nonRenderedActionButtonCount: activationTargets.filter((target) => target && !visible(target)).length,
|
|
|
|
|
originActionBarConnected: originActionBar.isConnected,
|
|
|
|
|
originSyntheticConnected: originSyntheticDislike?.isConnected ?? false,
|
|
|
|
|
rendererReused: renderer.isConnected,
|
|
|
|
|
rendererVideoId: renderer.getAttribute("video-id"),
|
|
|
|
|
timeline: [...timeline],
|
|
|
|
|
visibleActionButtonCount: activationTargets.filter(visible).length,
|
|
|
|
|
visibleRendererCount: visibleRenderers.length,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
{ once: true },
|
|
|
|
|
);
|
|
|
|
|
}, scenario);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function installReplacedRootShortStartOnlyFixture(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 shortsPage = fixturePage?.querySelector('[data-fixture-page-kind="shorts"]');
|
|
|
|
|
const originRoot = shortsPage?.querySelector("ytd-shorts");
|
|
|
|
|
const originRenderer = originRoot?.querySelector(
|
|
|
|
|
`ytd-reel-video-renderer[video-id="${matrixScenario.origin.videoId}"][is-active]`,
|
|
|
|
|
);
|
|
|
|
|
const originActionBar = originRenderer?.querySelector("reel-action-bar-view-model");
|
|
|
|
|
const next = document.getElementById("short-next");
|
|
|
|
|
if (!fixturePage || !shortsPage || !originRoot || !originRenderer || !originActionBar || !next) {
|
|
|
|
|
throw new Error("The replaced-root Shorts matrix requires a complete origin and a Next link.");
|
|
|
|
|
}
|
2026-09-01 23:41:28 +02:00
|
|
|
|
2026-09-06 12:51:12 +02:00
|
|
|
const nativeChildTemplates = Array.from(originActionBar.children)
|
|
|
|
|
.filter((child) => !child.matches("[data-ryd-synthetic-shorts-dislike]"))
|
|
|
|
|
.map((child) => child.cloneNode(true));
|
|
|
|
|
if (nativeChildTemplates.length !== 5) {
|
|
|
|
|
throw new Error("The replaced-root Shorts matrix requires exactly five native action controls.");
|
|
|
|
|
}
|
2026-09-01 23:41:28 +02:00
|
|
|
|
|
|
|
|
const documentIdentity = `matrix-${Date.now()}-${Math.random()}`;
|
2026-09-06 12:51:12 +02:00
|
|
|
const repeatedStart = matrixScenario.transition.navigateStarts === "before-and-after-route";
|
2026-09-01 23:41:28 +02:00
|
|
|
const timeline = [];
|
2026-09-06 12:51:12 +02:00
|
|
|
let destinationActionBar = null;
|
|
|
|
|
let destinationActionBarData = null;
|
|
|
|
|
let destinationActionBarDataReadCount = 0;
|
|
|
|
|
let destinationActionBarFirstDataReadAt = null;
|
|
|
|
|
let destinationActionBarMountedAt = null;
|
|
|
|
|
let destinationRenderer = null;
|
|
|
|
|
let destinationRoot = null;
|
|
|
|
|
let originSyntheticDislike = null;
|
|
|
|
|
|
|
|
|
|
function visible(element) {
|
|
|
|
|
if (!element?.isConnected || element.closest("[hidden], [aria-hidden='true']")) return false;
|
|
|
|
|
const bounds = element.getBoundingClientRect();
|
|
|
|
|
const style = getComputedStyle(element);
|
|
|
|
|
return style.display !== "none" && style.visibility !== "hidden" && bounds.width > 0 && bounds.height > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function replaceRoot() {
|
|
|
|
|
originSyntheticDislike = originActionBar.querySelector("[data-ryd-synthetic-shorts-dislike]");
|
|
|
|
|
if (!originSyntheticDislike) {
|
|
|
|
|
throw new Error("The replaced-root Shorts matrix requires an initialized origin Dislike.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-01 23:41:28 +02:00
|
|
|
document.dispatchEvent(new Event("yt-navigate-start", { bubbles: true }));
|
2026-09-06 12:51:12 +02:00
|
|
|
timeline.push(repeatedStart ? "navigate-start-a" : "navigate-start");
|
|
|
|
|
history.pushState({}, "", `/shorts/${matrixScenario.destination.videoId}?rydNavigationFixture=1`);
|
|
|
|
|
shortsPage.setAttribute("data-fixture-video-id", matrixScenario.destination.videoId);
|
|
|
|
|
|
|
|
|
|
destinationRoot = document.createElement("ytd-shorts");
|
|
|
|
|
destinationRoot.setAttribute("data-fixture-replaced-shorts-root", matrixScenario.destination.videoId);
|
|
|
|
|
const sequence = document.createElement("div");
|
|
|
|
|
sequence.className = "reel-video-in-sequence-new";
|
|
|
|
|
sequence.setAttribute("data-fixture-sequence-video-id", matrixScenario.destination.videoId);
|
|
|
|
|
const thumbnail = document.createElement("div");
|
|
|
|
|
thumbnail.className = "reel-video-in-sequence-thumbnail";
|
|
|
|
|
destinationRenderer = document.createElement("ytd-reel-video-renderer");
|
|
|
|
|
destinationRenderer.setAttribute("video-id", matrixScenario.destination.videoId);
|
|
|
|
|
destinationRenderer.setAttribute("is-active", "");
|
|
|
|
|
destinationRenderer.setAttribute("data-fixture-replaced-renderer", matrixScenario.destination.videoId);
|
|
|
|
|
const overlay = document.createElement("div");
|
|
|
|
|
overlay.id = "experiment-overlay";
|
|
|
|
|
overlay.innerHTML = "<span>Ready</span>";
|
|
|
|
|
const identityLink = document.createElement("a");
|
|
|
|
|
identityLink.href = `/shorts/${matrixScenario.destination.videoId}`;
|
|
|
|
|
identityLink.setAttribute("aria-label", `Short ${matrixScenario.destination.videoId}`);
|
|
|
|
|
destinationRenderer.append(overlay, identityLink);
|
|
|
|
|
sequence.append(thumbnail, destinationRenderer);
|
|
|
|
|
destinationRoot.append(sequence);
|
|
|
|
|
originRoot.replaceWith(destinationRoot);
|
|
|
|
|
timeline.push("route-and-root-without-action-bar");
|
|
|
|
|
|
|
|
|
|
if (repeatedStart) {
|
|
|
|
|
document.dispatchEvent(new Event("yt-navigate-start", { bubbles: true }));
|
|
|
|
|
timeline.push("navigate-start-b");
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-01 23:41:28 +02:00
|
|
|
|
2026-09-06 12:51:12 +02:00
|
|
|
function appendEmptyActionBar() {
|
|
|
|
|
if (!destinationRenderer?.isConnected || destinationActionBar) {
|
|
|
|
|
throw new Error("The replaced-root Shorts matrix cannot append its empty action bar.");
|
|
|
|
|
}
|
|
|
|
|
destinationActionBar = document.createElement("reel-action-bar-view-model");
|
|
|
|
|
destinationActionBar.setAttribute("data-fixture-role", "buttons");
|
|
|
|
|
destinationActionBar.setAttribute("data-fixture-staged-action-bar", matrixScenario.destination.videoId);
|
|
|
|
|
destinationActionBarData = null;
|
|
|
|
|
destinationActionBarDataReadCount = 0;
|
|
|
|
|
destinationActionBarFirstDataReadAt = null;
|
|
|
|
|
destinationActionBarMountedAt = performance.now();
|
|
|
|
|
Object.defineProperty(destinationActionBar, "data", {
|
|
|
|
|
configurable: true,
|
|
|
|
|
get() {
|
|
|
|
|
destinationActionBarDataReadCount += 1;
|
|
|
|
|
destinationActionBarFirstDataReadAt ??= performance.now();
|
|
|
|
|
return destinationActionBarData;
|
|
|
|
|
},
|
|
|
|
|
set(value) {
|
|
|
|
|
destinationActionBarData = value;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
destinationRenderer.append(destinationActionBar);
|
|
|
|
|
timeline.push("append-empty-data-null-action-bar");
|
|
|
|
|
}
|
2026-09-01 23:41:28 +02:00
|
|
|
|
2026-09-06 12:51:12 +02:00
|
|
|
function appendNativeChildren() {
|
|
|
|
|
if (!destinationActionBar?.isConnected || destinationActionBar.children.length !== 0) {
|
|
|
|
|
throw new Error("The replaced-root Shorts matrix cannot append its native action inventory.");
|
|
|
|
|
}
|
|
|
|
|
destinationActionBar.append(...nativeChildTemplates.map((child) => child.cloneNode(true)));
|
|
|
|
|
globalThis.__navigationFixture.setNativeLikeCount(destinationActionBar, matrixScenario.destination.videoId);
|
|
|
|
|
const like = destinationActionBar.querySelector('[data-fixture-role="like"]');
|
|
|
|
|
like?.classList.remove("style-default-active");
|
|
|
|
|
like?.classList.add("style-text");
|
|
|
|
|
like?.querySelector("button")?.setAttribute("aria-pressed", "false");
|
|
|
|
|
timeline.push("append-complete-native-inventory-data-null");
|
|
|
|
|
}
|
2026-09-01 23:41:28 +02:00
|
|
|
|
2026-09-06 12:51:12 +02:00
|
|
|
function hydrateActionBar() {
|
|
|
|
|
if (!destinationActionBar?.isConnected || destinationActionBar.children.length !== 5) {
|
|
|
|
|
throw new Error("The replaced-root Shorts matrix cannot hydrate an incomplete action bar.");
|
|
|
|
|
}
|
|
|
|
|
destinationActionBar.data = { hydrated: true, videoId: matrixScenario.destination.videoId };
|
|
|
|
|
timeline.push("hydrate-action-bar-data");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
next.addEventListener(
|
|
|
|
|
"click",
|
|
|
|
|
(event) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopImmediatePropagation();
|
|
|
|
|
replaceRoot();
|
|
|
|
|
},
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
globalThis.__navigationMatrixReplacedRootShortFixture = {
|
|
|
|
|
appendEmptyActionBar,
|
|
|
|
|
appendNativeChildren,
|
|
|
|
|
hydrateActionBar,
|
2026-09-01 23:41:28 +02:00
|
|
|
snapshot() {
|
2026-09-06 12:51:12 +02:00
|
|
|
const currentSyntheticDislikes = Array.from(
|
|
|
|
|
document.querySelectorAll("[data-ryd-synthetic-shorts-dislike]"),
|
|
|
|
|
);
|
2026-09-01 23:41:28 +02:00
|
|
|
return {
|
2026-09-06 12:51:12 +02:00
|
|
|
actionBarConnected: destinationActionBar?.isConnected ?? false,
|
|
|
|
|
actionBarDataReadCount: destinationActionBarDataReadCount,
|
|
|
|
|
actionBarDataReady: Boolean(destinationActionBarData),
|
|
|
|
|
actionBarFirstDataReadDelayMs:
|
|
|
|
|
destinationActionBarFirstDataReadAt === null || destinationActionBarMountedAt === null
|
|
|
|
|
? null
|
|
|
|
|
: destinationActionBarFirstDataReadAt - destinationActionBarMountedAt,
|
|
|
|
|
currentActionButtonCount:
|
|
|
|
|
destinationActionBar === null
|
|
|
|
|
? 0
|
|
|
|
|
: Array.from(destinationActionBar.querySelectorAll("button")).filter(visible).length,
|
|
|
|
|
currentNativeChildCount:
|
|
|
|
|
destinationActionBar === null
|
|
|
|
|
? 0
|
|
|
|
|
: Array.from(destinationActionBar.children).filter(
|
|
|
|
|
(child) => !child.matches("[data-ryd-synthetic-shorts-dislike]"),
|
|
|
|
|
).length,
|
|
|
|
|
currentSyntheticCount: currentSyntheticDislikes.length,
|
|
|
|
|
currentSyntheticVideoIds: currentSyntheticDislikes.map((control) =>
|
|
|
|
|
control.getAttribute("data-ryd-video-id"),
|
|
|
|
|
),
|
|
|
|
|
destinationRendererConnected: destinationRenderer?.isConnected ?? false,
|
|
|
|
|
destinationRendererVisible: visible(destinationRenderer),
|
|
|
|
|
destinationRootConnected: destinationRoot?.isConnected ?? false,
|
2026-09-01 23:41:28 +02:00
|
|
|
documentIdentity,
|
2026-09-06 12:51:12 +02:00
|
|
|
navigateFinishes: 0,
|
|
|
|
|
navigateStarts: repeatedStart ? 2 : 1,
|
|
|
|
|
originRootConnected: originRoot.isConnected,
|
|
|
|
|
originSyntheticConnected: originSyntheticDislike?.isConnected ?? false,
|
|
|
|
|
rendererVideoId: destinationRenderer?.getAttribute("video-id") ?? null,
|
2026-09-01 23:41:28 +02:00
|
|
|
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,
|
2026-09-06 12:51:12 +02:00
|
|
|
shortActiveMutations: [],
|
2026-09-01 23:41:28 +02:00
|
|
|
};
|
|
|
|
|
document.addEventListener("yt-navigate-start", () => {
|
|
|
|
|
probe.navigateStarts += 1;
|
|
|
|
|
});
|
|
|
|
|
document.addEventListener("yt-navigate-finish", () => {
|
|
|
|
|
probe.navigateFinishes += 1;
|
|
|
|
|
});
|
2026-09-06 12:51:12 +02:00
|
|
|
const shortActiveObserver = new MutationObserver((mutations) => {
|
|
|
|
|
for (const mutation of mutations) {
|
|
|
|
|
const renderer = mutation.target;
|
|
|
|
|
if (!renderer.matches?.("ytd-reel-video-renderer")) continue;
|
|
|
|
|
probe.shortActiveMutations.push({
|
|
|
|
|
active: renderer.hasAttribute("is-active"),
|
|
|
|
|
attribute: mutation.attributeName,
|
|
|
|
|
hidden: renderer.hidden,
|
|
|
|
|
oldValue: mutation.oldValue,
|
|
|
|
|
videoId: renderer.getAttribute("video-id"),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
shortActiveObserver.observe(fixturePage, {
|
|
|
|
|
attributeFilter: ["hidden", "is-active"],
|
|
|
|
|
attributeOldValue: true,
|
|
|
|
|
attributes: true,
|
|
|
|
|
subtree: true,
|
|
|
|
|
});
|
2026-09-01 23:41:28 +02:00
|
|
|
|
|
|
|
|
const delayedLinkId = {
|
|
|
|
|
"short-direct-watch-delayed": "short-to-watch",
|
|
|
|
|
"watch-direct-short-delayed": "watch-to-short",
|
|
|
|
|
}[matrixScenario.id];
|
|
|
|
|
if (delayedLinkId) {
|
2026-09-06 12:51:12 +02:00
|
|
|
document
|
|
|
|
|
.getElementById(delayedLinkId)
|
|
|
|
|
?.setAttribute("data-fixture-control-delay", String(matrixScenario.timing.controlDelayMs));
|
2026-09-01 23:41:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
2026-09-06 12:51:12 +02:00
|
|
|
globalThis.__navigationFixture.setNativeLikeCount(controls, videoId);
|
2026-09-01 23:41:28 +02:00
|
|
|
for (const role of ["like", "dislike"]) {
|
2026-09-06 12:51:12 +02:00
|
|
|
const control = controls.querySelector(`[data-fixture-role="${role}"]`);
|
2026-09-01 23:41:28 +02:00
|
|
|
control.classList.remove("style-default-active");
|
|
|
|
|
control.classList.add("style-text");
|
|
|
|
|
control.querySelector("button")?.setAttribute("aria-pressed", "false");
|
|
|
|
|
}
|
2026-09-06 12:51:12 +02:00
|
|
|
controls
|
|
|
|
|
.querySelectorAll(
|
|
|
|
|
'[data-fixture-role="dislike"] .ytSpecButtonShapeNextButtonTextContent, [data-fixture-role="dislike"] #text',
|
|
|
|
|
)
|
|
|
|
|
.forEach((element) => element.remove());
|
2026-09-01 23:41:28 +02:00
|
|
|
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,
|
2026-09-06 12:51:12 +02:00
|
|
|
shortActiveMutations: [...probe.shortActiveMutations],
|
2026-09-01 23:41:28 +02:00
|
|
|
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}.`);
|
|
|
|
|
}
|
2026-09-06 12:51:12 +02:00
|
|
|
if (
|
|
|
|
|
scenario.id === "short-next-short-reuse-renderer-start-no-finish" ||
|
|
|
|
|
scenario.id === "short-next-short-reuse-renderer-repeated-start-no-finish" ||
|
|
|
|
|
scenario.id === "short-next-short-exact-href-with-description-crosslink"
|
|
|
|
|
) {
|
|
|
|
|
await installSameRendererShortStartOnlyFixture(context, scenario);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (scenario.id === "short-next-short-reuse-renderer-replace-action-root-exact-href-persistent-data-null") {
|
|
|
|
|
await installPersistentDataNullShortFixture(context, scenario);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
scenario.id === "short-next-short-persistent-data-null-nonrendered-native-stays-inert" ||
|
|
|
|
|
scenario.id === "short-next-short-persistent-data-null-native-dislike"
|
|
|
|
|
) {
|
|
|
|
|
await installPersistentDataNullVariantShortFixture(context, scenario);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
scenario.id === "short-next-short-replace-root-start-no-finish-staged-hydration" ||
|
|
|
|
|
scenario.id === "short-next-short-replace-root-repeated-start-no-finish-staged-hydration"
|
|
|
|
|
) {
|
|
|
|
|
await installReplacedRootShortStartOnlyFixture(context, scenario);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-09-01 23:41:28 +02:00
|
|
|
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) {
|
2026-09-06 12:51:12 +02:00
|
|
|
const watchRoot = page.locator(`ytd-watch-flexy[video-id="${videoId}"], ytd-watch-grid[video-id="${videoId}"]`);
|
|
|
|
|
const controls = watchRoot.locator(`[data-fixture-control-video-id="${videoId}"]`);
|
2026-09-01 23:41:28 +02:00
|
|
|
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),
|
2026-09-06 12:51:12 +02:00
|
|
|
watchRoot,
|
2026-09-01 23:41:28 +02:00
|
|
|
wrapper,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-06 12:51:12 +02:00
|
|
|
function currentShortRendererLocator(page, videoId, shortIdentity = "active-video-id") {
|
|
|
|
|
if (shortIdentity === "exact-href-without-active-or-video-id") {
|
|
|
|
|
return page.locator(
|
|
|
|
|
`ytd-reel-video-renderer:not([hidden]):has(a[href^="/shorts/${videoId}"])` + `:not([is-active]):not([video-id])`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return page.locator(`ytd-reel-video-renderer[video-id="${videoId}"][is-active]`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function currentShortDislikeSelector(runtime, shortsDislikeControl = "synthetic") {
|
|
|
|
|
return shortsDislikeControl === "native"
|
|
|
|
|
? 'dislike-button-view-model[data-fixture-role="dislike"]'
|
|
|
|
|
: runtime.selectors.shortsDislike;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getDestinationDislikePostconditionTarget(page, runtime, scenario) {
|
|
|
|
|
if (scenario.postcondition !== SINGLE_DESTINATION_DISLIKE_POSTCONDITION) {
|
|
|
|
|
throw new Error(`Unsupported navigation postcondition for ${scenario.id}: ${scenario.postcondition}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { destination } = scenario;
|
|
|
|
|
const reactionRoot =
|
|
|
|
|
destination.kind === "shorts"
|
|
|
|
|
? currentShortRendererLocator(page, destination.videoId, destination.shortIdentity)
|
|
|
|
|
: currentWatchLocators(page, runtime, destination.videoId).controls;
|
|
|
|
|
const control =
|
|
|
|
|
destination.kind === "shorts"
|
|
|
|
|
? reactionRoot.locator(currentShortDislikeSelector(runtime, destination.shortsDislikeControl))
|
|
|
|
|
: reactionRoot.locator('[data-fixture-role="dislike"]');
|
|
|
|
|
const likeControl = reactionRoot.locator('[data-fixture-role="like"]');
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
button: control.locator("button"),
|
|
|
|
|
control,
|
|
|
|
|
count: control.locator(destination.kind === "shorts" ? "#text" : runtime.selectors.watchDislikeCount),
|
|
|
|
|
expectedCount: expectedDislikeText(destination.counts, 1),
|
|
|
|
|
expectedFinalDislikeCount: expectedDislikeText(destination.counts),
|
|
|
|
|
expectedInitialDislikeCount: expectedDislikeText(destination.counts),
|
|
|
|
|
expectedFinalLikeCount: destination.counts.likes + 1,
|
|
|
|
|
likeButton: likeControl.locator("button"),
|
|
|
|
|
likeControl,
|
|
|
|
|
likeCount: likeControl.locator("#text, [role='text']"),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-01 23:41:28 +02:00
|
|
|
async function expectOwnedWatchBar(page, runtime, videoId, counts) {
|
|
|
|
|
const locators = currentWatchLocators(page, runtime, videoId);
|
2026-09-06 12:51:12 +02:00
|
|
|
const currentCount = locators.controls
|
|
|
|
|
.locator('[data-fixture-role="dislike"]')
|
|
|
|
|
.locator(runtime.selectors.watchDislikeCount);
|
2026-09-01 23:41:28 +02:00
|
|
|
await expect(locators.controls).toHaveCount(1);
|
2026-09-06 12:51:12 +02:00
|
|
|
await expect(
|
|
|
|
|
locators.controls.locator(
|
|
|
|
|
":scope > yt-smartimation > [data-fixture-smartimation-content-shell] > [data-fixture-smartimation-content] > like-button-view-model",
|
|
|
|
|
),
|
|
|
|
|
).toHaveCount(1);
|
|
|
|
|
await expect(
|
|
|
|
|
locators.controls.locator(
|
|
|
|
|
":scope > yt-smartimation > [data-fixture-smartimation-content-shell] > [data-fixture-smartimation-content] > dislike-button-view-model",
|
|
|
|
|
),
|
|
|
|
|
).toHaveCount(1);
|
|
|
|
|
expect(
|
|
|
|
|
await page.evaluate(() => globalThis.__navigationFixtureBaseline?.renderedWatchNativeDislikeTextCount),
|
|
|
|
|
"the fixture must supply an icon-only native Watch Dislike control",
|
|
|
|
|
).toBe(0);
|
|
|
|
|
await expect(currentCount).toHaveCount(1);
|
|
|
|
|
await expect(currentCount).not.toHaveAttribute("data-fixture-provided-native-text", /.*/);
|
|
|
|
|
await expect(currentCount).toHaveText(expectedDislikeText(counts));
|
2026-09-01 23:41:28 +02:00
|
|
|
await expect(locators.wrapper).toHaveCount(1);
|
2026-09-06 12:51:12 +02:00
|
|
|
if (runtime.selectors.wrapperVideoAttribute) {
|
|
|
|
|
await expect(locators.wrapper).toHaveAttribute(runtime.selectors.wrapperVideoAttribute, videoId);
|
|
|
|
|
}
|
2026-09-01 23:41:28 +02:00
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
};
|
2026-09-06 12:51:12 +02:00
|
|
|
const like = reactionRegion.querySelector('[data-fixture-role="like"] button');
|
|
|
|
|
const dislike = reactionRegion.querySelector('[data-fixture-role="dislike"] button');
|
2026-09-01 23:41:28 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-06 12:51:12 +02:00
|
|
|
async function expectOwnedShortControl(
|
|
|
|
|
page,
|
|
|
|
|
runtime,
|
|
|
|
|
videoId,
|
|
|
|
|
counts,
|
|
|
|
|
shortIdentity = "active-video-id",
|
|
|
|
|
shortsDislikeControl = "synthetic",
|
|
|
|
|
) {
|
2026-09-01 23:41:28 +02:00
|
|
|
await expect(page).toHaveURL((url) => url.pathname === `/shorts/${videoId}`);
|
2026-09-06 12:51:12 +02:00
|
|
|
const currentRenderer = currentShortRendererLocator(page, videoId, shortIdentity);
|
|
|
|
|
const dislikeSelector = currentShortDislikeSelector(runtime, shortsDislikeControl);
|
|
|
|
|
const dislike = currentRenderer.locator(dislikeSelector);
|
|
|
|
|
await expect(currentRenderer).toHaveCount(1);
|
|
|
|
|
await expect(currentRenderer).toBeVisible();
|
2026-09-01 23:41:28 +02:00
|
|
|
await expect(dislike).toHaveCount(1);
|
|
|
|
|
await expect(dislike).toBeVisible();
|
|
|
|
|
await expect(dislike.locator("#text")).toHaveText(String(counts.dislikes));
|
2026-09-06 12:51:12 +02:00
|
|
|
if (shortsDislikeControl === "synthetic" && runtime.selectors.shortsVideoAttribute) {
|
2026-09-01 23:41:28 +02:00
|
|
|
await expect(dislike).toHaveAttribute(runtime.selectors.shortsVideoAttribute, videoId);
|
|
|
|
|
}
|
2026-09-06 12:51:12 +02:00
|
|
|
await expect(page.locator(`${dislikeSelector}:visible`)).toHaveCount(1);
|
|
|
|
|
await expect(currentRenderer.locator("reel-action-bar-view-model button")).toHaveCount(6);
|
|
|
|
|
await expect(currentRenderer.locator("reel-action-bar-view-model button:visible")).toHaveCount(6);
|
2026-09-01 23:41:28 +02:00
|
|
|
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);
|
2026-09-06 12:51:12 +02:00
|
|
|
return { currentRenderer, dislike };
|
2026-09-01 23:41:28 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-06 12:51:12 +02:00
|
|
|
async function readOwnedSurfaceInvariant(
|
|
|
|
|
page,
|
|
|
|
|
runtime,
|
|
|
|
|
videoId,
|
|
|
|
|
counts,
|
|
|
|
|
kind,
|
|
|
|
|
shortIdentity = "active-video-id",
|
|
|
|
|
shortsDislikeControl = "synthetic",
|
|
|
|
|
) {
|
|
|
|
|
const shortsDislikeSelector = currentShortDislikeSelector(runtime, shortsDislikeControl);
|
2026-09-01 23:41:28 +02:00
|
|
|
return page.evaluate(
|
2026-09-06 12:51:12 +02:00
|
|
|
({
|
|
|
|
|
counts: expectedCounts,
|
|
|
|
|
expectedCount,
|
|
|
|
|
expectedTooltip,
|
|
|
|
|
kind: expectedKind,
|
|
|
|
|
selectors,
|
|
|
|
|
shortsDislikeSelector: expectedShortsDislikeSelector,
|
|
|
|
|
shortIdentity: expectedShortIdentity,
|
|
|
|
|
videoId: expectedVideoId,
|
|
|
|
|
}) => {
|
2026-09-01 23:41:28 +02:00
|
|
|
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]`),
|
|
|
|
|
);
|
2026-09-06 12:51:12 +02:00
|
|
|
const exactHrefRenderers = Array.from(document.querySelectorAll("ytd-reel-video-renderer")).filter(
|
|
|
|
|
(renderer) =>
|
|
|
|
|
visible(renderer) &&
|
|
|
|
|
Array.from(renderer.querySelectorAll('a[href*="/shorts/"]')).some((link) => {
|
|
|
|
|
try {
|
|
|
|
|
return new URL(link.getAttribute("href"), location.origin).pathname === `/shorts/${expectedVideoId}`;
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}),
|
2026-09-01 23:41:28 +02:00
|
|
|
);
|
2026-09-06 12:51:12 +02:00
|
|
|
const currentRenderers =
|
|
|
|
|
expectedShortIdentity === "exact-href-without-active-or-video-id" ? exactHrefRenderers : activeRenderers;
|
|
|
|
|
const currentControls = currentRenderers.flatMap((renderer) =>
|
|
|
|
|
Array.from(renderer.querySelectorAll(expectedShortsDislikeSelector)),
|
2026-09-01 23:41:28 +02:00
|
|
|
);
|
2026-09-06 12:51:12 +02:00
|
|
|
const actionButtons = currentRenderers.flatMap((renderer) =>
|
|
|
|
|
Array.from(renderer.querySelectorAll("reel-action-bar-view-model button")),
|
|
|
|
|
);
|
|
|
|
|
const currentActionButtons = actionButtons.filter(visible);
|
|
|
|
|
const visibleControls = Array.from(document.querySelectorAll(expectedShortsDislikeSelector)).filter(visible);
|
2026-09-01 23:41:28 +02:00
|
|
|
return {
|
|
|
|
|
...common,
|
|
|
|
|
activeRenderers: activeRenderers.length,
|
|
|
|
|
count: currentControls[0]?.querySelector("#text")?.textContent?.trim() ?? null,
|
|
|
|
|
currentActionButtons: currentActionButtons.length,
|
2026-09-06 12:51:12 +02:00
|
|
|
currentRendererHasActive: currentRenderers[0]?.hasAttribute("is-active") ?? null,
|
|
|
|
|
currentRendererVideoId: currentRenderers[0]?.getAttribute("video-id") ?? null,
|
|
|
|
|
currentRenderers: currentRenderers.length,
|
|
|
|
|
totalActionButtons: actionButtons.length,
|
2026-09-01 23:41:28 +02:00
|
|
|
currentControls: currentControls.length,
|
|
|
|
|
currentVisible: currentControls.length === 1 && visible(currentControls[0]),
|
2026-09-06 12:51:12 +02:00
|
|
|
expectedCount,
|
2026-09-01 23:41:28 +02:00
|
|
|
expectedPathname: `/shorts/${expectedVideoId}`,
|
2026-09-06 12:51:12 +02:00
|
|
|
expectedShortIdentity,
|
2026-09-01 23:41:28 +02:00
|
|
|
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,
|
2026-09-06 12:51:12 +02:00
|
|
|
count:
|
|
|
|
|
currentControls[0]
|
|
|
|
|
?.querySelector('[data-fixture-role="dislike"]')
|
|
|
|
|
?.querySelector(selectors.watchDislikeCount)
|
|
|
|
|
?.textContent?.trim() ?? null,
|
2026-09-01 23:41:28 +02:00
|
|
|
currentControls: currentControls.length,
|
2026-09-06 12:51:12 +02:00
|
|
|
expectedCount,
|
2026-09-01 23:41:28 +02:00
|
|
|
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,
|
2026-09-06 12:51:12 +02:00
|
|
|
ownerVideoId: selectors.wrapperVideoAttribute
|
|
|
|
|
? wrapper?.getAttribute(selectors.wrapperVideoAttribute) ?? null
|
|
|
|
|
: expectedVideoId,
|
2026-09-01 23:41:28 +02:00
|
|
|
ownerWrapperVisible: visible(wrapper),
|
|
|
|
|
ratio: containerBounds?.width > 0 && barBounds ? barBounds.width / containerBounds.width : null,
|
|
|
|
|
videoId: currentUrl.searchParams.get("v"),
|
|
|
|
|
};
|
|
|
|
|
},
|
2026-09-06 12:51:12 +02:00
|
|
|
{
|
|
|
|
|
counts,
|
|
|
|
|
expectedCount: expectedDislikeText(counts),
|
|
|
|
|
expectedTooltip: runtime.tooltipText(counts),
|
|
|
|
|
kind,
|
|
|
|
|
selectors: runtime.selectors,
|
|
|
|
|
shortsDislikeSelector,
|
|
|
|
|
shortIdentity,
|
|
|
|
|
videoId,
|
|
|
|
|
},
|
2026-09-01 23:41:28 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ownedSurfaceInvariantIsValid(sample) {
|
|
|
|
|
if (sample.pathname !== sample.expectedPathname || sample.count !== sample.expectedCount) return false;
|
|
|
|
|
if (sample.kind === "shorts") {
|
2026-09-06 12:51:12 +02:00
|
|
|
const identityMatches =
|
|
|
|
|
sample.expectedShortIdentity === "exact-href-without-active-or-video-id"
|
|
|
|
|
? sample.activeRenderers === 0 &&
|
|
|
|
|
sample.currentRenderers === 1 &&
|
|
|
|
|
sample.currentRendererHasActive === false &&
|
|
|
|
|
sample.currentRendererVideoId === null
|
|
|
|
|
: sample.activeRenderers === 1 && sample.currentRenderers === 1;
|
2026-09-01 23:41:28 +02:00
|
|
|
return (
|
2026-09-06 12:51:12 +02:00
|
|
|
identityMatches &&
|
2026-09-01 23:41:28 +02:00
|
|
|
sample.currentControls === 1 &&
|
2026-09-06 12:51:12 +02:00
|
|
|
sample.currentActionButtons === 6 &&
|
2026-09-01 23:41:28 +02:00
|
|
|
sample.currentVisible &&
|
|
|
|
|
sample.visibleControls === 1 &&
|
2026-09-06 12:51:12 +02:00
|
|
|
sample.totalActionButtons === 6 &&
|
2026-09-01 23:41:28 +02:00
|
|
|
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 &&
|
2026-09-06 12:51:12 +02:00
|
|
|
sample.ownerVideoId === sample.expectedVideoId &&
|
2026-09-01 23:41:28 +02:00
|
|
|
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`,
|
2026-09-06 12:51:12 +02:00
|
|
|
read: () =>
|
|
|
|
|
readOwnedSurfaceInvariant(
|
|
|
|
|
page,
|
|
|
|
|
runtime,
|
|
|
|
|
surface.videoId,
|
|
|
|
|
surface.counts,
|
|
|
|
|
surface.kind,
|
|
|
|
|
surface.shortIdentity,
|
|
|
|
|
surface.shortsDislikeControl,
|
|
|
|
|
),
|
2026-09-01 23:41:28 +02:00
|
|
|
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}`,
|
2026-09-06 12:51:12 +02:00
|
|
|
read: () =>
|
|
|
|
|
readOwnedSurfaceInvariant(
|
|
|
|
|
page,
|
|
|
|
|
runtime,
|
|
|
|
|
surface.videoId,
|
|
|
|
|
surface.counts,
|
|
|
|
|
surface.kind,
|
|
|
|
|
surface.shortIdentity,
|
|
|
|
|
surface.shortsDislikeControl,
|
|
|
|
|
),
|
2026-09-01 23:41:28 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function expectCountRequestVideoIds(backend, expectedVideoIds) {
|
2026-09-06 12:51:12 +02:00
|
|
|
assertExactSuccessfulVotesTraffic(backend.requests, expectedVideoIds, "The shared navigation contract");
|
2026-09-01 23:41:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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"],
|
|
|
|
|
});
|
2026-09-06 12:51:12 +02:00
|
|
|
const retainedOutgoing = page.locator("#fixture-matrix-retained-trees");
|
|
|
|
|
await expect(retainedOutgoing).toBeHidden();
|
|
|
|
|
await expect(
|
|
|
|
|
page.locator('[data-fixture-role="dislike"]').locator(`:is(${runtime.selectors.watchDislikeCount}):visible`),
|
|
|
|
|
).toHaveCount(0);
|
|
|
|
|
await expect(page.locator(`${runtime.selectors.wrapper}:visible`)).toHaveCount(0);
|
|
|
|
|
if (runtime.clearsOutgoingWatchPresentationOnNavigateStart) {
|
|
|
|
|
await expect(
|
|
|
|
|
retainedOutgoing.locator('[data-fixture-role="dislike"]').locator(runtime.selectors.watchDislikeCount),
|
|
|
|
|
).toHaveText("");
|
|
|
|
|
await expect(retainedOutgoing.locator(runtime.selectors.wrapper)).toHaveCount(0);
|
|
|
|
|
await expect(page.locator(runtime.selectors.wrapper)).toHaveCount(0);
|
|
|
|
|
}
|
2026-09-01 23:41:28 +02:00
|
|
|
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);
|
2026-09-06 12:51:12 +02:00
|
|
|
await expect(gatedDestination.controls).toBeVisible();
|
|
|
|
|
await expect(
|
|
|
|
|
gatedDestination.controls.locator('[data-fixture-role="dislike"]').locator(runtime.selectors.watchDislikeCount),
|
|
|
|
|
).toHaveText("");
|
2026-09-01 23:41:28 +02:00
|
|
|
await expect(gatedDestination.wrapper).toHaveCount(0);
|
2026-09-06 12:51:12 +02:00
|
|
|
await expect(page.locator(`${runtime.selectors.wrapper}:visible`)).toHaveCount(0);
|
|
|
|
|
if (runtime.clearsOutgoingWatchPresentationOnNavigateStart) {
|
|
|
|
|
await expect(
|
|
|
|
|
retainedOutgoing.locator('[data-fixture-role="dislike"]').locator(runtime.selectors.watchDislikeCount),
|
|
|
|
|
).toHaveText("");
|
|
|
|
|
await expect(retainedOutgoing.locator(runtime.selectors.wrapper)).toHaveCount(0);
|
|
|
|
|
await expect(page.locator(runtime.selectors.wrapper)).toHaveCount(0);
|
|
|
|
|
}
|
2026-09-01 23:41:28 +02:00
|
|
|
} 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-06 12:51:12 +02:00
|
|
|
async function runWatchSameRootHiddenFirstScenario({
|
|
|
|
|
backend,
|
|
|
|
|
beforeNonCurrentDuplicateDetach,
|
|
|
|
|
page,
|
|
|
|
|
runtime,
|
|
|
|
|
scenario,
|
|
|
|
|
}) {
|
2026-09-01 23:41:28 +02:00
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-06 12:51:12 +02:00
|
|
|
await beforeNonCurrentDuplicateDetach?.();
|
|
|
|
|
|
2026-09-01 23:41:28 +02:00
|
|
|
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]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-06 12:51:12 +02:00
|
|
|
async function runWatchStartWithoutFinishScenario({ 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(
|
|
|
|
|
(videoId) => globalThis.__navigationFixture.navigateWatchAfterStartWithoutFinish(videoId),
|
|
|
|
|
destination.videoId,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await expect(page).toHaveURL((url) => url.pathname === "/watch" && url.searchParams.get("v") === destination.videoId);
|
|
|
|
|
expect(await readStandardProbe(page)).toMatchObject({
|
|
|
|
|
currentKind: "watch",
|
|
|
|
|
currentVideoId: destination.videoId,
|
|
|
|
|
documentIdentity: initialProbe.documentIdentity,
|
|
|
|
|
historyRenders: [],
|
|
|
|
|
navigateFinishes: 0,
|
|
|
|
|
navigateStarts: 1,
|
|
|
|
|
transitionPending: "watch-start-without-finish",
|
|
|
|
|
});
|
|
|
|
|
const outgoingControls = page.locator(`[data-fixture-control-video-id="${origin.videoId}"]`);
|
|
|
|
|
await expect(outgoingControls).toHaveCount(1);
|
|
|
|
|
await expect(outgoingControls).toBeVisible();
|
|
|
|
|
await page.waitForTimeout(650);
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
|
|
|
|
|
|
await page.evaluate(() => globalThis.__navigationFixture.finishWatchAfterStartWithoutFinish());
|
|
|
|
|
await expect(outgoingControls).toHaveCount(0);
|
|
|
|
|
|
|
|
|
|
await waitForOwnedSurfaceWithinBudget(page, runtime, destination);
|
|
|
|
|
await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
|
|
|
expect(await readStandardProbe(page)).toMatchObject({
|
|
|
|
|
currentKind: "watch",
|
|
|
|
|
currentVideoId: destination.videoId,
|
|
|
|
|
documentIdentity: initialProbe.documentIdentity,
|
|
|
|
|
historyRenders: [],
|
|
|
|
|
navigateFinishes: 0,
|
|
|
|
|
navigateStarts: 1,
|
|
|
|
|
transitionPending: null,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await assertOwnedSurfaceContinuously(page, runtime, destination);
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-01 23:41:28 +02:00
|
|
|
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]);
|
|
|
|
|
|
2026-09-06 12:51:12 +02:00
|
|
|
await waitForOwnedSurfaceWithinBudget(page, runtime, destination, scenario.timing?.maxFirstValidMs);
|
2026-09-01 23:41:28 +02:00
|
|
|
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"]) {
|
2026-09-06 12:51:12 +02:00
|
|
|
const control = controls.querySelector(`[data-fixture-role="${role}"]`);
|
2026-09-01 23:41:28 +02:00
|
|
|
control.classList.remove("style-default-active");
|
|
|
|
|
control.classList.add("style-text");
|
|
|
|
|
control.querySelector("button")?.setAttribute("aria-pressed", "false");
|
|
|
|
|
}
|
2026-09-06 12:51:12 +02:00
|
|
|
controls
|
|
|
|
|
.querySelectorAll(
|
|
|
|
|
'[data-fixture-role="dislike"] .ytSpecButtonShapeNextButtonTextContent, [data-fixture-role="dislike"] #text',
|
|
|
|
|
)
|
|
|
|
|
.forEach((element) => element.remove());
|
2026-09-01 23:41:28 +02:00
|
|
|
|
|
|
|
|
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;
|
2026-09-06 12:51:12 +02:00
|
|
|
expect(expectedDislikeText(origin.counts)).toBe(expectedDislikeText(destination.counts));
|
|
|
|
|
expect(origin.counts.dislikes).not.toBe(destination.counts.dislikes);
|
|
|
|
|
expect(origin.counts.likes / (origin.counts.likes + origin.counts.dislikes)).not.toBe(
|
|
|
|
|
destination.counts.likes / (destination.counts.likes + destination.counts.dislikes),
|
|
|
|
|
);
|
2026-09-01 23:41:28 +02:00
|
|
|
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);
|
2026-09-06 12:51:12 +02:00
|
|
|
expect(await page.evaluate(() => globalThis.__navigationMatrixSameNodeFixture.snapshot())).toMatchObject({
|
|
|
|
|
countAfterNavigateStart: "",
|
|
|
|
|
countAfterRouteAndRoot: "",
|
|
|
|
|
timeline: ["navigate-start", "route-and-root-only", "navigate-finish"],
|
|
|
|
|
});
|
2026-09-01 23:41:28 +02:00
|
|
|
|
|
|
|
|
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);
|
2026-09-06 12:51:12 +02:00
|
|
|
await expect(
|
|
|
|
|
pendingDestination.controls.locator('[data-fixture-role="dislike"]').locator(runtime.selectors.watchDislikeCount),
|
|
|
|
|
).toHaveText("");
|
2026-09-01 23:41:28 +02:00
|
|
|
await expect(pendingDestination.wrapper).toHaveCount(0);
|
2026-09-06 12:51:12 +02:00
|
|
|
if (runtime.selectors.wrapperVideoAttribute) {
|
|
|
|
|
await expect(
|
|
|
|
|
page.locator(`${runtime.selectors.wrapper}[${runtime.selectors.wrapperVideoAttribute}="${origin.videoId}"]`),
|
|
|
|
|
).toHaveCount(0);
|
|
|
|
|
}
|
2026-09-01 23:41:28 +02:00
|
|
|
|
|
|
|
|
destinationCountGate.release({ body: { ...destination.counts, rating: 4.5 } });
|
|
|
|
|
await waitForOwnedSurfaceWithinBudget(page, runtime, destination);
|
|
|
|
|
const destinationLocators = await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
|
2026-09-06 12:51:12 +02:00
|
|
|
if (runtime.selectors.wrapperVideoAttribute) {
|
|
|
|
|
await expect(destinationLocators.wrapper).toHaveAttribute(
|
|
|
|
|
runtime.selectors.wrapperVideoAttribute,
|
|
|
|
|
destination.videoId,
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-09-01 23:41:28 +02:00
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
|
|
|
expect(await page.evaluate(() => globalThis.__navigationMatrixSameNodeFixture.snapshot())).toEqual({
|
|
|
|
|
buttonsReused: true,
|
2026-09-06 12:51:12 +02:00
|
|
|
countAfterNavigateStart: "",
|
|
|
|
|
countAfterRouteAndRoot: "",
|
2026-09-01 23:41:28 +02:00
|
|
|
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();
|
2026-09-06 12:51:12 +02:00
|
|
|
} else if (corruptionKind === "missing-video-owner") {
|
|
|
|
|
wrapper.removeAttribute(selectors.wrapperVideoAttribute);
|
|
|
|
|
} else if (corruptionKind === "stale-video-owner") {
|
|
|
|
|
wrapper.setAttribute(selectors.wrapperVideoAttribute, "stalevid001");
|
2026-09-01 23:41:28 +02:00
|
|
|
} 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]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-06 12:51:12 +02:00
|
|
|
async function runShortSameRendererStartOnlyScenario({ backend, page, runtime, scenario }) {
|
|
|
|
|
const { destination, origin } = scenario;
|
|
|
|
|
const repeatedStart = scenario.transition.navigateStarts === "before-and-after-route";
|
|
|
|
|
const expectedTimeline = repeatedStart
|
|
|
|
|
? ["navigate-start-a", "synthetic-removed-before-route", "route-and-exact-href-only", "navigate-start-b"]
|
|
|
|
|
: ["navigate-start", "route-and-exact-href-only"];
|
|
|
|
|
const expectedImmediateAfterRoute = {
|
|
|
|
|
actionBarReused: true,
|
|
|
|
|
href: `/shorts/${destination.videoId}`,
|
|
|
|
|
isActive: false,
|
|
|
|
|
likeReused: true,
|
|
|
|
|
rendererReused: true,
|
|
|
|
|
rendererVideoId: null,
|
|
|
|
|
...(repeatedStart
|
|
|
|
|
? {
|
|
|
|
|
nativeDislikeReused: false,
|
|
|
|
|
syntheticCount: 0,
|
|
|
|
|
syntheticReused: false,
|
|
|
|
|
}
|
|
|
|
|
: {
|
|
|
|
|
nativeDislikeReused: true,
|
|
|
|
|
syntheticCount: 1,
|
|
|
|
|
syntheticReused: true,
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
await expectOwnedShortControl(page, runtime, origin.videoId, origin.counts);
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
|
|
|
|
|
|
const initial = await page.evaluate(() => globalThis.__navigationMatrixSameRendererShortFixture.snapshot());
|
|
|
|
|
await page.locator("#short-next").click();
|
|
|
|
|
await expect(page).toHaveURL((url) => url.pathname === `/shorts/${destination.videoId}`);
|
|
|
|
|
|
|
|
|
|
const expectedShortHrefs = destination.unrelatedDescriptionShortVideoId
|
|
|
|
|
? [`/shorts/${destination.videoId}`, `/shorts/${destination.unrelatedDescriptionShortVideoId}`]
|
|
|
|
|
: [`/shorts/${destination.videoId}`];
|
|
|
|
|
|
|
|
|
|
expect(await page.evaluate(() => globalThis.__navigationMatrixSameRendererShortFixture.snapshot())).toMatchObject({
|
|
|
|
|
actionBarReused: true,
|
|
|
|
|
documentIdentity: initial.documentIdentity,
|
|
|
|
|
href: `/shorts/${destination.videoId}`,
|
|
|
|
|
immediatelyAfterRoute: expectedImmediateAfterRoute,
|
|
|
|
|
isActive: false,
|
|
|
|
|
likeReused: true,
|
|
|
|
|
navigateFinishes: 0,
|
|
|
|
|
rendererReused: true,
|
|
|
|
|
rendererVideoId: null,
|
|
|
|
|
shortHrefs: expectedShortHrefs,
|
|
|
|
|
timeline: expectedTimeline,
|
|
|
|
|
visibleRendererCount: 1,
|
|
|
|
|
});
|
|
|
|
|
const immediateAfterRoute = await page.evaluate(
|
|
|
|
|
() => globalThis.__navigationMatrixSameRendererShortFixture.snapshot().immediatelyAfterRoute,
|
|
|
|
|
);
|
|
|
|
|
if (repeatedStart) {
|
|
|
|
|
expect(immediateAfterRoute).toMatchObject({
|
|
|
|
|
nativeDislikeReused: false,
|
|
|
|
|
syntheticCount: 0,
|
|
|
|
|
syntheticDisabled: null,
|
|
|
|
|
syntheticReused: false,
|
|
|
|
|
syntheticText: null,
|
|
|
|
|
});
|
|
|
|
|
} else if (runtime.name === "extension") {
|
|
|
|
|
expect(immediateAfterRoute).toMatchObject({
|
|
|
|
|
nativeDislikeReused: true,
|
|
|
|
|
syntheticCount: 1,
|
|
|
|
|
syntheticDisabled: true,
|
|
|
|
|
syntheticReused: true,
|
|
|
|
|
syntheticText: "",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await waitForOwnedSurfaceWithinBudget(page, runtime, destination);
|
|
|
|
|
await expectOwnedShortControl(page, runtime, destination.videoId, destination.counts, destination.shortIdentity);
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
|
|
|
expect(await page.evaluate(() => globalThis.__navigationMatrixSameRendererShortFixture.snapshot())).toMatchObject({
|
|
|
|
|
actionBarReused: true,
|
|
|
|
|
href: `/shorts/${destination.videoId}`,
|
|
|
|
|
isActive: false,
|
|
|
|
|
likeReused: true,
|
|
|
|
|
nativeDislikeReused: !repeatedStart,
|
|
|
|
|
navigateFinishes: 0,
|
|
|
|
|
rendererReused: true,
|
|
|
|
|
rendererVideoId: null,
|
|
|
|
|
shortHrefs: expectedShortHrefs,
|
|
|
|
|
syntheticCount: 1,
|
|
|
|
|
syntheticReused: !repeatedStart,
|
|
|
|
|
timeline: expectedTimeline,
|
|
|
|
|
visibleRendererCount: 1,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await assertOwnedSurfaceContinuously(page, runtime, destination);
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function runShortPersistentDataNullScenario({ backend, page, runtime, scenario }) {
|
|
|
|
|
const { destination, origin } = scenario;
|
|
|
|
|
const expectedTimeline = ["navigate-start", "route-and-fresh-five-native-persistent-data-null-action-root"];
|
|
|
|
|
const readFixture = () => page.evaluate(() => globalThis.__navigationMatrixPersistentDataNullShortFixture.snapshot());
|
|
|
|
|
const expectNoInteractions = () =>
|
|
|
|
|
expect(
|
|
|
|
|
backend.requests.filter(
|
|
|
|
|
(request) =>
|
|
|
|
|
request.method === "POST" && ["/interact/vote", "/interact/confirmVote"].includes(request.pathname),
|
|
|
|
|
),
|
|
|
|
|
).toEqual([]);
|
|
|
|
|
|
|
|
|
|
await expectOwnedShortControl(page, runtime, origin.videoId, origin.counts);
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
|
expectNoInteractions();
|
|
|
|
|
const initial = await readFixture();
|
|
|
|
|
|
|
|
|
|
await page.locator("#short-next").click();
|
|
|
|
|
await expect(page).toHaveURL((url) => url.pathname === `/shorts/${destination.videoId}`);
|
|
|
|
|
expect(await readFixture()).toMatchObject({
|
|
|
|
|
actionBarConnected: true,
|
|
|
|
|
actionBarDataReady: false,
|
|
|
|
|
actionBarDataWriteCount: 0,
|
|
|
|
|
actionBarReplaced: true,
|
|
|
|
|
actionNames: ["like", "comments", "share", "remix", "pivot"],
|
|
|
|
|
currentActionButtonCount: 5,
|
|
|
|
|
currentNativeChildCount: 5,
|
|
|
|
|
currentSyntheticCount: 0,
|
|
|
|
|
currentSyntheticVideoIds: [],
|
|
|
|
|
documentIdentity: initial.documentIdentity,
|
|
|
|
|
exactHrefRendererCount: 1,
|
|
|
|
|
hitTestedActionButtonCount: 5,
|
|
|
|
|
href: `/shorts/${destination.videoId}`,
|
|
|
|
|
isActive: false,
|
|
|
|
|
nativeChildrenStable: true,
|
|
|
|
|
nativeDislikeCount: 0,
|
|
|
|
|
navigateFinishes: 0,
|
|
|
|
|
navigateStarts: 1,
|
|
|
|
|
originActionBarConnected: false,
|
|
|
|
|
originSyntheticConnected: false,
|
|
|
|
|
rendererReused: true,
|
|
|
|
|
rendererVideoId: null,
|
|
|
|
|
syntheticEnabled: false,
|
|
|
|
|
timeline: expectedTimeline,
|
|
|
|
|
visibleActionButtonCount: 5,
|
|
|
|
|
visibleRendererCount: 1,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await page.waitForTimeout(scenario.timing.unsafeWindowMs);
|
|
|
|
|
const unsafeWindow = await readFixture();
|
|
|
|
|
expect(unsafeWindow).toMatchObject({
|
|
|
|
|
actionBarConnected: true,
|
|
|
|
|
actionBarDataReadCount: expect.any(Number),
|
|
|
|
|
actionBarDataReady: false,
|
|
|
|
|
actionBarDataWriteCount: 0,
|
|
|
|
|
actionBarReplaced: true,
|
|
|
|
|
actionNames: ["like", "comments", "share", "remix", "pivot"],
|
|
|
|
|
currentActionButtonCount: 5,
|
|
|
|
|
currentNativeChildCount: 5,
|
|
|
|
|
currentSyntheticCount: 0,
|
|
|
|
|
exactHrefRendererCount: 1,
|
|
|
|
|
hitTestedActionButtonCount: 5,
|
|
|
|
|
href: `/shorts/${destination.videoId}`,
|
|
|
|
|
isActive: false,
|
|
|
|
|
nativeChildrenStable: true,
|
|
|
|
|
nativeDislikeCount: 0,
|
|
|
|
|
rendererVideoId: null,
|
|
|
|
|
visibleActionButtonCount: 5,
|
|
|
|
|
visibleRendererCount: 1,
|
|
|
|
|
});
|
|
|
|
|
expect(unsafeWindow.actionBarDataReadCount).toBeGreaterThan(0);
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
|
expectNoInteractions();
|
|
|
|
|
|
|
|
|
|
await waitForOwnedSurfaceWithinBudget(page, runtime, destination, scenario.timing.maxFirstValidMs);
|
|
|
|
|
await expectOwnedShortControl(page, runtime, destination.videoId, destination.counts, destination.shortIdentity);
|
|
|
|
|
const recovered = await readFixture();
|
|
|
|
|
expect(recovered).toMatchObject({
|
|
|
|
|
actionBarConnected: true,
|
|
|
|
|
actionBarDataReadCount: expect.any(Number),
|
|
|
|
|
actionBarDataReady: false,
|
|
|
|
|
actionBarDataWriteCount: 0,
|
|
|
|
|
actionBarReplaced: true,
|
|
|
|
|
actionNames: ["like", "dislike", "comments", "share", "remix", "pivot"],
|
|
|
|
|
currentActionButtonCount: 6,
|
|
|
|
|
currentNativeChildCount: 5,
|
|
|
|
|
currentSyntheticCount: 1,
|
|
|
|
|
currentSyntheticVideoIds: [destination.videoId],
|
|
|
|
|
documentIdentity: initial.documentIdentity,
|
|
|
|
|
exactHrefRendererCount: 1,
|
|
|
|
|
hitTestedActionButtonCount: 6,
|
|
|
|
|
href: `/shorts/${destination.videoId}`,
|
|
|
|
|
isActive: false,
|
|
|
|
|
nativeChildrenStable: true,
|
|
|
|
|
nativeDislikeCount: 0,
|
|
|
|
|
navigateFinishes: 0,
|
|
|
|
|
navigateStarts: 1,
|
|
|
|
|
originActionBarConnected: false,
|
|
|
|
|
originSyntheticConnected: false,
|
|
|
|
|
rendererReused: true,
|
|
|
|
|
rendererVideoId: null,
|
|
|
|
|
syntheticEnabled: true,
|
|
|
|
|
timeline: expectedTimeline,
|
|
|
|
|
visibleActionButtonCount: 6,
|
|
|
|
|
visibleRendererCount: 1,
|
|
|
|
|
});
|
|
|
|
|
expect(recovered.actionBarDataReadCount).toBeGreaterThan(unsafeWindow.actionBarDataReadCount);
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
|
|
|
expectNoInteractions();
|
|
|
|
|
|
|
|
|
|
await assertOwnedSurfaceContinuously(page, runtime, destination);
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
|
|
|
expectNoInteractions();
|
|
|
|
|
expect(await readFixture()).toMatchObject({
|
|
|
|
|
actionBarDataReady: false,
|
|
|
|
|
actionBarDataWriteCount: 0,
|
|
|
|
|
actionNames: ["like", "dislike", "comments", "share", "remix", "pivot"],
|
|
|
|
|
currentNativeChildCount: 5,
|
|
|
|
|
currentSyntheticCount: 1,
|
|
|
|
|
currentSyntheticVideoIds: [destination.videoId],
|
|
|
|
|
hitTestedActionButtonCount: 6,
|
|
|
|
|
nativeChildrenStable: true,
|
|
|
|
|
visibleActionButtonCount: 6,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function runShortPersistentDataNullInertScenario({ backend, page, runtime, scenario }) {
|
|
|
|
|
const { destination, origin } = scenario;
|
|
|
|
|
const expectedTimeline = [
|
|
|
|
|
"navigate-start",
|
|
|
|
|
"hide-one-native-activation-target",
|
|
|
|
|
"route-and-fresh-persistent-data-null-action-root",
|
|
|
|
|
];
|
|
|
|
|
const readFixture = () =>
|
|
|
|
|
page.evaluate(() => globalThis.__navigationMatrixPersistentDataNullVariantFixture.snapshot());
|
|
|
|
|
const requestState = () => ({
|
|
|
|
|
interactionPaths: backend.requests
|
|
|
|
|
.filter(
|
|
|
|
|
(request) =>
|
|
|
|
|
request.method === "POST" && ["/interact/vote", "/interact/confirmVote"].includes(request.pathname),
|
|
|
|
|
)
|
|
|
|
|
.map((request) => request.pathname),
|
|
|
|
|
videoIds: backend.requests
|
|
|
|
|
.filter((request) => request.method === "GET" && request.pathname === "/votes")
|
|
|
|
|
.map((request) => request.query?.videoId),
|
|
|
|
|
});
|
|
|
|
|
const geometryMatches = (actual, expected) =>
|
|
|
|
|
actual !== null &&
|
|
|
|
|
expected !== null &&
|
|
|
|
|
["bottom", "height", "left", "right", "top", "width"].every(
|
|
|
|
|
(property) => Math.abs(actual[property] - expected[property]) <= 0.5,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await expectOwnedShortControl(page, runtime, origin.videoId, origin.counts);
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
|
expect(requestState().interactionPaths).toEqual([]);
|
|
|
|
|
const initial = await readFixture();
|
|
|
|
|
|
|
|
|
|
await page.locator("#short-next").click();
|
|
|
|
|
await expect(page).toHaveURL((url) => url.pathname === `/shorts/${destination.videoId}`);
|
|
|
|
|
const transitioned = await readFixture();
|
|
|
|
|
expect(transitioned).toMatchObject({
|
|
|
|
|
actionBarConnected: true,
|
|
|
|
|
actionBarDataReady: false,
|
|
|
|
|
actionBarDataWriteCount: 0,
|
|
|
|
|
actionBarReplaced: true,
|
|
|
|
|
actionNames: ["like", "comments", "share", "remix", "pivot"],
|
|
|
|
|
blockedActivationConnected: true,
|
|
|
|
|
currentActionButtonCount: 5,
|
|
|
|
|
currentNativeChildCount: 5,
|
|
|
|
|
currentSyntheticCount: 0,
|
|
|
|
|
documentIdentity: initial.documentIdentity,
|
|
|
|
|
exactHrefRendererCount: 1,
|
|
|
|
|
hitTestedActionButtonCount: 4,
|
|
|
|
|
href: `/shorts/${destination.videoId}`,
|
|
|
|
|
isActive: false,
|
|
|
|
|
nativeChildrenStable: true,
|
|
|
|
|
nativeDislikeCount: 0,
|
|
|
|
|
navigateFinishes: 0,
|
|
|
|
|
navigateStarts: 1,
|
|
|
|
|
nonRenderedActionButtonCount: 1,
|
|
|
|
|
originActionBarConnected: false,
|
|
|
|
|
originSyntheticConnected: false,
|
|
|
|
|
rendererReused: true,
|
|
|
|
|
rendererVideoId: null,
|
|
|
|
|
timeline: expectedTimeline,
|
|
|
|
|
visibleActionButtonCount: 4,
|
|
|
|
|
visibleRendererCount: 1,
|
|
|
|
|
});
|
|
|
|
|
expect(transitioned.actionBarBounds?.height).toBeGreaterThan(0);
|
|
|
|
|
expect(transitioned.actionBarBounds?.width).toBeGreaterThan(0);
|
|
|
|
|
|
|
|
|
|
await assertInvariantContinuously({
|
|
|
|
|
durationMs: scenario.timing.inertForMs,
|
|
|
|
|
intervalMs: 25,
|
|
|
|
|
isValid: ({ fixture, requests }) =>
|
|
|
|
|
fixture.actionBarConnected &&
|
|
|
|
|
fixture.actionBarDataReady === false &&
|
|
|
|
|
fixture.actionBarDataWriteCount === 0 &&
|
|
|
|
|
fixture.currentActionButtonCount === 5 &&
|
|
|
|
|
fixture.currentNativeChildCount === 5 &&
|
|
|
|
|
fixture.currentSyntheticCount === 0 &&
|
|
|
|
|
fixture.exactHrefRendererCount === 1 &&
|
|
|
|
|
fixture.hitTestedActionButtonCount === 4 &&
|
|
|
|
|
fixture.nativeChildrenStable &&
|
|
|
|
|
fixture.nativeDislikeCount === 0 &&
|
|
|
|
|
fixture.nonRenderedActionButtonCount === 1 &&
|
|
|
|
|
fixture.visibleActionButtonCount === 4 &&
|
|
|
|
|
fixture.visibleRendererCount === 1 &&
|
|
|
|
|
geometryMatches(fixture.actionBarBounds, transitioned.actionBarBounds) &&
|
|
|
|
|
requests.interactionPaths.length === 0 &&
|
|
|
|
|
requests.videoIds.length === 1 &&
|
|
|
|
|
requests.videoIds[0] === origin.videoId,
|
|
|
|
|
label: `${runtime.name} incomplete persistent data-null Shorts surface remains inert`,
|
|
|
|
|
read: async () => ({ fixture: await readFixture(), requests: requestState() }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
|
expect(requestState().interactionPaths).toEqual([]);
|
|
|
|
|
expect(await readFixture()).toMatchObject({
|
|
|
|
|
actionBarDataReady: false,
|
|
|
|
|
actionBarDataWriteCount: 0,
|
|
|
|
|
currentSyntheticCount: 0,
|
|
|
|
|
hitTestedActionButtonCount: 4,
|
|
|
|
|
nativeChildrenStable: true,
|
|
|
|
|
nativeDislikeCount: 0,
|
|
|
|
|
nonRenderedActionButtonCount: 1,
|
|
|
|
|
visibleActionButtonCount: 4,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function runShortPersistentDataNullNativeDislikeScenario({ backend, page, runtime, scenario }) {
|
|
|
|
|
const { destination, origin } = scenario;
|
|
|
|
|
const expectedTimeline = [
|
|
|
|
|
"navigate-start",
|
|
|
|
|
"append-native-dislike",
|
|
|
|
|
"route-and-fresh-persistent-data-null-action-root",
|
|
|
|
|
];
|
|
|
|
|
const readFixture = () =>
|
|
|
|
|
page.evaluate(() => globalThis.__navigationMatrixPersistentDataNullVariantFixture.snapshot());
|
|
|
|
|
const expectNoInteractions = () =>
|
|
|
|
|
expect(
|
|
|
|
|
backend.requests.filter(
|
|
|
|
|
(request) =>
|
|
|
|
|
request.method === "POST" && ["/interact/vote", "/interact/confirmVote"].includes(request.pathname),
|
|
|
|
|
),
|
|
|
|
|
).toEqual([]);
|
|
|
|
|
|
|
|
|
|
await expectOwnedShortControl(page, runtime, origin.videoId, origin.counts);
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
|
expectNoInteractions();
|
|
|
|
|
const initial = await readFixture();
|
|
|
|
|
|
|
|
|
|
await page.locator("#short-next").click();
|
|
|
|
|
await expect(page).toHaveURL((url) => url.pathname === `/shorts/${destination.videoId}`);
|
|
|
|
|
expect(await readFixture()).toMatchObject({
|
|
|
|
|
actionBarConnected: true,
|
|
|
|
|
actionBarDataReady: false,
|
|
|
|
|
actionBarDataWriteCount: 0,
|
|
|
|
|
actionBarReplaced: true,
|
|
|
|
|
actionNames: ["like", "dislike", "comments", "share", "remix", "pivot"],
|
|
|
|
|
currentActionButtonCount: 6,
|
|
|
|
|
currentNativeChildCount: 6,
|
|
|
|
|
currentSyntheticCount: 0,
|
|
|
|
|
documentIdentity: initial.documentIdentity,
|
|
|
|
|
exactHrefRendererCount: 1,
|
|
|
|
|
hitTestedActionButtonCount: 6,
|
|
|
|
|
href: `/shorts/${destination.videoId}`,
|
|
|
|
|
isActive: false,
|
|
|
|
|
nativeChildrenStable: true,
|
|
|
|
|
nativeDislikeCount: 1,
|
|
|
|
|
nativeDislikeText: "",
|
|
|
|
|
navigateFinishes: 0,
|
|
|
|
|
navigateStarts: 1,
|
|
|
|
|
nonRenderedActionButtonCount: 0,
|
|
|
|
|
originActionBarConnected: false,
|
|
|
|
|
originSyntheticConnected: false,
|
|
|
|
|
rendererReused: true,
|
|
|
|
|
rendererVideoId: null,
|
|
|
|
|
timeline: expectedTimeline,
|
|
|
|
|
visibleActionButtonCount: 6,
|
|
|
|
|
visibleRendererCount: 1,
|
|
|
|
|
});
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
|
expectNoInteractions();
|
|
|
|
|
|
|
|
|
|
await page.waitForTimeout(scenario.timing.unsafeWindowMs);
|
|
|
|
|
expect(await readFixture()).toMatchObject({
|
|
|
|
|
actionBarDataReady: false,
|
|
|
|
|
actionBarDataWriteCount: 0,
|
|
|
|
|
actionNames: ["like", "dislike", "comments", "share", "remix", "pivot"],
|
|
|
|
|
currentActionButtonCount: 6,
|
|
|
|
|
currentNativeChildCount: 6,
|
|
|
|
|
currentSyntheticCount: 0,
|
|
|
|
|
hitTestedActionButtonCount: 6,
|
|
|
|
|
nativeChildrenStable: true,
|
|
|
|
|
nativeDislikeCount: 1,
|
|
|
|
|
nativeDislikeText: "",
|
|
|
|
|
nonRenderedActionButtonCount: 0,
|
|
|
|
|
visibleActionButtonCount: 6,
|
|
|
|
|
});
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
|
expectNoInteractions();
|
|
|
|
|
|
|
|
|
|
const readiness = await waitForOwnedSurfaceWithinBudget(page, runtime, destination, scenario.timing.maxFirstValidMs);
|
|
|
|
|
expect(readiness.firstValidMs).toBeLessThan(scenario.timing.maxFirstValidMs);
|
|
|
|
|
await expectOwnedShortControl(
|
|
|
|
|
page,
|
|
|
|
|
runtime,
|
|
|
|
|
destination.videoId,
|
|
|
|
|
destination.counts,
|
|
|
|
|
destination.shortIdentity,
|
|
|
|
|
destination.shortsDislikeControl,
|
|
|
|
|
);
|
|
|
|
|
const initialized = await readFixture();
|
|
|
|
|
expect(initialized).toMatchObject({
|
|
|
|
|
actionBarConnected: true,
|
|
|
|
|
actionBarDataReady: false,
|
|
|
|
|
actionBarDataWriteCount: 0,
|
|
|
|
|
actionBarReplaced: true,
|
|
|
|
|
actionNames: ["like", "dislike", "comments", "share", "remix", "pivot"],
|
|
|
|
|
blockedActivationConnected: false,
|
|
|
|
|
currentActionButtonCount: 6,
|
|
|
|
|
currentNativeChildCount: 6,
|
|
|
|
|
currentSyntheticCount: 0,
|
|
|
|
|
documentIdentity: initial.documentIdentity,
|
|
|
|
|
exactHrefRendererCount: 1,
|
|
|
|
|
hitTestedActionButtonCount: 6,
|
|
|
|
|
href: `/shorts/${destination.videoId}`,
|
|
|
|
|
isActive: false,
|
|
|
|
|
nativeChildrenStable: true,
|
|
|
|
|
nativeDislikeCount: 1,
|
|
|
|
|
nativeDislikeText: String(destination.counts.dislikes),
|
|
|
|
|
navigateFinishes: 0,
|
|
|
|
|
navigateStarts: 1,
|
|
|
|
|
nonRenderedActionButtonCount: 0,
|
|
|
|
|
originActionBarConnected: false,
|
|
|
|
|
originSyntheticConnected: false,
|
|
|
|
|
rendererReused: true,
|
|
|
|
|
rendererVideoId: null,
|
|
|
|
|
timeline: expectedTimeline,
|
|
|
|
|
visibleActionButtonCount: 6,
|
|
|
|
|
visibleRendererCount: 1,
|
|
|
|
|
});
|
|
|
|
|
expect(initialized.actionBarBounds?.height).toBeGreaterThan(0);
|
|
|
|
|
expect(initialized.actionBarBounds?.width).toBeGreaterThan(0);
|
|
|
|
|
expect(initialized.actionBarDataReadCount).toBeGreaterThan(0);
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
|
|
|
expectNoInteractions();
|
|
|
|
|
|
|
|
|
|
await assertOwnedSurfaceContinuously(page, runtime, destination);
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
|
|
|
expectNoInteractions();
|
|
|
|
|
expect(await readFixture()).toMatchObject({
|
|
|
|
|
actionBarDataReady: false,
|
|
|
|
|
actionBarDataWriteCount: 0,
|
|
|
|
|
currentNativeChildCount: 6,
|
|
|
|
|
currentSyntheticCount: 0,
|
|
|
|
|
nativeChildrenStable: true,
|
|
|
|
|
nativeDislikeCount: 1,
|
|
|
|
|
nativeDislikeText: String(destination.counts.dislikes),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function runShortReplacedRootStartOnlyScenario({ backend, page, runtime, scenario }) {
|
|
|
|
|
const { destination, origin } = scenario;
|
|
|
|
|
const repeatedStart = scenario.transition.navigateStarts === "before-and-after-route";
|
|
|
|
|
const expectedRouteTimeline = repeatedStart
|
|
|
|
|
? ["navigate-start-a", "route-and-root-without-action-bar", "navigate-start-b"]
|
|
|
|
|
: ["navigate-start", "route-and-root-without-action-bar"];
|
|
|
|
|
|
|
|
|
|
await expectOwnedShortControl(page, runtime, origin.videoId, origin.counts);
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
|
const initial = await page.evaluate(() => globalThis.__navigationMatrixReplacedRootShortFixture.snapshot());
|
|
|
|
|
|
|
|
|
|
await page.locator("#short-next").click();
|
|
|
|
|
await expect(page).toHaveURL((url) => url.pathname === `/shorts/${destination.videoId}`);
|
|
|
|
|
expect(await page.evaluate(() => globalThis.__navigationMatrixReplacedRootShortFixture.snapshot())).toEqual({
|
|
|
|
|
actionBarConnected: false,
|
|
|
|
|
actionBarDataReadCount: 0,
|
|
|
|
|
actionBarDataReady: false,
|
|
|
|
|
actionBarFirstDataReadDelayMs: null,
|
|
|
|
|
currentActionButtonCount: 0,
|
|
|
|
|
currentNativeChildCount: 0,
|
|
|
|
|
currentSyntheticCount: 0,
|
|
|
|
|
currentSyntheticVideoIds: [],
|
|
|
|
|
destinationRendererConnected: true,
|
|
|
|
|
destinationRendererVisible: true,
|
|
|
|
|
destinationRootConnected: true,
|
|
|
|
|
documentIdentity: initial.documentIdentity,
|
|
|
|
|
navigateFinishes: 0,
|
|
|
|
|
navigateStarts: repeatedStart ? 2 : 1,
|
|
|
|
|
originRootConnected: false,
|
|
|
|
|
originSyntheticConnected: false,
|
|
|
|
|
rendererVideoId: destination.videoId,
|
|
|
|
|
timeline: expectedRouteTimeline,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Cross the periodic lifecycle boundary while the selected destination has
|
|
|
|
|
// no action root at all. This is the exact state that previously deadlocked
|
|
|
|
|
// the extension after its observer stayed attached to the detached A root.
|
|
|
|
|
await page.waitForTimeout(650);
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
|
await expect(page.locator("[data-ryd-synthetic-shorts-dislike]")).toHaveCount(0);
|
|
|
|
|
|
|
|
|
|
await page.evaluate(() => globalThis.__navigationMatrixReplacedRootShortFixture.appendEmptyActionBar());
|
|
|
|
|
await expect
|
|
|
|
|
.poll(
|
|
|
|
|
() =>
|
|
|
|
|
page.evaluate(() => globalThis.__navigationMatrixReplacedRootShortFixture.snapshot().actionBarDataReadCount),
|
|
|
|
|
{
|
|
|
|
|
intervals: [25],
|
|
|
|
|
message: `${runtime.name} must observe the replacement Shorts root before the next 500ms watchdog tick`,
|
|
|
|
|
timeout: 225,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.toBeGreaterThan(0);
|
|
|
|
|
expect(await page.evaluate(() => globalThis.__navigationMatrixReplacedRootShortFixture.snapshot())).toMatchObject({
|
|
|
|
|
actionBarConnected: true,
|
|
|
|
|
actionBarDataReadCount: expect.any(Number),
|
|
|
|
|
actionBarDataReady: false,
|
|
|
|
|
actionBarFirstDataReadDelayMs: expect.any(Number),
|
|
|
|
|
currentActionButtonCount: 0,
|
|
|
|
|
currentNativeChildCount: 0,
|
|
|
|
|
currentSyntheticCount: 0,
|
|
|
|
|
timeline: [...expectedRouteTimeline, "append-empty-data-null-action-bar"],
|
|
|
|
|
});
|
|
|
|
|
expect(
|
|
|
|
|
await page.evaluate(
|
|
|
|
|
() => globalThis.__navigationMatrixReplacedRootShortFixture.snapshot().actionBarFirstDataReadDelayMs,
|
|
|
|
|
),
|
|
|
|
|
).toBeLessThanOrEqual(175);
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
|
|
|
|
|
|
await page.evaluate(() => globalThis.__navigationMatrixReplacedRootShortFixture.appendNativeChildren());
|
|
|
|
|
await page.waitForTimeout(350);
|
|
|
|
|
expect(await page.evaluate(() => globalThis.__navigationMatrixReplacedRootShortFixture.snapshot())).toMatchObject({
|
|
|
|
|
actionBarConnected: true,
|
|
|
|
|
actionBarDataReadCount: expect.any(Number),
|
|
|
|
|
actionBarDataReady: false,
|
|
|
|
|
actionBarFirstDataReadDelayMs: expect.any(Number),
|
|
|
|
|
currentActionButtonCount: 5,
|
|
|
|
|
currentNativeChildCount: 5,
|
|
|
|
|
currentSyntheticCount: 0,
|
|
|
|
|
timeline: [
|
|
|
|
|
...expectedRouteTimeline,
|
|
|
|
|
"append-empty-data-null-action-bar",
|
|
|
|
|
"append-complete-native-inventory-data-null",
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
|
|
|
|
|
|
await page.evaluate(() => globalThis.__navigationMatrixReplacedRootShortFixture.hydrateActionBar());
|
|
|
|
|
await waitForOwnedSurfaceWithinBudget(page, runtime, destination);
|
|
|
|
|
await expectOwnedShortControl(page, runtime, destination.videoId, destination.counts);
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
|
|
|
expect(await page.evaluate(() => globalThis.__navigationMatrixReplacedRootShortFixture.snapshot())).toEqual({
|
|
|
|
|
actionBarConnected: true,
|
|
|
|
|
actionBarDataReadCount: expect.any(Number),
|
|
|
|
|
actionBarDataReady: true,
|
|
|
|
|
actionBarFirstDataReadDelayMs: expect.any(Number),
|
|
|
|
|
currentActionButtonCount: 6,
|
|
|
|
|
currentNativeChildCount: 5,
|
|
|
|
|
currentSyntheticCount: 1,
|
|
|
|
|
currentSyntheticVideoIds: [destination.videoId],
|
|
|
|
|
destinationRendererConnected: true,
|
|
|
|
|
destinationRendererVisible: true,
|
|
|
|
|
destinationRootConnected: true,
|
|
|
|
|
documentIdentity: initial.documentIdentity,
|
|
|
|
|
navigateFinishes: 0,
|
|
|
|
|
navigateStarts: repeatedStart ? 2 : 1,
|
|
|
|
|
originRootConnected: false,
|
|
|
|
|
originSyntheticConnected: false,
|
|
|
|
|
rendererVideoId: destination.videoId,
|
|
|
|
|
timeline: [
|
|
|
|
|
...expectedRouteTimeline,
|
|
|
|
|
"append-empty-data-null-action-bar",
|
|
|
|
|
"append-complete-native-inventory-data-null",
|
|
|
|
|
"hydrate-action-bar-data",
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await assertOwnedSurfaceContinuously(page, runtime, destination);
|
|
|
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-01 23:41:28 +02:00
|
|
|
const SCENARIO_RUNNERS = {
|
|
|
|
|
"short-next-short-active-reel": runShortNextScenario,
|
2026-09-06 12:51:12 +02:00
|
|
|
"short-next-short-active-reel-compact": runShortNextScenario,
|
|
|
|
|
"short-next-short-active-reel-medium": runShortNextScenario,
|
|
|
|
|
"short-next-short-exact-href-with-description-crosslink": runShortSameRendererStartOnlyScenario,
|
|
|
|
|
"short-next-short-persistent-data-null-native-dislike": runShortPersistentDataNullNativeDislikeScenario,
|
|
|
|
|
"short-next-short-persistent-data-null-nonrendered-native-stays-inert": runShortPersistentDataNullInertScenario,
|
|
|
|
|
"short-next-short-replace-root-repeated-start-no-finish-staged-hydration": runShortReplacedRootStartOnlyScenario,
|
|
|
|
|
"short-next-short-replace-root-start-no-finish-staged-hydration": runShortReplacedRootStartOnlyScenario,
|
|
|
|
|
"short-next-short-reuse-renderer-replace-action-root-exact-href-persistent-data-null":
|
|
|
|
|
runShortPersistentDataNullScenario,
|
|
|
|
|
"short-next-short-reuse-renderer-repeated-start-no-finish": runShortSameRendererStartOnlyScenario,
|
|
|
|
|
"short-next-short-reuse-renderer-start-no-finish": runShortSameRendererStartOnlyScenario,
|
2026-09-01 23:41:28 +02:00
|
|
|
"short-direct-watch-delayed": runCrossSurfaceScenario,
|
|
|
|
|
"watch-autoplay-watch-replace-no-finish": runWatchAutoplayScenario,
|
2026-09-06 12:51:12 +02:00
|
|
|
"watch-navigate-start-watch-replace-no-finish": runWatchStartWithoutFinishScenario,
|
2026-09-01 23:41:28 +02:00
|
|
|
"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,
|
|
|
|
|
};
|
2026-09-06 12:51:12 +02:00
|
|
|
const NAVIGATION_SCENARIO_RUNNER_IDS = Object.freeze(Object.keys(SCENARIO_RUNNERS));
|
2026-09-01 23:41:28 +02:00
|
|
|
|
|
|
|
|
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 = {
|
2026-09-06 12:51:12 +02:00
|
|
|
EXTENSION_MATRIX_RUNTIME,
|
2026-09-01 23:41:28 +02:00
|
|
|
NAVIGATION_MATRIX,
|
2026-09-06 12:51:12 +02:00
|
|
|
NAVIGATION_SCENARIO_RUNNER_IDS,
|
|
|
|
|
NO_DESTINATION_DISLIKE_POSTCONDITION,
|
|
|
|
|
SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
2026-09-01 23:41:28 +02:00
|
|
|
USERSCRIPT_MATRIX_RUNTIME,
|
|
|
|
|
WATCH_SIDEBAR_MATRIX,
|
2026-09-06 12:51:12 +02:00
|
|
|
getDestinationDislikePostconditionTarget,
|
2026-09-01 23:41:28 +02:00
|
|
|
installNavigationMatrixFixture,
|
|
|
|
|
runNavigationMatrixScenario,
|
|
|
|
|
};
|