import { test, expect, Page } from '@playwright/test'; // Relative luminance per WCAG, used to tell "light" from "dark" without // asserting exact hex values, which would break on any palette tweak. function luminance(rgb: string): number { const [r, g, b] = (rgb.match(/\d+(\.\d+)?/g) ?? ['0', '0', '0']).slice(0, 3).map(Number); const channel = (c: number) => { const s = c / 255; return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); }; return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b); } const suffix = () => `t${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`; // The Categories and Tags tabs render an empty state when there is nothing to // show, and the shared dev database gets truncated by the backend integration // suite. Seed what each test needs rather than depending on what happens to be // there. async function seed(page: Page) { const run = suffix(); await page.request.post('/api/admin/categories', { data: { name: `Theme ${run}`, parent_id: null } }); await page.request.post('/api/admin/tags', { data: { name: `theme-${run}` } }); } async function switchToDark(page: Page) { await page.goto('/admin'); const toggle = page.getByRole('switch'); if ((await toggle.getAttribute('aria-checked')) !== 'true') { await toggle.click(); } await expect(page.locator('body')).toHaveAttribute('data-theme', 'dark'); } test.describe('Admin dark mode', () => { test('the active tab label is readable against the dark background', async ({ page }) => { await switchToDark(page); const label = page.locator('.ant-tabs-tab-active .ant-tabs-tab-btn').first(); // The bug: colorPrimary stayed #1a1a1a in dark mode, so the active tab was // near-black text on a near-black background. await expect .poll(async () => luminance(await label.evaluate((el) => getComputedStyle(el).color))) .toBeGreaterThan(0.5); }); test('the Categories tab follows the dark theme', async ({ page }) => { await seed(page); await switchToDark(page); await page.getByRole('tab', { name: 'Categories' }).click(); const tree = page.locator('.ant-tabs-tabpane-active .ant-tree').first(); await expect(tree).toBeVisible(); const bg = await tree.evaluate((el) => getComputedStyle(el).backgroundColor); // The bug: deep imports from antd/lib loaded a second copy of antd that // never saw ConfigProvider, so this rendered pure white in dark mode. expect(luminance(bg)).toBeLessThan(0.5); }); test('the Tags tab follows the dark theme', async ({ page }) => { await seed(page); await switchToDark(page); await page.getByRole('tab', { name: 'Tags' }).click(); const table = page.locator('.ant-tabs-tabpane-active .ant-table').first(); await expect(table).toBeVisible(); const bg = await table.evaluate((el) => getComputedStyle(el).backgroundColor); expect(luminance(bg)).toBeLessThan(0.5); }); test('the item form category selector follows the dark theme', async ({ page }) => { await switchToDark(page); await page.getByRole('button', { name: 'Add Item' }).click(); await page.getByRole('dialog').getByLabel('Category', { exact: true }).click(); const popup = page.locator('.ant-select-dropdown').first(); await expect(popup).toBeVisible(); const bg = await popup.evaluate((el) => getComputedStyle(el).backgroundColor); expect(luminance(bg)).toBeLessThan(0.5); }); }); test.describe('Admin copy', () => { test('uses American English spelling for color', async ({ page }) => { await seed(page); await page.goto('/admin'); await page.getByRole('tab', { name: 'Tags' }).click(); await expect(page.getByRole('columnheader', { name: 'Color' })).toBeVisible(); await expect(page.getByText('Colour')).toHaveCount(0); }); });