Files
redefined-designs/frontend/tests/e2e/admin-inline-category.spec.ts
T
bermudalambandClaude Opus 5 77e58c0b92 fix(admin): confirm writes succeeded and allow inline category creation (#23)
Reported from testing: the item form said "Item added" for a save that
never happened.

saveItem and the other admin calls returned res.json() without checking
res.ok, so a 4xx/5xx resolved normally and every caller reported success
for a write the server had rejected. That is worse than failing outright,
because nothing prompts the user to look for the missing row. All admin
calls now throw on a non-OK response, and the handlers report the error,
keep the form open so entered values survive, and only claim success once
the server has accepted the write. Mark sold/available previously did
nothing visible on failure at all.

Categories can now be created from the item form, as tags already could.
Previously a category that did not exist yet meant abandoning a
half-filled form for the Categories tab. New categories are created at
the top level; nesting stays in the Categories tab.

The control lives in its own component: inline, every keystroke
re-rendered the whole Inventory component and rebuilt the category tree,
which visibly jittered the open popup. It sits above the tree rather than
below it, where a long list both hid it and made its position depend on
the list's measured height. The tree no longer expands everything on
open, which does not scale past a screenful; it has search instead.

The app now honours prefers-reduced-motion by disabling antd transitions,
and the e2e suite runs with that preference set.

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

78 lines
3.6 KiB
TypeScript

import { test, expect } from '@playwright/test';
const suffix = () => `i${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`;
// Opening the form kicks off a categories fetch. Interacting with the category
// field before it lands means the tree re-renders under the cursor, so wait for
// the data rather than racing it.
async function openItemForm(page: import('@playwright/test').Page) {
// Opening the form fetches both categories and tags, and each one re-renders
// the modal as it lands. Waiting for only one still leaves the second to
// reflow the popup mid-interaction.
const loaded = Promise.all([
page.waitForResponse((res) => res.url().includes('/api/admin/categories') && res.request().method() === 'GET'),
page.waitForResponse((res) => res.url().includes('/api/admin/tags') && res.request().method() === 'GET')
]);
await page.getByRole('button', { name: 'Add Item' }).click();
await loaded;
}
test.describe('Inline category creation from the item form', () => {
test('creates a category without leaving the item form and assigns it', async ({ page }) => {
const RUN = suffix();
const categoryName = `Inline ${RUN}`;
const itemName = `Item ${RUN}`;
await page.goto('/admin');
await openItemForm(page);
await page.getByLabel('Name').fill(itemName);
await page.getByLabel('Price (USD)').fill('99');
await page.getByRole('dialog').getByLabel('Category', { exact: true }).click();
const nameInput = page.getByPlaceholder('New category name');
await expect(nameInput).toBeVisible();
await expect(page.getByRole('button', { name: 'Create category' })).toBeVisible();
await nameInput.fill(categoryName);
// Submitted with Enter rather than a click: the popup sits over a
// virtualized tree that keeps re-measuring, so a click target inside it is
// never geometrically stable. Enter runs the same handler as the button.
await nameInput.press('Enter');
// The new category should be selected straight away — having to hunt for it
// in the tree afterwards defeats the point of creating it inline.
await expect(page.getByRole('dialog').getByText(categoryName)).toBeVisible();
await page.getByRole('button', { name: 'OK' }).click();
await expect(page.getByText('Item added')).toBeVisible();
const items = await (await page.request.get('/api/admin/items')).json();
const saved = items.find((item: { name: string }) => item.name === itemName);
expect(saved).toBeTruthy();
expect(saved.category_name).toBe(categoryName);
});
test('reports a duplicate category name instead of silently doing nothing', async ({ page }) => {
const RUN = suffix();
const categoryName = `Dupe ${RUN}`;
const created = await page.request.post('/api/admin/categories', {
data: { name: categoryName, parent_id: null }
});
expect(created.status()).toBe(201);
await page.goto('/admin');
await openItemForm(page);
await page.getByRole('dialog').getByLabel('Category', { exact: true }).click();
const nameInput = page.getByPlaceholder('New category name');
await expect(nameInput).toBeVisible();
await expect(page.getByRole('button', { name: 'Create category' })).toBeVisible();
await nameInput.fill(categoryName);
// Submitted with Enter rather than a click: the popup sits over a
// virtualized tree that keeps re-measuring, so a click target inside it is
// never geometrically stable. Enter runs the same handler as the button.
await nameInput.press('Enter');
await expect(page.getByText(/already exists/i)).toBeVisible();
});
});