import { expect, fulfillJson, mockTeamSettings, TEAM, TEAM_SETTINGS_PATH, test, } from "./helpers/run-checks-fixtures"; import type { Locator } from "@playwright/test"; async function switchAppearance(control: Locator) { return control.evaluate((element) => { const canvas = document.createElement("canvas"); canvas.width = 1; canvas.height = 1; const context = canvas.getContext("2d")!; const luminance = (color: string) => { context.clearRect(0, 0, 1, 1); context.fillStyle = color; context.fillRect(0, 0, 1, 1); const [red, green, blue] = context.getImageData(0, 0, 1, 1).data; const linear = (channel: number) => { const value = channel / 255; return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4; }; return ( 0.2126 * linear(red) + 0.7152 * linear(green) + 0.0722 * linear(blue) ); }; const contrast = (first: string, second: string) => { const a = luminance(first); const b = luminance(second); return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05); }; const style = getComputedStyle(element); const thumb = element.querySelector('[data-slot="switch-thumb"]')!; const secondarySurface = document.createElement("span"); secondarySurface.style.backgroundColor = "var(--secondary)"; element.append(secondarySurface); const secondaryColor = getComputedStyle(secondarySurface).backgroundColor; secondarySurface.style.backgroundColor = "var(--switch-on)"; const onColor = getComputedStyle(secondarySurface).backgroundColor; secondarySurface.remove(); const pageColor = getComputedStyle(document.body).backgroundColor; secondarySurface.style.backgroundColor = "var(--ring)"; element.append(secondarySurface); const ringColor = getComputedStyle(secondarySurface).backgroundColor; secondarySurface.remove(); return { border: style.borderColor, background: style.backgroundColor, secondaryColor, onColor, ringColor, thumbColor: getComputedStyle(thumb).backgroundColor, outlineContrast: contrast(style.borderColor, pageColor), thumbPageContrast: contrast( getComputedStyle(thumb).backgroundColor, pageColor ), thumbContrast: contrast( getComputedStyle(thumb).backgroundColor, style.backgroundColor ), thumbX: thumb.getBoundingClientRect().x, thumbRight: thumb.getBoundingClientRect().right, padding: Number.parseFloat(style.paddingRight) + Number.parseFloat(style.borderRightWidth), right: element.getBoundingClientRect().right, }; }); } for (const theme of ["light", "dark"] as const) { test(`${theme} switches show a grey thumb on an outlined track when off and a white thumb on green when on`, async ({ page, }) => { await mockTeamSettings(page); await page.route(/\/api\/settings(?:\?.*)?$/, (route) => fulfillJson(route, { default_model: null, theme }) ); await page.emulateMedia({ colorScheme: theme, reducedMotion: "reduce" }); await page.addInitScript( (value) => localStorage.setItem("theme", value), theme ); let enabled = false; await page.route(`**/api/teams/${TEAM.id}/signup-domain`, (route) => { if (route.request().method() === "PATCH") { enabled = route.request().postDataJSON().enabled; } return fulfillJson(route, { enabled, domain: "example.com", eligibleDomain: "example.com", viewer: { canManage: true }, }); }); await page.goto(`${TEAM_SETTINGS_PATH}/members`); await expect(page.locator("html")).toHaveClass(new RegExp(theme)); const control = page.getByRole("switch", { name: "Allow people from @example.com to join", }); await expect(control).toBeEnabled(); const off = await switchAppearance(control); expect(off.border).not.toBe("rgba(0, 0, 0, 0)"); // The off track is deliberately quiet; the thumb carries the 3:1 contrast. expect(off.thumbPageContrast).toBeGreaterThanOrEqual(3); expect(off.background).toBe(off.secondaryColor); expect(off.thumbContrast).toBeGreaterThanOrEqual(3); await expect(control).toHaveText(""); await control.focus(); await expect(control).toBeFocused(); await page.keyboard.press("Tab"); await page.keyboard.press("Shift+Tab"); await expect(control).toBeFocused(); const focused = await switchAppearance(control); // Keyboard focus recolors the border with --ring, as before the restyle. expect(focused.border).toBe(focused.ringColor); await page.keyboard.press("Space"); await expect(control).toBeChecked(); await expect(control).toHaveText(""); const on = await switchAppearance(control); expect(on.thumbX).toBeGreaterThan(off.thumbX + 10); expect(on.onColor).not.toBe("rgba(0, 0, 0, 0)"); expect(on.background).toBe(on.onColor); expect(on.thumbColor).toBe("rgb(255, 255, 255)"); expect(on.outlineContrast).toBeGreaterThanOrEqual(3); expect(on.thumbContrast).toBeGreaterThanOrEqual(3); await control.evaluate((element) => { element.style.width = "80px"; element.style.height = "32px"; element.style.padding = "4px"; }); const resized = await switchAppearance(control); expect(resized.right - resized.thumbRight).toBeCloseTo(resized.padding); await page.setViewportSize({ width: 390, height: 844 }); expect((await switchAppearance(control)).right).toBeLessThanOrEqual(390); await page.screenshot({ path: test.info().outputPath(`${theme}-switches.png`), }); }); }