Files
return-youtube-dislike/Extensions/UserScript/e2e/userscript-rate-bar.e2e.js
T

220 lines
8.9 KiB
JavaScript
Raw Normal View History

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([]);
});