mirror of
https://github.com/Anarios/return-youtube-dislike.git
synced 2026-09-12 10:22:10 +02:00
220 lines
8.9 KiB
JavaScript
220 lines
8.9 KiB
JavaScript
|
|
const { test, expect } = require("@playwright/test");
|
||
|
|
const {
|
||
|
|
CREDENTIAL_KEY,
|
||
|
|
VIDEO_A,
|
||
|
|
createFakeBackend,
|
||
|
|
injectGeneratedUserscript,
|
||
|
|
installGmEnvironment,
|
||
|
|
installHermeticRoutes,
|
||
|
|
openWatchFixture,
|
||
|
|
} = require("./harness");
|
||
|
|
|
||
|
|
const EXISTING_CREDENTIALS = {
|
||
|
|
userId: "ExistingUserscriptCredential000000000001",
|
||
|
|
registrationConfirmed: true,
|
||
|
|
};
|
||
|
|
const COUNTS = { likes: 300, dislikes: 100 };
|
||
|
|
const VIEWPORTS = [
|
||
|
|
{ name: "wide desktop", width: 1280, height: 720 },
|
||
|
|
{ name: "narrow desktop", width: 768, height: 720 },
|
||
|
|
{ name: "mobile-sized", width: 390, height: 844 },
|
||
|
|
];
|
||
|
|
|
||
|
|
function colorChannels(color) {
|
||
|
|
const srgb = color.match(/^color\(srgb\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)/);
|
||
|
|
if (srgb) return srgb.slice(1).map((channel) => Number(channel) * 255);
|
||
|
|
const channels = color
|
||
|
|
.match(/[\d.]+/g)
|
||
|
|
?.slice(0, 3)
|
||
|
|
.map(Number);
|
||
|
|
if (!channels || channels.length !== 3) throw new Error(`Unsupported computed color: ${color}`);
|
||
|
|
return channels;
|
||
|
|
}
|
||
|
|
|
||
|
|
function relativeLuminance(color) {
|
||
|
|
const channels = colorChannels(color);
|
||
|
|
const linear = channels.map((channel) => {
|
||
|
|
const normalized = channel / 255;
|
||
|
|
return normalized <= 0.04045 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
|
||
|
|
});
|
||
|
|
return linear[0] * 0.2126 + linear[1] * 0.7152 + linear[2] * 0.0722;
|
||
|
|
}
|
||
|
|
|
||
|
|
function contrastRatio(first, second) {
|
||
|
|
const lighter = Math.max(relativeLuminance(first), relativeLuminance(second));
|
||
|
|
const darker = Math.min(relativeLuminance(first), relativeLuminance(second));
|
||
|
|
return (lighter + 0.05) / (darker + 0.05);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function readBarColors(page) {
|
||
|
|
return page.evaluate(() => ({
|
||
|
|
background: getComputedStyle(document.body).backgroundColor,
|
||
|
|
negative: getComputedStyle(document.querySelector("#return-youtube-dislike-bar-container")).backgroundColor,
|
||
|
|
positive: getComputedStyle(document.querySelector("#return-youtube-dislike-bar")).backgroundColor,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
function expectVisibleBarColors(colors, expected) {
|
||
|
|
expect(colors.background).toBe(expected.background);
|
||
|
|
expect(colors.positive).toBe(expected.positive);
|
||
|
|
colorChannels(colors.negative).forEach((channel) => expect(channel).toBeCloseTo(expected.negative, 0));
|
||
|
|
expect(contrastRatio(colors.negative, colors.background)).toBeGreaterThanOrEqual(3);
|
||
|
|
expect(contrastRatio(colors.positive, colors.negative)).toBeGreaterThanOrEqual(3);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function launchRateBarFixture({ context, page }, { rateBarEnabled, theme = "dark" } = {}) {
|
||
|
|
const pageErrors = [];
|
||
|
|
page.on("pageerror", (error) => pageErrors.push(error.message));
|
||
|
|
|
||
|
|
const backend = createFakeBackend({ countsByVideo: { [VIDEO_A]: COUNTS } });
|
||
|
|
await installGmEnvironment(context, { [CREDENTIAL_KEY]: EXISTING_CREDENTIALS });
|
||
|
|
await installHermeticRoutes(context, backend);
|
||
|
|
await openWatchFixture(page, VIDEO_A);
|
||
|
|
if (theme === "light") {
|
||
|
|
await page.evaluate(() => {
|
||
|
|
document.documentElement.style.setProperty("--yt-spec-base-background", "rgb(255, 255, 255)");
|
||
|
|
document.documentElement.style.setProperty("--yt-spec-text-primary", "rgb(15, 15, 15)");
|
||
|
|
document.documentElement.style.setProperty("--yt-spec-text-secondary", "rgb(96, 96, 96)");
|
||
|
|
document.body.style.background = "rgb(255, 255, 255)";
|
||
|
|
});
|
||
|
|
}
|
||
|
|
await injectGeneratedUserscript(page, { rateBarEnabled });
|
||
|
|
|
||
|
|
await expect(page.locator('[data-ryd-role="dislike"] #text')).toHaveText(String(COUNTS.dislikes));
|
||
|
|
return { backend, pageErrors };
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const viewport of VIEWPORTS) {
|
||
|
|
test(`default ratio bar renders cleanly at ${viewport.name} width`, async ({ context, page }) => {
|
||
|
|
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
||
|
|
const { backend, pageErrors } = await launchRateBarFixture({ context, page });
|
||
|
|
|
||
|
|
const wrapper = page.locator(".ryd-tooltip");
|
||
|
|
const container = page.locator("#return-youtube-dislike-bar-container");
|
||
|
|
const bar = page.locator("#return-youtube-dislike-bar");
|
||
|
|
const tooltip = page.locator("#ryd-dislike-tooltip");
|
||
|
|
const dislikeCount = page.locator('[data-ryd-role="dislike"] #text');
|
||
|
|
|
||
|
|
await expect(dislikeCount).toBeVisible();
|
||
|
|
await expect(dislikeCount).toHaveText(String(COUNTS.dislikes));
|
||
|
|
await expect(wrapper).toBeVisible();
|
||
|
|
await expect(container).toBeVisible();
|
||
|
|
await expect(bar).toBeVisible();
|
||
|
|
await expect(tooltip).toContainText("300 / 100");
|
||
|
|
await expect(tooltip).toHaveAttribute("role", "tooltip");
|
||
|
|
await expect(tooltip).toBeHidden();
|
||
|
|
await expect(page.locator("tp-yt-paper-tooltip#ryd-dislike-tooltip")).toHaveCount(0);
|
||
|
|
|
||
|
|
const idleTooltipStyle = await tooltip.evaluate((element) => {
|
||
|
|
const style = getComputedStyle(element);
|
||
|
|
return { opacity: style.opacity, visibility: style.visibility };
|
||
|
|
});
|
||
|
|
expect(idleTooltipStyle).toEqual({ opacity: "0", visibility: "hidden" });
|
||
|
|
|
||
|
|
await wrapper.hover({ position: { x: 1, y: 1 } });
|
||
|
|
await expect(tooltip).toBeVisible();
|
||
|
|
await expect.poll(() => tooltip.evaluate((element) => getComputedStyle(element).opacity)).toBe("1");
|
||
|
|
const hoveredTooltip = await tooltip.evaluate((element) => {
|
||
|
|
const bounds = element.getBoundingClientRect();
|
||
|
|
const style = getComputedStyle(element);
|
||
|
|
return {
|
||
|
|
backgroundColor: style.backgroundColor,
|
||
|
|
bottom: bounds.bottom,
|
||
|
|
color: style.color,
|
||
|
|
left: bounds.left,
|
||
|
|
opacity: style.opacity,
|
||
|
|
pointerEvents: style.pointerEvents,
|
||
|
|
position: style.position,
|
||
|
|
right: bounds.right,
|
||
|
|
visibility: style.visibility,
|
||
|
|
viewportWidth: innerWidth,
|
||
|
|
};
|
||
|
|
});
|
||
|
|
expect(hoveredTooltip).toMatchObject({
|
||
|
|
backgroundColor: "rgba(28, 28, 28, 0.96)",
|
||
|
|
color: "rgb(255, 255, 255)",
|
||
|
|
opacity: "1",
|
||
|
|
pointerEvents: "none",
|
||
|
|
position: "absolute",
|
||
|
|
visibility: "visible",
|
||
|
|
});
|
||
|
|
expect(hoveredTooltip.left).toBeGreaterThanOrEqual(0);
|
||
|
|
expect(hoveredTooltip.right).toBeLessThanOrEqual(hoveredTooltip.viewportWidth);
|
||
|
|
expect(hoveredTooltip.bottom).toBeGreaterThan(0);
|
||
|
|
|
||
|
|
await page.mouse.move(viewport.width - 1, viewport.height - 1);
|
||
|
|
await expect(tooltip).toBeHidden();
|
||
|
|
await wrapper.focus();
|
||
|
|
await expect(tooltip).toBeVisible();
|
||
|
|
await wrapper.evaluate((element) => element.blur());
|
||
|
|
await expect(tooltip).toBeHidden();
|
||
|
|
|
||
|
|
const geometry = await page.evaluate(() => {
|
||
|
|
const rect = (selector) => {
|
||
|
|
const bounds = document.querySelector(selector).getBoundingClientRect();
|
||
|
|
return {
|
||
|
|
bottom: bounds.bottom,
|
||
|
|
height: bounds.height,
|
||
|
|
left: bounds.left,
|
||
|
|
right: bounds.right,
|
||
|
|
top: bounds.top,
|
||
|
|
width: bounds.width,
|
||
|
|
};
|
||
|
|
};
|
||
|
|
return {
|
||
|
|
bar: rect("#return-youtube-dislike-bar"),
|
||
|
|
container: rect("#return-youtube-dislike-bar-container"),
|
||
|
|
dislike: rect('[data-ryd-role="dislike"] button'),
|
||
|
|
like: rect('[data-ryd-role="like"] button'),
|
||
|
|
viewport: { height: innerHeight, width: innerWidth },
|
||
|
|
wrapper: rect(".ryd-tooltip"),
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(geometry.container.width).toBeGreaterThan(0);
|
||
|
|
expect(geometry.container.height).toBeGreaterThan(0);
|
||
|
|
expect(geometry.bar.width).toBeGreaterThan(0);
|
||
|
|
expect(geometry.bar.height).toBeGreaterThan(0);
|
||
|
|
expect(geometry.bar.width / geometry.container.width).toBeCloseTo(0.75, 2);
|
||
|
|
expect(geometry.wrapper.width).toBeCloseTo(geometry.like.width + geometry.dislike.width, 0);
|
||
|
|
expect(geometry.container.left).toBeGreaterThanOrEqual(0);
|
||
|
|
expect(geometry.container.top).toBeGreaterThanOrEqual(0);
|
||
|
|
expect(geometry.container.right).toBeLessThanOrEqual(geometry.viewport.width);
|
||
|
|
expect(geometry.container.bottom).toBeLessThanOrEqual(geometry.viewport.height);
|
||
|
|
expect(geometry.container.top).toBeGreaterThanOrEqual(Math.max(geometry.like.bottom, geometry.dislike.bottom));
|
||
|
|
|
||
|
|
expectVisibleBarColors(await readBarColors(page), {
|
||
|
|
background: "rgb(15, 15, 15)",
|
||
|
|
negative: 139,
|
||
|
|
positive: "rgb(241, 241, 241)",
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(backend.blockedRequests).toEqual([]);
|
||
|
|
expect(pageErrors).toEqual([]);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
test("default ratio bar keeps both sides distinct in the light theme", async ({ context, page }) => {
|
||
|
|
await page.setViewportSize({ width: 1280, height: 720 });
|
||
|
|
const { backend, pageErrors } = await launchRateBarFixture({ context, page }, { theme: "light" });
|
||
|
|
|
||
|
|
expectVisibleBarColors(await readBarColors(page), {
|
||
|
|
background: "rgb(255, 255, 255)",
|
||
|
|
negative: 123,
|
||
|
|
positive: "rgb(15, 15, 15)",
|
||
|
|
});
|
||
|
|
expect(backend.blockedRequests).toEqual([]);
|
||
|
|
expect(pageErrors).toEqual([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("in-memory disabled option omits the ratio bar", async ({ context, page }) => {
|
||
|
|
await page.setViewportSize({ width: 1280, height: 720 });
|
||
|
|
const { backend, pageErrors } = await launchRateBarFixture({ context, page }, { rateBarEnabled: false });
|
||
|
|
|
||
|
|
await expect(page.locator("#return-youtube-dislike-bar-container")).toHaveCount(0);
|
||
|
|
await expect(page.locator("#return-youtube-dislike-bar")).toHaveCount(0);
|
||
|
|
await expect(page.locator("#ryd-dislike-tooltip")).toHaveCount(0);
|
||
|
|
expect(backend.blockedRequests).toEqual([]);
|
||
|
|
expect(pageErrors).toEqual([]);
|
||
|
|
});
|