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>
62 lines
2.3 KiB
TypeScript
62 lines
2.3 KiB
TypeScript
import { test, expect, uniqueSuffix, findOrFail } from './fixtures';
|
|
|
|
// 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.
|
|
|
|
test.describe('Admin taxonomy', () => {
|
|
test('creates a category and a nested child that stays visible', async ({
|
|
page,
|
|
admin,
|
|
adminTaxonomy
|
|
}) => {
|
|
const RUN = `a${uniqueSuffix()}`;
|
|
await admin.open('Categories');
|
|
|
|
await adminTaxonomy.addCategory(`Furniture ${RUN}`);
|
|
await expect(page.getByText(`Furniture ${RUN}`)).toBeVisible();
|
|
|
|
await adminTaxonomy.addChildCategory(`Furniture ${RUN}`, `Tables ${RUN}`);
|
|
|
|
// 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,
|
|
admin,
|
|
adminTaxonomy
|
|
}) => {
|
|
const RUN = `a${uniqueSuffix()}`;
|
|
await admin.open('Tags');
|
|
|
|
await adminTaxonomy.addTag(`vintage-${RUN}`);
|
|
// Clicking OK only dispatches the request; wait for the confirmation so the
|
|
// lookup below can't race the create.
|
|
await expect(adminTaxonomy.tagAddedNotice).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: { name: string; color: string | null }[] = await (
|
|
await page.request.get('/api/admin/tags')
|
|
).json();
|
|
const created = findOrFail(
|
|
tags,
|
|
(tag) => tag.name === `vintage-${RUN}`,
|
|
`the tag vintage-${RUN}`
|
|
);
|
|
expect(created.color).toBeTruthy();
|
|
});
|
|
|
|
test('offers category and tag fields on the item form', async ({ admin, adminInventory }) => {
|
|
await admin.goto();
|
|
await adminInventory.openItemForm();
|
|
|
|
await expect(adminInventory.formDialog.getByText('Category', { exact: true })).toBeVisible();
|
|
await expect(adminInventory.formDialog.getByText('Pick existing tags')).toBeVisible();
|
|
});
|
|
});
|