mirror of
https://github.com/Alvin-Zilverstand/return-youtube-dislike.git
synced 2026-09-12 09:58:20 +02:00
e500088e65
Add Firefox data consent, repair contributor sign-in, and introduce optional Hide clutter buttons. Harden video navigation, voting, and action layouts with shared browser regression coverage and reproducible release packaging.
3499 lines
151 KiB
JavaScript
3499 lines
151 KiB
JavaScript
const { expect } = require("@playwright/test");
|
|
const { assertInvariantContinuously, waitForStableInvariant } = require("../../e2e/continuous-invariants");
|
|
const { assertExactSuccessfulVotesTraffic } = require("../../e2e/hermetic-api-contract");
|
|
const { VIDEO_A, VIDEO_B } = require("./harness");
|
|
|
|
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")}`;
|
|
}
|
|
|
|
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 },
|
|
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",
|
|
},
|
|
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,
|
|
},
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
timing: {
|
|
destinationCount: "gated",
|
|
navigateFinish: "before-destination-controls",
|
|
},
|
|
transition: {
|
|
destinationControls: "replace",
|
|
outgoing: "retain-hidden-top-row",
|
|
postInit: "prune-current-rate-bar",
|
|
trigger: "persistent-sidebar-link",
|
|
},
|
|
viewport: { height: 720, width: 1280 },
|
|
},
|
|
{
|
|
coverage: {
|
|
destination: "watch",
|
|
dom: [
|
|
"same-current-root",
|
|
"reuse-exact-control-nodes",
|
|
"no-useful-control-mutation",
|
|
"same-compact-dislike-text",
|
|
"different-exact-dislike-count",
|
|
"different-rate-bar-ratio",
|
|
],
|
|
origin: "watch",
|
|
timing: ["navigate-finish", "destination-count-gated"],
|
|
trigger: "sidebar-link",
|
|
width: "wide-desktop",
|
|
},
|
|
destination: {
|
|
counts: {
|
|
dislikes: 1_409,
|
|
displayedDislikes: "1.4K",
|
|
displayedDislikesAfterIncrement: "1.4K",
|
|
likes: 3_591,
|
|
},
|
|
kind: "watch",
|
|
videoId: VIDEO_B,
|
|
},
|
|
id: "watch-sidebar-watch-same-node-route-complete",
|
|
origin: {
|
|
counts: { dislikes: 1_401, displayedDislikes: "1.4K", likes: 8_599 },
|
|
kind: "watch",
|
|
state: "neutral",
|
|
videoId: VIDEO_A,
|
|
},
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
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",
|
|
"missing-rate-bar-video-owner",
|
|
"stale-rate-bar-video-owner",
|
|
"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 },
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
transition: {
|
|
corruptions: [
|
|
"hidden-wrapper",
|
|
"collapsed-wrapper",
|
|
"missing-fill",
|
|
"missing-video-owner",
|
|
"stale-video-owner",
|
|
"stripped-wrapper-class",
|
|
],
|
|
},
|
|
viewport: { height: 720, width: 1280 },
|
|
},
|
|
];
|
|
|
|
const NAVIGATION_MATRIX = [
|
|
...WATCH_SIDEBAR_MATRIX,
|
|
{
|
|
coverage: {
|
|
destination: "watch",
|
|
dom: ["same-current-root", "hidden-outgoing-first", "rendered-destination-second"],
|
|
origin: "watch",
|
|
timing: ["settled"],
|
|
trigger: "sidebar-link",
|
|
width: "wide-desktop",
|
|
},
|
|
destination: { counts: { dislikes: 300, likes: 100 }, kind: "watch", videoId: VIDEO_B },
|
|
id: "watch-sidebar-watch-same-root-hidden-first",
|
|
origin: { counts: { dislikes: 100, likes: 300 }, kind: "watch", state: "neutral", videoId: VIDEO_A },
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
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 },
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
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 },
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
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 },
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
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 },
|
|
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,
|
|
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 },
|
|
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 },
|
|
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 },
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
timing: { controlDelayMs: 600 },
|
|
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 },
|
|
postcondition: SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
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",
|
|
watchDislikeCount: "#text",
|
|
wrapperVideoAttribute: "data-ryd-video-id",
|
|
},
|
|
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",
|
|
},
|
|
tooltipText: expectedTooltipText,
|
|
};
|
|
|
|
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);
|
|
globalThis.__navigationFixture.setNativeLikeCount(destinationControls, matrixScenario.destination.videoId);
|
|
for (const role of ["like", "dislike"]) {
|
|
const control = destinationControls.querySelector(`[data-fixture-role="${role}"]`);
|
|
control.classList.remove("style-default-active");
|
|
control.classList.add("style-text");
|
|
control.querySelector("button")?.setAttribute("aria-pressed", "false");
|
|
}
|
|
destinationControls
|
|
.querySelectorAll(
|
|
'[data-fixture-role="dislike"] .ytSpecButtonShapeNextButtonTextContent, [data-fixture-role="dislike"] #text',
|
|
)
|
|
.forEach((element) => element.remove());
|
|
transition.destinationTopRow = destinationTopRow;
|
|
|
|
document.dispatchEvent(new Event("yt-navigate-start", { bubbles: true }));
|
|
record("navigate-start");
|
|
|
|
retainedTrees.appendChild(outgoingTopRow);
|
|
history.pushState({}, "", `/watch?v=${matrixScenario.destination.videoId}&rydNavigationFixture=1`);
|
|
watchPage.setAttribute("data-fixture-video-id", matrixScenario.destination.videoId);
|
|
watchFlexy.setAttribute("video-id", matrixScenario.destination.videoId);
|
|
record("route-and-shell");
|
|
|
|
document.dispatchEvent(new Event("yt-navigate-finish", { bubbles: true }));
|
|
record("navigate-finish");
|
|
});
|
|
|
|
globalThis.__navigationMatrixFixture = {
|
|
detachOutgoing() {
|
|
retainedTrees.replaceChildren();
|
|
record("detach-outgoing");
|
|
},
|
|
hydrateDestination() {
|
|
if (transition.phase !== "navigate-finish" || !transition.destinationTopRow) {
|
|
throw new Error(`Cannot hydrate the destination from matrix phase ${transition.phase}.`);
|
|
}
|
|
const watchPage = fixturePage.querySelector('[data-fixture-page-kind="watch"]');
|
|
const watchFlexy = watchPage?.querySelector("ytd-watch-flexy");
|
|
if (!watchFlexy) throw new Error("The destination Watch root is unavailable during hydration.");
|
|
watchFlexy.appendChild(transition.destinationTopRow);
|
|
transition.destinationTopRow = null;
|
|
record("hydrate-destination-controls");
|
|
},
|
|
snapshot() {
|
|
return {
|
|
currentControls: fixturePage.querySelectorAll("[data-fixture-control-video-id]").length,
|
|
documentIdentity: transition.documentIdentity,
|
|
phase: transition.phase,
|
|
retainedControls: retainedTrees.querySelectorAll("[data-fixture-control-video-id]").length,
|
|
retainedTreesConnected: retainedTrees.isConnected,
|
|
retainedTreesHidden: retainedTrees.hidden,
|
|
sidebarConnected: sidebar.isConnected,
|
|
timeline: [...transition.timeline],
|
|
};
|
|
},
|
|
};
|
|
},
|
|
{ once: true },
|
|
);
|
|
}, scenario);
|
|
}
|
|
|
|
async function installSameRootHiddenFirstFixture(context, scenario) {
|
|
await context.addInitScript((matrixScenario) => {
|
|
if (!location.hostname.endsWith("youtube.com")) return;
|
|
|
|
addEventListener(
|
|
"DOMContentLoaded",
|
|
() => {
|
|
if (new URL(location.href).searchParams.get("rydNavigationFixture") !== "1") return;
|
|
|
|
const fixturePage = document.getElementById("fixture-page");
|
|
const watchPage = fixturePage?.querySelector('[data-fixture-page-kind="watch"]');
|
|
const watchFlexy = watchPage?.querySelector("ytd-watch-flexy");
|
|
const originTopRow = watchPage?.querySelector("#top-row");
|
|
if (!fixturePage || !watchPage || !watchFlexy || !originTopRow) {
|
|
throw new Error("The same-root matrix requires a complete watch fixture.");
|
|
}
|
|
if (matrixScenario.transition?.controlMarkup === "legacy-segmented") {
|
|
const modernSegmented = originTopRow.querySelector("segmented-like-dislike-button-view-model");
|
|
if (!modernSegmented) {
|
|
throw new Error("The legacy segmented matrix could not find its source controls.");
|
|
}
|
|
const legacySegmented = document.createElement("ytd-segmented-like-dislike-button-renderer");
|
|
for (const attribute of modernSegmented.attributes) {
|
|
legacySegmented.setAttribute(attribute.name, attribute.value);
|
|
}
|
|
legacySegmented.style.cssText = "display:flex;gap:8px;min-height:48px;width:320px";
|
|
while (modernSegmented.firstChild) {
|
|
legacySegmented.appendChild(modernSegmented.firstChild);
|
|
}
|
|
const legacyLike = legacySegmented.querySelector('[data-fixture-role="like"]');
|
|
const legacyDislike = legacySegmented.querySelector('[data-fixture-role="dislike"]');
|
|
legacyLike.id = "segmented-like-button";
|
|
legacyDislike.id = "segmented-dislike-button";
|
|
for (const button of legacySegmented.querySelectorAll("button")) {
|
|
button.style.cssText = "min-height:36px;min-width:96px";
|
|
}
|
|
modernSegmented.replaceWith(legacySegmented);
|
|
}
|
|
watchFlexy.appendChild(originTopRow);
|
|
|
|
const sidebar = document.createElement("aside");
|
|
sidebar.id = "fixture-matrix-same-root-sidebar";
|
|
sidebar.setAttribute("aria-label", "Fixture same-root sidebar");
|
|
const destinationLink = document.createElement("a");
|
|
destinationLink.id = "fixture-matrix-same-root-watch";
|
|
destinationLink.href = `/watch?v=${matrixScenario.destination.videoId}&rydNavigationFixture=1`;
|
|
destinationLink.textContent = "Open same-root sidebar video";
|
|
sidebar.appendChild(destinationLink);
|
|
fixturePage.before(sidebar);
|
|
|
|
const documentIdentity = `matrix-${Date.now()}-${Math.random()}`;
|
|
destinationLink.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
const outgoingTopRow = watchFlexy.querySelector("#top-row");
|
|
if (!outgoingTopRow || outgoingTopRow.hasAttribute("data-fixture-matrix-hidden-outgoing")) {
|
|
throw new Error("The same-root matrix origin is unavailable.");
|
|
}
|
|
|
|
const destinationTopRow = outgoingTopRow.cloneNode(true);
|
|
destinationTopRow.removeAttribute("style");
|
|
destinationTopRow.querySelectorAll(".ryd-tooltip").forEach((element) => element.remove());
|
|
const destinationControls = destinationTopRow.querySelector("[data-fixture-control-video-id]");
|
|
destinationControls.setAttribute("data-fixture-control-video-id", matrixScenario.destination.videoId);
|
|
globalThis.__navigationFixture.setNativeLikeCount(destinationControls, matrixScenario.destination.videoId);
|
|
for (const role of ["like", "dislike"]) {
|
|
const control = destinationControls.querySelector(`[data-fixture-role="${role}"]`);
|
|
control.classList.remove("style-default-active");
|
|
control.classList.add("style-text");
|
|
control.querySelector("button")?.setAttribute("aria-pressed", "false");
|
|
}
|
|
destinationControls
|
|
.querySelectorAll(
|
|
'[data-fixture-role="dislike"] .ytSpecButtonShapeNextButtonTextContent, [data-fixture-role="dislike"] #text',
|
|
)
|
|
.forEach((element) => element.remove());
|
|
destinationTopRow.setAttribute("data-fixture-matrix-live-destination", "true");
|
|
|
|
document.dispatchEvent(new Event("yt-navigate-start", { bubbles: true }));
|
|
if (matrixScenario.transition?.outgoingPresentation === "offscreen") {
|
|
outgoingTopRow.style.setProperty("left", "-10000px", "important");
|
|
outgoingTopRow.style.setProperty("position", "fixed", "important");
|
|
outgoingTopRow.style.setProperty("top", "0", "important");
|
|
} else {
|
|
outgoingTopRow.hidden = true;
|
|
}
|
|
outgoingTopRow.setAttribute("data-fixture-matrix-hidden-outgoing", "true");
|
|
history.pushState({}, "", `/watch?v=${matrixScenario.destination.videoId}&rydNavigationFixture=1`);
|
|
watchPage.setAttribute("data-fixture-video-id", matrixScenario.destination.videoId);
|
|
watchFlexy.setAttribute("video-id", matrixScenario.destination.videoId);
|
|
watchFlexy.appendChild(destinationTopRow);
|
|
document.dispatchEvent(new Event("yt-navigate-finish", { bubbles: true }));
|
|
});
|
|
|
|
globalThis.__navigationMatrixSameRootFixture = {
|
|
detachOutgoing() {
|
|
watchFlexy.querySelector('[data-fixture-matrix-hidden-outgoing="true"]')?.remove();
|
|
},
|
|
snapshot() {
|
|
const hiddenOutgoing = watchFlexy.querySelector('[data-fixture-matrix-hidden-outgoing="true"]');
|
|
const liveDestination = watchFlexy.querySelector('[data-fixture-matrix-live-destination="true"]');
|
|
const hiddenBox = hiddenOutgoing?.getBoundingClientRect();
|
|
return {
|
|
documentIdentity,
|
|
hiddenFirst: Boolean(
|
|
hiddenOutgoing &&
|
|
liveDestination &&
|
|
hiddenOutgoing.compareDocumentPosition(liveDestination) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
),
|
|
hiddenOutgoingConnected: Boolean(hiddenOutgoing?.isConnected),
|
|
hiddenOutgoingHeight: hiddenBox?.height ?? null,
|
|
hiddenOutgoingWidth: hiddenBox?.width ?? null,
|
|
outgoingIntersectsViewport: Boolean(
|
|
hiddenBox &&
|
|
hiddenBox.width > 0 &&
|
|
hiddenBox.height > 0 &&
|
|
hiddenBox.bottom > 0 &&
|
|
hiddenBox.right > 0 &&
|
|
hiddenBox.top < innerHeight &&
|
|
hiddenBox.left < innerWidth,
|
|
),
|
|
outgoingPresentation: matrixScenario.transition?.outgoingPresentation ?? "hidden",
|
|
liveDestinationConnected: Boolean(liveDestination?.isConnected),
|
|
rootVideoId: watchFlexy.getAttribute("video-id"),
|
|
sameRoot: Boolean(
|
|
hiddenOutgoing &&
|
|
liveDestination &&
|
|
hiddenOutgoing.closest("ytd-watch-flexy") === liveDestination.closest("ytd-watch-flexy"),
|
|
),
|
|
sidebarConnected: sidebar.isConnected,
|
|
};
|
|
},
|
|
};
|
|
},
|
|
{ once: true },
|
|
);
|
|
}, scenario);
|
|
}
|
|
|
|
async function installSameNodeRouteCompletionFixture(context, scenario) {
|
|
await context.addInitScript((matrixScenario) => {
|
|
if (!location.hostname.endsWith("youtube.com")) return;
|
|
|
|
addEventListener(
|
|
"DOMContentLoaded",
|
|
() => {
|
|
if (new URL(location.href).searchParams.get("rydNavigationFixture") !== "1") return;
|
|
|
|
const fixturePage = document.getElementById("fixture-page");
|
|
const watchPage = fixturePage?.querySelector('[data-fixture-page-kind="watch"]');
|
|
const watchFlexy = watchPage?.querySelector("ytd-watch-flexy");
|
|
const topRow = watchPage?.querySelector("#top-row");
|
|
const buttons = topRow?.querySelector("#top-level-buttons-computed");
|
|
const controls = buttons?.querySelector("[data-fixture-control-video-id]");
|
|
const like = controls?.querySelector('[data-fixture-role="like"]');
|
|
const dislike = controls?.querySelector('[data-fixture-role="dislike"]');
|
|
if (!fixturePage || !watchPage || !watchFlexy || !topRow || !buttons || !controls || !like || !dislike) {
|
|
throw new Error("The same-node matrix requires a complete watch fixture.");
|
|
}
|
|
watchFlexy.appendChild(topRow);
|
|
|
|
const sidebar = document.createElement("aside");
|
|
sidebar.id = "fixture-matrix-same-node-sidebar";
|
|
sidebar.setAttribute("aria-label", "Fixture same-node sidebar");
|
|
const destinationLink = document.createElement("a");
|
|
destinationLink.id = "fixture-matrix-same-node-watch";
|
|
destinationLink.href = `/watch?v=${matrixScenario.destination.videoId}&rydNavigationFixture=1`;
|
|
destinationLink.textContent = "Open same-node sidebar video";
|
|
sidebar.appendChild(destinationLink);
|
|
fixturePage.before(sidebar);
|
|
|
|
const documentIdentity = `matrix-${Date.now()}-${Math.random()}`;
|
|
const timeline = [];
|
|
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.");
|
|
}
|
|
|
|
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.");
|
|
}
|
|
|
|
const documentIdentity = `matrix-${Date.now()}-${Math.random()}`;
|
|
const repeatedStart = matrixScenario.transition.navigateStarts === "before-and-after-route";
|
|
const timeline = [];
|
|
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.");
|
|
}
|
|
|
|
document.dispatchEvent(new Event("yt-navigate-start", { bubbles: true }));
|
|
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");
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
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,
|
|
snapshot() {
|
|
const currentSyntheticDislikes = Array.from(
|
|
document.querySelectorAll("[data-ryd-synthetic-shorts-dislike]"),
|
|
);
|
|
return {
|
|
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,
|
|
documentIdentity,
|
|
navigateFinishes: 0,
|
|
navigateStarts: repeatedStart ? 2 : 1,
|
|
originRootConnected: originRoot.isConnected,
|
|
originSyntheticConnected: originSyntheticDislike?.isConnected ?? false,
|
|
rendererVideoId: destinationRenderer?.getAttribute("video-id") ?? null,
|
|
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,
|
|
shortActiveMutations: [],
|
|
};
|
|
document.addEventListener("yt-navigate-start", () => {
|
|
probe.navigateStarts += 1;
|
|
});
|
|
document.addEventListener("yt-navigate-finish", () => {
|
|
probe.navigateFinishes += 1;
|
|
});
|
|
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,
|
|
});
|
|
|
|
const delayedLinkId = {
|
|
"short-direct-watch-delayed": "short-to-watch",
|
|
"watch-direct-short-delayed": "watch-to-short",
|
|
}[matrixScenario.id];
|
|
if (delayedLinkId) {
|
|
document
|
|
.getElementById(delayedLinkId)
|
|
?.setAttribute("data-fixture-control-delay", String(matrixScenario.timing.controlDelayMs));
|
|
}
|
|
|
|
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);
|
|
globalThis.__navigationFixture.setNativeLikeCount(controls, videoId);
|
|
for (const role of ["like", "dislike"]) {
|
|
const control = controls.querySelector(`[data-fixture-role="${role}"]`);
|
|
control.classList.remove("style-default-active");
|
|
control.classList.add("style-text");
|
|
control.querySelector("button")?.setAttribute("aria-pressed", "false");
|
|
}
|
|
controls
|
|
.querySelectorAll(
|
|
'[data-fixture-role="dislike"] .ytSpecButtonShapeNextButtonTextContent, [data-fixture-role="dislike"] #text',
|
|
)
|
|
.forEach((element) => element.remove());
|
|
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,
|
|
shortActiveMutations: [...probe.shortActiveMutations],
|
|
transitionPending: fixturePage.dataset.fixtureTransitionPending ?? null,
|
|
};
|
|
},
|
|
};
|
|
},
|
|
{ once: true },
|
|
);
|
|
}, scenario);
|
|
}
|
|
|
|
async function installNavigationMatrixFixture(context, scenario) {
|
|
if (!NAVIGATION_MATRIX.some((candidate) => candidate.id === scenario.id)) {
|
|
throw new Error(`No navigation matrix fixture is registered for ${scenario.id}.`);
|
|
}
|
|
if (
|
|
scenario.id === "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;
|
|
}
|
|
if (scenario.id === "watch-sidebar-watch-retain-prune") {
|
|
await installSidebarRetainPruneFixture(context, scenario);
|
|
return;
|
|
}
|
|
if (
|
|
scenario.id === "watch-sidebar-watch-same-root-hidden-first" ||
|
|
scenario.id === "watch-sidebar-watch-same-root-offscreen-first" ||
|
|
scenario.id === "watch-sidebar-watch-legacy-segmented-duplicate-ids"
|
|
) {
|
|
await installSameRootHiddenFirstFixture(context, scenario);
|
|
return;
|
|
}
|
|
if (scenario.id === "watch-sidebar-watch-same-node-route-complete") {
|
|
await installSameNodeRouteCompletionFixture(context, scenario);
|
|
return;
|
|
}
|
|
await installStandardNavigationMatrixFixture(context, scenario);
|
|
}
|
|
|
|
function currentWatchLocators(page, runtime, videoId) {
|
|
const 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}"]`);
|
|
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),
|
|
watchRoot,
|
|
wrapper,
|
|
};
|
|
}
|
|
|
|
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']"),
|
|
};
|
|
}
|
|
|
|
async function expectOwnedWatchBar(page, runtime, videoId, counts) {
|
|
const locators = currentWatchLocators(page, runtime, videoId);
|
|
const currentCount = locators.controls
|
|
.locator('[data-fixture-role="dislike"]')
|
|
.locator(runtime.selectors.watchDislikeCount);
|
|
await expect(locators.controls).toHaveCount(1);
|
|
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));
|
|
await expect(locators.wrapper).toHaveCount(1);
|
|
if (runtime.selectors.wrapperVideoAttribute) {
|
|
await expect(locators.wrapper).toHaveAttribute(runtime.selectors.wrapperVideoAttribute, videoId);
|
|
}
|
|
await expect(locators.wrapper).toBeVisible();
|
|
await expect(locators.container).toBeVisible();
|
|
await expect(locators.bar).toBeVisible();
|
|
await expect(locators.tooltip).toContainText(runtime.tooltipText(counts));
|
|
await expect(page.locator(runtime.selectors.wrapper)).toHaveCount(1);
|
|
await expect(page.locator(runtime.selectors.container)).toHaveCount(1);
|
|
await expect(page.locator(runtime.selectors.bar)).toHaveCount(1);
|
|
await expect(page.locator(runtime.selectors.tooltip)).toHaveCount(1);
|
|
|
|
const geometry = await locators.reactionRegion.evaluate((reactionRegion, selectors) => {
|
|
const box = (element) => {
|
|
const bounds = element.getBoundingClientRect();
|
|
return {
|
|
bottom: bounds.bottom,
|
|
left: bounds.left,
|
|
right: bounds.right,
|
|
top: bounds.top,
|
|
width: bounds.width,
|
|
};
|
|
};
|
|
const like = reactionRegion.querySelector('[data-fixture-role="like"] button');
|
|
const dislike = reactionRegion.querySelector('[data-fixture-role="dislike"] button');
|
|
const wrapper = reactionRegion.querySelector(`:scope > ${selectors.wrapper}`);
|
|
const container = wrapper.querySelector(selectors.container);
|
|
const bar = container.querySelector(selectors.bar);
|
|
return {
|
|
bar: box(bar),
|
|
container: box(container),
|
|
dislike: box(dislike),
|
|
like: box(like),
|
|
wrapper: box(wrapper),
|
|
};
|
|
}, runtime.selectors);
|
|
expect(geometry.wrapper.width).toBeCloseTo(geometry.like.width + geometry.dislike.width, 0);
|
|
expect(geometry.container.top).toBeGreaterThanOrEqual(Math.max(geometry.like.bottom, geometry.dislike.bottom) - 1);
|
|
expect(geometry.bar.left).toBeGreaterThanOrEqual(geometry.container.left - 1);
|
|
expect(geometry.bar.right).toBeLessThanOrEqual(geometry.container.right + 1);
|
|
expect(geometry.bar.width / geometry.container.width).toBeCloseTo(counts.likes / (counts.likes + counts.dislikes), 2);
|
|
return locators;
|
|
}
|
|
|
|
async function expectOwnedShortControl(
|
|
page,
|
|
runtime,
|
|
videoId,
|
|
counts,
|
|
shortIdentity = "active-video-id",
|
|
shortsDislikeControl = "synthetic",
|
|
) {
|
|
await expect(page).toHaveURL((url) => url.pathname === `/shorts/${videoId}`);
|
|
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();
|
|
await expect(dislike).toHaveCount(1);
|
|
await expect(dislike).toBeVisible();
|
|
await expect(dislike.locator("#text")).toHaveText(String(counts.dislikes));
|
|
if (shortsDislikeControl === "synthetic" && runtime.selectors.shortsVideoAttribute) {
|
|
await expect(dislike).toHaveAttribute(runtime.selectors.shortsVideoAttribute, videoId);
|
|
}
|
|
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);
|
|
await expect(page.locator(runtime.selectors.wrapper)).toHaveCount(0);
|
|
await expect(page.locator(runtime.selectors.container)).toHaveCount(0);
|
|
await expect(page.locator(runtime.selectors.bar)).toHaveCount(0);
|
|
return { currentRenderer, dislike };
|
|
}
|
|
|
|
async function readOwnedSurfaceInvariant(
|
|
page,
|
|
runtime,
|
|
videoId,
|
|
counts,
|
|
kind,
|
|
shortIdentity = "active-video-id",
|
|
shortsDislikeControl = "synthetic",
|
|
) {
|
|
const shortsDislikeSelector = currentShortDislikeSelector(runtime, shortsDislikeControl);
|
|
return page.evaluate(
|
|
({
|
|
counts: expectedCounts,
|
|
expectedCount,
|
|
expectedTooltip,
|
|
kind: expectedKind,
|
|
selectors,
|
|
shortsDislikeSelector: expectedShortsDislikeSelector,
|
|
shortIdentity: expectedShortIdentity,
|
|
videoId: expectedVideoId,
|
|
}) => {
|
|
const visible = (element) => {
|
|
if (!element?.isConnected || element.closest("[hidden], [aria-hidden='true']")) return false;
|
|
const style = getComputedStyle(element);
|
|
const bounds = element.getBoundingClientRect();
|
|
return style.display !== "none" && style.visibility !== "hidden" && bounds.width > 0 && bounds.height > 0;
|
|
};
|
|
const currentUrl = new URL(location.href);
|
|
const common = {
|
|
bars: document.querySelectorAll(selectors.bar).length,
|
|
containers: document.querySelectorAll(selectors.container).length,
|
|
pathname: currentUrl.pathname,
|
|
tooltips: document.querySelectorAll(selectors.tooltip).length,
|
|
wrappers: document.querySelectorAll(selectors.wrapper).length,
|
|
};
|
|
|
|
if (expectedKind === "shorts") {
|
|
const activeRenderers = Array.from(
|
|
document.querySelectorAll(`ytd-reel-video-renderer[video-id="${expectedVideoId}"][is-active]`),
|
|
);
|
|
const 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;
|
|
}
|
|
}),
|
|
);
|
|
const currentRenderers =
|
|
expectedShortIdentity === "exact-href-without-active-or-video-id" ? exactHrefRenderers : activeRenderers;
|
|
const currentControls = currentRenderers.flatMap((renderer) =>
|
|
Array.from(renderer.querySelectorAll(expectedShortsDislikeSelector)),
|
|
);
|
|
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);
|
|
return {
|
|
...common,
|
|
activeRenderers: activeRenderers.length,
|
|
count: currentControls[0]?.querySelector("#text")?.textContent?.trim() ?? null,
|
|
currentActionButtons: currentActionButtons.length,
|
|
currentRendererHasActive: currentRenderers[0]?.hasAttribute("is-active") ?? null,
|
|
currentRendererVideoId: currentRenderers[0]?.getAttribute("video-id") ?? null,
|
|
currentRenderers: currentRenderers.length,
|
|
totalActionButtons: actionButtons.length,
|
|
currentControls: currentControls.length,
|
|
currentVisible: currentControls.length === 1 && visible(currentControls[0]),
|
|
expectedCount,
|
|
expectedPathname: `/shorts/${expectedVideoId}`,
|
|
expectedShortIdentity,
|
|
kind: expectedKind,
|
|
visibleControls: visibleControls.length,
|
|
};
|
|
}
|
|
|
|
const controls = Array.from(document.querySelectorAll(`[data-fixture-control-video-id="${expectedVideoId}"]`));
|
|
const currentControls = controls.filter(visible);
|
|
const reactionRegion = currentControls[0]?.parentElement ?? null;
|
|
const wrapper = reactionRegion?.querySelector(`:scope > ${selectors.wrapper}`) ?? null;
|
|
const container = wrapper?.querySelector(selectors.container) ?? null;
|
|
const bar = container?.querySelector(selectors.bar) ?? null;
|
|
const containerBounds = container?.getBoundingClientRect();
|
|
const barBounds = bar?.getBoundingClientRect();
|
|
return {
|
|
...common,
|
|
count:
|
|
currentControls[0]
|
|
?.querySelector('[data-fixture-role="dislike"]')
|
|
?.querySelector(selectors.watchDislikeCount)
|
|
?.textContent?.trim() ?? null,
|
|
currentControls: currentControls.length,
|
|
expectedCount,
|
|
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,
|
|
ownerVideoId: selectors.wrapperVideoAttribute
|
|
? wrapper?.getAttribute(selectors.wrapperVideoAttribute) ?? null
|
|
: expectedVideoId,
|
|
ownerWrapperVisible: visible(wrapper),
|
|
ratio: containerBounds?.width > 0 && barBounds ? barBounds.width / containerBounds.width : null,
|
|
videoId: currentUrl.searchParams.get("v"),
|
|
};
|
|
},
|
|
{
|
|
counts,
|
|
expectedCount: expectedDislikeText(counts),
|
|
expectedTooltip: runtime.tooltipText(counts),
|
|
kind,
|
|
selectors: runtime.selectors,
|
|
shortsDislikeSelector,
|
|
shortIdentity,
|
|
videoId,
|
|
},
|
|
);
|
|
}
|
|
|
|
function ownedSurfaceInvariantIsValid(sample) {
|
|
if (sample.pathname !== sample.expectedPathname || sample.count !== sample.expectedCount) return false;
|
|
if (sample.kind === "shorts") {
|
|
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;
|
|
return (
|
|
identityMatches &&
|
|
sample.currentControls === 1 &&
|
|
sample.currentActionButtons === 6 &&
|
|
sample.currentVisible &&
|
|
sample.visibleControls === 1 &&
|
|
sample.totalActionButtons === 6 &&
|
|
sample.wrappers === 0 &&
|
|
sample.containers === 0 &&
|
|
sample.bars === 0
|
|
);
|
|
}
|
|
return (
|
|
sample.videoId === sample.expectedVideoId &&
|
|
sample.currentControls === 1 &&
|
|
sample.wrappers === 1 &&
|
|
sample.containers === 1 &&
|
|
sample.bars === 1 &&
|
|
sample.tooltips === 1 &&
|
|
sample.ownerWrapperVisible &&
|
|
sample.ownerContainerVisible &&
|
|
sample.ownerBarVisible &&
|
|
sample.ownerVideoId === sample.expectedVideoId &&
|
|
sample.ownerTooltip?.includes(sample.expectedTooltip) &&
|
|
Math.abs(sample.ratio - sample.expectedRatio) <= 0.015
|
|
);
|
|
}
|
|
|
|
async function waitForOwnedSurfaceStability(page, runtime, surface, timing = {}) {
|
|
return waitForStableInvariant({
|
|
intervalMs: 25,
|
|
isValid: ownedSurfaceInvariantIsValid,
|
|
label: `${runtime.name} ${surface.kind} ${surface.videoId} ownership`,
|
|
read: () =>
|
|
readOwnedSurfaceInvariant(
|
|
page,
|
|
runtime,
|
|
surface.videoId,
|
|
surface.counts,
|
|
surface.kind,
|
|
surface.shortIdentity,
|
|
surface.shortsDislikeControl,
|
|
),
|
|
stableForMs: timing.stableForMs ?? 250,
|
|
timeoutMs: timing.timeoutMs ?? 2_000,
|
|
});
|
|
}
|
|
|
|
async function waitForOwnedSurfaceWithinBudget(page, runtime, surface, maxFirstValidMs = 1_000) {
|
|
const readiness = await waitForOwnedSurfaceStability(page, runtime, surface, {
|
|
stableForMs: 250,
|
|
timeoutMs: maxFirstValidMs + 500,
|
|
});
|
|
expect(readiness.firstValidMs).toBeLessThanOrEqual(maxFirstValidMs);
|
|
return readiness;
|
|
}
|
|
|
|
async function assertOwnedSurfaceContinuously(page, runtime, surface, durationMs = 600) {
|
|
return assertInvariantContinuously({
|
|
durationMs,
|
|
intervalMs: 25,
|
|
isValid: ownedSurfaceInvariantIsValid,
|
|
label: `${runtime.name} settled ${surface.kind} ${surface.videoId}`,
|
|
read: () =>
|
|
readOwnedSurfaceInvariant(
|
|
page,
|
|
runtime,
|
|
surface.videoId,
|
|
surface.counts,
|
|
surface.kind,
|
|
surface.shortIdentity,
|
|
surface.shortsDislikeControl,
|
|
),
|
|
});
|
|
}
|
|
|
|
function expectCountRequestVideoIds(backend, expectedVideoIds) {
|
|
assertExactSuccessfulVotesTraffic(backend.requests, expectedVideoIds, "The shared navigation contract");
|
|
}
|
|
|
|
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"],
|
|
});
|
|
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);
|
|
}
|
|
expect(backend.requestsFor("GET", "/votes").map((request) => request.query.videoId)).toEqual([origin.videoId]);
|
|
|
|
await page.evaluate(() => globalThis.__navigationMatrixFixture.hydrateDestination());
|
|
const destinationRequest = await destinationCountGate.seen;
|
|
try {
|
|
expect(destinationRequest.query.videoId).toBe(destination.videoId);
|
|
expect(await page.evaluate(() => globalThis.__navigationMatrixFixture.snapshot())).toMatchObject({
|
|
currentControls: 1,
|
|
documentIdentity: initialDocumentIdentity,
|
|
phase: "hydrate-destination-controls",
|
|
retainedControls: 1,
|
|
retainedTreesConnected: true,
|
|
retainedTreesHidden: true,
|
|
sidebarConnected: true,
|
|
timeline: ["navigate-start", "route-and-shell", "navigate-finish", "hydrate-destination-controls"],
|
|
});
|
|
const gatedDestination = currentWatchLocators(page, runtime, destination.videoId);
|
|
await expect(gatedDestination.controls).toHaveCount(1);
|
|
await expect(gatedDestination.controls).toBeVisible();
|
|
await expect(
|
|
gatedDestination.controls.locator('[data-fixture-role="dislike"]').locator(runtime.selectors.watchDislikeCount),
|
|
).toHaveText("");
|
|
await expect(gatedDestination.wrapper).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);
|
|
}
|
|
} finally {
|
|
if (!destinationCountGate.released) {
|
|
destinationCountGate.release({ body: { ...destination.counts, rating: 4.5 } });
|
|
}
|
|
}
|
|
|
|
await waitForOwnedSurfaceWithinBudget(page, runtime, destination);
|
|
const destinationLocators = await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
|
|
await expect(page.locator("#fixture-matrix-retained-trees").locator(runtime.selectors.wrapper)).toHaveCount(0);
|
|
expect(backend.requestsFor("GET", "/votes").map((request) => request.query.videoId)).toEqual([
|
|
origin.videoId,
|
|
destination.videoId,
|
|
]);
|
|
|
|
const pruneStats = await pruneCurrentWatchBar(page, runtime, destinationLocators, destination);
|
|
expect(pruneStats).toEqual({ addedWrappers: 1, removedWrappers: 1 });
|
|
await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
|
|
expect(backend.requestsFor("GET", "/votes").map((request) => request.query.videoId)).toEqual([
|
|
origin.videoId,
|
|
destination.videoId,
|
|
]);
|
|
|
|
await page.evaluate(() => globalThis.__navigationMatrixFixture.detachOutgoing());
|
|
await assertOwnedSurfaceContinuously(page, runtime, destination, 550);
|
|
await expect(page.locator("#fixture-matrix-retained-trees").locator("[data-fixture-control-video-id]")).toHaveCount(
|
|
0,
|
|
);
|
|
await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
|
|
expect(await page.evaluate(() => globalThis.__navigationMatrixFixture.snapshot())).toMatchObject({
|
|
currentControls: 1,
|
|
documentIdentity: initialDocumentIdentity,
|
|
phase: "detach-outgoing",
|
|
retainedControls: 0,
|
|
retainedTreesConnected: true,
|
|
retainedTreesHidden: true,
|
|
sidebarConnected: true,
|
|
timeline: [
|
|
"navigate-start",
|
|
"route-and-shell",
|
|
"navigate-finish",
|
|
"hydrate-destination-controls",
|
|
"detach-outgoing",
|
|
],
|
|
});
|
|
|
|
expect(backend.requestsFor("POST", "/interact/vote")).toHaveLength(0);
|
|
expect(backend.requestsFor("POST", "/interact/confirmVote")).toHaveLength(0);
|
|
}
|
|
|
|
async function runWatchSameRootHiddenFirstScenario({
|
|
backend,
|
|
beforeNonCurrentDuplicateDetach,
|
|
page,
|
|
runtime,
|
|
scenario,
|
|
}) {
|
|
const { destination, origin } = scenario;
|
|
const initialDocumentIdentity = await page.evaluate(
|
|
() => globalThis.__navigationMatrixSameRootFixture.snapshot().documentIdentity,
|
|
);
|
|
await expectOwnedWatchBar(page, runtime, origin.videoId, origin.counts);
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
await page.locator("#fixture-matrix-same-root-watch").click();
|
|
|
|
await expect(page).toHaveURL((url) => url.pathname === "/watch" && url.searchParams.get("v") === destination.videoId);
|
|
await waitForOwnedSurfaceWithinBudget(page, runtime, destination);
|
|
await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
|
|
const outgoingIsOffscreen = scenario.transition.outgoingPresentation === "offscreen";
|
|
const settledTopology = await page.evaluate(() => globalThis.__navigationMatrixSameRootFixture.snapshot());
|
|
expect(settledTopology).toEqual({
|
|
documentIdentity: initialDocumentIdentity,
|
|
hiddenFirst: true,
|
|
hiddenOutgoingConnected: true,
|
|
hiddenOutgoingHeight: outgoingIsOffscreen ? expect.any(Number) : 0,
|
|
hiddenOutgoingWidth: outgoingIsOffscreen ? expect.any(Number) : 0,
|
|
liveDestinationConnected: true,
|
|
outgoingIntersectsViewport: false,
|
|
outgoingPresentation: scenario.transition.outgoingPresentation,
|
|
rootVideoId: destination.videoId,
|
|
sameRoot: true,
|
|
sidebarConnected: true,
|
|
});
|
|
if (outgoingIsOffscreen) {
|
|
expect(settledTopology.hiddenOutgoingHeight).toBeGreaterThan(0);
|
|
expect(settledTopology.hiddenOutgoingWidth).toBeGreaterThan(0);
|
|
}
|
|
const currentRoot = page.locator(`ytd-watch-flexy[video-id="${destination.videoId}"]`);
|
|
await expect(currentRoot.locator("ytd-menu-renderer.ytd-watch-metadata > div")).toHaveCount(2);
|
|
await expect(
|
|
currentRoot.locator('[data-fixture-matrix-hidden-outgoing="true"]').locator(runtime.selectors.wrapper),
|
|
).toHaveCount(0);
|
|
await expect(
|
|
currentRoot.locator('[data-fixture-matrix-live-destination="true"]').locator(runtime.selectors.wrapper),
|
|
).toHaveCount(1);
|
|
if (scenario.transition.controlMarkup === "legacy-segmented") {
|
|
await expect(currentRoot.locator("#segmented-like-button")).toHaveCount(2);
|
|
await expect(currentRoot.locator("#segmented-dislike-button")).toHaveCount(2);
|
|
await expect(
|
|
currentRoot.locator('[data-fixture-matrix-live-destination="true"]').locator("#segmented-dislike-button #text"),
|
|
).toHaveText(String(destination.counts.dislikes));
|
|
}
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
|
|
await assertOwnedSurfaceContinuously(page, runtime, destination);
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
expect(await page.evaluate(() => globalThis.__navigationMatrixSameRootFixture.snapshot())).toMatchObject({
|
|
documentIdentity: initialDocumentIdentity,
|
|
hiddenFirst: true,
|
|
hiddenOutgoingConnected: true,
|
|
hiddenOutgoingHeight: outgoingIsOffscreen ? expect.any(Number) : 0,
|
|
hiddenOutgoingWidth: outgoingIsOffscreen ? expect.any(Number) : 0,
|
|
liveDestinationConnected: true,
|
|
outgoingIntersectsViewport: false,
|
|
outgoingPresentation: scenario.transition.outgoingPresentation,
|
|
rootVideoId: destination.videoId,
|
|
sameRoot: true,
|
|
sidebarConnected: true,
|
|
});
|
|
|
|
await beforeNonCurrentDuplicateDetach?.();
|
|
|
|
await page.evaluate(() => globalThis.__navigationMatrixSameRootFixture.detachOutgoing());
|
|
await assertOwnedSurfaceContinuously(page, runtime, destination, 550);
|
|
await expect(currentRoot.locator("ytd-menu-renderer.ytd-watch-metadata > div")).toHaveCount(1);
|
|
await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
}
|
|
|
|
async function runWatchHistoryScenario({ backend, page, runtime, scenario }) {
|
|
const { destination, origin } = scenario;
|
|
const initialProbe = await readStandardProbe(page);
|
|
await expectOwnedWatchBar(page, runtime, origin.videoId, origin.counts);
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
await page.locator("#watch-next").click();
|
|
await waitForOwnedSurfaceWithinBudget(page, runtime, destination);
|
|
await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
|
|
expect(await readStandardProbe(page)).toMatchObject({
|
|
currentKind: "watch",
|
|
currentVideoId: destination.videoId,
|
|
documentIdentity: initialProbe.documentIdentity,
|
|
historyRenders: [],
|
|
navigateFinishes: 1,
|
|
navigateStarts: 0,
|
|
transitionPending: null,
|
|
});
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
|
|
await page.goBack();
|
|
await waitForOwnedSurfaceWithinBudget(page, runtime, origin);
|
|
await expectOwnedWatchBar(page, runtime, origin.videoId, origin.counts);
|
|
expect(await readStandardProbe(page)).toMatchObject({
|
|
currentKind: "watch",
|
|
currentVideoId: origin.videoId,
|
|
documentIdentity: initialProbe.documentIdentity,
|
|
historyRenders: [origin.videoId],
|
|
navigateFinishes: 2,
|
|
navigateStarts: 0,
|
|
transitionPending: null,
|
|
});
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId, origin.videoId]);
|
|
|
|
await page.goForward();
|
|
await waitForOwnedSurfaceWithinBudget(page, runtime, destination);
|
|
await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
|
|
expect(await readStandardProbe(page)).toMatchObject({
|
|
currentKind: "watch",
|
|
currentVideoId: destination.videoId,
|
|
documentIdentity: initialProbe.documentIdentity,
|
|
historyRenders: [origin.videoId, destination.videoId],
|
|
navigateFinishes: 3,
|
|
navigateStarts: 0,
|
|
transitionPending: null,
|
|
});
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId, origin.videoId, destination.videoId]);
|
|
|
|
await assertOwnedSurfaceContinuously(page, runtime, destination);
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId, origin.videoId, destination.videoId]);
|
|
}
|
|
|
|
async function runWatchAutoplayScenario({ backend, page, runtime, scenario }) {
|
|
const { destination, origin } = scenario;
|
|
const initialProbe = await readStandardProbe(page);
|
|
await expectOwnedWatchBar(page, runtime, origin.videoId, origin.counts);
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
await page.evaluate(() => globalThis.__navigationFixture.dispatchEnded());
|
|
|
|
await waitForOwnedSurfaceWithinBudget(page, runtime, destination);
|
|
await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
|
|
expect(await readStandardProbe(page)).toMatchObject({
|
|
currentKind: "watch",
|
|
currentVideoId: destination.videoId,
|
|
documentIdentity: initialProbe.documentIdentity,
|
|
historyRenders: [],
|
|
navigateFinishes: 0,
|
|
navigateStarts: 0,
|
|
transitionPending: null,
|
|
});
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
|
|
await assertOwnedSurfaceContinuously(page, runtime, destination);
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
}
|
|
|
|
async function 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]);
|
|
}
|
|
|
|
async function runCrossSurfaceScenario({ backend, page, runtime, scenario }) {
|
|
const { destination, origin } = scenario;
|
|
const initialProbe = await readStandardProbe(page);
|
|
if (origin.kind === "watch") {
|
|
await expectOwnedWatchBar(page, runtime, origin.videoId, origin.counts);
|
|
} else {
|
|
await expectOwnedShortControl(page, runtime, origin.videoId, origin.counts);
|
|
}
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
const linkId = origin.kind === "watch" ? "watch-to-short" : "short-to-watch";
|
|
await page.locator(`#${linkId}`).click();
|
|
|
|
if (destination.kind === "watch") {
|
|
await expect(page).toHaveURL(
|
|
(url) => url.pathname === "/watch" && url.searchParams.get("v") === destination.videoId,
|
|
);
|
|
} else {
|
|
await expect(page).toHaveURL((url) => url.pathname === `/shorts/${destination.videoId}`);
|
|
}
|
|
expect(await readStandardProbe(page)).toMatchObject({
|
|
currentKind: origin.kind,
|
|
currentVideoId: origin.videoId,
|
|
documentIdentity: initialProbe.documentIdentity,
|
|
historyRenders: [],
|
|
navigateFinishes: 1,
|
|
navigateStarts: 0,
|
|
transitionPending: destination.kind,
|
|
});
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
await waitForOwnedSurfaceWithinBudget(page, runtime, destination, scenario.timing?.maxFirstValidMs);
|
|
if (destination.kind === "watch") {
|
|
await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
|
|
} else {
|
|
await expectOwnedShortControl(page, runtime, destination.videoId, destination.counts);
|
|
}
|
|
expect(await readStandardProbe(page)).toMatchObject({
|
|
currentKind: destination.kind,
|
|
currentVideoId: destination.videoId,
|
|
documentIdentity: initialProbe.documentIdentity,
|
|
historyRenders: [],
|
|
navigateFinishes: 1,
|
|
navigateStarts: 0,
|
|
transitionPending: null,
|
|
});
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
|
|
await assertOwnedSurfaceContinuously(page, runtime, destination);
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
}
|
|
|
|
async function runWatchActionContainerReplacementScenario({ backend, page, runtime, scenario }) {
|
|
const { origin } = scenario;
|
|
const initialProbe = await readStandardProbe(page);
|
|
await expectOwnedWatchBar(page, runtime, origin.videoId, origin.counts);
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
await page.evaluate((selectors) => {
|
|
const currentActions = document.querySelector(
|
|
'[data-fixture-page-kind="watch"] ytd-menu-renderer.ytd-watch-metadata > div',
|
|
);
|
|
if (!currentActions) throw new Error("The current watch fixture has no action container to replace.");
|
|
const replacement = currentActions.cloneNode(true);
|
|
replacement.setAttribute("data-fixture-matrix-action-replacement", "true");
|
|
replacement.querySelectorAll(selectors.wrapper).forEach((element) => element.remove());
|
|
const controls = replacement.querySelector("[data-fixture-control-video-id]");
|
|
for (const role of ["like", "dislike"]) {
|
|
const control = controls.querySelector(`[data-fixture-role="${role}"]`);
|
|
control.classList.remove("style-default-active");
|
|
control.classList.add("style-text");
|
|
control.querySelector("button")?.setAttribute("aria-pressed", "false");
|
|
}
|
|
controls
|
|
.querySelectorAll(
|
|
'[data-fixture-role="dislike"] .ytSpecButtonShapeNextButtonTextContent, [data-fixture-role="dislike"] #text',
|
|
)
|
|
.forEach((element) => element.remove());
|
|
|
|
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;
|
|
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),
|
|
);
|
|
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);
|
|
expect(await page.evaluate(() => globalThis.__navigationMatrixSameNodeFixture.snapshot())).toMatchObject({
|
|
countAfterNavigateStart: "",
|
|
countAfterRouteAndRoot: "",
|
|
timeline: ["navigate-start", "route-and-root-only", "navigate-finish"],
|
|
});
|
|
|
|
const destinationRequest = await destinationCountGate.seen;
|
|
expect(destinationRequest.query.videoId).toBe(destination.videoId);
|
|
const pendingDestination = currentWatchLocators(page, runtime, destination.videoId);
|
|
await expect(pendingDestination.controls).toHaveCount(1);
|
|
await expect(
|
|
pendingDestination.controls.locator('[data-fixture-role="dislike"]').locator(runtime.selectors.watchDislikeCount),
|
|
).toHaveText("");
|
|
await expect(pendingDestination.wrapper).toHaveCount(0);
|
|
if (runtime.selectors.wrapperVideoAttribute) {
|
|
await expect(
|
|
page.locator(`${runtime.selectors.wrapper}[${runtime.selectors.wrapperVideoAttribute}="${origin.videoId}"]`),
|
|
).toHaveCount(0);
|
|
}
|
|
|
|
destinationCountGate.release({ body: { ...destination.counts, rating: 4.5 } });
|
|
await waitForOwnedSurfaceWithinBudget(page, runtime, destination);
|
|
const destinationLocators = await expectOwnedWatchBar(page, runtime, destination.videoId, destination.counts);
|
|
if (runtime.selectors.wrapperVideoAttribute) {
|
|
await expect(destinationLocators.wrapper).toHaveAttribute(
|
|
runtime.selectors.wrapperVideoAttribute,
|
|
destination.videoId,
|
|
);
|
|
}
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
expect(await page.evaluate(() => globalThis.__navigationMatrixSameNodeFixture.snapshot())).toEqual({
|
|
buttonsReused: true,
|
|
countAfterNavigateStart: "",
|
|
countAfterRouteAndRoot: "",
|
|
controlsReused: true,
|
|
dislikeReused: true,
|
|
documentIdentity: initialTopology.documentIdentity,
|
|
likeReused: true,
|
|
rootVideoId: destination.videoId,
|
|
sidebarConnected: true,
|
|
timeline: ["navigate-start", "route-and-root-only", "navigate-finish"],
|
|
});
|
|
|
|
await assertOwnedSurfaceContinuously(page, runtime, destination);
|
|
expectCountRequestVideoIds(backend, [origin.videoId, destination.videoId]);
|
|
}
|
|
|
|
async function runWatchRateBarCorruptionScenario({ backend, page, runtime, scenario }) {
|
|
const { origin } = scenario;
|
|
await expectOwnedWatchBar(page, runtime, origin.videoId, origin.counts);
|
|
expectCountRequestVideoIds(backend, [origin.videoId]);
|
|
|
|
for (const corruption of scenario.transition.corruptions) {
|
|
const corrupted = await page.evaluate(
|
|
({ corruption: corruptionKind, selectors }) => {
|
|
const wrapper = document.querySelector(selectors.wrapper);
|
|
const container = wrapper?.querySelector(selectors.container);
|
|
const fill = container?.querySelector(selectors.bar);
|
|
if (!wrapper || !container || !fill) {
|
|
throw new Error(`Cannot apply ${corruptionKind}; the current rate bar is incomplete.`);
|
|
}
|
|
globalThis.__navigationMatrixCorruptedRateBar = wrapper;
|
|
if (corruptionKind === "hidden-wrapper") {
|
|
wrapper.hidden = true;
|
|
} else if (corruptionKind === "collapsed-wrapper") {
|
|
wrapper.style.width = "0px";
|
|
wrapper.style.overflow = "hidden";
|
|
} else if (corruptionKind === "missing-fill") {
|
|
fill.remove();
|
|
} else if (corruptionKind === "missing-video-owner") {
|
|
wrapper.removeAttribute(selectors.wrapperVideoAttribute);
|
|
} else if (corruptionKind === "stale-video-owner") {
|
|
wrapper.setAttribute(selectors.wrapperVideoAttribute, "stalevid001");
|
|
} 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]);
|
|
}
|
|
|
|
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]);
|
|
}
|
|
|
|
const SCENARIO_RUNNERS = {
|
|
"short-next-short-active-reel": runShortNextScenario,
|
|
"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,
|
|
"short-direct-watch-delayed": runCrossSurfaceScenario,
|
|
"watch-autoplay-watch-replace-no-finish": runWatchAutoplayScenario,
|
|
"watch-navigate-start-watch-replace-no-finish": runWatchStartWithoutFinishScenario,
|
|
"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,
|
|
};
|
|
const NAVIGATION_SCENARIO_RUNNER_IDS = Object.freeze(Object.keys(SCENARIO_RUNNERS));
|
|
|
|
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 = {
|
|
EXTENSION_MATRIX_RUNTIME,
|
|
NAVIGATION_MATRIX,
|
|
NAVIGATION_SCENARIO_RUNNER_IDS,
|
|
NO_DESTINATION_DISLIKE_POSTCONDITION,
|
|
SINGLE_DESTINATION_DISLIKE_POSTCONDITION,
|
|
USERSCRIPT_MATRIX_RUNTIME,
|
|
WATCH_SIDEBAR_MATRIX,
|
|
getDestinationDislikePostconditionTarget,
|
|
installNavigationMatrixFixture,
|
|
runNavigationMatrixScenario,
|
|
};
|