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/lib/create-test-api.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
const nodeTest = require('node:test')
|
|
2
2
|
const { createAppManager } = require('./create-app-manager')
|
|
3
3
|
const { createExpect } = require('./create-expect')
|
|
4
|
+
const { createRuntime } = require('./create-runtime')
|
|
5
|
+
const { createSoundingError } = require('./create-error')
|
|
6
|
+
const { runWithTrialContext } = require('./trial-context')
|
|
7
|
+
const { normalizeTestArgs, splitTestOptions } = require('./validate-test-args')
|
|
8
|
+
|
|
9
|
+
/** @typedef {import('./types').SoundingRuntime} SoundingRuntime */
|
|
10
|
+
/** @typedef {import('./types').SoundingExpect} SoundingExpect */
|
|
11
|
+
/** @typedef {import('./types').SoundingBrowserArtifacts} SoundingBrowserArtifacts */
|
|
12
|
+
/** @typedef {import('./types').SoundingBrowserSession} SoundingBrowserSession */
|
|
13
|
+
/** @typedef {import('./types').SoundingTest} SoundingTest */
|
|
14
|
+
/** @typedef {import('./types').SoundingTestOptions} SoundingTestOptions */
|
|
15
|
+
/** @typedef {import('./types').SoundingTrialContext} SoundingTrialContext */
|
|
16
|
+
/** @typedef {import('./types').SoundingTrialHandler} SoundingTrialHandler */
|
|
17
|
+
/** @typedef {import('./types').SoundingTrialRegistrar} SoundingTrialRegistrar */
|
|
18
|
+
/** @typedef {Function & { skip?: Function, todo?: Function, only?: Function }} NodeTestLike */
|
|
4
19
|
|
|
5
20
|
let defaultAppManager = null
|
|
6
21
|
let defaultCleanupRegistered = false
|
|
@@ -25,71 +40,365 @@ function ensureDefaultAppManagerCleanup() {
|
|
|
25
40
|
defaultCleanupRegistered = true
|
|
26
41
|
}
|
|
27
42
|
|
|
28
|
-
|
|
29
|
-
|
|
43
|
+
/**
|
|
44
|
+
* @param {{ requiresHttp?: boolean, browser?: boolean, socket?: boolean }} [options]
|
|
45
|
+
* @returns {Promise<{ sounding: SoundingRuntime, teardown(): Promise<void> }>}
|
|
46
|
+
*/
|
|
47
|
+
async function resolveRuntimeFromGlobals(options = {}) {
|
|
48
|
+
const runtime = globalThis.sounding || globalThis.sails?.sounding || globalThis.sails?.hooks?.sounding
|
|
49
|
+
const requiresHttp = Boolean(options.requiresHttp || options.browser || options.socket)
|
|
50
|
+
const httpServer = globalThis.sails?.hooks?.http?.server
|
|
51
|
+
const hasHttpServer = Boolean(
|
|
52
|
+
httpServer &&
|
|
53
|
+
(httpServer.listening ||
|
|
54
|
+
(typeof httpServer.address === 'function' && httpServer.address()))
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if (runtime && (!requiresHttp || hasHttpServer)) {
|
|
30
58
|
return {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
handler: optionsOrHandler,
|
|
59
|
+
sounding: runtime,
|
|
60
|
+
teardown: async () => runtime.lower(),
|
|
34
61
|
}
|
|
35
62
|
}
|
|
36
63
|
|
|
64
|
+
const appManager = getDefaultAppManager()
|
|
65
|
+
ensureDefaultAppManagerCleanup()
|
|
66
|
+
const sounding = await appManager.runtime({ app: requiresHttp ? 'lift' : 'load' })
|
|
37
67
|
return {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
handler: maybeHandler,
|
|
68
|
+
sounding,
|
|
69
|
+
teardown: async () => sounding.lower(),
|
|
41
70
|
}
|
|
42
71
|
}
|
|
43
72
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
73
|
+
/**
|
|
74
|
+
* @param {{ requiresHttp?: boolean, browser?: boolean, socket?: boolean }} [options]
|
|
75
|
+
* @returns {Promise<{ sounding: SoundingRuntime, teardown(): Promise<void> }>}
|
|
76
|
+
*/
|
|
77
|
+
async function resolveIsolatedRuntimeFromGlobals(options = {}) {
|
|
78
|
+
const requiresHttp = Boolean(options.requiresHttp || options.browser || options.socket)
|
|
47
79
|
const httpServer = globalThis.sails?.hooks?.http?.server
|
|
48
80
|
const hasHttpServer = Boolean(
|
|
49
81
|
httpServer &&
|
|
50
82
|
(httpServer.listening ||
|
|
51
83
|
(typeof httpServer.address === 'function' && httpServer.address()))
|
|
52
84
|
)
|
|
85
|
+
let sails = null
|
|
86
|
+
|
|
87
|
+
if (globalThis.sails && (!requiresHttp || hasHttpServer)) {
|
|
88
|
+
sails = globalThis.sails
|
|
89
|
+
} else {
|
|
90
|
+
const appManager = getDefaultAppManager()
|
|
91
|
+
ensureDefaultAppManagerCleanup()
|
|
92
|
+
sails = requiresHttp ? await appManager.lift() : await appManager.load()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const sounding = createRuntime(sails)
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
sounding,
|
|
99
|
+
teardown: async () => sounding.lower(),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {SoundingRuntime | (() => SoundingRuntime | Promise<SoundingRuntime>) | undefined} runtime
|
|
105
|
+
* @param {SoundingTestOptions} options
|
|
106
|
+
* @returns {Promise<{ sounding: SoundingRuntime, teardown(): Promise<void>, isolated: boolean }>}
|
|
107
|
+
*/
|
|
108
|
+
async function resolveTrialRuntime(runtime, options = {}) {
|
|
109
|
+
const requires = {
|
|
110
|
+
requiresHttp: options.transport === 'http',
|
|
111
|
+
browser: Boolean(options.browser),
|
|
112
|
+
socket: Boolean(options.socket),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (typeof runtime === 'function') {
|
|
116
|
+
const sounding = await runtime()
|
|
117
|
+
return {
|
|
118
|
+
sounding,
|
|
119
|
+
teardown: async () => sounding.lower(),
|
|
120
|
+
isolated: Boolean(options.concurrent),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (runtime) {
|
|
125
|
+
if (options.concurrent) {
|
|
126
|
+
throw createSoundingError({
|
|
127
|
+
code: 'E_SOUNDING_CONCURRENT_RUNTIME_SHARED',
|
|
128
|
+
name: 'SoundingConcurrencyError',
|
|
129
|
+
message:
|
|
130
|
+
'Sounding concurrent trials need isolated runtime state. Pass a runtime factory to createTestApi({ runtime: () => createRuntime(sails) }) or use the default app manager.',
|
|
131
|
+
details: {
|
|
132
|
+
suggestion:
|
|
133
|
+
'Use `concurrent: true` only when each trial receives its own Sounding runtime.',
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
}
|
|
53
137
|
|
|
54
|
-
if (runtime && (!requiresHttp || hasHttpServer)) {
|
|
55
138
|
return {
|
|
56
139
|
sounding: runtime,
|
|
57
140
|
teardown: async () => runtime.lower(),
|
|
141
|
+
isolated: false,
|
|
58
142
|
}
|
|
59
143
|
}
|
|
60
144
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
145
|
+
const resolved = options.concurrent
|
|
146
|
+
? await resolveIsolatedRuntimeFromGlobals(requires)
|
|
147
|
+
: await resolveRuntimeFromGlobals(requires)
|
|
148
|
+
|
|
64
149
|
return {
|
|
150
|
+
...resolved,
|
|
151
|
+
isolated: Boolean(options.concurrent),
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {Record<string, any>} sails
|
|
157
|
+
* @param {SoundingRuntime} sounding
|
|
158
|
+
* @param {{ isolated?: boolean }} [options]
|
|
159
|
+
* @returns {Record<string, any>}
|
|
160
|
+
*/
|
|
161
|
+
function createTrialSails(sails, sounding, options = {}) {
|
|
162
|
+
if (!options.isolated) {
|
|
163
|
+
sails.sounding ||= sounding
|
|
164
|
+
sails.hooks ||= {}
|
|
165
|
+
sails.hooks.sounding ||= sounding
|
|
166
|
+
sails.helpers ||= sounding.helpers
|
|
167
|
+
return sails
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const hooks = {
|
|
171
|
+
...(sails.hooks || {}),
|
|
65
172
|
sounding,
|
|
66
|
-
teardown: async () => sounding.lower(),
|
|
67
173
|
}
|
|
174
|
+
|
|
175
|
+
return new Proxy(sails, {
|
|
176
|
+
get(target, property, receiver) {
|
|
177
|
+
if (property === 'sounding') {
|
|
178
|
+
return sounding
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (property === 'hooks') {
|
|
182
|
+
return hooks
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (property === 'helpers') {
|
|
186
|
+
return target.helpers || sounding.helpers
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return Reflect.get(target, property, receiver)
|
|
190
|
+
},
|
|
191
|
+
set(target, property, value, receiver) {
|
|
192
|
+
if (property === 'sounding') {
|
|
193
|
+
return true
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (property === 'hooks') {
|
|
197
|
+
Object.assign(hooks, value || {})
|
|
198
|
+
hooks.sounding = sounding
|
|
199
|
+
return true
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return Reflect.set(target, property, value, receiver)
|
|
203
|
+
},
|
|
204
|
+
})
|
|
68
205
|
}
|
|
69
206
|
|
|
207
|
+
/**
|
|
208
|
+
* @param {import('./types').SoundingRequestClient} request
|
|
209
|
+
* @param {'get' | 'head' | 'post' | 'put' | 'patch' | 'delete'} method
|
|
210
|
+
* @returns {Function | undefined}
|
|
211
|
+
*/
|
|
70
212
|
function bindRequestMethod(request, method) {
|
|
71
213
|
return typeof request?.[method] === 'function' ? request[method].bind(request) : undefined
|
|
72
214
|
}
|
|
73
215
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
216
|
+
/**
|
|
217
|
+
* @param {SoundingTestOptions['world']} worldOption
|
|
218
|
+
* @returns {{ name: string, context: Record<string, any> } | null}
|
|
219
|
+
*/
|
|
220
|
+
function normalizeWorldOption(worldOption) {
|
|
221
|
+
if (worldOption === undefined) {
|
|
222
|
+
return null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (typeof worldOption === 'string') {
|
|
226
|
+
return {
|
|
227
|
+
name: worldOption.trim(),
|
|
228
|
+
context: {},
|
|
229
|
+
}
|
|
230
|
+
}
|
|
80
231
|
|
|
81
232
|
return {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
...nodeOptions,
|
|
85
|
-
},
|
|
86
|
-
trialOptions: {
|
|
87
|
-
transport,
|
|
88
|
-
browser,
|
|
89
|
-
},
|
|
233
|
+
name: worldOption.name.trim(),
|
|
234
|
+
context: worldOption.context || {},
|
|
90
235
|
}
|
|
91
236
|
}
|
|
92
237
|
|
|
238
|
+
/**
|
|
239
|
+
* @param {SoundingTestOptions['browser']} browserOption
|
|
240
|
+
* @param {string | undefined} title
|
|
241
|
+
* @returns {import('./types').SoundingBrowserOpenOptions}
|
|
242
|
+
*/
|
|
243
|
+
function normalizeBrowserOpenOptions(browserOption, title) {
|
|
244
|
+
if (browserOption === true) {
|
|
245
|
+
return {
|
|
246
|
+
trialName: title,
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (typeof browserOption === 'string') {
|
|
251
|
+
return {
|
|
252
|
+
project: browserOption.trim(),
|
|
253
|
+
trialName: title,
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
...(browserOption || {}),
|
|
259
|
+
trialName: title,
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* @param {unknown} error
|
|
265
|
+
* @returns {string}
|
|
266
|
+
*/
|
|
267
|
+
function formatUnknownError(error) {
|
|
268
|
+
if (error instanceof Error) {
|
|
269
|
+
return error.message
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return String(error)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* @param {SoundingBrowserArtifacts | null | undefined} artifacts
|
|
277
|
+
* @returns {boolean}
|
|
278
|
+
*/
|
|
279
|
+
function hasBrowserArtifacts(artifacts) {
|
|
280
|
+
return Boolean(
|
|
281
|
+
artifacts &&
|
|
282
|
+
(artifacts.currentUrl ||
|
|
283
|
+
artifacts.currentUrlPath ||
|
|
284
|
+
artifacts.screenshot ||
|
|
285
|
+
artifacts.trace ||
|
|
286
|
+
artifacts.video ||
|
|
287
|
+
artifacts.errors?.length)
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* @param {SoundingBrowserArtifacts} artifacts
|
|
293
|
+
* @returns {string}
|
|
294
|
+
*/
|
|
295
|
+
function formatBrowserArtifacts(artifacts) {
|
|
296
|
+
const lines = ['Sounding browser artifacts:']
|
|
297
|
+
|
|
298
|
+
if (artifacts.currentUrl) {
|
|
299
|
+
lines.push(`- URL: ${artifacts.currentUrl}`)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (artifacts.currentUrlPath) {
|
|
303
|
+
lines.push(`- current URL file: ${artifacts.currentUrlPath}`)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (artifacts.screenshot) {
|
|
307
|
+
lines.push(`- screenshot: ${artifacts.screenshot}`)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (artifacts.trace) {
|
|
311
|
+
lines.push(`- trace: ${artifacts.trace}`)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (artifacts.video) {
|
|
315
|
+
lines.push(`- video: ${artifacts.video}`)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for (const captureError of artifacts.errors || []) {
|
|
319
|
+
lines.push(`- ${captureError.artifact} capture failed: ${captureError.message}`)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return lines.join('\n')
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* @param {SoundingBrowserSession | null} browserSession
|
|
327
|
+
* @returns {Promise<SoundingBrowserArtifacts | null>}
|
|
328
|
+
*/
|
|
329
|
+
async function captureBrowserFailureArtifacts(browserSession) {
|
|
330
|
+
if (typeof browserSession?.captureFailureArtifacts !== 'function') {
|
|
331
|
+
return null
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
return await browserSession.captureFailureArtifacts()
|
|
336
|
+
} catch (captureError) {
|
|
337
|
+
return {
|
|
338
|
+
outputDir: '',
|
|
339
|
+
directory: '',
|
|
340
|
+
project: browserSession.project,
|
|
341
|
+
errors: [
|
|
342
|
+
{
|
|
343
|
+
artifact: 'browser',
|
|
344
|
+
message: formatUnknownError(captureError),
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* @param {unknown} error
|
|
353
|
+
* @param {{ world?: { name: string, context: Record<string, any> }, browserArtifacts?: SoundingBrowserArtifacts | null }} metadata
|
|
354
|
+
* @returns {unknown}
|
|
355
|
+
*/
|
|
356
|
+
function decorateTrialError(error, metadata) {
|
|
357
|
+
const browserArtifacts = hasBrowserArtifacts(metadata.browserArtifacts)
|
|
358
|
+
? metadata.browserArtifacts
|
|
359
|
+
: null
|
|
360
|
+
|
|
361
|
+
if ((!metadata.world && !browserArtifacts) || !error || typeof error !== 'object') {
|
|
362
|
+
return error
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const target = /** @type {Record<string, any>} */ (error)
|
|
366
|
+
const existingSounding =
|
|
367
|
+
target.sounding && typeof target.sounding === 'object' ? target.sounding : {}
|
|
368
|
+
const existingDetails =
|
|
369
|
+
target.details && typeof target.details === 'object' ? target.details : null
|
|
370
|
+
|
|
371
|
+
target.sounding = {
|
|
372
|
+
...existingSounding,
|
|
373
|
+
...(metadata.world ? { world: metadata.world } : {}),
|
|
374
|
+
...(browserArtifacts ? { browserArtifacts } : {}),
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (existingDetails) {
|
|
378
|
+
target.details = {
|
|
379
|
+
...existingDetails,
|
|
380
|
+
...(metadata.world
|
|
381
|
+
? {
|
|
382
|
+
world: metadata.world.name,
|
|
383
|
+
worldContext: metadata.world.context,
|
|
384
|
+
}
|
|
385
|
+
: {}),
|
|
386
|
+
...(browserArtifacts ? { browserArtifacts } : {}),
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (browserArtifacts && typeof target.message === 'string') {
|
|
391
|
+
target.message = `${target.message}\n\n${formatBrowserArtifacts(browserArtifacts)}`
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return error
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* @template T
|
|
399
|
+
* @param {() => Promise<T>} handler
|
|
400
|
+
* @returns {Promise<T>}
|
|
401
|
+
*/
|
|
93
402
|
async function runInTrialQueue(handler) {
|
|
94
403
|
const previous = trialQueue
|
|
95
404
|
let release = () => {}
|
|
@@ -107,77 +416,148 @@ async function runInTrialQueue(handler) {
|
|
|
107
416
|
}
|
|
108
417
|
}
|
|
109
418
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
419
|
+
/**
|
|
420
|
+
* @param {{
|
|
421
|
+
* runtime?: SoundingRuntime | (() => SoundingRuntime | Promise<SoundingRuntime>),
|
|
422
|
+
* mode: string,
|
|
423
|
+
* title?: string,
|
|
424
|
+
* nodeContext: Record<string, any>,
|
|
425
|
+
* handler: SoundingTrialHandler,
|
|
426
|
+
* options?: SoundingTestOptions,
|
|
427
|
+
* }} args
|
|
428
|
+
* @returns {Promise<any>}
|
|
429
|
+
*/
|
|
430
|
+
async function runTrial({ runtime, mode, title, nodeContext, handler, options = {} }) {
|
|
431
|
+
const resolved = await resolveTrialRuntime(runtime, options)
|
|
121
432
|
const sounding = resolved.sounding
|
|
122
|
-
const booted = await sounding.boot({ mode })
|
|
123
|
-
const sails = booted.sails || {}
|
|
124
|
-
|
|
125
|
-
sails.sounding ||= sounding
|
|
126
|
-
sails.hooks ||= {}
|
|
127
|
-
sails.hooks.sounding ||= sounding
|
|
128
|
-
sails.helpers ||= sounding.helpers
|
|
129
|
-
|
|
130
|
-
const request = options.transport ? sounding.request.using(options.transport) : sounding.request
|
|
131
|
-
const visit = options.transport ? sounding.visit.using(options.transport) : sounding.visit
|
|
132
|
-
|
|
133
|
-
let browserSession = null
|
|
134
|
-
if (options.browser) {
|
|
135
|
-
browserSession = await sounding.browser.open(options.browser === true ? {} : options.browser)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const expect = browserSession?.expect
|
|
139
|
-
? createExpect.withFallback(browserSession.expect)
|
|
140
|
-
: createExpect
|
|
141
|
-
|
|
142
|
-
const context = {
|
|
143
|
-
...nodeContext,
|
|
144
|
-
t: nodeContext,
|
|
145
|
-
expect,
|
|
146
|
-
sails,
|
|
147
|
-
request,
|
|
148
|
-
visit,
|
|
149
|
-
auth: sounding.auth,
|
|
150
|
-
login: sounding.auth?.login,
|
|
151
|
-
world: sounding.world,
|
|
152
|
-
mailbox: sounding.mailbox,
|
|
153
|
-
browser: browserSession?.browser,
|
|
154
|
-
browserContext: browserSession?.context,
|
|
155
|
-
page: browserSession?.page,
|
|
156
|
-
get: bindRequestMethod(request, 'get'),
|
|
157
|
-
head: bindRequestMethod(request, 'head'),
|
|
158
|
-
post: bindRequestMethod(request, 'post'),
|
|
159
|
-
put: bindRequestMethod(request, 'put'),
|
|
160
|
-
patch: bindRequestMethod(request, 'patch'),
|
|
161
|
-
del: bindRequestMethod(request, 'delete'),
|
|
162
|
-
}
|
|
163
433
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
434
|
+
return runWithTrialContext(
|
|
435
|
+
{
|
|
436
|
+
runtime: sounding,
|
|
437
|
+
mailbox: sounding.mailbox,
|
|
438
|
+
getConfig: () => sounding.config,
|
|
439
|
+
},
|
|
440
|
+
async () => {
|
|
441
|
+
const booted = await sounding.boot({ mode })
|
|
442
|
+
const sails = createTrialSails(booted.sails || {}, sounding, {
|
|
443
|
+
isolated: resolved.isolated,
|
|
444
|
+
})
|
|
445
|
+
const worldOption = normalizeWorldOption(options.world)
|
|
446
|
+
const trialMetadata = {
|
|
447
|
+
...(worldOption ? { world: worldOption } : {}),
|
|
448
|
+
}
|
|
449
|
+
let browserSession = null
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
if (worldOption) {
|
|
453
|
+
await sounding.world.use(worldOption.name, worldOption.context)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const request = options.transport ? sounding.request.using(options.transport) : sounding.request
|
|
457
|
+
const visit = options.transport ? sounding.visit.using(options.transport) : sounding.visit
|
|
458
|
+
const socketOptions =
|
|
459
|
+
options.socket && typeof options.socket === 'object' ? options.socket : {}
|
|
460
|
+
const sockets =
|
|
461
|
+
options.socket && typeof options.socket === 'object'
|
|
462
|
+
? {
|
|
463
|
+
connect(connectOptions = {}) {
|
|
464
|
+
return sounding.sockets.connect({
|
|
465
|
+
...socketOptions,
|
|
466
|
+
...connectOptions,
|
|
467
|
+
})
|
|
468
|
+
},
|
|
469
|
+
as(actor) {
|
|
470
|
+
return {
|
|
471
|
+
connect(connectOptions = {}) {
|
|
472
|
+
return sounding.sockets.as(actor).connect({
|
|
473
|
+
...socketOptions,
|
|
474
|
+
...connectOptions,
|
|
475
|
+
})
|
|
476
|
+
},
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
closeAll() {
|
|
480
|
+
return sounding.sockets.closeAll()
|
|
481
|
+
},
|
|
482
|
+
}
|
|
483
|
+
: sounding.sockets
|
|
484
|
+
|
|
485
|
+
if (options.browser) {
|
|
486
|
+
browserSession = await sounding.browser.open(
|
|
487
|
+
normalizeBrowserOpenOptions(options.browser, title)
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** @type {SoundingExpect} */
|
|
492
|
+
const expect = browserSession?.expect
|
|
493
|
+
? createExpect.withFallback(browserSession.expect)
|
|
494
|
+
: /** @type {SoundingExpect} */ (createExpect)
|
|
495
|
+
|
|
496
|
+
/** @type {SoundingTrialContext} */
|
|
497
|
+
const context = {
|
|
498
|
+
...nodeContext,
|
|
499
|
+
t: nodeContext,
|
|
500
|
+
expect,
|
|
501
|
+
sails,
|
|
502
|
+
request,
|
|
503
|
+
visit,
|
|
504
|
+
sockets,
|
|
505
|
+
auth: sounding.auth,
|
|
506
|
+
login: sounding.auth?.login,
|
|
507
|
+
world: sounding.world,
|
|
508
|
+
mailbox: sounding.mailbox,
|
|
509
|
+
browser: browserSession?.browser,
|
|
510
|
+
browserContext: browserSession?.context,
|
|
511
|
+
page: browserSession?.page,
|
|
512
|
+
get: /** @type {any} */ (bindRequestMethod(request, 'get')),
|
|
513
|
+
head: /** @type {any} */ (bindRequestMethod(request, 'head')),
|
|
514
|
+
post: /** @type {any} */ (bindRequestMethod(request, 'post')),
|
|
515
|
+
put: /** @type {any} */ (bindRequestMethod(request, 'put')),
|
|
516
|
+
patch: /** @type {any} */ (bindRequestMethod(request, 'patch')),
|
|
517
|
+
del: /** @type {any} */ (bindRequestMethod(request, 'delete')),
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return await handler(context)
|
|
521
|
+
} catch (error) {
|
|
522
|
+
const browserArtifacts = await captureBrowserFailureArtifacts(browserSession)
|
|
523
|
+
|
|
524
|
+
throw decorateTrialError(error, {
|
|
525
|
+
...trialMetadata,
|
|
526
|
+
browserArtifacts,
|
|
527
|
+
})
|
|
528
|
+
} finally {
|
|
529
|
+
await resolved.teardown()
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
)
|
|
169
533
|
}
|
|
170
534
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
535
|
+
/**
|
|
536
|
+
* @param {NodeTestLike} baseTest
|
|
537
|
+
* @param {SoundingRuntime | (() => SoundingRuntime | Promise<SoundingRuntime>) | undefined} runtime
|
|
538
|
+
* @param {string} mode
|
|
539
|
+
* @param {boolean} [forceConcurrent]
|
|
540
|
+
* @returns {SoundingTrialRegistrar}
|
|
541
|
+
*/
|
|
542
|
+
function createTrialMethod(baseTest, runtime, mode, apiName = 'test', forceConcurrent = false) {
|
|
543
|
+
const registerTrial = function registerTrial(title, optionsOrHandler, maybeHandler) {
|
|
544
|
+
const { options, handler } = normalizeTestArgs(
|
|
545
|
+
title,
|
|
546
|
+
optionsOrHandler,
|
|
547
|
+
maybeHandler,
|
|
548
|
+
apiName
|
|
549
|
+
)
|
|
550
|
+
const nextOptions = forceConcurrent ? { ...options, concurrent: true } : options
|
|
551
|
+
const { nodeOptions, trialOptions } = splitTestOptions(nextOptions, apiName)
|
|
175
552
|
|
|
176
553
|
return baseTest(title, nodeOptions, async (nodeContext) => {
|
|
177
|
-
|
|
554
|
+
const run = trialOptions.concurrent ? (action) => action() : runInTrialQueue
|
|
555
|
+
|
|
556
|
+
return run(async () => {
|
|
178
557
|
return runTrial({
|
|
179
558
|
runtime,
|
|
180
559
|
mode,
|
|
560
|
+
title,
|
|
181
561
|
nodeContext,
|
|
182
562
|
handler,
|
|
183
563
|
options: trialOptions,
|
|
@@ -185,18 +565,29 @@ function createTrialMethod(baseTest, runtime, mode) {
|
|
|
185
565
|
})
|
|
186
566
|
})
|
|
187
567
|
}
|
|
568
|
+
|
|
569
|
+
return /** @type {SoundingTrialRegistrar} */ (registerTrial)
|
|
188
570
|
}
|
|
189
571
|
|
|
572
|
+
/**
|
|
573
|
+
* Create Sounding's `test()` API.
|
|
574
|
+
*
|
|
575
|
+
* @param {{ baseTest?: NodeTestLike, runtime?: SoundingRuntime | (() => SoundingRuntime | Promise<SoundingRuntime>) }} [options]
|
|
576
|
+
* @returns {SoundingTest}
|
|
577
|
+
*/
|
|
190
578
|
function createTestApi({ baseTest = nodeTest, runtime } = {}) {
|
|
191
579
|
function soundingTest(title, optionsOrHandler, maybeHandler) {
|
|
192
|
-
const { options, handler } = normalizeTestArgs(title, optionsOrHandler, maybeHandler)
|
|
193
|
-
const { nodeOptions, trialOptions } = splitTestOptions(options)
|
|
580
|
+
const { options, handler } = normalizeTestArgs(title, optionsOrHandler, maybeHandler, 'test')
|
|
581
|
+
const { nodeOptions, trialOptions } = splitTestOptions(options, 'test')
|
|
194
582
|
|
|
195
583
|
return baseTest(title, nodeOptions, async (nodeContext) => {
|
|
196
|
-
|
|
584
|
+
const run = trialOptions.concurrent ? (action) => action() : runInTrialQueue
|
|
585
|
+
|
|
586
|
+
return run(async () => {
|
|
197
587
|
return runTrial({
|
|
198
588
|
runtime,
|
|
199
589
|
mode: 'trial',
|
|
590
|
+
title,
|
|
200
591
|
nodeContext,
|
|
201
592
|
handler,
|
|
202
593
|
options: trialOptions,
|
|
@@ -205,21 +596,19 @@ function createTestApi({ baseTest = nodeTest, runtime } = {}) {
|
|
|
205
596
|
})
|
|
206
597
|
}
|
|
207
598
|
|
|
208
|
-
soundingTest.skip = (...args) => baseTest.skip(...args)
|
|
209
|
-
soundingTest.todo = (...args) => baseTest.todo(...args)
|
|
599
|
+
soundingTest.skip = (...args) => baseTest.skip?.(...args)
|
|
600
|
+
soundingTest.todo = (...args) => baseTest.todo?.(...args)
|
|
210
601
|
if (typeof baseTest.only === 'function') {
|
|
211
|
-
soundingTest.only = (
|
|
602
|
+
soundingTest.only = createTrialMethod(baseTest.only.bind(baseTest), runtime, 'trial', 'test.only')
|
|
212
603
|
}
|
|
213
|
-
|
|
214
|
-
// Temporary compatibility aliases while the public docs move fully to `test()`.
|
|
215
|
-
soundingTest.helper = createTrialMethod(baseTest, runtime, 'helper')
|
|
216
|
-
soundingTest.endpoint = createTrialMethod(baseTest, runtime, 'endpoint')
|
|
604
|
+
soundingTest.concurrent = createTrialMethod(baseTest, runtime, 'trial', 'test.concurrent', true)
|
|
217
605
|
|
|
218
606
|
return soundingTest
|
|
219
607
|
}
|
|
220
608
|
|
|
221
609
|
module.exports = {
|
|
222
610
|
createTestApi,
|
|
611
|
+
normalizeBrowserOpenOptions,
|
|
223
612
|
normalizeTestArgs,
|
|
224
613
|
resolveRuntimeFromGlobals,
|
|
225
614
|
runInTrialQueue,
|