Resource library

QA How-To

How to Use Playwright expect soft (2026)

Learn playwright expect soft syntax, failure behavior, checkpoints, reporting, timeouts, and safe patterns for clear, useful Playwright test diagnostics.

23 min read | 2,757 words

TL;DR

Use `await expect.soft(locator).matcher()` to record an assertion failure and continue the test. The test still fails, and locator assertions still retry. Keep prerequisites hard, group only independent checks softly, then use `expect(test.info().errors).toHaveLength(0)` before dependent actions.

Key Takeaways

  • A failed soft assertion continues execution but still makes the Playwright test fail.
  • Locator matchers keep their normal web-first retry behavior when used with expect.soft.
  • Use hard assertions for prerequisites and soft assertions for independent peer observations.
  • Check test.info().errors before performing actions that depend on a clean soft-validation phase.
  • Create a configured soft expect instance when a focused helper needs consistent soft checks.
  • Control assertion timeouts carefully because multiple failing soft checks run sequentially.
  • Use custom messages and semantic locators to make aggregated failures easy to diagnose.

Playwright expect soft lets a test keep running after an assertion fails, while still marking the test as failed. Use it when later checks provide independent diagnostic value, such as validating several fields in a summary panel, but do not use it to continue through a broken prerequisite.

The core syntax is await expect.soft(locator).toHaveText(expected) for locator assertions and expect.soft(value).toEqual(expected) for synchronous value assertions. This guide explains the behavior, setup, failure control, reporting, and design decisions an SDET should understand before adding soft checks to a production suite.

TL;DR

Need Recommended API Result
One non-blocking locator check await expect.soft(locator).toBeVisible() Records failure and continues
One non-blocking value check expect.soft(value).toBe(expected) Records failure and continues
Many soft checks in one scope expect.configure({ soft: true }) Creates a reusable soft expect instance
Stop after a soft validation phase expect(test.info().errors).toHaveLength(0) Converts accumulated errors into a checkpoint
Retry an external condition softly await expect.soft.poll(callback).toBe(value) Polls, records failure, then continues
A prerequisite must pass Regular expect Fails immediately and stops the test

The most important rule is simple: a soft assertion changes failure control, not assertion meaning. It retries like the corresponding web-first assertion, emits a normal assertion error when unsuccessful, and makes the final test fail.

1. What Is Playwright expect soft?

Playwright expect soft is a mode of the Playwright Test assertion library. A regular failed assertion throws and ends the current test body unless code catches the error. A failed soft assertion is added to the test's error collection, execution continues with the next statement, and the test is reported as failed when it finishes.

That behavior is useful when several observations are independent. Imagine an order confirmation page with an order number, payment status, shipping method, tax, and total. If the payment text is wrong, stopping immediately hides information about the remaining fields. Soft checks can report the complete mismatch set in one run.

import { test, expect } from '@playwright/test';

test('shows complete order summary', async ({ page }) => {
  await page.goto('/orders/ORD-1042');

  await expect.soft(page.getByTestId('order-number')).toHaveText('ORD-1042');
  await expect.soft(page.getByTestId('payment-status')).toHaveText('Paid');
  await expect.soft(page.getByTestId('shipping-method')).toHaveText('Express');
  await expect.soft(page.getByTestId('total')).toHaveText('$89.00');
});

Every locator assertion above remains retrying and web-first. Soft does not turn toHaveText into an immediate DOM read. It only changes what Playwright Test does after the assertion exhausts its timeout.

Soft assertions are a Playwright Test runner feature. Import test and expect from @playwright/test, and run the file through Playwright Test. A generic assertion library with a similarly named API does not automatically provide Playwright's error aggregation or test lifecycle integration.

For a wider assertion overview, read the Playwright web-first assertions guide.

2. Install and Run a Minimal Playwright expect soft Test

A new TypeScript project can install the test package and browser binaries with the normal Playwright workflow. Keep examples inside the configured test directory and run them with the Playwright CLI.

npm init playwright@latest
npx playwright test

The following file is self-contained because it uses page.setContent() instead of depending on an external application:

