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-expect.js
CHANGED
|
@@ -1,21 +1,102 @@
|
|
|
1
1
|
const assert = require('node:assert/strict')
|
|
2
2
|
|
|
3
|
+
const { createSoundingError } = require('./create-error')
|
|
4
|
+
|
|
5
|
+
/** @typedef {import('./types').SoundingExpect} SoundingExpect */
|
|
6
|
+
/** @typedef {import('./types').SoundingExpectation} SoundingExpectation */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {any} target
|
|
10
|
+
* @param {string} path
|
|
11
|
+
* @returns {any}
|
|
12
|
+
*/
|
|
3
13
|
function getPath(target, path) {
|
|
4
14
|
return path.split('.').reduce((current, segment) => current?.[segment], target)
|
|
5
15
|
}
|
|
6
16
|
|
|
17
|
+
/**
|
|
18
|
+
* @param {any} headers
|
|
19
|
+
* @param {string} name
|
|
20
|
+
* @returns {any}
|
|
21
|
+
*/
|
|
22
|
+
function getHeaderValue(headers, name) {
|
|
23
|
+
if (!headers) {
|
|
24
|
+
return undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof headers.get === 'function') {
|
|
28
|
+
const value = headers.get(name)
|
|
29
|
+
return value === null ? undefined : value
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (headers[name] !== undefined) {
|
|
33
|
+
return headers[name]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const normalizedName = name.toLowerCase()
|
|
37
|
+
const matchingEntry = Object.entries(headers).find(
|
|
38
|
+
([key]) => key.toLowerCase() === normalizedName
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return matchingEntry?.[1]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {any} actual
|
|
46
|
+
* @param {string} name
|
|
47
|
+
* @returns {any}
|
|
48
|
+
*/
|
|
7
49
|
function getHeader(actual, name) {
|
|
8
50
|
if (typeof actual?.header === 'function') {
|
|
9
51
|
return actual.header(name)
|
|
10
52
|
}
|
|
11
53
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
54
|
+
return getHeaderValue(actual?.headers, name)
|
|
55
|
+
}
|
|
15
56
|
|
|
16
|
-
|
|
57
|
+
/**
|
|
58
|
+
* @param {any} actual
|
|
59
|
+
* @param {string} name
|
|
60
|
+
* @returns {any}
|
|
61
|
+
*/
|
|
62
|
+
function getRequestHeader(actual, name) {
|
|
63
|
+
return getHeaderValue(actual?.request?.headers, name)
|
|
17
64
|
}
|
|
18
65
|
|
|
66
|
+
/**
|
|
67
|
+
* @param {any} actual
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
function isMailbox(actual) {
|
|
71
|
+
return Boolean(
|
|
72
|
+
actual &&
|
|
73
|
+
typeof actual === 'object' &&
|
|
74
|
+
typeof actual.all === 'function' &&
|
|
75
|
+
typeof actual.latest === 'function'
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {any} actual
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
function isMailMessage(actual) {
|
|
84
|
+
return Boolean(
|
|
85
|
+
actual &&
|
|
86
|
+
typeof actual === 'object' &&
|
|
87
|
+
!Array.isArray(actual) &&
|
|
88
|
+
('to' in actual ||
|
|
89
|
+
'subject' in actual ||
|
|
90
|
+
'template' in actual ||
|
|
91
|
+
'ctaUrl' in actual ||
|
|
92
|
+
'status' in actual)
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {any} actual
|
|
98
|
+
* @returns {any}
|
|
99
|
+
*/
|
|
19
100
|
function resolveStructuredValue(actual) {
|
|
20
101
|
if (actual?.data !== undefined) {
|
|
21
102
|
return actual.data
|
|
@@ -24,7 +105,19 @@ function resolveStructuredValue(actual) {
|
|
|
24
105
|
return actual
|
|
25
106
|
}
|
|
26
107
|
|
|
108
|
+
/**
|
|
109
|
+
* @param {any} actual
|
|
110
|
+
* @returns {boolean}
|
|
111
|
+
*/
|
|
27
112
|
function shouldUseFallback(actual) {
|
|
113
|
+
if (typeof actual?.receive === 'function' && typeof actual?.events === 'function') {
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (isMailbox(actual) || isMailMessage(actual)) {
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
|
|
28
121
|
return Boolean(
|
|
29
122
|
actual &&
|
|
30
123
|
typeof actual === 'object' &&
|
|
@@ -36,6 +129,681 @@ function shouldUseFallback(actual) {
|
|
|
36
129
|
)
|
|
37
130
|
}
|
|
38
131
|
|
|
132
|
+
/**
|
|
133
|
+
* @param {any} actual
|
|
134
|
+
* @param {any} expected
|
|
135
|
+
* @returns {boolean}
|
|
136
|
+
*/
|
|
137
|
+
function partiallyMatches(actual, expected) {
|
|
138
|
+
if (expected instanceof RegExp) {
|
|
139
|
+
return expected.test(String(actual))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (typeof expected === 'function') {
|
|
143
|
+
return Boolean(expected(actual))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (Array.isArray(expected)) {
|
|
147
|
+
if (!Array.isArray(actual) || actual.length < expected.length) {
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return expected.every((entry, index) => partiallyMatches(actual[index], entry))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (expected && typeof expected === 'object') {
|
|
155
|
+
if (!actual || typeof actual !== 'object') {
|
|
156
|
+
return false
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return Object.entries(expected).every(([key, value]) => partiallyMatches(actual[key], value))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return Object.is(actual, expected)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @param {any} actual
|
|
167
|
+
* @param {any} expected
|
|
168
|
+
* @param {string} message
|
|
169
|
+
*/
|
|
170
|
+
function assertPartialMatch(actual, expected, message) {
|
|
171
|
+
if (expected === undefined) {
|
|
172
|
+
assert.notStrictEqual(actual, undefined)
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
assert.ok(partiallyMatches(actual, expected), message)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @returns {Error}
|
|
181
|
+
*/
|
|
182
|
+
function createResponseSessionUnavailableError() {
|
|
183
|
+
return createSoundingError({
|
|
184
|
+
code: 'E_SOUNDING_RESPONSE_SESSION_UNAVAILABLE',
|
|
185
|
+
name: 'SoundingExpectationError',
|
|
186
|
+
message:
|
|
187
|
+
'Sounding session assertions require a virtual request response. HTTP responses do not expose server-side session state, so use the virtual transport or assert cookies, headers, and follow-up behavior instead.',
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @param {any} actual
|
|
193
|
+
* @returns {any}
|
|
194
|
+
*/
|
|
195
|
+
function resolveResponseSession(actual) {
|
|
196
|
+
if (actual?.session && typeof actual.session === 'object' && !Array.isArray(actual.session)) {
|
|
197
|
+
return actual.session
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
throw createResponseSessionUnavailableError()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @param {any[]} messages
|
|
205
|
+
* @param {any} expected
|
|
206
|
+
* @returns {boolean}
|
|
207
|
+
*/
|
|
208
|
+
function flashMessagesMatch(messages, expected) {
|
|
209
|
+
if (expected === undefined) {
|
|
210
|
+
return messages.length > 0
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (Array.isArray(expected)) {
|
|
214
|
+
return partiallyMatches(messages, expected)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return messages.some((message) => partiallyMatches(message, expected))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @param {any} value
|
|
222
|
+
* @returns {string}
|
|
223
|
+
*/
|
|
224
|
+
function describeExpected(value) {
|
|
225
|
+
if (value instanceof RegExp) {
|
|
226
|
+
return String(value)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (typeof value === 'function') {
|
|
230
|
+
return value.name ? `[Function: ${value.name}]` : '[Function]'
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return JSON.stringify(value, (_key, nested) => {
|
|
234
|
+
if (nested instanceof RegExp) {
|
|
235
|
+
return String(nested)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (typeof nested === 'function') {
|
|
239
|
+
return nested.name ? `[Function: ${nested.name}]` : '[Function]'
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return nested
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @param {string} path
|
|
248
|
+
* @param {any} expected
|
|
249
|
+
* @returns {string}
|
|
250
|
+
*/
|
|
251
|
+
function formatExpectation(path, expected) {
|
|
252
|
+
if (expected === undefined) {
|
|
253
|
+
return `\`${path}\` to be present`
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return `\`${path}\` to match ${describeExpected(expected)}`
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @param {any} target
|
|
261
|
+
* @returns {any[]}
|
|
262
|
+
*/
|
|
263
|
+
function resolveMailboxMessages(target) {
|
|
264
|
+
if (isMailbox(target)) {
|
|
265
|
+
return target.all()
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
throw new TypeError('Sounding expect().toHaveSentMail() requires a Sounding mailbox.')
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* @param {any} target
|
|
273
|
+
* @returns {any}
|
|
274
|
+
*/
|
|
275
|
+
function resolveMailMessage(target) {
|
|
276
|
+
if (isMailMessage(target)) {
|
|
277
|
+
return target
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
throw new TypeError('Sounding expect().toHaveCtaUrl() requires a captured mail message.')
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* @param {any[]} actual
|
|
285
|
+
* @param {any} expected
|
|
286
|
+
* @returns {boolean}
|
|
287
|
+
*/
|
|
288
|
+
function listContainsPartial(actual, expected) {
|
|
289
|
+
if (Array.isArray(expected)) {
|
|
290
|
+
return partiallyMatches(actual, expected)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return actual.some((entry) => partiallyMatches(entry, expected))
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* @param {any} message
|
|
298
|
+
* @param {any} expected
|
|
299
|
+
* @returns {boolean}
|
|
300
|
+
*/
|
|
301
|
+
function mailMatches(message, expected = {}) {
|
|
302
|
+
if (typeof expected === 'function' || expected instanceof RegExp) {
|
|
303
|
+
return partiallyMatches(message, expected)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return Object.entries(expected).every(([key, value]) => {
|
|
307
|
+
const actualValue = getPath(message, key)
|
|
308
|
+
|
|
309
|
+
if (Array.isArray(actualValue)) {
|
|
310
|
+
return listContainsPartial(actualValue, value)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return partiallyMatches(actualValue, value)
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* @param {any} message
|
|
319
|
+
* @returns {any}
|
|
320
|
+
*/
|
|
321
|
+
function summarizeMailMessage(message) {
|
|
322
|
+
return {
|
|
323
|
+
to: message?.to,
|
|
324
|
+
subject: message?.subject,
|
|
325
|
+
template: message?.template,
|
|
326
|
+
status: message?.status,
|
|
327
|
+
ctaUrl: message?.ctaUrl,
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* @param {any[]} messages
|
|
333
|
+
* @returns {string}
|
|
334
|
+
*/
|
|
335
|
+
function summarizeMailMessages(messages) {
|
|
336
|
+
return describeExpected(messages.map(summarizeMailMessage))
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* @param {any} headers
|
|
341
|
+
* @returns {Array<[string, string]>}
|
|
342
|
+
*/
|
|
343
|
+
function getHeaderEntries(headers) {
|
|
344
|
+
if (!headers) {
|
|
345
|
+
return []
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (typeof headers.forEach === 'function') {
|
|
349
|
+
const entries = []
|
|
350
|
+
headers.forEach((value, key) => {
|
|
351
|
+
entries.push([key, value])
|
|
352
|
+
})
|
|
353
|
+
return entries
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return Object.entries(headers).map(([key, value]) => [key, String(value)])
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* @param {any} headers
|
|
361
|
+
* @returns {string}
|
|
362
|
+
*/
|
|
363
|
+
function summarizeHeaders(headers) {
|
|
364
|
+
const entries = getHeaderEntries(headers)
|
|
365
|
+
const limit = usesVerboseDiagnostics() ? entries.length : 6
|
|
366
|
+
const visibleEntries = entries.slice(0, limit).map(([key, value]) => `${key}: ${value}`)
|
|
367
|
+
|
|
368
|
+
if (entries.length > visibleEntries.length) {
|
|
369
|
+
visibleEntries.push(`... ${entries.length - visibleEntries.length} more`)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return visibleEntries.join(', ')
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* @param {string} value
|
|
377
|
+
* @param {number} maxLength
|
|
378
|
+
* @returns {string}
|
|
379
|
+
*/
|
|
380
|
+
function truncate(value, maxLength) {
|
|
381
|
+
if (value.length <= maxLength) {
|
|
382
|
+
return value
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return `${value.slice(0, maxLength)}...`
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* @param {any} actual
|
|
390
|
+
* @returns {string}
|
|
391
|
+
*/
|
|
392
|
+
function summarizeResponseBody(actual) {
|
|
393
|
+
const body = actual?.body || (actual?.data === undefined ? '' : describeExpected(actual.data))
|
|
394
|
+
const limit = usesVerboseDiagnostics() ? Infinity : 500
|
|
395
|
+
return truncate(String(body).replace(/\s+/g, ' ').trim(), limit)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* @returns {boolean}
|
|
400
|
+
*/
|
|
401
|
+
function usesVerboseDiagnostics() {
|
|
402
|
+
return process.env.SOUNDING_DIAGNOSTICS === 'verbose'
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* @param {any} actual
|
|
407
|
+
* @returns {string}
|
|
408
|
+
*/
|
|
409
|
+
function formatResponseDiagnostics(actual) {
|
|
410
|
+
const request = actual?.request
|
|
411
|
+
const hasResponseContext =
|
|
412
|
+
Boolean(request) || actual?.status !== undefined || actual?.url !== undefined
|
|
413
|
+
|
|
414
|
+
if (!hasResponseContext) {
|
|
415
|
+
return ''
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const lines = []
|
|
419
|
+
|
|
420
|
+
if (request) {
|
|
421
|
+
const transport = request.transport ? ` (${request.transport})` : ''
|
|
422
|
+
const url = request.url && request.url !== request.target ? ` -> ${request.url}` : ''
|
|
423
|
+
lines.push(`Request: ${request.method} ${request.target}${transport}${url}`)
|
|
424
|
+
const requestHeaders = summarizeHeaders(request.headers)
|
|
425
|
+
if (requestHeaders) {
|
|
426
|
+
lines.push(`Request headers: ${requestHeaders}`)
|
|
427
|
+
}
|
|
428
|
+
} else if (actual?.url) {
|
|
429
|
+
lines.push(`URL: ${actual.url}`)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (actual?.status !== undefined) {
|
|
433
|
+
const statusText = actual.statusText ? ` ${actual.statusText}` : ''
|
|
434
|
+
lines.push(`Response: ${actual.status}${statusText}`)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const headers = summarizeHeaders(actual?.headers)
|
|
438
|
+
if (headers) {
|
|
439
|
+
lines.push(`Headers: ${headers}`)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const body = summarizeResponseBody(actual)
|
|
443
|
+
if (body) {
|
|
444
|
+
lines.push(`Body: ${body}`)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (lines.length === 0) {
|
|
448
|
+
return ''
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return `\n\nSounding response diagnostics:\n${lines.map((line) => `- ${line}`).join('\n')}`
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* @param {string} message
|
|
456
|
+
* @param {any} actual
|
|
457
|
+
*/
|
|
458
|
+
function failWithResponseDiagnostics(message, actual) {
|
|
459
|
+
assert.fail(`${message}${formatResponseDiagnostics(actual)}`)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* @param {any} value
|
|
464
|
+
* @param {any} expected
|
|
465
|
+
* @param {string} message
|
|
466
|
+
* @param {any} actual
|
|
467
|
+
*/
|
|
468
|
+
function assertDeepEqualWithResponseDiagnostics(value, expected, message, actual) {
|
|
469
|
+
try {
|
|
470
|
+
assert.deepStrictEqual(value, expected)
|
|
471
|
+
} catch (_error) {
|
|
472
|
+
failWithResponseDiagnostics(message, actual)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* @param {any} actual
|
|
478
|
+
* @returns {any}
|
|
479
|
+
*/
|
|
480
|
+
function resolveInertiaPage(actual) {
|
|
481
|
+
return resolveStructuredValue(actual) || {}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* @param {any} actual
|
|
486
|
+
* @returns {any}
|
|
487
|
+
*/
|
|
488
|
+
function resolveInertiaProps(actual) {
|
|
489
|
+
return resolveInertiaPage(actual)?.props || {}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* @param {any} actual
|
|
494
|
+
* @returns {any}
|
|
495
|
+
*/
|
|
496
|
+
function resolveSharedInertiaProps(actual) {
|
|
497
|
+
const page = resolveInertiaPage(actual)
|
|
498
|
+
return page?.sharedProps || page?.shared || page?.props?.shared || page?.props || {}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* @param {any} actual
|
|
503
|
+
* @returns {any}
|
|
504
|
+
*/
|
|
505
|
+
function resolveInertiaErrors(actual) {
|
|
506
|
+
return resolveInertiaProps(actual)?.errors || {}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* @param {any} value
|
|
511
|
+
* @returns {boolean}
|
|
512
|
+
*/
|
|
513
|
+
function hasEntries(value) {
|
|
514
|
+
if (Array.isArray(value)) {
|
|
515
|
+
return value.length > 0
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (value && typeof value === 'object') {
|
|
519
|
+
return Object.keys(value).length > 0
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return Boolean(value)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* @param {any} source
|
|
527
|
+
* @param {string} path
|
|
528
|
+
* @param {any} expected
|
|
529
|
+
* @param {string} label
|
|
530
|
+
* @param {any} actual
|
|
531
|
+
*/
|
|
532
|
+
function assertInertiaPath(source, path, expected, label, actual) {
|
|
533
|
+
const value = getPath(source, path)
|
|
534
|
+
|
|
535
|
+
if (expected === undefined) {
|
|
536
|
+
if (value === undefined) {
|
|
537
|
+
failWithResponseDiagnostics(`Expected ${label} \`${path}\` to be present.`, actual)
|
|
538
|
+
}
|
|
539
|
+
return
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (!partiallyMatches(value, expected)) {
|
|
543
|
+
failWithResponseDiagnostics(
|
|
544
|
+
`Expected ${label} ${formatExpectation(path, expected)}, received ${describeExpected(value)}.`,
|
|
545
|
+
actual
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* @param {any} source
|
|
552
|
+
* @param {string} path
|
|
553
|
+
* @param {any} expected
|
|
554
|
+
* @param {string} label
|
|
555
|
+
* @param {any} actual
|
|
556
|
+
*/
|
|
557
|
+
function assertInertiaPathAbsent(source, path, expected, label, actual) {
|
|
558
|
+
const value = getPath(source, path)
|
|
559
|
+
|
|
560
|
+
if (expected === undefined) {
|
|
561
|
+
if (value !== undefined) {
|
|
562
|
+
failWithResponseDiagnostics(
|
|
563
|
+
`Expected ${label} \`${path}\` to be absent, received ${describeExpected(value)}.`,
|
|
564
|
+
actual
|
|
565
|
+
)
|
|
566
|
+
}
|
|
567
|
+
return
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (partiallyMatches(value, expected)) {
|
|
571
|
+
failWithResponseDiagnostics(
|
|
572
|
+
`Expected ${label} \`${path}\` not to match ${describeExpected(expected)}.`,
|
|
573
|
+
actual
|
|
574
|
+
)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* @param {any} source
|
|
580
|
+
* @param {Record<string, any>} expected
|
|
581
|
+
* @param {string} matcherName
|
|
582
|
+
* @param {string} pathLabel
|
|
583
|
+
* @param {any} actual
|
|
584
|
+
*/
|
|
585
|
+
function assertInertiaPathMap(source, expected, matcherName, pathLabel, actual) {
|
|
586
|
+
if (!expected || typeof expected !== 'object' || Array.isArray(expected)) {
|
|
587
|
+
throw new TypeError(`Sounding expect().${matcherName}() requires an object of prop paths.`)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
for (const [path, value] of Object.entries(expected)) {
|
|
591
|
+
assertInertiaPath(source, path, value, pathLabel, actual)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* @param {any} value
|
|
597
|
+
* @returns {number | null}
|
|
598
|
+
*/
|
|
599
|
+
function countCollection(value) {
|
|
600
|
+
if (Array.isArray(value)) {
|
|
601
|
+
return value.length
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (value && typeof value === 'object') {
|
|
605
|
+
return Object.keys(value).length
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return null
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* @param {any} actual
|
|
613
|
+
* @param {string} path
|
|
614
|
+
* @param {number} expected
|
|
615
|
+
*/
|
|
616
|
+
function assertInertiaPropCount(actual, path, expected) {
|
|
617
|
+
const value = getPath(resolveInertiaProps(actual), path)
|
|
618
|
+
const count = countCollection(value)
|
|
619
|
+
|
|
620
|
+
if (count === null) {
|
|
621
|
+
failWithResponseDiagnostics(
|
|
622
|
+
`Expected Inertia prop \`${path}\` to be an array or object with ${expected} item(s), received ${describeExpected(value)}.`,
|
|
623
|
+
actual
|
|
624
|
+
)
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (count !== expected) {
|
|
628
|
+
failWithResponseDiagnostics(
|
|
629
|
+
`Expected Inertia prop \`${path}\` to have ${expected} item(s), received ${count}.`,
|
|
630
|
+
actual
|
|
631
|
+
)
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* @param {any} actual
|
|
637
|
+
* @param {string[]} expected
|
|
638
|
+
*/
|
|
639
|
+
function assertOnlyInertiaProps(actual, expected) {
|
|
640
|
+
if (!Array.isArray(expected)) {
|
|
641
|
+
throw new TypeError('Sounding expect().toHaveOnlyInertiaProps() requires an array of top-level prop names.')
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const actualKeys = Object.keys(resolveInertiaProps(actual)).sort()
|
|
645
|
+
const expectedKeys = [...expected].sort()
|
|
646
|
+
|
|
647
|
+
assertDeepEqualWithResponseDiagnostics(
|
|
648
|
+
actualKeys,
|
|
649
|
+
expectedKeys,
|
|
650
|
+
`Expected Inertia props to include only ${describeExpected(expectedKeys)}, received ${describeExpected(actualKeys)}.`,
|
|
651
|
+
actual
|
|
652
|
+
)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* @param {string | string[]} value
|
|
657
|
+
* @returns {string[]}
|
|
658
|
+
*/
|
|
659
|
+
function normalizeHeaderList(value) {
|
|
660
|
+
if (Array.isArray(value)) {
|
|
661
|
+
return value.map(String)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return String(value || '')
|
|
665
|
+
.split(',')
|
|
666
|
+
.map((entry) => entry.trim())
|
|
667
|
+
.filter(Boolean)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* @param {any} actual
|
|
672
|
+
* @param {string} name
|
|
673
|
+
* @param {string[]} expected
|
|
674
|
+
* @param {string} label
|
|
675
|
+
*/
|
|
676
|
+
function assertPartialReloadList(actual, name, expected, label) {
|
|
677
|
+
if (!Array.isArray(expected)) {
|
|
678
|
+
throw new TypeError(`Sounding expect().toHaveInertiaPartialReload() requires \`${label}\` to be an array.`)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const value = getRequestHeader(actual, name)
|
|
682
|
+
const actualList = normalizeHeaderList(value)
|
|
683
|
+
|
|
684
|
+
assertDeepEqualWithResponseDiagnostics(
|
|
685
|
+
actualList,
|
|
686
|
+
expected,
|
|
687
|
+
`Expected Inertia partial reload \`${label}\` to equal ${describeExpected(expected)}, received ${describeExpected(actualList)}.`,
|
|
688
|
+
actual
|
|
689
|
+
)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* @param {any} actual
|
|
694
|
+
* @param {{ component?: string, only?: string[], except?: string[], reset?: string[], version?: string, errorBag?: string }} [expected]
|
|
695
|
+
*/
|
|
696
|
+
function assertInertiaPartialReload(actual, expected = {}) {
|
|
697
|
+
const headerNames = [
|
|
698
|
+
'x-inertia-partial-component',
|
|
699
|
+
'x-inertia-partial-data',
|
|
700
|
+
'x-inertia-partial-except',
|
|
701
|
+
'x-inertia-reset',
|
|
702
|
+
]
|
|
703
|
+
const hasPartialReloadHeader = headerNames.some((name) => getRequestHeader(actual, name) !== undefined)
|
|
704
|
+
|
|
705
|
+
if (!hasPartialReloadHeader) {
|
|
706
|
+
failWithResponseDiagnostics('Expected request to include Inertia partial reload headers.', actual)
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (expected.component !== undefined) {
|
|
710
|
+
const component = getRequestHeader(actual, 'x-inertia-partial-component')
|
|
711
|
+
if (component !== expected.component) {
|
|
712
|
+
failWithResponseDiagnostics(
|
|
713
|
+
`Expected Inertia partial reload component ${describeExpected(expected.component)}, received ${describeExpected(component)}.`,
|
|
714
|
+
actual
|
|
715
|
+
)
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (expected.only !== undefined) {
|
|
720
|
+
assertPartialReloadList(actual, 'x-inertia-partial-data', expected.only, 'only')
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (expected.except !== undefined) {
|
|
724
|
+
assertPartialReloadList(actual, 'x-inertia-partial-except', expected.except, 'except')
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (expected.reset !== undefined) {
|
|
728
|
+
assertPartialReloadList(actual, 'x-inertia-reset', expected.reset, 'reset')
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (expected.version !== undefined) {
|
|
732
|
+
const version = getRequestHeader(actual, 'x-inertia-version')
|
|
733
|
+
if (version !== expected.version) {
|
|
734
|
+
failWithResponseDiagnostics(
|
|
735
|
+
`Expected Inertia version ${describeExpected(expected.version)}, received ${describeExpected(version)}.`,
|
|
736
|
+
actual
|
|
737
|
+
)
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (expected.errorBag !== undefined) {
|
|
742
|
+
const errorBag = getRequestHeader(actual, 'x-inertia-error-bag')
|
|
743
|
+
if (errorBag !== expected.errorBag) {
|
|
744
|
+
failWithResponseDiagnostics(
|
|
745
|
+
`Expected Inertia error bag ${describeExpected(expected.errorBag)}, received ${describeExpected(errorBag)}.`,
|
|
746
|
+
actual
|
|
747
|
+
)
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* @param {any} actual
|
|
754
|
+
* @param {string | string[] | Record<string, any>} [expected]
|
|
755
|
+
*/
|
|
756
|
+
function assertInertiaErrors(actual, expected) {
|
|
757
|
+
const errors = resolveInertiaErrors(actual)
|
|
758
|
+
|
|
759
|
+
if (expected === undefined) {
|
|
760
|
+
if (!hasEntries(errors)) {
|
|
761
|
+
failWithResponseDiagnostics('Expected Inertia validation errors to be present.', actual)
|
|
762
|
+
}
|
|
763
|
+
return
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (typeof expected === 'string') {
|
|
767
|
+
assertInertiaPath(errors, expected, undefined, 'Inertia validation error', actual)
|
|
768
|
+
return
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (Array.isArray(expected)) {
|
|
772
|
+
for (const path of expected) {
|
|
773
|
+
assertInertiaPath(errors, path, undefined, 'Inertia validation error', actual)
|
|
774
|
+
}
|
|
775
|
+
return
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (expected && typeof expected === 'object') {
|
|
779
|
+
for (const [path, value] of Object.entries(expected)) {
|
|
780
|
+
assertInertiaPath(errors, path, value, 'Inertia validation error', actual)
|
|
781
|
+
}
|
|
782
|
+
return
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
throw new TypeError('Sounding expect().toHaveInertiaErrors() requires a string, array, object, or no argument.')
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* @param {any} actual
|
|
790
|
+
*/
|
|
791
|
+
function assertNoInertiaErrors(actual) {
|
|
792
|
+
const errors = resolveInertiaErrors(actual)
|
|
793
|
+
|
|
794
|
+
if (hasEntries(errors)) {
|
|
795
|
+
failWithResponseDiagnostics(
|
|
796
|
+
`Expected Inertia validation errors to be empty, received ${describeExpected(errors)}.`,
|
|
797
|
+
actual
|
|
798
|
+
)
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* @param {any} actual
|
|
804
|
+
* @param {{ fallback?: (actual: any) => any }} [options]
|
|
805
|
+
* @returns {SoundingExpectation | any}
|
|
806
|
+
*/
|
|
39
807
|
function createExpect(actual, { fallback } = {}) {
|
|
40
808
|
if (fallback && shouldUseFallback(actual)) {
|
|
41
809
|
return fallback(actual)
|
|
@@ -86,70 +854,345 @@ function createExpect(actual, { fallback } = {}) {
|
|
|
86
854
|
},
|
|
87
855
|
|
|
88
856
|
toHaveStatus(expected) {
|
|
89
|
-
|
|
857
|
+
if (actual?.status !== expected) {
|
|
858
|
+
failWithResponseDiagnostics(
|
|
859
|
+
`Expected response status ${expected}, received ${describeExpected(actual?.status)}.`,
|
|
860
|
+
actual
|
|
861
|
+
)
|
|
862
|
+
}
|
|
90
863
|
},
|
|
91
864
|
|
|
92
865
|
toHaveHeader(name, expected) {
|
|
93
866
|
const header = getHeader(actual, name)
|
|
94
|
-
|
|
95
|
-
|
|
867
|
+
if (header === null || header === undefined) {
|
|
868
|
+
failWithResponseDiagnostics(`Expected response header \`${name}\` to be present.`, actual)
|
|
869
|
+
}
|
|
96
870
|
|
|
97
|
-
if (expected !== undefined) {
|
|
98
|
-
|
|
871
|
+
if (expected !== undefined && header !== expected) {
|
|
872
|
+
failWithResponseDiagnostics(
|
|
873
|
+
`Expected response header \`${name}\` to equal ${describeExpected(expected)}, received ${describeExpected(header)}.`,
|
|
874
|
+
actual
|
|
875
|
+
)
|
|
99
876
|
}
|
|
100
877
|
},
|
|
101
878
|
|
|
102
879
|
toRedirectTo(expected) {
|
|
103
880
|
const location = getHeader(actual, 'location')
|
|
104
|
-
|
|
881
|
+
if (location !== expected) {
|
|
882
|
+
failWithResponseDiagnostics(
|
|
883
|
+
`Expected response to redirect to ${describeExpected(expected)}, received ${describeExpected(location)}.`,
|
|
884
|
+
actual
|
|
885
|
+
)
|
|
886
|
+
}
|
|
105
887
|
},
|
|
106
888
|
|
|
107
889
|
toHaveJsonPath(path, expected) {
|
|
108
890
|
const value = getPath(resolveStructuredValue(actual), path)
|
|
109
|
-
|
|
891
|
+
assertDeepEqualWithResponseDiagnostics(
|
|
892
|
+
value,
|
|
893
|
+
expected,
|
|
894
|
+
`Expected JSON path ${formatExpectation(path, expected)}, received ${describeExpected(value)}.`,
|
|
895
|
+
actual
|
|
896
|
+
)
|
|
897
|
+
},
|
|
898
|
+
|
|
899
|
+
toHaveSentCount(expected) {
|
|
900
|
+
const messages = resolveMailboxMessages(actual)
|
|
901
|
+
assert.strictEqual(
|
|
902
|
+
messages.length,
|
|
903
|
+
expected,
|
|
904
|
+
`Expected mailbox to have sent ${expected} message(s), received ${messages.length}. Captured mail: ${summarizeMailMessages(messages)}.`
|
|
905
|
+
)
|
|
906
|
+
},
|
|
907
|
+
|
|
908
|
+
toHaveSentMail(expected = {}) {
|
|
909
|
+
const messages = resolveMailboxMessages(actual)
|
|
910
|
+
|
|
911
|
+
assert.ok(
|
|
912
|
+
messages.some((message) => mailMatches(message, expected)),
|
|
913
|
+
`Expected mailbox to have sent mail matching ${describeExpected(expected)}. Captured mail: ${summarizeMailMessages(messages)}.`
|
|
914
|
+
)
|
|
915
|
+
},
|
|
916
|
+
|
|
917
|
+
toHaveCtaUrl(expected) {
|
|
918
|
+
const message = resolveMailMessage(actual)
|
|
919
|
+
const ctaUrl = message.ctaUrl
|
|
920
|
+
|
|
921
|
+
if (expected === undefined) {
|
|
922
|
+
assert.notStrictEqual(ctaUrl, undefined, 'Expected captured mail to have a CTA URL.')
|
|
923
|
+
return
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
assertPartialMatch(
|
|
927
|
+
ctaUrl,
|
|
928
|
+
expected,
|
|
929
|
+
`Expected captured mail CTA URL to match ${describeExpected(expected)}, received ${describeExpected(ctaUrl)}.`
|
|
930
|
+
)
|
|
931
|
+
},
|
|
932
|
+
|
|
933
|
+
toHaveSession(path, expected) {
|
|
934
|
+
const session = resolveResponseSession(actual)
|
|
935
|
+
const value = getPath(session, path)
|
|
936
|
+
|
|
937
|
+
if (expected === undefined) {
|
|
938
|
+
assert.notStrictEqual(value, undefined, `Expected session ${formatExpectation(path)}.`)
|
|
939
|
+
return
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
assertPartialMatch(
|
|
943
|
+
value,
|
|
944
|
+
expected,
|
|
945
|
+
`Expected session ${formatExpectation(path, expected)}, received ${describeExpected(value)}.`
|
|
946
|
+
)
|
|
947
|
+
},
|
|
948
|
+
|
|
949
|
+
toHaveFlash(type, expected) {
|
|
950
|
+
const session = resolveResponseSession(actual)
|
|
951
|
+
const messages = session.__soundingFlashStore?.[type] || []
|
|
952
|
+
|
|
953
|
+
assert.ok(
|
|
954
|
+
flashMessagesMatch(messages, expected),
|
|
955
|
+
expected === undefined
|
|
956
|
+
? `Expected flash \`${type}\` to be present.`
|
|
957
|
+
: `Expected flash \`${type}\` to match ${describeExpected(expected)}, received ${describeExpected(messages)}.`
|
|
958
|
+
)
|
|
110
959
|
},
|
|
111
960
|
|
|
112
961
|
toBeInertiaPage(component) {
|
|
113
962
|
const value = resolveStructuredValue(actual)
|
|
114
|
-
|
|
963
|
+
if (value?.component !== component) {
|
|
964
|
+
failWithResponseDiagnostics(
|
|
965
|
+
`Expected Inertia component ${describeExpected(component)}, received ${describeExpected(value?.component)}.`,
|
|
966
|
+
actual
|
|
967
|
+
)
|
|
968
|
+
}
|
|
969
|
+
},
|
|
970
|
+
|
|
971
|
+
toHaveInertiaProp(path, expected) {
|
|
972
|
+
assertInertiaPath(resolveInertiaProps(actual), path, expected, 'Inertia prop', actual)
|
|
973
|
+
},
|
|
974
|
+
|
|
975
|
+
toHaveInertiaProps(expected) {
|
|
976
|
+
assertInertiaPathMap(
|
|
977
|
+
resolveInertiaProps(actual),
|
|
978
|
+
expected,
|
|
979
|
+
'toHaveInertiaProps',
|
|
980
|
+
'Inertia prop',
|
|
981
|
+
actual
|
|
982
|
+
)
|
|
983
|
+
},
|
|
984
|
+
|
|
985
|
+
toHaveInertiaPropCount(path, expected) {
|
|
986
|
+
assertInertiaPropCount(actual, path, expected)
|
|
115
987
|
},
|
|
116
988
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
assert.deepStrictEqual(value, expected)
|
|
989
|
+
toHaveOnlyInertiaProps(expected) {
|
|
990
|
+
assertOnlyInertiaProps(actual, expected)
|
|
120
991
|
},
|
|
121
992
|
|
|
122
|
-
|
|
123
|
-
const value = getPath(
|
|
993
|
+
toMatchInertiaProp(path, expected) {
|
|
994
|
+
const value = getPath(resolveInertiaProps(actual), path)
|
|
124
995
|
|
|
125
996
|
if (expected instanceof RegExp) {
|
|
126
|
-
|
|
997
|
+
if (!expected.test(String(value))) {
|
|
998
|
+
failWithResponseDiagnostics(
|
|
999
|
+
`Expected Inertia prop ${formatExpectation(path, expected)}, received ${describeExpected(value)}.`,
|
|
1000
|
+
actual
|
|
1001
|
+
)
|
|
1002
|
+
}
|
|
127
1003
|
return
|
|
128
1004
|
}
|
|
129
1005
|
|
|
130
|
-
|
|
1006
|
+
if (!String(value).includes(String(expected))) {
|
|
1007
|
+
failWithResponseDiagnostics(
|
|
1008
|
+
`Expected Inertia prop \`${path}\` to include ${describeExpected(expected)}, received ${describeExpected(value)}.`,
|
|
1009
|
+
actual
|
|
1010
|
+
)
|
|
1011
|
+
}
|
|
1012
|
+
},
|
|
1013
|
+
|
|
1014
|
+
toHaveSharedInertiaProp(path, expected) {
|
|
1015
|
+
assertInertiaPath(
|
|
1016
|
+
resolveSharedInertiaProps(actual),
|
|
1017
|
+
path,
|
|
1018
|
+
expected,
|
|
1019
|
+
'shared Inertia prop',
|
|
1020
|
+
actual
|
|
1021
|
+
)
|
|
1022
|
+
},
|
|
1023
|
+
|
|
1024
|
+
toHaveSharedInertiaProps(expected) {
|
|
1025
|
+
assertInertiaPathMap(
|
|
1026
|
+
resolveSharedInertiaProps(actual),
|
|
1027
|
+
expected,
|
|
1028
|
+
'toHaveSharedInertiaProps',
|
|
1029
|
+
'shared Inertia prop',
|
|
1030
|
+
actual
|
|
1031
|
+
)
|
|
1032
|
+
},
|
|
1033
|
+
|
|
1034
|
+
toHaveInertiaError(path, expected) {
|
|
1035
|
+
assertInertiaPath(
|
|
1036
|
+
resolveInertiaErrors(actual),
|
|
1037
|
+
path,
|
|
1038
|
+
expected,
|
|
1039
|
+
'Inertia validation error',
|
|
1040
|
+
actual
|
|
1041
|
+
)
|
|
1042
|
+
},
|
|
1043
|
+
|
|
1044
|
+
toHaveInertiaErrors(expected) {
|
|
1045
|
+
assertInertiaErrors(actual, expected)
|
|
131
1046
|
},
|
|
132
1047
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
assert.deepStrictEqual(value, expected)
|
|
1048
|
+
toHaveNoInertiaErrors() {
|
|
1049
|
+
assertNoInertiaErrors(actual)
|
|
136
1050
|
},
|
|
137
1051
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
1052
|
+
toHaveInertiaPartialReload(expected) {
|
|
1053
|
+
assertInertiaPartialReload(actual, expected)
|
|
1054
|
+
},
|
|
1055
|
+
|
|
1056
|
+
async toReceive(event, expected, options) {
|
|
1057
|
+
if (typeof actual?.receive !== 'function') {
|
|
1058
|
+
throw new TypeError('Sounding expect().toReceive() requires a Sounding socket client.')
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const payload = await actual.receive(event, options)
|
|
1062
|
+
assertPartialMatch(
|
|
1063
|
+
payload,
|
|
1064
|
+
expected,
|
|
1065
|
+
`Expected socket event \`${event}\` to match ${JSON.stringify(expected)}, received ${JSON.stringify(payload)}.`
|
|
1066
|
+
)
|
|
1067
|
+
},
|
|
1068
|
+
|
|
1069
|
+
toHaveReceived(event, expected) {
|
|
1070
|
+
if (typeof actual?.events !== 'function') {
|
|
1071
|
+
throw new TypeError('Sounding expect().toHaveReceived() requires a Sounding socket client.')
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const payloads = actual.events(event)
|
|
1075
|
+
assert.ok(payloads.length > 0, `Expected socket to have received \`${event}\`.`)
|
|
141
1076
|
|
|
142
1077
|
if (expected !== undefined) {
|
|
143
|
-
assert.
|
|
1078
|
+
assert.ok(
|
|
1079
|
+
payloads.some((payload) => partiallyMatches(payload, expected)),
|
|
1080
|
+
`Expected received socket event \`${event}\` to match ${JSON.stringify(expected)}.`
|
|
1081
|
+
)
|
|
144
1082
|
}
|
|
145
1083
|
},
|
|
1084
|
+
|
|
1085
|
+
not: {
|
|
1086
|
+
toHaveSentMail(expected = {}) {
|
|
1087
|
+
const messages = resolveMailboxMessages(actual)
|
|
1088
|
+
|
|
1089
|
+
assert.ok(
|
|
1090
|
+
!messages.some((message) => mailMatches(message, expected)),
|
|
1091
|
+
`Expected mailbox not to have sent mail matching ${describeExpected(expected)}. Captured mail: ${summarizeMailMessages(messages)}.`
|
|
1092
|
+
)
|
|
1093
|
+
},
|
|
1094
|
+
|
|
1095
|
+
toHaveCtaUrl(expected) {
|
|
1096
|
+
const message = resolveMailMessage(actual)
|
|
1097
|
+
const ctaUrl = message.ctaUrl
|
|
1098
|
+
|
|
1099
|
+
if (expected === undefined) {
|
|
1100
|
+
assert.strictEqual(ctaUrl, undefined, 'Expected captured mail not to have a CTA URL.')
|
|
1101
|
+
return
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
assert.ok(
|
|
1105
|
+
!partiallyMatches(ctaUrl, expected),
|
|
1106
|
+
`Expected captured mail CTA URL not to match ${describeExpected(expected)}.`
|
|
1107
|
+
)
|
|
1108
|
+
},
|
|
1109
|
+
|
|
1110
|
+
toHaveSession(path, expected) {
|
|
1111
|
+
const session = resolveResponseSession(actual)
|
|
1112
|
+
const value = getPath(session, path)
|
|
1113
|
+
|
|
1114
|
+
if (expected === undefined) {
|
|
1115
|
+
assert.strictEqual(value, undefined, `Expected session not to include \`${path}\`.`)
|
|
1116
|
+
return
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
assert.ok(
|
|
1120
|
+
!partiallyMatches(value, expected),
|
|
1121
|
+
`Expected session \`${path}\` not to match ${describeExpected(expected)}.`
|
|
1122
|
+
)
|
|
1123
|
+
},
|
|
1124
|
+
|
|
1125
|
+
toHaveFlash(type, expected) {
|
|
1126
|
+
const session = resolveResponseSession(actual)
|
|
1127
|
+
const messages = session.__soundingFlashStore?.[type] || []
|
|
1128
|
+
|
|
1129
|
+
assert.ok(
|
|
1130
|
+
!flashMessagesMatch(messages, expected),
|
|
1131
|
+
expected === undefined
|
|
1132
|
+
? `Expected flash \`${type}\` not to be present.`
|
|
1133
|
+
: `Expected flash \`${type}\` not to match ${describeExpected(expected)}.`
|
|
1134
|
+
)
|
|
1135
|
+
},
|
|
1136
|
+
|
|
1137
|
+
toHaveInertiaProp(path, expected) {
|
|
1138
|
+
assertInertiaPathAbsent(resolveInertiaProps(actual), path, expected, 'Inertia prop', actual)
|
|
1139
|
+
},
|
|
1140
|
+
|
|
1141
|
+
toHaveSharedInertiaProp(path, expected) {
|
|
1142
|
+
assertInertiaPathAbsent(
|
|
1143
|
+
resolveSharedInertiaProps(actual),
|
|
1144
|
+
path,
|
|
1145
|
+
expected,
|
|
1146
|
+
'shared Inertia prop',
|
|
1147
|
+
actual
|
|
1148
|
+
)
|
|
1149
|
+
},
|
|
1150
|
+
|
|
1151
|
+
toHaveInertiaError(path, expected) {
|
|
1152
|
+
assertInertiaPathAbsent(
|
|
1153
|
+
resolveInertiaErrors(actual),
|
|
1154
|
+
path,
|
|
1155
|
+
expected,
|
|
1156
|
+
'Inertia validation error',
|
|
1157
|
+
actual
|
|
1158
|
+
)
|
|
1159
|
+
},
|
|
1160
|
+
|
|
1161
|
+
async toReceive(event, expected, options = {}) {
|
|
1162
|
+
if (typeof actual?.receive !== 'function') {
|
|
1163
|
+
throw new TypeError('Sounding expect().not.toReceive() requires a Sounding socket client.')
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const timeout = options.timeout || 50
|
|
1167
|
+
|
|
1168
|
+
try {
|
|
1169
|
+
const payload = await actual.receive(event, { ...options, timeout })
|
|
1170
|
+
if (expected === undefined || partiallyMatches(payload, expected)) {
|
|
1171
|
+
assert.fail(`Expected socket not to receive \`${event}\`, but it did.`)
|
|
1172
|
+
}
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
if (error?.code === 'E_SOUNDING_SOCKET_EVENT_TIMEOUT') {
|
|
1175
|
+
return
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
throw error
|
|
1179
|
+
}
|
|
1180
|
+
},
|
|
1181
|
+
},
|
|
146
1182
|
}
|
|
147
1183
|
}
|
|
148
1184
|
|
|
1185
|
+
/**
|
|
1186
|
+
* @param {(actual: any) => any} fallback
|
|
1187
|
+
* @returns {SoundingExpect}
|
|
1188
|
+
*/
|
|
149
1189
|
createExpect.withFallback = function withFallback(fallback) {
|
|
150
|
-
|
|
1190
|
+
function soundingExpect(actual) {
|
|
151
1191
|
return createExpect(actual, { fallback })
|
|
152
1192
|
}
|
|
1193
|
+
|
|
1194
|
+
soundingExpect.withFallback = createExpect.withFallback
|
|
1195
|
+
return soundingExpect
|
|
153
1196
|
}
|
|
154
1197
|
|
|
155
1198
|
module.exports = { createExpect }
|