main did not build, so every Portainer deploy failed with `npm run build` exit code 2. My regression from #241. findOrFail<T>(items: T[], predicate: (item: T) => boolean) infers T from both parameters. The call sites annotate the predicate as documentation, and the arrays come from .json(), which is any and offers no competing candidate — so T became the one-field shape written in the lambda and every caller failed on the field it actually wanted. Array.prototype.find has no such problem, which is why the code this replaced type-checked. The four responses are now typed at their call sites, so T is inferred from real data and the predicates need no annotation. findOrFail additionally takes NoInfer<T> on its predicate, so a stray annotation can never drive the element type again. The specs are better typed than before this change: `.json()` was plain any, and the annotations only ever documented a shape nothing enforced. I did not catch this because I verified with a bare `npx tsc --noEmit`, and tsconfig.json is `"include": ["src"]` — it structurally cannot see tests/. The specs are checked by the second command in `npm run build`, which is the step Docker runs and the step that failed. Verified this time with `npm run build` itself, plus lint, the unit suite, and two consecutive full e2e passes. Closes #254 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
66 lines
2.5 KiB
TypeScript
66 lines
2.5 KiB
TypeScript
import { test, expect, uniqueSuffix, createCategory, findOrFail } from './fixtures';
|
|
|
|
test.describe('Inline category creation from the item form', () => {
|
|
test('creates a category without leaving the item form and assigns it', async ({
|
|
page,
|
|
admin,
|
|
adminInventory
|
|
}) => {
|
|
const RUN = `i${uniqueSuffix()}`;
|
|
const categoryName = `Inline ${RUN}`;
|
|
const itemName = `Item ${RUN}`;
|
|
|
|
await admin.goto();
|
|
await adminInventory.openItemFormLoaded();
|
|
await adminInventory.name.fill(itemName);
|
|
await adminInventory.price.fill('99');
|
|
|
|
await adminInventory.categoryField.click();
|
|
await expect(adminInventory.newCategoryName).toBeVisible();
|
|
await expect(adminInventory.createCategoryButton).toBeVisible();
|
|
await adminInventory.newCategoryName.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 adminInventory.newCategoryName.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(adminInventory.formDialog.getByText(categoryName)).toBeVisible();
|
|
|
|
await adminInventory.confirmButton.click();
|
|
await expect(page.getByText('Item added')).toBeVisible();
|
|
|
|
const items: { name: string; category_name: string | null }[] = await (
|
|
await page.request.get('/api/admin/items')
|
|
).json();
|
|
// findOrFail already fails with the row count when the item is missing,
|
|
// which the toBeTruthy assertion this replaces could not report.
|
|
const saved = findOrFail(
|
|
items,
|
|
(item) => item.name === itemName,
|
|
`the item named ${itemName}`
|
|
);
|
|
expect(saved.category_name).toBe(categoryName);
|
|
});
|
|
|
|
test('reports a duplicate category name instead of silently doing nothing', async ({
|
|
page,
|
|
admin,
|
|
adminInventory
|
|
}) => {
|
|
const categoryName = `Dupe i${uniqueSuffix()}`;
|
|
await createCategory(page.request, categoryName);
|
|
|
|
await admin.goto();
|
|
await adminInventory.openItemFormLoaded();
|
|
await adminInventory.categoryField.click();
|
|
await expect(adminInventory.newCategoryName).toBeVisible();
|
|
await expect(adminInventory.createCategoryButton).toBeVisible();
|
|
await adminInventory.newCategoryName.fill(categoryName);
|
|
await adminInventory.newCategoryName.press('Enter');
|
|
|
|
await expect(page.getByText(/already exists/i)).toBeVisible();
|
|
});
|
|
});
|