import { test, expect } from '@playwright/test';

test('collects independent profile mismatches', async ({ page }) => {
  await page.setContent(`
    <main>
      <h1>Account profile</h1>
      <dl>
        <div><dt>Name</dt><dd data-testid="name">Ava Patel</dd></div>
        <div><dt>Plan</dt><dd data-testid="plan">Team</dd></div>
        <div><dt>Region</dt><dd data-testid="region">US East</dd></div>
      </dl>
    </main>
  `);

  await expect(page.getByRole('heading', { name: 'Account profile' }))
    .toBeVisible();

  await expect.soft(page.getByTestId('name')).toHaveText('Ava Patel');
  await expect.soft(page.getByTestId('plan')).toHaveText('Enterprise');
  await expect.soft(page.getByTestId('region')).toHaveText('Europe');
});

The heading uses a hard assertion because it establishes that the correct page is open. The three details are soft because they can be inspected independently. The test completes all three checks, then fails with the plan and region mismatches.

Do not wrap expect.soft in try/catch. Soft mode already records the error through the runner. Catching unrelated exceptions around a large block can hide action failures and make the result misleading.

3. How Playwright expect soft Changes Test Execution

Understanding the lifecycle prevents a common misconception: continuing does not mean passing. Internally, the runner associates assertion failures with the active test. After a failed soft check, test.info().errors contains recorded errors. The test can perform more assertions, attach evidence, or deliberately stop at a checkpoint.

Consider this sequence:

import { test, expect } from '@playwright/test';

