@alloy-js/core 0.23.0-dev.10 → 0.23.0-dev.11
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/dist/devtools/index.html +29 -17
- package/dist/src/binder.d.ts.map +1 -1
- package/dist/src/binder.js +5 -0
- package/dist/src/binder.js.map +1 -1
- package/dist/src/components/For.d.ts.map +1 -1
- package/dist/src/components/For.js +1 -1
- package/dist/src/components/For.js.map +1 -1
- package/dist/src/components/List.d.ts.map +1 -1
- package/dist/src/components/List.js +1 -1
- package/dist/src/components/List.js.map +1 -1
- package/dist/src/components/Switch.d.ts.map +1 -1
- package/dist/src/components/Switch.js +1 -1
- package/dist/src/components/Switch.js.map +1 -1
- package/dist/src/debug/diagnostics.test.js +3 -2
- package/dist/src/debug/diagnostics.test.js.map +1 -1
- package/dist/src/debug/effects.d.ts +12 -4
- package/dist/src/debug/effects.d.ts.map +1 -1
- package/dist/src/debug/effects.js +182 -52
- package/dist/src/debug/effects.js.map +1 -1
- package/dist/src/debug/effects.test.js +213 -41
- package/dist/src/debug/effects.test.js.map +1 -1
- package/dist/src/debug/files.d.ts.map +1 -1
- package/dist/src/debug/files.js +7 -18
- package/dist/src/debug/files.js.map +1 -1
- package/dist/src/debug/files.test.js +13 -36
- package/dist/src/debug/files.test.js.map +1 -1
- package/dist/src/debug/index.d.ts +4 -2
- package/dist/src/debug/index.d.ts.map +1 -1
- package/dist/src/debug/index.js +4 -2
- package/dist/src/debug/index.js.map +1 -1
- package/dist/src/debug/message-format.test.d.ts +2 -0
- package/dist/src/debug/message-format.test.d.ts.map +1 -0
- package/dist/src/debug/message-format.test.js +700 -0
- package/dist/src/debug/message-format.test.js.map +1 -0
- package/dist/src/debug/render-tree-orphans.test.d.ts +2 -0
- package/dist/src/debug/render-tree-orphans.test.d.ts.map +1 -0
- package/dist/src/debug/render-tree-orphans.test.js +297 -0
- package/dist/src/debug/render-tree-orphans.test.js.map +1 -0
- package/dist/src/debug/render.d.ts.map +1 -1
- package/dist/src/debug/render.js +83 -130
- package/dist/src/debug/render.js.map +1 -1
- package/dist/src/debug/render.test.js +91 -128
- package/dist/src/debug/render.test.js.map +1 -1
- package/dist/src/debug/symbols.d.ts +6 -5
- package/dist/src/debug/symbols.d.ts.map +1 -1
- package/dist/src/debug/symbols.js +46 -23
- package/dist/src/debug/symbols.js.map +1 -1
- package/dist/src/debug/symbols.test.js +15 -26
- package/dist/src/debug/symbols.test.js.map +1 -1
- package/dist/src/debug/trace-writer.d.ts +55 -0
- package/dist/src/debug/trace-writer.d.ts.map +1 -0
- package/dist/src/debug/trace-writer.js +658 -0
- package/dist/src/debug/trace-writer.js.map +1 -0
- package/dist/src/debug/trace.d.ts +10 -10
- package/dist/src/debug/trace.d.ts.map +1 -1
- package/dist/src/debug/trace.js +23 -20
- package/dist/src/debug/trace.js.map +1 -1
- package/dist/src/devtools/devtools-protocol.d.ts +318 -161
- package/dist/src/devtools/devtools-protocol.d.ts.map +1 -1
- package/dist/src/devtools/devtools-server.browser.d.ts +0 -5
- package/dist/src/devtools/devtools-server.browser.d.ts.map +1 -1
- package/dist/src/devtools/devtools-server.browser.js +0 -3
- package/dist/src/devtools/devtools-server.browser.js.map +1 -1
- package/dist/src/devtools/devtools-server.d.ts +0 -6
- package/dist/src/devtools/devtools-server.d.ts.map +1 -1
- package/dist/src/devtools/devtools-server.js +212 -24
- package/dist/src/devtools/devtools-server.js.map +1 -1
- package/dist/src/devtools/devtools-transport.d.ts +2 -2
- package/dist/src/devtools/devtools-transport.d.ts.map +1 -1
- package/dist/src/devtools/devtools-transport.js +2 -2
- package/dist/src/devtools/devtools-transport.js.map +1 -1
- package/dist/src/devtools-entry.browser.d.ts +1 -1
- package/dist/src/devtools-entry.browser.d.ts.map +1 -1
- package/dist/src/devtools-entry.browser.js.map +1 -1
- package/dist/src/devtools-entry.d.ts +1 -1
- package/dist/src/devtools-entry.d.ts.map +1 -1
- package/dist/src/devtools-entry.js.map +1 -1
- package/dist/src/diagnostics.d.ts.map +1 -1
- package/dist/src/diagnostics.js +5 -5
- package/dist/src/diagnostics.js.map +1 -1
- package/dist/src/reactivity.d.ts +13 -2
- package/dist/src/reactivity.d.ts.map +1 -1
- package/dist/src/reactivity.js +96 -13
- package/dist/src/reactivity.js.map +1 -1
- package/dist/src/render.d.ts.map +1 -1
- package/dist/src/render.js +84 -30
- package/dist/src/render.js.map +1 -1
- package/dist/src/scheduler.d.ts +5 -0
- package/dist/src/scheduler.d.ts.map +1 -1
- package/dist/src/scheduler.js +94 -23
- package/dist/src/scheduler.js.map +1 -1
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +11 -5
- package/dist/src/utils.js.map +1 -1
- package/dist/testing/devtools-utils.d.ts +12 -3
- package/dist/testing/devtools-utils.d.ts.map +1 -1
- package/dist/testing/devtools-utils.js +26 -4
- package/dist/testing/devtools-utils.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/binder.ts +47 -38
- package/src/components/For.tsx +14 -10
- package/src/components/List.tsx +7 -4
- package/src/components/Switch.tsx +11 -7
- package/src/debug/diagnostics.test.tsx +3 -2
- package/src/debug/effects.test.tsx +248 -36
- package/src/debug/effects.ts +276 -62
- package/src/debug/files.test.tsx +15 -35
- package/src/debug/files.ts +11 -11
- package/src/debug/index.ts +4 -0
- package/src/debug/message-format.test.tsx +759 -0
- package/src/debug/render-tree-orphans.test.tsx +344 -0
- package/src/debug/render.test.tsx +96 -118
- package/src/debug/render.ts +183 -124
- package/src/debug/symbols.test.tsx +19 -20
- package/src/debug/symbols.ts +106 -23
- package/src/debug/trace-writer.ts +969 -0
- package/src/debug/trace.ts +25 -28
- package/src/devtools/devtools-protocol.ts +361 -176
- package/src/devtools/devtools-server.browser.ts +0 -9
- package/src/devtools/devtools-server.ts +210 -32
- package/src/devtools/devtools-transport.ts +4 -4
- package/src/devtools-entry.browser.ts +11 -15
- package/src/devtools-entry.ts +9 -15
- package/src/diagnostics.ts +14 -5
- package/src/reactivity.ts +113 -17
- package/src/render.ts +104 -30
- package/src/scheduler.ts +145 -26
- package/src/utils.tsx +7 -4
- package/temp/api.json +142 -20
- package/testing/devtools-utils.ts +46 -4
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import WebSocket from "ws";
|
|
3
|
+
import {
|
|
4
|
+
createMessageCollector,
|
|
5
|
+
filterRenderTreeMessages,
|
|
6
|
+
type DevtoolsMessage,
|
|
7
|
+
} from "../../testing/devtools-utils.js";
|
|
8
|
+
import { For } from "../components/For.jsx";
|
|
9
|
+
import { Output } from "../components/Output.jsx";
|
|
10
|
+
import { Show } from "../components/Show.jsx";
|
|
11
|
+
import {
|
|
12
|
+
enableDevtools,
|
|
13
|
+
resetDevtoolsServerForTests,
|
|
14
|
+
} from "../devtools/devtools-server.js";
|
|
15
|
+
import { ref } from "../reactivity.js";
|
|
16
|
+
import { renderAsync } from "../render.js";
|
|
17
|
+
import { flushJobsAsync } from "../scheduler.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build a snapshot of active render tree nodes from a stream of messages.
|
|
21
|
+
* Returns a Map of nodeId → { parentId, kind, name, value }.
|
|
22
|
+
*/
|
|
23
|
+
function buildRenderTreeSnapshot(messages: DevtoolsMessage[]) {
|
|
24
|
+
const active = new Map<
|
|
25
|
+
number,
|
|
26
|
+
{ parentId: number | null; kind: string; name?: string; value?: string }
|
|
27
|
+
>();
|
|
28
|
+
|
|
29
|
+
for (const msg of messages) {
|
|
30
|
+
if (msg.type === "render:node_added") {
|
|
31
|
+
active.set(msg.id as number, {
|
|
32
|
+
parentId: msg.parent_id as number | null,
|
|
33
|
+
kind: msg.kind as string,
|
|
34
|
+
name: msg.name as string | undefined,
|
|
35
|
+
value: msg.value as string | undefined,
|
|
36
|
+
});
|
|
37
|
+
} else if (msg.type === "render:node_removed") {
|
|
38
|
+
active.delete(msg.id as number);
|
|
39
|
+
} else if (msg.type === "render:reset") {
|
|
40
|
+
active.clear();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return active;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Find orphaned nodes: nodes whose parentId references a node not in the active set.
|
|
49
|
+
*/
|
|
50
|
+
function findOrphans(
|
|
51
|
+
active: Map<
|
|
52
|
+
number,
|
|
53
|
+
{ parentId: number | null; kind: string; name?: string; value?: string }
|
|
54
|
+
>,
|
|
55
|
+
) {
|
|
56
|
+
const orphans: Array<{
|
|
57
|
+
id: number;
|
|
58
|
+
parentId: number;
|
|
59
|
+
kind: string;
|
|
60
|
+
name?: string;
|
|
61
|
+
value?: string;
|
|
62
|
+
}> = [];
|
|
63
|
+
|
|
64
|
+
for (const [id, node] of active) {
|
|
65
|
+
if (node.parentId !== null && !active.has(node.parentId)) {
|
|
66
|
+
orphans.push({ id, ...node, parentId: node.parentId! });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return orphans;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe("render tree node orphans", () => {
|
|
74
|
+
let socket: WebSocket | undefined;
|
|
75
|
+
|
|
76
|
+
beforeEach(async () => {
|
|
77
|
+
const server = await enableDevtools({ port: 0 });
|
|
78
|
+
socket = new WebSocket(`ws://127.0.0.1:${server.port}`);
|
|
79
|
+
await new Promise<void>((resolve, reject) => {
|
|
80
|
+
socket?.once("open", resolve);
|
|
81
|
+
socket?.once("error", reject);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
afterEach(async () => {
|
|
86
|
+
if (socket) {
|
|
87
|
+
socket.close();
|
|
88
|
+
socket = undefined;
|
|
89
|
+
}
|
|
90
|
+
await resetDevtoolsServerForTests();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("no orphans after removing items from For component", async () => {
|
|
94
|
+
const items = ref(["a", "b", "c", "d"]);
|
|
95
|
+
const collector = await createMessageCollector(socket!);
|
|
96
|
+
|
|
97
|
+
function ItemView(props: { item: string }) {
|
|
98
|
+
return (
|
|
99
|
+
<>
|
|
100
|
+
<>{props.item}</>
|
|
101
|
+
<> - suffix</>
|
|
102
|
+
</>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await renderAsync(
|
|
107
|
+
<Output>
|
|
108
|
+
<For each={items}>{(item) => <ItemView item={item} />}</For>
|
|
109
|
+
</Output>,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const renderMessages = await collector.waitForRender();
|
|
113
|
+
const allMessages = [...filterRenderTreeMessages(renderMessages)];
|
|
114
|
+
|
|
115
|
+
items.value = ["a", "d"];
|
|
116
|
+
await flushJobsAsync();
|
|
117
|
+
|
|
118
|
+
const updateMessages = await collector.waitForFlush();
|
|
119
|
+
allMessages.push(...filterRenderTreeMessages(updateMessages));
|
|
120
|
+
|
|
121
|
+
const active = buildRenderTreeSnapshot(allMessages);
|
|
122
|
+
const orphans = findOrphans(active);
|
|
123
|
+
|
|
124
|
+
expect(orphans).toEqual([]);
|
|
125
|
+
collector.stop();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("no orphans after toggling Show component with nested content", async () => {
|
|
129
|
+
const visible = ref(true);
|
|
130
|
+
const collector = await createMessageCollector(socket!);
|
|
131
|
+
|
|
132
|
+
function Inner() {
|
|
133
|
+
return (
|
|
134
|
+
<>
|
|
135
|
+
<>nested fragment</>
|
|
136
|
+
<>more content</>
|
|
137
|
+
</>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await renderAsync(
|
|
142
|
+
<Output>
|
|
143
|
+
<Show when={visible.value}>
|
|
144
|
+
<Inner />
|
|
145
|
+
</Show>
|
|
146
|
+
</Output>,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const renderMessages = await collector.waitForRender();
|
|
150
|
+
const allMessages = [...filterRenderTreeMessages(renderMessages)];
|
|
151
|
+
|
|
152
|
+
visible.value = false;
|
|
153
|
+
await flushJobsAsync();
|
|
154
|
+
const offMessages = await collector.waitForFlush();
|
|
155
|
+
allMessages.push(...filterRenderTreeMessages(offMessages));
|
|
156
|
+
|
|
157
|
+
visible.value = true;
|
|
158
|
+
await flushJobsAsync();
|
|
159
|
+
const onMessages = await collector.waitForFlush();
|
|
160
|
+
allMessages.push(...filterRenderTreeMessages(onMessages));
|
|
161
|
+
|
|
162
|
+
const active = buildRenderTreeSnapshot(allMessages);
|
|
163
|
+
const orphans = findOrphans(active);
|
|
164
|
+
|
|
165
|
+
expect(orphans).toEqual([]);
|
|
166
|
+
collector.stop();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("no orphans after replacing all items in For component", async () => {
|
|
170
|
+
const items = ref(["x", "y", "z"]);
|
|
171
|
+
const collector = await createMessageCollector(socket!);
|
|
172
|
+
|
|
173
|
+
function Nested(props: { label: string }) {
|
|
174
|
+
return (
|
|
175
|
+
<>
|
|
176
|
+
<>{props.label}</>
|
|
177
|
+
<>
|
|
178
|
+
<>deep nesting</>
|
|
179
|
+
</>
|
|
180
|
+
</>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
await renderAsync(
|
|
185
|
+
<Output>
|
|
186
|
+
<For each={items}>{(item) => <Nested label={item} />}</For>
|
|
187
|
+
</Output>,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const renderMessages = await collector.waitForRender();
|
|
191
|
+
const allMessages = [...filterRenderTreeMessages(renderMessages)];
|
|
192
|
+
|
|
193
|
+
items.value = ["1", "2"];
|
|
194
|
+
await flushJobsAsync();
|
|
195
|
+
const replaceMessages = await collector.waitForFlush();
|
|
196
|
+
allMessages.push(...filterRenderTreeMessages(replaceMessages));
|
|
197
|
+
|
|
198
|
+
const active = buildRenderTreeSnapshot(allMessages);
|
|
199
|
+
const orphans = findOrphans(active);
|
|
200
|
+
|
|
201
|
+
expect(orphans).toEqual([]);
|
|
202
|
+
collector.stop();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("no orphans when For re-renders and some items are kept (cached subtree path)", async () => {
|
|
206
|
+
// When `For` re-renders after items change, items that haven't changed
|
|
207
|
+
// reuse the same CustomContext object (via mapJoin's slot cache). The
|
|
208
|
+
// element cache detects these as cached elements and takes the `isCached`
|
|
209
|
+
// path in recordSubtreeAdded. If the idToNode reverse mapping isn't
|
|
210
|
+
// restored for cached nodes, their children won't be cleaned up on the
|
|
211
|
+
// next re-render, creating orphans.
|
|
212
|
+
const items = ref(["a", "b", "c"]);
|
|
213
|
+
const collector = await createMessageCollector(socket!);
|
|
214
|
+
|
|
215
|
+
function Item(props: { value: string }) {
|
|
216
|
+
return <>item: {props.value}</>;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
await renderAsync(
|
|
220
|
+
<Output>
|
|
221
|
+
<For each={items}>{(item) => <Item value={item} />}</For>
|
|
222
|
+
</Output>,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const renderMessages = await collector.waitForRender();
|
|
226
|
+
const allMessages = [...filterRenderTreeMessages(renderMessages)];
|
|
227
|
+
|
|
228
|
+
// Change items but keep "a" — its CustomContext stays cached
|
|
229
|
+
items.value = ["a", "x"];
|
|
230
|
+
await flushJobsAsync();
|
|
231
|
+
const update1 = await collector.waitForFlush();
|
|
232
|
+
allMessages.push(...filterRenderTreeMessages(update1));
|
|
233
|
+
|
|
234
|
+
// Change again, keeping "a" — cached subtree re-added
|
|
235
|
+
items.value = ["a", "y", "z"];
|
|
236
|
+
await flushJobsAsync();
|
|
237
|
+
const update2 = await collector.waitForFlush();
|
|
238
|
+
allMessages.push(...filterRenderTreeMessages(update2));
|
|
239
|
+
|
|
240
|
+
const active = buildRenderTreeSnapshot(allMessages);
|
|
241
|
+
const orphans = findOrphans(active);
|
|
242
|
+
|
|
243
|
+
expect(orphans).toEqual([]);
|
|
244
|
+
|
|
245
|
+
// No duplicate text nodes under the same parent
|
|
246
|
+
const textByParent = new Map<number, string[]>();
|
|
247
|
+
for (const [_id, node] of active) {
|
|
248
|
+
if (node.kind === "text" && node.value && node.parentId !== null) {
|
|
249
|
+
const list = textByParent.get(node.parentId) ?? [];
|
|
250
|
+
list.push(node.value);
|
|
251
|
+
textByParent.set(node.parentId, list);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const [parentId, values] of textByParent) {
|
|
255
|
+
const dupes = values.filter((v, i) => values.indexOf(v) !== i);
|
|
256
|
+
expect(dupes, `Duplicate text nodes under parent ${parentId}`).toEqual(
|
|
257
|
+
[],
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
collector.stop();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("no orphans when For with separators re-renders and keeps some items", async () => {
|
|
265
|
+
// mapJoin creates separator/joiner slots between items. These separators
|
|
266
|
+
// contain printHook subtrees that are part of the cached element tree.
|
|
267
|
+
// When items change, the cached subtree is re-added but separator fragments
|
|
268
|
+
// from previous renders must be properly cascade-deleted.
|
|
269
|
+
//
|
|
270
|
+
// This test checks the DB directly for orphaned rows.
|
|
271
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
272
|
+
const os = await import("node:os");
|
|
273
|
+
const path = await import("node:path");
|
|
274
|
+
const fs = await import("node:fs");
|
|
275
|
+
const { initTrace, closeTrace } = await import("../debug/trace-writer.js");
|
|
276
|
+
const { Block } = await import("../components/Block.jsx");
|
|
277
|
+
const { Indent } = await import("../components/Indent.jsx");
|
|
278
|
+
|
|
279
|
+
const tracePath = path.join(
|
|
280
|
+
os.tmpdir(),
|
|
281
|
+
`alloy-orphan-test-${Date.now()}.db`,
|
|
282
|
+
);
|
|
283
|
+
await initTrace(tracePath);
|
|
284
|
+
|
|
285
|
+
const items = ref(["a", "b", "c", "d"]);
|
|
286
|
+
const collector = await createMessageCollector(socket!);
|
|
287
|
+
|
|
288
|
+
// Nested blocks simulate the deeply nested printHook trees
|
|
289
|
+
// found in real emitters (e.g., flight-instructor)
|
|
290
|
+
function Item(props: { value: string }) {
|
|
291
|
+
return (
|
|
292
|
+
<Block>
|
|
293
|
+
<Indent>
|
|
294
|
+
<>label: {props.value}</>
|
|
295
|
+
</Indent>
|
|
296
|
+
</Block>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
await renderAsync(
|
|
301
|
+
<Output>
|
|
302
|
+
<For each={items} joiner={"\n"}>
|
|
303
|
+
{(item) => <Item value={item} />}
|
|
304
|
+
</For>
|
|
305
|
+
</Output>,
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
await collector.waitForRender();
|
|
309
|
+
|
|
310
|
+
// Change items, keeping "a" — triggers cached subtree path for "a"
|
|
311
|
+
items.value = ["a", "x", "y"];
|
|
312
|
+
await flushJobsAsync();
|
|
313
|
+
await collector.waitForFlush();
|
|
314
|
+
|
|
315
|
+
// Change again
|
|
316
|
+
items.value = ["a", "z"];
|
|
317
|
+
await flushJobsAsync();
|
|
318
|
+
await collector.waitForFlush();
|
|
319
|
+
|
|
320
|
+
// Third change to exercise multiple cached re-add cycles
|
|
321
|
+
items.value = ["a", "w", "v", "u"];
|
|
322
|
+
await flushJobsAsync();
|
|
323
|
+
await collector.waitForFlush();
|
|
324
|
+
|
|
325
|
+
// Check the DB directly for orphaned nodes
|
|
326
|
+
closeTrace();
|
|
327
|
+
const traceDb = new DatabaseSync(tracePath, { readOnly: true });
|
|
328
|
+
const orphans = traceDb
|
|
329
|
+
.prepare(
|
|
330
|
+
`SELECT n.id, n.parent_id, n.kind, n.name
|
|
331
|
+
FROM render_nodes n
|
|
332
|
+
WHERE n.parent_id IS NOT NULL
|
|
333
|
+
AND NOT EXISTS (SELECT 1 FROM render_nodes p WHERE p.id = n.parent_id)`,
|
|
334
|
+
)
|
|
335
|
+
.all();
|
|
336
|
+
|
|
337
|
+
traceDb.close();
|
|
338
|
+
fs.unlinkSync(tracePath);
|
|
339
|
+
|
|
340
|
+
expect(orphans).toEqual([]);
|
|
341
|
+
|
|
342
|
+
collector.stop();
|
|
343
|
+
});
|
|
344
|
+
});
|