@webstir-io/webstir-frontend 0.1.40 → 0.1.41
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 +124 -60
- package/dist/assets/imageOptimizer.js +10 -15
- package/dist/assets/precompression.js +1 -1
- package/dist/builders/contentBuilder.js +102 -90
- package/dist/builders/cssBuilder.js +25 -19
- package/dist/builders/htmlBuilder.js +57 -42
- package/dist/builders/index.js +1 -1
- package/dist/builders/jsBuilder.js +219 -76
- package/dist/builders/staticAssetsBuilder.js +27 -9
- package/dist/builders/types.d.ts +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +6 -30
- package/dist/config/manifest.js +7 -6
- package/dist/config/paths.js +2 -2
- package/dist/config/schema.d.ts +8 -0
- package/dist/config/schema.js +7 -6
- package/dist/config/setup.js +1 -1
- package/dist/config/workspace.js +11 -9
- package/dist/core/constants.d.ts +1 -1
- package/dist/core/constants.js +5 -5
- package/dist/core/diagnostics.js +1 -1
- package/dist/core/pages.js +4 -4
- package/dist/hooks.js +3 -3
- package/dist/html/criticalCss.js +6 -3
- package/dist/html/htmlSecurity.d.ts +6 -1
- package/dist/html/htmlSecurity.js +28 -14
- package/dist/html/lazyLoad.js +1 -1
- package/dist/html/pageScaffold.js +1 -1
- package/dist/html/resourceHints.js +5 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/inspect.d.ts +2 -0
- package/dist/inspect.js +110 -0
- package/dist/modes/ssg/metadata.js +4 -4
- package/dist/modes/ssg/routing.js +2 -5
- package/dist/modes/ssg/seo.js +5 -5
- package/dist/modes/ssg/views.js +17 -11
- package/dist/operations.js +18 -10
- package/dist/pipeline.d.ts +1 -0
- package/dist/pipeline.js +6 -1
- package/dist/provider.js +28 -24
- package/dist/runtime/boundary.d.ts +28 -0
- package/dist/runtime/boundary.js +247 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/types.d.ts +52 -0
- package/dist/utils/fs.d.ts +11 -10
- package/dist/utils/fs.js +48 -20
- package/dist/utils/glob.d.ts +8 -0
- package/dist/utils/glob.js +21 -0
- package/dist/utils/hash.js +1 -2
- package/dist/utils/pagePaths.js +2 -2
- package/package.json +19 -14
- package/scripts/publish.sh +2 -94
- package/scripts/update-contract.sh +12 -10
- package/src/assets/assetManifest.ts +39 -29
- package/src/assets/imageOptimizer.ts +91 -82
- package/src/assets/precompression.ts +22 -16
- package/src/builders/contentBuilder.ts +1224 -1149
- package/src/builders/cssBuilder.ts +466 -417
- package/src/builders/htmlBuilder.ts +511 -448
- package/src/builders/index.ts +7 -7
- package/src/builders/jsBuilder.ts +538 -280
- package/src/builders/staticAssetsBuilder.ts +166 -135
- package/src/builders/types.ts +7 -6
- package/src/cli.ts +66 -90
- package/src/config/manifest.ts +16 -14
- package/src/config/paths.ts +5 -5
- package/src/config/schema.ts +38 -37
- package/src/config/setup.ts +7 -7
- package/src/config/workspace.ts +118 -116
- package/src/config/workspaceManifest.ts +14 -14
- package/src/core/constants.ts +62 -62
- package/src/core/diagnostics.ts +26 -26
- package/src/core/pages.ts +19 -19
- package/src/hooks.ts +128 -118
- package/src/html/criticalCss.ts +84 -77
- package/src/html/htmlSecurity.ts +107 -66
- package/src/html/lazyLoad.ts +22 -19
- package/src/html/pageScaffold.ts +37 -28
- package/src/html/resourceHints.ts +83 -74
- package/src/index.ts +2 -0
- package/src/inspect.ts +158 -0
- package/src/modes/ssg/metadata.ts +53 -51
- package/src/modes/ssg/routing.ts +177 -177
- package/src/modes/ssg/seo.ts +208 -200
- package/src/modes/ssg/validation.ts +31 -25
- package/src/modes/ssg/views.ts +257 -238
- package/src/operations.ts +105 -95
- package/src/pipeline.ts +81 -69
- package/src/provider.ts +184 -176
- package/src/runtime/boundary.ts +325 -0
- package/src/runtime/index.ts +1 -0
- package/src/types.ts +107 -48
- package/src/utils/changedFile.ts +22 -22
- package/src/utils/fs.ts +73 -26
- package/src/utils/glob.ts +38 -0
- package/src/utils/hash.ts +2 -4
- package/src/utils/pagePaths.ts +35 -23
- package/src/utils/pathMatch.ts +26 -23
- package/tests/add-page-defaults.test.js +44 -39
- package/tests/bundlerParity.test.js +252 -0
- package/tests/cli.contract.test.js +13 -0
- package/tests/content-pages.test.js +108 -13
- package/tests/css-app-imports.test.js +22 -11
- package/tests/css-page-imports.test.js +26 -13
- package/tests/diagnostics.test.js +39 -36
- package/tests/features.test.js +48 -43
- package/tests/hooks.test.js +58 -42
- package/tests/htmlSecurity.test.js +66 -0
- package/tests/inspect.test.js +148 -0
- package/tests/provider.integration.test.js +71 -20
- package/tests/runtime.test.js +493 -0
- package/tests/ssg-defaults.test.js +284 -177
- package/tests/ssg-guardrails.test.js +51 -51
- package/tsconfig.json +3 -10
- package/dist/watch/frontendFiles.d.ts +0 -3
- package/dist/watch/frontendFiles.js +0 -25
- package/dist/watch/hotUpdateTracker.d.ts +0 -51
- package/dist/watch/hotUpdateTracker.js +0 -205
- package/dist/watch/pipelineHelpers.d.ts +0 -26
- package/dist/watch/pipelineHelpers.js +0 -177
- package/dist/watch/types.d.ts +0 -27
- package/dist/watch/types.js +0 -1
- package/dist/watch/watchCoordinator.d.ts +0 -36
- package/dist/watch/watchCoordinator.js +0 -551
- package/dist/watch/watchDaemon.d.ts +0 -17
- package/dist/watch/watchDaemon.js +0 -127
- package/dist/watch/watchReporter.d.ts +0 -21
- package/dist/watch/watchReporter.js +0 -64
- package/scripts/smoke.mjs +0 -35
- package/src/watch/frontendFiles.ts +0 -32
- package/src/watch/hotUpdateTracker.ts +0 -285
- package/src/watch/pipelineHelpers.ts +0 -242
- package/src/watch/types.ts +0 -23
- package/src/watch/watchCoordinator.ts +0 -666
- package/src/watch/watchDaemon.ts +0 -144
- package/src/watch/watchReporter.ts +0 -98
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createAbortController,
|
|
6
|
+
createCleanupScope,
|
|
7
|
+
defineBoundary,
|
|
8
|
+
listen,
|
|
9
|
+
scheduleInterval,
|
|
10
|
+
scheduleTimeout,
|
|
11
|
+
trackObserver,
|
|
12
|
+
} from '../dist/runtime/index.js';
|
|
13
|
+
|
|
14
|
+
test('cleanup scope continues reverse disposal after a cleanup throws', async () => {
|
|
15
|
+
const events = [];
|
|
16
|
+
const scope = createCleanupScope();
|
|
17
|
+
const error = new Error('cleanup failed');
|
|
18
|
+
|
|
19
|
+
scope.add(() => {
|
|
20
|
+
events.push('first');
|
|
21
|
+
});
|
|
22
|
+
scope.add(() => {
|
|
23
|
+
events.push('second');
|
|
24
|
+
throw error;
|
|
25
|
+
});
|
|
26
|
+
scope.add(() => events.push('third'));
|
|
27
|
+
|
|
28
|
+
await assert.rejects(() => scope.dispose(), error);
|
|
29
|
+
|
|
30
|
+
assert.deepEqual(events, ['third', 'second', 'first']);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('cleanup scope disposal is idempotent', async () => {
|
|
34
|
+
let calls = 0;
|
|
35
|
+
const scope = createCleanupScope();
|
|
36
|
+
|
|
37
|
+
scope.add(() => {
|
|
38
|
+
calls += 1;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await scope.dispose();
|
|
42
|
+
await scope.dispose();
|
|
43
|
+
|
|
44
|
+
assert.equal(calls, 1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('boundary mount and unmount reuse the same instance safely', async () => {
|
|
48
|
+
const events = [];
|
|
49
|
+
const boundary = defineBoundary({
|
|
50
|
+
mount(root, scope) {
|
|
51
|
+
events.push('mount');
|
|
52
|
+
const button = createFakeButton();
|
|
53
|
+
const onClick = () => {
|
|
54
|
+
events.push('click');
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
button.addEventListener('click', onClick);
|
|
58
|
+
scope.add(() => {
|
|
59
|
+
events.push('cleanup');
|
|
60
|
+
button.removeEventListener('click', onClick);
|
|
61
|
+
});
|
|
62
|
+
root.append(button);
|
|
63
|
+
return { button };
|
|
64
|
+
},
|
|
65
|
+
unmount(state, scope) {
|
|
66
|
+
events.push('unmount');
|
|
67
|
+
scope.add(() => {
|
|
68
|
+
events.push('unmount-cleanup');
|
|
69
|
+
});
|
|
70
|
+
state.button.remove();
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const root = createFakeRoot();
|
|
75
|
+
|
|
76
|
+
const first = await boundary.mount(root);
|
|
77
|
+
first.button.dispatchEvent({ type: 'click' });
|
|
78
|
+
assert.deepEqual(events, ['mount', 'click']);
|
|
79
|
+
|
|
80
|
+
await boundary.unmount();
|
|
81
|
+
first.button.dispatchEvent({ type: 'click' });
|
|
82
|
+
assert.deepEqual(events, ['mount', 'click', 'unmount', 'unmount-cleanup', 'cleanup']);
|
|
83
|
+
|
|
84
|
+
const second = await boundary.mount(root);
|
|
85
|
+
assert.notEqual(second, first);
|
|
86
|
+
|
|
87
|
+
await boundary.unmount();
|
|
88
|
+
assert.deepEqual(events, [
|
|
89
|
+
'mount',
|
|
90
|
+
'click',
|
|
91
|
+
'unmount',
|
|
92
|
+
'unmount-cleanup',
|
|
93
|
+
'cleanup',
|
|
94
|
+
'mount',
|
|
95
|
+
'unmount',
|
|
96
|
+
'unmount-cleanup',
|
|
97
|
+
'cleanup',
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('boundary restores hot state when snapshotState and restoreState are provided', async () => {
|
|
102
|
+
const events = [];
|
|
103
|
+
const boundary = defineBoundary({
|
|
104
|
+
mount() {
|
|
105
|
+
events.push('mount');
|
|
106
|
+
return { label: 'initial' };
|
|
107
|
+
},
|
|
108
|
+
snapshotState(state) {
|
|
109
|
+
events.push(`snapshot:${state.label}`);
|
|
110
|
+
return { label: state.label };
|
|
111
|
+
},
|
|
112
|
+
restoreState(_root, _scope, hotState) {
|
|
113
|
+
events.push(`restore:${hotState.label}`);
|
|
114
|
+
return { label: hotState.label };
|
|
115
|
+
},
|
|
116
|
+
unmount(state) {
|
|
117
|
+
events.push(`unmount:${state.label}`);
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const root = createFakeRoot();
|
|
122
|
+
const first = await boundary.mount(root);
|
|
123
|
+
first.label = 'persisted';
|
|
124
|
+
|
|
125
|
+
await boundary.unmount();
|
|
126
|
+
|
|
127
|
+
const second = await boundary.mount(root);
|
|
128
|
+
|
|
129
|
+
assert.equal(second.label, 'persisted');
|
|
130
|
+
assert.deepEqual(events, [
|
|
131
|
+
'mount',
|
|
132
|
+
'snapshot:persisted',
|
|
133
|
+
'unmount:persisted',
|
|
134
|
+
'restore:persisted',
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('boundary remounts with fresh state when no hot state hooks are provided', async () => {
|
|
139
|
+
const boundary = defineBoundary({
|
|
140
|
+
mount() {
|
|
141
|
+
return { label: 'initial' };
|
|
142
|
+
},
|
|
143
|
+
unmount(state) {
|
|
144
|
+
state.label = 'unmounted';
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const root = createFakeRoot();
|
|
149
|
+
const first = await boundary.mount(root);
|
|
150
|
+
first.label = 'persisted';
|
|
151
|
+
|
|
152
|
+
await boundary.unmount();
|
|
153
|
+
|
|
154
|
+
const second = await boundary.mount(root);
|
|
155
|
+
|
|
156
|
+
assert.equal(second.label, 'initial');
|
|
157
|
+
assert.notEqual(second, first);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('boundary ignores snapshotted hot state when restoreState is not provided', async () => {
|
|
161
|
+
const events = [];
|
|
162
|
+
const boundary = defineBoundary({
|
|
163
|
+
mount() {
|
|
164
|
+
events.push('mount');
|
|
165
|
+
return { label: 'initial' };
|
|
166
|
+
},
|
|
167
|
+
snapshotState(state) {
|
|
168
|
+
events.push(`snapshot:${state.label}`);
|
|
169
|
+
return { label: state.label };
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const root = createFakeRoot();
|
|
174
|
+
const first = await boundary.mount(root);
|
|
175
|
+
first.label = 'persisted';
|
|
176
|
+
|
|
177
|
+
await boundary.unmount();
|
|
178
|
+
const second = await boundary.mount(root);
|
|
179
|
+
|
|
180
|
+
assert.equal(second.label, 'initial');
|
|
181
|
+
assert.notEqual(second, first);
|
|
182
|
+
assert.deepEqual(events, ['mount', 'snapshot:persisted', 'mount']);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('managed side-effect helpers dispose listeners, timers, observers, and abort controllers', async () => {
|
|
186
|
+
const events = [];
|
|
187
|
+
const target = createFakeEventTarget();
|
|
188
|
+
const scheduledTimeouts = [];
|
|
189
|
+
const scheduledIntervals = [];
|
|
190
|
+
const originalSetTimeout = globalThis.setTimeout;
|
|
191
|
+
const originalClearTimeout = globalThis.clearTimeout;
|
|
192
|
+
const originalSetInterval = globalThis.setInterval;
|
|
193
|
+
const originalClearInterval = globalThis.clearInterval;
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
globalThis.setTimeout = (callback, delay, ...args) => {
|
|
197
|
+
const handle = { cleared: false, callback, delay, args };
|
|
198
|
+
scheduledTimeouts.push(handle);
|
|
199
|
+
return handle;
|
|
200
|
+
};
|
|
201
|
+
globalThis.clearTimeout = (handle) => {
|
|
202
|
+
handle.cleared = true;
|
|
203
|
+
};
|
|
204
|
+
globalThis.setInterval = (callback, delay, ...args) => {
|
|
205
|
+
const handle = { cleared: false, callback, delay, args };
|
|
206
|
+
scheduledIntervals.push(handle);
|
|
207
|
+
return handle;
|
|
208
|
+
};
|
|
209
|
+
globalThis.clearInterval = (handle) => {
|
|
210
|
+
handle.cleared = true;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const boundary = defineBoundary({
|
|
214
|
+
mount(root, scope) {
|
|
215
|
+
const onPing = () => {
|
|
216
|
+
events.push('ping');
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
listen(scope, target, 'ping', onPing);
|
|
220
|
+
scheduleTimeout(
|
|
221
|
+
scope,
|
|
222
|
+
() => {
|
|
223
|
+
events.push('timeout-fired');
|
|
224
|
+
},
|
|
225
|
+
25,
|
|
226
|
+
);
|
|
227
|
+
scheduleInterval(
|
|
228
|
+
scope,
|
|
229
|
+
() => {
|
|
230
|
+
events.push('interval-fired');
|
|
231
|
+
},
|
|
232
|
+
50,
|
|
233
|
+
);
|
|
234
|
+
trackObserver(scope, {
|
|
235
|
+
disconnect() {
|
|
236
|
+
events.push('observer-disconnect');
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
scope.add(() => {
|
|
240
|
+
events.push('scope-cleanup');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const abortController = createAbortController(scope);
|
|
244
|
+
events.push(`aborted:${abortController.signal.aborted}`);
|
|
245
|
+
|
|
246
|
+
root.append(createFakeButton());
|
|
247
|
+
return { abortController };
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const root = createFakeRoot();
|
|
252
|
+
const state = await boundary.mount(root);
|
|
253
|
+
target.dispatchEvent({ type: 'ping' });
|
|
254
|
+
assert.deepEqual(events, ['aborted:false', 'ping']);
|
|
255
|
+
|
|
256
|
+
await boundary.unmount();
|
|
257
|
+
|
|
258
|
+
target.dispatchEvent({ type: 'ping' });
|
|
259
|
+
assert.equal(target.listenerCount('ping'), 0);
|
|
260
|
+
assert.equal(scheduledTimeouts[0].cleared, true);
|
|
261
|
+
assert.equal(scheduledIntervals[0].cleared, true);
|
|
262
|
+
assert.equal(state.abortController.signal.aborted, true);
|
|
263
|
+
assert.deepEqual(events, ['aborted:false', 'ping', 'scope-cleanup', 'observer-disconnect']);
|
|
264
|
+
} finally {
|
|
265
|
+
globalThis.setTimeout = originalSetTimeout;
|
|
266
|
+
globalThis.clearTimeout = originalClearTimeout;
|
|
267
|
+
globalThis.setInterval = originalSetInterval;
|
|
268
|
+
globalThis.clearInterval = originalClearInterval;
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('nested boundaries unmount children before parent cleanup', async () => {
|
|
273
|
+
const events = [];
|
|
274
|
+
|
|
275
|
+
const childBoundary = defineBoundary({
|
|
276
|
+
mount(root, scope) {
|
|
277
|
+
events.push('child-mount');
|
|
278
|
+
const button = createFakeButton();
|
|
279
|
+
const onClick = () => {
|
|
280
|
+
events.push('child-click');
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
button.addEventListener('click', onClick);
|
|
284
|
+
scope.add(() => {
|
|
285
|
+
events.push('child-cleanup');
|
|
286
|
+
button.removeEventListener('click', onClick);
|
|
287
|
+
});
|
|
288
|
+
root.append(button);
|
|
289
|
+
return { button };
|
|
290
|
+
},
|
|
291
|
+
unmount(state, scope) {
|
|
292
|
+
events.push('child-unmount');
|
|
293
|
+
scope.add(() => {
|
|
294
|
+
events.push('child-unmount-cleanup');
|
|
295
|
+
});
|
|
296
|
+
state.button.remove();
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const parentBoundary = defineBoundary({
|
|
301
|
+
async mount(root, scope) {
|
|
302
|
+
events.push('parent-mount');
|
|
303
|
+
const childRoot = createFakeRoot();
|
|
304
|
+
const child = await scope.mountChild(childBoundary, childRoot);
|
|
305
|
+
|
|
306
|
+
scope.add(() => {
|
|
307
|
+
events.push('parent-cleanup');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
root.append(childRoot);
|
|
311
|
+
return { child, childRoot };
|
|
312
|
+
},
|
|
313
|
+
unmount(state, scope) {
|
|
314
|
+
events.push('parent-unmount');
|
|
315
|
+
scope.add(() => {
|
|
316
|
+
events.push('parent-unmount-cleanup');
|
|
317
|
+
});
|
|
318
|
+
state.childRoot.remove();
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const root = createFakeRoot();
|
|
323
|
+
const state = await parentBoundary.mount(root);
|
|
324
|
+
assert.equal(state.childRoot.children[0].listenerCount('click'), 1);
|
|
325
|
+
|
|
326
|
+
await state.child.unmount();
|
|
327
|
+
await state.child.mount(state.childRoot);
|
|
328
|
+
|
|
329
|
+
assert.equal(state.childRoot.children[0].listenerCount('click'), 1);
|
|
330
|
+
await parentBoundary.unmount();
|
|
331
|
+
|
|
332
|
+
assert.deepEqual(events, [
|
|
333
|
+
'parent-mount',
|
|
334
|
+
'child-mount',
|
|
335
|
+
'child-unmount',
|
|
336
|
+
'child-unmount-cleanup',
|
|
337
|
+
'child-cleanup',
|
|
338
|
+
'child-mount',
|
|
339
|
+
'child-unmount',
|
|
340
|
+
'child-unmount-cleanup',
|
|
341
|
+
'child-cleanup',
|
|
342
|
+
'parent-unmount',
|
|
343
|
+
'parent-unmount-cleanup',
|
|
344
|
+
'parent-cleanup',
|
|
345
|
+
]);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('failed parent mount cleans mounted children and can mount again cleanly', async () => {
|
|
349
|
+
const events = [];
|
|
350
|
+
let failParentMount = true;
|
|
351
|
+
|
|
352
|
+
const childBoundary = defineBoundary({
|
|
353
|
+
mount(root, scope) {
|
|
354
|
+
events.push('child-mount');
|
|
355
|
+
const button = createFakeButton();
|
|
356
|
+
const onClick = () => {};
|
|
357
|
+
button.addEventListener('click', onClick);
|
|
358
|
+
scope.add(() => {
|
|
359
|
+
events.push('child-cleanup');
|
|
360
|
+
button.removeEventListener('click', onClick);
|
|
361
|
+
});
|
|
362
|
+
root.append(button);
|
|
363
|
+
return { button };
|
|
364
|
+
},
|
|
365
|
+
unmount: (state) => {
|
|
366
|
+
events.push('child-unmount');
|
|
367
|
+
state.button.remove();
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const parentBoundary = defineBoundary({
|
|
372
|
+
async mount(root, scope) {
|
|
373
|
+
events.push('parent-mount');
|
|
374
|
+
const childRoot = createFakeRoot();
|
|
375
|
+
await scope.mountChild(childBoundary, childRoot);
|
|
376
|
+
root.append(childRoot);
|
|
377
|
+
if (failParentMount) throw new Error('parent failed');
|
|
378
|
+
return { childRoot };
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const root = createFakeRoot();
|
|
383
|
+
await assert.rejects(() => parentBoundary.mount(root), /parent failed/);
|
|
384
|
+
assert.deepEqual(events, ['parent-mount', 'child-mount', 'child-unmount', 'child-cleanup']);
|
|
385
|
+
|
|
386
|
+
failParentMount = false;
|
|
387
|
+
const state = await parentBoundary.mount(root);
|
|
388
|
+
const mountedButton = state.childRoot.children[0];
|
|
389
|
+
assert.equal(mountedButton.listenerCount('click'), 1);
|
|
390
|
+
|
|
391
|
+
await parentBoundary.unmount();
|
|
392
|
+
assert.equal(mountedButton.listenerCount('click'), 0);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
function createFakeRoot() {
|
|
396
|
+
return {
|
|
397
|
+
parentNode: null,
|
|
398
|
+
children: [],
|
|
399
|
+
append(...nodes) {
|
|
400
|
+
for (const node of nodes) {
|
|
401
|
+
node.parentNode = this;
|
|
402
|
+
this.children.push(node);
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
remove() {
|
|
406
|
+
if (!this.parentNode) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
this.parentNode.children = this.parentNode.children.filter((node) => node !== this);
|
|
411
|
+
this.parentNode = null;
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function createFakeButton() {
|
|
417
|
+
const listeners = new Map();
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
listenerCount(type) {
|
|
421
|
+
return listeners.get(type)?.length ?? 0;
|
|
422
|
+
},
|
|
423
|
+
textContent: '',
|
|
424
|
+
parentNode: null,
|
|
425
|
+
addEventListener(type, listener) {
|
|
426
|
+
const list = listeners.get(type) ?? [];
|
|
427
|
+
list.push(listener);
|
|
428
|
+
listeners.set(type, list);
|
|
429
|
+
},
|
|
430
|
+
removeEventListener(type, listener) {
|
|
431
|
+
const list = listeners.get(type);
|
|
432
|
+
if (!list) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
listeners.set(
|
|
437
|
+
type,
|
|
438
|
+
list.filter((candidate) => candidate !== listener),
|
|
439
|
+
);
|
|
440
|
+
},
|
|
441
|
+
dispatchEvent(event) {
|
|
442
|
+
const list = listeners.get(event.type) ?? [];
|
|
443
|
+
for (const listener of list) {
|
|
444
|
+
listener.call(this, event);
|
|
445
|
+
}
|
|
446
|
+
return true;
|
|
447
|
+
},
|
|
448
|
+
remove() {
|
|
449
|
+
if (this.parentNode) {
|
|
450
|
+
this.parentNode.children = this.parentNode.children.filter((node) => node !== this);
|
|
451
|
+
this.parentNode = null;
|
|
452
|
+
}
|
|
453
|
+
this.removed = true;
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function createFakeEventTarget() {
|
|
459
|
+
const listeners = new Map();
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
addEventListener(type, listener) {
|
|
463
|
+
const list = listeners.get(type) ?? [];
|
|
464
|
+
list.push(listener);
|
|
465
|
+
listeners.set(type, list);
|
|
466
|
+
},
|
|
467
|
+
removeEventListener(type, listener) {
|
|
468
|
+
const list = listeners.get(type);
|
|
469
|
+
if (!list) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
listeners.set(
|
|
474
|
+
type,
|
|
475
|
+
list.filter((candidate) => candidate !== listener),
|
|
476
|
+
);
|
|
477
|
+
},
|
|
478
|
+
dispatchEvent(event) {
|
|
479
|
+
const list = listeners.get(event.type) ?? [];
|
|
480
|
+
for (const listener of list) {
|
|
481
|
+
if (typeof listener === 'function') {
|
|
482
|
+
listener.call(this, event);
|
|
483
|
+
} else {
|
|
484
|
+
listener.handleEvent?.call(listener, event);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return true;
|
|
488
|
+
},
|
|
489
|
+
listenerCount(type) {
|
|
490
|
+
return listeners.get(type)?.length ?? 0;
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
}
|