chore: reproduce pre-hydration writes failing across async reactivity PRs

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
teemingc-hydration-regression-test
Tee Ming Chew 1 day ago
parent 943cb84c53
commit 834d798208

@ -0,0 +1,13 @@
<script>
let { children } = $props();
</script>
<div>Resolving a promise here: {await Promise.resolve('ok')}</div>
<svelte:boundary>
{@render children?.()}
{#snippet pending()}
loading...
{/snippet}
</svelte:boundary>

@ -0,0 +1,11 @@
<script>
let { data, children } = $props();
let { root, child } = $derived(data);
$effect(() => {
console.log(root, child);
});
</script>
{@render children?.()}

@ -0,0 +1,5 @@
<script>
let { children } = $props();
</script>
{@render children?.()}

@ -0,0 +1,30 @@
import { settled } from 'svelte';
import { test } from '../../test';
// Regression test for https://github.com/sveltejs/kit/issues/17383
export default test({
mode: ['hydrate'],
skip_initial_flushSync: true,
async test({ assert, target, component, mod, hydrate, logs }) {
const tree = mod.create_tree();
const { data, child } = tree;
tree.data = {};
tree.child = undefined;
component.tree = tree;
await Promise.resolve();
// Kit applies the loaded data to its existing root immediately before hydration.
// Do not flush these writes: the async boundary must encounter the pending batch.
tree.data = data;
tree.child = child;
hydrate();
await settled();
assert.htmlEqual(
target.innerHTML,
'<div>Resolving a promise here: ok</div><div>this is the child</div>'
);
assert.deepEqual(logs, ['root', 'child']);
}
});

@ -0,0 +1,94 @@
<script module>
import RootLayout from './RootLayout.svelte';
import AsyncLayout from './AsyncLayout.svelte';
import DataLayout from './DataLayout.svelte';
import Page from './Page.svelte';
import { RenderNode } from './state.svelte.js';
function onerror(error) {
throw error;
}
const root_data = { root: 'root' };
const async_data = { root: 'root' };
const data_layout_data = { root: 'root', child: 'child' };
const page_data = { root: 'root', child: 'child' };
/**
* Builds a plain, non-reactive tree with the same shape as the one produced
* by `create_tree`. Used for SSR, where no class writes should happen.
*/
function create_server_tree() {
return {
component: RootLayout,
error: undefined,
data: root_data,
child: {
component: AsyncLayout,
error: undefined,
data: async_data,
child: {
component: DataLayout,
error: undefined,
data: data_layout_data,
child: {
component: Page,
error: undefined,
data: page_data,
child: undefined
}
}
}
};
}
/**
* Builds the reactive tree. The `data` and `child` writes happen here, i.e.
* before hydration starts, rather than during component initialization.
*/
export function create_tree() {
const root = new RenderNode(RootLayout);
const async_layout = new RenderNode(AsyncLayout);
const data_layout = new RenderNode(DataLayout);
const page = new RenderNode(Page);
root.data = root_data;
root.child = async_layout;
async_layout.data = async_data;
async_layout.child = data_layout;
data_layout.data = data_layout_data;
data_layout.child = page;
page.data = page_data;
return root;
}
</script>
<script>
let { tree = create_server_tree() } = $props();
</script>
{#snippet node(n, depth)}
{const Component = $derived(n.component)}
{const Error = $derived(n.error)}
{const data = $derived(n.data)}
{#snippet failed(error)}
<Error {error} />
{/snippet}
<svelte:boundary onerror={Error ? onerror : undefined} failed={Error ? failed : undefined}>
{#if n.child}
<Component {data}>
{@render node(n.child, depth + 1)}
</Component>
{:else}
<Component {data} />
{/if}
</svelte:boundary>
{/snippet}
{@render node(tree, 0)}

@ -0,0 +1,21 @@
export class RenderNode {
/** @type {any} */
component;
/** @type {any} */
error;
data = $state.raw({});
/** @type {RenderNode | undefined} */
child = $state.raw();
/**
* @param {any} component
* @param {any} [error]
*/
constructor(component, error) {
this.component = component;
this.error = error;
}
}
Loading…
Cancel
Save