sounding 0.0.3 → 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 +336 -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 +174 -25
- 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 +26 -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
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
const { createSoundingError } = require('./create-error')
|
|
2
|
+
|
|
3
|
+
/** @typedef {import('./types').SoundingTestOptions} SoundingTestOptions */
|
|
4
|
+
/** @typedef {import('./types').SoundingTrialHandler} SoundingTrialHandler */
|
|
5
|
+
|
|
6
|
+
const ALLOWED_TRANSPORTS = ['virtual', 'http']
|
|
7
|
+
const ALLOWED_BROWSER_ARTIFACT_KEYS = ['outputDir', 'screenshot', 'trace', 'video', 'currentUrl']
|
|
8
|
+
const ALLOWED_BROWSER_ARTIFACT_MODES = ['off', 'on', 'on-failure']
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {any} value
|
|
12
|
+
* @returns {value is Record<string, any>}
|
|
13
|
+
*/
|
|
14
|
+
function isPlainObject(value) {
|
|
15
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} apiName
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
function formatTrialSignature(apiName) {
|
|
23
|
+
return `${apiName}(name, [options], handler)`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {{
|
|
28
|
+
* code: string,
|
|
29
|
+
* message: string,
|
|
30
|
+
* apiName: string,
|
|
31
|
+
* value?: any,
|
|
32
|
+
* path?: string,
|
|
33
|
+
* allowed?: string[],
|
|
34
|
+
* suggestion?: string,
|
|
35
|
+
* }} input
|
|
36
|
+
* @returns {Error}
|
|
37
|
+
*/
|
|
38
|
+
function createTestArgumentError(input) {
|
|
39
|
+
const { code, message, apiName, value, path, allowed, suggestion } = input
|
|
40
|
+
/** @type {Record<string, any>} */
|
|
41
|
+
const details = {
|
|
42
|
+
api: apiName,
|
|
43
|
+
signature: formatTrialSignature(apiName),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (Object.prototype.hasOwnProperty.call(input, 'value')) {
|
|
47
|
+
details.value = value
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (path) {
|
|
51
|
+
details.path = path
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (allowed) {
|
|
55
|
+
details.allowed = allowed
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (suggestion) {
|
|
59
|
+
details.suggestion = suggestion
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return createSoundingError({
|
|
63
|
+
code,
|
|
64
|
+
name: 'SoundingTestArgumentError',
|
|
65
|
+
message,
|
|
66
|
+
details,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {any} title
|
|
72
|
+
* @param {string} apiName
|
|
73
|
+
*/
|
|
74
|
+
function assertTrialTitle(title, apiName) {
|
|
75
|
+
if (typeof title === 'string' && title.trim()) {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw createTestArgumentError({
|
|
80
|
+
code: 'E_SOUNDING_TEST_TITLE_REQUIRED',
|
|
81
|
+
message: `Sounding ${apiName} requires a non-empty trial name. Use \`${formatTrialSignature(apiName)}\`.`,
|
|
82
|
+
apiName,
|
|
83
|
+
value: title,
|
|
84
|
+
path: 'name',
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {any} handler
|
|
90
|
+
* @param {string} apiName
|
|
91
|
+
*/
|
|
92
|
+
function assertTrialHandler(handler, apiName) {
|
|
93
|
+
if (typeof handler === 'function') {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
throw createTestArgumentError({
|
|
98
|
+
code: 'E_SOUNDING_TEST_HANDLER_REQUIRED',
|
|
99
|
+
message: `Sounding ${apiName} requires a trial handler. Use \`${formatTrialSignature(apiName)}\`.`,
|
|
100
|
+
apiName,
|
|
101
|
+
value: handler,
|
|
102
|
+
path: 'handler',
|
|
103
|
+
suggestion: `Pass an async function as the final argument: \`${formatTrialSignature(apiName)}\`.`,
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {any} options
|
|
109
|
+
* @param {string} apiName
|
|
110
|
+
*/
|
|
111
|
+
function assertBrowserOptions(options, apiName) {
|
|
112
|
+
if (
|
|
113
|
+
options.browser !== undefined &&
|
|
114
|
+
typeof options.browser !== 'boolean' &&
|
|
115
|
+
typeof options.browser !== 'string' &&
|
|
116
|
+
!isPlainObject(options.browser)
|
|
117
|
+
) {
|
|
118
|
+
throw createTestArgumentError({
|
|
119
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
120
|
+
message: `Sounding ${apiName} option \`browser\` must be a boolean, project name, or browser options object.`,
|
|
121
|
+
apiName,
|
|
122
|
+
value: options.browser,
|
|
123
|
+
path: 'options.browser',
|
|
124
|
+
suggestion: 'Use `browser: true`, `browser: "mobile"`, or `browser: { project: "desktop" }`.',
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (typeof options.browser === 'string') {
|
|
129
|
+
if (options.browser.trim()) {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
throw createTestArgumentError({
|
|
134
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
135
|
+
message: `Sounding ${apiName} option \`browser\` must name a browser project when provided as a string.`,
|
|
136
|
+
apiName,
|
|
137
|
+
value: options.browser,
|
|
138
|
+
path: 'options.browser',
|
|
139
|
+
suggestion: 'Use `browser: "mobile"` or `browser: { project: "mobile" }`.',
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!isPlainObject(options.browser)) {
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (options.browser.type !== undefined && typeof options.browser.type !== 'string') {
|
|
148
|
+
throw createTestArgumentError({
|
|
149
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
150
|
+
message: `Sounding ${apiName} option \`browser.type\` must be a string.`,
|
|
151
|
+
apiName,
|
|
152
|
+
value: options.browser.type,
|
|
153
|
+
path: 'options.browser.type',
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (options.browser.project !== undefined && typeof options.browser.project !== 'string') {
|
|
158
|
+
throw createTestArgumentError({
|
|
159
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
160
|
+
message: `Sounding ${apiName} option \`browser.project\` must be a string.`,
|
|
161
|
+
apiName,
|
|
162
|
+
value: options.browser.project,
|
|
163
|
+
path: 'options.browser.project',
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (options.browser.launchOptions !== undefined && !isPlainObject(options.browser.launchOptions)) {
|
|
168
|
+
throw createTestArgumentError({
|
|
169
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
170
|
+
message: `Sounding ${apiName} option \`browser.launchOptions\` must be an object.`,
|
|
171
|
+
apiName,
|
|
172
|
+
value: options.browser.launchOptions,
|
|
173
|
+
path: 'options.browser.launchOptions',
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (options.browser.contextOptions !== undefined && !isPlainObject(options.browser.contextOptions)) {
|
|
178
|
+
throw createTestArgumentError({
|
|
179
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
180
|
+
message: `Sounding ${apiName} option \`browser.contextOptions\` must be an object.`,
|
|
181
|
+
apiName,
|
|
182
|
+
value: options.browser.contextOptions,
|
|
183
|
+
path: 'options.browser.contextOptions',
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (
|
|
188
|
+
options.browser.artifacts !== undefined &&
|
|
189
|
+
typeof options.browser.artifacts !== 'boolean' &&
|
|
190
|
+
!isPlainObject(options.browser.artifacts)
|
|
191
|
+
) {
|
|
192
|
+
throw createTestArgumentError({
|
|
193
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
194
|
+
message: `Sounding ${apiName} option \`browser.artifacts\` must be a boolean or artifacts options object.`,
|
|
195
|
+
apiName,
|
|
196
|
+
value: options.browser.artifacts,
|
|
197
|
+
path: 'options.browser.artifacts',
|
|
198
|
+
suggestion: 'Use `browser: { artifacts: { trace: true } }` for richer failure artifacts.',
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!isPlainObject(options.browser.artifacts)) {
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const key of Object.keys(options.browser.artifacts)) {
|
|
207
|
+
if (ALLOWED_BROWSER_ARTIFACT_KEYS.includes(key)) {
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
throw createTestArgumentError({
|
|
212
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
213
|
+
message: `Sounding ${apiName} option \`browser.artifacts.${key}\` is unknown.`,
|
|
214
|
+
apiName,
|
|
215
|
+
value: options.browser.artifacts[key],
|
|
216
|
+
path: `options.browser.artifacts.${key}`,
|
|
217
|
+
allowed: ALLOWED_BROWSER_ARTIFACT_KEYS,
|
|
218
|
+
suggestion:
|
|
219
|
+
'Use `outputDir`, `screenshot`, `trace`, `video`, or `currentUrl` inside `browser.artifacts`.',
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (
|
|
224
|
+
options.browser.artifacts.outputDir !== undefined &&
|
|
225
|
+
typeof options.browser.artifacts.outputDir !== 'string'
|
|
226
|
+
) {
|
|
227
|
+
throw createTestArgumentError({
|
|
228
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
229
|
+
message: `Sounding ${apiName} option \`browser.artifacts.outputDir\` must be a string.`,
|
|
230
|
+
apiName,
|
|
231
|
+
value: options.browser.artifacts.outputDir,
|
|
232
|
+
path: 'options.browser.artifacts.outputDir',
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const key of ['screenshot', 'trace', 'video']) {
|
|
237
|
+
const value = options.browser.artifacts[key]
|
|
238
|
+
|
|
239
|
+
if (
|
|
240
|
+
value !== undefined &&
|
|
241
|
+
typeof value !== 'boolean' &&
|
|
242
|
+
!ALLOWED_BROWSER_ARTIFACT_MODES.includes(value)
|
|
243
|
+
) {
|
|
244
|
+
throw createTestArgumentError({
|
|
245
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
246
|
+
message: `Sounding ${apiName} option \`browser.artifacts.${key}\` must be a boolean or one of \`off\`, \`on\`, \`on-failure\`.`,
|
|
247
|
+
apiName,
|
|
248
|
+
value,
|
|
249
|
+
path: `options.browser.artifacts.${key}`,
|
|
250
|
+
allowed: ALLOWED_BROWSER_ARTIFACT_MODES,
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (
|
|
256
|
+
options.browser.artifacts.currentUrl !== undefined &&
|
|
257
|
+
typeof options.browser.artifacts.currentUrl !== 'boolean'
|
|
258
|
+
) {
|
|
259
|
+
throw createTestArgumentError({
|
|
260
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
261
|
+
message: `Sounding ${apiName} option \`browser.artifacts.currentUrl\` must be a boolean.`,
|
|
262
|
+
apiName,
|
|
263
|
+
value: options.browser.artifacts.currentUrl,
|
|
264
|
+
path: 'options.browser.artifacts.currentUrl',
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* @param {any} options
|
|
271
|
+
* @param {string} apiName
|
|
272
|
+
*/
|
|
273
|
+
function assertSocketOptions(options, apiName) {
|
|
274
|
+
if (
|
|
275
|
+
options.socket !== undefined &&
|
|
276
|
+
typeof options.socket !== 'boolean' &&
|
|
277
|
+
!isPlainObject(options.socket)
|
|
278
|
+
) {
|
|
279
|
+
throw createTestArgumentError({
|
|
280
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
281
|
+
message: `Sounding ${apiName} option \`socket\` must be a boolean or socket options object.`,
|
|
282
|
+
apiName,
|
|
283
|
+
value: options.socket,
|
|
284
|
+
path: 'options.socket',
|
|
285
|
+
suggestion: 'Use `socket: true` for trials that need Sails websocket helpers.',
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!isPlainObject(options.socket)) {
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (options.socket.timeout !== undefined && typeof options.socket.timeout !== 'number') {
|
|
294
|
+
throw createTestArgumentError({
|
|
295
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
296
|
+
message: `Sounding ${apiName} option \`socket.timeout\` must be a number.`,
|
|
297
|
+
apiName,
|
|
298
|
+
value: options.socket.timeout,
|
|
299
|
+
path: 'options.socket.timeout',
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (options.socket.baseUrl !== undefined && typeof options.socket.baseUrl !== 'string') {
|
|
304
|
+
throw createTestArgumentError({
|
|
305
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
306
|
+
message: `Sounding ${apiName} option \`socket.baseUrl\` must be a string.`,
|
|
307
|
+
apiName,
|
|
308
|
+
value: options.socket.baseUrl,
|
|
309
|
+
path: 'options.socket.baseUrl',
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* @param {any} options
|
|
316
|
+
* @param {string} apiName
|
|
317
|
+
*/
|
|
318
|
+
function assertWorldOptions(options, apiName) {
|
|
319
|
+
if (options.world === undefined) {
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (typeof options.world === 'string') {
|
|
324
|
+
if (options.world.trim()) {
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
throw createTestArgumentError({
|
|
329
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
330
|
+
message: `Sounding ${apiName} option \`world\` must name a world scenario.`,
|
|
331
|
+
apiName,
|
|
332
|
+
value: options.world,
|
|
333
|
+
path: 'options.world',
|
|
334
|
+
suggestion: 'Use `world: "signed-in-user"` to auto-load a named scenario.',
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!isPlainObject(options.world)) {
|
|
339
|
+
throw createTestArgumentError({
|
|
340
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
341
|
+
message: `Sounding ${apiName} option \`world\` must be a scenario name or world options object.`,
|
|
342
|
+
apiName,
|
|
343
|
+
value: options.world,
|
|
344
|
+
path: 'options.world',
|
|
345
|
+
suggestion: 'Use `world: "signed-in-user"` or `world: { name: "signed-in-user" }`.',
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (typeof options.world.name !== 'string' || !options.world.name.trim()) {
|
|
350
|
+
throw createTestArgumentError({
|
|
351
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
352
|
+
message: `Sounding ${apiName} option \`world.name\` must be a non-empty scenario name.`,
|
|
353
|
+
apiName,
|
|
354
|
+
value: options.world.name,
|
|
355
|
+
path: 'options.world.name',
|
|
356
|
+
suggestion: 'Use `world: { name: "signed-in-user" }`.',
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (options.world.context !== undefined && !isPlainObject(options.world.context)) {
|
|
361
|
+
throw createTestArgumentError({
|
|
362
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
363
|
+
message: `Sounding ${apiName} option \`world.context\` must be an object when provided.`,
|
|
364
|
+
apiName,
|
|
365
|
+
value: options.world.context,
|
|
366
|
+
path: 'options.world.context',
|
|
367
|
+
suggestion: 'Use `world: { name: "signed-in-user", context: { ... } }`.',
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* @param {any} options
|
|
374
|
+
* @param {string} apiName
|
|
375
|
+
*/
|
|
376
|
+
function assertTrialOptions(options, apiName) {
|
|
377
|
+
if (!isPlainObject(options)) {
|
|
378
|
+
throw createTestArgumentError({
|
|
379
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
380
|
+
message: `Sounding ${apiName} options must be an object when provided. Use \`${formatTrialSignature(apiName)}\`.`,
|
|
381
|
+
apiName,
|
|
382
|
+
value: options,
|
|
383
|
+
path: 'options',
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (options.concurrent !== undefined && typeof options.concurrent !== 'boolean') {
|
|
388
|
+
throw createTestArgumentError({
|
|
389
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
390
|
+
message: `Sounding ${apiName} option \`concurrent\` must be a boolean.`,
|
|
391
|
+
apiName,
|
|
392
|
+
value: options.concurrent,
|
|
393
|
+
path: 'options.concurrent',
|
|
394
|
+
suggestion:
|
|
395
|
+
'Use `concurrent: true` only for trials that can run with isolated runtime state.',
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (options.transport !== undefined && !ALLOWED_TRANSPORTS.includes(options.transport)) {
|
|
400
|
+
throw createTestArgumentError({
|
|
401
|
+
code: 'E_SOUNDING_TEST_OPTIONS_INVALID',
|
|
402
|
+
message: `Sounding ${apiName} option \`transport\` must be either \`virtual\` or \`http\`.`,
|
|
403
|
+
apiName,
|
|
404
|
+
value: options.transport,
|
|
405
|
+
path: 'options.transport',
|
|
406
|
+
allowed: ALLOWED_TRANSPORTS,
|
|
407
|
+
suggestion: 'Use `virtual` for Sails-native in-process requests or `http` for real HTTP requests.',
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
assertBrowserOptions(options, apiName)
|
|
412
|
+
assertSocketOptions(options, apiName)
|
|
413
|
+
assertWorldOptions(options, apiName)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* @param {any} title
|
|
418
|
+
* @param {SoundingTestOptions | SoundingTrialHandler} optionsOrHandler
|
|
419
|
+
* @param {SoundingTrialHandler} [maybeHandler]
|
|
420
|
+
* @param {string} [apiName]
|
|
421
|
+
* @returns {{ title: string, options: SoundingTestOptions, handler: SoundingTrialHandler }}
|
|
422
|
+
*/
|
|
423
|
+
function normalizeTestArgs(title, optionsOrHandler, maybeHandler, apiName = 'test') {
|
|
424
|
+
assertTrialTitle(title, apiName)
|
|
425
|
+
|
|
426
|
+
if (typeof optionsOrHandler === 'function') {
|
|
427
|
+
assertTrialHandler(optionsOrHandler, apiName)
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
title,
|
|
431
|
+
options: {},
|
|
432
|
+
handler: optionsOrHandler,
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const options = optionsOrHandler === undefined ? {} : optionsOrHandler
|
|
437
|
+
assertTrialOptions(options, apiName)
|
|
438
|
+
assertTrialHandler(maybeHandler, apiName)
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
title,
|
|
442
|
+
options,
|
|
443
|
+
handler: maybeHandler,
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* @param {SoundingTestOptions} [options]
|
|
449
|
+
* @param {string} [apiName]
|
|
450
|
+
* @returns {{ nodeOptions: Record<string, any>, trialOptions: SoundingTestOptions }}
|
|
451
|
+
*/
|
|
452
|
+
function splitTestOptions(options = {}, apiName = 'test') {
|
|
453
|
+
assertTrialOptions(options, apiName)
|
|
454
|
+
|
|
455
|
+
const { transport, browser, socket, world, concurrent, ...nodeOptions } = options
|
|
456
|
+
const nodeConcurrency = nodeOptions.concurrency
|
|
457
|
+
const concurrentEnabled =
|
|
458
|
+
concurrent === true ||
|
|
459
|
+
nodeConcurrency === true ||
|
|
460
|
+
(typeof nodeConcurrency === 'number' && nodeConcurrency > 1)
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
nodeOptions: {
|
|
464
|
+
...nodeOptions,
|
|
465
|
+
concurrency: concurrentEnabled ? nodeConcurrency ?? true : false,
|
|
466
|
+
},
|
|
467
|
+
trialOptions: {
|
|
468
|
+
transport,
|
|
469
|
+
browser,
|
|
470
|
+
socket,
|
|
471
|
+
world,
|
|
472
|
+
concurrent: concurrentEnabled,
|
|
473
|
+
},
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
module.exports = {
|
|
478
|
+
normalizeTestArgs,
|
|
479
|
+
splitTestOptions,
|
|
480
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sounding",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Testing framework for Sails applications and The Boring JavaScript Stack.",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sounding": "bin/sounding.js"
|
|
8
|
+
},
|
|
6
9
|
"license": "MIT",
|
|
7
10
|
"keywords": [
|
|
8
11
|
"sails",
|
|
@@ -12,6 +15,7 @@
|
|
|
12
15
|
"tbjs"
|
|
13
16
|
],
|
|
14
17
|
"files": [
|
|
18
|
+
"bin",
|
|
15
19
|
"index.js",
|
|
16
20
|
"lib",
|
|
17
21
|
"README.md",
|
|
@@ -19,7 +23,8 @@
|
|
|
19
23
|
"LICENSE"
|
|
20
24
|
],
|
|
21
25
|
"scripts": {
|
|
22
|
-
"test": "node --test"
|
|
26
|
+
"test": "node --test test/*.test.js",
|
|
27
|
+
"typecheck": "tsc -p jsconfig.json --noEmit"
|
|
23
28
|
},
|
|
24
29
|
"sails": {
|
|
25
30
|
"isHook": true,
|
|
@@ -35,5 +40,14 @@
|
|
|
35
40
|
},
|
|
36
41
|
"publishConfig": {
|
|
37
42
|
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^25.9.1",
|
|
46
|
+
"sails": "^1.5.18",
|
|
47
|
+
"sails-hook-orm": "^4.0.3",
|
|
48
|
+
"sails-hook-sockets": "^3.0.2",
|
|
49
|
+
"sails-sqlite": "^0.2.6",
|
|
50
|
+
"socket.io-client": "^4.8.3",
|
|
51
|
+
"typescript": "^6.0.3"
|
|
38
52
|
}
|
|
39
53
|
}
|