ripple 0.3.11 → 0.3.13
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/CHANGELOG.md +43 -0
- package/package.json +8 -2
- package/src/compiler/phases/1-parse/index.js +73 -30
- package/src/compiler/phases/2-analyze/index.js +28 -58
- package/src/compiler/phases/3-transform/client/index.js +127 -164
- package/src/compiler/phases/3-transform/segments.js +4 -8
- package/src/compiler/phases/3-transform/server/index.js +210 -360
- package/src/compiler/types/import.d.ts +0 -12
- package/src/compiler/types/index.d.ts +12 -5
- package/src/compiler/types/parse.d.ts +2 -0
- package/src/compiler/utils.js +39 -44
- package/src/helpers.d.ts +2 -0
- package/src/runtime/index-client.js +15 -13
- package/src/runtime/index-server.js +18 -11
- package/src/runtime/internal/client/blocks.js +19 -23
- package/src/runtime/internal/client/constants.js +20 -9
- package/src/runtime/internal/client/index.js +14 -4
- package/src/runtime/internal/client/runtime.js +435 -173
- package/src/runtime/internal/client/try.js +334 -156
- package/src/runtime/internal/client/types.d.ts +26 -0
- package/src/runtime/internal/server/blocks.js +183 -0
- package/src/runtime/internal/server/constants.js +7 -0
- package/src/runtime/internal/server/index.js +780 -148
- package/src/runtime/internal/server/types.d.ts +35 -0
- package/src/server/index.js +1 -1
- package/src/utils/async.js +35 -0
- package/src/utils/builders.js +3 -1
- package/tests/client/__snapshots__/computed-properties.test.rsrx.snap +49 -0
- package/tests/client/__snapshots__/for.test.rsrx.snap +319 -0
- package/tests/client/__snapshots__/html.test.rsrx.snap +40 -0
- package/tests/client/_etc.test.rsrx +7 -0
- package/tests/client/array/{array.static.test.ripple → array.static.test.rsrx} +18 -20
- package/tests/client/async-suspend.test.rsrx +662 -0
- package/tests/client/basic/__snapshots__/basic.attributes.test.rsrx.snap +60 -0
- package/tests/client/basic/__snapshots__/basic.rendering.test.rsrx.snap +59 -0
- package/tests/client/basic/{basic.errors.test.ripple → basic.errors.test.rsrx} +2 -2
- package/tests/client/compiler/__snapshots__/compiler.assignments.test.rsrx.snap +12 -0
- package/tests/client/compiler/__snapshots__/compiler.typescript.test.rsrx.snap +46 -0
- package/tests/client/compiler/{compiler.try-in-function.test.ripple → compiler.try-in-function.test.rsrx} +8 -6
- package/tests/client/composite/__snapshots__/composite.render.test.rsrx.snap +37 -0
- package/tests/client/{function-overload.test.ripple → function-overload.test.rsrx} +1 -1
- package/tests/client/try.test.rsrx +1702 -0
- package/tests/hydration/build-components.js +5 -3
- package/tests/hydration/compiled/client/head.js +11 -11
- package/tests/hydration/compiled/client/mixed-control-flow.js +55 -70
- package/tests/hydration/compiled/client/nested-control-flow.js +72 -88
- package/tests/hydration/compiled/client/try.js +42 -54
- package/tests/hydration/compiled/server/basic.js +491 -369
- package/tests/hydration/compiled/server/composite.js +153 -128
- package/tests/hydration/compiled/server/events.js +166 -145
- package/tests/hydration/compiled/server/for.js +821 -677
- package/tests/hydration/compiled/server/head.js +200 -165
- package/tests/hydration/compiled/server/hmr.js +62 -54
- package/tests/hydration/compiled/server/html-in-template.js +64 -55
- package/tests/hydration/compiled/server/html.js +1477 -1360
- package/tests/hydration/compiled/server/if-children.js +448 -408
- package/tests/hydration/compiled/server/if.js +204 -171
- package/tests/hydration/compiled/server/mixed-control-flow.js +237 -195
- package/tests/hydration/compiled/server/nested-control-flow.js +533 -467
- package/tests/hydration/compiled/server/portal.js +94 -107
- package/tests/hydration/compiled/server/reactivity.js +87 -64
- package/tests/hydration/compiled/server/return.js +1424 -1174
- package/tests/hydration/compiled/server/switch.js +268 -238
- package/tests/hydration/compiled/server/try.js +98 -87
- package/tests/hydration/components/{mixed-control-flow.ripple → mixed-control-flow.rsrx} +2 -2
- package/tests/hydration/components/{try.ripple → try.rsrx} +4 -2
- package/tests/hydration/mixed-control-flow.test.js +14 -0
- package/tests/hydration/nested-control-flow.test.js +50 -48
- package/tests/hydration/try.test.js +25 -0
- package/tests/server/__snapshots__/compiler.test.ripple.snap +0 -32
- package/tests/server/__snapshots__/compiler.test.rsrx.snap +95 -0
- package/tests/server/{compiler.test.ripple → compiler.test.rsrx} +0 -17
- package/tests/server/{html-nesting-validation.test.ripple → html-nesting-validation.test.rsrx} +3 -3
- package/tests/server/streaming-ssr.test.rsrx +115 -0
- package/tests/server/try.test.rsrx +503 -0
- package/tests/utils/compiler-compat-config.test.js +3 -3
- package/tests/utils/vite-plugin-config.test.js +1 -1
- package/tests/utils/vite-plugin-hmr.test.js +5 -5
- package/tsconfig.json +4 -0
- package/types/index.d.ts +13 -23
- package/types/server.d.ts +43 -16
- package/tests/client/_etc.test.ripple +0 -5
- package/tests/client/async-suspend.test.ripple +0 -94
- package/tests/client/try.test.ripple +0 -196
- package/tests/server/streaming-ssr.test.ripple +0 -68
- package/tests/server/try.test.ripple +0 -82
- /package/tests/client/array/{array.copy-within.test.ripple → array.copy-within.test.rsrx} +0 -0
- /package/tests/client/array/{array.derived.test.ripple → array.derived.test.rsrx} +0 -0
- /package/tests/client/array/{array.iteration.test.ripple → array.iteration.test.rsrx} +0 -0
- /package/tests/client/array/{array.mutations.test.ripple → array.mutations.test.rsrx} +0 -0
- /package/tests/client/array/{array.to-methods.test.ripple → array.to-methods.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.attributes.test.ripple → basic.attributes.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.collections.test.ripple → basic.collections.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.components.test.ripple → basic.components.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.events.test.ripple → basic.events.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.get-set.test.ripple → basic.get-set.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.hmr.test.ripple → basic.hmr.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.reactivity.test.ripple → basic.reactivity.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.rendering.test.ripple → basic.rendering.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.styling.test.ripple → basic.styling.test.rsrx} +0 -0
- /package/tests/client/basic/{basic.utilities.test.ripple → basic.utilities.test.rsrx} +0 -0
- /package/tests/client/{boundaries.test.ripple → boundaries.test.rsrx} +0 -0
- /package/tests/client/compiler/{compiler.assignments.test.ripple → compiler.assignments.test.rsrx} +0 -0
- /package/tests/client/compiler/{compiler.attributes.test.ripple → compiler.attributes.test.rsrx} +0 -0
- /package/tests/client/compiler/{compiler.basic.test.ripple → compiler.basic.test.rsrx} +0 -0
- /package/tests/client/compiler/{compiler.regex.test.ripple → compiler.regex.test.rsrx} +0 -0
- /package/tests/client/compiler/{compiler.tracked-access.test.ripple → compiler.tracked-access.test.rsrx} +0 -0
- /package/tests/client/compiler/{compiler.typescript.test.ripple → compiler.typescript.test.rsrx} +0 -0
- /package/tests/client/composite/{composite.dynamic-components.test.ripple → composite.dynamic-components.test.rsrx} +0 -0
- /package/tests/client/composite/{composite.generics.test.ripple → composite.generics.test.rsrx} +0 -0
- /package/tests/client/composite/{composite.props.test.ripple → composite.props.test.rsrx} +0 -0
- /package/tests/client/composite/{composite.reactivity.test.ripple → composite.reactivity.test.rsrx} +0 -0
- /package/tests/client/composite/{composite.render.test.ripple → composite.render.test.rsrx} +0 -0
- /package/tests/client/{computed-properties.test.ripple → computed-properties.test.rsrx} +0 -0
- /package/tests/client/{context.test.ripple → context.test.rsrx} +0 -0
- /package/tests/client/css/{global-additional-cases.test.ripple → global-additional-cases.test.rsrx} +0 -0
- /package/tests/client/css/{global-advanced-selectors.test.ripple → global-advanced-selectors.test.rsrx} +0 -0
- /package/tests/client/css/{global-at-rules.test.ripple → global-at-rules.test.rsrx} +0 -0
- /package/tests/client/css/{global-basic.test.ripple → global-basic.test.rsrx} +0 -0
- /package/tests/client/css/{global-classes-ids.test.ripple → global-classes-ids.test.rsrx} +0 -0
- /package/tests/client/css/{global-combinators.test.ripple → global-combinators.test.rsrx} +0 -0
- /package/tests/client/css/{global-complex-nesting.test.ripple → global-complex-nesting.test.rsrx} +0 -0
- /package/tests/client/css/{global-edge-cases.test.ripple → global-edge-cases.test.rsrx} +0 -0
- /package/tests/client/css/{global-keyframes.test.ripple → global-keyframes.test.rsrx} +0 -0
- /package/tests/client/css/{global-nested.test.ripple → global-nested.test.rsrx} +0 -0
- /package/tests/client/css/{global-pseudo.test.ripple → global-pseudo.test.rsrx} +0 -0
- /package/tests/client/css/{global-scoping.test.ripple → global-scoping.test.rsrx} +0 -0
- /package/tests/client/css/{style-identifier.test.ripple → style-identifier.test.rsrx} +0 -0
- /package/tests/client/{date.test.ripple → date.test.rsrx} +0 -0
- /package/tests/client/{dynamic-elements.test.ripple → dynamic-elements.test.rsrx} +0 -0
- /package/tests/client/{events.test.ripple → events.test.rsrx} +0 -0
- /package/tests/client/{for.test.ripple → for.test.rsrx} +0 -0
- /package/tests/client/{function-overload-import.ripple → function-overload-import.rsrx} +0 -0
- /package/tests/client/{head.test.ripple → head.test.rsrx} +0 -0
- /package/tests/client/{html.test.ripple → html.test.rsrx} +0 -0
- /package/tests/client/{input-value.test.ripple → input-value.test.rsrx} +0 -0
- /package/tests/client/{lazy-destructuring.test.ripple → lazy-destructuring.test.rsrx} +0 -0
- /package/tests/client/{map.test.ripple → map.test.rsrx} +0 -0
- /package/tests/client/{media-query.test.ripple → media-query.test.rsrx} +0 -0
- /package/tests/client/{object.test.ripple → object.test.rsrx} +0 -0
- /package/tests/client/{portal.test.ripple → portal.test.rsrx} +0 -0
- /package/tests/client/{ref.test.ripple → ref.test.rsrx} +0 -0
- /package/tests/client/{return.test.ripple → return.test.rsrx} +0 -0
- /package/tests/client/{set.test.ripple → set.test.rsrx} +0 -0
- /package/tests/client/{svg.test.ripple → svg.test.rsrx} +0 -0
- /package/tests/client/{switch.test.ripple → switch.test.rsrx} +0 -0
- /package/tests/client/{tsx.test.ripple → tsx.test.rsrx} +0 -0
- /package/tests/client/{typescript-generics.test.ripple → typescript-generics.test.rsrx} +0 -0
- /package/tests/client/url/{url.derived.test.ripple → url.derived.test.rsrx} +0 -0
- /package/tests/client/url/{url.parsing.test.ripple → url.parsing.test.rsrx} +0 -0
- /package/tests/client/url/{url.partial-removal.test.ripple → url.partial-removal.test.rsrx} +0 -0
- /package/tests/client/url/{url.reactivity.test.ripple → url.reactivity.test.rsrx} +0 -0
- /package/tests/client/url/{url.serialization.test.ripple → url.serialization.test.rsrx} +0 -0
- /package/tests/client/url-search-params/{url-search-params.derived.test.ripple → url-search-params.derived.test.rsrx} +0 -0
- /package/tests/client/url-search-params/{url-search-params.initialization.test.ripple → url-search-params.initialization.test.rsrx} +0 -0
- /package/tests/client/url-search-params/{url-search-params.iteration.test.ripple → url-search-params.iteration.test.rsrx} +0 -0
- /package/tests/client/url-search-params/{url-search-params.mutation.test.ripple → url-search-params.mutation.test.rsrx} +0 -0
- /package/tests/client/url-search-params/{url-search-params.retrieval.test.ripple → url-search-params.retrieval.test.rsrx} +0 -0
- /package/tests/client/url-search-params/{url-search-params.serialization.test.ripple → url-search-params.serialization.test.rsrx} +0 -0
- /package/tests/client/url-search-params/{url-search-params.tracked-url.test.ripple → url-search-params.tracked-url.test.rsrx} +0 -0
- /package/tests/hydration/components/{basic.ripple → basic.rsrx} +0 -0
- /package/tests/hydration/components/{composite.ripple → composite.rsrx} +0 -0
- /package/tests/hydration/components/{events.ripple → events.rsrx} +0 -0
- /package/tests/hydration/components/{for.ripple → for.rsrx} +0 -0
- /package/tests/hydration/components/{head.ripple → head.rsrx} +0 -0
- /package/tests/hydration/components/{hmr.ripple → hmr.rsrx} +0 -0
- /package/tests/hydration/components/{html-in-template.ripple → html-in-template.rsrx} +0 -0
- /package/tests/hydration/components/{html.ripple → html.rsrx} +0 -0
- /package/tests/hydration/components/{if-children.ripple → if-children.rsrx} +0 -0
- /package/tests/hydration/components/{if.ripple → if.rsrx} +0 -0
- /package/tests/hydration/components/{nested-control-flow.ripple → nested-control-flow.rsrx} +0 -0
- /package/tests/hydration/components/{portal.ripple → portal.rsrx} +0 -0
- /package/tests/hydration/components/{reactivity.ripple → reactivity.rsrx} +0 -0
- /package/tests/hydration/components/{return.ripple → return.rsrx} +0 -0
- /package/tests/hydration/components/{switch.ripple → switch.rsrx} +0 -0
- /package/tests/server/{await.test.ripple → await.test.rsrx} +0 -0
- /package/tests/server/{basic.attributes.test.ripple → basic.attributes.test.rsrx} +0 -0
- /package/tests/server/{basic.components.test.ripple → basic.components.test.rsrx} +0 -0
- /package/tests/server/{basic.test.ripple → basic.test.rsrx} +0 -0
- /package/tests/server/{composite.props.test.ripple → composite.props.test.rsrx} +0 -0
- /package/tests/server/{composite.test.ripple → composite.test.rsrx} +0 -0
- /package/tests/server/{context.test.ripple → context.test.rsrx} +0 -0
- /package/tests/server/{dynamic-elements.test.ripple → dynamic-elements.test.rsrx} +0 -0
- /package/tests/server/{for.test.ripple → for.test.rsrx} +0 -0
- /package/tests/server/{head.test.ripple → head.test.rsrx} +0 -0
- /package/tests/server/{if.test.ripple → if.test.rsrx} +0 -0
- /package/tests/server/{lazy-destructuring.test.ripple → lazy-destructuring.test.rsrx} +0 -0
- /package/tests/server/{return.test.ripple → return.test.rsrx} +0 -0
- /package/tests/server/{style-identifier.test.ripple → style-identifier.test.rsrx} +0 -0
- /package/tests/server/{switch.test.ripple → switch.test.rsrx} +0 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
import type { Tracked } from 'ripple';
|
|
2
|
+
import { DERIVED_UPDATED, effect, flushSync, track, trackAsync } from 'ripple';
|
|
3
|
+
|
|
4
|
+
describe('async suspense', () => {
|
|
5
|
+
it('hides child content during re-suspension when tracked dependency changes', async () => {
|
|
6
|
+
let resolve_fn: (() => void) | null = null;
|
|
7
|
+
|
|
8
|
+
component Child({ countTracked }: { countTracked: Tracked<number> }) {
|
|
9
|
+
trackAsync(() => {
|
|
10
|
+
countTracked.value;
|
|
11
|
+
return new Promise<void>((resolve) => {
|
|
12
|
+
resolve_fn = resolve;
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
<div class="child-content">{'child content'}</div>
|
|
17
|
+
<div class="count">
|
|
18
|
+
{'count is: '}
|
|
19
|
+
{countTracked.value}
|
|
20
|
+
</div>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
component App() {
|
|
24
|
+
let &[count, countTracked] = track(0);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
<Child {countTracked} />
|
|
28
|
+
} pending {
|
|
29
|
+
<div class="pending">{'pending...'}</div>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
<button onClick={() => count++}>{'Increment'}</button>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
render(App);
|
|
36
|
+
|
|
37
|
+
// Initial state: should show pending
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
39
|
+
flushSync();
|
|
40
|
+
expect(container.innerHTML).toContain('pending...');
|
|
41
|
+
expect(container.innerHTML).not.toContain('child content');
|
|
42
|
+
|
|
43
|
+
// Resolve the first promise
|
|
44
|
+
(resolve_fn as () => void)?.();
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
46
|
+
flushSync();
|
|
47
|
+
|
|
48
|
+
// After resolution: should show child content, not pending
|
|
49
|
+
expect(container.innerHTML).toContain('child content');
|
|
50
|
+
expect(container.innerHTML).not.toContain('pending...');
|
|
51
|
+
|
|
52
|
+
// Changing count should keep the ui in the same state
|
|
53
|
+
const button = container.querySelector('button');
|
|
54
|
+
button?.click();
|
|
55
|
+
flushSync();
|
|
56
|
+
expect(container.innerHTML).toContain('count is: 1');
|
|
57
|
+
|
|
58
|
+
// Wait for microtask to process
|
|
59
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
60
|
+
flushSync();
|
|
61
|
+
|
|
62
|
+
// After count change: should still show child content, not pending
|
|
63
|
+
expect(container.innerHTML).not.toContain('pending...');
|
|
64
|
+
expect(container.innerHTML).toContain('child content');
|
|
65
|
+
expect(container.innerHTML).toContain('count is: 1');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('ignores settled promises after the surrounding boundary is destroyed', async () => {
|
|
69
|
+
let resolve_value: ((value: string) => void) | null = null;
|
|
70
|
+
|
|
71
|
+
component Child() {
|
|
72
|
+
let &[value] = trackAsync(
|
|
73
|
+
() => new Promise<string>((resolve) => {
|
|
74
|
+
resolve_value = resolve;
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
<div class="value">{value}</div>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
component App() {
|
|
82
|
+
let &[show] = track(true);
|
|
83
|
+
|
|
84
|
+
<button onClick={() => (show = false)}>{'Hide'}</button>
|
|
85
|
+
|
|
86
|
+
if (show) {
|
|
87
|
+
try {
|
|
88
|
+
<Child />
|
|
89
|
+
} pending {
|
|
90
|
+
<div class="pending">{'loading...'}</div>
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
<div class="hidden">{'hidden'}</div>
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
render(App);
|
|
98
|
+
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
100
|
+
flushSync();
|
|
101
|
+
expect(container.innerHTML).toContain('loading...');
|
|
102
|
+
|
|
103
|
+
(container.querySelector('button') as HTMLButtonElement).click();
|
|
104
|
+
flushSync();
|
|
105
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
106
|
+
flushSync();
|
|
107
|
+
|
|
108
|
+
expect(container.innerHTML).toContain('hidden');
|
|
109
|
+
|
|
110
|
+
(resolve_value as (value: string) => void)('late value');
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
112
|
+
flushSync();
|
|
113
|
+
|
|
114
|
+
expect(container.innerHTML).toContain('hidden');
|
|
115
|
+
expect(container.innerHTML).not.toContain('late value');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('aborts superseded requests with DERIVED_UPDATED without rendering catch', async () => {
|
|
119
|
+
const requests = new Map<
|
|
120
|
+
string,
|
|
121
|
+
{ resolve: (value: string) => void; abortController: AbortController }
|
|
122
|
+
>();
|
|
123
|
+
|
|
124
|
+
function createRequest(label: string) {
|
|
125
|
+
const abortController = new AbortController();
|
|
126
|
+
let resolve_value: (value: string) => void = () => {};
|
|
127
|
+
|
|
128
|
+
const promise = new Promise<string>((resolve, reject) => {
|
|
129
|
+
resolve_value = resolve;
|
|
130
|
+
abortController.signal.addEventListener('abort', () => {
|
|
131
|
+
reject(abortController.signal.reason);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
requests.set(label, { resolve: resolve_value, abortController });
|
|
136
|
+
|
|
137
|
+
return { promise, abortController };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
component App() {
|
|
141
|
+
let &[query] = track('a');
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
let &[value] = trackAsync(() => createRequest(query));
|
|
145
|
+
<div class="value">{value}</div>
|
|
146
|
+
} pending {
|
|
147
|
+
<div class="pending">{'loading...'}</div>
|
|
148
|
+
} catch (error) {
|
|
149
|
+
<div class="error">{String(error)}</div>
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
<button
|
|
153
|
+
onClick={() => {
|
|
154
|
+
query = query === 'a' ? 'b' : 'c';
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
{'Next'}
|
|
158
|
+
</button>
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
render(App);
|
|
162
|
+
|
|
163
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
164
|
+
flushSync();
|
|
165
|
+
expect(container.innerHTML).toContain('loading...');
|
|
166
|
+
|
|
167
|
+
(requests.get('a') as { resolve: (value: string) => void }).resolve('value-a');
|
|
168
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
169
|
+
flushSync();
|
|
170
|
+
|
|
171
|
+
expect(container.innerHTML).toContain('value-a');
|
|
172
|
+
|
|
173
|
+
const button = container.querySelector('button') as HTMLButtonElement;
|
|
174
|
+
button.click();
|
|
175
|
+
flushSync();
|
|
176
|
+
button.click();
|
|
177
|
+
flushSync();
|
|
178
|
+
|
|
179
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
180
|
+
flushSync();
|
|
181
|
+
|
|
182
|
+
expect(
|
|
183
|
+
(requests.get('b') as { abortController: AbortController }).abortController.signal.reason,
|
|
184
|
+
).toBe(DERIVED_UPDATED);
|
|
185
|
+
expect(container.innerHTML).not.toContain('class="error"');
|
|
186
|
+
expect(container.innerHTML).toContain('value-a');
|
|
187
|
+
|
|
188
|
+
(requests.get('c') as { resolve: (value: string) => void }).resolve('value-c');
|
|
189
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
190
|
+
flushSync();
|
|
191
|
+
|
|
192
|
+
expect(container.innerHTML).toContain('value-c');
|
|
193
|
+
expect(container.innerHTML).not.toContain('class="error"');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it(
|
|
197
|
+
'updates the existing reactive graph when tracked dependency triggers re-fetch in child component',
|
|
198
|
+
async () => {
|
|
199
|
+
let pending_render_count = 0;
|
|
200
|
+
|
|
201
|
+
component FilteredList({ queryTracked }: { queryTracked: Tracked<string> }) {
|
|
202
|
+
let &[query] = queryTracked;
|
|
203
|
+
let &[items] = trackAsync(
|
|
204
|
+
() => Promise.resolve(
|
|
205
|
+
!query ? ['apple', 'banana', 'cherry'] : ['avocado', 'blueberry', 'citrus'],
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
let &[filtered] = track(() => {
|
|
210
|
+
return items.filter((item: string) => item.includes(query));
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
<ul>
|
|
214
|
+
for (let item of filtered) {
|
|
215
|
+
<li>{item}</li>
|
|
216
|
+
}
|
|
217
|
+
</ul>
|
|
218
|
+
|
|
219
|
+
<pre>{JSON.stringify(items)}</pre>
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
component App() {
|
|
223
|
+
let &[query, queryTracked] = track('');
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
<FilteredList {queryTracked} />
|
|
227
|
+
} pending {
|
|
228
|
+
pending_render_count += 1;
|
|
229
|
+
<p class="pending">{'loading...'}</p>
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
<button onClick={() => (query = 'a')}>{'Search'}</button>
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
render(App);
|
|
236
|
+
|
|
237
|
+
// Promise.resolve settles in microtask — wait for it
|
|
238
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
239
|
+
flushSync();
|
|
240
|
+
expect(pending_render_count).toBe(1);
|
|
241
|
+
|
|
242
|
+
// Initial: filtered by '' matches apple, banana (both contain '')
|
|
243
|
+
expect(container.querySelectorAll('li').length).toBe(3);
|
|
244
|
+
expect(container.innerHTML).toContain('apple');
|
|
245
|
+
expect(container.innerHTML).toContain('banana');
|
|
246
|
+
expect(container.innerHTML).toContain('cherry');
|
|
247
|
+
expect(container.querySelectorAll('ul').length).toBe(1);
|
|
248
|
+
expect(container.querySelectorAll('pre').length).toBe(1);
|
|
249
|
+
expect(container.querySelector('pre')!.textContent).toBe('["apple","banana","cherry"]');
|
|
250
|
+
|
|
251
|
+
// Change query to 'a' — triggers re-fetch with new items
|
|
252
|
+
(container.querySelector('button') as HTMLButtonElement).click();
|
|
253
|
+
flushSync();
|
|
254
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
255
|
+
flushSync();
|
|
256
|
+
|
|
257
|
+
// The resolved branch should update in place without appending duplicates.
|
|
258
|
+
expect(pending_render_count).toBe(1);
|
|
259
|
+
expect(container.innerHTML).not.toContain('loading...');
|
|
260
|
+
expect(container.querySelectorAll('ul').length).toBe(1);
|
|
261
|
+
expect(container.querySelectorAll('pre').length).toBe(1);
|
|
262
|
+
// After re-fetch, the branch re-runs with the new data.
|
|
263
|
+
expect(container.innerHTML).toContain('avocado');
|
|
264
|
+
expect(container.innerHTML).toContain('blueberry');
|
|
265
|
+
expect(container.innerHTML).toContain('citrus');
|
|
266
|
+
expect(container.querySelector('pre')!.textContent).toBe('["avocado","blueberry","citrus"]');
|
|
267
|
+
expect(container.innerHTML).not.toContain('apple');
|
|
268
|
+
expect(container.innerHTML).not.toContain('banana');
|
|
269
|
+
expect(container.innerHTML).not.toContain('cherry');
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
it('updates direct try-block trackAsync consumers and renders pending only once', async () => {
|
|
274
|
+
let pending_render_count = 0;
|
|
275
|
+
|
|
276
|
+
component App() {
|
|
277
|
+
let &[query] = track('');
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
let &[items] = trackAsync(
|
|
281
|
+
() => Promise.resolve(
|
|
282
|
+
!query ? ['apple', 'banana', 'cherry'] : ['avocado', 'blueberry', 'citrus'],
|
|
283
|
+
),
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
let &[filtered] = track(() => {
|
|
287
|
+
return items.filter((item: string) => item.includes(query));
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
<ul>
|
|
291
|
+
for (let item of filtered) {
|
|
292
|
+
<li>{item}</li>
|
|
293
|
+
}
|
|
294
|
+
</ul>
|
|
295
|
+
|
|
296
|
+
<pre>{JSON.stringify(items)}</pre>
|
|
297
|
+
} pending {
|
|
298
|
+
pending_render_count += 1;
|
|
299
|
+
<p class="pending">{'loading...'}</p>
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
<button onClick={() => (query = 'a')}>{'Search'}</button>
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
render(App);
|
|
306
|
+
|
|
307
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
308
|
+
flushSync();
|
|
309
|
+
|
|
310
|
+
expect(pending_render_count).toBe(1);
|
|
311
|
+
expect(container.querySelectorAll('li').length).toBe(3);
|
|
312
|
+
expect(container.querySelector('pre')!.textContent).toBe('["apple","banana","cherry"]');
|
|
313
|
+
|
|
314
|
+
(container.querySelector('button') as HTMLButtonElement).click();
|
|
315
|
+
flushSync();
|
|
316
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
317
|
+
flushSync();
|
|
318
|
+
|
|
319
|
+
expect(pending_render_count).toBe(1);
|
|
320
|
+
expect(container.innerHTML).not.toContain('loading...');
|
|
321
|
+
expect(container.querySelectorAll('ul').length).toBe(1);
|
|
322
|
+
expect(container.querySelectorAll('pre').length).toBe(1);
|
|
323
|
+
// After re-fetch, the branch re-runs with the new data.
|
|
324
|
+
expect(container.innerHTML).toContain('avocado');
|
|
325
|
+
expect(container.innerHTML).toContain('blueberry');
|
|
326
|
+
expect(container.innerHTML).toContain('citrus');
|
|
327
|
+
expect(container.innerHTML).not.toContain('apple');
|
|
328
|
+
expect(container.innerHTML).not.toContain('banana');
|
|
329
|
+
expect(container.innerHTML).not.toContain('cherry');
|
|
330
|
+
expect(container.querySelector('pre')!.textContent).toBe('["avocado","blueberry","citrus"]');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it(
|
|
334
|
+
'defers paused async consumer blocks until chained async requests settle with render only running once',
|
|
335
|
+
async () => {
|
|
336
|
+
let resolve_double: ((value: number) => void) | null = null;
|
|
337
|
+
let resolve_quadruple: ((value: number) => void) | null = null;
|
|
338
|
+
let double_effect_runs = 0;
|
|
339
|
+
let quadruple_effect_runs = 0;
|
|
340
|
+
let times_rendered = 0;
|
|
341
|
+
component Child(&{ count }: { count: number }) {
|
|
342
|
+
const &[double]: [number] = trackAsync(() => {
|
|
343
|
+
const result = count * 2;
|
|
344
|
+
|
|
345
|
+
return new Promise<number>((resolve) => {
|
|
346
|
+
resolve_double = () => resolve(result);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const &[quadruple]: [number] = trackAsync(() => {
|
|
351
|
+
const result = double * 2;
|
|
352
|
+
|
|
353
|
+
return new Promise<number>((resolve) => {
|
|
354
|
+
resolve_quadruple = () => resolve(result);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
effect(() => {
|
|
359
|
+
double;
|
|
360
|
+
double_effect_runs += 1;
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
effect(() => {
|
|
364
|
+
quadruple;
|
|
365
|
+
quadruple_effect_runs += 1;
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// this is to make the times_rendered render together with double
|
|
369
|
+
<div class="double">{double + (++times_rendered - times_rendered)}</div>
|
|
370
|
+
<div class="quadruple">{quadruple}</div>
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
component App() {
|
|
374
|
+
let &[count] = track(2);
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
<Child {count} />
|
|
378
|
+
} pending {
|
|
379
|
+
<div class="pending">{'Loading...'}</div>
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
<button onClick={() => count++}>{'Increment'}</button>
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
render(App);
|
|
386
|
+
|
|
387
|
+
const button = container.querySelector('button') as HTMLButtonElement;
|
|
388
|
+
|
|
389
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
390
|
+
flushSync();
|
|
391
|
+
|
|
392
|
+
expect(container.innerHTML).toContain('Loading...');
|
|
393
|
+
expect(double_effect_runs).toBe(0);
|
|
394
|
+
expect(quadruple_effect_runs).toBe(0);
|
|
395
|
+
|
|
396
|
+
(resolve_double as (value: number) => void)(4);
|
|
397
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
398
|
+
flushSync();
|
|
399
|
+
|
|
400
|
+
expect(container.innerHTML).toContain('Loading...');
|
|
401
|
+
expect(double_effect_runs).toBe(0);
|
|
402
|
+
expect(quadruple_effect_runs).toBe(0);
|
|
403
|
+
|
|
404
|
+
(resolve_quadruple as (value: number) => void)(8);
|
|
405
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
406
|
+
flushSync();
|
|
407
|
+
|
|
408
|
+
expect(container.innerHTML).not.toContain('Loading...');
|
|
409
|
+
expect(container.querySelector('.double')!.textContent).toBe('4');
|
|
410
|
+
expect(container.querySelector('.quadruple')!.textContent).toBe('8');
|
|
411
|
+
expect(double_effect_runs).toBe(1);
|
|
412
|
+
expect(quadruple_effect_runs).toBe(1);
|
|
413
|
+
expect(times_rendered).toBe(1);
|
|
414
|
+
|
|
415
|
+
button.click();
|
|
416
|
+
flushSync();
|
|
417
|
+
|
|
418
|
+
(resolve_double as (value: number) => void)(6);
|
|
419
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
420
|
+
(resolve_quadruple as (value: number) => void)(12);
|
|
421
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
422
|
+
expect(container.querySelector('.double')!.textContent).toBe('6');
|
|
423
|
+
expect(container.querySelector('.quadruple')!.textContent).toBe('12');
|
|
424
|
+
expect(times_rendered).toBe(2);
|
|
425
|
+
},
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
it(
|
|
429
|
+
'chained async requests settle with render only running once when multiple consecutive requests are fired off',
|
|
430
|
+
async () => {
|
|
431
|
+
let resolve_double: ((value: number) => void) | null = null;
|
|
432
|
+
let resolve_quadruple: ((value: number) => void) | null = null;
|
|
433
|
+
let double_effect_runs = 0;
|
|
434
|
+
let quadruple_effect_runs = 0;
|
|
435
|
+
let times_rendered = 0;
|
|
436
|
+
component Child(&{ count }: { count: number }) {
|
|
437
|
+
const &[double]: [number] = trackAsync(() => {
|
|
438
|
+
const result = count * 2;
|
|
439
|
+
|
|
440
|
+
return new Promise<number>((resolve) => {
|
|
441
|
+
resolve_double = () => resolve(result);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const &[quadruple]: [number] = trackAsync(() => {
|
|
446
|
+
const result = double * 2;
|
|
447
|
+
|
|
448
|
+
return new Promise<number>((resolve) => {
|
|
449
|
+
resolve_quadruple = () => resolve(result);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
effect(() => {
|
|
454
|
+
double;
|
|
455
|
+
double_effect_runs += 1;
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
effect(() => {
|
|
459
|
+
quadruple;
|
|
460
|
+
quadruple_effect_runs += 1;
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// this is to make the times_rendered render together with double
|
|
464
|
+
<div class="double">{double + (++times_rendered - times_rendered)}</div>
|
|
465
|
+
<div class="quadruple">{quadruple}</div>
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
component App() {
|
|
469
|
+
let &[count] = track(2);
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
<Child {count} />
|
|
473
|
+
} pending {
|
|
474
|
+
<div class="pending">{'Loading...'}</div>
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
<button onClick={() => count++}>{'Increment'}</button>
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
render(App);
|
|
481
|
+
|
|
482
|
+
const button = container.querySelector('button') as HTMLButtonElement;
|
|
483
|
+
|
|
484
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
485
|
+
flushSync();
|
|
486
|
+
|
|
487
|
+
expect(container.innerHTML).toContain('Loading...');
|
|
488
|
+
expect(double_effect_runs).toBe(0);
|
|
489
|
+
expect(quadruple_effect_runs).toBe(0);
|
|
490
|
+
|
|
491
|
+
(resolve_double as (value: number) => void)(4);
|
|
492
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
493
|
+
flushSync();
|
|
494
|
+
|
|
495
|
+
expect(container.innerHTML).toContain('Loading...');
|
|
496
|
+
expect(double_effect_runs).toBe(0);
|
|
497
|
+
expect(quadruple_effect_runs).toBe(0);
|
|
498
|
+
|
|
499
|
+
(resolve_quadruple as (value: number) => void)(8);
|
|
500
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
501
|
+
flushSync();
|
|
502
|
+
|
|
503
|
+
expect(container.innerHTML).not.toContain('Loading...');
|
|
504
|
+
expect(container.querySelector('.double')!.textContent).toBe('4');
|
|
505
|
+
expect(container.querySelector('.quadruple')!.textContent).toBe('8');
|
|
506
|
+
expect(double_effect_runs).toBe(1);
|
|
507
|
+
expect(quadruple_effect_runs).toBe(1);
|
|
508
|
+
expect(times_rendered).toBe(1);
|
|
509
|
+
|
|
510
|
+
button.click();
|
|
511
|
+
flushSync();
|
|
512
|
+
button.click();
|
|
513
|
+
flushSync();
|
|
514
|
+
button.click();
|
|
515
|
+
flushSync();
|
|
516
|
+
|
|
517
|
+
(resolve_double as (value: number) => void)(10);
|
|
518
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
519
|
+
(resolve_quadruple as (value: number) => void)(12);
|
|
520
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
521
|
+
expect(container.querySelector('.double')!.textContent).toBe('10');
|
|
522
|
+
expect(container.querySelector('.quadruple')!.textContent).toBe('20');
|
|
523
|
+
expect(times_rendered).toBe(2);
|
|
524
|
+
},
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
it('resolves chained async values without explicit flushSync', async () => {
|
|
528
|
+
let times_rendered = 0;
|
|
529
|
+
component Child(&{ count }: { count: number }) {
|
|
530
|
+
const &[double]: [number] = trackAsync(() => {
|
|
531
|
+
const result = count * 2;
|
|
532
|
+
|
|
533
|
+
return new Promise<number>((resolve) => {
|
|
534
|
+
setTimeout(() => resolve(result), 0);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
const &[quadruple]: [number] = trackAsync(() => {
|
|
539
|
+
const result = double * 2;
|
|
540
|
+
|
|
541
|
+
return new Promise<number>((resolve) => {
|
|
542
|
+
setTimeout(() => resolve(result), 0);
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// this is to make the times_rendered render together with double
|
|
547
|
+
<div class="double">{double + (++times_rendered - times_rendered)}</div>
|
|
548
|
+
<div class="quadruple">{quadruple}</div>
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
component App() {
|
|
552
|
+
let &[count] = track(0);
|
|
553
|
+
|
|
554
|
+
<button onClick={() => count++}>{count}</button>
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
<Child {count} />
|
|
558
|
+
} pending {
|
|
559
|
+
<div class="pending">{'Loading...'}</div>
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
render(App);
|
|
564
|
+
|
|
565
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
566
|
+
|
|
567
|
+
expect(container.innerHTML).not.toContain('Loading...');
|
|
568
|
+
expect(container.querySelector('.double')!.textContent).toBe('0');
|
|
569
|
+
expect(container.querySelector('.quadruple')!.textContent).toBe('0');
|
|
570
|
+
expect(times_rendered).toBe(1);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it(
|
|
574
|
+
'registers async derived under multiple try/pending boundaries across components',
|
|
575
|
+
async () => {
|
|
576
|
+
let resolve_fn: ((value: string[]) => void) | null = null;
|
|
577
|
+
|
|
578
|
+
component Child({ itemsTracked }: { itemsTracked: Tracked<string[]> }) {
|
|
579
|
+
try {
|
|
580
|
+
<div class="child-content">
|
|
581
|
+
{'child: '}
|
|
582
|
+
{itemsTracked.value.join(', ')}
|
|
583
|
+
</div>
|
|
584
|
+
} pending {
|
|
585
|
+
<div class="child-pending">{'child loading...'}</div>
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
component App() {
|
|
590
|
+
let &[query] = track('initial');
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
let &[items, itemsTracked] = trackAsync(() => {
|
|
594
|
+
const q = query;
|
|
595
|
+
return new Promise<string[]>((resolve) => {
|
|
596
|
+
resolve_fn = resolve;
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
<div class="parent-content">
|
|
601
|
+
{'parent: '}
|
|
602
|
+
{items.join(', ')}
|
|
603
|
+
</div>
|
|
604
|
+
<Child {itemsTracked} />
|
|
605
|
+
} pending {
|
|
606
|
+
<div class="parent-pending">{'parent loading...'}</div>
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
<button onClick={() => (query = 'next')}>{'Change'}</button>
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
render(App);
|
|
613
|
+
|
|
614
|
+
// Initial: parent pending, child not yet rendered
|
|
615
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
616
|
+
expect(container.innerHTML).toContain('parent loading...');
|
|
617
|
+
expect(container.innerHTML).not.toContain('parent-content');
|
|
618
|
+
expect(container.innerHTML).not.toContain('child-content');
|
|
619
|
+
|
|
620
|
+
// Resolve first request — both parent and child should show content,
|
|
621
|
+
// child reading the derived registers its own try boundary on the same derived
|
|
622
|
+
(resolve_fn as (value: string[]) => void)(['apple', 'banana']);
|
|
623
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
624
|
+
|
|
625
|
+
expect(container.innerHTML).toContain('parent: apple, banana');
|
|
626
|
+
expect(container.innerHTML).toContain('child: apple, banana');
|
|
627
|
+
expect(container.innerHTML).not.toContain('parent loading...');
|
|
628
|
+
expect(container.innerHTML).not.toContain('child loading...');
|
|
629
|
+
|
|
630
|
+
// Trigger re-fetch — derived now has entries for both parent and child boundaries
|
|
631
|
+
const button = container.querySelector('button') as HTMLButtonElement;
|
|
632
|
+
button.click();
|
|
633
|
+
flushSync();
|
|
634
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
635
|
+
|
|
636
|
+
// After has_resolved, re-fetch keeps resolved branch visible (no re-suspension)
|
|
637
|
+
expect(container.innerHTML).not.toContain('parent loading...');
|
|
638
|
+
expect(container.innerHTML).not.toContain('child loading...');
|
|
639
|
+
|
|
640
|
+
// Resolve second request — both should update in place
|
|
641
|
+
(resolve_fn as (value: string[]) => void)(['cherry', 'date']);
|
|
642
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
643
|
+
flushSync();
|
|
644
|
+
|
|
645
|
+
expect(container.innerHTML).toContain('parent: cherry, date');
|
|
646
|
+
expect(container.innerHTML).toContain('child: cherry, date');
|
|
647
|
+
expect(container.innerHTML).not.toContain('parent loading...');
|
|
648
|
+
expect(container.innerHTML).not.toContain('child loading...');
|
|
649
|
+
},
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
it('throws when trackAsync is used without a try/pending boundary', () => {
|
|
653
|
+
component App() {
|
|
654
|
+
let &[value] = trackAsync(() => Promise.resolve('test'));
|
|
655
|
+
<div>{value}</div>
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
expect(() => {
|
|
659
|
+
render(App);
|
|
660
|
+
}).toThrow('Missing parent `try { ... } pending { ... }` statement');
|
|
661
|
+
});
|
|
662
|
+
});
|