From 835101b64262ee6c1de95be7df7c1b8e285462a2 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Thu, 28 Aug 2025 16:17:02 -0600 Subject: [PATCH] whoop, tests --- .../svelte/src/internal/server/payload.js | 10 +- .../src/internal/server/payload.test.ts | 232 ++++++++++++++++++ 2 files changed, 237 insertions(+), 5 deletions(-) create mode 100644 packages/svelte/src/internal/server/payload.test.ts diff --git a/packages/svelte/src/internal/server/payload.js b/packages/svelte/src/internal/server/payload.js index c6e32a918a..a01f8a7786 100644 --- a/packages/svelte/src/internal/server/payload.js +++ b/packages/svelte/src/internal/server/payload.js @@ -35,9 +35,9 @@ export class Payload { * Asynchronous work associated with this payload. `initial` is the promise from the function * this payload was passed to (if that function was async), and `followup` is any any additional * work from `compact` calls that needs to complete prior to collecting this payload's content. - * @type {{ initial: Promise | undefined, followup: Promise[] | undefined }} + * @type {{ initial: Promise | undefined, followup: Promise[] }} */ - promises = { initial: undefined, followup: undefined }; + promises = { initial: undefined, followup: [] }; /** * State which is associated with the content tree as a whole. @@ -114,7 +114,7 @@ export class Payload { .then((transformed_content) => Payload.#push_accumulated_content(child, transformed_content) ); - (this.promises.followup ??= []).push(followup); + this.promises.followup.push(followup); } else { Payload.#push_accumulated_content(child, fn(content)); } @@ -194,7 +194,7 @@ export class Payload { } else { flush(); - if (item.promises.initial) { + if (item.promises.initial || item.promises.followup.length) { has_async = true; segments.push( Payload.#collect_content_async([item], current_type, { head: '', body: '' }) @@ -238,7 +238,7 @@ export class Payload { // we can't do anything until it's done and we know our `out` array is complete. await item.promises.initial; } - for (const followup of item.promises.followup ?? []) { + for (const followup of item.promises.followup) { // this is sequential because `compact` could synchronously queue up additional followup work await followup; } diff --git a/packages/svelte/src/internal/server/payload.test.ts b/packages/svelte/src/internal/server/payload.test.ts new file mode 100644 index 0000000000..678afbcb3c --- /dev/null +++ b/packages/svelte/src/internal/server/payload.test.ts @@ -0,0 +1,232 @@ +import { assert, expect, test } from 'vitest'; +import { Payload, TreeState, TreeHeadState } from './payload.js'; + +test('collects synchronous body content by default', () => { + const payload = new Payload(); + payload.push('a'); + payload.child(($$payload) => { + $$payload.push('b'); + }); + payload.push('c'); + + const { head, body } = payload.collect(); + assert.equal(head, ''); + assert.equal(body, 'abc'); +}); + +test('child type switches content area (head vs body)', () => { + const payload = new Payload(); + payload.push('a'); + payload.child(($$payload) => { + $$payload.push('T'); + }, 'head'); + payload.push('b'); + + const { head, body } = payload.collect(); + assert.equal(head, 'T'); + assert.equal(body, 'ab'); +}); + +test('child inherits parent type when not specified', () => { + const parent = new Payload(undefined, undefined, undefined, 'head'); + parent.push(''); + parent.child(($$payload) => { + $$payload.push(''); + }); + const { head, body } = parent.collect(); + assert.equal(body, ''); + assert.equal(head, ''); +}); + +test('get_path returns the path indexes to a payload', () => { + const root = new Payload(); + let child_a: InstanceType | undefined; + let child_b: InstanceType | undefined; + let child_b_0: InstanceType | undefined; + + root.child(($$payload) => { + child_a = $$payload; + $$payload.push('A'); + }); + root.child(($$payload) => { + child_b = $$payload; + $$payload.child(($$inner) => { + child_b_0 = $$inner; + $$inner.push('B0'); + }); + $$payload.push('B1'); + }); + + assert.deepEqual(child_a!.get_path(), [0]); + assert.deepEqual(child_b!.get_path(), [1]); + assert.deepEqual(child_b_0!.get_path(), [1, 0]); +}); + +test('awaiting payload resolves async children; collect throws on async', async () => { + const payload = new Payload(); + payload.push('a'); + payload.child(async ($$payload) => { + await Promise.resolve(); + $$payload.push('x'); + }); + payload.push('y'); + + expect(() => payload.collect()).toThrow( + 'Encountered an asynchronous component while rendering synchronously' + ); + + const { body, head } = await payload; + assert.equal(head, ''); + assert.equal(body, 'axy'); +}); + +test('then() allows awaiting payload to get aggregated content', async () => { + const payload = new Payload(); + payload.push('1'); + payload.child(async ($$payload) => { + await Promise.resolve(); + $$payload.push('2'); + }); + payload.push('3'); + + const result = await payload; + assert.deepEqual(result, { head: '', body: '123' }); +}); + +test('compact synchronously aggregates a range and can transform into head/body', () => { + const payload = new Payload(); + payload.push('a'); + payload.push('b'); + payload.push('c'); + + payload.compact({ + start: 0, + end: 2, + fn: (content) => ({ head: 'H', body: content.body + 'd' }) + }); + + assert.equal(payload.length, 2); + const { head, body } = payload.collect(); + assert.equal(head, 'H'); + assert.equal(body, 'abdc'); +}); + +test('compact schedules followup when compaction input is async', async () => { + const payload = new Payload(); + payload.push('a'); + payload.child(async ($$payload) => { + await Promise.resolve(); + $$payload.push('X'); + }); + payload.push('b'); + + payload.compact({ + start: 0, + fn: (content) => ({ body: content.body.toLowerCase(), head: '' }) + }); + + const { body, head } = await payload; + assert.equal(head, ''); + assert.equal(body, 'axb'); +}); + +test('copy creates a deep copy of the tree and shares promises reference', () => { + const payload = new Payload(); + let child_ref: InstanceType | undefined; + payload.child(($$payload) => { + child_ref = $$payload; + $$payload.push('x'); + }); + payload.push('y'); + + const copy = payload.copy(); + assert.strictEqual(copy.promises, payload.promises); + + // mutate original + child_ref!.push('!'); + payload.push('?'); + + const original = payload.collect(); + const cloned = copy.collect(); + + assert.deepEqual(original, { head: '', body: 'x!y?' }); + assert.deepEqual(cloned, { head: '', body: 'xy' }); +}); + +test('local state is shallow-copied to children', () => { + const root = new Payload(); + root.local.select_value = 'A'; + let child: InstanceType | undefined; + root.child(($$payload) => { + child = $$payload; + }); + + assert.equal(child!.local.select_value, 'A'); + child!.local.select_value = 'B'; + assert.equal(root.local.select_value, 'A'); +}); + +test('subsume replaces tree content and state from other', () => { + const a = new Payload(undefined, undefined, undefined, 'head'); + a.push(''); + a.local.select_value = 'A'; + + const b = new Payload(); + b.child(async ($$payload) => { + await Promise.resolve(); + $$payload.push('body'); + }); + b.global.css.add({ hash: 'h', code: 'c' }); + b.global.head.title = { path: [1], value: 'Title' }; + b.local.select_value = 'B'; + b.promises.initial = Promise.resolve(); + + a.subsume(b); + + // content now matches b and is async + expect(() => a.collect()).toThrow( + 'Encountered an asynchronous component while rendering synchronously' + ); + assert.equal(a.type, 'body'); + assert.equal(a.local.select_value, 'B'); + assert.strictEqual(a.promises, b.promises); + + // global state transferred + assert.ok(a.global.css.has({ hash: 'h', code: 'c' }) || [...a.global.css][0]?.hash === 'h'); + assert.equal(a.global.head.title.value, 'Title'); +}); + +test('TreeState uid generator uses prefix and is shared by copy()', () => { + const state = new TreeState('id-'); + assert.equal(state.uid(), 'id-s1'); + const state_copy = state.copy(); + assert.equal(state_copy.uid(), 'id-s2'); + assert.equal(state.uid(), 'id-s3'); +}); + +test('TreeHeadState title ordering favors later lexicographic paths', () => { + const head = new TreeHeadState(() => ''); + + head.title = { path: [1], value: 'A' }; + assert.equal(head.title.value, 'A'); + + // equal path -> unchanged + head.title = { path: [1], value: 'B' }; + assert.equal(head.title.value, 'A'); + + // earlier -> unchanged + head.title = { path: [0, 9], value: 'C' }; + assert.equal(head.title.value, 'A'); + + // later -> update + head.title = { path: [2], value: 'D' }; + assert.equal(head.title.value, 'D'); + + // longer but same prefix -> update + head.title = { path: [2, 0], value: 'E' }; + assert.equal(head.title.value, 'E'); + + // shorter (earlier) than current with same prefix -> unchanged + head.title = { path: [2], value: 'F' }; + assert.equal(head.title.value, 'E'); +});