sounding
v0.2.0
Published
Testing framework for Sails applications and The Boring JavaScript Stack.
Maintainers
Readme
Sounding
Sounding is a testing framework for Sails applications and The Boring JavaScript Stack.
It is designed to be:
- a Sails hook first
- a CLI second
- powered by the native Node.js test runner
- integrated with Playwright for browser testing
- elegant for helper, endpoint, JSON API, Inertia, mail, and browser trials
The canonical Sails-native surface is:
- optional
config/sounding.jswhen you need overrides sails.soundingsails.helpers.user.signupWithTeam(...)inside trialsget('/api/issues')orsails.sounding.request.get('/api/issues')inside endpoint-style trialsawait auth.login.withPassword('[email protected]', page, { password: 'secret123' })inside browser trialsawait auth.request.withPassword('[email protected]', { password: 'secret123' })inside request trialstest.it('...', async ({ expect }) => {})when you want the behavior-reading aliastest('...', { world: 'signed-in-user' }, async ({ request }) => {})can auto-load named worlds before the handler runsrequest.as('owner')andvisit.as('owner')can resolve actor aliases from the current worldtest('...', { browser: 'mobile' }, async ({ page }) => {})can select a named browser project without extra ceremony- failed browser trials capture the current URL and a full-page screenshot under
.tmp/sounding/artifacts - opt-in visual snapshots can compare browser pages with
expect(page).toMatchScreenshot('pricing') - request helpers default to Sails virtual requests powered by
sails.request() - virtual request responses expose the final
req.sessionsnapshot asresponse.session; HTTP responses leave it undefined - request assertions can check auth/session state with
expect(response).toHaveSession('userId', user.id)and flash messages withexpect(response).toHaveFlash('info', /welcome/i) - failed response assertions include concise request/response diagnostics; set
SOUNDING_DIAGNOSTICS=verbosefor full response excerpts - Inertia-style visits can use
visit('/pricing')and partial reload options like{ component, only } - browser smoke helpers can check one route or a route list with
smoke(['/pricing', '/contact']) - mail assertions can check captured emails with
expect(mailbox).toHaveSentMail({ to, subject })andexpect(mailbox.latest()).toHaveCtaUrl(/magic-link/) - a trial can opt into stricter parity with
test('...', { transport: 'http' }, ...) - upload trials use
FormDataover the HTTP transport so Sails can exercise real Skipper streams - independent trials can opt into concurrent execution with
test('...', { concurrent: true }, ...)ortest.concurrent(...) - any trial can also scope a request client with
sails.sounding.request.using('http')
Sounding also owns its own built-in world engine, so the same package can:
- define factories under
tests/factories - define scenarios under
tests/scenarios - auto-load named worlds for endpoint, Inertia, socket, and browser trials
- compose world records with fluent builders like
await create('user').trait('admin').with({ email }) - merge repeated builder
.with()calls, with.withOnly()available when you want to use only the next overrides - capture outgoing mail by wrapping
sails.helpers.mail.sendand storing normalized messages insails.sounding.mailbox
Request-level Inertia trials can assert page contracts without launching a browser:
const { test } = require('sounding')
test('dashboard shows the signed-in creator', { world: 'signed-in-user' }, async ({ visit, expect }) => {
const page = await visit.as('owner')('/dashboard')
expect(page).toBeInertiaPage('dashboard/index')
expect(page).toHaveInertiaProps({
'auth.user.email': '[email protected]',
'stats.projects': 2,
projects: [{ name: 'Launch Plan' }],
})
expect(page).toHaveNoInertiaErrors()
})
test('dashboard partial reload returns only notifications', async ({ visit, expect }) => {
const page = await visit('/dashboard', {
component: 'dashboard/index',
only: ['notifications'],
reset: ['sidebar'],
})
expect(page).toBeInertiaPage('dashboard/index')
expect(page).toHaveInertiaPartialReload({
component: 'dashboard/index',
only: ['notifications'],
reset: ['sidebar'],
})
expect(page).toHaveOnlyInertiaProps(['notifications'])
})The default configuration story is intentionally calm:
- Sounding only enables its hook in the environments listed under
sounding.environments - the default is
['test'], so non-test boot paths stay dark unless you opt in explicitly - if you intentionally need Sounding in another environment, add that environment name to the list
- auth conventions auto-detect
User/userIdandCreator/creatorId, withsounding.authavailable for overrides - Sounding manages a temporary
sails-sqlitedatastore by default - managed SQLite artifacts live under
.tmp/db - the default datastore identity is
default - browser projects start with
desktop - browser projects can be strings or named project objects with
type,device,viewport,contextOptions, andlaunchOptions - browser failure artifacts store screenshots and current URLs by default, while traces and videos are opt-in
- mail capture previews use the
maillayout by default, matching the currentsails-hook-mailconvention - apps with a different mail layout can set
sounding.mail.layout, for examplelayout-email inheritremains available when an app already has a serious test datastore story
For example:
module.exports.sounding = {
environments: ['test'],
}If you intentionally want Sounding during another boot path, widen the list explicitly, for example ['test', 'console'] or ['test', 'production'].
Upload trials
Use HTTP request trials for Sails file uploads. Uploads in Sails are streaming Skipper requests, so they need the HTTP stack that Sails uses for real multipart forms.
const { test } = require('sounding')
test(
'creator can upload a receipt',
{ transport: 'http' },
async ({ request, expect }) => {
const form = new FormData()
form.append('description', 'Home office monitor')
form.append('amount', '1200')
form.append(
'receipt',
new Blob(['receipt bytes'], { type: 'application/pdf' }),
'receipt.pdf'
)
const response = await request.post('/expenses', form)
expect(response).toRedirectTo('/expenses/new')
}
)When a multipart form mixes text fields and files, append the text fields before the files. That matches Sails and Skipper's streaming model, where actions can start while file streams are still arriving.
Do not use the virtual transport for upload behavior. Virtual requests are still
right for normal endpoints, JSON APIs, session assertions, redirects, and
Inertia contracts, but real req.file() uploads are HTTP-only in Sails.
Project init
Use the initializer from a Sails app to add the first Sounding test lane:
npx sounding initIt updates package.json, creates tests/factories, tests/scenarios, and tests/sounding, and writes starter examples without overwriting existing files. The default setup relies on Sounding's built-in conventions, so it skips config/sounding.js unless you ask for an editable config scaffold:
npx sounding init --configTypical output looks like:
Sounding initialized /path/to/my-sails-app
Auth convention: User
~ Updated package.json (added `npm test`, added `sounding` devDependency, added `sails-sqlite` devDependency)
+ Created tests
+ Created tests/factories
+ Created tests/scenarios
+ Created tests/sounding
+ Created tests/factories/user.js
+ Created tests/scenarios/signed-in-user.js
+ Created tests/sounding/examples.test.js
- Skipped config/sounding.js because Sounding defaults are enough
Next: run npm install, then npm test.CLI test runner
Run a Sounding suite with the framework-level runner:
npx sounding testThe command discovers .test.js files under tests/ and test/, then runs Node's native test runner with Sounding-friendly filters:
npx sounding test --grep "dashboard"
npx sounding test --file tests/sounding/examples.test.js
npx sounding test --lane browser
npx sounding test --shard=1/4
npx sounding test --parallel
npx sounding test --watchCommon Node test flags pass through, and CI reporters are available without memorizing the longer Node flag names:
npx sounding test --reporter spec
npx sounding test --junit reports/sounding-junit.xml
npx sounding test --json
npx sounding test --coverageWhen no reporter is specified, sounding test uses Sounding's readable reporter. Failed response assertions group the request, response, body, file location, and code frame so the behavior is visible without digging through a raw stack trace. Use --compact for failure-focused output, --profile to print the slowest trials before the final summary, and --slow=N to control how many profiled trials are shown:
npx sounding test --compact
npx sounding test --profile --slow=10For larger suites in CI, split the same discovered file list across matrix jobs with --shard=part/total. Sharding composes with lanes and explicit files:
npx sounding test --lane browser --shard=1/4
npx sounding test --file tests/sounding/examples.test.js --shard=2/4strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx sounding test --shard=${{ matrix.shard }}/4 --profile --slow=10Use --verbose when you want full stacks and expanded Sounding diagnostics:
npx sounding test --verboseUse --raw-error or SOUNDING_RAW=1 when the formatted view hides something important. Raw mode keeps the pretty failure first, then prints the original Node test error, its cause, Sounding metadata, and the primary frame payload:
npx sounding test --raw-errorUse --update-snapshots to create or update visual screenshot baselines:
npx sounding test --update-snapshotsUse --dry-run to inspect the exact node --test command before running it.
The repository includes an intentionally failing reporter fixture that is useful for before/after screenshots:
node ./bin/sounding.js test --app examples/pretty-output-demoThere is also a small passing fixture for successful-output screenshots:
node ./bin/sounding.js test --app examples/pretty-output-successPlugins
Sounding keeps heavy or specialized features in installable plugins. Core
discovers plugin packages installed in your app when their package names match
sounding-plugin-* or @sounding/plugin-*, so official plugins work as soon as
they are added as dev dependencies.
The setup is intentionally install-only:
npm install -D sounding-plugin-stressThere is no plugins array and no config/sounding.js registration step. The
dev dependency is the registration: Sounding reads your app's package.json,
loads matching plugin packages, and lets each plugin register:
- CLI commands, such as
sounding stress - focused test methods, such as
test.stress(...) - trial context helpers, such as
{ stress }
For example, stress testing lives in sounding-plugin-stress:
sounding stress /api/health --duration=10 --concurrency=25Plugin commands, trial helpers, and focused test methods are registered through
the plugin package. Core also provides a small event bus for lifecycle and
streaming use cases such as stress:start and stress:done, while keeping
capability registration explicit and predictable.
Event emitters are the right tool for observability and lifecycle side effects, not for discovering the public API. A plugin should register commands, test methods, and trial helpers explicitly, then use events for things other tools may want to watch.
If sounding stress is run before the plugin is installed, Sounding prints the
plugin install command instead of making stress testing a required dependency for
every project.
Stress Testing
Install sounding-plugin-stress to run real HTTP load checks from the CLI or
inside Sounding trials.
npm install -D sounding-plugin-stressSounding uses autocannon as the first stress engine. It is owned by the stress
plugin, not by Sounding core, so apps that never run load checks do not install a
load-testing engine. autocannon is a Node-native HTTP benchmarking tool, which
fits Sounding's runtime and maps cleanly to Sails HTTP routes.
The public API is still Sounding's API. The plugin translates fluent Sounding calls into engine options, then normalizes engine output into assertion-friendly metrics. That gives us room to add another engine later without changing trial code.
External targets run directly:
sounding stress https://staging.example.com/api/health --duration=10 --concurrency=25Relative targets are Sails-native. Sounding lifts the app, then stresses the real HTTP route:
sounding stress /api/health --duration=10 --concurrency=25Relative paths can also target a deployed host without lifting a local app:
sounding stress /api/health --base-url=https://staging.example.comWorlds and actor aliases work for local Sails app stress runs:
sounding stress /api/billing/summary \
--world=subscribed-creator \
--as=owner \
--duration=10 \
--concurrency=20Remote and --base-url targets should use normal HTTP auth, such as headers or
tokens:
sounding stress https://staging.example.com/api/me \
--header "Authorization: Bearer $TOKEN"Useful CLI options:
sounding stress <target> \
--duration=10 \
--concurrency=25 \
--method=POST \
--header "x-test-lane: stress" \
--json '{"plan":"pro"}'Method shorthands are available too:
sounding stress /api/invoices --post='{"plan":"pro"}'
sounding stress /api/health --get
sounding stress /api/session --deleteInside a trial, use test.stress() when the behavior needs real HTTP load:
const { test } = require('sounding')
test.stress(
'billing summary stays fast under creator load',
{ world: 'subscribed-creator' },
async ({ stress, expect }) => {
const result = await stress
.get('/api/billing/summary')
.as('owner')
.concurrently(20)
.for(10)
.seconds()
expect(result.requests.failed().count()).toBe(0)
expect(result.requests.duration().p95()).toBeLessThan(250)
}
)You can also use the stress helper in any HTTP-capable trial:
test(
'health endpoint has no failed requests',
{ transport: 'http' },
async ({ stress, expect }) => {
const result = await stress.get('/api/health').concurrently(25).for(10).seconds()
expect(result.requests.failed().count()).toBe(0)
}
)The fluent request API covers the common HTTP shapes:
await stress.get('/api/health')
await stress.head('/api/health')
await stress.options('/api/health')
await stress.post('/api/invoices').json({ plan: 'pro' })
await stress.put('/api/invoices/1').body('raw body')
await stress.patch('/api/invoices/1', { memo: 'updated' })
await stress.delete('/api/session')
await stress.request('POST', '/api/events').headers({ authorization: 'Bearer token' })The result exposes stable metrics like requests.count(),
requests.rate(), requests.failed().count(), requests.duration().p95(),
and testRun.concurrency(). It also keeps the raw engine result at result.raw
when you need to inspect autocannon-specific fields.
App lifecycle
Sounding keeps a warm Sails app by default. Virtual request trials load the app without opening an HTTP listener, while HTTP, socket, and browser-capable trials lift the app so the network stack exists.
The app manager makes those lanes explicit:
const { createAppManager } = require('sounding')
const manager = createAppManager()
const virtualRuntime = await manager.runtime({ app: 'load' })
const httpRuntime = await manager.runtime({ app: 'lift' })
const alsoHttpRuntime = await manager.runtime({ transport: 'http' })Use the warm default for most suites. When a trial mutates process-global app state and needs a fresh Sails instance, force a reload:
const freshRuntime = await manager.runtime({ app: 'load', reload: true })Lifecycle timings are available for diagnostics:
console.log(manager.lifecycle.load.durationMs)
console.log(manager.lifecycle.lift.status)Set SOUNDING_LIFECYCLE=verbose or SOUNDING_DIAGNOSTICS=verbose to print app load/lift timing messages while the suite runs.
Concurrent trials
Sounding runs trials serially by default. That keeps shared Sails app state boring while request sessions, worlds, mailboxes, sockets, and browser sessions continue to reset between trials.
Independent trials can opt into Node test concurrency:
test.it('health check is readable', async ({ get, expect }) => {
const response = await get('/health')
expect(response).toHaveStatus(200)
})
test.concurrent('health check is isolated', async ({ get, expect }) => {
const response = await get('/health')
expect(response).toHaveStatus(200)
})
test('dashboard contract is isolated too', { concurrent: true }, async ({ visit, expect }) => {
const page = await visit('/dashboard')
expect(page).toBeInertiaPage('dashboard/index')
})Concurrent Sounding trials bypass the global serial queue and receive isolated runtime state. Their request session, mailbox, world, sockets, and browser manager are separate from other concurrent trials. Managed SQLite datastore paths remain isolated by worker using .tmp/db/<identity>/worker-<token>.db, where the worker token comes from SOUNDING_WORKER_INDEX, PLAYWRIGHT_WORKER_INDEX, TEST_WORKER_INDEX, or the process id.
Use concurrent mode for trials that do not mutate process-global app state. If you build a custom createTestApi({ runtime }), pass a runtime factory such as () => createRuntime(sails) for concurrent trials; a single shared runtime object stays serial-only.
Typing and editor support
Sounding is JSDoc-first today. The public API types live beside the CommonJS source, with shared typedefs in lib/types.js, so JavaScript Sails apps get autocomplete and inline docs without a separate hand-maintained declaration surface.
The type smoke test in typecheck/public-api-smoke.js checks the exported API that consumers use: test(), request and visit clients, worlds, mail, auth, browser, socket helpers, runtime factories, and default config. Run it with:
npm run typecheckSounding does not ship hand-written .d.ts files right now. If TypeScript consumers need declaration files later, they should be generated from the JSDoc source of truth and verified against the same public API smoke test.
Browser projects
Browser trials start on the desktop project:
test('subscriber can read a gated issue', { browser: true }, async ({ visit, expect }) => {
const page = await visit('/issues/the-nerve-to-build')
await expect(page).toSee('The rest of the story')
expect(page).toHaveNoSmoke()
})Use a string when a trial needs a named project:
test('mobile navigation opens the account menu', { browser: 'mobile' }, async ({ visit, expect }) => {
const page = await visit('/dashboard').inDarkMode()
await page.click('Account')
await expect(page).toSee('Settings')
})Browser page is a Sounding wrapper around the Playwright page. It keeps common
actions fluent while assertions stay under expect(page):
await page
.click('@send-link')
.type('email', '[email protected]')
.press('Send link')
await expect(page).toSee('Check your email')
await expect(page).not.toSee('Invalid email')
expect(page).toHavePath('/check-email')Browser test handles
Use @name for stable test handles. This is a Sounding convention inspired by
Pest's browser-testing ergonomics: the @ prefix does not mean CSS, id, or a
JavaScript decorator. It means "find this element by its test handle."
Sounding maps @send-link to elements with data-test="send-link" or
data-testid="send-link":
<button data-test="send-link">Email me a link</button>await page.click('@send-link')Prefer test handles for controls whose visible copy may change, repeated UI, or elements that do not have a natural accessible label. Plain text and normal CSS selectors still work:
await page.click('Email me a link')
await page.click('#send-link')
await page.fill('input[name="email"]', '[email protected]')Host-aware browser visits
Use withHost() when the host matters to the app:
const page = await visit('/dashboard')
.withHost('app.test')
.inDarkMode()That resolves the relative visit target to http://app.test/dashboard. If the
host includes a scheme, Sounding preserves it:
await visit('/dashboard').withHost('https://creator.example.com')This is useful for tenant domains, custom host routing, signed-link hosts, and
apps that branch on the request Host header.
Use the trial option for browser projects:
test('mobile nav works', { browser: 'mobile' }, async ({ visit }) => {
await visit('/dashboard')
})Use visit().on() when one visit inside a browser trial needs a different
project before navigation:
test('mobile nav works', { browser: true }, async ({ visit }) => {
await visit('/dashboard').onMobile()
await visit('/dashboard').on('safari')
})For a whole trial, prefer { browser: 'mobile' }. For one specific visit,
visit('/path').onMobile() closes the current browser session, opens the named
project, then applies actor login, color scheme, locale, host, and other setup
before it navigates.
The wrapper also includes common browser journey verbs:
await visit('/settings')
.as('owner')
.inDarkMode()
.withGeolocation(6.5244, 3.3792)
.click('@avatar')
.attach('@avatar-file', 'fixtures/avatar.png')
.typeSlowly('@display-name', 'Kelvin')
.clear('@tagline')
.append('@tagline', 'Building in public')
.key('Enter')
await page.withinFrame('@billing-frame', async (frame) => {
await frame.click('@save-card')
})
await page.screenshotElement('@receipt', '.tmp/receipt.png')
expect(page).toHaveNoConsoleErrors()
expect(page).toHaveNoSmoke()Supported browser page actions:
| API | Purpose |
| --- | --- |
| page.click(target) | Click visible text, a test handle, or selector. |
| page.type(target, value) / page.fill(target, value) | Fill an input by label, test handle, or selector. |
| page.typeSlowly(target, value) | Type with a small delay for search boxes, masks, and key-driven UI. |
| page.append(target, value) | Add text to the current input value. |
| page.clear(target) | Empty an input. |
| page.press(target, key) | Press a key while focused on a target. |
| page.select(target, value) | Select an option. |
| page.check(target) / page.uncheck(target) | Toggle checkboxes and radios. |
| page.hover(target) | Hover over a target. |
| page.attach(target, files) | Attach one or more files to an upload input. |
| page.drag(source, target) | Drag one target onto another. |
| page.scroll(target) | Scroll an element into view, or scroll the page with numeric coordinates. |
| page.wait(target) | Wait for a timeout, selector, test handle, or load state. |
| page.resize(width, height) | Resize the page viewport. |
| page.key(key) / page.keys(keys) | Press keyboard shortcuts. |
| page.back() / page.forward() / page.reload() | Navigate browser history or reload. |
| page.debug() | Pause in Playwright when the runtime supports it. |
| page.withinFrame(target, callback) | Scope actions to an iframe. |
Supported browser setup helpers:
| API | Purpose |
| --- | --- |
| visit('/path').as(actor) | Log in as a world actor before navigation. |
| visit('/path').on(project) / onMobile() | Open a named browser project before navigation. |
| visit('/path').withHost(host) | Resolve relative paths against a specific host. |
| visit('/path').inDarkMode() / inLightMode() | Emulate color scheme before navigation. |
| visit('/path').withLocale(locale) | Override browser locale signals where possible. |
| visit('/path').withTimezone(timezone) | Store timezone intent for browser metadata and future context support. |
| visit('/path').withUserAgent(userAgent) | Set the page user-agent header where possible. |
| visit('/path').withGeolocation(lat, lon) | Grant geolocation permission and set coordinates where possible. |
Supported browser read and capture helpers:
| API | Purpose |
| --- | --- |
| page.url() | Read the current URL. |
| page.text() / page.text(target) | Read full-page text or target text. |
| page.html() / page.content() | Read the page HTML. |
| page.script(fn, arg) | Evaluate code in the browser. |
| page.screenshot(pathOrOptions) | Capture a page screenshot. |
| page.screenshotElement(target, pathOrOptions) | Capture one element. |
Supported browser expectations:
| API | Purpose |
| --- | --- |
| expect(page).toSee(text) | Assert page text is visible in the document text. |
| expect(page).not.toSee(text) | Assert page text is absent. |
| expect(page).toHaveUrl(url) | Assert the full URL, or path when the expected value starts with /. |
| expect(page).toHavePath(path) | Assert path, query, and hash. |
| expect(page).toHaveTitle(title) | Assert document title. |
| expect(page).toHaveNoJavascriptErrors() | Fail on browser runtime errors. |
| expect(page).toHaveNoConsoleLogs() | Fail on any console message, including log, warn, info, and error. |
| expect(page).toHaveNoConsoleErrors() | Fail only on console.error. |
| expect(page).toHaveNoSmoke() | Fail on JavaScript errors or console errors. |
| expect(page).toMatchScreenshot(name) | Compare a full-page screenshot against an approved visual baseline. |
Visual regression snapshots
Use visual snapshots only when the rendered page shape matters more than a DOM contract:
test('pricing page matches approved desktop screenshot', { browser: 'desktop' }, async ({ visit, expect }) => {
const page = await visit('/pricing')
await expect(page).toMatchScreenshot('pricing')
})Baselines live under tests/screenshots/<browser-project>/<name>.png, so
desktop and mobile screenshots do not collide. Create or update baselines with:
SOUNDING_UPDATE_SNAPSHOTS=1 npx sounding test
# or
npx sounding test --update-snapshotsWhen a baseline is missing, Sounding tells you how to create it. When a
screenshot differs, Sounding writes actual.png, expected.png, and
diff.html under .tmp/sounding/artifacts/visual/<browser-project>/<name>/
and includes those paths in the failure output.
Browser smoke helpers
Use smoke() for public pages where the whole point is "open these routes and
fail if the browser sees JavaScript errors or console.error output":
test('public pages do not smoke', async ({ smoke }) => {
await smoke(['/', '/pricing', '/contact'])
})smoke() opens a browser lazily, so an otherwise request-level trial can stay
light until it actually needs the browser. Pass a browser project when the smoke
check should run through a named project:
test('mobile pages do not smoke', async ({ smoke }) => {
await smoke(['/', '/pricing'], { project: 'mobile' })
})Use visit.all() when you want the inspected collection back:
test('public pages do not smoke', async ({ visit, expect }) => {
const pages = await visit.all(['/', '/pricing', '/contact'])
expect(pages).toHaveNoSmoke()
expect(pages.entries[0].target).toBe('/')
})Each collection entry includes the original target, project, currentUrl,
captured JavaScript errors, console messages, console errors, and the wrapped
browser page. On failure, Sounding stops at the first smoky route so terminal
diagnostics and browser artifacts point at the route that actually failed.
Raw Playwright access remains available through page.raw or
page.playwrightPage when a browser flow needs a lower-level escape hatch.
Configure named projects in config/sounding.js when an app needs mobile devices, WebKit, or custom context options:
module.exports.sounding = {
browser: {
projects: {
desktop: {},
mobile: {
device: 'iPhone 13'
},
safari: {
type: 'webkit',
viewport: {
width: 1280,
height: 720
}
}
}
}
}The object form stays Sails-simple while still passing through to Playwright where it matters.
Browser failure artifacts
Browser-capable trials should be easy to debug without turning every run into a heavyweight recording session.
By default, a failed { browser: true } trial writes:
current-url.txtscreenshot.png
under a stable, readable directory:
.tmp/sounding/artifacts/<trial-name>/<browser-project>/For a trial named dashboard shows owner stats on the default desktop project, that becomes:
.tmp/sounding/artifacts/dashboard-shows-owner-stats/desktop/When a failure happens, Sounding appends the current URL and artifact paths to the thrown error so the terminal output points straight at the evidence.
Traces and videos are intentionally off by default because they cost more disk and time. Turn them on for a whole app:
module.exports.sounding = {
browser: {
artifacts: {
trace: true,
video: true
}
}
}Or scope them to one suspicious trial:
test(
'checkout keeps the cart after refresh',
{
browser: {
artifacts: {
trace: true,
video: true
}
}
},
async ({ page, expect }) => {
await page.goto('/checkout')
await page.reload()
await expect(page).toSee('Your cart')
expect(page).toHaveNoSmoke()
}
)Use false as a concise off switch:
test('fast smoke flow', { browser: { artifacts: false } }, async ({ visit, expect }) => {
const page = await visit('/health')
await expect(page).toSee('OK')
})For artifact settings, true means “keep this when the trial fails.” If you need an artifact on successful browser trials too, use on instead:
module.exports.sounding = {
browser: {
artifacts: {
trace: 'on'
}
}
}This repository starts with docs-driven product research and the first hook/runtime scaffolding for that vision.
See RESEARCH.md.
