Skip to content

Commit 835101b

Browse files
whoop, tests
1 parent 259d286 commit 835101b

File tree

2 files changed

+237
-5
lines changed

2 files changed

+237
-5
lines changed

packages/svelte/src/internal/server/payload.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ export class Payload {
3535
* Asynchronous work associated with this payload. `initial` is the promise from the function
3636
* this payload was passed to (if that function was async), and `followup` is any any additional
3737
* work from `compact` calls that needs to complete prior to collecting this payload's content.
38-
* @type {{ initial: Promise<void> | undefined, followup: Promise<void>[] | undefined }}
38+
* @type {{ initial: Promise<void> | undefined, followup: Promise<void>[] }}
3939
*/
40-
promises = { initial: undefined, followup: undefined };
40+
promises = { initial: undefined, followup: [] };
4141

4242
/**
4343
* State which is associated with the content tree as a whole.
@@ -114,7 +114,7 @@ export class Payload {
114114
.then((transformed_content) =>
115115
Payload.#push_accumulated_content(child, transformed_content)
116116
);
117-
(this.promises.followup ??= []).push(followup);
117+
this.promises.followup.push(followup);
118118
} else {
119119
Payload.#push_accumulated_content(child, fn(content));
120120
}
@@ -194,7 +194,7 @@ export class Payload {
194194
} else {
195195
flush();
196196

197-
if (item.promises.initial) {
197+
if (item.promises.initial || item.promises.followup.length) {
198198
has_async = true;
199199
segments.push(
200200
Payload.#collect_content_async([item], current_type, { head: '', body: '' })
@@ -238,7 +238,7 @@ export class Payload {
238238
// we can't do anything until it's done and we know our `out` array is complete.
239239
await item.promises.initial;
240240
}
241-
for (const followup of item.promises.followup ?? []) {
241+
for (const followup of item.promises.followup) {
242242
// this is sequential because `compact` could synchronously queue up additional followup work
243243
await followup;
244244
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { assert, expect, test } from 'vitest';
2+
import { Payload, TreeState, TreeHeadState } from './payload.js';
3+
4+
test('collects synchronous body content by default', () => {
5+
const payload = new Payload();
6+
payload.push('a');
7+
payload.child(($$payload) => {
8+
$$payload.push('b');
9+
});
10+
payload.push('c');
11+
12+
const { head, body } = payload.collect();
13+
assert.equal(head, '');
14+
assert.equal(body, 'abc');
15+
});
16+
17+
test('child type switches content area (head vs body)', () => {
18+
const payload = new Payload();
19+
payload.push('a');
20+
payload.child(($$payload) => {
21+
$$payload.push('<title>T</title>');
22+
}, 'head');
23+
payload.push('b');
24+
25+
const { head, body } = payload.collect();
26+
assert.equal(head, '<title>T</title>');
27+
assert.equal(body, 'ab');
28+
});
29+
30+
test('child inherits parent type when not specified', () => {
31+
const parent = new Payload(undefined, undefined, undefined, 'head');
32+
parent.push('<meta name="x"/>');
33+
parent.child(($$payload) => {
34+
$$payload.push('<style>/* css */</style>');
35+
});
36+
const { head, body } = parent.collect();
37+
assert.equal(body, '');
38+
assert.equal(head, '<meta name="x"/><style>/* css */</style>');
39+
});
40+
41+
test('get_path returns the path indexes to a payload', () => {
42+
const root = new Payload();
43+
let child_a: InstanceType<typeof Payload> | undefined;
44+
let child_b: InstanceType<typeof Payload> | undefined;
45+
let child_b_0: InstanceType<typeof Payload> | undefined;
46+
47+
root.child(($$payload) => {
48+
child_a = $$payload;
49+
$$payload.push('A');
50+
});
51+
root.child(($$payload) => {
52+
child_b = $$payload;
53+
$$payload.child(($$inner) => {
54+
child_b_0 = $$inner;
55+
$$inner.push('B0');
56+
});
57+
$$payload.push('B1');
58+
});
59+
60+
assert.deepEqual(child_a!.get_path(), [0]);
61+
assert.deepEqual(child_b!.get_path(), [1]);
62+
assert.deepEqual(child_b_0!.get_path(), [1, 0]);
63+
});
64+
65+
test('awaiting payload resolves async children; collect throws on async', async () => {
66+
const payload = new Payload();
67+
payload.push('a');
68+
payload.child(async ($$payload) => {
69+
await Promise.resolve();
70+
$$payload.push('x');
71+
});
72+
payload.push('y');
73+
74+
expect(() => payload.collect()).toThrow(
75+
'Encountered an asynchronous component while rendering synchronously'
76+
);
77+
78+
const { body, head } = await payload;
79+
assert.equal(head, '');
80+
assert.equal(body, 'axy');
81+
});
82+
83+
test('then() allows awaiting payload to get aggregated content', async () => {
84+
const payload = new Payload();
85+
payload.push('1');
86+
payload.child(async ($$payload) => {
87+
await Promise.resolve();
88+
$$payload.push('2');
89+
});
90+
payload.push('3');
91+
92+
const result = await payload;
93+
assert.deepEqual(result, { head: '', body: '123' });
94+
});
95+
96+
test('compact synchronously aggregates a range and can transform into head/body', () => {
97+
const payload = new Payload();
98+
payload.push('a');
99+
payload.push('b');
100+
payload.push('c');
101+
102+
payload.compact({
103+
start: 0,
104+
end: 2,
105+
fn: (content) => ({ head: '<h>H</h>', body: content.body + 'd' })
106+
});
107+
108+
assert.equal(payload.length, 2);
109+
const { head, body } = payload.collect();
110+
assert.equal(head, '<h>H</h>');
111+
assert.equal(body, 'abdc');
112+
});
113+
114+
test('compact schedules followup when compaction input is async', async () => {
115+
const payload = new Payload();
116+
payload.push('a');
117+
payload.child(async ($$payload) => {
118+
await Promise.resolve();
119+
$$payload.push('X');
120+
});
121+
payload.push('b');
122+
123+
payload.compact({
124+
start: 0,
125+
fn: (content) => ({ body: content.body.toLowerCase(), head: '' })
126+
});
127+
128+
const { body, head } = await payload;
129+
assert.equal(head, '');
130+
assert.equal(body, 'axb');
131+
});
132+
133+
test('copy creates a deep copy of the tree and shares promises reference', () => {
134+
const payload = new Payload();
135+
let child_ref: InstanceType<typeof Payload> | undefined;
136+
payload.child(($$payload) => {
137+
child_ref = $$payload;
138+
$$payload.push('x');
139+
});
140+
payload.push('y');
141+
142+
const copy = payload.copy();
143+
assert.strictEqual(copy.promises, payload.promises);
144+
145+
// mutate original
146+
child_ref!.push('!');
147+
payload.push('?');
148+
149+
const original = payload.collect();
150+
const cloned = copy.collect();
151+
152+
assert.deepEqual(original, { head: '', body: 'x!y?' });
153+
assert.deepEqual(cloned, { head: '', body: 'xy' });
154+
});
155+
156+
test('local state is shallow-copied to children', () => {
157+
const root = new Payload();
158+
root.local.select_value = 'A';
159+
let child: InstanceType<typeof Payload> | undefined;
160+
root.child(($$payload) => {
161+
child = $$payload;
162+
});
163+
164+
assert.equal(child!.local.select_value, 'A');
165+
child!.local.select_value = 'B';
166+
assert.equal(root.local.select_value, 'A');
167+
});
168+
169+
test('subsume replaces tree content and state from other', () => {
170+
const a = new Payload(undefined, undefined, undefined, 'head');
171+
a.push('<meta />');
172+
a.local.select_value = 'A';
173+
174+
const b = new Payload();
175+
b.child(async ($$payload) => {
176+
await Promise.resolve();
177+
$$payload.push('body');
178+
});
179+
b.global.css.add({ hash: 'h', code: 'c' });
180+
b.global.head.title = { path: [1], value: 'Title' };
181+
b.local.select_value = 'B';
182+
b.promises.initial = Promise.resolve();
183+
184+
a.subsume(b);
185+
186+
// content now matches b and is async
187+
expect(() => a.collect()).toThrow(
188+
'Encountered an asynchronous component while rendering synchronously'
189+
);
190+
assert.equal(a.type, 'body');
191+
assert.equal(a.local.select_value, 'B');
192+
assert.strictEqual(a.promises, b.promises);
193+
194+
// global state transferred
195+
assert.ok(a.global.css.has({ hash: 'h', code: 'c' }) || [...a.global.css][0]?.hash === 'h');
196+
assert.equal(a.global.head.title.value, 'Title');
197+
});
198+
199+
test('TreeState uid generator uses prefix and is shared by copy()', () => {
200+
const state = new TreeState('id-');
201+
assert.equal(state.uid(), 'id-s1');
202+
const state_copy = state.copy();
203+
assert.equal(state_copy.uid(), 'id-s2');
204+
assert.equal(state.uid(), 'id-s3');
205+
});
206+
207+
test('TreeHeadState title ordering favors later lexicographic paths', () => {
208+
const head = new TreeHeadState(() => '');
209+
210+
head.title = { path: [1], value: 'A' };
211+
assert.equal(head.title.value, 'A');
212+
213+
// equal path -> unchanged
214+
head.title = { path: [1], value: 'B' };
215+
assert.equal(head.title.value, 'A');
216+
217+
// earlier -> unchanged
218+
head.title = { path: [0, 9], value: 'C' };
219+
assert.equal(head.title.value, 'A');
220+
221+
// later -> update
222+
head.title = { path: [2], value: 'D' };
223+
assert.equal(head.title.value, 'D');
224+
225+
// longer but same prefix -> update
226+
head.title = { path: [2, 0], value: 'E' };
227+
assert.equal(head.title.value, 'E');
228+
229+
// shorter (earlier) than current with same prefix -> unchanged
230+
head.title = { path: [2], value: 'F' };
231+
assert.equal(head.title.value, 'E');
232+
});

0 commit comments

Comments
 (0)