@uniflowed/test 0.0.0-alpha.1 → 0.0.0-alpha.5
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 +312 -81
- package/internal/frames.js +9 -3
- package/internal/namespace.js +251 -0
- package/internal/output.js +309 -0
- package/internal/registry.js +30 -27
- package/internal/run.js +75 -38
- 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 +157 -25
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
|
}
|
|
@@ -136,8 +88,28 @@ function matchesThrown(thrown: mixed, expected: mixed): boolean {
|
|
|
136
88
|
*
|
|
137
89
|
* Every entry returns a [`Verdict`] rather than throwing, which is what lets
|
|
138
90
|
* `.not` reuse all of them.
|
|
91
|
+
*
|
|
92
|
+
* # The `any` in the indexer
|
|
93
|
+
*
|
|
94
|
+
* The entries do not agree about their arguments — `toBe` takes a `mixed`,
|
|
95
|
+
* `toHaveLength` takes a `number`, `toBeCloseTo` takes two — and [`bind`]
|
|
96
|
+
* applies whichever one it was asked for to a `$ReadOnlyArray<mixed>` it
|
|
97
|
+
* collected from a caller. Parameters are contravariant, so one indexer cannot
|
|
98
|
+
* describe both ends: `(...args: $ReadOnlyArray<mixed>)` rejects every entry
|
|
99
|
+
* that wants a `number`, and `(...args: $ReadOnlyArray<empty>)` accepts every
|
|
100
|
+
* entry and rejects the call.
|
|
101
|
+
*
|
|
102
|
+
* `mixed` with a cast at the call would move the same unsoundness one line
|
|
103
|
+
* without checking anything, because the caller is `bind`, whose result is
|
|
104
|
+
* `$FlowFixMe` and whose result's result is `expect`, also `$FlowFixMe`. The
|
|
105
|
+
* type that makes any of this checked is a written-out matcher interface —
|
|
106
|
+
* one signature per matcher, plus `.not`, `.resolves` and `.rejects` — which
|
|
107
|
+
* is what `expect`'s own annotation is waiting for. Until that exists, a
|
|
108
|
+
* narrower type here would be precision nobody can reach.
|
|
139
109
|
*/
|
|
140
|
-
function verdicts(received: mixed): {
|
|
110
|
+
function verdicts(received: mixed): {
|
|
111
|
+
readonly [string]: (...args: $ReadOnlyArray<any>) => Verdict,
|
|
112
|
+
} {
|
|
141
113
|
const shown = () => render(received);
|
|
142
114
|
const simple = (pass: boolean, what: string, expected?: mixed): Verdict => ({
|
|
143
115
|
pass,
|
|
@@ -146,7 +118,8 @@ function verdicts(received: mixed): { +[string]: (...args: $ReadOnlyArray<any>)
|
|
|
146
118
|
failure: () => `expected ${shown()} ${what}`,
|
|
147
119
|
negatedFailure: () => `expected ${shown()} not ${what}`,
|
|
148
120
|
});
|
|
149
|
-
const spyCalls = (): Array<SpyCall> =>
|
|
121
|
+
const spyCalls = (): Array<SpyCall> =>
|
|
122
|
+
isSpy(received) ? (received as $FlowFixMe).mock.calls : [];
|
|
150
123
|
const requireSpy = (matcher: string) => {
|
|
151
124
|
if (!isSpy(received)) {
|
|
152
125
|
throw new AssertionError(
|
|
@@ -187,17 +160,33 @@ function verdicts(received: mixed): { +[string]: (...args: $ReadOnlyArray<any>)
|
|
|
187
160
|
toBeDefined: () => simple(received !== undefined, "to be defined"),
|
|
188
161
|
toBeNaN: () => simple(typeof received === "number" && Number.isNaN(received), "to be NaN"),
|
|
189
162
|
toBeGreaterThan: (expected: mixed) =>
|
|
190
|
-
simple(
|
|
163
|
+
simple(
|
|
164
|
+
(received as $FlowFixMe) > (expected as $FlowFixMe),
|
|
165
|
+
`to be greater than ${render(expected)}`,
|
|
166
|
+
expected,
|
|
167
|
+
),
|
|
191
168
|
toBeGreaterThanOrEqual: (expected: mixed) =>
|
|
192
|
-
simple(
|
|
169
|
+
simple(
|
|
170
|
+
(received as $FlowFixMe) >= (expected as $FlowFixMe),
|
|
171
|
+
`to be at least ${render(expected)}`,
|
|
172
|
+
expected,
|
|
173
|
+
),
|
|
193
174
|
toBeLessThan: (expected: mixed) =>
|
|
194
|
-
simple(
|
|
175
|
+
simple(
|
|
176
|
+
(received as $FlowFixMe) < (expected as $FlowFixMe),
|
|
177
|
+
`to be less than ${render(expected)}`,
|
|
178
|
+
expected,
|
|
179
|
+
),
|
|
195
180
|
toBeLessThanOrEqual: (expected: mixed) =>
|
|
196
|
-
simple(
|
|
181
|
+
simple(
|
|
182
|
+
(received as $FlowFixMe) <= (expected as $FlowFixMe),
|
|
183
|
+
`to be at most ${render(expected)}`,
|
|
184
|
+
expected,
|
|
185
|
+
),
|
|
197
186
|
toBeCloseTo: (expected: number, digits?: number) => {
|
|
198
187
|
const places = digits ?? 2;
|
|
199
188
|
const tolerance = 10 ** -places / 2;
|
|
200
|
-
const difference = Math.abs((received
|
|
189
|
+
const difference = Math.abs((received as $FlowFixMe) - expected);
|
|
201
190
|
return simple(
|
|
202
191
|
difference < tolerance,
|
|
203
192
|
`to be within ${tolerance} of ${expected}, but it is off by ${difference}`,
|
|
@@ -216,7 +205,11 @@ function verdicts(received: mixed): { +[string]: (...args: $ReadOnlyArray<any>)
|
|
|
216
205
|
return simple(pass, `to contain ${render(expected)}`, expected);
|
|
217
206
|
},
|
|
218
207
|
toContainEqual: (expected: mixed) => {
|
|
219
|
-
const items = Array.isArray(received)
|
|
208
|
+
const items = Array.isArray(received)
|
|
209
|
+
? received
|
|
210
|
+
: received instanceof Set
|
|
211
|
+
? [...received]
|
|
212
|
+
: [];
|
|
220
213
|
return simple(
|
|
221
214
|
items.some((item) => equals(item, expected)),
|
|
222
215
|
`to contain something equal to ${render(expected)}`,
|
|
@@ -224,8 +217,12 @@ function verdicts(received: mixed): { +[string]: (...args: $ReadOnlyArray<any>)
|
|
|
224
217
|
);
|
|
225
218
|
},
|
|
226
219
|
toHaveLength: (expected: number) => {
|
|
227
|
-
const length = received == null ? undefined : (received
|
|
228
|
-
return simple(
|
|
220
|
+
const length = received == null ? undefined : (received as $FlowFixMe).length;
|
|
221
|
+
return simple(
|
|
222
|
+
length === expected,
|
|
223
|
+
`to have length ${expected}, not ${render(length)}`,
|
|
224
|
+
expected,
|
|
225
|
+
);
|
|
229
226
|
},
|
|
230
227
|
toHaveProperty: (path: string, ...rest: $ReadOnlyArray<mixed>) => {
|
|
231
228
|
const found = propertyAt(received, path);
|
|
@@ -240,21 +237,60 @@ function verdicts(received: mixed): { +[string]: (...args: $ReadOnlyArray<any>)
|
|
|
240
237
|
},
|
|
241
238
|
toMatch: (expected: mixed) => {
|
|
242
239
|
const text = typeof received === "string" ? received : String(received);
|
|
243
|
-
const pass =
|
|
240
|
+
const pass =
|
|
241
|
+
typeof expected === "string"
|
|
242
|
+
? text.includes(expected)
|
|
243
|
+
: (expected as $FlowFixMe).test(text);
|
|
244
244
|
return simple(pass, `to match ${render(expected)}`, expected);
|
|
245
245
|
},
|
|
246
246
|
toMatchObject: (expected: mixed) =>
|
|
247
247
|
simple(matchesObject(received, expected), `to match ${render(expected)}`, expected),
|
|
248
248
|
toBeInstanceOf: (expected: mixed) =>
|
|
249
249
|
simple(
|
|
250
|
-
typeof expected === "function" && received instanceof (expected
|
|
250
|
+
typeof expected === "function" && received instanceof (expected as $FlowFixMe),
|
|
251
251
|
`to be an instance of ${render(expected)}`,
|
|
252
252
|
expected,
|
|
253
253
|
),
|
|
254
254
|
toBeTypeOf: (expected: string) =>
|
|
255
|
-
simple(
|
|
255
|
+
simple(
|
|
256
|
+
typeof received === expected,
|
|
257
|
+
`to be of type ${expected}, not ${typeof received}`,
|
|
258
|
+
expected,
|
|
259
|
+
),
|
|
256
260
|
toSatisfy: (predicate: (value: mixed) => boolean) =>
|
|
257
261
|
simple(predicate(received) === true, "to satisfy the predicate"),
|
|
262
|
+
toMatchSnapshot: (hint?: string): Verdict => {
|
|
263
|
+
const verdict = snapshot.matchSnapshot(received, hint);
|
|
264
|
+
return {
|
|
265
|
+
pass: verdict.pass,
|
|
266
|
+
expected: verdict.expected ?? "(no snapshot yet)",
|
|
267
|
+
received: verdict.received,
|
|
268
|
+
// The whole of both sides, because a mismatch that says only "the
|
|
269
|
+
// snapshot did not match" makes a reader open two files.
|
|
270
|
+
failure: () =>
|
|
271
|
+
`snapshot did not match.\n\nstored:\n${verdict.expected ?? "(none)"}\n\n` +
|
|
272
|
+
`received:\n${verdict.received}\n\n` +
|
|
273
|
+
"Run `uf test -u` if the new value is the right one.",
|
|
274
|
+
negatedFailure: () => "expected the value not to match its snapshot",
|
|
275
|
+
};
|
|
276
|
+
},
|
|
277
|
+
toMatchInlineSnapshot: (expected?: string): Verdict => {
|
|
278
|
+
const verdict = snapshot.matchInlineSnapshot(received, expected);
|
|
279
|
+
return {
|
|
280
|
+
pass: verdict.pass,
|
|
281
|
+
expected: verdict.expected ?? "(no inline snapshot yet)",
|
|
282
|
+
received: verdict.received,
|
|
283
|
+
failure: () =>
|
|
284
|
+
verdict.expected == null
|
|
285
|
+
? // uf does not rewrite a test file — a tool that edits the file you
|
|
286
|
+
// are editing is a tool that loses work — so it reports what to
|
|
287
|
+
// paste in and leaves the decision to a person.
|
|
288
|
+
`no inline snapshot yet. Paste this into the call:\n\n\`\`\`\n${verdict.received}\n\`\`\``
|
|
289
|
+
: `inline snapshot did not match.\n\nstored:\n${verdict.expected}\n\n` +
|
|
290
|
+
`received:\n${verdict.received}`,
|
|
291
|
+
negatedFailure: () => "expected the value not to match its inline snapshot",
|
|
292
|
+
};
|
|
293
|
+
},
|
|
258
294
|
toThrow: (...rest: $ReadOnlyArray<mixed>) => {
|
|
259
295
|
const expected = rest[0];
|
|
260
296
|
if (typeof received !== "function") {
|
|
@@ -314,7 +350,157 @@ function verdicts(received: mixed): { +[string]: (...args: $ReadOnlyArray<any>)
|
|
|
314
350
|
args,
|
|
315
351
|
);
|
|
316
352
|
},
|
|
353
|
+
|
|
354
|
+
// ---------------------------------------------------------------- //
|
|
355
|
+
// Elements
|
|
356
|
+
//
|
|
357
|
+
// These read properties of whatever they are given, so this module still
|
|
358
|
+
// needs no DOM and no dependency on one: an element is an object with a
|
|
359
|
+
// `tagName`, and a process without a document simply never has one to
|
|
360
|
+
// pass in. `element` says so when it does not.
|
|
361
|
+
// ---------------------------------------------------------------- //
|
|
362
|
+
|
|
363
|
+
toBeInTheDocument: () => {
|
|
364
|
+
const node = element("toBeInTheDocument");
|
|
365
|
+
const root = node.ownerDocument;
|
|
366
|
+
return simple(root != null && root.contains(node), "to be in the document");
|
|
367
|
+
},
|
|
368
|
+
toBeVisible: () => {
|
|
369
|
+
const node = element("toBeVisible");
|
|
370
|
+
return simple(isVisible(node), "to be visible");
|
|
371
|
+
},
|
|
372
|
+
toBeDisabled: () => {
|
|
373
|
+
const node = element("toBeDisabled");
|
|
374
|
+
return simple(isDisabled(node), "to be disabled");
|
|
375
|
+
},
|
|
376
|
+
toBeEnabled: () => {
|
|
377
|
+
const node = element("toBeEnabled");
|
|
378
|
+
return simple(!isDisabled(node), "to be enabled");
|
|
379
|
+
},
|
|
380
|
+
toBeChecked: () => {
|
|
381
|
+
const node = element("toBeChecked");
|
|
382
|
+
const aria = node.getAttribute("aria-checked");
|
|
383
|
+
const checked = aria != null ? aria === "true" : (node as $FlowFixMe).checked === true;
|
|
384
|
+
return simple(checked, "to be checked");
|
|
385
|
+
},
|
|
386
|
+
toBeRequired: () => {
|
|
387
|
+
const node = element("toBeRequired");
|
|
388
|
+
return simple(
|
|
389
|
+
(node as $FlowFixMe).required === true || node.getAttribute("aria-required") === "true",
|
|
390
|
+
"to be required",
|
|
391
|
+
);
|
|
392
|
+
},
|
|
393
|
+
toHaveFocus: () => {
|
|
394
|
+
const node = element("toHaveFocus");
|
|
395
|
+
return simple(node.ownerDocument?.activeElement === node, "to have focus");
|
|
396
|
+
},
|
|
397
|
+
toHaveAttribute: (name: mixed, value?: mixed) => {
|
|
398
|
+
const node = element("toHaveAttribute");
|
|
399
|
+
const actual = node.getAttribute(String(name));
|
|
400
|
+
if (value === undefined) {
|
|
401
|
+
return simple(actual != null, `to have the attribute ${render(name)}`, name);
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
pass: actual === String(value),
|
|
405
|
+
expected: render(value),
|
|
406
|
+
received: render(actual),
|
|
407
|
+
failure: () => `expected ${render(name)} to be ${render(value)}, not ${render(actual)}`,
|
|
408
|
+
};
|
|
409
|
+
},
|
|
410
|
+
toHaveClass: (...names: $ReadOnlyArray<mixed>) => {
|
|
411
|
+
const node = element("toHaveClass");
|
|
412
|
+
const classes = (node.getAttribute("class") ?? "").split(/\s+/).filter(Boolean);
|
|
413
|
+
const wanted = names.map(String);
|
|
414
|
+
return {
|
|
415
|
+
pass: wanted.every((name) => classes.includes(name)),
|
|
416
|
+
expected: render(wanted),
|
|
417
|
+
received: render(classes),
|
|
418
|
+
failure: () => `expected the class list ${render(classes)} to include ${render(wanted)}`,
|
|
419
|
+
};
|
|
420
|
+
},
|
|
421
|
+
toHaveTextContent: (expected: mixed) => {
|
|
422
|
+
const node = element("toHaveTextContent");
|
|
423
|
+
const text = (node.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
424
|
+
const pass =
|
|
425
|
+
expected instanceof RegExp ? expected.test(text) : text.includes(String(expected));
|
|
426
|
+
return {
|
|
427
|
+
pass,
|
|
428
|
+
expected: render(expected),
|
|
429
|
+
received: render(text),
|
|
430
|
+
failure: () => `expected the text ${render(text)} to contain ${render(expected)}`,
|
|
431
|
+
};
|
|
432
|
+
},
|
|
433
|
+
toHaveValue: (expected: mixed) => {
|
|
434
|
+
const node = element("toHaveValue");
|
|
435
|
+
const actual = (node as $FlowFixMe).value;
|
|
436
|
+
return {
|
|
437
|
+
pass: equals(actual, expected),
|
|
438
|
+
expected: render(expected),
|
|
439
|
+
received: render(actual),
|
|
440
|
+
failure: () => `expected the value ${render(actual)} to be ${render(expected)}`,
|
|
441
|
+
};
|
|
442
|
+
},
|
|
317
443
|
};
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* The received value as an element, or a failure that says what it was.
|
|
447
|
+
*
|
|
448
|
+
* An element matcher applied to a string is almost always a query whose
|
|
449
|
+
* result was used without being awaited, and "received a Promise" is a much
|
|
450
|
+
* better message than a `TypeError` about `getAttribute`.
|
|
451
|
+
*/
|
|
452
|
+
function element(matcher: string): Element {
|
|
453
|
+
const node: $FlowFixMe = received;
|
|
454
|
+
if (node == null || typeof node.getAttribute !== "function") {
|
|
455
|
+
throw new AssertionError(`${matcher} needs an element, and received ${render(received)}`);
|
|
456
|
+
}
|
|
457
|
+
return node;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Whether a reader would see this element.
|
|
463
|
+
*
|
|
464
|
+
* Walks the ancestors, because `display: none` on a parent hides a child whose
|
|
465
|
+
* own style says nothing. `hidden`, `aria-hidden` and a `details` that is not
|
|
466
|
+
* open each hide their subtree too.
|
|
467
|
+
*/
|
|
468
|
+
function isVisible(node: Element): boolean {
|
|
469
|
+
let current: $FlowFixMe = node;
|
|
470
|
+
while (current != null && current.nodeType === 1) {
|
|
471
|
+
if (current.hasAttribute("hidden") || current.getAttribute("aria-hidden") === "true") {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
if (current.tagName === "DETAILS" && !current.hasAttribute("open") && current !== node) {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
const style = current.ownerDocument?.defaultView?.getComputedStyle?.(current);
|
|
478
|
+
if (style != null) {
|
|
479
|
+
if (style.display === "none" || style.visibility === "hidden") {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
if (style.opacity === "0") {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
current = current.parentElement;
|
|
487
|
+
}
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Whether the control is disabled, by its own attribute or a fieldset's. */
|
|
492
|
+
function isDisabled(node: Element): boolean {
|
|
493
|
+
let current: $FlowFixMe = node;
|
|
494
|
+
while (current != null && current.nodeType === 1) {
|
|
495
|
+
if (current.hasAttribute("disabled")) {
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
if (current.getAttribute("aria-disabled") === "true") {
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
current = current.parentElement;
|
|
502
|
+
}
|
|
503
|
+
return false;
|
|
318
504
|
}
|
|
319
505
|
|
|
320
506
|
/**
|
|
@@ -333,7 +519,12 @@ function bind(received: mixed, negated: boolean): $FlowFixMe {
|
|
|
333
519
|
return undefined;
|
|
334
520
|
}
|
|
335
521
|
const message = negated ? verdict.negatedFailure() : verdict.failure();
|
|
336
|
-
throw new AssertionError(
|
|
522
|
+
throw new AssertionError(
|
|
523
|
+
message,
|
|
524
|
+
name,
|
|
525
|
+
verdict.expected ?? "",
|
|
526
|
+
verdict.received ?? render(received),
|
|
527
|
+
);
|
|
337
528
|
};
|
|
338
529
|
}
|
|
339
530
|
Object.defineProperty(bound, "not", { get: () => bind(received, !negated) });
|
|
@@ -351,7 +542,7 @@ function settled(promise: mixed, wanted: "resolve" | "reject", negated: boolean)
|
|
|
351
542
|
let value: mixed;
|
|
352
543
|
let rejected = false;
|
|
353
544
|
try {
|
|
354
|
-
value = await (promise
|
|
545
|
+
value = await (promise as $FlowFixMe);
|
|
355
546
|
} catch (error) {
|
|
356
547
|
rejected = true;
|
|
357
548
|
value = error;
|
|
@@ -399,9 +590,49 @@ function settled(promise: mixed, wanted: "resolve" | "reject", negated: boolean)
|
|
|
399
590
|
* await expect(load()).resolves.toHaveLength(3);
|
|
400
591
|
* ```
|
|
401
592
|
*/
|
|
402
|
-
|
|
593
|
+
function expectValue(received: mixed): $FlowFixMe {
|
|
403
594
|
const expectation: $FlowFixMe = bind(received, false);
|
|
404
|
-
Object.defineProperty(expectation, "resolves", {
|
|
595
|
+
Object.defineProperty(expectation, "resolves", {
|
|
596
|
+
get: () => settled(received, "resolve", false),
|
|
597
|
+
});
|
|
405
598
|
Object.defineProperty(expectation, "rejects", { get: () => settled(received, "reject", false) });
|
|
406
599
|
return expectation;
|
|
407
600
|
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Assert about a value.
|
|
604
|
+
*
|
|
605
|
+
* Declared with its matchers attached rather than assigned afterwards: a
|
|
606
|
+
* shipped module may only declare, import and export at its top level, and
|
|
607
|
+
* `expect.any = …` is a statement that runs when the module is imported.
|
|
608
|
+
*
|
|
609
|
+
* The `expect.*` half are the matchers that stand in for a value instead of
|
|
610
|
+
* being one. `expect(user).toEqual({ id: expect.any(String), name: "uf" })`
|
|
611
|
+
* says what a test means; spelling out the id would either be a lie or a second
|
|
612
|
+
* source of truth. They work at any depth, because `equals` asks every value it
|
|
613
|
+
* meets whether it is one.
|
|
614
|
+
*
|
|
615
|
+
* `expect.not.*` is the negated form, spelled the way Jest and Vitest spell it
|
|
616
|
+
* — `expect.not.objectContaining({ error: expect.anything() })` reads better
|
|
617
|
+
* than a negated assertion around the whole object, and is the form a suite
|
|
618
|
+
* being ported will already have.
|
|
619
|
+
*/
|
|
620
|
+
export const expect: $FlowFixMe = Object.assign(expectValue, {
|
|
621
|
+
any: asymmetric.any,
|
|
622
|
+
anything: asymmetric.anything,
|
|
623
|
+
objectContaining: asymmetric.objectContaining,
|
|
624
|
+
arrayContaining: asymmetric.arrayContaining,
|
|
625
|
+
stringContaining: asymmetric.stringContaining,
|
|
626
|
+
stringMatching: asymmetric.stringMatching,
|
|
627
|
+
closeTo: asymmetric.closeTo,
|
|
628
|
+
not: {
|
|
629
|
+
objectContaining: (expected: interface {}) =>
|
|
630
|
+
asymmetric.not(asymmetric.objectContaining(expected)),
|
|
631
|
+
arrayContaining: (expected: $ReadOnlyArray<mixed>) =>
|
|
632
|
+
asymmetric.not(asymmetric.arrayContaining(expected)),
|
|
633
|
+
stringContaining: (substring: string) => asymmetric.not(asymmetric.stringContaining(substring)),
|
|
634
|
+
stringMatching: (pattern: string | RegExp) =>
|
|
635
|
+
asymmetric.not(asymmetric.stringMatching(pattern)),
|
|
636
|
+
closeTo: (value: number, digits?: number) => asymmetric.not(asymmetric.closeTo(value, digits)),
|
|
637
|
+
},
|
|
638
|
+
});
|
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
|
}
|