diff --git a/Extensions/UserScript/Return Youtube Dislike.user.js b/Extensions/UserScript/Return Youtube Dislike.user.js index b7b7fb2..fb35b58 100644 --- a/Extensions/UserScript/Return Youtube Dislike.user.js +++ b/Extensions/UserScript/Return Youtube Dislike.user.js @@ -1739,20 +1739,38 @@ function getDislikeTextContainer(dislikeButton = getDislikeButton()) { let result = dislikeButton.querySelector("#text") ?? dislikeButton.getElementsByTagName("yt-formatted-string")[0] ?? - dislikeButton.querySelector("span[role='text']"); + dislikeButton.querySelector("span[role='text']") ?? + dislikeButton.querySelector( + ".ytSpecButtonShapeNextButtonTextContent, .yt-spec-button-shape-next__button-text-content", + ); if (result == null) { const activationTarget = getActivationTarget(dislikeButton); if (!activationTarget?.matches("button, tp-yt-paper-button#button")) { return null; } - const textSpan = document.createElement("span"); - textSpan.id = "text"; - textSpan.setAttribute("role", "text"); - textSpan.style.marginLeft = "6px"; - activationTarget.appendChild(textSpan); - activationTarget.style.width = "auto"; - result = textSpan; + const shorts = isShorts(); + const usesNativeShape = + !shorts && + activationTarget.matches( + ".ytSpecButtonShapeNextHost, .yt-spec-button-shape-next, " + + ".ytSpecButtonShapeNextIconButton, .yt-spec-button-shape-next--icon-button, " + + ".ytSpecButtonShapeNextIconLeading, .yt-spec-button-shape-next--icon-leading", + ); + const text = document.createElement(usesNativeShape ? "div" : "span"); + text.id = "text"; + text.setAttribute("role", "text"); + if (usesNativeShape) { + text.className = "ytSpecButtonShapeNextButtonTextContent yt-spec-button-shape-next__button-text-content"; + activationTarget.classList.remove("ytSpecButtonShapeNextIconButton", "yt-spec-button-shape-next--icon-button"); + activationTarget.classList.add("ytSpecButtonShapeNextIconLeading", "yt-spec-button-shape-next--icon-leading"); + } else { + if (shorts) text.style.marginLeft = "6px"; + else text.style.marginInlineStart = "6px"; + activationTarget.style.width = "auto"; + } + activationTarget.appendChild(text); + result = text; } return result; } @@ -2278,7 +2296,10 @@ function clearWatchPresentation(buttons, dislikeButton) { const dislikeText = dislikeButton?.querySelector("#text") ?? dislikeButton?.getElementsByTagName("yt-formatted-string")[0] ?? - dislikeButton?.querySelector("span[role='text']"); + dislikeButton?.querySelector("span[role='text']") ?? + dislikeButton?.querySelector( + ".ytSpecButtonShapeNextButtonTextContent, .yt-spec-button-shape-next__button-text-content", + ); if (dislikeText) { dislikeText.textContent = ""; } diff --git a/Extensions/UserScript/e2e/userscript-dislike-text-layout.e2e.js b/Extensions/UserScript/e2e/userscript-dislike-text-layout.e2e.js new file mode 100644 index 0000000..b91e0d7 --- /dev/null +++ b/Extensions/UserScript/e2e/userscript-dislike-text-layout.e2e.js @@ -0,0 +1,263 @@ +const { test, expect } = require("@playwright/test"); +const { + CREDENTIAL_KEY, + VIDEO_A, + VIDEO_B, + createFakeBackend, + injectGeneratedUserscript, + installGmEnvironment, + installHermeticRoutes, + openWatchFixture, +} = require("./harness"); + +// This fixture uses the native shape classes to model icon-only and text-button spacing. +function installNativeWatchTextLayout({ shape = "current", direction = "ltr" } = {}) { + document.documentElement.dir = direction; + const current = shape === "current"; + const shapeClasses = { + host: current ? "ytSpecButtonShapeNextHost" : "yt-spec-button-shape-next", + iconOnly: current ? "ytSpecButtonShapeNextIconButton" : "yt-spec-button-shape-next--icon-button", + iconLeading: current ? "ytSpecButtonShapeNextIconLeading" : "yt-spec-button-shape-next--icon-leading", + icon: current ? "ytSpecButtonShapeNextIcon" : "yt-spec-button-shape-next__icon", + text: current ? "ytSpecButtonShapeNextButtonTextContent" : "yt-spec-button-shape-next__button-text-content", + }; + + if (!document.getElementById("fixture-native-watch-text-style")) { + const style = document.createElement("style"); + style.id = "fixture-native-watch-text-style"; + style.textContent = ` + #fixture-watch #top-level-buttons-computed, + #fixture-watch [data-fixture-smartimation-content] { width: max-content; } + #fixture-watch [data-ryd-role="like"], + #fixture-watch [data-ryd-role="dislike"] { width: auto; flex: 0 0 auto; } + #fixture-watch [data-ryd-role] button { + display: inline-flex; position: relative; align-items: center; justify-content: center; + gap: 0; width: auto; min-width: 0; padding: 0 16px; height: 36px; + } + #fixture-watch [data-ryd-role] button:is(.ytSpecButtonShapeNextIconButton, .yt-spec-button-shape-next--icon-button) { + width: 40px; padding: 0; + } + #fixture-watch [data-ryd-role] :is(.ytSpecButtonShapeNextIcon, .yt-spec-button-shape-next__icon) { + display: flex; flex: 0 0 20px; width: 20px; height: 20px; margin: 0; + } + #fixture-watch [data-ryd-role] button:is(.ytSpecButtonShapeNextIconLeading, .yt-spec-button-shape-next--icon-leading) + > :is(.ytSpecButtonShapeNextIcon, .yt-spec-button-shape-next__icon) { + margin-inline-start: -6px; margin-inline-end: 6px; + } + #fixture-watch [data-ryd-role] :is(.ytSpecButtonShapeNextButtonTextContent, .yt-spec-button-shape-next__button-text-content) { + display: block; line-height: 20px; white-space: nowrap; + } + `; + document.head.appendChild(style); + } + + for (const role of ["like", "dislike"]) { + const host = document.querySelector(`#fixture-watch [data-ryd-role="${role}"]`); + const button = host.querySelector("button"); + const svg = button.querySelector("svg"); + let icon = svg.parentElement; + if (icon === button) { + icon = document.createElement("span"); + icon.setAttribute("aria-hidden", "true"); + svg.replaceWith(icon); + icon.appendChild(svg); + } + icon.className = shapeClasses.icon; + button.className = `${shapeClasses.host} ${role === "like" ? shapeClasses.iconLeading : shapeClasses.iconOnly}`; + if (role === "like") { + const text = button.querySelector(".ytSpecButtonShapeNextButtonTextContent, #text"); + text.textContent = text.textContent.trim(); + text.className = shapeClasses.text; + text.id = "text"; + text.setAttribute("role", "text"); + } else { + for (const text of button.querySelectorAll("#text")) text.remove(); + } + } + globalThis.__nativeWatchShape = shapeClasses; +} + +async function expectNativeCountLayout(page) { + const layout = await page.evaluate(() => { + const rect = (element) => { + const bounds = element.getBoundingClientRect(); + return { left: bounds.left, right: bounds.right, top: bounds.top, bottom: bounds.bottom, width: bounds.width }; + }; + const direction = document.documentElement.dir; + return ["like", "dislike"].map((role) => { + const host = document.querySelector(`#fixture-watch [data-ryd-role="${role}"]`); + const button = host.querySelector("button"); + const icon = rect(button.querySelector("svg")); + const text = button.querySelector("#text"); + const count = rect(text); + const bounds = rect(button); + return { + button: bounds, + icon, + count, + gap: direction === "rtl" ? icon.left - count.right : count.left - icon.right, + startPadding: direction === "rtl" ? bounds.right - icon.right : icon.left - bounds.left, + endPadding: direction === "rtl" ? count.left - bounds.left : bounds.right - count.right, + textFont: getComputedStyle(text).font, + textTag: text.tagName, + nativeTextClass: text.classList.contains(globalThis.__nativeWatchShape.text), + countContainers: host.querySelectorAll("#text").length, + }; + }); + }); + const [like, dislike] = layout; + expect(dislike.textTag).toBe("DIV"); + expect(dislike.nativeTextClass).toBe(true); + expect(dislike.countContainers).toBe(1); + expect(dislike.count.width).toBeGreaterThan(0); + expect(dislike.gap).toBeGreaterThan(0); + expect(dislike.gap).toBeCloseTo(like.gap, 1); + expect(dislike.startPadding).toBeCloseTo(like.startPadding, 1); + expect(dislike.endPadding).toBeCloseTo(like.endPadding, 1); + expect(dislike.textFont).toBe(like.textFont); + expect(dislike.count.left).toBeGreaterThanOrEqual(dislike.button.left); + expect(dislike.count.right).toBeLessThanOrEqual(dislike.button.right); + expect(Math.abs(dislike.icon.top + dislike.icon.bottom - dislike.count.top - dislike.count.bottom)).toBeLessThan(1); +} + +for (const shape of ["current", "older"]) { + for (const direction of ["ltr", "rtl"]) { + test(`${shape} watch fallback matches native text layout in ${direction} across updates and navigation`, async ({ + context, + page, + }) => { + const pageErrors = []; + page.on("pageerror", (error) => pageErrors.push(error.message)); + const backend = createFakeBackend({ + countsByVideo: { [VIDEO_A]: { likes: 100, dislikes: 25 }, [VIDEO_B]: { likes: 200, dislikes: 37 } }, + fixture: { nativeDislikeText: false }, + }); + await installGmEnvironment(context, { + [CREDENTIAL_KEY]: { registrationConfirmed: true, userId: "ExistingUserscriptCredential00000001" }, + }); + await installHermeticRoutes(context, backend); + await openWatchFixture(page, VIDEO_A); + await page.evaluate(installNativeWatchTextLayout, { shape, direction }); + await page.evaluate(() => { + const host = document.querySelector('#fixture-watch [data-ryd-role="dislike"]'); + globalThis.__nativeWatchOriginal = { + button: host.querySelector("button"), + icon: host.querySelector("svg"), + feedback: host.querySelector("[data-fixture-touch-feedback]"), + }; + }); + await injectGeneratedUserscript(page); + + const dislike = page.locator('#fixture-watch [data-ryd-role="dislike"]'); + await expect(dislike.locator("#text")).toHaveText("25"); + await expectNativeCountLayout(page); + expect( + await page.evaluate(() => { + const host = document.querySelector('#fixture-watch [data-ryd-role="dislike"]'); + const original = globalThis.__nativeWatchOriginal; + globalThis.__nativeWatchFirstCount = host.querySelector("#text"); + return ( + host.querySelector("button") === original.button && + host.querySelector("svg") === original.icon && + host.querySelector("[data-fixture-touch-feedback]") === original.feedback + ); + }), + ).toBe(true); + await expect(dislike.locator("button")).toHaveAttribute("aria-label", "Dislike this video"); + await expect(dislike.locator("button")).toHaveAttribute("aria-pressed", "false"); + + await dislike.locator("#text").click(); + await expect(dislike.locator("#text")).toHaveText("26"); + await expect.poll(() => backend.requestsFor("POST", "/interact/confirmVote").length).toBe(1); + await expectNativeCountLayout(page); + expect( + await page.evaluate( + () => + document.querySelector('#fixture-watch [data-ryd-role="dislike"] #text') === + globalThis.__nativeWatchFirstCount, + ), + ).toBe(true); + + await page.evaluate(() => { + const host = document.querySelector('#fixture-watch [data-ryd-role="dislike"]'); + const replacement = host.cloneNode(true); + replacement.querySelector("#text").remove(); + const button = replacement.querySelector("button"); + button.className = `${globalThis.__nativeWatchShape.host} ${globalThis.__nativeWatchShape.iconOnly}`; + host.replaceWith(replacement); + }); + await expect(dislike.locator("#text")).toHaveText("26"); + await expectNativeCountLayout(page); + expect(backend.requestsFor("GET", "/votes")).toHaveLength(1); + + await page.evaluate(({ videoId }) => globalThis.__youtubeFixture.navigate(videoId), { videoId: VIDEO_B }); + await page.evaluate(installNativeWatchTextLayout, { shape, direction }); + await expect(dislike.locator("#text")).toHaveText("37"); + await expectNativeCountLayout(page); + await expect(dislike.locator("button")).toHaveAttribute("aria-pressed", "false"); + expect(backend.requestsFor("GET", "/votes").map(({ query }) => query.videoId)).toEqual([VIDEO_A, VIDEO_B]); + expect( + backend.requestsFor("POST", "/interact/vote").map(({ body }) => ({ videoId: body.videoId, value: body.value })), + ).toEqual([{ videoId: VIDEO_A, value: -1 }]); + expect(backend.blockedRequests).toEqual([]); + expect(pageErrors).toEqual([]); + }); + } + + test(`${shape} class-only native count clears during navigation and reuses its node for the next video`, async ({ + context, + page, + }) => { + const backend = createFakeBackend({ + countsByVideo: { [VIDEO_A]: { likes: 100, dislikes: 25 }, [VIDEO_B]: { likes: 200, dislikes: 37 } }, + fixture: { nativeDislikeText: false }, + }); + await installGmEnvironment(context, { + [CREDENTIAL_KEY]: { registrationConfirmed: true, userId: "ExistingUserscriptCredential00000001" }, + }); + await installHermeticRoutes(context, backend); + await openWatchFixture(page, VIDEO_A); + await page.evaluate(installNativeWatchTextLayout, { shape }); + await page.evaluate(() => { + const button = document.querySelector('#fixture-watch [data-ryd-role="dislike"] button'); + const count = document.createElement("div"); + count.className = globalThis.__nativeWatchShape.text; + count.setAttribute("data-fixture-native-count", "true"); + button.className = `${globalThis.__nativeWatchShape.host} ${globalThis.__nativeWatchShape.iconLeading}`; + button.appendChild(count); + globalThis.__nativeWatchExistingCount = count; + }); + await injectGeneratedUserscript(page); + const count = page.locator("[data-fixture-native-count]"); + await expect(count).toHaveText("25"); + await expect(count).not.toHaveAttribute("id"); + await expect(count).not.toHaveAttribute("role"); + + const nextCounts = backend.defer("GET", "/votes"); + await page.evaluate((videoId) => { + document.dispatchEvent(new Event("yt-navigate-start", { bubbles: true })); + history.pushState({}, "", `/watch?v=${videoId}`); + document.querySelector("#fixture-watch").setAttribute("video-id", videoId); + for (const role of ["like", "dislike"]) { + const button = document.querySelector(`#fixture-watch [data-ryd-role="${role}"] button`); + button.setAttribute("data-video-id", videoId); + button.setAttribute("aria-label", role === "like" ? "200 likes" : "Dislike the next video"); + } + document.dispatchEvent(new Event("yt-navigate-finish", { bubbles: true })); + }, VIDEO_B); + await nextCounts.seen; + await expect(count).toHaveText(""); + nextCounts.release({ body: { likes: 200, dislikes: 37 } }); + await expect(count).toHaveText("37"); + expect( + await page.evaluate( + () => document.querySelector("[data-fixture-native-count]") === globalThis.__nativeWatchExistingCount, + ), + ).toBe(true); + await expect(count).not.toHaveAttribute("id"); + await expect(count).not.toHaveAttribute("role"); + await expect(page.locator('#fixture-watch [data-ryd-role="dislike"] #text')).toHaveCount(0); + expect(backend.requestsFor("POST", "/interact/vote")).toHaveLength(0); + expect(backend.blockedRequests).toEqual([]); + }); +} diff --git a/Extensions/UserScript/src/dislike-text-container.spec.js b/Extensions/UserScript/src/dislike-text-container.spec.js new file mode 100644 index 0000000..214c5b6 --- /dev/null +++ b/Extensions/UserScript/src/dislike-text-container.spec.js @@ -0,0 +1,224 @@ +/** + * @jest-environment jsdom + * @jest-environment-options {"url":"https://www.youtube.com/"} + */ + +jest.mock("../../common/vote-client", () => ({ + createVoteClient: () => ({ ensureRegistered: async () => undefined }), +})); + +const SHAPES = ["ytSpecButtonShapeNext", "yt-spec-button-shape-next--"]; +const originalFetch = globalThis.fetch; +let userscript; +let restoreDislikeButton; + +function classesFor(prefix) { + const older = prefix.includes("--"); + return { + iconOnly: `${prefix}${older ? "icon-button" : "IconButton"}`, + iconLeading: `${prefix}${older ? "icon-leading" : "IconLeading"}`, + text: older ? "yt-spec-button-shape-next__button-text-content" : `${prefix}ButtonTextContent`, + }; +} + +function mountControl({ prefix = SHAPES[0], tag = "button", text = "", classes } = {}) { + const shape = classesFor(prefix); + document.body.innerHTML = ` + + <${tag} id="button" class="${classes ?? `${shape.iconOnly} native-unrelated-class`}" aria-label="Dislike this video" aria-pressed="false" title="Dislike" style="width:40px"> + + ${text} + + + `; + return { + host: document.querySelector("dislike-button-view-model"), + button: document.querySelector(tag), + icon: document.querySelector("[data-native-icon]"), + feedback: document.querySelector("[data-native-feedback]"), + shape, + }; +} + +beforeAll(() => { + jest.useFakeTimers(); + globalThis.fetch = jest.fn(); + globalThis.__RYD_LIVE_TEST_BUILD__ = false; + userscript = require("./userscript-entry").__RewireAPI__; + jest.clearAllTimers(); + jest.useRealTimers(); +}); + +beforeEach(() => { + history.replaceState({}, "", "/watch?v=abcdefghijk"); +}); + +afterEach(() => { + restoreDislikeButton?.(); + restoreDislikeButton = undefined; + document.body.replaceChildren(); + document.documentElement.removeAttribute("dir"); + history.replaceState({}, "", "/"); +}); + +afterAll(() => { + globalThis.fetch = originalFetch; + delete globalThis.__RYD_LIVE_TEST_BUILD__; +}); + +test.each(SHAPES)("gives an icon-only %s watch button the native text layout without rebuilding it", (prefix) => { + const { host, button, icon, feedback, shape } = mountControl({ prefix }); + const beforeIcon = icon.outerHTML; + const count = userscript.__get__("getDislikeTextContainer")(host); + + expect(count.tagName).toBe("DIV"); + expect(count.id).toBe("text"); + expect(count.getAttribute("role")).toBe("text"); + expect(count.classList.contains(shape.text)).toBe(true); + expect(count.parentElement).toBe(button); + expect(count.style.marginLeft).toBe(""); + expect(count.style.marginInlineStart).toBe(""); + expect(button.classList.contains(shape.iconOnly)).toBe(false); + expect(button.classList.contains(shape.iconLeading)).toBe(true); + expect(button.classList.contains("native-unrelated-class")).toBe(true); + expect(button.style.width).toBe("40px"); + expect(button.getAttribute("aria-label")).toBe("Dislike this video"); + expect(button.getAttribute("aria-pressed")).toBe("false"); + expect(button.title).toBe("Dislike"); + expect(host.querySelector("[data-native-icon]")).toBe(icon); + expect(icon.outerHTML).toBe(beforeIcon); + expect(host.querySelector("[data-native-feedback]")).toBe(feedback); + expect(userscript.__get__("getActivationTarget")(host)).toBe(button); + expect(userscript.__get__("getDislikeTextContainer")(host)).toBe(count); + expect(host.querySelectorAll("#text")).toHaveLength(1); +}); + +test.each([ + '12', + '12', + '12', + '
12
', + '
12
', +])("reuses an existing native count and all native button layout (%s)", (text) => { + const { host, button } = mountControl({ text }); + const existing = host.querySelector(".native-count"); + const before = host.outerHTML; + + expect(userscript.__get__("getDislikeTextContainer")(host)).toBe(existing); + expect(userscript.__get__("getDislikeTextContainer")(host)).toBe(existing); + expect(host.outerHTML).toBe(before); + expect(userscript.__get__("getActivationTarget")(host)).toBe(button); +}); + +test("repeated count updates retain one container, its native icon, and its activation target", () => { + const { host, button, icon } = mountControl(); + restoreDislikeButton = userscript.__set__("getDislikeButton", () => host); + userscript.__get__("setDislikes")("25"); + const count = userscript.__get__("getDislikeTextContainer")(host); + userscript.__get__("setDislikes")("26"); + userscript.__get__("setDislikes")("26"); + + expect(count.innerText).toBe("26"); + expect(host.querySelectorAll("#text")).toHaveLength(1); + expect(host.querySelector("#text")).toBe(count); + expect(host.querySelector("[data-native-icon]")).toBe(icon); + expect(userscript.__get__("getActivationTarget")(host)).toBe(button); + expect(globalThis.fetch).not.toHaveBeenCalled(); +}); + +test.each(["ytSpecButtonShapeNextButtonTextContent", "yt-spec-button-shape-next__button-text-content"])( + "clears a reused class-only native %s count without replacing its node or attributes", + (textClass) => { + const { host, button, icon } = mountControl({ + text: `
25
`, + }); + const count = host.querySelector(`.${textClass}`); + const beforeAttributes = count.getAttributeNames().map((name) => [name, count.getAttribute(name)]); + + userscript.__get__("clearWatchPresentation")(document.body, host); + + expect(count.textContent).toBe(""); + expect(host.querySelector(`.${textClass}`)).toBe(count); + expect(count.getAttributeNames().map((name) => [name, count.getAttribute(name)])).toEqual(beforeAttributes); + expect(userscript.__get__("getDislikeTextContainer")(host)).toBe(count); + expect(host.querySelector("[data-native-icon]")).toBe(icon); + expect(userscript.__get__("getActivationTarget")(host)).toBe(button); + }, +); + +test("creates a fresh count on replacement controls inside the same view model", () => { + const { host, button } = mountControl(); + const count = userscript.__get__("getDislikeTextContainer")(host); + const replacement = document.createElement("button"); + replacement.className = classesFor(SHAPES[0]).iconOnly; + replacement.setAttribute("aria-pressed", "true"); + replacement.appendChild(document.createElement("svg")); + button.replaceWith(replacement); + const nextCount = userscript.__get__("getDislikeTextContainer")(host); + + expect(count.isConnected).toBe(false); + expect(nextCount).not.toBe(count); + expect(nextCount.parentElement).toBe(replacement); + expect(replacement.querySelectorAll("#text")).toHaveLength(1); + expect(replacement.getAttribute("aria-pressed")).toBe("true"); + expect(userscript.__get__("getActivationTarget")(host)).toBe(replacement); +}); + +test.each(["button", "tp-yt-paper-button"])( + "keeps %s legacy activation and uses direction-aware fallback spacing", + (tag) => { + const { host, button, icon } = mountControl({ tag, classes: "legacy-native-button" }); + document.documentElement.dir = "rtl"; + const count = userscript.__get__("getDislikeTextContainer")(host); + + expect(count.tagName).toBe("SPAN"); + expect(count.getAttribute("role")).toBe("text"); + expect(count.style.marginInlineStart).toBe("6px"); + expect(count.style.marginLeft).toBe(""); + expect(count.parentElement).toBe(button); + expect(button.style.width).toBe("auto"); + expect(host.querySelector("[data-native-icon]")).toBe(icon); + expect(userscript.__get__("getActivationTarget")(host)).toBe(button); + expect(userscript.__get__("getDislikeTextContainer")(button)).toBe(count); + }, +); + +test("preserves the existing fallback layout on native Shorts controls", () => { + history.replaceState({}, "", "/shorts/abcdefghijk"); + const { host, button, shape } = mountControl(); + const count = userscript.__get__("getDislikeTextContainer")(host); + + expect(count.tagName).toBe("SPAN"); + expect(count.style.marginLeft).toBe("6px"); + expect(count.style.marginInlineStart).toBe(""); + expect(button.style.width).toBe("auto"); + expect(button.classList.contains(shape.iconOnly)).toBe(true); + expect(button.classList.contains(shape.iconLeading)).toBe(false); +}); + +test("preserves a Shorts label beside its native activation button", () => { + history.replaceState({}, "", "/shorts/abcdefghijk"); + const { host, button } = mountControl(); + const label = document.createElement("label"); + const count = document.createElement("span"); + count.id = "text"; + count.setAttribute("role", "text"); + count.textContent = "25"; + host.appendChild(label); + label.append(button, count); + const before = host.outerHTML; + + expect(userscript.__get__("getDislikeTextContainer")(host)).toBe(count); + expect(userscript.__get__("getActivationTarget")(host)).toBe(button); + expect(host.outerHTML).toBe(before); +}); + +test("waits when a dislike host has no native activation target", () => { + document.body.innerHTML = ''; + const host = document.querySelector("dislike-button-view-model"); + const before = host.outerHTML; + + expect(userscript.__get__("getDislikeTextContainer")(host)).toBeNull(); + expect(host.outerHTML).toBe(before); + expect(userscript.__get__("getDislikeTextContainer")(null)).toBeNull(); +}); diff --git a/Extensions/UserScript/src/userscript-entry.js b/Extensions/UserScript/src/userscript-entry.js index c44adf6..67b70d7 100644 --- a/Extensions/UserScript/src/userscript-entry.js +++ b/Extensions/UserScript/src/userscript-entry.js @@ -839,20 +839,38 @@ function getDislikeTextContainer(dislikeButton = getDislikeButton()) { let result = dislikeButton.querySelector("#text") ?? dislikeButton.getElementsByTagName("yt-formatted-string")[0] ?? - dislikeButton.querySelector("span[role='text']"); + dislikeButton.querySelector("span[role='text']") ?? + dislikeButton.querySelector( + ".ytSpecButtonShapeNextButtonTextContent, .yt-spec-button-shape-next__button-text-content", + ); if (result == null) { const activationTarget = getActivationTarget(dislikeButton); if (!activationTarget?.matches("button, tp-yt-paper-button#button")) { return null; } - const textSpan = document.createElement("span"); - textSpan.id = "text"; - textSpan.setAttribute("role", "text"); - textSpan.style.marginLeft = "6px"; - activationTarget.appendChild(textSpan); - activationTarget.style.width = "auto"; - result = textSpan; + const shorts = isShorts(); + const usesNativeShape = + !shorts && + activationTarget.matches( + ".ytSpecButtonShapeNextHost, .yt-spec-button-shape-next, " + + ".ytSpecButtonShapeNextIconButton, .yt-spec-button-shape-next--icon-button, " + + ".ytSpecButtonShapeNextIconLeading, .yt-spec-button-shape-next--icon-leading", + ); + const text = document.createElement(usesNativeShape ? "div" : "span"); + text.id = "text"; + text.setAttribute("role", "text"); + if (usesNativeShape) { + text.className = "ytSpecButtonShapeNextButtonTextContent yt-spec-button-shape-next__button-text-content"; + activationTarget.classList.remove("ytSpecButtonShapeNextIconButton", "yt-spec-button-shape-next--icon-button"); + activationTarget.classList.add("ytSpecButtonShapeNextIconLeading", "yt-spec-button-shape-next--icon-leading"); + } else { + if (shorts) text.style.marginLeft = "6px"; + else text.style.marginInlineStart = "6px"; + activationTarget.style.width = "auto"; + } + activationTarget.appendChild(text); + result = text; } return result; } @@ -1378,7 +1396,10 @@ function clearWatchPresentation(buttons, dislikeButton) { const dislikeText = dislikeButton?.querySelector("#text") ?? dislikeButton?.getElementsByTagName("yt-formatted-string")[0] ?? - dislikeButton?.querySelector("span[role='text']"); + dislikeButton?.querySelector("span[role='text']") ?? + dislikeButton?.querySelector( + ".ytSpecButtonShapeNextButtonTextContent, .yt-spec-button-shape-next__button-text-content", + ); if (dislikeText) { dislikeText.textContent = ""; }