Getting started
Install Sounding, keep your Sails test environment in config/env/test.js, and add config/sounding.js only when you need overrides.
1. Install Sounding
npm install -D sounding @playwright/test sails-sqliteInstall the Sounding skill for AI agents
If you use Codex, Claude Code, Cursor, or another coding agent, install the Sounding skill too. That gives the agent guidance for worlds, actors, transports, mailbox assertions, browser trials, and Sounding's auth helpers.
npx skills add sailscastshq/sounding/skills2. Set up your test environment
Sounding does not replace Sails test config.
Your app still defines its test environment in config/env/test.js, and Sounding builds on top of that.
One common setup is to let Sounding manage the datastore and keep config/env/test.js focused on app behavior.
// config/env/test.js
module.exports = {
port: 3333,
models: {
migrate: 'drop'
},
mail: {
default: 'log'
},
log: {
level: 'error'
}
}If your app already has a good test datastore strategy, Sounding can still respect it through datastore.mode = 'inherit'.
3. Add config/sounding.js only if you need overrides
module.exports.sounding = {
environments: ['test'],
datastore: 'inherit'
}Most apps can skip this file entirely. Sounding already defaults to:
environments = ['test']datastore.mode = 'managed'datastore.identity = 'default'datastore.adapter = 'sails-sqlite'datastore.root = '.tmp/db'datastore.isolation = 'worker'mail.capture = truerequest.transport = 'virtual'- browser projects start with
desktop
Only add config/sounding.js when your app needs a real override, such as datastore: 'inherit', datastore: 'external', custom browser behavior, or widening Sounding beyond the default test-only environment.
If you intentionally need Sounding in another boot path, widen the allowlist explicitly:
module.exports.sounding = {
environments: ['test', 'console']
}Optional: install plugin features
Sounding keeps heavier capabilities in plugins. Install the plugin package, then use the API it adds.
This is separate from the core configuration above. There is no plugins array and no config/sounding.js registration step for plugin discovery. The dev dependency is the registration.
For example, stress testing is powered by sounding-plugin-stress:
npm install -D sounding-plugin-stress
npx sounding stress /api/health --duration=10 --concurrency=25Read Plugins for the plugin model and Stress testing for the full stress API.
4. Write your first trial
If you are new to Sounding's callback shape, read Trials and Trial context alongside this guide. They explain what a trial is, what arrives inside test(), and when things like page and login appear.
import { test } from 'sounding'
test('signupWithTeam creates a team and membership', async ({
sails,
expect
}) => {
const result = await sails.helpers.user.signupWithTeam({
fullName: 'Kelvin O',
email: '[email protected]',
tosAcceptedByIp: '127.0.0.1'
})
expect(result.user.email).toBe('[email protected]')
expect(result.team.name).toBeDefined()
})5. Use the same trial surface for endpoints
import { test } from 'sounding'
test('guest is redirected from the dashboard', async ({ get, expect }) => {
const response = await get('/dashboard')
expect(response).toRedirectTo('/login')
})6. Write your first browser trial
import { test } from 'sounding'
test(
'subscriber can read a members-only issue',
{ browser: true, world: 'issue-access' },
async ({ page, login, expect }) => {
await login.as('subscriber', page)
await page.goto('/issues/the-nerve-to-build')
await expect(page.getByText('The rest of the story')).toBeVisible()
}
)6.1 Define your first world
Define a named world for repeatable setup. Common starting points are:
- a signed-out guest
- a subscriber with active access
- a publisher with a draft
- a reader waiting on a magic link
For example:
import { defineScenario } from 'sounding'
export default defineScenario('issue-access', async ({ create }) => {
const publisher = await create('user').trait('publisher')
const subscriber = await create('user').trait('subscriber')
const issue = await create('issue', {
author: publisher.id,
isFree: false
})
return {
users: { publisher, subscriber },
issues: { gatedIssue: issue }
}
})Then the trial can read from the product situation directly:
test(
'subscriber can read the issue',
{ world: 'issue-access' },
async ({ world, request, expect }) => {
const response = await request
.as('subscriber')
.get(`/i/${world.current.issues.gatedIssue.slug}`)
expect(response).toHaveStatus(200)
}
)Use worlds when several trials need the same business state.
7. Run the suite
npx sounding testMost generated apps wire npm test to the same command.
By default, sounding test uses Sounding's readable reporter. Small passing runs list the trial names and end with a compact summary:
PASS tests/arch.test.js
✓ request helpers stay response-shaped 1ms
✓ JSON paths read like product facts 0ms
PASS Tests: 2 passed, 2 total
Duration: 94msWhen a trial fails, Sounding keeps the first application frame in view and groups the context that usually explains the behavior:
FAIL tests/billing.test.js
× creator sees billing summary
Expected response status 200, received 500.
Request
GET /api/billing/summary (http) -> http://localhost:1337/api/billing/summary
headers: accept: application/json
Response
500 Server Error
headers: content-type: application/json
Body
{"message":"boom"}
at tests/billing.test.js:25
-> 25 expect(response).toHaveStatus(200)
FAIL Tests: 1 failed, 1 total
Duration: 70msUse --verbose when you want full stacks and expanded diagnostics:
npx sounding test --verboseUse --raw-error when formatted output hides something you need. Raw mode keeps the readable failure first, then prints the original Node test error, its cause, Sounding metadata, and the primary frame payload:
npx sounding test --raw-errorUse --compact for failure-focused output in CI. Passing trials stay quiet, but the final summary and the full Sounding failure block remain readable:
npx sounding test --compactUse --profile when a suite starts feeling slow. The reporter prints the slowest trials before the final summary so you can see whether time is going into browser flows, app boot, auth setup, or request trials:
npx sounding test --profile
npx sounding test --profile --slow=10--slow controls how many trials appear in the profile list and implies --profile.
For larger suites, split the same discovered test files across CI jobs with --shard=part/total:
npx sounding test --shard=1/4
npx sounding test --shard=2/4 --parallelSharding composes with the normal Sounding filters:
npx sounding test --lane browser --shard=1/4
npx sounding test --file tests/sounding/examples.test.js --shard=2/4A GitHub Actions matrix can pass the shard number directly:
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx sounding test --shard=${{ matrix.shard }}/4 --profile --slow=10What to add next
Once the basic runtime is in place, most apps will want to add:
- factories under
tests/factories - scenarios under
tests/scenarios - a few named actors like
guest,subscriber, andpublisher - at least one endpoint, Inertia, and browser trial for a mission-critical flow
- a mobile browser project once the core journeys are stable