skip to content

On a bank statement page, which Playwright checks would you make soft and which must stay hard?

level: seniorimportance: should knowfreq 46%

answer

  1. Ask what depends on this check
  2. Observations soft, preconditions hard
  3. Cascade noise from a soft anchor
  4. Failing soft checks each cost a timeout
  5. Anchor, observe, then guard

basics

~20 s

Make a check soft when nothing later in the test depends on it, such as the balance format or the export button's label. Keep it hard when the following steps are meaningless or misleading if it fails.

solid answer

~50 s

The deciding question is dependency, not importance. Independent observations of one rendered state - the balance widget's formatting, the transaction table's row count, the export button's accessible name - are good soft candidates, because collecting all of them turns three debugging cycles into one. Anything the rest of the test stands on stays hard: that the statement rendered at all, that the row you are about to click exists, that navigation completed. Make those soft and one root cause becomes a page of derived failures, with the real problem buried among them. There is also a runtime cost: every failing soft check spends its full expect timeout, so ten of them on a page that never loaded turn a fast failure into a slow one. A good compromise is an anchor-then-observe shape, with `expect(test.info().errors).toHaveLength(0)` before any expensive phase.

code

typescript · 18 lines
typescript
import { test, expect } from '@playwright/test';

test('statement page', async ({ page }) => {
  await page.goto('/accounts/1/statement');

  // Hard: the rest of the test means nothing without these.
  await expect(page.getByTestId('statement')).toBeVisible();
  const rows = page.getByRole('row');
  await expect(rows).toHaveCount(13);

  // Soft: independent details of the same rendered state.
  await expect.soft(page.getByTestId('balance')).toHaveText('$1,204.55');
  await expect.soft(rows.nth(1)).toContainText('Direct debit');

  // Hard again: do not export a page already known to be wrong.
  expect(test.info().errors).toHaveLength(0);
  await page.getByRole('button', { name: 'Export' }).click();
});

go deeper

for a junior

Learn the rule of thumb first: if the next lines need this check to have passed, keep it hard; if the check is just another fact about a page that already rendered, soft is fine.

for a middle

Explain why a soft anchor produces cascade noise, and that each failing soft check still waits its full expect timeout before recording anything.

for a senior

Walk a real test out loud - hard anchor, soft observation block, an errors-length guard before the expensive phase - and justify each placement by what depends on it.

for a principal

Own the exchange explicitly: soft buys one-run diagnosis and pays in wall-clock time and report noise on failure. Say where that trade is worth making and make the shape obvious enough to copy.

## The question that decides it Not "how important is this check?" but **"does anything later in this test depend on it?"** A soft assertion changes only whether the test body continues. If the statements below would still produce trustworthy information after this check fails, soft is safe and useful. If they would produce garbage - or nothing at all - the check is an anchor and must stay hard. ## Good soft candidates On a bank statement page with a balance widget, a transaction table and an export button: - **Formatting details.** The balance renders as `$1,204.55` rather than `1204.55`. - **Independent counts.** The table shows the thirteen rows the seed data implies. - **Labels and affordances.** The export button carries the accessible name the design calls for and is enabled. - **Parallel field checks.** Date, description and amount within the same already-located row. What these share: they are observations of one state that has already been established, and each is meaningful whether or not its neighbours passed. ## Checks that must stay hard - **The anchor.** The statement container rendered - without it, every soft check below fails for the same reason. - **Preconditions for an action.** The row you are about to click exists; the dialog you are about to fill is open. - **Navigation and identity.** You are on the right account's statement, not someone else's. - **Anything that guards a destructive or costly step.** Exporting, downloading, submitting. ## The two costs of getting it wrong 1. **Cascade noise.** A soft anchor turns one root cause into a dozen derived failures. The report says twelve things are broken; a reader has to reconstruct that eleven were consequences of the first. 2. **Timeout multiplication.** A failing retrying matcher waits its full expect timeout - 5 seconds by default - before recording. Ten failing soft checks on a page that never loaded cost fifty seconds where a hard anchor would have failed in five. | Check | Soft or hard | Why | |---|---|---| | Statement container rendered | hard | everything below reads the same failure | | Balance formatted as currency | soft | nothing later depends on the formatting | | Table has thirteen rows | soft | independent observation of the same state | | Target row exists before clicking | hard | the click would fail with a worse message | | Export button enabled | soft | unless the test goes on to click it | ## The shape that works 1. **Anchor hard** on the state the test is about. 2. **Observe softly** - the block of independent facts about that state. 3. **Guard** with `expect(test.info().errors).toHaveLength(0)` before any phase whose runtime you would rather not spend on a page already known to be wrong. 4. **Anchor hard again** for the next phase, and repeat. That shape gives you the best of both: one run enumerates every rendering defect it can see, and the test still refuses to march into an export flow it cannot interpret. ## Two habits to avoid - **Soft as flake management.** Making an intermittently failing check soft does not stabilise anything; it just moves the failure to the end of the test while the run still goes red. Fix the wait or delete the check. - **Soft everywhere by default.** A test with no hard anchors has no defined failure point; every run has to be read in full to find out what actually happened, and failing runs get slower the more broken the page is. The judgment being tested is whether you can tell an observation from a precondition. Interviewers ask it because candidates who cannot make everything soft, then wonder why their reports are unreadable and their failing builds are the slow ones.

  • A colleague makes the page-loaded check soft so the test reports more. What goes wrong?
    Every later check fails for the same missing page, so one root cause is reported as a dozen defects and the real one is buried. The run is also much slower, because each of those checks waits its full timeout before recording a failure.
  • Is making a flaky assertion soft a reasonable stopgap?
    No. Soft does not stop the failure counting - the test still goes red - so nothing is stabilised, and the failure now arrives at the end of the test rather than at the point of trouble. Either fix the wait or remove the check.
  • How do you decide where to put the errors-length guard?
    Immediately before the first phase that would be expensive or uninterpretable on a known-bad page - an export, a download, a multi-step flow. Cheap observations that still add information can run past a failure; anything with real runtime should not.

saying these in an interview costs you the question

  • Making the page-loaded anchor soft and drowning in cascades
  • Treating soft assertions as a fix for flaky checks
  • Assuming soft failures cost nothing in run time
  • Expecting a run to go green despite soft failures
  • Never guarding an expensive phase on collected errors