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>
95 lines
4.0 KiB
TypeScript
95 lines
4.0 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
const suffix = () => `r${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`;
|
|
|
|
// Reserves an item by registering a customer and adding it to their cart, which
|
|
// is the only way an item legitimately reaches 'reserved'.
|
|
async function reserveItem(page: import('@playwright/test').Page, itemName: string) {
|
|
const email = `reserve-${suffix()}@example.com`;
|
|
|
|
const created = await page.request.post('/api/admin/items', {
|
|
multipart: { name: itemName, description: '', price: '75', category_id: '', tags: '[]' }
|
|
});
|
|
expect(created.ok()).toBeTruthy();
|
|
const itemId = (await created.json()).id as number;
|
|
|
|
await page.goto('/register');
|
|
await page.getByRole('textbox', { name: 'Email' }).fill(email);
|
|
await page.getByLabel('Password').fill('supersecret123');
|
|
await page.getByRole('button', { name: 'Create account' }).click();
|
|
await expect(page).toHaveURL(/\/account/);
|
|
|
|
const added = await page.request.post(`/api/cart/items/${itemId}`);
|
|
expect(added.status()).toBe(201);
|
|
|
|
return { email, itemId };
|
|
}
|
|
|
|
test.describe('Admin reserved items', () => {
|
|
test('shows a reserved count that opens the held items', async ({ page }) => {
|
|
const itemName = `Held ${suffix()}`;
|
|
const { email } = await reserveItem(page, itemName);
|
|
|
|
await page.goto('/admin');
|
|
await page.getByRole('tab', { name: 'Customers' }).click();
|
|
|
|
const row = page.getByRole('row').filter({ hasText: email });
|
|
await expect(row).toBeVisible();
|
|
await row.getByRole('button', { name: /item/ }).click();
|
|
|
|
const dialog = page.getByRole('dialog', { name: /Items reserved by/ });
|
|
await expect(dialog.getByText(itemName)).toBeVisible();
|
|
});
|
|
|
|
test('releasing an item returns it to the storefront as available', async ({ page }) => {
|
|
const itemName = `Freed ${suffix()}`;
|
|
const { email, itemId } = await reserveItem(page, itemName);
|
|
|
|
await page.goto('/admin');
|
|
await page.getByRole('tab', { name: 'Customers' }).click();
|
|
const row = page.getByRole('row').filter({ hasText: email });
|
|
await row.getByRole('button', { name: /item/ }).click();
|
|
|
|
const dialog = page.getByRole('dialog', { name: /Items reserved by/ });
|
|
await dialog.getByRole('button', { name: 'Release' }).click();
|
|
await expect(page.getByText(`Released "${itemName}"`)).toBeVisible();
|
|
|
|
// The point of releasing is that the item becomes purchasable again.
|
|
const item = await (await page.request.get(`/api/items/${itemId}`)).json();
|
|
expect(item.status).toBe('available');
|
|
});
|
|
|
|
test('the count drops once the item is released', async ({ page }) => {
|
|
const itemName = `Recount ${suffix()}`;
|
|
const { email } = await reserveItem(page, itemName);
|
|
|
|
await page.goto('/admin');
|
|
await page.getByRole('tab', { name: 'Customers' }).click();
|
|
const row = page.getByRole('row').filter({ hasText: email });
|
|
await row.getByRole('button', { name: /item/ }).click();
|
|
|
|
const dialog = page.getByRole('dialog', { name: /Items reserved by/ });
|
|
await dialog.getByRole('button', { name: 'Release' }).click();
|
|
await expect(dialog.getByText("This customer isn't holding any items")).toBeVisible();
|
|
|
|
// The row behind the dialog must agree with the dialog it opened.
|
|
await dialog.getByRole('button', { name: 'Close' }).click();
|
|
await expect(row.getByRole('button', { name: /item/ })).toHaveCount(0);
|
|
});
|
|
|
|
test('a customer holding nothing shows no link to click', async ({ page }) => {
|
|
const email = `idle-${suffix()}@example.com`;
|
|
await page.goto('/register');
|
|
await page.getByRole('textbox', { name: 'Email' }).fill(email);
|
|
await page.getByLabel('Password').fill('supersecret123');
|
|
await page.getByRole('button', { name: 'Create account' }).click();
|
|
await expect(page).toHaveURL(/\/account/);
|
|
|
|
await page.goto('/admin');
|
|
await page.getByRole('tab', { name: 'Customers' }).click();
|
|
const row = page.getByRole('row').filter({ hasText: email });
|
|
await expect(row).toBeVisible();
|
|
await expect(row.getByRole('button', { name: /item/ })).toHaveCount(0);
|
|
});
|
|
});
|