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
|
@@ -1,15 +1,41 @@
|
|
|
1
|
+
const { createSoundingError } = require('./create-error')
|
|
2
|
+
|
|
3
|
+
/** @typedef {import('./types').AnyRecord} AnyRecord */
|
|
4
|
+
/** @typedef {import('./types').SoundingHelperRunner} SoundingHelperRunner */
|
|
5
|
+
/** @typedef {import('./types').SoundingSailsApp} SoundingSailsApp */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {SoundingSailsApp} sails
|
|
9
|
+
* @param {string} identity
|
|
10
|
+
* @returns {any}
|
|
11
|
+
*/
|
|
1
12
|
function resolveHelper(sails, identity) {
|
|
2
13
|
return identity
|
|
3
14
|
.split('.')
|
|
4
15
|
.reduce((current, segment) => current?.[segment], sails.helpers)
|
|
5
16
|
}
|
|
6
17
|
|
|
18
|
+
/**
|
|
19
|
+
* @param {{ sails: SoundingSailsApp }} input
|
|
20
|
+
* @returns {SoundingHelperRunner}
|
|
21
|
+
*/
|
|
7
22
|
function createHelperRunner({ sails }) {
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} identity
|
|
25
|
+
* @param {AnyRecord} [inputs]
|
|
26
|
+
* @returns {Promise<any>}
|
|
27
|
+
*/
|
|
8
28
|
async function invoke(identity, inputs = {}) {
|
|
9
29
|
const helper = resolveHelper(sails, identity)
|
|
10
30
|
|
|
11
31
|
if (!helper) {
|
|
12
|
-
throw
|
|
32
|
+
throw createSoundingError({
|
|
33
|
+
code: 'E_SOUNDING_HELPER_UNKNOWN',
|
|
34
|
+
message: `Unknown Sounding helper: ${identity}`,
|
|
35
|
+
details: {
|
|
36
|
+
identity,
|
|
37
|
+
},
|
|
38
|
+
})
|
|
13
39
|
}
|
|
14
40
|
|
|
15
41
|
if (typeof helper.with === 'function') {
|
|
@@ -20,9 +46,19 @@ function createHelperRunner({ sails }) {
|
|
|
20
46
|
return helper(inputs)
|
|
21
47
|
}
|
|
22
48
|
|
|
23
|
-
throw
|
|
49
|
+
throw createSoundingError({
|
|
50
|
+
code: 'E_SOUNDING_HELPER_NOT_CALLABLE',
|
|
51
|
+
message: `Sounding helper \`${identity}\` is not callable.`,
|
|
52
|
+
details: {
|
|
53
|
+
identity,
|
|
54
|
+
},
|
|
55
|
+
})
|
|
24
56
|
}
|
|
25
57
|
|
|
58
|
+
/**
|
|
59
|
+
* @param {string[]} [path]
|
|
60
|
+
* @returns {SoundingHelperRunner}
|
|
61
|
+
*/
|
|
26
62
|
function buildProxy(path = []) {
|
|
27
63
|
const callable = async (...args) => {
|
|
28
64
|
if (path.length === 0) {
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
const path = require('node:path')
|
|
2
2
|
const url = require('node:url')
|
|
3
|
+
const { createSoundingError } = require('./create-error')
|
|
4
|
+
const { getTrialContext } = require('./trial-context')
|
|
3
5
|
|
|
4
6
|
const DEFAULT_MAIL_LAYOUT = 'mail'
|
|
5
|
-
|
|
7
|
+
const mailCaptureRegistry = new WeakMap()
|
|
8
|
+
|
|
9
|
+
/** @typedef {import('./types').AnyRecord} AnyRecord */
|
|
10
|
+
/** @typedef {import('./types').SoundingMailCapture} SoundingMailCapture */
|
|
11
|
+
/** @typedef {import('./types').SoundingMailbox} SoundingMailbox */
|
|
12
|
+
/** @typedef {import('./types').SoundingMailMessage} SoundingMailMessage */
|
|
13
|
+
/** @typedef {import('./types').SoundingSailsApp} SoundingSailsApp */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {any} value
|
|
17
|
+
* @returns {any[]}
|
|
18
|
+
*/
|
|
6
19
|
function normalizeList(value) {
|
|
7
20
|
if (value === undefined || value === null || value === '') {
|
|
8
21
|
return []
|
|
@@ -22,6 +35,10 @@ function normalizeList(value) {
|
|
|
22
35
|
return [value]
|
|
23
36
|
}
|
|
24
37
|
|
|
38
|
+
/**
|
|
39
|
+
* @param {string | undefined} html
|
|
40
|
+
* @returns {string[]}
|
|
41
|
+
*/
|
|
25
42
|
function extractLinks(html) {
|
|
26
43
|
if (!html) {
|
|
27
44
|
return []
|
|
@@ -31,6 +48,11 @@ function extractLinks(html) {
|
|
|
31
48
|
return [...matches].map((match) => match[1])
|
|
32
49
|
}
|
|
33
50
|
|
|
51
|
+
/**
|
|
52
|
+
* @param {AnyRecord} [inputs]
|
|
53
|
+
* @param {string[]} [links]
|
|
54
|
+
* @returns {string | undefined}
|
|
55
|
+
*/
|
|
34
56
|
function resolvePrimaryLink(inputs = {}, links = []) {
|
|
35
57
|
const templateData = inputs.templateData || {}
|
|
36
58
|
const preferredKeys = [
|
|
@@ -52,6 +74,10 @@ function resolvePrimaryLink(inputs = {}, links = []) {
|
|
|
52
74
|
return links.find((link) => !/\/unsubscribe\b/i.test(link)) || links[0]
|
|
53
75
|
}
|
|
54
76
|
|
|
77
|
+
/**
|
|
78
|
+
* @param {any} view
|
|
79
|
+
* @returns {Promise<string | undefined>}
|
|
80
|
+
*/
|
|
55
81
|
function createRenderViewPromise(view) {
|
|
56
82
|
if (!view) {
|
|
57
83
|
return Promise.resolve(undefined)
|
|
@@ -64,6 +90,12 @@ function createRenderViewPromise(view) {
|
|
|
64
90
|
return Promise.resolve(view)
|
|
65
91
|
}
|
|
66
92
|
|
|
93
|
+
/**
|
|
94
|
+
* @param {SoundingSailsApp} sails
|
|
95
|
+
* @param {AnyRecord} [inputs]
|
|
96
|
+
* @param {AnyRecord} [options]
|
|
97
|
+
* @returns {Promise<string | undefined>}
|
|
98
|
+
*/
|
|
67
99
|
async function renderTemplatePreview(sails, inputs = {}, options = {}) {
|
|
68
100
|
const { template, templateData = {} } = inputs
|
|
69
101
|
|
|
@@ -73,6 +105,7 @@ async function renderTemplatePreview(sails, inputs = {}, options = {}) {
|
|
|
73
105
|
|
|
74
106
|
const resolvedLayout = resolvePreviewLayout(sails, inputs, options)
|
|
75
107
|
const emailTemplatePath = path.join('emails/', template)
|
|
108
|
+
/** @type {string | false} */
|
|
76
109
|
let emailTemplateLayout = false
|
|
77
110
|
|
|
78
111
|
if (resolvedLayout) {
|
|
@@ -91,10 +124,19 @@ async function renderTemplatePreview(sails, inputs = {}, options = {}) {
|
|
|
91
124
|
)
|
|
92
125
|
}
|
|
93
126
|
|
|
127
|
+
/**
|
|
128
|
+
* @param {SoundingSailsApp} sails
|
|
129
|
+
* @returns {AnyRecord}
|
|
130
|
+
*/
|
|
94
131
|
function resolveMailConfig(sails) {
|
|
95
132
|
return sails?.config?.mail || {}
|
|
96
133
|
}
|
|
97
134
|
|
|
135
|
+
/**
|
|
136
|
+
* @param {SoundingSailsApp} sails
|
|
137
|
+
* @param {AnyRecord} [options]
|
|
138
|
+
* @returns {AnyRecord}
|
|
139
|
+
*/
|
|
98
140
|
function resolveSoundingMailConfig(sails, options = {}) {
|
|
99
141
|
if (options.mailSettings) {
|
|
100
142
|
return options.mailSettings
|
|
@@ -106,6 +148,12 @@ function resolveSoundingMailConfig(sails, options = {}) {
|
|
|
106
148
|
return soundingConfig.mail || {}
|
|
107
149
|
}
|
|
108
150
|
|
|
151
|
+
/**
|
|
152
|
+
* @param {SoundingSailsApp} sails
|
|
153
|
+
* @param {AnyRecord} [inputs]
|
|
154
|
+
* @param {AnyRecord} [options]
|
|
155
|
+
* @returns {string | false | undefined}
|
|
156
|
+
*/
|
|
109
157
|
function resolvePreviewLayout(sails, inputs = {}, options = {}) {
|
|
110
158
|
if (Object.prototype.hasOwnProperty.call(inputs, 'layout')) {
|
|
111
159
|
return inputs.layout
|
|
@@ -151,6 +199,12 @@ function toErrorShape(error) {
|
|
|
151
199
|
}
|
|
152
200
|
}
|
|
153
201
|
|
|
202
|
+
/**
|
|
203
|
+
* @param {SoundingSailsApp} sails
|
|
204
|
+
* @param {AnyRecord} [inputs]
|
|
205
|
+
* @param {AnyRecord} [options]
|
|
206
|
+
* @returns {Promise<SoundingMailMessage>}
|
|
207
|
+
*/
|
|
154
208
|
async function buildCapturedMail(sails, inputs = {}, options = {}) {
|
|
155
209
|
const mailer = resolveMailerName(sails, inputs)
|
|
156
210
|
const transport = resolveTransportName(sails, mailer)
|
|
@@ -181,6 +235,10 @@ async function buildCapturedMail(sails, inputs = {}, options = {}) {
|
|
|
181
235
|
}
|
|
182
236
|
}
|
|
183
237
|
|
|
238
|
+
/**
|
|
239
|
+
* @param {{ sails?: SoundingSailsApp, mailbox: SoundingMailbox, getConfig?: () => AnyRecord }} input
|
|
240
|
+
* @returns {SoundingMailCapture}
|
|
241
|
+
*/
|
|
184
242
|
function createMailCapture({ sails, mailbox, getConfig }) {
|
|
185
243
|
let originalSend = null
|
|
186
244
|
|
|
@@ -191,18 +249,27 @@ function createMailCapture({ sails, mailbox, getConfig }) {
|
|
|
191
249
|
return soundingConfig.mail || {}
|
|
192
250
|
}
|
|
193
251
|
|
|
252
|
+
function resolveCaptureTarget() {
|
|
253
|
+
const context = getTrialContext()
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
mailbox: context?.mailbox || mailbox,
|
|
257
|
+
mailSettings: context?.getConfig?.().mail || resolveMailSettings(),
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
194
261
|
function isEnabled() {
|
|
195
262
|
return resolveMailSettings().capture !== false
|
|
196
263
|
}
|
|
197
264
|
|
|
198
265
|
function shouldPassthrough() {
|
|
199
|
-
const settings =
|
|
266
|
+
const settings = resolveCaptureTarget().mailSettings
|
|
200
267
|
return settings.deliver === true || settings.mode === 'passthrough'
|
|
201
268
|
}
|
|
202
269
|
|
|
203
270
|
function resolveMessageLayout(inputs = {}) {
|
|
204
271
|
return resolvePreviewLayout(sails, inputs, {
|
|
205
|
-
mailSettings:
|
|
272
|
+
mailSettings: resolveCaptureTarget().mailSettings,
|
|
206
273
|
})
|
|
207
274
|
}
|
|
208
275
|
|
|
@@ -283,7 +350,10 @@ function createMailCapture({ sails, mailbox, getConfig }) {
|
|
|
283
350
|
return sendHelper.with.bind(sendHelper)
|
|
284
351
|
}
|
|
285
352
|
|
|
286
|
-
throw
|
|
353
|
+
throw createSoundingError({
|
|
354
|
+
code: 'E_SOUNDING_MAIL_SEND_UNAVAILABLE',
|
|
355
|
+
message: 'Sounding could not invoke `sails.helpers.mail.send`.',
|
|
356
|
+
})
|
|
287
357
|
}
|
|
288
358
|
|
|
289
359
|
function resolveWithInvoker(sendHelper) {
|
|
@@ -295,20 +365,25 @@ function createMailCapture({ sails, mailbox, getConfig }) {
|
|
|
295
365
|
return sendHelper.bind(sendHelper)
|
|
296
366
|
}
|
|
297
367
|
|
|
298
|
-
throw
|
|
368
|
+
throw createSoundingError({
|
|
369
|
+
code: 'E_SOUNDING_MAIL_SEND_WITH_UNAVAILABLE',
|
|
370
|
+
message: 'Sounding could not invoke `sails.helpers.mail.send.with()`.',
|
|
371
|
+
})
|
|
299
372
|
}
|
|
300
373
|
|
|
301
374
|
async function captureSuccessfulSend(inputs = {}) {
|
|
375
|
+
const target = resolveCaptureTarget()
|
|
376
|
+
|
|
302
377
|
try {
|
|
303
378
|
const message = await buildCapturedMail(sails, inputs, {
|
|
304
|
-
mailSettings:
|
|
379
|
+
mailSettings: target.mailSettings,
|
|
305
380
|
})
|
|
306
|
-
mailbox.capture({
|
|
381
|
+
target.mailbox.capture({
|
|
307
382
|
...message,
|
|
308
383
|
status: 'sent',
|
|
309
384
|
})
|
|
310
385
|
} catch (error) {
|
|
311
|
-
mailbox.capture({
|
|
386
|
+
target.mailbox.capture({
|
|
312
387
|
mailer: resolveMailerName(sails, inputs),
|
|
313
388
|
transport: resolveTransportName(sails, resolveMailerName(sails, inputs)),
|
|
314
389
|
to: normalizeList(inputs.to),
|
|
@@ -322,11 +397,12 @@ function createMailCapture({ sails, mailbox, getConfig }) {
|
|
|
322
397
|
}
|
|
323
398
|
|
|
324
399
|
async function captureFailedSend(inputs = {}, error) {
|
|
400
|
+
const target = resolveCaptureTarget()
|
|
325
401
|
let message
|
|
326
402
|
|
|
327
403
|
try {
|
|
328
404
|
message = await buildCapturedMail(sails, inputs, {
|
|
329
|
-
mailSettings:
|
|
405
|
+
mailSettings: target.mailSettings,
|
|
330
406
|
})
|
|
331
407
|
} catch (captureError) {
|
|
332
408
|
message = {
|
|
@@ -340,7 +416,7 @@ function createMailCapture({ sails, mailbox, getConfig }) {
|
|
|
340
416
|
}
|
|
341
417
|
}
|
|
342
418
|
|
|
343
|
-
mailbox.capture({
|
|
419
|
+
target.mailbox.capture({
|
|
344
420
|
...message,
|
|
345
421
|
status: 'failed',
|
|
346
422
|
error: toErrorShape(error),
|
|
@@ -386,27 +462,60 @@ function createMailCapture({ sails, mailbox, getConfig }) {
|
|
|
386
462
|
return false
|
|
387
463
|
}
|
|
388
464
|
|
|
389
|
-
const
|
|
465
|
+
const mailHelpers = sails?.helpers?.mail
|
|
466
|
+
const sendHelper = mailHelpers?.send
|
|
390
467
|
if (!sendHelper) {
|
|
391
468
|
return false
|
|
392
469
|
}
|
|
393
470
|
|
|
394
|
-
|
|
395
|
-
|
|
471
|
+
const registered = mailCaptureRegistry.get(mailHelpers) || sendHelper.__soundingRegistry
|
|
472
|
+
if (registered && sendHelper === registered.wrappedSend) {
|
|
473
|
+
registered.installs += 1
|
|
474
|
+
originalSend = registered.originalSend
|
|
475
|
+
mailCaptureRegistry.set(mailHelpers, registered)
|
|
396
476
|
return true
|
|
397
477
|
}
|
|
398
478
|
|
|
399
479
|
originalSend = sendHelper
|
|
400
|
-
|
|
480
|
+
const wrappedSend = wrapSendHelper(sendHelper)
|
|
481
|
+
const registry = {
|
|
482
|
+
originalSend,
|
|
483
|
+
wrappedSend,
|
|
484
|
+
installs: 1,
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
Object.defineProperty(wrappedSend, '__soundingRegistry', {
|
|
488
|
+
value: registry,
|
|
489
|
+
configurable: true,
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
mailCaptureRegistry.set(mailHelpers, registry)
|
|
493
|
+
mailHelpers.send = wrappedSend
|
|
401
494
|
return true
|
|
402
495
|
}
|
|
403
496
|
|
|
404
497
|
function uninstall() {
|
|
405
|
-
|
|
498
|
+
const mailHelpers = sails?.helpers?.mail
|
|
499
|
+
|
|
500
|
+
if (!originalSend || !mailHelpers) {
|
|
406
501
|
return false
|
|
407
502
|
}
|
|
408
503
|
|
|
409
|
-
|
|
504
|
+
const registered = mailCaptureRegistry.get(mailHelpers)
|
|
505
|
+
|
|
506
|
+
if (registered && registered.originalSend === originalSend) {
|
|
507
|
+
registered.installs -= 1
|
|
508
|
+
|
|
509
|
+
if (registered.installs <= 0) {
|
|
510
|
+
mailHelpers.send = originalSend
|
|
511
|
+
mailCaptureRegistry.delete(mailHelpers)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
originalSend = null
|
|
515
|
+
return true
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
mailHelpers.send = originalSend
|
|
410
519
|
originalSend = null
|
|
411
520
|
return true
|
|
412
521
|
}
|
package/lib/create-mailbox.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
/** @typedef {import('./types').SoundingMailbox} SoundingMailbox */
|
|
2
|
+
/** @typedef {import('./types').SoundingMailMessage} SoundingMailMessage */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @returns {SoundingMailbox}
|
|
6
|
+
*/
|
|
1
7
|
function createMailbox() {
|
|
8
|
+
/** @type {SoundingMailMessage[]} */
|
|
2
9
|
const messages = []
|
|
3
10
|
|
|
4
11
|
return {
|
|
12
|
+
/**
|
|
13
|
+
* @param {SoundingMailMessage} message
|
|
14
|
+
* @returns {SoundingMailMessage}
|
|
15
|
+
*/
|
|
5
16
|
capture(message) {
|
|
6
17
|
const normalized = {
|
|
7
18
|
capturedAt: new Date().toISOString(),
|
|
@@ -11,14 +22,23 @@ function createMailbox() {
|
|
|
11
22
|
return normalized
|
|
12
23
|
},
|
|
13
24
|
|
|
25
|
+
/**
|
|
26
|
+
* @returns {SoundingMailMessage[]}
|
|
27
|
+
*/
|
|
14
28
|
all() {
|
|
15
29
|
return [...messages]
|
|
16
30
|
},
|
|
17
31
|
|
|
32
|
+
/**
|
|
33
|
+
* @returns {SoundingMailMessage | undefined}
|
|
34
|
+
*/
|
|
18
35
|
latest() {
|
|
19
36
|
return messages.at(-1)
|
|
20
37
|
},
|
|
21
38
|
|
|
39
|
+
/**
|
|
40
|
+
* @returns {void}
|
|
41
|
+
*/
|
|
22
42
|
clear() {
|
|
23
43
|
messages.length = 0
|
|
24
44
|
},
|