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