test(e2e): add a find-or-fail helper for collection lookups (#241)
Nine sites across five specs do `collection.find(...)` and dereference the result immediately. When the row is missing the test dies with "Cannot read properties of undefined" naming a line of test plumbing, which says nothing about what was expected — and that is exactly how favorites-filter:169 failed without producing a usable signal. The message names what was wanted and how many rows were searched. That distinction carries real diagnostic weight: "0 rows" means the fixture never landed, "37 rows" means it landed and the predicate is wrong, and those are different bugs to chase. It lives in its own module importing nothing, rather than in support/api.ts. That file imports @playwright/test, and vitest.config.ts runs tests/unit with environment: 'node' — putting six lines of pure logic there would drag a browser harness into the unit suite to test them. api.ts re-exports it so specs still reach it through fixtures. Throws rather than returning null, because every caller wants the row: an error at the point of the miss beats a null threaded through three more lines before something unrelated fails. Frontend: 30 unit tests pass, lint unchanged at 2 pre-existing warnings, build clean. Ref #241
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
import { APIRequestContext, expect } from '@playwright/test';
|
import { APIRequestContext, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
// Re-exported so specs get it from './fixtures' with everything else, rather
|
||||||
|
// than reaching into support/ directly — fixtures.ts does `export * from
|
||||||
|
// './support/api'`. It lives in its own module because that one imports
|
||||||
|
// nothing, which is what lets the Vitest unit suite cover it without loading
|
||||||
|
// @playwright/test. See findOrFail.ts.
|
||||||
|
export { findOrFail } from './findOrFail';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Where the app under test is served.
|
* Where the app under test is served.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Picks one row out of a collection, failing loudly when it is not there.
|
||||||
|
*
|
||||||
|
* The pattern this replaces is `collection.find(...)` dereferenced straight
|
||||||
|
* away — `customers.find(c => c.email === x).id`. When the row is missing the
|
||||||
|
* test dies with "Cannot read properties of undefined" pointing at a line of
|
||||||
|
* test plumbing, which says nothing about what was expected. The suite runs
|
||||||
|
* fullyParallel against one shared database, so a lookup over a collection
|
||||||
|
* other specs also write to can miss for reasons that have nothing to do with
|
||||||
|
* the behaviour under test (#241).
|
||||||
|
*
|
||||||
|
* Deliberately imports nothing. It lives apart from `api.ts` so the Vitest
|
||||||
|
* unit suite can cover it without pulling `@playwright/test` — and therefore a
|
||||||
|
* browser harness — into a run configured for `environment: 'node'`.
|
||||||
|
*
|
||||||
|
* Throws rather than returning null: every caller wants the row, and an
|
||||||
|
* Error at the point of the miss beats a null threaded through three more
|
||||||
|
* lines before something else fails.
|
||||||
|
*/
|
||||||
|
export function findOrFail<T>(
|
||||||
|
items: T[],
|
||||||
|
predicate: (item: T) => boolean,
|
||||||
|
description: string
|
||||||
|
): T {
|
||||||
|
const found = items.find(predicate);
|
||||||
|
|
||||||
|
if (found === undefined) {
|
||||||
|
// The count matters as much as the description: "0 rows" means the
|
||||||
|
// fixture never landed, while "37 rows" means it landed and the predicate
|
||||||
|
// is wrong. Those are different bugs and the message should tell them
|
||||||
|
// apart without a re-run.
|
||||||
|
throw new Error(
|
||||||
|
`expected to find ${description}, but none of the ${items.length} rows matched`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { findOrFail } from '../e2e/support/findOrFail';
|
||||||
|
|
||||||
|
interface Row {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows: Row[] = [
|
||||||
|
{ id: 1, name: 'first' },
|
||||||
|
{ id: 2, name: 'second' }
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('findOrFail', () => {
|
||||||
|
it('returns the matching row', () => {
|
||||||
|
expect(findOrFail(rows, (r) => r.name === 'second', 'the second row')).toEqual({
|
||||||
|
id: 2,
|
||||||
|
name: 'second'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the first match when several qualify', () => {
|
||||||
|
expect(findOrFail(rows, () => true, 'anything').id).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The whole point. Dereferencing a missed `.find()` gives "Cannot read
|
||||||
|
// properties of undefined" pointing at test plumbing; this says what was
|
||||||
|
// wanted and how many rows were searched.
|
||||||
|
it('throws naming what it looked for', () => {
|
||||||
|
expect(() => findOrFail(rows, (r) => r.name === 'absent', 'the absent row')).toThrow(
|
||||||
|
/the absent row/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports how many rows it searched', () => {
|
||||||
|
expect(() => findOrFail(rows, () => false, 'nothing')).toThrow(/2 rows/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('says so when the collection was empty, which reads differently', () => {
|
||||||
|
expect(() => findOrFail([], () => true, 'anything')).toThrow(/0 rows/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user