sounding 0.0.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +334 -1
- package/bin/sounding.js +156 -0
- package/index.js +23 -0
- package/lib/create-app-manager.js +380 -26
- package/lib/create-auth-helpers.js +168 -21
- package/lib/create-browser-manager.js +578 -31
- package/lib/create-error.js +35 -0
- package/lib/create-expect.js +1070 -27
- package/lib/create-helper-runner.js +38 -2
- package/lib/create-mail-capture.js +125 -16
- package/lib/create-mailbox.js +20 -0
- package/lib/create-request-client.js +635 -57
- package/lib/create-runtime.js +222 -21
- package/lib/create-socket-manager.js +706 -0
- package/lib/create-test-api.js +491 -102
- package/lib/create-visit-client.js +40 -2
- package/lib/create-world-engine.js +106 -7
- package/lib/create-world-loader.js +150 -8
- package/lib/default-config.js +25 -0
- package/lib/define-world.js +27 -2
- package/lib/init-project.js +403 -0
- package/lib/merge-config.js +11 -0
- package/lib/normalize-config.js +16 -19
- package/lib/resolve-auth-config.js +36 -0
- package/lib/resolve-datastore.js +50 -7
- package/lib/resolve-dependency.js +145 -0
- package/lib/test-runner.js +427 -0
- package/lib/trial-context.js +29 -0
- package/lib/types.js +675 -0
- package/lib/validate-config.js +633 -0
- package/lib/validate-test-args.js +480 -0
- package/package.json +16 -2
package/README.md
CHANGED
|
@@ -16,17 +16,63 @@ The canonical Sails-native surface is:
|
|
|
16
16
|
- `get('/api/issues')` or `sails.sounding.request.get('/api/issues')` inside endpoint-style trials
|
|
17
17
|
- `await auth.login.withPassword('creator@example.com', page, { password: 'secret123' })` inside browser trials
|
|
18
18
|
- `await auth.request.withPassword('creator@example.com', { password: 'secret123' })` inside request trials
|
|
19
|
+
- `test('...', { world: 'signed-in-user' }, async ({ request }) => {})` can auto-load named worlds before the handler runs
|
|
20
|
+
- `request.as('owner')` and `visit.as('owner')` can resolve actor aliases from the current world
|
|
21
|
+
- `test('...', { browser: 'mobile' }, async ({ page }) => {})` can select a named browser project without extra ceremony
|
|
22
|
+
- failed browser trials capture the current URL and a full-page screenshot under `.tmp/sounding/artifacts`
|
|
19
23
|
- request helpers default to Sails virtual requests powered by `sails.request()`
|
|
24
|
+
- virtual request responses expose the final `req.session` snapshot as `response.session`; HTTP responses leave it undefined
|
|
25
|
+
- request assertions can check auth/session state with `expect(response).toHaveSession('userId', user.id)` and flash messages with `expect(response).toHaveFlash('info', /welcome/i)`
|
|
26
|
+
- failed response assertions include concise request/response diagnostics; set `SOUNDING_DIAGNOSTICS=verbose` for full response excerpts
|
|
20
27
|
- Inertia-style visits can use `visit('/pricing')` and partial reload options like `{ component, only }`
|
|
28
|
+
- mail assertions can check captured emails with `expect(mailbox).toHaveSentMail({ to, subject })` and `expect(mailbox.latest()).toHaveCtaUrl(/magic-link/)`
|
|
21
29
|
- a trial can opt into stricter parity with `test('...', { transport: 'http' }, ...)`
|
|
30
|
+
- upload trials use `FormData` over the HTTP transport so Sails can exercise real Skipper streams
|
|
31
|
+
- independent trials can opt into concurrent execution with `test('...', { concurrent: true }, ...)` or `test.concurrent(...)`
|
|
22
32
|
- any trial can also scope a request client with `sails.sounding.request.using('http')`
|
|
23
33
|
|
|
24
34
|
Sounding also owns its own built-in world engine, so the same package can:
|
|
25
35
|
- define factories under `tests/factories`
|
|
26
36
|
- define scenarios under `tests/scenarios`
|
|
27
|
-
- load named worlds for endpoint and browser trials
|
|
37
|
+
- auto-load named worlds for endpoint, Inertia, socket, and browser trials
|
|
38
|
+
- compose world records with fluent builders like `await create('user').trait('admin').with({ email })`
|
|
39
|
+
- merge repeated builder `.with()` calls, with `.withOnly()` available when you want to use only the next overrides
|
|
28
40
|
- capture outgoing mail by wrapping `sails.helpers.mail.send` and storing normalized messages in `sails.sounding.mailbox`
|
|
29
41
|
|
|
42
|
+
Request-level Inertia trials can assert page contracts without launching a browser:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
const { test } = require('sounding')
|
|
46
|
+
|
|
47
|
+
test('dashboard shows the signed-in creator', { world: 'signed-in-user' }, async ({ visit, expect }) => {
|
|
48
|
+
const page = await visit.as('owner')('/dashboard')
|
|
49
|
+
|
|
50
|
+
expect(page).toBeInertiaPage('dashboard/index')
|
|
51
|
+
expect(page).toHaveInertiaProps({
|
|
52
|
+
'auth.user.email': 'owner@example.com',
|
|
53
|
+
'stats.projects': 2,
|
|
54
|
+
projects: [{ name: 'Launch Plan' }],
|
|
55
|
+
})
|
|
56
|
+
expect(page).toHaveNoInertiaErrors()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('dashboard partial reload returns only notifications', async ({ visit, expect }) => {
|
|
60
|
+
const page = await visit('/dashboard', {
|
|
61
|
+
component: 'dashboard/index',
|
|
62
|
+
only: ['notifications'],
|
|
63
|
+
reset: ['sidebar'],
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
expect(page).toBeInertiaPage('dashboard/index')
|
|
67
|
+
expect(page).toHaveInertiaPartialReload({
|
|
68
|
+
component: 'dashboard/index',
|
|
69
|
+
only: ['notifications'],
|
|
70
|
+
reset: ['sidebar'],
|
|
71
|
+
})
|
|
72
|
+
expect(page).toHaveOnlyInertiaProps(['notifications'])
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
30
76
|
The default configuration story is intentionally calm:
|
|
31
77
|
- Sounding only enables its hook in the environments listed under `sounding.environments`
|
|
32
78
|
- the default is `['test']`, so non-test boot paths stay dark unless you opt in explicitly
|
|
@@ -36,6 +82,8 @@ The default configuration story is intentionally calm:
|
|
|
36
82
|
- managed SQLite artifacts live under `.tmp/db`
|
|
37
83
|
- the default datastore identity is `default`
|
|
38
84
|
- browser projects start with `desktop`
|
|
85
|
+
- browser projects can be strings or named project objects with `type`, `device`, `viewport`, `contextOptions`, and `launchOptions`
|
|
86
|
+
- browser failure artifacts store screenshots and current URLs by default, while traces and videos are opt-in
|
|
39
87
|
- mail capture previews use the `mail` layout by default, matching the current `sails-hook-mail` convention
|
|
40
88
|
- apps with a different mail layout can set `sounding.mail.layout`, for example `layout-email`
|
|
41
89
|
- `inherit` remains available when an app already has a serious test datastore story
|
|
@@ -50,6 +98,291 @@ module.exports.sounding = {
|
|
|
50
98
|
|
|
51
99
|
If you intentionally want Sounding during another boot path, widen the list explicitly, for example `['test', 'console']` or `['test', 'production']`.
|
|
52
100
|
|
|
101
|
+
## Upload trials
|
|
102
|
+
|
|
103
|
+
Use HTTP request trials for Sails file uploads. Uploads in Sails are streaming
|
|
104
|
+
Skipper requests, so they need the HTTP stack that Sails uses for real multipart
|
|
105
|
+
forms.
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
const { test } = require('sounding')
|
|
109
|
+
|
|
110
|
+
test(
|
|
111
|
+
'creator can upload a receipt',
|
|
112
|
+
{ transport: 'http' },
|
|
113
|
+
async ({ request, expect }) => {
|
|
114
|
+
const form = new FormData()
|
|
115
|
+
|
|
116
|
+
form.append('description', 'Home office monitor')
|
|
117
|
+
form.append('amount', '1200')
|
|
118
|
+
form.append(
|
|
119
|
+
'receipt',
|
|
120
|
+
new Blob(['receipt bytes'], { type: 'application/pdf' }),
|
|
121
|
+
'receipt.pdf'
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
const response = await request.post('/expenses', form)
|
|
125
|
+
|
|
126
|
+
expect(response).toRedirectTo('/expenses/new')
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
When a multipart form mixes text fields and files, append the text fields before
|
|
132
|
+
the files. That matches Sails and Skipper's streaming model, where actions can
|
|
133
|
+
start while file streams are still arriving.
|
|
134
|
+
|
|
135
|
+
Do not use the virtual transport for upload behavior. Virtual requests are still
|
|
136
|
+
right for normal endpoints, JSON APIs, session assertions, redirects, and
|
|
137
|
+
Inertia contracts, but real `req.file()` uploads are HTTP-only in Sails.
|
|
138
|
+
|
|
139
|
+
## Project init
|
|
140
|
+
|
|
141
|
+
Use the initializer from a Sails app to add the first Sounding test lane:
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
npx sounding init
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
It 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:
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
npx sounding init --config
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Typical output looks like:
|
|
154
|
+
|
|
155
|
+
```txt
|
|
156
|
+
Sounding initialized /path/to/my-sails-app
|
|
157
|
+
Auth convention: User
|
|
158
|
+
~ Updated package.json (added `npm test`, added `sounding` devDependency, added `sails-sqlite` devDependency)
|
|
159
|
+
+ Created tests
|
|
160
|
+
+ Created tests/factories
|
|
161
|
+
+ Created tests/scenarios
|
|
162
|
+
+ Created tests/sounding
|
|
163
|
+
+ Created tests/factories/user.js
|
|
164
|
+
+ Created tests/scenarios/signed-in-user.js
|
|
165
|
+
+ Created tests/sounding/examples.test.js
|
|
166
|
+
- Skipped config/sounding.js because Sounding defaults are enough
|
|
167
|
+
|
|
168
|
+
Next: run npm install, then npm test.
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## CLI test runner
|
|
172
|
+
|
|
173
|
+
Run a Sounding suite with the framework-level runner:
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
npx sounding test
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The command discovers `.test.js` files under `tests/` and `test/`, then runs Node's native test runner with Sounding-friendly filters:
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
npx sounding test --grep "dashboard"
|
|
183
|
+
npx sounding test --file tests/sounding/examples.test.js
|
|
184
|
+
npx sounding test --lane browser
|
|
185
|
+
npx sounding test --watch
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Common Node test flags pass through, and CI reporters are available without memorizing the longer Node flag names:
|
|
189
|
+
|
|
190
|
+
```sh
|
|
191
|
+
npx sounding test --reporter spec
|
|
192
|
+
npx sounding test --junit reports/sounding-junit.xml
|
|
193
|
+
npx sounding test --json
|
|
194
|
+
npx sounding test --coverage
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Use `--dry-run` to inspect the exact `node --test` command before running it.
|
|
198
|
+
|
|
199
|
+
## App lifecycle
|
|
200
|
+
|
|
201
|
+
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.
|
|
202
|
+
|
|
203
|
+
The app manager makes those lanes explicit:
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
const { createAppManager } = require('sounding')
|
|
207
|
+
|
|
208
|
+
const manager = createAppManager()
|
|
209
|
+
|
|
210
|
+
const virtualRuntime = await manager.runtime({ app: 'load' })
|
|
211
|
+
const httpRuntime = await manager.runtime({ app: 'lift' })
|
|
212
|
+
const alsoHttpRuntime = await manager.runtime({ transport: 'http' })
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Use the warm default for most suites. When a trial mutates process-global app state and needs a fresh Sails instance, force a reload:
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
const freshRuntime = await manager.runtime({ app: 'load', reload: true })
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Lifecycle timings are available for diagnostics:
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
console.log(manager.lifecycle.load.durationMs)
|
|
225
|
+
console.log(manager.lifecycle.lift.status)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Set `SOUNDING_LIFECYCLE=verbose` or `SOUNDING_DIAGNOSTICS=verbose` to print app load/lift timing messages while the suite runs.
|
|
229
|
+
|
|
230
|
+
## Concurrent trials
|
|
231
|
+
|
|
232
|
+
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.
|
|
233
|
+
|
|
234
|
+
Independent trials can opt into Node test concurrency:
|
|
235
|
+
|
|
236
|
+
```js
|
|
237
|
+
test.concurrent('health check is isolated', async ({ get, expect }) => {
|
|
238
|
+
const response = await get('/health')
|
|
239
|
+
|
|
240
|
+
expect(response).toHaveStatus(200)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('dashboard contract is isolated too', { concurrent: true }, async ({ visit, expect }) => {
|
|
244
|
+
const page = await visit('/dashboard')
|
|
245
|
+
|
|
246
|
+
expect(page).toBeInertiaPage('dashboard/index')
|
|
247
|
+
})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
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.
|
|
251
|
+
|
|
252
|
+
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.
|
|
253
|
+
|
|
254
|
+
## Typing and editor support
|
|
255
|
+
|
|
256
|
+
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.
|
|
257
|
+
|
|
258
|
+
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:
|
|
259
|
+
|
|
260
|
+
```sh
|
|
261
|
+
npm run typecheck
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Sounding 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.
|
|
265
|
+
|
|
266
|
+
## Browser projects
|
|
267
|
+
|
|
268
|
+
Browser trials start on the `desktop` project:
|
|
269
|
+
|
|
270
|
+
```js
|
|
271
|
+
test('subscriber can read a gated issue', { browser: true }, async ({ page }) => {
|
|
272
|
+
await page.goto('/issues/the-nerve-to-build')
|
|
273
|
+
})
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Use a string when a trial needs a named project:
|
|
277
|
+
|
|
278
|
+
```js
|
|
279
|
+
test('mobile navigation opens the account menu', { browser: 'mobile' }, async ({ page }) => {
|
|
280
|
+
await page.goto('/dashboard')
|
|
281
|
+
})
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Configure named projects in `config/sounding.js` when an app needs mobile devices, WebKit, or custom context options:
|
|
285
|
+
|
|
286
|
+
```js
|
|
287
|
+
module.exports.sounding = {
|
|
288
|
+
browser: {
|
|
289
|
+
projects: {
|
|
290
|
+
desktop: {},
|
|
291
|
+
mobile: {
|
|
292
|
+
device: 'iPhone 13'
|
|
293
|
+
},
|
|
294
|
+
safari: {
|
|
295
|
+
type: 'webkit',
|
|
296
|
+
viewport: {
|
|
297
|
+
width: 1280,
|
|
298
|
+
height: 720
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The object form stays Sails-simple while still passing through to Playwright where it matters.
|
|
307
|
+
|
|
308
|
+
## Browser failure artifacts
|
|
309
|
+
|
|
310
|
+
Browser-capable trials should be easy to debug without turning every run into a heavyweight recording session.
|
|
311
|
+
|
|
312
|
+
By default, a failed `{ browser: true }` trial writes:
|
|
313
|
+
|
|
314
|
+
- `current-url.txt`
|
|
315
|
+
- `screenshot.png`
|
|
316
|
+
|
|
317
|
+
under a stable, readable directory:
|
|
318
|
+
|
|
319
|
+
```txt
|
|
320
|
+
.tmp/sounding/artifacts/<trial-name>/<browser-project>/
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
For a trial named `dashboard shows owner stats` on the default `desktop` project, that becomes:
|
|
324
|
+
|
|
325
|
+
```txt
|
|
326
|
+
.tmp/sounding/artifacts/dashboard-shows-owner-stats/desktop/
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
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.
|
|
330
|
+
|
|
331
|
+
Traces and videos are intentionally off by default because they cost more disk and time. Turn them on for a whole app:
|
|
332
|
+
|
|
333
|
+
```js
|
|
334
|
+
module.exports.sounding = {
|
|
335
|
+
browser: {
|
|
336
|
+
artifacts: {
|
|
337
|
+
trace: true,
|
|
338
|
+
video: true
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Or scope them to one suspicious trial:
|
|
345
|
+
|
|
346
|
+
```js
|
|
347
|
+
test(
|
|
348
|
+
'checkout keeps the cart after refresh',
|
|
349
|
+
{
|
|
350
|
+
browser: {
|
|
351
|
+
artifacts: {
|
|
352
|
+
trace: true,
|
|
353
|
+
video: true
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
async ({ page, expect }) => {
|
|
358
|
+
await page.goto('/checkout')
|
|
359
|
+
await page.reload()
|
|
360
|
+
|
|
361
|
+
await expect(page.getByText('Your cart')).toBeVisible()
|
|
362
|
+
}
|
|
363
|
+
)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Use `false` as a concise off switch:
|
|
367
|
+
|
|
368
|
+
```js
|
|
369
|
+
test('fast smoke flow', { browser: { artifacts: false } }, async ({ page }) => {
|
|
370
|
+
await page.goto('/health')
|
|
371
|
+
})
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
For artifact settings, `true` means “keep this when the trial fails.” If you need an artifact on successful browser trials too, use `on` instead:
|
|
375
|
+
|
|
376
|
+
```js
|
|
377
|
+
module.exports.sounding = {
|
|
378
|
+
browser: {
|
|
379
|
+
artifacts: {
|
|
380
|
+
trace: 'on'
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
53
386
|
This repository starts with docs-driven product research and the first hook/runtime scaffolding for that vision.
|
|
54
387
|
|
|
55
388
|
See `RESEARCH.md`.
|
package/bin/sounding.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { initProject } = require('../lib/init-project')
|
|
4
|
+
const { formatTestCommand, runTests } = require('../lib/test-runner')
|
|
5
|
+
|
|
6
|
+
function printHelp() {
|
|
7
|
+
process.stdout.write(`Sounding
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
sounding init [--app <path>] [--config]
|
|
11
|
+
sounding test [options] [files or folders]
|
|
12
|
+
|
|
13
|
+
Commands:
|
|
14
|
+
init Scaffold Sounding tests, worlds, and package scripts in a Sails app.
|
|
15
|
+
test Run Sounding trials through the Node.js test runner.
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--app Target app directory. Defaults to the current working directory.
|
|
19
|
+
--config Also create config/sounding.js when it does not already exist.
|
|
20
|
+
--help Show this help.
|
|
21
|
+
`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function printTestHelp() {
|
|
25
|
+
process.stdout.write(`Sounding test
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
sounding test [options] [files or folders]
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
--app <path> Target app directory.
|
|
32
|
+
--grep <pattern> Forward to --test-name-pattern.
|
|
33
|
+
--file <path> Run one file. May be repeated.
|
|
34
|
+
--lane <name> Run tests under tests/<name> or test/<name>.
|
|
35
|
+
--changed Run changed .test.js files when git metadata is available.
|
|
36
|
+
--watch Forward to Node watch mode.
|
|
37
|
+
--reporter <name> Forward to --test-reporter.
|
|
38
|
+
--reporter-destination <path> Forward to --test-reporter-destination.
|
|
39
|
+
--junit [path] Use the junit reporter.
|
|
40
|
+
--json Use the json reporter.
|
|
41
|
+
--coverage Enable Node test coverage.
|
|
42
|
+
--dry-run Print the Node command without running it.
|
|
43
|
+
|
|
44
|
+
Unknown --test-* and Node flags pass through to node.
|
|
45
|
+
`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseArgs(argv) {
|
|
49
|
+
const args = [...argv]
|
|
50
|
+
const options = {}
|
|
51
|
+
const command = args.shift()
|
|
52
|
+
|
|
53
|
+
if (command === '--help' || command === '-h') {
|
|
54
|
+
return {
|
|
55
|
+
command: null,
|
|
56
|
+
options: {
|
|
57
|
+
help: true,
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (command === 'test') {
|
|
63
|
+
return {
|
|
64
|
+
command,
|
|
65
|
+
options: {
|
|
66
|
+
argv: args,
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
while (args.length > 0) {
|
|
72
|
+
const arg = args.shift()
|
|
73
|
+
|
|
74
|
+
if (arg === '--help' || arg === '-h') {
|
|
75
|
+
options.help = true
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (arg === '--config') {
|
|
80
|
+
options.config = true
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (arg === '--app') {
|
|
85
|
+
options.appPath = args.shift()
|
|
86
|
+
if (!options.appPath) {
|
|
87
|
+
throw new Error('Sounding option `--app` requires a path.')
|
|
88
|
+
}
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw new Error(`Unknown Sounding option: ${arg}`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
command,
|
|
97
|
+
options,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function printInitResult(result) {
|
|
102
|
+
process.stdout.write(`Sounding initialized ${result.appPath}\n`)
|
|
103
|
+
process.stdout.write(
|
|
104
|
+
`Auth convention: ${result.auth.modelName}${result.auth.detected ? '' : ' (default)'}\n`
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
for (const action of result.actions) {
|
|
108
|
+
const marker = action.type === 'created' ? '+' : action.type === 'updated' ? '~' : '-'
|
|
109
|
+
process.stdout.write(`${marker} ${action.message}\n`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
process.stdout.write('\nNext: run npm install, then npm test.\n')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function main() {
|
|
116
|
+
const { command, options } = parseArgs(process.argv.slice(2))
|
|
117
|
+
|
|
118
|
+
if (!command || options.help) {
|
|
119
|
+
printHelp()
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (command === 'init') {
|
|
124
|
+
const result = initProject(options)
|
|
125
|
+
printInitResult(result)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (command === 'test') {
|
|
130
|
+
if (options.argv.includes('--help') || options.argv.includes('-h')) {
|
|
131
|
+
printTestHelp()
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const result = await runTests({
|
|
136
|
+
argv: options.argv,
|
|
137
|
+
stdio: 'inherit',
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
if (result.command.dryRun) {
|
|
141
|
+
process.stdout.write(`${formatTestCommand(result.command)}\n`)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
process.exitCode = result.status
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
{
|
|
149
|
+
throw new Error(`Unknown Sounding command: ${command}`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
main().catch((error) => {
|
|
154
|
+
process.stderr.write(`${error.message}\n`)
|
|
155
|
+
process.exitCode = 1
|
|
156
|
+
})
|
package/index.js
CHANGED
|
@@ -9,15 +9,27 @@ const { createHelperRunner } = require('./lib/create-helper-runner')
|
|
|
9
9
|
const { createRequestClient } = require('./lib/create-request-client')
|
|
10
10
|
const { createVisitClient } = require('./lib/create-visit-client')
|
|
11
11
|
const { createBrowserManager } = require('./lib/create-browser-manager')
|
|
12
|
+
const { createSocketManager } = require('./lib/create-socket-manager')
|
|
12
13
|
const { createAuthHelpers } = require('./lib/create-auth-helpers')
|
|
13
14
|
const { createExpect } = require('./lib/create-expect')
|
|
14
15
|
const { createTestApi } = require('./lib/create-test-api')
|
|
15
16
|
const { getDefaultConfig } = require('./lib/default-config')
|
|
16
17
|
|
|
18
|
+
/** @typedef {import('./lib/types').SoundingSailsApp} SoundingSailsApp */
|
|
19
|
+
/** @typedef {import('./lib/types').SoundingSailsHook} SoundingSailsHook */
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {SoundingSailsApp} sails
|
|
23
|
+
* @returns {string | undefined}
|
|
24
|
+
*/
|
|
17
25
|
function getCurrentEnvironment(sails) {
|
|
18
26
|
return sails.config?.environment || process.env.NODE_ENV
|
|
19
27
|
}
|
|
20
28
|
|
|
29
|
+
/**
|
|
30
|
+
* @param {SoundingSailsApp} sails
|
|
31
|
+
* @returns {string[]}
|
|
32
|
+
*/
|
|
21
33
|
function getEnabledEnvironments(sails) {
|
|
22
34
|
const configured = sails.config?.sounding?.environments
|
|
23
35
|
|
|
@@ -28,10 +40,20 @@ function getEnabledEnvironments(sails) {
|
|
|
28
40
|
return getDefaultConfig().environments
|
|
29
41
|
}
|
|
30
42
|
|
|
43
|
+
/**
|
|
44
|
+
* @param {SoundingSailsApp} sails
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
31
47
|
function shouldEnableHook(sails) {
|
|
32
48
|
return getEnabledEnvironments(sails).includes(getCurrentEnvironment(sails))
|
|
33
49
|
}
|
|
34
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Sails hook factory.
|
|
53
|
+
*
|
|
54
|
+
* @param {SoundingSailsApp} sails
|
|
55
|
+
* @returns {SoundingSailsHook}
|
|
56
|
+
*/
|
|
35
57
|
function soundingHook(sails) {
|
|
36
58
|
const runtime = createRuntime(sails)
|
|
37
59
|
|
|
@@ -79,6 +101,7 @@ module.exports.createHelperRunner = createHelperRunner
|
|
|
79
101
|
module.exports.createRequestClient = createRequestClient
|
|
80
102
|
module.exports.createVisitClient = createVisitClient
|
|
81
103
|
module.exports.createBrowserManager = createBrowserManager
|
|
104
|
+
module.exports.createSocketManager = createSocketManager
|
|
82
105
|
module.exports.createAuthHelpers = createAuthHelpers
|
|
83
106
|
module.exports.createExpect = createExpect
|
|
84
107
|
module.exports.createTestApi = createTestApi
|