End-to-End Testing
Sounding uses Playwright under the hood for browser-capable trials, but the public story stays the same:
- one
test()API - one trial context
- browser surfaces only when the browser matters
Opting into the browser
Browser-capable trials are explicit:
const { test } = require('sounding')
test(
'magic link login reaches the dashboard',
{ browser: true },
async ({ page, login, expect }) => {
await login.as('reader@example.com', page)
await expect(page).toHaveURL(/\/dashboard$/)
}
)When { browser: true } is enabled, the trial context can include:
pagebrowserContextbrowser- Playwright-backed
expect()behavior
Use named projects when the trial needs mobile or a specific browser engine:
test(
'mobile nav opens the account menu',
{ browser: 'mobile' },
async ({ page }) => {
await page.goto('/dashboard')
}
)Use the object form when the trial also needs artifacts or overrides:
test(
'checkout works in WebKit',
{ browser: { project: 'safari' } },
async ({ page }) => {
await page.goto('/checkout')
}
)Failed browser-capable trials also keep useful evidence by default:
- the current URL
.tmp/sounding/artifacts/<trial-name>/<browser-project>/current-url.txt.tmp/sounding/artifacts/<trial-name>/<browser-project>/screenshot.png
When a failure is flaky or timing-sensitive, scope heavier capture to that trial:
test(
'checkout keeps the cart after refresh',
{
browser: {
artifacts: {
trace: true,
video: true
}
}
},
async ({ page }) => {
await page.goto('/checkout')
}
)Keep trace and video opt-in unless the app has a deliberate CI artifact policy.
Use browser-capable trials for the right things
Good uses:
- sign-in journeys
- editor flows
- gated content flows
- checkout handoff
- mobile navigation
- anything where DOM state and navigation are the behavior
Bad uses:
- simple redirects
- raw JSON response contracts
- page props that
visit()can prove more cheaply
Worlds matter even more here
Browser trials become fragile when they carry too much setup. Use a named world whenever it improves readability.
test(
'subscriber can finish a members-only issue',
{ browser: true },
async ({ sails, login, page, expect }) => {
const current = await sails.sounding.world.use('issue-access')
await login.as('subscriber', page)
await page.goto(`/issues/${current.issues.gatedIssue.slug}`)
await expect(
page.getByText(current.issues.gatedIssue.premiumDetail)
).toBeVisible()
}
)Auth in browser-capable trials
Prefer Sounding's auth helpers:
login.as(actorOrEmail, page)for the common pathauth.issueMagicLink(actorOrEmail)when you want the token or URL directlyauth.requestMagicLink(actorOrEmail)when you want to exercise the real request + mail flow
Rules:
- use actor names like
'subscriber'when the user comes from the current world - use explicit emails when the trial is really about the auth path itself
Mobile should stay first-class
A good Boring Stack suite should not treat mobile as an afterthought.
When a flow is navigation-heavy or layout-sensitive, prefer at least one browser-capable trial that exercises the mobile path too.
File structure
Keep browser-capable trials under the normal product tree:
tests/e2e/pages/
auth/
magic-link-browser.test.js
dashboard/
editor.test.js
issues/
reader-access.test.jsDo not create a separate framework-branded folder just because the browser is involved.