@uniflowed/test 0.0.0-alpha.1 → 0.0.0-alpha.4
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/index.js +19 -2
- package/internal/asymmetric.js +184 -0
- package/internal/equality.js +79 -19
- package/internal/expect.js +294 -81
- package/internal/frames.js +9 -3
- package/internal/namespace.js +251 -0
- package/internal/output.js +260 -0
- package/internal/registry.js +30 -27
- package/internal/run.js +44 -21
- package/internal/snapshot.js +306 -0
- package/internal/spy.js +236 -0
- package/internal/timers.js +362 -0
- package/package.json +5 -2
- package/worker.js +62 -15
package/internal/expect.js
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
// matcher table to what came out, so `await expect(p).resolves.toBe(1)` reads
|
|
13
13
|
// the way the synchronous form does.
|
|
14
14
|
|
|
15
|
+
import type { SpyCall } from "./spy.js";
|
|
16
|
+
import * as asymmetric from "./asymmetric.js";
|
|
17
|
+
import * as snapshot from "./snapshot.js";
|
|
18
|
+
import { isSpy } from "./spy.js";
|
|
15
19
|
import { equals, matchesObject, render } from "./equality.js";
|
|
16
20
|
|
|
17
21
|
/** Thrown when a matcher does not hold. */
|
|
@@ -34,78 +38,26 @@ export class AssertionError extends Error {
|
|
|
34
38
|
|
|
35
39
|
/** What a matcher decided, and how to say it either way. */
|
|
36
40
|
type Verdict = {|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
readonly pass: boolean,
|
|
42
|
+
readonly failure: () => string,
|
|
43
|
+
readonly negatedFailure: () => string,
|
|
44
|
+
readonly expected?: string,
|
|
45
|
+
readonly received?: string,
|
|
42
46
|
|};
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
+threw?: mixed,
|
|
49
|
-
|};
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* A spy that records its calls.
|
|
53
|
-
*
|
|
54
|
-
* `fn()` records and returns `undefined`; `fn(implementation)` records and
|
|
55
|
-
* delegates. A throw is recorded and then re-thrown, so wrapping a function in
|
|
56
|
-
* a spy never changes whether the code under test fails.
|
|
57
|
-
*/
|
|
58
|
-
export function fn(implementation?: (...args: $ReadOnlyArray<mixed>) => mixed): $FlowFixMe {
|
|
59
|
-
const calls: Array<SpyCall> = [];
|
|
60
|
-
let current = implementation;
|
|
61
|
-
|
|
62
|
-
const spy: $FlowFixMe = (...args: $ReadOnlyArray<mixed>) => {
|
|
63
|
-
try {
|
|
64
|
-
const returned = current == null ? undefined : current(...args);
|
|
65
|
-
calls.push({ args, returned });
|
|
66
|
-
return returned;
|
|
67
|
-
} catch (thrown) {
|
|
68
|
-
calls.push({ args, threw: thrown });
|
|
69
|
-
throw thrown;
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
spy.mock = { calls };
|
|
73
|
-
spy.mockClear = () => {
|
|
74
|
-
calls.length = 0;
|
|
75
|
-
};
|
|
76
|
-
spy.mockReturnValue = (value: mixed) => {
|
|
77
|
-
current = () => value;
|
|
78
|
-
return spy;
|
|
79
|
-
};
|
|
80
|
-
spy.mockResolvedValue = (value: mixed) => {
|
|
81
|
-
current = () => Promise.resolve(value);
|
|
82
|
-
return spy;
|
|
83
|
-
};
|
|
84
|
-
spy.mockRejectedValue = (reason: mixed) => {
|
|
85
|
-
current = () => Promise.reject(reason);
|
|
86
|
-
return spy;
|
|
87
|
-
};
|
|
88
|
-
spy.mockImplementation = (next: (...args: $ReadOnlyArray<mixed>) => mixed) => {
|
|
89
|
-
current = next;
|
|
90
|
-
return spy;
|
|
91
|
-
};
|
|
92
|
-
return spy;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function isSpy(value: mixed): boolean {
|
|
96
|
-
return typeof value === "function" && (value: $FlowFixMe).mock != null;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function propertyAt(value: mixed, path: string): {| +found: boolean, +value: mixed |} {
|
|
48
|
+
function propertyAt(
|
|
49
|
+
value: mixed,
|
|
50
|
+
path: string,
|
|
51
|
+
): {| readonly found: boolean, readonly value: mixed |} {
|
|
100
52
|
let current = value;
|
|
101
53
|
for (const key of path.split(".")) {
|
|
102
54
|
if (current == null) {
|
|
103
55
|
return { found: false, value: undefined };
|
|
104
56
|
}
|
|
105
|
-
if (!Object.prototype.hasOwnProperty.call(
|
|
57
|
+
if (!Object.prototype.hasOwnProperty.call(current as $FlowFixMe, key)) {
|
|
106
58
|
return { found: false, value: undefined };
|
|
107
59
|
}
|
|
108
|
-
current = (current
|
|
60
|
+
current = (current as $FlowFixMe)[key];
|
|
109
61
|
}
|
|
110
62
|
return { found: true, value: current };
|
|
111
63
|
}
|
|
@@ -126,7 +78,7 @@ function matchesThrown(thrown: mixed, expected: mixed): boolean {
|
|
|
126
78
|
return message === expected.message;
|
|
127
79
|
}
|
|
128
80
|
if (typeof expected === "function") {
|
|
129
|
-
return thrown instanceof (expected
|
|
81
|
+
return thrown instanceof (expected as $FlowFixMe);
|
|
130
82
|
}
|
|
131
83
|
return equals(thrown, expected);
|
|
132
84
|
}
|
|
@@ -137,7 +89,9 @@ function matchesThrown(thrown: mixed, expected: mixed): boolean {
|
|
|
137
89
|
* Every entry returns a [`Verdict`] rather than throwing, which is what lets
|
|
138
90
|
* `.not` reuse all of them.
|
|
139
91
|
*/
|
|
140
|
-
function verdicts(received: mixed): {
|
|
92
|
+
function verdicts(received: mixed): {
|
|
93
|
+
readonly [string]: (...args: $ReadOnlyArray<any>) => Verdict,
|
|
94
|
+
} {
|
|
141
95
|
const shown = () => render(received);
|
|
142
96
|
const simple = (pass: boolean, what: string, expected?: mixed): Verdict => ({
|
|
143
97
|
pass,
|
|
@@ -146,7 +100,8 @@ function verdicts(received: mixed): { +[string]: (...args: $ReadOnlyArray<any>)
|
|
|
146
100
|
failure: () => `expected ${shown()} ${what}`,
|
|
147
101
|
negatedFailure: () => `expected ${shown()} not ${what}`,
|
|
148
102
|
});
|
|
149
|
-
const spyCalls = (): Array<SpyCall> =>
|
|
103
|
+
const spyCalls = (): Array<SpyCall> =>
|
|
104
|
+
isSpy(received) ? (received as $FlowFixMe).mock.calls : [];
|
|
150
105
|
const requireSpy = (matcher: string) => {
|
|
151
106
|
if (!isSpy(received)) {
|
|
152
107
|
throw new AssertionError(
|
|
@@ -187,17 +142,33 @@ function verdicts(received: mixed): { +[string]: (...args: $ReadOnlyArray<any>)
|
|
|
187
142
|
toBeDefined: () => simple(received !== undefined, "to be defined"),
|
|
188
143
|
toBeNaN: () => simple(typeof received === "number" && Number.isNaN(received), "to be NaN"),
|
|
189
144
|
toBeGreaterThan: (expected: mixed) =>
|
|
190
|
-
simple(
|
|
145
|
+
simple(
|
|
146
|
+
(received as $FlowFixMe) > (expected as $FlowFixMe),
|
|
147
|
+
`to be greater than ${render(expected)}`,
|
|
148
|
+
expected,
|
|
149
|
+
),
|
|
191
150
|
toBeGreaterThanOrEqual: (expected: mixed) =>
|
|
192
|
-
simple(
|
|
151
|
+
simple(
|
|
152
|
+
(received as $FlowFixMe) >= (expected as $FlowFixMe),
|
|
153
|
+
`to be at least ${render(expected)}`,
|
|
154
|
+
expected,
|
|
155
|
+
),
|
|
193
156
|
toBeLessThan: (expected: mixed) =>
|
|
194
|
-
simple(
|
|
157
|
+
simple(
|
|
158
|
+
(received as $FlowFixMe) < (expected as $FlowFixMe),
|
|
159
|
+
`to be less than ${render(expected)}`,
|
|
160
|
+
expected,
|
|
161
|
+
),
|
|
195
162
|
toBeLessThanOrEqual: (expected: mixed) =>
|
|
196
|
-
simple(
|
|
163
|
+
simple(
|
|
164
|
+
(received as $FlowFixMe) <= (expected as $FlowFixMe),
|
|
165
|
+
`to be at most ${render(expected)}`,
|
|
166
|
+
expected,
|
|
167
|
+
),
|
|
197
168
|
toBeCloseTo: (expected: number, digits?: number) => {
|
|
198
169
|
const places = digits ?? 2;
|
|
199
170
|
const tolerance = 10 ** -places / 2;
|
|
200
|
-
const difference = Math.abs((received
|
|
171
|
+
const difference = Math.abs((received as $FlowFixMe) - expected);
|
|
201
172
|
return simple(
|
|
202
173
|
difference < tolerance,
|
|
203
174
|
`to be within ${tolerance} of ${expected}, but it is off by ${difference}`,
|
|
@@ -216,7 +187,11 @@ function verdicts(received: mixed): { +[string]: (...args: $ReadOnlyArray<any>)
|
|
|
216
187
|
return simple(pass, `to contain ${render(expected)}`, expected);
|
|
217
188
|
},
|
|
218
189
|
toContainEqual: (expected: mixed) => {
|
|
219
|
-
const items = Array.isArray(received)
|
|
190
|
+
const items = Array.isArray(received)
|
|
191
|
+
? received
|
|
192
|
+
: received instanceof Set
|
|
193
|
+
? [...received]
|
|
194
|
+
: [];
|
|
220
195
|
return simple(
|
|
221
196
|
items.some((item) => equals(item, expected)),
|
|
222
197
|
`to contain something equal to ${render(expected)}`,
|
|
@@ -224,8 +199,12 @@ function verdicts(received: mixed): { +[string]: (...args: $ReadOnlyArray<any>)
|
|
|
224
199
|
);
|
|
225
200
|
},
|
|
226
201
|
toHaveLength: (expected: number) => {
|
|
227
|
-
const length = received == null ? undefined : (received
|
|
228
|
-
return simple(
|
|
202
|
+
const length = received == null ? undefined : (received as $FlowFixMe).length;
|
|
203
|
+
return simple(
|
|
204
|
+
length === expected,
|
|
205
|
+
`to have length ${expected}, not ${render(length)}`,
|
|
206
|
+
expected,
|
|
207
|
+
);
|
|
229
208
|
},
|
|
230
209
|
toHaveProperty: (path: string, ...rest: $ReadOnlyArray<mixed>) => {
|
|
231
210
|
const found = propertyAt(received, path);
|
|
@@ -240,21 +219,60 @@ function verdicts(received: mixed): { +[string]: (...args: $ReadOnlyArray<any>)
|
|
|
240
219
|
},
|
|
241
220
|
toMatch: (expected: mixed) => {
|
|
242
221
|
const text = typeof received === "string" ? received : String(received);
|
|
243
|
-
const pass =
|
|
222
|
+
const pass =
|
|
223
|
+
typeof expected === "string"
|
|
224
|
+
? text.includes(expected)
|
|
225
|
+
: (expected as $FlowFixMe).test(text);
|
|
244
226
|
return simple(pass, `to match ${render(expected)}`, expected);
|
|
245
227
|
},
|
|
246
228
|
toMatchObject: (expected: mixed) =>
|
|
247
229
|
simple(matchesObject(received, expected), `to match ${render(expected)}`, expected),
|
|
248
230
|
toBeInstanceOf: (expected: mixed) =>
|
|
249
231
|
simple(
|
|
250
|
-
typeof expected === "function" && received instanceof (expected
|
|
232
|
+
typeof expected === "function" && received instanceof (expected as $FlowFixMe),
|
|
251
233
|
`to be an instance of ${render(expected)}`,
|
|
252
234
|
expected,
|
|
253
235
|
),
|
|
254
236
|
toBeTypeOf: (expected: string) =>
|
|
255
|
-
simple(
|
|
237
|
+
simple(
|
|
238
|
+
typeof received === expected,
|
|
239
|
+
`to be of type ${expected}, not ${typeof received}`,
|
|
240
|
+
expected,
|
|
241
|
+
),
|
|
256
242
|
toSatisfy: (predicate: (value: mixed) => boolean) =>
|
|
257
243
|
simple(predicate(received) === true, "to satisfy the predicate"),
|
|
244
|
+
toMatchSnapshot: (hint?: string): Verdict => {
|
|
245
|
+
const verdict = snapshot.matchSnapshot(received, hint);
|
|
246
|
+
return {
|
|
247
|
+
pass: verdict.pass,
|
|
248
|
+
expected: verdict.expected ?? "(no snapshot yet)",
|
|
249
|
+
received: verdict.received,
|
|
250
|
+
// The whole of both sides, because a mismatch that says only "the
|
|
251
|
+
// snapshot did not match" makes a reader open two files.
|
|
252
|
+
failure: () =>
|
|
253
|
+
`snapshot did not match.\n\nstored:\n${verdict.expected ?? "(none)"}\n\n` +
|
|
254
|
+
`received:\n${verdict.received}\n\n` +
|
|
255
|
+
"Run `uf test -u` if the new value is the right one.",
|
|
256
|
+
negatedFailure: () => "expected the value not to match its snapshot",
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
toMatchInlineSnapshot: (expected?: string): Verdict => {
|
|
260
|
+
const verdict = snapshot.matchInlineSnapshot(received, expected);
|
|
261
|
+
return {
|
|
262
|
+
pass: verdict.pass,
|
|
263
|
+
expected: verdict.expected ?? "(no inline snapshot yet)",
|
|
264
|
+
received: verdict.received,
|
|
265
|
+
failure: () =>
|
|
266
|
+
verdict.expected == null
|
|
267
|
+
? // uf does not rewrite a test file — a tool that edits the file you
|
|
268
|
+
// are editing is a tool that loses work — so it reports what to
|
|
269
|
+
// paste in and leaves the decision to a person.
|
|
270
|
+
`no inline snapshot yet. Paste this into the call:\n\n\`\`\`\n${verdict.received}\n\`\`\``
|
|
271
|
+
: `inline snapshot did not match.\n\nstored:\n${verdict.expected}\n\n` +
|
|
272
|
+
`received:\n${verdict.received}`,
|
|
273
|
+
negatedFailure: () => "expected the value not to match its inline snapshot",
|
|
274
|
+
};
|
|
275
|
+
},
|
|
258
276
|
toThrow: (...rest: $ReadOnlyArray<mixed>) => {
|
|
259
277
|
const expected = rest[0];
|
|
260
278
|
if (typeof received !== "function") {
|
|
@@ -314,7 +332,157 @@ function verdicts(received: mixed): { +[string]: (...args: $ReadOnlyArray<any>)
|
|
|
314
332
|
args,
|
|
315
333
|
);
|
|
316
334
|
},
|
|
335
|
+
|
|
336
|
+
// ---------------------------------------------------------------- //
|
|
337
|
+
// Elements
|
|
338
|
+
//
|
|
339
|
+
// These read properties of whatever they are given, so this module still
|
|
340
|
+
// needs no DOM and no dependency on one: an element is an object with a
|
|
341
|
+
// `tagName`, and a process without a document simply never has one to
|
|
342
|
+
// pass in. `element` says so when it does not.
|
|
343
|
+
// ---------------------------------------------------------------- //
|
|
344
|
+
|
|
345
|
+
toBeInTheDocument: () => {
|
|
346
|
+
const node = element("toBeInTheDocument");
|
|
347
|
+
const root = node.ownerDocument;
|
|
348
|
+
return simple(root != null && root.contains(node), "to be in the document");
|
|
349
|
+
},
|
|
350
|
+
toBeVisible: () => {
|
|
351
|
+
const node = element("toBeVisible");
|
|
352
|
+
return simple(isVisible(node), "to be visible");
|
|
353
|
+
},
|
|
354
|
+
toBeDisabled: () => {
|
|
355
|
+
const node = element("toBeDisabled");
|
|
356
|
+
return simple(isDisabled(node), "to be disabled");
|
|
357
|
+
},
|
|
358
|
+
toBeEnabled: () => {
|
|
359
|
+
const node = element("toBeEnabled");
|
|
360
|
+
return simple(!isDisabled(node), "to be enabled");
|
|
361
|
+
},
|
|
362
|
+
toBeChecked: () => {
|
|
363
|
+
const node = element("toBeChecked");
|
|
364
|
+
const aria = node.getAttribute("aria-checked");
|
|
365
|
+
const checked = aria != null ? aria === "true" : (node as $FlowFixMe).checked === true;
|
|
366
|
+
return simple(checked, "to be checked");
|
|
367
|
+
},
|
|
368
|
+
toBeRequired: () => {
|
|
369
|
+
const node = element("toBeRequired");
|
|
370
|
+
return simple(
|
|
371
|
+
(node as $FlowFixMe).required === true || node.getAttribute("aria-required") === "true",
|
|
372
|
+
"to be required",
|
|
373
|
+
);
|
|
374
|
+
},
|
|
375
|
+
toHaveFocus: () => {
|
|
376
|
+
const node = element("toHaveFocus");
|
|
377
|
+
return simple(node.ownerDocument?.activeElement === node, "to have focus");
|
|
378
|
+
},
|
|
379
|
+
toHaveAttribute: (name: mixed, value?: mixed) => {
|
|
380
|
+
const node = element("toHaveAttribute");
|
|
381
|
+
const actual = node.getAttribute(String(name));
|
|
382
|
+
if (value === undefined) {
|
|
383
|
+
return simple(actual != null, `to have the attribute ${render(name)}`, name);
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
pass: actual === String(value),
|
|
387
|
+
expected: render(value),
|
|
388
|
+
received: render(actual),
|
|
389
|
+
failure: () => `expected ${render(name)} to be ${render(value)}, not ${render(actual)}`,
|
|
390
|
+
};
|
|
391
|
+
},
|
|
392
|
+
toHaveClass: (...names: $ReadOnlyArray<mixed>) => {
|
|
393
|
+
const node = element("toHaveClass");
|
|
394
|
+
const classes = (node.getAttribute("class") ?? "").split(/\s+/).filter(Boolean);
|
|
395
|
+
const wanted = names.map(String);
|
|
396
|
+
return {
|
|
397
|
+
pass: wanted.every((name) => classes.includes(name)),
|
|
398
|
+
expected: render(wanted),
|
|
399
|
+
received: render(classes),
|
|
400
|
+
failure: () => `expected the class list ${render(classes)} to include ${render(wanted)}`,
|
|
401
|
+
};
|
|
402
|
+
},
|
|
403
|
+
toHaveTextContent: (expected: mixed) => {
|
|
404
|
+
const node = element("toHaveTextContent");
|
|
405
|
+
const text = (node.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
406
|
+
const pass =
|
|
407
|
+
expected instanceof RegExp ? expected.test(text) : text.includes(String(expected));
|
|
408
|
+
return {
|
|
409
|
+
pass,
|
|
410
|
+
expected: render(expected),
|
|
411
|
+
received: render(text),
|
|
412
|
+
failure: () => `expected the text ${render(text)} to contain ${render(expected)}`,
|
|
413
|
+
};
|
|
414
|
+
},
|
|
415
|
+
toHaveValue: (expected: mixed) => {
|
|
416
|
+
const node = element("toHaveValue");
|
|
417
|
+
const actual = (node as $FlowFixMe).value;
|
|
418
|
+
return {
|
|
419
|
+
pass: equals(actual, expected),
|
|
420
|
+
expected: render(expected),
|
|
421
|
+
received: render(actual),
|
|
422
|
+
failure: () => `expected the value ${render(actual)} to be ${render(expected)}`,
|
|
423
|
+
};
|
|
424
|
+
},
|
|
317
425
|
};
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* The received value as an element, or a failure that says what it was.
|
|
429
|
+
*
|
|
430
|
+
* An element matcher applied to a string is almost always a query whose
|
|
431
|
+
* result was used without being awaited, and "received a Promise" is a much
|
|
432
|
+
* better message than a `TypeError` about `getAttribute`.
|
|
433
|
+
*/
|
|
434
|
+
function element(matcher: string): Element {
|
|
435
|
+
const node: $FlowFixMe = received;
|
|
436
|
+
if (node == null || typeof node.getAttribute !== "function") {
|
|
437
|
+
throw new AssertionError(`${matcher} needs an element, and received ${render(received)}`);
|
|
438
|
+
}
|
|
439
|
+
return node;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Whether a reader would see this element.
|
|
445
|
+
*
|
|
446
|
+
* Walks the ancestors, because `display: none` on a parent hides a child whose
|
|
447
|
+
* own style says nothing. `hidden`, `aria-hidden` and a `details` that is not
|
|
448
|
+
* open each hide their subtree too.
|
|
449
|
+
*/
|
|
450
|
+
function isVisible(node: Element): boolean {
|
|
451
|
+
let current: $FlowFixMe = node;
|
|
452
|
+
while (current != null && current.nodeType === 1) {
|
|
453
|
+
if (current.hasAttribute("hidden") || current.getAttribute("aria-hidden") === "true") {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
if (current.tagName === "DETAILS" && !current.hasAttribute("open") && current !== node) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
const style = current.ownerDocument?.defaultView?.getComputedStyle?.(current);
|
|
460
|
+
if (style != null) {
|
|
461
|
+
if (style.display === "none" || style.visibility === "hidden") {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
if (style.opacity === "0") {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
current = current.parentElement;
|
|
469
|
+
}
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** Whether the control is disabled, by its own attribute or a fieldset's. */
|
|
474
|
+
function isDisabled(node: Element): boolean {
|
|
475
|
+
let current: $FlowFixMe = node;
|
|
476
|
+
while (current != null && current.nodeType === 1) {
|
|
477
|
+
if (current.hasAttribute("disabled")) {
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
if (current.getAttribute("aria-disabled") === "true") {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
current = current.parentElement;
|
|
484
|
+
}
|
|
485
|
+
return false;
|
|
318
486
|
}
|
|
319
487
|
|
|
320
488
|
/**
|
|
@@ -333,7 +501,12 @@ function bind(received: mixed, negated: boolean): $FlowFixMe {
|
|
|
333
501
|
return undefined;
|
|
334
502
|
}
|
|
335
503
|
const message = negated ? verdict.negatedFailure() : verdict.failure();
|
|
336
|
-
throw new AssertionError(
|
|
504
|
+
throw new AssertionError(
|
|
505
|
+
message,
|
|
506
|
+
name,
|
|
507
|
+
verdict.expected ?? "",
|
|
508
|
+
verdict.received ?? render(received),
|
|
509
|
+
);
|
|
337
510
|
};
|
|
338
511
|
}
|
|
339
512
|
Object.defineProperty(bound, "not", { get: () => bind(received, !negated) });
|
|
@@ -351,7 +524,7 @@ function settled(promise: mixed, wanted: "resolve" | "reject", negated: boolean)
|
|
|
351
524
|
let value: mixed;
|
|
352
525
|
let rejected = false;
|
|
353
526
|
try {
|
|
354
|
-
value = await (promise
|
|
527
|
+
value = await (promise as $FlowFixMe);
|
|
355
528
|
} catch (error) {
|
|
356
529
|
rejected = true;
|
|
357
530
|
value = error;
|
|
@@ -399,9 +572,49 @@ function settled(promise: mixed, wanted: "resolve" | "reject", negated: boolean)
|
|
|
399
572
|
* await expect(load()).resolves.toHaveLength(3);
|
|
400
573
|
* ```
|
|
401
574
|
*/
|
|
402
|
-
|
|
575
|
+
function expectValue(received: mixed): $FlowFixMe {
|
|
403
576
|
const expectation: $FlowFixMe = bind(received, false);
|
|
404
|
-
Object.defineProperty(expectation, "resolves", {
|
|
577
|
+
Object.defineProperty(expectation, "resolves", {
|
|
578
|
+
get: () => settled(received, "resolve", false),
|
|
579
|
+
});
|
|
405
580
|
Object.defineProperty(expectation, "rejects", { get: () => settled(received, "reject", false) });
|
|
406
581
|
return expectation;
|
|
407
582
|
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Assert about a value.
|
|
586
|
+
*
|
|
587
|
+
* Declared with its matchers attached rather than assigned afterwards: a
|
|
588
|
+
* shipped module may only declare, import and export at its top level, and
|
|
589
|
+
* `expect.any = …` is a statement that runs when the module is imported.
|
|
590
|
+
*
|
|
591
|
+
* The `expect.*` half are the matchers that stand in for a value instead of
|
|
592
|
+
* being one. `expect(user).toEqual({ id: expect.any(String), name: "uf" })`
|
|
593
|
+
* says what a test means; spelling out the id would either be a lie or a second
|
|
594
|
+
* source of truth. They work at any depth, because `equals` asks every value it
|
|
595
|
+
* meets whether it is one.
|
|
596
|
+
*
|
|
597
|
+
* `expect.not.*` is the negated form, spelled the way Jest and Vitest spell it
|
|
598
|
+
* — `expect.not.objectContaining({ error: expect.anything() })` reads better
|
|
599
|
+
* than a negated assertion around the whole object, and is the form a suite
|
|
600
|
+
* being ported will already have.
|
|
601
|
+
*/
|
|
602
|
+
export const expect: $FlowFixMe = Object.assign(expectValue, {
|
|
603
|
+
any: asymmetric.any,
|
|
604
|
+
anything: asymmetric.anything,
|
|
605
|
+
objectContaining: asymmetric.objectContaining,
|
|
606
|
+
arrayContaining: asymmetric.arrayContaining,
|
|
607
|
+
stringContaining: asymmetric.stringContaining,
|
|
608
|
+
stringMatching: asymmetric.stringMatching,
|
|
609
|
+
closeTo: asymmetric.closeTo,
|
|
610
|
+
not: {
|
|
611
|
+
objectContaining: (expected: interface {}) =>
|
|
612
|
+
asymmetric.not(asymmetric.objectContaining(expected)),
|
|
613
|
+
arrayContaining: (expected: $ReadOnlyArray<mixed>) =>
|
|
614
|
+
asymmetric.not(asymmetric.arrayContaining(expected)),
|
|
615
|
+
stringContaining: (substring: string) => asymmetric.not(asymmetric.stringContaining(substring)),
|
|
616
|
+
stringMatching: (pattern: string | RegExp) =>
|
|
617
|
+
asymmetric.not(asymmetric.stringMatching(pattern)),
|
|
618
|
+
closeTo: (value: number, digits?: number) => asymmetric.not(asymmetric.closeTo(value, digits)),
|
|
619
|
+
},
|
|
620
|
+
});
|
package/internal/frames.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// regex-shaped.
|
|
15
15
|
|
|
16
16
|
/** A position in a source file, one-based line and column. */
|
|
17
|
-
export type Site = {|
|
|
17
|
+
export type Site = {| readonly line: number, readonly column: number |};
|
|
18
18
|
|
|
19
19
|
/** Frames belonging to the runner itself, which no test author wrote. */
|
|
20
20
|
const INTERNAL_MARKERS = [
|
|
@@ -54,7 +54,10 @@ export function frameSite(frame: string): Site | null {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/** The run of digits ending at `end`, with where it starts. */
|
|
57
|
-
function digitsBefore(
|
|
57
|
+
function digitsBefore(
|
|
58
|
+
text: string,
|
|
59
|
+
end: number,
|
|
60
|
+
): {| readonly value: number, readonly start: number |} | null {
|
|
58
61
|
let start = end;
|
|
59
62
|
while (start > 0 && text[start - 1] >= "0" && text[start - 1] <= "9") {
|
|
60
63
|
start -= 1;
|
|
@@ -71,7 +74,10 @@ function digitsBefore(text: string, end: number): {| +value: number, +start: num
|
|
|
71
74
|
* `skipInternal` is false when the caller has already trimmed the runner's own
|
|
72
75
|
* frames and wants the first frame whatever it is.
|
|
73
76
|
*/
|
|
74
|
-
export function firstUserSite(
|
|
77
|
+
export function firstUserSite(
|
|
78
|
+
stack: string | null | void,
|
|
79
|
+
skipInternal: boolean = true,
|
|
80
|
+
): Site | null {
|
|
75
81
|
if (stack == null) {
|
|
76
82
|
return null;
|
|
77
83
|
}
|