fix(e2e): restore the frontend build by typing the responses findOrFail searches (#254)
Linting / lint (pull_request) Successful in 2m10s
SonarQube Analysis / sonarqube (pull_request) Successful in 28m5s

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>
This commit is contained in:
2026-09-01 09:09:01 -05:00
co-authored by Claude Opus 5
parent 3ab265a653
commit 42412e8eeb
6 changed files with 42 additions and 19 deletions
+16 -8
View File
@@ -50,10 +50,12 @@ test.describe('Editing the customer emails', () => {
await expect(page.getByText('Password reset saved')).toBeVisible();
// Persisted, not merely accepted by the form.
const stored = await (await page.request.get('/api/admin/email-templates')).json();
const stored: { key: string; subject: string | null; body: string | null }[] = await (
await page.request.get('/api/admin/email-templates')
).json();
const reset = findOrFail(
stored,
(t: { key: string }) => t.key === 'passwordReset',
(t) => t.key === 'passwordReset',
'the passwordReset template'
);
expect(reset.subject).toBe(subject);
@@ -72,10 +74,12 @@ test.describe('Editing the customer emails', () => {
await expect(page.getByText('the body must keep {{resetUrl}}')).toBeVisible();
// And nothing was stored.
const stored = await (await page.request.get('/api/admin/email-templates')).json();
const stored: { key: string; subject: string | null; body: string | null }[] = await (
await page.request.get('/api/admin/email-templates')
).json();
const reset = findOrFail(
stored,
(t: { key: string }) => t.key === 'passwordReset',
(t) => t.key === 'passwordReset',
'the passwordReset template'
);
expect(reset.body).toBeNull();
@@ -95,10 +99,12 @@ test.describe('Editing the customer emails', () => {
await expect(page.getByText('Password reset restored to the default')).toBeVisible();
const stored = await (await page.request.get('/api/admin/email-templates')).json();
const stored: { key: string; subject: string | null; body: string | null }[] = await (
await page.request.get('/api/admin/email-templates')
).json();
const reset = findOrFail(
stored,
(t: { key: string }) => t.key === 'passwordReset',
(t) => t.key === 'passwordReset',
'the passwordReset template'
);
expect(reset.subject).toBeNull();
@@ -124,10 +130,12 @@ test.describe('Previewing the customer emails', () => {
// point: an admin sees the effect before committing to it.
await expect(adminEmails.preview('Password reset').getByText(wording)).toBeVisible();
const stored = await (await page.request.get('/api/admin/email-templates')).json();
const stored: { key: string; subject: string | null; body: string | null }[] = await (
await page.request.get('/api/admin/email-templates')
).json();
expect(findOrFail(
stored,
(t: { key: string }) => t.key === 'passwordReset',
(t) => t.key === 'passwordReset',
'the passwordReset template'
).body).toBeNull();
});