Regular assertions are like a strict teacher. First wrong answer? Test over. Go home. Soft assertions are like a final exam. Answer every question. Get your score at the end. Both have their place. Let me explain when to use which.
Imagine you are verifying a dashboard page. It has 8 widgets. You write 8 assertions. The first widget has wrong text. Test fails. You fix it, run again. Now the third widget is broken. Fix, run, fifth widget broken. You just burned 4 test runs to find 3 bugs. With soft assertions, you find all 3 in one run.
import { test, expect } from '@playwright/test';
test('verify dashboard widgets', async ({ page }) => {
await page.goto('/banking/dashboard');
// Soft assertions -- test continues even if one fails
await expect.soft(page.getByTestId('account-name')).toHaveText('John Doe');
await expect.soft(page.getByTestId('account-type')).toHaveText('Savings');
await expect.soft(page.getByTestId('balance')).toContainText('$');
await expect.soft(page.getByTestId('status')).toHaveText('Active');
await expect.soft(page.getByTestId('last-login')).toBeVisible();
await expect.soft(page.getByTestId('notifications')).toHaveCount(3);
await expect.soft(page.getByTestId('quick-actions')).toBeVisible();
await expect.soft(page.getByTestId('recent-transactions')).toBeVisible();
// If any soft assertion fails, the test fails HERE at the end
// But ALL assertions are evaluated -- you see all failures at once
});Use soft assertions for independent checks and hard assertions when the next step depends on the previous one passing.
import { test, expect } from '@playwright/test';
test('transfer funds flow', async ({ page }) => {
await page.goto('/banking/transfer');
// SOFT -- verify page elements (independent checks)
await expect.soft(page.getByRole('heading', { name: 'Transfer Funds' })).toBeVisible();
await expect.soft(page.getByLabel('From Account')).toBeVisible();
await expect.soft(page.getByLabel('To Account')).toBeVisible();
await expect.soft(page.getByLabel('Amount')).toBeVisible();
// HARD -- fill the form (each step depends on the previous)
await page.getByLabel('From Account').selectOption('savings');
await page.getByLabel('To Account').selectOption('current');
await page.getByLabel('Amount').fill('1000');
await page.getByRole('button', { name: 'Transfer' }).click();
// HARD -- verify the transfer succeeded (critical assertion)
await expect(page.getByText('Transfer successful')).toBeVisible();
});When soft assertions fail, Playwright reports all failures in the test result. You see every failed check with its expected vs actual value. This is gold in CI/CD -- one test run, all failures visible.
Error: 3 soft assertion(s) failed:
1) expect.soft(page.getByTestId('account-type')).toHaveText('Savings')
Expected: "Savings"
Received: "Current"
2) expect.soft(page.getByTestId('status')).toHaveText('Active')
Expected: "Active"
Received: "Inactive"
3) expect.soft(page.getByTestId('notifications')).toHaveCount(3)
Expected: 3
Received: 0Use soft assertions when testing a page for the first time. You discover all broken elements in one run instead of fixing them one by one. Switch to hard assertions once the page is stable and you are writing flow tests.
Do not use soft assertions for everything. If a login fails, there is no point checking dashboard elements -- they will all fail. Use hard assertions for gates and soft assertions for verification sweeps.
Q: What are soft assertions in Playwright and when would you use them?
A: Soft assertions use expect.soft() instead of expect(). The difference is that soft assertions do not stop the test on failure. They collect all failures and report them together at the end. I use them when I have multiple independent checks -- like verifying all widgets on a dashboard page. If widget 1 is broken, I still want to know about widgets 2 through 8. For sequential flows like login or checkout, I use hard assertions because there is no point continuing if a critical step fails. The best approach is mixing both: soft assertions for page verification, hard assertions for user flow steps.
Key Point: expect.soft() collects all failures without stopping. Use it for independent checks. Use hard assertions for sequential flows where later steps depend on earlier ones.