test(e2e): design, plan, and the find-or-fail helper (#241) #247
@@ -1,5 +1,12 @@
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -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