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-runtime.js
CHANGED
|
@@ -3,39 +3,197 @@ const path = require('node:path')
|
|
|
3
3
|
const { createMailbox } = require('./create-mailbox')
|
|
4
4
|
const { createMailCapture } = require('./create-mail-capture')
|
|
5
5
|
const { createWorldEngine } = require('./create-world-engine')
|
|
6
|
-
const { loadWorldFiles } = require('./create-world-loader')
|
|
6
|
+
const { createWorldLoaderCache, loadWorldFiles } = require('./create-world-loader')
|
|
7
7
|
const { createHelperRunner } = require('./create-helper-runner')
|
|
8
8
|
const { createRequestClient } = require('./create-request-client')
|
|
9
9
|
const { createVisitClient } = require('./create-visit-client')
|
|
10
10
|
const { createBrowserManager } = require('./create-browser-manager')
|
|
11
11
|
const { createAuthHelpers } = require('./create-auth-helpers')
|
|
12
|
+
const { createSocketManager } = require('./create-socket-manager')
|
|
12
13
|
const { getDefaultConfig } = require('./default-config')
|
|
13
14
|
const { mergeConfig } = require('./merge-config')
|
|
14
15
|
const { normalizeUserConfig } = require('./normalize-config')
|
|
15
16
|
const { resolveDatastore } = require('./resolve-datastore')
|
|
17
|
+
const { createSoundingError } = require('./create-error')
|
|
18
|
+
const { validateConfig } = require('./validate-config')
|
|
16
19
|
|
|
20
|
+
/** @typedef {import('./types').SoundingBootResult} SoundingBootResult */
|
|
21
|
+
/** @typedef {import('./types').SoundingConfig} SoundingConfig */
|
|
22
|
+
/** @typedef {import('./types').SoundingRuntime} SoundingRuntime */
|
|
23
|
+
/** @typedef {import('./types').SoundingSailsApp} SoundingSailsApp */
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {SoundingSailsApp} sails
|
|
27
|
+
* @returns {SoundingConfig}
|
|
28
|
+
*/
|
|
17
29
|
function resolveConfig(sails) {
|
|
18
|
-
return
|
|
30
|
+
return resolveConfigValue(sails.config?.sounding || {})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {any} value
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
function getConfigSignature(value) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.stringify(value || {})
|
|
40
|
+
} catch (_error) {
|
|
41
|
+
return String(Date.now())
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {any} userConfig
|
|
47
|
+
* @returns {SoundingConfig}
|
|
48
|
+
*/
|
|
49
|
+
function resolveConfigValue(userConfig) {
|
|
50
|
+
return validateConfig(
|
|
51
|
+
/** @type {SoundingConfig} */ (
|
|
52
|
+
mergeConfig(getDefaultConfig(), normalizeUserConfig(userConfig || {}))
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {SoundingSailsApp} sails
|
|
59
|
+
* @returns {(() => SoundingConfig) & { stats: { resolutions: number }, invalidate(): void }}
|
|
60
|
+
*/
|
|
61
|
+
function createConfigResolver(sails) {
|
|
62
|
+
/** @type {any} */
|
|
63
|
+
let cachedInput = null
|
|
64
|
+
/** @type {string | null} */
|
|
65
|
+
let cachedSignature = null
|
|
66
|
+
/** @type {SoundingConfig | null} */
|
|
67
|
+
let cachedConfig = null
|
|
68
|
+
const stats = {
|
|
69
|
+
resolutions: 0,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const resolver = /** @type {(() => SoundingConfig) & { stats: { resolutions: number }, invalidate(): void }} */ (
|
|
73
|
+
function resolveCachedConfig() {
|
|
74
|
+
const input = sails.config?.sounding || {}
|
|
75
|
+
const signature = getConfigSignature(input)
|
|
76
|
+
|
|
77
|
+
if (cachedConfig && cachedInput === input && cachedSignature === signature) {
|
|
78
|
+
return cachedConfig
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
cachedInput = input
|
|
82
|
+
cachedSignature = signature
|
|
83
|
+
cachedConfig = resolveConfigValue(input)
|
|
84
|
+
stats.resolutions += 1
|
|
85
|
+
return cachedConfig
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
resolver.stats = stats
|
|
90
|
+
resolver.invalidate = function invalidateConfigCache() {
|
|
91
|
+
cachedInput = null
|
|
92
|
+
cachedSignature = null
|
|
93
|
+
cachedConfig = null
|
|
94
|
+
stats.resolutions = 0
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return resolver
|
|
19
98
|
}
|
|
20
99
|
|
|
100
|
+
/**
|
|
101
|
+
* @param {SoundingSailsApp} sails
|
|
102
|
+
* @param {SoundingConfig} config
|
|
103
|
+
* @returns {string}
|
|
104
|
+
*/
|
|
21
105
|
function resolveAppPath(sails, config) {
|
|
22
106
|
const basePath = sails?.config?.appPath || process.cwd()
|
|
23
107
|
return path.resolve(basePath, config.app?.path || '.')
|
|
24
108
|
}
|
|
25
109
|
|
|
110
|
+
/**
|
|
111
|
+
* @param {unknown} error
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
function formatCleanupError(error) {
|
|
115
|
+
if (error instanceof Error) {
|
|
116
|
+
return error.message
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return String(error)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {{ resource: string, cleanup: () => void | Promise<void> }[]} steps
|
|
124
|
+
* @returns {Promise<void>}
|
|
125
|
+
*/
|
|
126
|
+
async function runCleanupSteps(steps) {
|
|
127
|
+
const failures = []
|
|
128
|
+
|
|
129
|
+
for (const step of steps) {
|
|
130
|
+
try {
|
|
131
|
+
await step.cleanup()
|
|
132
|
+
} catch (error) {
|
|
133
|
+
failures.push({
|
|
134
|
+
resource: step.resource,
|
|
135
|
+
error,
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (failures.length === 0) {
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const resources = failures.map((failure) => failure.resource).join(', ')
|
|
145
|
+
const errors = failures.map(
|
|
146
|
+
(failure) =>
|
|
147
|
+
createSoundingError({
|
|
148
|
+
code: 'E_SOUNDING_CLEANUP_RESOURCE_FAILED',
|
|
149
|
+
message: `${failure.resource}: ${formatCleanupError(failure.error)}`,
|
|
150
|
+
details: {
|
|
151
|
+
resource: failure.resource,
|
|
152
|
+
},
|
|
153
|
+
cause: failure.error,
|
|
154
|
+
})
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
const error = /** @type {AggregateError & { code: string, resources: string[], details: { resources: string[] } }} */ (
|
|
158
|
+
new AggregateError(errors, `Sounding cleanup failed for ${resources}.`)
|
|
159
|
+
)
|
|
160
|
+
error.name = 'SoundingCleanupError'
|
|
161
|
+
error.code = 'E_SOUNDING_CLEANUP_FAILED'
|
|
162
|
+
error.resources = failures.map((failure) => failure.resource)
|
|
163
|
+
error.details = {
|
|
164
|
+
resources: error.resources,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
throw error
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Create a Sounding runtime bound to a loaded Sails app.
|
|
172
|
+
*
|
|
173
|
+
* @param {SoundingSailsApp} sails
|
|
174
|
+
* @returns {SoundingRuntime}
|
|
175
|
+
*/
|
|
26
176
|
function createRuntime(sails) {
|
|
27
177
|
const mailbox = createMailbox()
|
|
28
178
|
const world = createWorldEngine({ sails })
|
|
29
179
|
const helpers = createHelperRunner({ sails })
|
|
180
|
+
const getConfig = createConfigResolver(sails)
|
|
181
|
+
const worldLoaderCache = createWorldLoaderCache()
|
|
30
182
|
const request = createRequestClient({
|
|
31
183
|
sails,
|
|
32
|
-
getConfig
|
|
184
|
+
getConfig,
|
|
185
|
+
world,
|
|
33
186
|
})
|
|
34
187
|
const visit = createVisitClient({ request })
|
|
188
|
+
const sockets = createSocketManager({
|
|
189
|
+
sails,
|
|
190
|
+
getConfig,
|
|
191
|
+
world,
|
|
192
|
+
})
|
|
35
193
|
const browser = createBrowserManager({
|
|
36
194
|
sails,
|
|
37
|
-
getConfig
|
|
38
|
-
appPathResolver: () => resolveAppPath(sails,
|
|
195
|
+
getConfig,
|
|
196
|
+
appPathResolver: () => resolveAppPath(sails, getConfig()),
|
|
39
197
|
})
|
|
40
198
|
const auth = createAuthHelpers({
|
|
41
199
|
sails,
|
|
@@ -46,14 +204,14 @@ function createRuntime(sails) {
|
|
|
46
204
|
const mailCapture = createMailCapture({
|
|
47
205
|
sails,
|
|
48
206
|
mailbox,
|
|
49
|
-
getConfig
|
|
207
|
+
getConfig,
|
|
50
208
|
})
|
|
51
209
|
let bootState = null
|
|
52
210
|
let datastoreState = null
|
|
53
211
|
|
|
54
212
|
return {
|
|
55
213
|
get config() {
|
|
56
|
-
return
|
|
214
|
+
return getConfig()
|
|
57
215
|
},
|
|
58
216
|
|
|
59
217
|
get appPath() {
|
|
@@ -72,11 +230,6 @@ function createRuntime(sails) {
|
|
|
72
230
|
return helpers
|
|
73
231
|
},
|
|
74
232
|
|
|
75
|
-
// Temporary compatibility alias while the DX settles.
|
|
76
|
-
get helper() {
|
|
77
|
-
return helpers
|
|
78
|
-
},
|
|
79
|
-
|
|
80
233
|
get request() {
|
|
81
234
|
return request
|
|
82
235
|
},
|
|
@@ -85,6 +238,10 @@ function createRuntime(sails) {
|
|
|
85
238
|
return visit
|
|
86
239
|
},
|
|
87
240
|
|
|
241
|
+
get sockets() {
|
|
242
|
+
return sockets
|
|
243
|
+
},
|
|
244
|
+
|
|
88
245
|
get browser() {
|
|
89
246
|
return browser
|
|
90
247
|
},
|
|
@@ -96,7 +253,7 @@ function createRuntime(sails) {
|
|
|
96
253
|
configure() {
|
|
97
254
|
datastoreState = resolveDatastore({
|
|
98
255
|
sails,
|
|
99
|
-
soundingConfig:
|
|
256
|
+
soundingConfig: getConfig(),
|
|
100
257
|
})
|
|
101
258
|
|
|
102
259
|
return datastoreState
|
|
@@ -106,27 +263,34 @@ function createRuntime(sails) {
|
|
|
106
263
|
return datastoreState
|
|
107
264
|
},
|
|
108
265
|
|
|
266
|
+
/**
|
|
267
|
+
* @param {{ mode?: string }} [options]
|
|
268
|
+
* @returns {Promise<SoundingBootResult>}
|
|
269
|
+
*/
|
|
109
270
|
async boot(options = {}) {
|
|
110
271
|
if (!datastoreState) {
|
|
111
272
|
datastoreState = this.configure()
|
|
112
273
|
}
|
|
113
274
|
|
|
275
|
+
const config = getConfig()
|
|
276
|
+
const appPath = resolveAppPath(sails, config)
|
|
114
277
|
world.reset({ preserveSequences: true })
|
|
115
278
|
const loadedWorldFiles = await loadWorldFiles({
|
|
116
279
|
world,
|
|
117
|
-
appPath
|
|
118
|
-
config
|
|
280
|
+
appPath,
|
|
281
|
+
config,
|
|
119
282
|
sails,
|
|
283
|
+
cache: worldLoaderCache,
|
|
120
284
|
})
|
|
121
285
|
const captureInstalled = mailCapture.install()
|
|
122
286
|
|
|
123
287
|
bootState = {
|
|
124
288
|
bootedAt: new Date().toISOString(),
|
|
125
289
|
mode: options.mode || 'unit',
|
|
126
|
-
config
|
|
290
|
+
config,
|
|
127
291
|
datastore: datastoreState,
|
|
128
292
|
mail: {
|
|
129
|
-
captureEnabled:
|
|
293
|
+
captureEnabled: config.mail?.capture !== false,
|
|
130
294
|
captureInstalled,
|
|
131
295
|
},
|
|
132
296
|
world: {
|
|
@@ -142,6 +306,7 @@ function createRuntime(sails) {
|
|
|
142
306
|
world,
|
|
143
307
|
request,
|
|
144
308
|
visit,
|
|
309
|
+
sockets,
|
|
145
310
|
browser,
|
|
146
311
|
auth,
|
|
147
312
|
login: auth.login,
|
|
@@ -151,20 +316,56 @@ function createRuntime(sails) {
|
|
|
151
316
|
async lower() {
|
|
152
317
|
bootState = null
|
|
153
318
|
datastoreState = null
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
319
|
+
|
|
320
|
+
await runCleanupSteps([
|
|
321
|
+
{
|
|
322
|
+
resource: 'sockets',
|
|
323
|
+
cleanup: () => sockets.closeAll(),
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
resource: 'request session',
|
|
327
|
+
cleanup: () => request.clearSession(),
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
resource: 'browser',
|
|
331
|
+
cleanup: () => browser.close(),
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
resource: 'mail capture',
|
|
335
|
+
cleanup: () => mailCapture.uninstall(),
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
resource: 'mailbox',
|
|
339
|
+
cleanup: () => mailbox.clear(),
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
resource: 'world',
|
|
343
|
+
cleanup: () => world.reset({ preserveSequences: true }),
|
|
344
|
+
},
|
|
345
|
+
])
|
|
158
346
|
},
|
|
159
347
|
|
|
160
348
|
get state() {
|
|
161
349
|
return bootState
|
|
162
350
|
},
|
|
351
|
+
|
|
352
|
+
get cacheStats() {
|
|
353
|
+
return {
|
|
354
|
+
config: { ...getConfig.stats },
|
|
355
|
+
worldLoader: { ...worldLoaderCache.stats },
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
invalidateCaches() {
|
|
360
|
+
getConfig.invalidate()
|
|
361
|
+
worldLoaderCache.clear()
|
|
362
|
+
},
|
|
163
363
|
}
|
|
164
364
|
}
|
|
165
365
|
|
|
166
366
|
module.exports = {
|
|
167
367
|
createRuntime,
|
|
368
|
+
createConfigResolver,
|
|
168
369
|
resolveConfig,
|
|
169
370
|
resolveAppPath,
|
|
170
371
|
}
|