Expose liked state on ratio bar (#95)

This commit is contained in:
Anarios
2026-10-18 10:07:48 +02:00
parent b770a0102c
commit 0bc86ff0c8
5 changed files with 125 additions and 4 deletions
+3 -1
View File
@@ -1,5 +1,5 @@
import { getButtons, getDislikeButton, getLikeButton } from "./buttons";
import { extConfig, isMobile, isLikesDisabled, isNewDesign, isRoundedDesign, isShorts } from "./state";
import { extConfig, isMobile, isLikesDisabled, isNewDesign, isRoundedDesign, isShorts, isVideoLiked } from "./state";
import { getColorFromTheme, getElementWidth, getVideoId, isInViewport, querySelector } from "./utils";
function closestConfigured(element, selectors) {
@@ -147,6 +147,7 @@ function createRateBar(likes, dislikes) {
<div class="ryd-tooltip-bar-container">
<div
id="ryd-bar-container"
data-ryd-liked="${isVideoLiked()}"
style="width: 100%; height: 2px;${colorDislikeStyle}"
>
<div
@@ -175,6 +176,7 @@ function createRateBar(likes, dislikes) {
} else {
const currentParts = getRateBarParts(buttons);
currentParts.wrapper.setAttribute("data-ryd-video-id", videoId);
currentParts.rateBar.setAttribute("data-ryd-liked", String(isVideoLiked()));
currentParts.wrapper.style.width = widthPx + "px";
currentParts.fill.style.width = widthPercent + "%";
const tooltipHost = buttons.querySelector("#ryd-dislike-tooltip");
+32 -2
View File
@@ -29,6 +29,7 @@ jest.mock("./state", () => ({
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);
});
+1 -1
View File
@@ -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,
@@ -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();
+5
View File
@@ -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) {