test('validates a dashboard before opening details', async ({ page }) => {
  await page.goto('/dashboard');

  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();

  await expect.soft(page.getByTestId('open-incidents')).toHaveText('0');
  await expect.soft(page.getByTestId('deployment-status')).toHaveText('Healthy');

  expect(test.info().errors).toHaveLength(0);

  await page.getByRole('link', { name: 'View deployment' }).click();
  await expect(page).toHaveURL(/\/deployments\//);
});

The hard checkpoint runs after the diagnostic group. If either soft check failed, it throws before the click. That boundary matters because clicking the deployment link may be unsafe or meaningless when the dashboard is not in the expected state.

Without the checkpoint, Playwright would attempt the later navigation. The test would still fail at completion, but a secondary timeout could add noise and consume time. Good suites use soft checks within a bounded observation phase, then decide whether continued interaction is valid.

The error array is scoped to the current test, including errors recorded earlier in fixtures or steps. If you need to know whether a specific phase introduced new errors, save the starting length and compare after the group:

const errorCountBefore = test.info().errors.length;

await expect.soft(page.getByTestId('subtotal')).toHaveText('$80.00');
await expect.soft(page.getByTestId('tax')).toHaveText('$8.00');

expect(test.info().errors.length).toBe(errorCountBefore);

Usually a zero-error checkpoint is clearer. The baseline technique is useful only when earlier diagnostic soft checks are intentionally allowed to remain recorded.

4. Hard vs Soft Assertions: Decision Table

Choose failure behavior from dependency, not from a desire to collect more screenshots. The following comparison is a practical review reference.

Question Hard assertion Soft assertion
Does later code require this state? Yes No
Should the test stop on failure? Yes No
Are later checks independently meaningful? Maybe Yes
Is failure still reported? Yes Yes
Does a locator assertion still retry? Yes Yes
Is it appropriate for login success? Usually Rarely
Is it appropriate for a read-only summary audit? Sometimes Often
Can it hide cascading action failures? Less likely Yes, if misused

Use a hard assertion for authentication, page identity, a selected account, required test data, and any state that makes later actions valid. Use soft assertions for sibling labels, table cell values, accessibility attributes, summary cards, or multiple visual facts that do not depend on one another.

A helpful question in review is: "If this check fails, would a real user or the test still be able to perform the next action meaningfully?" If the answer is no, keep it hard.

Soft assertions are not a substitute for splitting unrelated behaviors into separate tests. One giant test with fifty soft checks has poor ownership, slow feedback, and an unclear business purpose. Use them to improve diagnosis inside one coherent scenario.

The Playwright test architecture guide covers boundaries between tests, fixtures, and page objects.

5. Use Custom Messages and Configured Soft Expect

Custom messages make aggregated failures easier to scan. Pass the message as the second argument to expect.soft, before the matcher:

await expect.soft(
  page.getByTestId('invoice-total'),
  'invoice total should include tax and shipping',
).toHaveText('$124.50');

The message appears in reporter output and helps reviewers understand business intent without reconstructing it from a selector. Keep it specific and stable. "Total is correct" is less useful than "invoice total should include tax and shipping."

When a whole helper or test section should use soft checks, create a configured instance:

import { test, expect } from '@playwright/test';

const auditExpect = expect.configure({
  soft: true,
  timeout: 10_000,
});

test('audits account settings', async ({ page }) => {
  await page.goto('/settings');

  await auditExpect(page.getByLabel('Display name')).toHaveValue('Ava Patel');
  await auditExpect(page.getByLabel('Time zone')).toHaveValue('Asia/Kolkata');
  await auditExpect(page.getByRole('checkbox', { name: 'Weekly digest' }))
    .toBeChecked();
});

The configured instance is not global configuration. It is a local expect function with defaults. A descriptive name such as auditExpect or softExpect communicates intent. Avoid naming it simply expect because readers may not realize checks continue after failure.

Timeout belongs to the condition, not to soft mode. Increase it only when the application has a legitimate longer readiness window. A large timeout multiplied across many failing soft checks can make one test very slow because each failed check may consume its full retry budget.

6. Group Soft Checks Without Creating Cascading Failures

Soft assertions work best when the page is already stable and checks observe peer facts. First establish the page or component with a hard assertion. Next run the soft audit. Finally apply a hard boundary before any dependent action.

import { test, expect } from '@playwright/test';

test('reviews checkout totals before payment', async ({ page }) => {
  await page.goto('/checkout/review');

  const review = page.getByRole('region', { name: 'Order review' });
  await expect(review).toBeVisible();

  await test.step('audit displayed totals', async () => {
    await expect.soft(review.getByTestId('subtotal')).toHaveText('$80.00');
    await expect.soft(review.getByTestId('shipping')).toHaveText('$5.00');
    await expect.soft(review.getByTestId('tax')).toHaveText('$8.00');
    await expect.soft(review.getByTestId('grand-total')).toHaveText('$93.00');
  });

  expect(test.info().errors).toHaveLength(0);

  await review.getByRole('button', { name: 'Place order' }).click();
  await expect(page.getByRole('heading', { name: 'Order confirmed' }))
    .toBeVisible();
});

test.step improves reporter organization but does not isolate errors or make a soft group atomic. The explicit checkpoint provides the control boundary.

Do not continue filling a form, deleting data, or submitting payment after a failed prerequisite merely because the API permits it. That can create secondary failures or side effects. If you want diagnostics after a prerequisite failure, collect passive evidence such as text, attributes, or a screenshot, then stop.

Also avoid a chain in which each soft assertion depends on the prior action. An action failure is not a soft assertion failure, so it still throws. More importantly, continuing through a broken state gives little trustworthy information.

7. Playwright expect soft With Locators, Values, and Polling

The soft modifier works across Playwright's supported expect forms. Locator assertions are asynchronous and must be awaited. Generic value assertions are normally synchronous. Polling assertions are asynchronous.

import { test, expect } from '@playwright/test';

test('uses three soft assertion forms', async ({ page, request }) => {
  await page.setContent('<p data-testid="status">Ready</p>');

  await expect.soft(page.getByTestId('status')).toHaveText('Ready');

  const labels = ['Ready', 'Queued'];
  expect.soft(labels).toContain('Queued');

  await expect.soft.poll(
    async () => {
      const response = await request.get('/api/health');
      return response.status();
    },
    {
      message: 'health endpoint should become available',
      timeout: 10_000,
    },
  ).toBe(200);
});

The request example expects the configured application base URL to serve /api/health. It uses the real APIRequestContext.get and expect.soft.poll APIs.

For locators, prefer web-first matchers such as toHaveText, toHaveValue, toBeVisible, and toHaveCount instead of soft-checking values returned by immediate DOM reads. This version retries the UI condition:

await expect.soft(page.getByRole('status')).toHaveText('Complete');

This version takes one snapshot and can be flaky:

expect.soft(await page.getByRole('status').textContent()).toBe('Complete');

Soft does not add retries to a plain value. The retry behavior comes from the locator matcher or from expect.poll. The Playwright auto-waiting assertions guide explains that distinction in depth.

8. Timeouts, Retries, and Parallel Workers

Assertion timeout, test timeout, retries, and soft behavior solve different problems. The assertion timeout controls how long a web-first check retries. The test timeout limits the test lifecycle. Retries rerun the test after failure. Workers control process-level concurrency.

A soft assertion still consumes assertion time when its condition never becomes true. Four failing checks with a ten-second timeout can approach forty seconds when evaluated sequentially. Do not raise timeouts globally to compensate for an unclear readiness signal.

import { defineConfig } from '@playwright/test';

export default defineConfig({
  timeout: 30_000,
  expect: {
    timeout: 5_000,
  },
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : undefined,
});

This configuration uses current Playwright Test options. It does not make soft failures pass. If the first attempt records a soft error, the test fails and can be retried according to retries. A later passing retry may classify the test as flaky in reporting, just like a hard assertion failure.

Soft assertions inside parallel tests remain isolated because each test has its own test.info(). Do not share mutable arrays or reporter state across workers to aggregate assertion failures manually. Let the runner own test errors.

If timeouts appear only in CI, inspect traces and application readiness before changing soft strategy. The Playwright trace viewer debugging guide can reveal whether each assertion waited on the same missing state.

9. Reporting and Debugging Soft Assertion Failures

Soft failures are valuable only when the report explains them. Use semantic locators, custom messages, and small logical steps. The HTML reporter shows failed assertions, call logs, source locations, attachments, and traces according to your configuration.

A practical configuration keeps a trace for the first retry and a screenshot on failure:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  reporter: [['html', { open: 'never' }], ['list']],
  use: {
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
});

