@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/index.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
export type { Body as TestBody, Case, Modifier, Suite, TestOptions } from "./internal/registry.js";
|
|
16
16
|
export type { Outcome, Result, RunOptions } from "./internal/run.js";
|
|
17
17
|
export type { Site } from "./internal/frames.js";
|
|
18
|
-
export type { SpyCall } from "./internal/
|
|
18
|
+
export type { SpyCall, SpyResult } from "./internal/spy.js";
|
|
19
19
|
export type { Strictness } from "./internal/equality.js";
|
|
20
20
|
|
|
21
21
|
export {
|
|
@@ -28,7 +28,24 @@ export {
|
|
|
28
28
|
test,
|
|
29
29
|
} from "./internal/registry.js";
|
|
30
30
|
|
|
31
|
-
export { AssertionError, expect
|
|
31
|
+
export { AssertionError, expect } from "./internal/expect.js";
|
|
32
|
+
|
|
33
|
+
export { fn, spyOn } from "./internal/spy.js";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Spies, stubs, a controllable clock, and waiting — under uf's own name.
|
|
37
|
+
*
|
|
38
|
+
* The same operations Vitest groups under `vi`, so a suite being ported keeps
|
|
39
|
+
* its shape; the name is uf's, because borrowing another tool's brand for it
|
|
40
|
+
* would be claiming something uf has not earned.
|
|
41
|
+
*
|
|
42
|
+
* A namespace rather than loose named exports, because several of these names
|
|
43
|
+
* are generic enough to collide: `@uniflowed/testing` re-exports both this
|
|
44
|
+
* package and `@uniflowed/react-testing`, and both have a `waitFor`.
|
|
45
|
+
*/
|
|
46
|
+
export { UnsupportedError, uft } from "./internal/namespace.js";
|
|
47
|
+
|
|
48
|
+
export { RunawayTimersError } from "./internal/timers.js";
|
|
32
49
|
|
|
33
50
|
export { DEFAULT_TIMEOUT_MS, NAME_SEPARATOR } from "./internal/run.js";
|
|
34
51
|
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Internal to `@uniflowed/test`: `expect.any`, `expect.objectContaining`, and
|
|
4
|
+
// the rest of the matchers that stand in for a value instead of being one.
|
|
5
|
+
//
|
|
6
|
+
// They exist because most assertions are about the parts of a value a test
|
|
7
|
+
// controls, and equality is about all of it. `expect(user).toEqual({ id:
|
|
8
|
+
// expect.any(String), name: "uf" })` says what the test means; spelling out the
|
|
9
|
+
// id would either be a lie or a second source of truth.
|
|
10
|
+
//
|
|
11
|
+
// The mechanism is one method. A matcher is an object carrying
|
|
12
|
+
// `asymmetricMatch(received)`, and `equals` asks any value it meets whether it
|
|
13
|
+
// is one — so a matcher nested six levels down inside an expected object works
|
|
14
|
+
// for the same reason a top-level one does, with no special case anywhere.
|
|
15
|
+
// Vitest and Jest use the same protocol, which is why a matcher from either
|
|
16
|
+
// works here.
|
|
17
|
+
|
|
18
|
+
// `equality` and this module are mutually recursive, and that is the domain:
|
|
19
|
+
// comparing two values has to recognise a matcher, and a matcher has to compare
|
|
20
|
+
// recursively. ESM handles a cycle between hoisted function declarations — by
|
|
21
|
+
// the time any matcher runs, both modules have finished evaluating — and the
|
|
22
|
+
// alternative was wiring the comparison in at load, which is an import-time side
|
|
23
|
+
// effect and the one thing every shipped module here is forbidden.
|
|
24
|
+
import { equals } from "./equality.js";
|
|
25
|
+
|
|
26
|
+
/** The brand every matcher carries, so `equals` can recognise one. */
|
|
27
|
+
const ASYMMETRIC = "$$uf.asymmetricMatch";
|
|
28
|
+
|
|
29
|
+
/** A stand-in for a value, rather than a value. */
|
|
30
|
+
export type AsymmetricMatcher = {
|
|
31
|
+
readonly [typeof ASYMMETRIC]: true,
|
|
32
|
+
readonly asymmetricMatch: (received: mixed) => boolean,
|
|
33
|
+
readonly toString: () => string,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether `value` is a matcher rather than something to compare against.
|
|
38
|
+
*
|
|
39
|
+
* Duck-typed on `asymmetricMatch` as well as uf's own brand, so a matcher built
|
|
40
|
+
* by Jest or Vitest — or by a project's own helper — is recognised too. The
|
|
41
|
+
* protocol is the interface; the brand is only a fast path.
|
|
42
|
+
*/
|
|
43
|
+
export function isAsymmetric(value: mixed): boolean {
|
|
44
|
+
if (value == null || typeof value !== "object") {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const candidate = value as $FlowFixMe;
|
|
48
|
+
return candidate[ASYMMETRIC] === true || typeof candidate.asymmetricMatch === "function";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Ask a matcher whether `received` satisfies it. */
|
|
52
|
+
export function matchesAsymmetric(matcher: mixed, received: mixed): boolean {
|
|
53
|
+
return (matcher as $FlowFixMe).asymmetricMatch(received) === true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Build a matcher from a predicate and how it describes itself. */
|
|
57
|
+
function matcher(label: string, predicate: (received: mixed) => boolean): AsymmetricMatcher {
|
|
58
|
+
return {
|
|
59
|
+
[ASYMMETRIC]: true,
|
|
60
|
+
asymmetricMatch: predicate,
|
|
61
|
+
toString: () => label,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Anything constructed by `constructor`, or any value of a primitive's type.
|
|
67
|
+
*
|
|
68
|
+
* `expect.any(String)` accepts `"uf"` as well as `new String("uf")`, because a
|
|
69
|
+
* string literal is not an instance of anything and a test that wrote
|
|
70
|
+
* `expect.any(String)` meant the type rather than the wrapper. The same for
|
|
71
|
+
* `Number`, `Boolean`, `BigInt`, `Symbol` and `Function`.
|
|
72
|
+
*/
|
|
73
|
+
export function any(constructor: mixed): AsymmetricMatcher {
|
|
74
|
+
const name = (constructor as $FlowFixMe)?.name ?? String(constructor);
|
|
75
|
+
return matcher(`Any<${name}>`, (received) => {
|
|
76
|
+
switch (constructor) {
|
|
77
|
+
case String:
|
|
78
|
+
return typeof received === "string" || received instanceof String;
|
|
79
|
+
case Number:
|
|
80
|
+
return typeof received === "number" || received instanceof Number;
|
|
81
|
+
case Boolean:
|
|
82
|
+
return typeof received === "boolean" || received instanceof Boolean;
|
|
83
|
+
case BigInt:
|
|
84
|
+
return typeof received === "bigint";
|
|
85
|
+
case Symbol:
|
|
86
|
+
return typeof received === "symbol";
|
|
87
|
+
case Function:
|
|
88
|
+
return typeof received === "function";
|
|
89
|
+
case Object:
|
|
90
|
+
// `Object` means "any non-null object", which is what a test asking for
|
|
91
|
+
// one means — not "has Object.prototype in its chain", which a
|
|
92
|
+
// null-prototype object would fail and a test would find baffling.
|
|
93
|
+
return received != null && (typeof received === "object" || typeof received === "function");
|
|
94
|
+
default:
|
|
95
|
+
return typeof constructor === "function" && received instanceof constructor;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Anything at all except `null` and `undefined`. */
|
|
101
|
+
export function anything(): AsymmetricMatcher {
|
|
102
|
+
return matcher("Anything", (received) => received != null);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* An object with at least these properties, compared with `equals`.
|
|
107
|
+
*
|
|
108
|
+
* The comparison is recursive, so a matcher nested inside `expected` works.
|
|
109
|
+
*/
|
|
110
|
+
export function objectContaining(expected: interface {}): AsymmetricMatcher {
|
|
111
|
+
return matcher(`ObjectContaining(${describe(expected)})`, (received) => {
|
|
112
|
+
if (received == null || typeof received !== "object") {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
const target = received as $FlowFixMe;
|
|
116
|
+
const source = expected as $FlowFixMe;
|
|
117
|
+
for (const key of Object.keys(source)) {
|
|
118
|
+
if (!(key in target) || !equals(target[key], source[key])) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** An array holding at least these elements, in any order. */
|
|
127
|
+
export function arrayContaining(expected: $ReadOnlyArray<mixed>): AsymmetricMatcher {
|
|
128
|
+
return matcher(`ArrayContaining(${describe(expected)})`, (received) => {
|
|
129
|
+
if (!Array.isArray(received)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return expected.every((wanted) => received.some((item) => equals(item, wanted)));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A string containing `substring`. */
|
|
137
|
+
export function stringContaining(substring: string): AsymmetricMatcher {
|
|
138
|
+
return matcher(`StringContaining(${JSON.stringify(substring)})`, (received) => {
|
|
139
|
+
return typeof received === "string" && received.includes(substring);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** A string the pattern matches. */
|
|
144
|
+
export function stringMatching(pattern: string | RegExp): AsymmetricMatcher {
|
|
145
|
+
return matcher(`StringMatching(${String(pattern)})`, (received) => {
|
|
146
|
+
if (typeof received !== "string") {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
return typeof pattern === "string" ? received.includes(pattern) : pattern.test(received);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Default digits of precision for `closeTo`, matching Jest and Vitest. */
|
|
154
|
+
const CLOSE_TO_DIGITS = 2;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A number within `10 ** -digits / 2` of `expected`.
|
|
158
|
+
*
|
|
159
|
+
* The same tolerance `toBeCloseTo` uses, so the asymmetric form and the matcher
|
|
160
|
+
* agree — a test that moves an assertion from one to the other should not have
|
|
161
|
+
* its verdict change.
|
|
162
|
+
*/
|
|
163
|
+
export function closeTo(expected: number, digits: number = CLOSE_TO_DIGITS): AsymmetricMatcher {
|
|
164
|
+
const tolerance = 10 ** -digits / 2;
|
|
165
|
+
return matcher(`CloseTo(${expected}, ${digits})`, (received) => {
|
|
166
|
+
return typeof received === "number" && Math.abs(received - expected) < tolerance;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** A matcher that holds when `inner` does not. */
|
|
171
|
+
export function not(inner: AsymmetricMatcher): AsymmetricMatcher {
|
|
172
|
+
return matcher(`Not(${inner.toString()})`, (received) => !matchesAsymmetric(inner, received));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** A short rendering of an expected value, for a matcher's own name. */
|
|
176
|
+
function describe(value: mixed): string {
|
|
177
|
+
try {
|
|
178
|
+
return JSON.stringify(value) ?? String(value);
|
|
179
|
+
} catch {
|
|
180
|
+
// A cyclic or otherwise unserialisable expectation still has to have a
|
|
181
|
+
// name; what it is called matters less than that naming it cannot throw.
|
|
182
|
+
return String(value);
|
|
183
|
+
}
|
|
184
|
+
}
|
package/internal/equality.js
CHANGED
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
// * `toEqual` ignores `undefined` properties and `toStrictEqual` does not,
|
|
19
19
|
// which is the one place the two matchers differ besides prototypes.
|
|
20
20
|
|
|
21
|
+
import { isAsymmetric, matchesAsymmetric } from "./asymmetric.js";
|
|
22
|
+
|
|
21
23
|
/** How strictly two values are compared. */
|
|
22
24
|
export type Strictness = "loose" | "strict";
|
|
23
25
|
|
|
24
|
-
type Pair = {|
|
|
26
|
+
type Pair = {| readonly left: mixed, readonly right: mixed |};
|
|
25
27
|
|
|
26
28
|
/** Longest rendering of one value inside a failure message. */
|
|
27
29
|
export const MAX_RENDER_BYTES: number = 4096;
|
|
@@ -58,10 +60,15 @@ function ownKeys(value: interface {}, strictness: Strictness): Array<string | sy
|
|
|
58
60
|
}
|
|
59
61
|
// `toEqual` treats an absent property and one set to `undefined` as the
|
|
60
62
|
// same thing, so a key holding `undefined` is not a difference.
|
|
61
|
-
return keys.filter((key) => (value
|
|
63
|
+
return keys.filter((key) => (value as $FlowFixMe)[key] !== undefined);
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
function sameSet(
|
|
66
|
+
function sameSet(
|
|
67
|
+
left: Set<mixed>,
|
|
68
|
+
right: Set<mixed>,
|
|
69
|
+
seen: Array<Pair>,
|
|
70
|
+
strictness: Strictness,
|
|
71
|
+
): boolean {
|
|
65
72
|
if (left.size !== right.size) {
|
|
66
73
|
return false;
|
|
67
74
|
}
|
|
@@ -114,6 +121,16 @@ export function equals(
|
|
|
114
121
|
if (Object.is(left, right)) {
|
|
115
122
|
return true;
|
|
116
123
|
}
|
|
124
|
+
// A matcher stands in for a value rather than being one, and either side may
|
|
125
|
+
// be the expectation depending on which way round the caller passed them.
|
|
126
|
+
// Asking here rather than at the top level is what makes a matcher nested six
|
|
127
|
+
// levels down work for the same reason a top-level one does.
|
|
128
|
+
if (isAsymmetric(right)) {
|
|
129
|
+
return matchesAsymmetric(right, left);
|
|
130
|
+
}
|
|
131
|
+
if (isAsymmetric(left)) {
|
|
132
|
+
return matchesAsymmetric(left, right);
|
|
133
|
+
}
|
|
117
134
|
if (!isObject(left) || !isObject(right)) {
|
|
118
135
|
return false;
|
|
119
136
|
}
|
|
@@ -150,14 +167,14 @@ export function equals(
|
|
|
150
167
|
}
|
|
151
168
|
if (ArrayBuffer.isView(left) && ArrayBuffer.isView(right)) {
|
|
152
169
|
const leftBytes = new Uint8Array(
|
|
153
|
-
(left
|
|
154
|
-
(left
|
|
155
|
-
(left
|
|
170
|
+
(left as $FlowFixMe).buffer,
|
|
171
|
+
(left as $FlowFixMe).byteOffset,
|
|
172
|
+
(left as $FlowFixMe).byteLength,
|
|
156
173
|
);
|
|
157
174
|
const rightBytes = new Uint8Array(
|
|
158
|
-
(right
|
|
159
|
-
(right
|
|
160
|
-
(right
|
|
175
|
+
(right as $FlowFixMe).buffer,
|
|
176
|
+
(right as $FlowFixMe).byteOffset,
|
|
177
|
+
(right as $FlowFixMe).byteLength,
|
|
161
178
|
);
|
|
162
179
|
if (leftBytes.length !== rightBytes.length) {
|
|
163
180
|
return false;
|
|
@@ -176,8 +193,8 @@ export function equals(
|
|
|
176
193
|
return false;
|
|
177
194
|
}
|
|
178
195
|
|
|
179
|
-
const leftKeys = ownKeys(
|
|
180
|
-
const rightKeys = ownKeys(
|
|
196
|
+
const leftKeys = ownKeys(left as $FlowFixMe, strictness);
|
|
197
|
+
const rightKeys = ownKeys(right as $FlowFixMe, strictness);
|
|
181
198
|
if (leftKeys.length !== rightKeys.length) {
|
|
182
199
|
return false;
|
|
183
200
|
}
|
|
@@ -185,7 +202,7 @@ export function equals(
|
|
|
185
202
|
if (!Object.prototype.hasOwnProperty.call(right, key)) {
|
|
186
203
|
return false;
|
|
187
204
|
}
|
|
188
|
-
if (!equals((left
|
|
205
|
+
if (!equals((left as $FlowFixMe)[key], (right as $FlowFixMe)[key], nested, strictness)) {
|
|
189
206
|
return false;
|
|
190
207
|
}
|
|
191
208
|
}
|
|
@@ -199,6 +216,9 @@ export function equals(
|
|
|
199
216
|
* fine, missing or different ones are not.
|
|
200
217
|
*/
|
|
201
218
|
export function matchesObject(received: mixed, expected: mixed, seen: Array<Pair> = []): boolean {
|
|
219
|
+
if (isAsymmetric(expected)) {
|
|
220
|
+
return matchesAsymmetric(expected, received);
|
|
221
|
+
}
|
|
202
222
|
if (!isObject(expected) || !isObject(received)) {
|
|
203
223
|
return equals(received, expected, seen);
|
|
204
224
|
}
|
|
@@ -215,11 +235,11 @@ export function matchesObject(received: mixed, expected: mixed, seen: Array<Pair
|
|
|
215
235
|
}
|
|
216
236
|
return expected.every((item, index) => matchesObject(received[index], item, nested));
|
|
217
237
|
}
|
|
218
|
-
for (const key of ownKeys(
|
|
238
|
+
for (const key of ownKeys(expected as $FlowFixMe, "loose")) {
|
|
219
239
|
if (!Object.prototype.hasOwnProperty.call(received, key)) {
|
|
220
240
|
return false;
|
|
221
241
|
}
|
|
222
|
-
if (!matchesObject((received
|
|
242
|
+
if (!matchesObject((received as $FlowFixMe)[key], (expected as $FlowFixMe)[key], nested)) {
|
|
223
243
|
return false;
|
|
224
244
|
}
|
|
225
245
|
}
|
|
@@ -238,6 +258,33 @@ function quoteString(value: string): string {
|
|
|
238
258
|
* or refuses. Depth, breadth and total size are bounded, because a failure
|
|
239
259
|
* message that scrolls the terminal is a failure message nobody reads.
|
|
240
260
|
*/
|
|
261
|
+
/**
|
|
262
|
+
* An element's opening tag, or `null` if this is not an element.
|
|
263
|
+
*
|
|
264
|
+
* Duck-typed rather than `instanceof Element`, because this module must not
|
|
265
|
+
* depend on a DOM existing: a test process without a document simply never has
|
|
266
|
+
* a value that answers to this shape.
|
|
267
|
+
*/
|
|
268
|
+
function elementTag(value: mixed): string | null {
|
|
269
|
+
const node: $FlowFixMe = value;
|
|
270
|
+
if (
|
|
271
|
+
node == null ||
|
|
272
|
+
typeof node.tagName !== "string" ||
|
|
273
|
+
typeof node.getAttribute !== "function" ||
|
|
274
|
+
node.nodeType !== 1
|
|
275
|
+
) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
const name = node.tagName.toLowerCase();
|
|
279
|
+
const attributes = Array.from(node.attributes ?? [])
|
|
280
|
+
.slice(0, MAX_RENDER_ENTRIES)
|
|
281
|
+
.map((attribute: $FlowFixMe) => ` ${attribute.name}="${attribute.value}"`)
|
|
282
|
+
.join("");
|
|
283
|
+
const text = (node.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
284
|
+
const shown = text.length > 40 ? `${text.slice(0, 40)}…` : text;
|
|
285
|
+
return shown === "" ? `<${name}${attributes} />` : `<${name}${attributes}>${shown}</${name}>`;
|
|
286
|
+
}
|
|
287
|
+
|
|
241
288
|
export function render(value: mixed, depth: number = 0, seen: Array<mixed> = []): string {
|
|
242
289
|
const text = renderInner(value, depth, seen);
|
|
243
290
|
return text.length > MAX_RENDER_BYTES ? `${text.slice(0, MAX_RENDER_BYTES)}… (elided)` : text;
|
|
@@ -262,7 +309,7 @@ function renderInner(value: mixed, depth: number, seen: Array<mixed>): string {
|
|
|
262
309
|
case "symbol":
|
|
263
310
|
return String(value);
|
|
264
311
|
case "function": {
|
|
265
|
-
const name = (value
|
|
312
|
+
const name = (value as $FlowFixMe).name;
|
|
266
313
|
return name === "" ? "[Function (anonymous)]" : `[Function ${name}]`;
|
|
267
314
|
}
|
|
268
315
|
default:
|
|
@@ -278,6 +325,14 @@ function renderInner(value: mixed, depth: number, seen: Array<mixed>): string {
|
|
|
278
325
|
const nested = [...seen, value];
|
|
279
326
|
const inner = (item: mixed) => renderInner(item, depth + 1, nested);
|
|
280
327
|
|
|
328
|
+
// An element, before the object branch reaches it. A DOM node's own
|
|
329
|
+
// properties are event-listener maps and parent pointers, so rendering it as
|
|
330
|
+
// an object buries the failure under a page of internals — and the whole
|
|
331
|
+
// document, through the parent chain. Its opening tag is what a reader needs.
|
|
332
|
+
const tag = elementTag(value);
|
|
333
|
+
if (tag != null) {
|
|
334
|
+
return tag;
|
|
335
|
+
}
|
|
281
336
|
if (value instanceof Date) {
|
|
282
337
|
return `Date(${value.toISOString()})`;
|
|
283
338
|
}
|
|
@@ -289,19 +344,22 @@ function renderInner(value: mixed, depth: number, seen: Array<mixed>): string {
|
|
|
289
344
|
}
|
|
290
345
|
if (value instanceof Set) {
|
|
291
346
|
const items = [...value].slice(0, MAX_RENDER_ENTRIES).map(inner);
|
|
292
|
-
const more =
|
|
347
|
+
const more =
|
|
348
|
+
value.size > MAX_RENDER_ENTRIES ? `, …${value.size - MAX_RENDER_ENTRIES} more` : "";
|
|
293
349
|
return `Set { ${items.join(", ")}${more} }`;
|
|
294
350
|
}
|
|
295
351
|
if (value instanceof Map) {
|
|
296
352
|
const items = [...value]
|
|
297
353
|
.slice(0, MAX_RENDER_ENTRIES)
|
|
298
354
|
.map(([key, item]) => `${inner(key)} => ${inner(item)}`);
|
|
299
|
-
const more =
|
|
355
|
+
const more =
|
|
356
|
+
value.size > MAX_RENDER_ENTRIES ? `, …${value.size - MAX_RENDER_ENTRIES} more` : "";
|
|
300
357
|
return `Map { ${items.join(", ")}${more} }`;
|
|
301
358
|
}
|
|
302
359
|
if (Array.isArray(value)) {
|
|
303
360
|
const items = value.slice(0, MAX_RENDER_ENTRIES).map(inner);
|
|
304
|
-
const more =
|
|
361
|
+
const more =
|
|
362
|
+
value.length > MAX_RENDER_ENTRIES ? `, …${value.length - MAX_RENDER_ENTRIES} more` : "";
|
|
305
363
|
return `[${items.join(", ")}${more}]`;
|
|
306
364
|
}
|
|
307
365
|
|
|
@@ -318,5 +376,7 @@ function renderInner(value: mixed, depth: number, seen: Array<mixed>): string {
|
|
|
318
376
|
prototype != null && prototype.constructor != null && prototype.constructor.name !== "Object"
|
|
319
377
|
? `${prototype.constructor.name} `
|
|
320
378
|
: "";
|
|
321
|
-
return entries.length === 0
|
|
379
|
+
return entries.length === 0
|
|
380
|
+
? `${constructorName}{}`
|
|
381
|
+
: `${constructorName}{ ${entries.join(", ")}${more} }`;
|
|
322
382
|
}
|