Storefront gains a Filters drawer holding the category tree, colour-coded tag pills, and a price range, with applied filters shown as removable chips. Filter state lives in the URL query string, so a filtered view is shareable and the back button works. Item cards now show their category and tags. Admin gains Categories and Tags tabs, and the item form gains a category TreeSelect plus a tags Select that creates new tags on the fly. The admin category tree tracks expansion in state rather than using defaultExpandAll: that prop is evaluated once at mount, so a branch added afterwards rendered collapsed and its children were unreachable. Creating or moving a node now expands its parent. Caught by the new admin e2e spec. The chip row is marked as a named group so its "Clear all" stays distinguishable from the drawer's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
63 lines
2.8 KiB
TypeScript
63 lines
2.8 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
// The e2e database is shared and never reset, so every fixture name carries a
|
|
// unique suffix and assertions are scoped to the nodes this run created. The
|
|
// suffix is generated per test rather than per module: a worker can run this
|
|
// file more than once, and a module-level constant would collide with itself.
|
|
const suffix = () => `a${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`;
|
|
|
|
test.describe('Admin taxonomy', () => {
|
|
test('creates a category and a nested child that stays visible', async ({ page }) => {
|
|
const RUN = suffix();
|
|
await page.goto('/admin');
|
|
await page.getByRole('tab', { name: 'Categories' }).click();
|
|
|
|
await page.getByRole('button', { name: 'Add Category' }).click();
|
|
await page.getByLabel('Name').fill(`Furniture ${RUN}`);
|
|
await page.getByRole('button', { name: 'OK' }).click();
|
|
await expect(page.getByText(`Furniture ${RUN}`)).toBeVisible();
|
|
|
|
await page
|
|
.getByRole('treeitem')
|
|
.filter({ hasText: `Furniture ${RUN}` })
|
|
.getByRole('button', { name: 'Add child' })
|
|
.click();
|
|
await page.getByLabel('Name').fill(`Tables ${RUN}`);
|
|
await page.getByRole('button', { name: 'OK' }).click();
|
|
|
|
// The tree is mounted before this branch exists, so the child is only
|
|
// visible if expansion follows newly created nodes rather than the state
|
|
// captured at first render.
|
|
await expect(page.getByText(`Tables ${RUN}`)).toBeVisible();
|
|
});
|
|
|
|
test('creates a tag with an automatically assigned colour', async ({ page }) => {
|
|
const RUN = suffix();
|
|
await page.goto('/admin');
|
|
await page.getByRole('tab', { name: 'Tags' }).click();
|
|
|
|
await page.getByRole('button', { name: 'Add Tag' }).click();
|
|
await page.getByLabel('Name').fill(`vintage-${RUN}`);
|
|
await page.getByRole('button', { name: 'OK' }).click();
|
|
// Clicking OK only dispatches the request; wait for the confirmation so the
|
|
// lookup below can't race the create.
|
|
await expect(page.getByText('Tag added')).toBeVisible();
|
|
|
|
// The table paginates and the shared database holds many tags, so the new
|
|
// row is confirmed through the API rather than hunted for across pages.
|
|
const tags = await (await page.request.get('/api/admin/tags')).json();
|
|
const created = tags.find((tag: { name: string }) => tag.name === `vintage-${RUN}`);
|
|
expect(created).toBeTruthy();
|
|
expect(created.color).toBeTruthy();
|
|
});
|
|
|
|
test('offers category and tag fields on the item form', async ({ page }) => {
|
|
await page.goto('/admin');
|
|
await page.getByRole('button', { name: 'Add Item' }).click();
|
|
|
|
const modal = page.getByRole('dialog');
|
|
await expect(modal.getByText('Category', { exact: true })).toBeVisible();
|
|
await expect(modal.getByText('Pick existing tags')).toBeVisible();
|
|
});
|
|
});
|