When several checks fail, examine the earliest business mismatch first. A wrong account, locale, or test record can make every field disagree. Soft aggregation gives breadth, but root cause analysis still requires dependency awareness.

Add messages where the same matcher appears repeatedly:

const summary = page.getByRole('region', { name: 'Billing summary' });

await expect.soft(
  summary.getByTestId('currency'),
  'billing summary should use the account currency',
).toHaveText('USD');

await expect.soft(
  summary.getByTestId('renewal-date'),
  'renewal date should reflect the annual plan',
).toHaveText('July 13, 2027');

Do not print pass or fail messages manually with console.log. Reporter-integrated assertions retain source and call-log information. If supporting data helps, attach a JSON snapshot with test.info().attach(), but keep secrets and personal data out of artifacts.

10. Page Object and Helper Design

A page object can expose a soft audit, but its name should reveal that it records failures rather than throwing immediately. Return no boolean from an assertion helper unless callers need a genuine value. Let Playwright own the assertion result.

import { expect, type Locator, type Page } from '@playwright/test';

export class OrderSummary {
  readonly root: Locator;

  constructor(page: Page) {
    this.root = page.getByRole('region', { name: 'Order summary' });
  }

  async expectFieldsSoftly(expected: {
    subtotal: string;
    tax: string;
    total: string;
  }): Promise<void> {
    await expect.soft(this.root.getByTestId('subtotal')).toHaveText(
      expected.subtotal,
    );
    await expect.soft(this.root.getByTestId('tax')).toHaveText(expected.tax);
    await expect.soft(this.root.getByTestId('total')).toHaveText(expected.total);
  }
}

The calling test should establish page identity and decide where to stop:

await expect(orderSummary.root).toBeVisible();
await orderSummary.expectFieldsSoftly({
  subtotal: '$80.00',
  tax: '$8.00',
  total: '$88.00',
});
expect(test.info().errors).toHaveLength(0);

