Shared Playwright fixture that normalizes pixel-test UI state and
auto-asserts no console errors after every E2E test.
The previous setup left it to each spec to remember an explicit
assertNoConsoleErrors(page) call, and most never did — exactly the
surface where console errors are the first symptom of regression.
This fixture moves that check into the harness, taken from Playwright's
own console / pageerror page events, so it runs whether the spec
author remembered or not. (71 specs today; 63 import test from here,
the other 8 import @playwright/test directly and get no teardown at
all. 45 of those 63 still make no explicit call of their own.)
Specs that genuinely tolerate certain errors annotate the test:
test('intentionally errors when the URL is bad', async ({ page }) => { test.info().annotations.push({ type:ALLOW_CONSOLE_ERRORS, description:'Bad-URL recovery surfaces a console.error by design.', }); // ... test body ... });
The annotation type is checked verbatim — typos turn into hard
failures rather than silent opt-outs.
ONE gate, and it is the Playwright-side one (#1760): page.on('console')
where msg.type() === 'error', plus page.on('pageerror'). That is
deliberately the wider of the two sources available. It sees errors fired
BEFORE the viewer's in-page console interceptor installs (a mistyped asset,
a pre-init ReferenceError), it sees uncaught exceptions via pageerror
and browser-generated errors the app never routed through console, it
accumulates into an unbounded array rather than the interceptor's ring
buffer (which evicts at DEFAULT_MAX_BUFFER_SIZE), and it survives
navigation, which resets the in-page buffer. Everything the in-page buffer
holds under errors reaches it too: every write into that buffer goes
through the interceptor's private captureMessage, reachable only from its
five patch() closures, each of which re-emits through
this.originalConsole.<method> (src/utils/console-interceptor.ts).
So the fixture does NOT read the in-page buffer. It used to, via
assertNoConsoleErrors — which consults only messages.errors, i.e. a
second, narrower opinion on the verdict just rendered above — at the price
of a page.evaluate round trip a saturated main thread can withhold for
minutes (#1651/#1746/#1747/#1760). A spec that wants the in-page buffer
specifically — for its warnings / logs buckets, which have no
Playwright-side gate, or for a stricter no-allow-list check — still calls
assertNoConsoleErrors / getConsoleMessages itself, and several do.
Shared Playwright fixture that normalizes pixel-test UI state and auto-asserts no console errors after every E2E test.
The previous setup left it to each spec to remember an explicit
assertNoConsoleErrors(page)call, and most never did — exactly the surface where console errors are the first symptom of regression. This fixture moves that check into the harness, taken from Playwright's ownconsole/pageerrorpage events, so it runs whether the spec author remembered or not. (71 specs today; 63 importtestfrom here, the other 8 import@playwright/testdirectly and get no teardown at all. 45 of those 63 still make no explicit call of their own.)Specs that genuinely tolerate certain errors annotate the test:
The annotation type is checked verbatim — typos turn into hard failures rather than silent opt-outs.
ONE gate, and it is the Playwright-side one (#1760):
page.on('console')wheremsg.type() === 'error', pluspage.on('pageerror'). That is deliberately the wider of the two sources available. It sees errors fired BEFORE the viewer's in-page console interceptor installs (a mistyped asset, a pre-initReferenceError), it sees uncaught exceptions viapageerrorand browser-generated errors the app never routed throughconsole, it accumulates into an unbounded array rather than the interceptor's ring buffer (which evicts atDEFAULT_MAX_BUFFER_SIZE), and it survives navigation, which resets the in-page buffer. Everything the in-page buffer holds undererrorsreaches it too: every write into that buffer goes through the interceptor's privatecaptureMessage, reachable only from its fivepatch()closures, each of which re-emits throughthis.originalConsole.<method>(src/utils/console-interceptor.ts).So the fixture does NOT read the in-page buffer. It used to, via
assertNoConsoleErrors— which consults onlymessages.errors, i.e. a second, narrower opinion on the verdict just rendered above — at the price of apage.evaluateround trip a saturated main thread can withhold for minutes (#1651/#1746/#1747/#1760). A spec that wants the in-page buffer specifically — for itswarnings/logsbuckets, which have no Playwright-side gate, or for a stricter no-allow-list check — still callsassertNoConsoleErrors/getConsoleMessagesitself, and several do.