QA How-To
Playwright expect soft: Examples and Best Practices
Use playwright expect soft examples for UI audits, API comparisons, polling, page objects, checkpoints, and reports without masking real test failures.
24 min read | 2,340 words
TL;DR
The safest Playwright soft-assertion pattern is hard prerequisite, soft peer checks, then a hard `test.info().errors` checkpoint. Use locator matchers for automatic retries, custom messages for business context, and explicit helper names so continuation never surprises the caller.
Key Takeaways
- Hard-check page, record, and response prerequisites before beginning a soft audit.
- Use soft assertions for peer facts whose results stay meaningful when a sibling check fails.
- Add business-focused custom messages so aggregated failures are easy to scan.
- Use configured soft expect instances only inside clearly named, focused audit scopes.
- Place a test.info().errors checkpoint before navigation, submission, deletion, or other side effects.
- Prefer sequential web-first assertions and keep their combined timeout cost under control.
- Attach only minimal, sanitized diagnostic context when a soft group introduces errors.
Playwright expect soft examples are most useful when they show where continuation improves diagnosis and where it corrupts the scenario. The syntax is short, but production-quality usage requires clear prerequisites, independent checks, bounded failure collection, and reports that explain every mismatch.
This example-driven guide moves from a runnable summary audit to data-driven helpers, API and UI comparisons, polling, page objects, reporting, and refactoring patterns. Each pattern uses current Playwright Test APIs and keeps the final result honest: any failed soft assertion still fails the test.
TL;DR
| Scenario | Soft check? | Reason |
|---|---|---|
| Verify five independent invoice fields | Yes | All mismatches are useful in one report |
| Confirm login succeeded before opening settings | No | Every later step depends on login |
| Compare API data with rendered cards | Yes, after hard setup checks | Each field comparison is independently useful |
| Validate a destructive-action confirmation | No | Stop before creating an unintended side effect |
| Audit optional badges across stable product cards | Yes | A complete defect pattern is useful |
| Wait for a background status without blocking later diagnostics | Sometimes | Use soft polling, then add a checkpoint |
A reliable pattern has three phases: hard-arrange the page, soft-audit peer facts, then hard-stop with expect(test.info().errors).toHaveLength(0) before any dependent action.
1. First Playwright expect soft Examples: A Runnable Audit
The following test runs without a web server. It creates an invoice summary, verifies page identity with a hard assertion, and softly validates four independent amounts.
import { test, expect } from '@playwright/test';
test('audits every invoice amount', async ({ page }) => {
await page.setContent(`
<main>
<h1>Invoice INV-2048</h1>
<section aria-label="Invoice totals">
<p>Subtotal <strong data-testid="subtotal">$80.00</strong></p>
<p>Discount <strong data-testid="discount">$10.00</strong></p>
<p>Tax <strong data-testid="tax">$7.00</strong></p>
<p>Total <strong data-testid="total">$77.00</strong></p>
</section>
</main>
`);
await expect(
page.getByRole('heading', { name: 'Invoice INV-2048' }),
).toBeVisible();
const totals = page.getByRole('region', { name: 'Invoice totals' });
await expect.soft(totals.getByTestId('subtotal')).toHaveText('$80.00');
await expect.soft(totals.getByTestId('discount')).toHaveText('$8.00');
await expect.soft(totals.getByTestId('tax')).toHaveText('$7.20');
await expect.soft(totals.getByTestId('total')).toHaveText('$79.20');
});
The heading is a prerequisite. If it fails, the test may be on the wrong invoice, so continuing would mislabel every later result. Once identity is established, the amount checks are siblings. Three mismatches can point to one discount-calculation defect more clearly than the first mismatch alone.
Locator assertions must be awaited. Each toHaveText retries until its assertion timeout expires or the expected text appears. Soft mode changes only post-failure control. It does not make an immediate snapshot, shorten the timeout, or downgrade the final status.
This is the simplest useful shape: one business object, one stable component, and a small set of peer facts.
2. Add Business-Focused Messages to Playwright expect soft Examples
Aggregated failures need labels that explain intent. The selector and expected value often tell only part of the story. Supply a custom message as the second argument to expect.soft.
const pricing = page.getByRole('region', { name: 'Pricing summary' });
await expect.soft(
pricing.getByTestId('discount'),
'annual plan should apply the 10 percent loyalty discount',
).toHaveText('$12.00');
await expect.soft(
pricing.getByTestId('renewal-total'),
'renewal total should include discount before tax',
).toHaveText('$118.80');
A good message describes the violated rule, not the mechanics. "Discount text should equal $12.00" repeats the matcher. "Annual plan should apply the loyalty discount" gives a developer a starting hypothesis.
Messages also appear for successful assertions in reporters that show steps. Keep them concise enough to scan and stable enough that copy changes do not make the label misleading.
For repeated fields, include a record key:
for (const expected of expectedUsers) {
const row = page.getByRole('row', { name: new RegExp(expected.email) });
await expect.soft(
row.getByRole('cell', { name: expected.role }),
'role should match the directory record for ' + expected.email,
).toBeVisible();
}
This is valid TypeScript and uses the custom message to identify the affected input row. The locator still conveys the actual UI object. Avoid placing secrets, tokens, or full personal records in messages because reporter output may be retained as a CI artifact.
The Playwright reporters guide explains how assertion details appear across list, HTML, and CI-oriented formats.
3. Data-Driven Soft Checks for Tables and Summary Panels
A table of expectations reduces repetition while retaining one assertion per field. Store locators, expected values, and business labels, then iterate sequentially.
import { test, expect, type Locator } from '@playwright/test';
type TextCheck = {
label: string;
locator: Locator;
expected: string | RegExp;
};
async function expectTextChecksSoftly(checks: TextCheck[]): Promise<void> {
for (const check of checks) {
await expect.soft(
check.locator,
check.label,
).toHaveText(check.expected);
}
}
test('audits an order summary', async ({ page }) => {
await page.goto('/orders/ORD-810/review');
const summary = page.getByRole('region', { name: 'Order summary' });
await expect(summary).toBeVisible();
await expectTextChecksSoftly([
{
label: 'order number should match the requested record',
locator: summary.getByTestId('order-number'),
expected: 'ORD-810',
},
{
label: 'shipping service should match checkout selection',
locator: summary.getByTestId('shipping-service'),
expected: 'Next day',
},
{
label: 'currency should match the account market',
locator: summary.getByTestId('currency'),
expected: 'USD',
},
]);
});
Sequential execution keeps reporter order predictable. Avoid Promise.all around many Playwright assertions unless you have measured a compelling need and understand failure reporting. Parallel assertion retries can compete for a page that is still changing and make timelines harder to read.
For row counts or collection order, use the matchers designed for collections. toHaveCount retries exact cardinality, and toHaveText accepts an array when a locator resolves to a list. Do not turn every collection into a loop of immediate reads.
Data-driven checks should remain type-safe. Accept string | RegExp if both are intentionally supported. Do not accept unknown and cast inside the helper, because mismatched expectation types then fail late and obscure authoring mistakes.
4. Create a Reusable Configured Soft Expect
When every assertion in a focused audit should continue, use expect.configure({ soft: true }). It creates a separate expect function with local defaults.
import { test, expect } from '@playwright/test';
const contentAudit = expect.configure({
soft: true,
timeout: 7_500,
});
test('audits product content', async ({ page }) => {
await page.goto('/products/mechanical-keyboard');
const product = page.getByRole('main');
await expect(
product.getByRole('heading', { name: 'Mechanical Keyboard' }),
).toBeVisible();
await contentAudit(product.getByTestId('sku')).toHaveText('KB-104');
await contentAudit(product.getByTestId('availability')).toHaveText('In stock');
await contentAudit(product.getByTestId('price')).toHaveText('$129.00');
});
The name contentAudit makes continuation visible in review. Do not shadow the imported expect name. A reader should be able to tell which checks stop execution without finding the configuration declaration.
A configured instance can also be local to a helper:
async function auditProfile(page: Page): Promise<void> {
const audit = expect.configure({ soft: true });
const profile = page.getByRole('region', { name: 'Profile details' });
await audit(profile.getByTestId('name')).toHaveText('Ava Patel');
await audit(profile.getByTestId('team')).toHaveText('Quality Engineering');
}
Keep hard prerequisites outside this helper or use the original expect explicitly. Making all assertions in an entire codebase soft through an abstraction is a dangerous default. The choice belongs to the business flow, not merely to a utility module.
5. API and UI Comparison Example
Soft assertions are effective when an API payload is trusted as test data and several rendered fields must match it. First hard-check the response because parsing and UI comparisons depend on a successful request. Then compare independent fields softly.
import { test, expect } from '@playwright/test';
type Order = {
id: string;
status: string;
customerName: string;
total: number;
};
test('matches order details to the API', async ({ page, request }) => {
const response = await request.get('/api/orders/ORD-902');
expect(response.ok()).toBeTruthy();
const order = (await response.json()) as Order;
await page.goto('/orders/ORD-902');
await expect(
page.getByRole('heading', { name: 'Order ' + order.id }),
).toBeVisible();
await expect.soft(
page.getByTestId('status'),
'rendered status should match the order API',
).toHaveText(order.status);
await expect.soft(
page.getByTestId('customer'),
'rendered customer should match the order API',
).toHaveText(order.customerName);
await expect.soft(
page.getByTestId('total'),
'rendered total should match the order API',
).toHaveText('#39; + order.total.toFixed(2));
});
The hard response.ok() assertion prevents an error page or unauthorized payload from being treated as expected data. Schema validation can be another hard prerequisite if the payload is external or unstable.
Be careful with numeric formatting. The UI may localize currency, add separators, or round values according to product rules. Build expected display text through the same public requirement, not by importing the application's formatter and duplicating its bug.
For more request-context patterns, see the Playwright API testing guide.
6. Soft Polling for Eventually Consistent Status
expect.soft.poll combines polling with soft failure control. It is useful when a noncritical backend observation may converge after the UI is otherwise ready.
import { test, expect } from '@playwright/test';
test('collects deployment diagnostics', async ({ page, request }) => {
await page.goto('/deployments/dep-417');
await expect(
page.getByRole('heading', { name: 'Deployment dep-417' }),
).toBeVisible();
await expect.soft.poll(
async () => {
const response = await request.get('/api/deployments/dep-417');
if (!response.ok()) return response.status();
const body = (await response.json()) as { status: string };
return body.status;
},
{
message: 'deployment API should reach ready state',
timeout: 20_000,
intervals: [500, 1_000, 2_000],
},
).toBe('ready');
await expect.soft(
page.getByRole('status'),
'deployment page should show the ready state',
).toHaveText('Ready');
});
The poll retries the callback at the configured intervals until the matcher passes or timeout expires. A failure is recorded softly, then the UI assertion still runs. This produces useful evidence when the API and UI disagree.
Do not use soft polling before a dependent production action. If the deployment must be ready before promotion, readiness is a hard gate. Also keep polling callbacks read-only and idempotent. Repeatedly creating jobs or updating records inside a poll is a test design defect.
Use polling for conditions outside the DOM. For UI text, a locator assertion already retries and is simpler.
7. Checkpoint Before Navigation, Submission, or Deletion
The strongest soft-assertion pattern ends a diagnostic phase explicitly. test.info().errors contains accumulated errors for the current test. A hard length assertion stops the scenario when the audit is not clean.
import { test, expect } from '@playwright/test';
test('submits only a valid review', async ({ page }) => {
await page.goto('/applications/APP-51/review');
const review = page.getByRole('region', { name: 'Application review' });
await expect(review).toBeVisible();
await expect.soft(review.getByTestId('candidate')).toHaveText('Ava Patel');
await expect.soft(review.getByTestId('role')).toHaveText('Senior SDET');
await expect.soft(review.getByTestId('location')).toHaveText('Remote');
expect(
test.info().errors,
'review must have no soft assertion failures before submission',
).toHaveLength(0);
await review.getByRole('button', { name: 'Submit application' }).click();
await expect(page.getByRole('status')).toHaveText('Application submitted');
});
This boundary protects the side effect. If the candidate, role, or location is wrong, the application is not submitted.
A checkpoint observes all prior recorded errors, including failures from fixtures or earlier steps. That is normally desirable. If a test deliberately records a nonblocking audit that should not gate a later diagnostic section, keep the scenario read-only and document why. The final test still fails.
Do not clear or mutate test.info().errors. Treat it as runner-owned evidence. If one workflow truly requires independent error phases, separate tests usually provide a cleaner lifecycle.
8. Page Object Example With an Explicit Audit Contract
A page object method should reveal that it performs assertions and that failures are soft. Avoid hiding continuation inside a method named validate() or open().
import { expect, type Locator, type Page } from '@playwright/test';
export type ExpectedAccount = {
name: string;
plan: string;
region: string;
};
export class AccountOverview {
readonly root: Locator;
constructor(page: Page) {
this.root = page.getByRole('region', { name: 'Account overview' });
}
async expectLoaded(): Promise<void> {
await expect(this.root).toBeVisible();
}
async auditFieldsSoftly(expected: ExpectedAccount): Promise<void> {
await expect.soft(
this.root.getByTestId('account-name'),
'account name should match test data',
).toHaveText(expected.name);
await expect.soft(
this.root.getByTestId('plan'),
'plan should match test data',
).toHaveText(expected.plan);
await expect.soft(
this.root.getByTestId('region'),
'region should match test data',
).toHaveText(expected.region);
}
}
The test decides sequence and control:
const overview = new AccountOverview(page);
await overview.expectLoaded();
await overview.auditFieldsSoftly({
name: 'Acme Quality',
plan: 'Enterprise',
region: 'US East',
});
expect(test.info().errors).toHaveLength(0);
Keep locators scoped to the component so hidden duplicates elsewhere do not pollute results. Use role, accessible name, label, and intentional test IDs. The Playwright locator best practices guide provides selector design guidance.
Do not return a list of homemade booleans from the page object. Native assertions preserve retries, call logs, source locations, and reporter integration.
9. Attach Context Only When a Soft Group Adds Errors
When several fields fail, one structured attachment can make the report easier to diagnose. Record the current error count, run the audit, and attach a safe UI snapshot only if new errors appeared.
import { test, expect } from '@playwright/test';
test('attaches summary data after audit failures', async ({ page }) => {
await page.goto('/billing/summary');
const fields = page.getByTestId('billing-field');
await expect(fields).toHaveCount(3);
const errorsBefore = test.info().errors.length;
await expect.soft(fields.nth(0)).toContainText('Enterprise');
await expect.soft(fields.nth(1)).toContainText('Annual');
await expect.soft(fields.nth(2)).toContainText('USD');
if (test.info().errors.length > errorsBefore) {
const snapshot = await fields.allTextContents();
await test.info().attach('billing-summary-values', {
body: Buffer.from(JSON.stringify(snapshot, null, 2)),
contentType: 'application/json',
});
}
});
allTextContents() is an immediate read, but here it collects diagnostic evidence after the retrying assertions have finished. It is not used as the pass or fail condition.
Sanitize attachments. Billing pages, profiles, and API payloads can contain personal data or secrets. Prefer a small field allowlist over a complete DOM or response dump.
Use traces and screenshots according to suite configuration before building elaborate custom artifact systems. Native artifacts already connect the action timeline to the DOM and source.
10. Refactor Repetitive Hard Assertions Safely
Do not bulk-replace every expect( with expect.soft(. Refactor according to dependency boundaries.
Original test:
await expect(page.getByRole('heading', { name: 'User profile' })).toBeVisible();
await expect(page.getByTestId('name')).toHaveText('Ava Patel');
await expect(page.getByTestId('role')).toHaveText('Senior SDET');
await expect(page.getByTestId('team')).toHaveText('Quality Engineering');
await page.getByRole('link', { name: 'Edit profile' }).click();
Safe refactor:
await expect(page.getByRole('heading', { name: 'User profile' })).toBeVisible();
await expect.soft(page.getByTestId('name')).toHaveText('Ava Patel');
await expect.soft(page.getByTestId('role')).toHaveText('Senior SDET');
await expect.soft(page.getByTestId('team')).toHaveText('Quality Engineering');
expect(test.info().errors).toHaveLength(0);
await page.getByRole('link', { name: 'Edit profile' }).click();
The heading remains hard because it proves page identity. The profile fields become soft because they are peers. The checkpoint protects navigation into the edit workflow.
Use this review table:
| Code shape | Recommendation |
|---|---|
| Assertion followed by independent read-only assertions | Consider soft |
| Assertion followed by click, fill, submit, or delete | Keep hard or add checkpoint |
| Assertion establishes record identity | Keep hard |
| Repeated sibling fields in one stable component | Good soft-audit candidate |
| Assertions span unrelated features | Split tests |
| Flaky assertion with no root-cause work | Do not soften |
Refactoring is successful when it increases useful evidence without changing what the test permits.
11. Best Practices for Maintainable Soft-Assertion Suites
Treat soft assertions as a precise tool, not a suite-wide style.
- Define a hard prerequisite for page identity and component readiness.
- Keep each soft group centered on one business object.
- Use locator assertions instead of immediate property reads.
- Add custom messages for repeated or domain-specific rules.
- Set a hard checkpoint before any dependent or mutating action.
- Keep assertion timeouts small enough for fast failure and large enough for legitimate readiness.
- Prefer sequential checks for readable reporting.
- Name configured expect instances and page object methods explicitly.
- Retain native traces, screenshots, and HTML reports.
- Split unrelated validations into separate tests.
- Never include secrets in messages or attachments.
- Review retry behavior separately from soft behavior.
The test should remain understandable when every soft check passes. Excessive helper abstraction can make reporter source lines point to one generic function. Balance reuse with local business context.
When failures cluster, analyze the shared prerequisite first. Five wrong fields may come from one incorrect account or locale. Soft examples improve symptom visibility, but they do not replace root-cause reasoning.
12. Playwright expect soft Examples Review Checklist
Before committing a new example or helper, answer these questions:
- Is
expectimported from@playwright/test? - Is the primary page or record identity hard-asserted?
- Are all checks inside the soft group independently meaningful?
- Are asynchronous locator matchers awaited?
- Do generic value checks intentionally use immediate values?
- Would a configured soft instance be clearer than repeated
expect.soft? - Does each custom message add business context?
- Can any failed check lead to an unsafe later action?
- Is there a checkpoint before that action?
- Could sequential assertion timeouts make the failure excessively slow?
- Are reporter artifacts useful and safe to retain?
- Would separate tests create a clearer ownership boundary?
Run a deliberate negative test during development by changing two expected values. Confirm both failures appear, the final test status is failed, and dependent actions do not run. Restore the expectations before commit.
A soft pattern is production-ready only when its failure path is designed as carefully as its passing path.
Interview Questions and Answers
Q: Give a practical use case for expect.soft.
I use it to audit several independent values in a stable order summary. A wrong tax value should not prevent the test from also reporting the displayed subtotal and total, but submission should remain behind a hard checkpoint.
Q: Why should page identity remain a hard assertion?
If the page or record is wrong, every later comparison may be invalid. Stopping early prevents a large set of misleading secondary failures.
Q: How do custom messages help soft assertions?
They label each aggregated failure with its business rule. That is especially useful when many checks use the same matcher or locator pattern.
Q: Would you run soft assertions with Promise.all?
Usually no. Sequential execution gives predictable report order and simpler interaction with a changing page. I would parallelize only after measuring a real benefit and verifying reliable diagnostics.
Q: How do you compare API data with UI data safely?
I hard-check the response status and schema first, then soft-check independent rendered fields against trusted expected values. This prevents an invalid payload from contaminating every comparison.
Q: What is the role of test.info().errors?
It exposes errors recorded for the active test, including soft assertion failures. I use a hard length assertion as a checkpoint before navigation, submission, or deletion.
Q: Why name a page object method auditFieldsSoftly?
The name makes control flow explicit. Callers know the method may return after recording failures and must decide whether to add a checkpoint.
Q: How do you debug a slow soft-assertion test?
I count failing assertions and inspect their individual timeouts. Each failed web-first check may consume the full timeout sequentially, so I fix the shared readiness cause before increasing limits.
Common Mistakes
- Converting a full test to soft mode without analyzing dependencies.
- Soft-checking a failed API response, then attempting to parse it.
- Omitting record identifiers from repeated custom messages.
- Using immediate DOM reads as if soft mode added retries.
- Continuing to submit, promote, or delete after soft mismatches.
- Running a large soft group with long per-assertion timeouts.
- Hiding soft behavior behind generic page object method names.
- Parallelizing assertions and degrading report clarity.
- Attaching complete sensitive payloads to CI reports.
- Treating multiple symptoms as multiple root causes.
- Leaving unrelated assertions in one oversized test.
- Clearing or mutating runner-owned error collections.
Conclusion
Strong Playwright expect soft examples are built around control flow, not just syntax. They establish trustworthy state with hard assertions, collect independent mismatches with retrying soft matchers, and stop before any action that requires a clean audit.
Start with one stable component and three or four peer fields. Add business-focused messages, verify multiple deliberate failures appear in the report, and place an explicit error checkpoint before continuing the workflow.
Interview Questions and Answers
Describe a production-ready soft assertion pattern.
I hard-assert page and record identity, then run a small sequential group of soft locator assertions on independent fields. I use custom messages for business rules and add a hard `test.info().errors` checkpoint before any dependent action.
How would you implement data-driven soft assertions?
I create typed expectation objects containing a label, locator, and expected value, then iterate and await each `expect.soft` locator assertion. Sequential execution keeps reporting predictable and preserves web-first retry behavior.
How do you safely compare an API response with the UI?
I hard-check response success and validate the required payload shape first. Then I soft-check independent UI fields against the trusted response data. A failed prerequisite stops parsing and prevents misleading comparisons.
When would you use expect.soft.poll?
I use it for an eventually consistent, read-only external condition when later diagnostic checks remain useful if polling fails. If the condition gates promotion, submission, or another side effect, I use a hard poll instead.
Why avoid Promise.all for a group of soft assertions?
Sequential assertions produce clearer source order and are easier to reason about while the page changes. Parallel retries can complicate timelines and artifacts. I parallelize only with evidence that it is safe and valuable.
How should page objects expose soft checks?
The method name should explicitly indicate audit and soft behavior, and the helper should use native Playwright assertions. The test remains responsible for page prerequisites and the control-flow checkpoint.
What information belongs in a custom soft assertion message?
The message should identify the record or field and explain the business expectation. It should avoid duplicating the expected literal and must not include secrets or unnecessary personal data.
How do you prove a soft assertion implementation is correct?
During development I deliberately make two independent expectations wrong. I confirm both failures appear, the test is reported failed, native diagnostics are useful, and no dependent side effect runs after the checkpoint.
Frequently Asked Questions
What is a good Playwright expect soft example?
A good example hard-checks that the correct page or component is loaded, then softly validates several independent fields in that component. It adds a hard checkpoint before any later action depends on those fields.
Can I use expect.soft with toHaveText?
Yes. Use `await expect.soft(locator).toHaveText(expected)`. The locator assertion retains its retry behavior and records a failure if the text does not match before the assertion timeout.
How do I add a message to expect.soft?
Pass the message as the second argument: `expect.soft(locator, 'business rule').matcher()`. Keep the message focused on why the value matters instead of repeating the matcher.
Can I use expect.configure for soft assertions?
Yes. `expect.configure({ soft: true })` returns a configured expect instance. Give it a clear name and use the regular expect for hard prerequisites.
Should I put soft assertions in a page object?
You can, but name the method explicitly, such as `auditFieldsSoftly`, so callers understand its control flow. The test should still own any checkpoint before dependent actions.
Can expect.soft poll an API?
Yes. `expect.soft.poll(callback, options)` retries the callback and records a soft failure if the matcher does not pass. Keep polling callbacks read-only and use a hard check when readiness gates an action.
Why are many soft assertions slow when they fail?
Each failed web-first assertion can use its full timeout before the next sequential check starts. Several failures therefore add their waiting time, so fix shared readiness problems and keep groups focused.
Related Guides
- Playwright drag and drop: Examples and Best Practices
- Playwright expect toBeVisible: Examples and Best Practices
- Playwright expect toHaveCount: Examples and Best Practices
- Playwright expect toHaveText: Examples and Best Practices
- Playwright expect toHaveURL: Examples and Best Practices
- Playwright expect toHaveValue: Examples and Best Practices