Avoid placing soft assertions in generic actions such as login() or save(). Callers reasonably expect those methods either to complete their contract or throw. An explicit audit...Softly name makes the continuation semantics visible.

Fixtures can use soft checks for noncritical environment diagnostics, but any fixture contract required by the test should fail hard. Otherwise every dependent test may continue from invalid setup and produce misleading failures.

11. When Not to Use Playwright expect soft

Do not use Playwright expect soft for a condition that guards an action or identifies the scenario. Login success, selected tenant, loaded record, enabled destructive button, and correct environment are typical hard requirements.

Avoid soft mode in these situations:

  1. The next statement mutates production-like data.
  2. The assertion proves navigation reached the intended page.
  3. A failed check means later locators may refer to a different component.
  4. The suite needs one failure per narrowly scoped behavior.
  5. The assertion failure itself should abort an expensive workflow.
  6. A security or access-control check must stop the scenario.
  7. Continued execution could expose credentials or personal information in artifacts.

Also resist converting every hard assertion after one flaky run. Flakiness usually comes from unstable data, a broad locator, missing readiness signals, or shared state. Soft mode can make the test slower and noisier without fixing any cause.

The proper use case is diagnostic density inside one coherent, read-oriented state. A product-details audit can inspect title, price, inventory label, and badge. A checkout action should not proceed when the selected product or final total is already wrong.

12. Review Checklist for Playwright expect soft in 2026

Before merging a soft assertion, review the complete control flow.

  • The test imports expect from @playwright/test.
  • Locator assertions are awaited.
  • Generic value assertions do not pretend to auto-retry.
  • The page or component prerequisite is hard-asserted first.
  • Every later soft check remains meaningful if a sibling check fails.
  • A checkpoint stops dependent actions after the audit.
  • Custom messages describe business intent where aggregation could be ambiguous.
  • Timeouts are appropriate and do not multiply into a very slow failure.
  • The helper name exposes soft behavior.
  • The final test fails when any soft assertion fails.
  • Test retries are not mistaken for assertion retries.
  • The report retains enough evidence to find the shared root cause.

During code review, mentally force the first soft check to fail and trace every later statement. If a later action would be unsafe, confusing, or invalid, add a hard boundary earlier. If the later checks only read independent details, continued execution is likely appropriate.

Finally, keep the group small. Five useful related mismatches can accelerate diagnosis. Dozens of unrelated soft failures usually indicate the test should be decomposed.

Interview Questions and Answers

Q: What is a soft assertion in Playwright?

A soft assertion records a normal assertion failure against the active test and then continues execution. The test is still reported as failed. I use it when later checks are independent and add diagnostic value.

Q: Does expect.soft wait and retry?

Soft itself controls failure handling. A locator matcher such as toHaveText or toBeVisible still performs its normal web-first retries, while a generic value matcher remains immediate.

Q: How do you stop after several soft assertions?

I add a hard checkpoint such as expect(test.info().errors).toHaveLength(0) before dependent actions. This lets the audit collect sibling mismatches but prevents cascading interactions.

Q: What is the difference between expect.soft and expect.configure({ soft: true })?

expect.soft marks one assertion as soft. expect.configure({ soft: true }) creates an expect instance whose assertions are soft by default, and it can also define a local timeout.

Q: Do soft assertions work without Playwright Test?

The documented soft error aggregation depends on the Playwright Test runner. I import from @playwright/test and execute the spec with playwright test.

Q: Can a test pass after a soft assertion fails?

No. Execution can continue, but the recorded error makes the test fail at completion. Retries may rerun the failed test, but that is separate behavior.

Q: When is a soft assertion dangerous?

It is dangerous when later steps depend on the failed condition or perform side effects. Continuing can create cascading timeouts, false conclusions, or unintended data changes.

Q: How do soft failures behave in reports?

They appear as test errors with source locations and assertion details. Custom messages, semantic locators, and test.step labels make multiple failures easier to interpret.

