({
isNewDesign: jest.fn(() => true),
isRoundedDesign: jest.fn(() => true),
isShorts: jest.fn(() => false),
+ isVideoLiked: jest.fn(() => false),
}));
jest.mock("./utils", () => ({
@@ -48,8 +49,8 @@ jest.mock("./utils", () => ({
const { getButtons, getDislikeButton, getLikeButton } = require("./buttons");
const { createRateBar, hasUsableRateBar } = require("./bar");
-const { isLikesDisabled, isMobile, isNewDesign, isRoundedDesign, isShorts } = require("./state");
-const { getVideoId } = require("./utils");
+const { isLikesDisabled, isMobile, isNewDesign, isRoundedDesign, isShorts, isVideoLiked } = require("./state");
+const { getVideoId, isInViewport } = require("./utils");
describe("rate-bar stylesheet contrast", () => {
test("uses the same high-contrast negative track recipe as the userscript", () => {
@@ -95,6 +96,7 @@ describe("rate-bar visual ownership", () => {
isNewDesign.mockReturnValue(true);
isRoundedDesign.mockReturnValue(true);
isShorts.mockReturnValue(false);
+ isVideoLiked.mockReturnValue(false);
getVideoId.mockReturnValue("BBBBBBBBBBB");
const buttons = document.querySelector('[data-watch-buttons="BBBBBBBBBBB"]');
getButtons.mockReturnValue(buttons);
@@ -102,6 +104,32 @@ describe("rate-bar visual ownership", () => {
getDislikeButton.mockReturnValue(buttons.querySelector("dislike-button-view-model"));
});
+ test.each([false, true])("initializes the custom CSS hook from the native liked state: %s", (liked) => {
+ isVideoLiked.mockReturnValue(liked);
+
+ createRateBar(80, 20);
+
+ expect(getButtons().querySelector("#ryd-bar-container").getAttribute("data-ryd-liked")).toBe(String(liked));
+ });
+
+ test("refreshes the liked hook when reusing or recreating the bar", () => {
+ isInViewport.mockReturnValue(true);
+ createRateBar(80, 20);
+ const originalBar = getButtons().querySelector("#ryd-bar-container");
+ isVideoLiked.mockReturnValue(true);
+
+ createRateBar(81, 20);
+
+ expect(getButtons().querySelector("#ryd-bar-container")).toBe(originalBar);
+ expect(originalBar.getAttribute("data-ryd-liked")).toBe("true");
+
+ originalBar.closest(".ryd-tooltip").remove();
+ createRateBar(81, 20);
+ const recreatedBar = getButtons().querySelector("#ryd-bar-container");
+ expect(recreatedBar).not.toBe(originalBar);
+ expect(recreatedBar.getAttribute("data-ryd-liked")).toBe("true");
+ });
+
test("mutates only the selected current watch tree when stale duplicate IDs come first", () => {
const currentButtons = document.querySelector('[data-watch-buttons="BBBBBBBBBBB"]');
expect(currentButtons.closest("#top-row")).toBe(document.querySelector('[data-watch-row="BBBBBBBBBBB"]'));
@@ -160,6 +188,7 @@ describe("rate-bar visual ownership", () => {
expect(currentButtons.querySelector("#ryd-bar").style.width).toBe("85.99%");
getVideoId.mockReturnValue("AAAAAAAAAAA");
+ isVideoLiked.mockReturnValue(true);
expect(hasUsableRateBar(currentButtons)).toBe(false);
createRateBar(3_591, 1_409);
@@ -168,6 +197,7 @@ describe("rate-bar visual ownership", () => {
expect(staleWrapper.isConnected).toBe(false);
expect(currentWrapper).not.toBe(staleWrapper);
expect(currentWrapper.getAttribute("data-ryd-video-id")).toBe("AAAAAAAAAAA");
+ expect(currentWrapper.querySelector("#ryd-bar-container").getAttribute("data-ryd-liked")).toBe("true");
expect(currentButtons.querySelector("#ryd-bar").style.width).toBe("71.82%");
expect(hasUsableRateBar(currentButtons)).toBe(true);
});
diff --git a/Extensions/combined/src/events.js b/Extensions/combined/src/events.js
index d6bf75c..2916b63 100644
--- a/Extensions/combined/src/events.js
+++ b/Extensions/combined/src/events.js
@@ -111,7 +111,7 @@ function observeReactionPresentation(likeButton, dislikeButton) {
presentationObserver.disconnect();
const options = {
attributes: true,
- attributeFilter: ["aria-label", "aria-pressed"],
+ attributeFilter: ["aria-label", "aria-pressed", "class"],
characterData: true,
childList: true,
subtree: true,
diff --git a/Extensions/combined/src/reaction-presentation.spec.js b/Extensions/combined/src/reaction-presentation.spec.js
index 81cf1e1..b6e71d2 100644
--- a/Extensions/combined/src/reaction-presentation.spec.js
+++ b/Extensions/combined/src/reaction-presentation.spec.js
@@ -17,6 +17,7 @@ import {
NEUTRAL_STATE,
} from "./state";
+const { createRateBar: renderRateBar } = jest.requireActual("./bar");
const innerTextDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "innerText");
beforeAll(() => {
@@ -148,6 +149,89 @@ test("native count markup is untouched when reformatting is off", async () => {
expect(node.data).toBe("731");
});
+test("an initially liked video exposes its native state as soon as the bar is created", () => {
+ const controls = mount();
+ controls.nativeLikeButton.setAttribute("aria-pressed", "true");
+
+ renderRateBar(storedData.likes, storedData.dislikes);
+
+ expect(document.querySelector("#ryd-bar-container").getAttribute("data-ryd-liked")).toBe("true");
+});
+
+test("native like, unlike, and dislike transitions update the CSS hook without changing bar styling", async () => {
+ const controls = mount();
+ renderRateBar(storedData.likes, storedData.dislikes);
+ const bar = document.querySelector("#ryd-bar-container");
+ const barStyle = bar.getAttribute("style");
+ const fillStyle = bar.querySelector("#ryd-bar").getAttribute("style");
+ expect(bar.getAttribute("data-ryd-liked")).toBe("false");
+ refreshReactionPresentation();
+ createRateBar.mockClear();
+
+ for (const [liked, disliked] of [
+ [true, false],
+ [false, false],
+ [true, false],
+ [false, true],
+ ]) {
+ controls.nativeLikeButton.setAttribute("aria-pressed", String(liked));
+ controls.nativeDislikeButton.setAttribute("aria-pressed", String(disliked));
+ await flushMutations();
+
+ expect(bar.getAttribute("data-ryd-liked")).toBe(String(liked));
+ expect(bar.getAttribute("style")).toBe(barStyle);
+ expect(bar.querySelector("#ryd-bar").getAttribute("style")).toBe(fillStyle);
+ }
+ expect(storedData.likes).toBe(730);
+ expect(createRateBar).not.toHaveBeenCalled();
+});
+
+test("a replaced native like button refreshes the bar's liked hook", async () => {
+ const controls = mount();
+ renderRateBar(storedData.likes, storedData.dislikes);
+ const replacement = controls.nativeLikeButton.cloneNode(true);
+ replacement.setAttribute("aria-pressed", "true");
+
+ controls.nativeLikeButton.replaceWith(replacement);
+ await flushMutations();
+
+ expect(document.querySelector("#ryd-bar-container").getAttribute("data-ryd-liked")).toBe("true");
+});
+
+test("native active-class transitions refresh the bar's liked hook", async () => {
+ const controls = mount();
+ renderRateBar(storedData.likes, storedData.dislikes);
+ const bar = document.querySelector("#ryd-bar-container");
+
+ controls.likeButton.classList.add("style-default-active");
+ await flushMutations();
+ expect(bar.getAttribute("data-ryd-liked")).toBe("true");
+
+ controls.likeButton.classList.remove("style-default-active");
+ await flushMutations();
+ expect(bar.getAttribute("data-ryd-liked")).toBe("false");
+});
+
+test("navigation on reused controls replaces the outgoing bar's liked state", async () => {
+ const controls = mount();
+ controls.nativeLikeButton.setAttribute("aria-pressed", "true");
+ renderRateBar(storedData.likes, storedData.dislikes);
+ const outgoingBar = document.querySelector("#ryd-bar-container");
+ expect(outgoingBar.getAttribute("data-ryd-liked")).toBe("true");
+
+ history.replaceState({}, "", "/watch?v=zyxwvutsrqp");
+ Object.assign(storedData, { videoId: "zyxwvutsrqp", likes: 3591, dislikes: 1409 });
+ controls.nativeLikeButton.setAttribute("aria-pressed", "false");
+ renderRateBar(storedData.likes, storedData.dislikes);
+ const destinationBar = document.querySelector("#ryd-bar-container");
+ expect(destinationBar).not.toBe(outgoingBar);
+ expect(destinationBar.getAttribute("data-ryd-liked")).toBe("false");
+
+ controls.nativeLikeButton.setAttribute("aria-pressed", "true");
+ await flushMutations();
+ expect(destinationBar.getAttribute("data-ryd-liked")).toBe("true");
+});
+
test("later native labels refresh stored likes and the rate bar when reformatting is off", async () => {
const controls = mount();
refreshReactionPresentation();
diff --git a/Extensions/combined/src/state.js b/Extensions/combined/src/state.js
index 0d43a6c..9fcee4e 100644
--- a/Extensions/combined/src/state.js
+++ b/Extensions/combined/src/state.js
@@ -281,6 +281,11 @@ function setLikes(likesCount) {
}
function refreshReactionPresentation() {
+ const rateBar = getButtons()?.querySelector("#ryd-bar-container");
+ if (rateBar) {
+ const liked = String(isVideoLiked());
+ if (rateBar.getAttribute("data-ryd-liked") !== liked) rateBar.setAttribute("data-ryd-liked", liked);
+ }
const nativeLikeCount = readNativeLikeCount();
const nativeLikes = nativeLikeCount?.likes ?? false;
if (nativeLikes > 0 && nativeLikeCount.videoId === storedData.videoId && !nativeLikeCount.presented) {