How can a Playwright test tell mid-run that an earlier expect.soft() check already failed?
answer
- The running test knows what failed
- Live list, not an end summary
- Assert it is empty to bail out
- test.info() only inside a test
- errors is the list, error the first
basics
~20 sRead test.info().errors, the array of errors the runner has recorded for the running test. Soft failures land there as they happen, so asserting the array is empty stops the test at the point you choose.
solid answer
~40 sEvery soft failure is recorded on the running test, and `test.info()` hands you that test's live info object, whose `errors` array holds what has been collected so far. The idiomatic guard is `expect(test.info().errors).toHaveLength(0)` placed after a block of soft checks: if any of them failed, this plain assertion throws and the test stops before an expensive phase such as clicking Export and waiting on a download. Each entry is an error record carrying a `message` and a `stack`, so you can also inspect or count them. Two limits are worth naming: `test.info()` is only callable while a test or hook is running, and clearing the array is not a supported way to rescue a test - the recorded failures still fail it.
code
typescript · 13 linesimport { test, expect } from '@playwright/test';
test('statement then export', async ({ page }) => {
await page.goto('/accounts/1/statement');
await expect.soft(page.getByTestId('balance')).toHaveText('$1,204.55');
await expect.soft(page.getByTestId('currency')).toHaveText('USD');
// Stop here if either observation above failed.
expect(test.info().errors, 'soft failures before export').toHaveLength(0);
await page.getByRole('button', { name: 'Export' }).click();
});go deeper
Remember that a running test can look at its own recorded failures through test.info().errors, and that asserting the array is empty is how a test stops after soft checks.
Explain the mechanics: soft failures are appended as they happen, the array is live for the current attempt, and a plain expect on its length converts collection back into a stop.
Show where you place the guard in a real test - after the cheap observation block and before any phase whose runtime you would rather not spend on a page you already know is broken.
Frame it as controlling the blast radius of a failing test: decide which phases are worth entering after a known defect, and make that boundary explicit rather than incidental.
## The problem soft assertions create Soft assertions deliberately keep a test going after a failure. That is useful while you are gathering independent facts about a rendered page, and actively harmful once the test moves into a phase that costs real time - clicking Export on a bank statement page, waiting for a download, then parsing the file. You want the collection behaviour for the first part and the stop behaviour for the second, which means the test needs a way to ask: *has anything failed so far?* ## `test.info()` and its `errors` array `test.info()` returns the `TestInfo` object for the test that is currently running. Among other things it carries **`errors`**, the list of errors recorded for this test so far. A soft failure is appended to that list at the moment the matcher gives up, so the array is a live view rather than an end-of-run summary. - The array is **ordered** in the sequence the failures were recorded. - Each entry is an error record with fields such as `message` and `stack`. - It reflects the current attempt only - a retry starts a fresh test with a fresh list. - `test.info()` throws if you call it outside a running test or hook, so it belongs in the test body, in a `beforeEach`/`afterEach`, or in a fixture's test-scoped code. ## The bail-out pattern The canonical use is a single hard assertion that converts "something soft failed" into "stop now": 1. Run the block of soft checks that describe one rendered state. 2. Assert `expect(test.info().errors).toHaveLength(0)` - a plain, throwing assertion. 3. If anything above failed, this line throws, the test stops, and the report still contains every soft failure plus this one. 4. If everything passed, the test continues into the expensive phase. Give the guard a message argument when the failure would otherwise read as a bare length mismatch: `expect(test.info().errors, 'soft failures before export').toHaveLength(0)`. ## Reading versus mutating `errors` is there to be **read**. Deleting entries is not a supported route to a green test: the failures are recorded on the test result, and a test that recorded a failure fails. If you find yourself wanting to drop an entry, the real question is whether the check should have been an assertion at all. | Goal | Reach for | |---|---| | Know whether an earlier soft check failed | `test.info().errors` | | Stop the test at that point | `expect(test.info().errors).toHaveLength(0)` | | Count how many observations failed | `test.info().errors.length` | | Make a failed check stop mattering | nothing - remove the check instead | ## Related fields, and the one that misleads `TestInfo` also exposes `error`, which is the **first** error recorded. Reaching for it and concluding "there is only one problem" is a common misread: with soft assertions there are usually several, and `errors` is the complete list. Prefer `errors` whenever your logic depends on how many failures occurred. ## A worked shape on the statement page - Load the statement and assert hard that it rendered. - Softly check the balance text, the row count, and the export button's state - three independent observations. - Guard with `expect(test.info().errors).toHaveLength(0)`. - Only then click Export and assert on the downloaded statement. The result is a test that reports every rendering defect it can see in one run, yet refuses to spend a minute exporting a page it already knows is wrong.
- What is the difference between test.info().errors and test.info().error?`errors` is the full list of errors recorded for the current attempt; `error` is just the first of them. With soft assertions there are usually several, so any logic that counts failures or reports on them should read `errors`.
- Can you empty test.info().errors to rescue a test that had a soft failure?No. The failures are recorded on the test result, and a test with a recorded failure fails regardless of what you do to the array afterwards. If a check should not fail the test, it should not be an assertion at all.
- Where else can you read the collected errors besides the test body?Anywhere test-scoped code runs during the same test - an afterEach hook or the teardown half of a test-scoped fixture can call `test.info()` and inspect `errors` after the body has finished, which is a natural place to react to a failed attempt.
saying these in an interview costs you the question
- Calling test.info() outside a running test or hook
- Reading test.info().error and assuming it lists everything
- Thinking soft failures only appear after the run ends
- Emptying the errors array to make a test pass
- Checking the errors array before any soft check has run