Files
redefined-designs/frontend/tests/e2e/admin-theme.spec.ts
T
bermudalambandClaude Opus 5 f537314259
SonarQube Analysis / sonarqube (pull_request) Successful in 4m23s
Tests / backend-unit (pull_request) Successful in 1m6s
Tests / backend-integration (pull_request) Failing after 4m55s
Tests / frontend-e2e (pull_request) Failing after 12m41s
fix(admin): theme, American English, and inventory/reservation tooling (#27)
Seven reported items, of which the first four had two root causes.

The active tab was invisible in dark mode because colorPrimary was
hardcoded to #1a1a1a in both themes. The accent now inverts with the
theme, and colorTextLightSolid inverts with it, or a near-white accent
would get antd's default white label and disappear.

The Category tab, Tag tab, and item-form category selector ignored the
theme entirely. antd declares main: lib/index.js and module: es/index.js,
so importing from 'antd' resolves to the ES build while 'antd/lib/...'
loads the CommonJS one — two copies, two React contexts, and no
ConfigProvider for anything deep-imported. Switching those files to
antd/es/* keeps the deep-import convention and shares the instance. This
was introduced by my own use of the lib path; es is correct under Vite.
Two storefront components had the same latent bug.

"Colour" is now "Color".

The Customers tab shows how many items each customer is holding, as a
link opening the item list with a Release button. Release mirrors the
customer's own cart removal — drop the cart row, return the item to
available, guarded on 'reserved' so it can never resurrect a sold item —
and deliberately sends no email about an action the customer did not
take. The count is a subquery rather than another join, which would have
multiplied rows and inflated order_count and total_spent_cents.

The Inventory tab filters by category, tags, price, and status, reusing
the storefront's parser and query builder so the two cannot drift.
Reserved is one option in a Status filter rather than a standalone toggle.

Also fixes two defects the screenshots exposed: the reserved-count link
bubbled to the row handler and opened the customer drawer behind the
dialog, and .admin-category-node had no CSS at all, so the tree node name,
item count, and actions ran together as one string.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:49:05 -05:00

95 lines
3.8 KiB
TypeScript

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);
});
});