@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.8 → 3.9.3-ultramodern.1
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/cjs/{reactContext.js → localisedUrls/config.js} +14 -10
- package/dist/cjs/localisedUrls/index.js +95 -0
- package/dist/cjs/localisedUrls/normalise.js +87 -0
- package/dist/cjs/localisedUrls/pathname.js +68 -0
- package/dist/cjs/localisedUrls/patterns.js +214 -0
- package/dist/cjs/localisedUrls/redirect.js +104 -0
- package/dist/cjs/localisedUrls/resolve.js +78 -0
- package/dist/cjs/localisedUrls/routeIdentity.js +92 -0
- package/dist/cjs/localisedUrls/routes.js +217 -0
- package/dist/cjs/localisedUrls/types.js +18 -0
- package/dist/cjs/rendererHead.js +18 -5
- package/dist/cjs/request-policy/policyCore.js +1 -1
- package/dist/cjs/request-policy/transport.js +2 -2
- package/dist/cjs/routerState.js +85 -77
- package/dist/cjs/routerStatePlugin.js +63 -9
- package/dist/esm/localisedUrls/config.mjs +11 -0
- package/dist/esm/localisedUrls/index.mjs +8 -0
- package/dist/esm/localisedUrls/normalise.mjs +37 -0
- package/dist/esm/localisedUrls/pathname.mjs +24 -0
- package/dist/esm/localisedUrls/patterns.mjs +152 -0
- package/dist/esm/localisedUrls/redirect.mjs +57 -0
- package/dist/esm/localisedUrls/resolve.mjs +37 -0
- package/dist/esm/localisedUrls/routeIdentity.mjs +54 -0
- package/dist/esm/localisedUrls/routes.mjs +176 -0
- package/dist/esm/localisedUrls/types.mjs +0 -0
- package/dist/esm/rendererHead.mjs +14 -5
- package/dist/esm/request-policy/policyCore.mjs +1 -1
- package/dist/esm/request-policy/transport.mjs +2 -2
- package/dist/esm/routerState.mjs +81 -73
- package/dist/esm/routerStatePlugin.mjs +62 -11
- package/dist/esm-node/localisedUrls/config.mjs +12 -0
- package/dist/esm-node/localisedUrls/index.mjs +9 -0
- package/dist/esm-node/localisedUrls/normalise.mjs +38 -0
- package/dist/esm-node/localisedUrls/pathname.mjs +25 -0
- package/dist/esm-node/localisedUrls/patterns.mjs +153 -0
- package/dist/esm-node/localisedUrls/redirect.mjs +58 -0
- package/dist/esm-node/localisedUrls/resolve.mjs +38 -0
- package/dist/esm-node/localisedUrls/routeIdentity.mjs +55 -0
- package/dist/esm-node/localisedUrls/routes.mjs +177 -0
- package/dist/esm-node/localisedUrls/types.mjs +1 -0
- package/dist/esm-node/rendererHead.mjs +14 -5
- package/dist/esm-node/request-policy/policyCore.mjs +1 -1
- package/dist/esm-node/request-policy/transport.mjs +2 -2
- package/dist/esm-node/routerState.mjs +81 -73
- package/dist/esm-node/routerStatePlugin.mjs +62 -11
- package/dist/types/helmetCompat.d.ts +1 -1
- package/dist/types/localisedUrls/config.d.ts +9 -0
- package/dist/types/localisedUrls/index.d.ts +10 -0
- package/dist/types/localisedUrls/normalise.d.ts +10 -0
- package/dist/types/localisedUrls/pathname.d.ts +4 -0
- package/dist/types/localisedUrls/patterns.d.ts +11 -0
- package/dist/types/localisedUrls/redirect.d.ts +19 -0
- package/dist/types/localisedUrls/resolve.d.ts +7 -0
- package/dist/types/localisedUrls/routeIdentity.d.ts +22 -0
- package/dist/types/localisedUrls/routes.d.ts +3 -0
- package/dist/types/localisedUrls/types.d.ts +14 -0
- package/dist/types/rendererHead.d.ts +5 -3
- package/dist/types/request-policy/types.d.ts +1 -1
- package/dist/types/routeHydration.d.ts +2 -2
- package/dist/types/routerProvider.d.ts +1 -3
- package/dist/types/routerState.d.ts +11 -6
- package/dist/types/routerStatePlugin.d.ts +12 -0
- package/dist/types/routerStateTypes.d.ts +47 -17
- package/package.json +42 -44
- package/src/helmetCompat.ts +1 -1
- package/src/localisedUrls/config.ts +18 -0
- package/src/localisedUrls/index.ts +25 -0
- package/src/localisedUrls/normalise.ts +62 -0
- package/src/localisedUrls/pathname.ts +60 -0
- package/src/localisedUrls/patterns.ts +283 -0
- package/src/localisedUrls/redirect.ts +110 -0
- package/src/localisedUrls/resolve.ts +106 -0
- package/src/localisedUrls/routeIdentity.ts +83 -0
- package/src/localisedUrls/routes.ts +451 -0
- package/src/localisedUrls/types.ts +18 -0
- package/src/rendererHead.ts +25 -8
- package/src/request-policy/policyCore.ts +1 -1
- package/src/request-policy/transport.ts +2 -2
- package/src/request-policy/types.ts +1 -1
- package/src/routeHydration.ts +2 -2
- package/src/routerProvider.ts +1 -1
- package/src/routerState.ts +127 -115
- package/src/routerStatePlugin.ts +106 -10
- package/src/routerStateTypes.ts +47 -17
- package/dist/cjs/buildIdentity.js +0 -73
- package/dist/esm/buildIdentity.mjs +0 -35
- package/dist/esm/reactContext.mjs +0 -7
- package/dist/esm-node/buildIdentity.mjs +0 -36
- package/dist/esm-node/reactContext.mjs +0 -8
- package/dist/types/buildIdentity.d.ts +0 -37
- package/dist/types/reactContext.d.ts +0 -6
- package/rslib.config.mts +0 -19
- package/rstest.config.mts +0 -38
- package/src/buildIdentity.ts +0 -61
- package/src/reactContext.ts +0 -16
- package/tests/buildIdentity.test.ts +0 -51
- package/tests/contextExtensions.test.ts +0 -27
- package/tests/headRuntime.test.tsx +0 -229
- package/tests/rendererHead.test.ts +0 -187
- package/tests/request-policy/browser-policy.test.ts +0 -108
- package/tests/request-policy/client-isolation.test.ts +0 -109
- package/tests/request-policy/node-policy.test.ts +0 -447
- package/tests/request-policy/policyCore.test.ts +0 -129
- package/tests/request-policy/requestContext.test.ts +0 -66
- package/tests/request-policy/requestFactory.outbound-contract.test.ts +0 -290
- package/tests/request-policy/traceparent.test.ts +0 -52
- package/tests/request-policy/transport-retry.test.ts +0 -367
- package/tests/routeHydration.test.ts +0 -180
- package/tests/routerPrefetchPolicy.test.ts +0 -102
- package/tests/routerProvider.test.ts +0 -29
- package/tests/routerState.test.ts +0 -78
- package/tests/routerStatePlugin.test.ts +0 -63
- package/tests/runtimeContextProjection.test.ts +0 -83
- package/tests/safeFailure.test.ts +0 -136
- package/tests/shellStream.test.ts +0 -122
- package/tests/workspaceEvents.test.ts +0 -221
- package/tsconfig.json +0 -10
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
beginHeadRender,
|
|
3
|
-
collectHeadRecord,
|
|
4
|
-
completeHeadRender,
|
|
5
|
-
createConservingWebShellStream,
|
|
6
|
-
} from '../src';
|
|
7
|
-
|
|
8
|
-
const MARKER = '<!--<?- SHELL_STREAM_END ?>-->';
|
|
9
|
-
const encoder = new TextEncoder();
|
|
10
|
-
const headMarker = (props: Record<string, string>) =>
|
|
11
|
-
`<template data-modern-helmet="${props['data-modern-helmet']}"></template>`;
|
|
12
|
-
|
|
13
|
-
const streamFromChunks = (chunks: Uint8Array[]) =>
|
|
14
|
-
new ReadableStream<Uint8Array>({
|
|
15
|
-
start(controller) {
|
|
16
|
-
for (const chunk of chunks) {
|
|
17
|
-
controller.enqueue(chunk);
|
|
18
|
-
}
|
|
19
|
-
controller.close();
|
|
20
|
-
},
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const splitAt = (value: Uint8Array, offsets: number[]) => {
|
|
24
|
-
const chunks: Uint8Array[] = [];
|
|
25
|
-
let cursor = 0;
|
|
26
|
-
for (const offset of offsets) {
|
|
27
|
-
chunks.push(value.slice(cursor, offset));
|
|
28
|
-
cursor = offset;
|
|
29
|
-
}
|
|
30
|
-
chunks.push(value.slice(cursor));
|
|
31
|
-
return chunks;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
describe('conserving web shell stream', () => {
|
|
35
|
-
it('preserves a large multichunk shell and tail across UTF-8 splits', async () => {
|
|
36
|
-
const shell = `α${'s'.repeat(9 * 1024)}`;
|
|
37
|
-
const tail = `尾${'t'.repeat(9 * 1024)}🌐done`;
|
|
38
|
-
const html = `${shell}${MARKER}${tail}`;
|
|
39
|
-
const bytes = encoder.encode(html);
|
|
40
|
-
const markerStart = encoder.encode(shell).length;
|
|
41
|
-
const markerEnd = markerStart + encoder.encode(MARKER).length;
|
|
42
|
-
const chunks = splitAt(bytes, [
|
|
43
|
-
1,
|
|
44
|
-
8192,
|
|
45
|
-
markerStart + 7,
|
|
46
|
-
markerEnd - 3,
|
|
47
|
-
markerEnd + 1,
|
|
48
|
-
bytes.length - 2,
|
|
49
|
-
]);
|
|
50
|
-
|
|
51
|
-
const output = await new Response(
|
|
52
|
-
createConservingWebShellStream(streamFromChunks(chunks), {}, MARKER),
|
|
53
|
-
).text();
|
|
54
|
-
|
|
55
|
-
expect(output).toBe(html);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('reports once and errors when the source closes without a marker', async () => {
|
|
59
|
-
const onMissingMarker = rstest.fn();
|
|
60
|
-
const context = {};
|
|
61
|
-
let published: string[] = [];
|
|
62
|
-
const publish = (records: string[]) => {
|
|
63
|
-
published = records;
|
|
64
|
-
};
|
|
65
|
-
beginHeadRender(context);
|
|
66
|
-
const previous = collectHeadRecord(context, () => 'previous', publish)!;
|
|
67
|
-
completeHeadRender(context, headMarker(previous));
|
|
68
|
-
beginHeadRender(context);
|
|
69
|
-
collectHeadRecord(context, () => 'incomplete', publish);
|
|
70
|
-
const bytes = encoder.encode(`α${'x'.repeat(9 * 1024)}尾`);
|
|
71
|
-
const stream = createConservingWebShellStream(
|
|
72
|
-
streamFromChunks(splitAt(bytes, [1, 8192, bytes.length - 1])),
|
|
73
|
-
context,
|
|
74
|
-
MARKER,
|
|
75
|
-
onMissingMarker,
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
await expect(new Response(stream).text()).rejects.toThrow(
|
|
79
|
-
'React SSR stream ended before the shell marker',
|
|
80
|
-
);
|
|
81
|
-
expect(onMissingMarker).toHaveBeenCalledTimes(1);
|
|
82
|
-
expect(onMissingMarker).toHaveBeenCalledWith(expect.any(Error));
|
|
83
|
-
expect(published).toEqual(['previous']);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test('cancellation waits for the owned source cancellation to finish once', async () => {
|
|
88
|
-
let release!: () => void;
|
|
89
|
-
let started!: () => void;
|
|
90
|
-
const cancelling = new Promise<void>(resolve => {
|
|
91
|
-
started = resolve;
|
|
92
|
-
});
|
|
93
|
-
const gate = new Promise<void>(resolve => {
|
|
94
|
-
release = resolve;
|
|
95
|
-
});
|
|
96
|
-
const cancel = rstest.fn(async () => {
|
|
97
|
-
started();
|
|
98
|
-
await gate;
|
|
99
|
-
});
|
|
100
|
-
const source = new ReadableStream<Uint8Array>({ cancel });
|
|
101
|
-
const reader = createConservingWebShellStream(source, {}, MARKER).getReader();
|
|
102
|
-
let settled = false;
|
|
103
|
-
const cancellation = reader.cancel('stop').then(() => {
|
|
104
|
-
settled = true;
|
|
105
|
-
});
|
|
106
|
-
await cancelling;
|
|
107
|
-
expect(settled).toBe(false);
|
|
108
|
-
release();
|
|
109
|
-
await cancellation;
|
|
110
|
-
expect(cancel).toHaveBeenCalledExactlyOnceWith('stop');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test('cancellation propagates an owned source cleanup failure', async () => {
|
|
114
|
-
const source = new ReadableStream<Uint8Array>({
|
|
115
|
-
cancel() {
|
|
116
|
-
throw new Error('cleanup failed');
|
|
117
|
-
},
|
|
118
|
-
});
|
|
119
|
-
await expect(
|
|
120
|
-
createConservingWebShellStream(source, {}, MARKER).cancel('stop'),
|
|
121
|
-
).rejects.toThrow('cleanup failed');
|
|
122
|
-
});
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import * as contracts from '../src/workspaceEvents';
|
|
2
|
-
|
|
3
|
-
const cases = [
|
|
4
|
-
{
|
|
5
|
-
name: 'ultramodern:navigate',
|
|
6
|
-
valid: { to: '/dashboard', replace: false, state: { from: 'shell' } },
|
|
7
|
-
invalid: [
|
|
8
|
-
{},
|
|
9
|
-
{ to: '' },
|
|
10
|
-
{ to: ' ' },
|
|
11
|
-
{ to: 42 },
|
|
12
|
-
{ to: '/', replace: 'false' },
|
|
13
|
-
{ to: '/', state: [] },
|
|
14
|
-
{ to: '/', state: null },
|
|
15
|
-
],
|
|
16
|
-
validate: contracts.isUltramodernNavigatePayload,
|
|
17
|
-
dispatch: contracts.dispatchUltramodernNavigate,
|
|
18
|
-
on: contracts.onUltramodernNavigate,
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
name: 'ultramodern:route-settled',
|
|
22
|
-
valid: { pathname: '/cs', locale: 'cs', title: 'Home' },
|
|
23
|
-
invalid: [
|
|
24
|
-
{},
|
|
25
|
-
{ pathname: ' ' },
|
|
26
|
-
{ pathname: '/', locale: 'de' },
|
|
27
|
-
{ pathname: '/', locale: null },
|
|
28
|
-
{ pathname: '/', title: '' },
|
|
29
|
-
{ pathname: '/', title: 42 },
|
|
30
|
-
],
|
|
31
|
-
validate: contracts.isUltramodernRouteSettledPayload,
|
|
32
|
-
dispatch: contracts.dispatchUltramodernRouteSettled,
|
|
33
|
-
on: contracts.onUltramodernRouteSettled,
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
name: 'ultramodern:remote-ready',
|
|
37
|
-
valid: { appId: 'catalog', build: 'a', surface: 'ui', version: '1' },
|
|
38
|
-
invalid: [
|
|
39
|
-
{},
|
|
40
|
-
{ appId: 42 },
|
|
41
|
-
{ appId: ' ' },
|
|
42
|
-
{ appId: 'catalog', build: '' },
|
|
43
|
-
{ appId: 'catalog', surface: null },
|
|
44
|
-
{ appId: 'catalog', version: 1 },
|
|
45
|
-
],
|
|
46
|
-
validate: contracts.isUltramodernRemoteReadyPayload,
|
|
47
|
-
dispatch: contracts.dispatchUltramodernRemoteReady,
|
|
48
|
-
on: contracts.onUltramodernRemoteReady,
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
name: 'ultramodern:performance-signal',
|
|
52
|
-
valid: {
|
|
53
|
-
signalId: 'bfcache',
|
|
54
|
-
status: 'pass',
|
|
55
|
-
durationMs: 0,
|
|
56
|
-
detail: { source: 'browser' },
|
|
57
|
-
},
|
|
58
|
-
invalid: [
|
|
59
|
-
{},
|
|
60
|
-
{ signalId: 'unknown', status: 'pass' },
|
|
61
|
-
{ signalId: 'bfcache', status: 'unknown' },
|
|
62
|
-
...[-1, Number.NaN, Number.POSITIVE_INFINITY, '1', null].map(
|
|
63
|
-
durationMs => ({ signalId: 'bfcache', status: 'pass', durationMs }),
|
|
64
|
-
),
|
|
65
|
-
{ signalId: 'bfcache', status: 'pass', detail: [] },
|
|
66
|
-
{ signalId: 'bfcache', status: 'pass', detail: null },
|
|
67
|
-
],
|
|
68
|
-
validate: contracts.isUltramodernPerformanceSignalPayload,
|
|
69
|
-
dispatch: contracts.dispatchUltramodernPerformanceSignal,
|
|
70
|
-
on: contracts.onUltramodernPerformanceSignal,
|
|
71
|
-
},
|
|
72
|
-
] as const;
|
|
73
|
-
|
|
74
|
-
describe('workspace event contracts', () => {
|
|
75
|
-
for (const { name, valid, invalid, validate, dispatch, on } of cases) {
|
|
76
|
-
it(`${name} preserves schema validation and error data`, () => {
|
|
77
|
-
expect(validate(valid)).toBe(true);
|
|
78
|
-
expect(
|
|
79
|
-
contracts.assertUltramodernWorkspaceEventPayload(name, valid),
|
|
80
|
-
).toBe(valid);
|
|
81
|
-
for (const payload of [null, undefined, [], false, 'text', ...invalid]) {
|
|
82
|
-
expect(validate(payload)).toBe(false);
|
|
83
|
-
expect(
|
|
84
|
-
contracts.isUltramodernWorkspaceEventPayload(name, payload),
|
|
85
|
-
).toBe(false);
|
|
86
|
-
let error: unknown;
|
|
87
|
-
try {
|
|
88
|
-
contracts.assertUltramodernWorkspaceEventPayload(name, payload);
|
|
89
|
-
} catch (caught) {
|
|
90
|
-
error = caught;
|
|
91
|
-
}
|
|
92
|
-
expect(error).toBeInstanceOf(
|
|
93
|
-
contracts.UltramodernWorkspaceEventValidationError,
|
|
94
|
-
);
|
|
95
|
-
expect(error).toMatchObject({
|
|
96
|
-
name: 'UltramodernWorkspaceEventValidationError',
|
|
97
|
-
eventName: name,
|
|
98
|
-
payload,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it(`${name} uses native dispatch, preserves payload identity and cleans up`, () => {
|
|
104
|
-
const target = new EventTarget();
|
|
105
|
-
const received: unknown[] = [];
|
|
106
|
-
const handler = (payload: unknown, event: CustomEvent<unknown>) => {
|
|
107
|
-
expect(event).toBeInstanceOf(CustomEvent);
|
|
108
|
-
expect(event.type).toBe(name);
|
|
109
|
-
expect(event.bubbles).toBe(true);
|
|
110
|
-
expect(event.composed).toBe(true);
|
|
111
|
-
expect(event.cancelable).toBe(false);
|
|
112
|
-
expect(event.target).toBe(target);
|
|
113
|
-
expect(event.detail).toBe(payload);
|
|
114
|
-
received.push(payload);
|
|
115
|
-
};
|
|
116
|
-
const unsubscribe = on(target, handler);
|
|
117
|
-
// Each table row correlates the convenience dispatch with its own schema.
|
|
118
|
-
const dispatchPayload = dispatch as (
|
|
119
|
-
target: EventTarget,
|
|
120
|
-
payload: typeof valid,
|
|
121
|
-
) => boolean;
|
|
122
|
-
expect(dispatchPayload(target, valid)).toBe(true);
|
|
123
|
-
expect(received).toEqual([valid]);
|
|
124
|
-
expect(received[0]).toBe(valid);
|
|
125
|
-
unsubscribe();
|
|
126
|
-
unsubscribe();
|
|
127
|
-
dispatchPayload(target, valid);
|
|
128
|
-
expect(received).toEqual([valid]);
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
it('accepts consumer-owned fields, explicit undefined optionals and every signal id', () => {
|
|
133
|
-
const payload = {
|
|
134
|
-
to: '/raw',
|
|
135
|
-
replace: undefined,
|
|
136
|
-
state: undefined,
|
|
137
|
-
extension: 'consumer-owned',
|
|
138
|
-
};
|
|
139
|
-
expect(
|
|
140
|
-
contracts.assertUltramodernWorkspaceEventPayload(
|
|
141
|
-
'ultramodern:navigate',
|
|
142
|
-
payload,
|
|
143
|
-
),
|
|
144
|
-
).toBe(payload);
|
|
145
|
-
expect(
|
|
146
|
-
contracts.isUltramodernRouteSettledPayload({
|
|
147
|
-
pathname: '/',
|
|
148
|
-
locale: undefined,
|
|
149
|
-
title: undefined,
|
|
150
|
-
}),
|
|
151
|
-
).toBe(true);
|
|
152
|
-
for (const signalId of [
|
|
153
|
-
'bfcache',
|
|
154
|
-
'core-web-vitals-rum',
|
|
155
|
-
'duplicate-prefetch-warmup',
|
|
156
|
-
'cache-policy-sanity',
|
|
157
|
-
'save-data-behavior',
|
|
158
|
-
'cloudflare-ssr-cache-hints',
|
|
159
|
-
]) {
|
|
160
|
-
expect(
|
|
161
|
-
contracts.isUltramodernPerformanceSignalPayload({
|
|
162
|
-
signalId,
|
|
163
|
-
status: 'warn',
|
|
164
|
-
}),
|
|
165
|
-
).toBe(true);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('accepts detail-carrying events from another realm', () => {
|
|
170
|
-
const target = new EventTarget();
|
|
171
|
-
const add = rstest.spyOn(target, 'addEventListener');
|
|
172
|
-
const handler = rstest.fn();
|
|
173
|
-
const unsubscribe = contracts.onUltramodernNavigate(target, handler);
|
|
174
|
-
const listener = add.mock.calls[0]?.[1] as EventListener;
|
|
175
|
-
// A CustomEvent from an iframe/foreign realm fails `instanceof`; the
|
|
176
|
-
// listener must duck-type on `detail` instead of rejecting it.
|
|
177
|
-
const payload = { to: '/foreign' };
|
|
178
|
-
listener({ type: 'ultramodern:navigate', detail: payload } as never);
|
|
179
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
180
|
-
expect(handler.mock.calls[0]?.[0]).toBe(payload);
|
|
181
|
-
unsubscribe();
|
|
182
|
-
add.mockRestore();
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('rejects invalid dispatch before notifying listeners', () => {
|
|
186
|
-
const target = new EventTarget();
|
|
187
|
-
const handler = rstest.fn();
|
|
188
|
-
const unsubscribe = contracts.onUltramodernNavigate(target, handler);
|
|
189
|
-
expect(() =>
|
|
190
|
-
contracts.dispatchUltramodernNavigate(target, { to: '' }),
|
|
191
|
-
).toThrow();
|
|
192
|
-
expect(handler).not.toHaveBeenCalled();
|
|
193
|
-
unsubscribe();
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('validates incoming event details and removes only its own listener', () => {
|
|
197
|
-
const target = new EventTarget();
|
|
198
|
-
const add = rstest.spyOn(target, 'addEventListener');
|
|
199
|
-
const remove = rstest.spyOn(target, 'removeEventListener');
|
|
200
|
-
const handler = rstest.fn();
|
|
201
|
-
const first = contracts.onUltramodernNavigate(target, handler);
|
|
202
|
-
const listener = add.mock.calls[0]?.[1] as EventListener;
|
|
203
|
-
for (const event of [
|
|
204
|
-
new Event('ultramodern:navigate'),
|
|
205
|
-
new CustomEvent('ultramodern:navigate', { detail: { to: '' } }),
|
|
206
|
-
]) {
|
|
207
|
-
// Native dispatch reports listener errors asynchronously; invoke the
|
|
208
|
-
// registered listener directly to inspect the precise validation failure.
|
|
209
|
-
expect(() => listener(event)).toThrow();
|
|
210
|
-
}
|
|
211
|
-
expect(handler).not.toHaveBeenCalled();
|
|
212
|
-
const second = contracts.onUltramodernNavigate(target, handler);
|
|
213
|
-
first();
|
|
214
|
-
expect(remove).toHaveBeenCalledWith('ultramodern:navigate', listener);
|
|
215
|
-
contracts.dispatchUltramodernNavigate(target, { to: '/still-subscribed' });
|
|
216
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
217
|
-
second();
|
|
218
|
-
add.mockRestore();
|
|
219
|
-
remove.mockRestore();
|
|
220
|
-
});
|
|
221
|
-
});
|