Common Mistakes

  • Assuming "soft" means warning-only and expecting the test to pass.
  • Soft-asserting login or page identity, then continuing into the wrong state.
  • Forgetting await on soft locator assertions.
  • Reading textContent() and expecting a generic soft matcher to retry.
  • Adding a long timeout to every soft assertion.
  • Running dependent clicks after an audit without checking test.info().errors.
  • Catching errors around soft assertions and hiding unrelated exceptions.
  • Creating a global configured soft expect and using it where hard checks are required.
  • Combining unrelated requirements into one enormous soft-assertion test.
  • Using soft mode to mask flaky data or poor locators.
  • Writing vague messages that do not identify the business rule.
  • Assuming test.step isolates or clears accumulated errors.

Conclusion

Playwright expect soft is best treated as controlled error aggregation. It preserves the behavior of the chosen matcher, records failures through Playwright Test, and lets independent validations continue so one run provides a fuller diagnostic picture.

Start with a hard prerequisite, keep the soft group focused, then place a hard checkpoint before any dependent action. That structure gives engineers more evidence without sacrificing the honesty or safety of the test result.

Interview Questions and Answers

Explain soft assertions in Playwright Test.

A soft assertion records its failure in the current test and permits subsequent statements to run. The test still fails at completion. I use soft checks for independent facts, while keeping scenario prerequisites hard.

Does expect.soft change auto-waiting behavior?

No. A locator assertion still retries according to its normal assertion timeout. Soft changes what happens after that assertion fails, not how the matcher evaluates the condition.

How do you prevent cascading failures after soft checks?

I establish page identity with a hard assertion, run a small group of independent soft checks, and then add `expect(test.info().errors).toHaveLength(0)`. Dependent clicks or mutations occur only after that checkpoint passes.

When would you use expect.configure with soft true?

I use it in a focused audit helper or test section where every assertion is intentionally non-blocking. I give the configured instance a descriptive name and keep hard prerequisites on the regular expect instance.

How do soft assertions affect test duration?

Every failed web-first soft assertion can consume its complete assertion timeout before execution continues. Several sequential failures can therefore make a test slow. I keep groups small and timeouts tied to real readiness expectations.

What is stored in test.info().errors?

It is the current test's collected error list, including recorded soft assertion failures. I can inspect its length to create a control-flow checkpoint, but I normally let Playwright reporters present the detailed errors.

Can expect.soft replace multiple tests?

No. It can improve diagnostic density inside one coherent scenario, but unrelated behaviors should remain separate tests. Large soft-audit tests weaken ownership, isolation, and feedback speed.

How would you review a proposed soft assertion?

I ask whether later checks remain meaningful if it fails and whether any later action depends on it. I also verify the matcher is awaited when asynchronous, the timeout is reasonable, and the report message explains the business rule.

Frequently Asked Questions

What does expect soft do in Playwright?

It records an assertion failure on the current Playwright test and continues with the next statement. The test still finishes with a failed status, so soft does not mean optional or ignored.

Do I need to await expect.soft in Playwright?

Await asynchronous locator assertions such as `toBeVisible()` and `toHaveText()`. Generic value assertions such as `expect.soft(value).toBe(expected)` are synchronous and do not need `await`.

Does Playwright expect soft retry?

A soft locator assertion uses the same retry behavior as its hard equivalent. Soft changes failure control only. A generic value assertion is still immediate unless you use `expect.soft.poll()`.

How can I stop a test after soft assertions fail?

Add a hard checkpoint with `expect(test.info().errors).toHaveLength(0)` before dependent actions. If any prior soft assertion recorded an error, the checkpoint throws and stops the test body.

Can I configure all assertions in a helper to be soft?

Yes. Create a local instance with `const softExpect = expect.configure({ soft: true })`. Name it clearly and avoid using it for prerequisites that must stop execution.

Will a Playwright retry rerun a test with soft failures?

Yes, if retries are configured, a test that ends with recorded soft errors is a failed attempt and can be rerun. Test retries are separate from the retry loop performed by web-first assertions.

When should I avoid Playwright soft assertions?

Avoid them for login, navigation identity, required data, permissions, or any condition that makes later actions valid. Use a hard assertion whenever continuing could create cascading failures or side effects.

Related Guides