chore: improve hydration tests (#10887)

* use server-rendered HTML as hydration test starting point

* update tests

* remove _before.html files

* remove _before_head.html files

* override output with _expected.html

* expected output for binding-input case

* remove unused files

* fix

* changeset
pull/10892/head
Rich Harris 1 year ago committed by GitHub
parent fe7c45ba13
commit 7f10642add
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly hydrate controlled each-else block

@ -50,7 +50,7 @@ export function set_current_each_item(item) {
* @param {number} flags
* @param {null | ((item: V) => string)} get_key
* @param {(anchor: null, item: V, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn
* @param {null | ((anchor: Node | null) => void)} fallback_fn
* @param {typeof reconcile_indexed_array | reconcile_tracked_array} reconcile_fn
* @returns {void}
*/
@ -158,7 +158,7 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
resume_effect(fallback);
} else {
fallback = render_effect(() => {
var dom = fallback_fn(anchor);
var dom = fallback_fn(hydrating ? null : anchor);
return () => {
if (dom !== undefined) {
@ -199,7 +199,7 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
* @param {number} flags
* @param {null | ((item: V) => string)} get_key
* @param {(anchor: null, item: V, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn
* @param {null | ((anchor: Node | null) => void)} fallback_fn
* @returns {void}
*/
export function each_keyed(anchor, get_collection, flags, get_key, render_fn, fallback_fn) {
@ -212,7 +212,7 @@ export function each_keyed(anchor, get_collection, flags, get_key, render_fn, fa
* @param {() => V[]} get_collection
* @param {number} flags
* @param {(anchor: null, item: V, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn
* @param {null | ((anchor: Node | null) => void)} fallback_fn
* @returns {void}
*/
export function each_indexed(anchor, get_collection, flags, render_fn, fallback_fn) {

@ -1 +0,0 @@
<!--ssr:0--><h1>Hello world!</h1><!--ssr:0-->

@ -1,2 +0,0 @@
<!--ssr:0--><input />
<p>Hello world!</p><!--ssr:0-->

@ -0,0 +1 @@
<!--ssr:0--><input> <p>Hello world!</p><!--ssr:0-->

@ -1,3 +0,0 @@
<!--ssr:0--><div><!-- test1 --></div>
p
<div><!-- test1 --><!-- test2 --></div><!--ssr:0-->

@ -1,8 +0,0 @@
<!--ssr:0--><div>hello</div>
<div><div>bye</div></div>
<div>
<div>aaa</div>
<div>bbb</div>
</div><!--ssr:0-->

@ -1,5 +0,0 @@
<!--ssr:0-->
<!--ssr:1--><p>This <code>p</code> and the <code>slot</code> below are direct children of the root.</p>
<!--ssr:2--><main>There should be one</main><!--ssr:2-->
<!--ssr:1-->
<!--ssr:0-->

@ -1,3 +0,0 @@
<!--ssr:0--><div>
<!--ssr:1--><p>nested</p><!--ssr:1-->
</div><!--ssr:0-->

@ -1,3 +0,0 @@
<!--ssr:0-->
<!--ssr:1--><p>nested</p><!--ssr:1-->
<!--ssr:0-->

@ -1 +0,0 @@
<!--ssr:0--><h1>Hello world!</h1><!--ssr:0-->

@ -1,6 +1,10 @@
import { test } from '../../test';
export default test({
server_props: {
name: 'world'
},
props: {
name: 'everybody'
},

@ -1 +0,0 @@
<!--ssr:0--><hr><hr> <p></p> <p></p><!--ssr:0-->

@ -1 +0,0 @@
<!--ssr:0--><h1>Hello world!</h1><!--ssr:0-->

@ -1,5 +0,0 @@
<!--ssr:0--><ul><!--ssr:1-->
<!--ssr:2--><li>animal</li><!--ssr:2-->
<!--ssr:3--><li>vegetable</li><!--ssr:3-->
<!--ssr:4--><li>mineral</li><!--ssr:4-->
<!--ssr:1--></ul><!--ssr:0-->

@ -1,2 +0,0 @@
<!--ssr:0--><!--ssr:1--><!--ssr:each_else--><p>empty</p><!--ssr:1-->
<!--ssr:2--><!--ssr:3--><p>a</p><!--ssr:3--><!--ssr:2--><!--ssr:0-->

@ -1,3 +1,13 @@
import { test } from '../../test';
export default test({});
export default test({
server_props: {
items1: [],
items2: [{ name: 'a' }]
},
props: {
items1: [{ name: 'a' }],
items2: []
}
});

@ -1,6 +1,5 @@
<script>
let items1 = $state(typeof window !== 'undefined' ? [{name: 'a'}]: []);
let items2 = $state(typeof window === 'undefined' ? [{name: 'a'}]: []);
let { items1, items2 } = $props();
</script>
{#each items1 as item}

@ -1,12 +0,0 @@
<!--ssr:0--><ul><!--ssr:1--><!--ssr:7--><li>a</li><!--ssr:7--><!--ssr:8--><li>b</li><!--ssr:8--><!--ssr:1--></ul>
<ul><!--ssr:2--><!--ssr:9--><li>a</li><!--ssr:9--><!--ssr:10--><li>b</li><!--ssr:10--><!--ssr:2--></ul>
<ul><!--ssr:3--><!--ssr:11--><li>a</li><!--ssr:11--><!--ssr:12--><li>b</li><!--ssr:12--><!--ssr:3--></ul>
<!--ssr:4--><!--ssr:13--><li>a</li>
<li>a</li><!--ssr:13--><!--ssr:14--><li>b</li>
<li>b</li><!--ssr:14--><!--ssr:4-->
<!--ssr:5--><!--ssr:15--><li>a</li>
<li>a</li><!--ssr:15--><!--ssr:16--><li>b</li>
<li>b</li><!--ssr:16--><!--ssr:5-->
<!--ssr:6--><!--ssr:17--><li>a</li>
<li>a</li><!--ssr:17--><!--ssr:18--><li>b</li>
<li>b</li><!--ssr:18--><!--ssr:6--><!--ssr:0--></div>

@ -1,6 +1,14 @@
import { assert_ok, test } from '../../test';
export default test({
server_props: {
items: [{ name: 'a' }, { name: 'b' }]
},
props: {
items: [{ name: 'a' }]
},
snapshot(target) {
const ul = target.querySelector('ul');
assert_ok(ul);

@ -1,5 +1,5 @@
<script>
let items = $state(typeof window !== 'undefined' ? [{name: 'a'}]: [{name: 'a'}, {name: 'b'}]);
let { items } = $props();
</script>
<ul>

@ -1,9 +0,0 @@
<!--ssr:0--><ul><!--ssr:1--><!--ssr:7--><li>a</li><!--ssr:7--><!--ssr:1--></ul>
<ul><!--ssr:2--><!--ssr:8--><li>a</li><!--ssr:8--><!--ssr:2--></ul>
<ul><!--ssr:3--><!--ssr:9--><li>a</li><!--ssr:9--><!--ssr:3--></ul>
<!--ssr:4--><!--ssr:10--><li>a</li>
<li>a</li><!--ssr:10--><!--ssr:4-->
<!--ssr:5--><!--ssr:11--><li>a</li>
<li>a</li><!--ssr:11--><!--ssr:5-->
<!--ssr:6--><!--ssr:12--><li>a</li>
<li>a</li><!--ssr:12--><!--ssr:6--><!--ssr:0--></div>

@ -1,6 +1,14 @@
import { assert_ok, test } from '../../test';
export default test({
server_props: {
items: [{ name: 'x' }]
},
props: {
items: [{ name: 'a' }, { name: 'b' }]
},
snapshot(target) {
const ul = target.querySelector('ul');
assert_ok(ul);

@ -1,5 +1,5 @@
<script>
let items = $state(typeof window === 'undefined' ? [{name: 'x'}]: [{name: 'a'}, {name: 'b'}]);
let { items } = $props();
</script>
<ul>

@ -1,5 +0,0 @@
<!--ssr:0--><ul><!--ssr:1-->
<!--ssr:2--><li>animal</li><!--ssr:2-->
<!--ssr:3--><li>vegetable</li><!--ssr:3-->
<!--ssr:4--><li>mineral</li><!--ssr:4-->
<!--ssr:1--></ul><!--ssr:0-->

@ -1,3 +0,0 @@
<!--ssr:0--><h1>Hello, world</h1>
<!--ssr:1--><p>foo</p><!--ssr:1-->
<div><!--ssr:2--><p>foo</p><!--ssr:2--></div><!--ssr:0-->

@ -1 +0,0 @@
<!--ssr:0--><div class="foo"></div><!--ssr:0-->

@ -1,6 +1,8 @@
import { test } from '../../test';
export default test({
server_props: {},
props: {
className: 'bar'
},

@ -1,6 +1,10 @@
import { test } from '../../test';
export default test({
server_props: {
className: 'foo'
},
props: {
className: 'bar'
},

@ -1,6 +1,10 @@
import { test } from '../../test';
export default test({
server_props: {
id: 'foo'
},
snapshot(target) {
const div = target.querySelector('div');

@ -1 +0,0 @@
<!--ssr:0--><div class="bar" foo="bar"></div> <div class="bar" foo="bar"></div><!--ssr:0-->

@ -1,3 +0,0 @@
<!--ssr:0--><p><span>1</span>
<!--ssr:1-->
<!--ssr:if:true--><code>2</code><!--ssr:1--></p><!--ssr:0-->

@ -1,3 +0,0 @@
<!--ssr:0--><div>
<p>nested</p>
</div><!--ssr:0-->

@ -1 +0,0 @@
<!--ssr:0--><h1>Hello world!</h1><!--ssr:0-->

@ -1,5 +0,0 @@
<!--ssr:0--><button>click me</button>
<!--ssr:1-->
<!--ssr:if:false-->
<!--ssr:1-->
<!--ssr:0-->

@ -1 +0,0 @@
<!--ssr:0--><p>1 2 <span>3</span></p><!--ssr:0-->

@ -1 +0,0 @@
<!--ssr:0--><!--ssr:1--><!--ssr:1--><!--ssr:0-->

@ -1,6 +0,0 @@
<!--ssr:0-->
<!--ssr:1--><meta name="main_html" content="main_html"><!--ssr:1-->
<meta name="main" content="main">
<!--ssr:2--><!--ssr:3--><meta name="head_nested_html" content="head_nested_html"><!--ssr:3-->
<meta name="head_nested" content="head_nested"><!--ssr:2--><!--ssr:4--><meta name="nested_html" content="nested_html"><!--ssr:4-->
<meta name="nested" content="nested"><!--ssr:0-->

@ -1 +0,0 @@
<!--ssr:0--><div>Just a dummy page.</div><!--ssr:0-->

@ -1,4 +0,0 @@
<title>Some Title</title><!--ssr:0-->
<link rel="canonical" href="/">
<meta name="description" content="some description">
<meta name="keywords" content="some keywords"><!--ssr:0-->

@ -1 +0,0 @@
<!--ssr:0-->&nbsp;1 <!--ssr:1-->2<!--ssr:1--> 3<!--ssr:0-->

@ -1,2 +0,0 @@
<!--ssr:0--><div><!--ssr:1--><!--ssr:if:true--><p>foo!</p><!--ssr:1-->
<!--ssr:2--><!--ssr:if:true--><p>bar!</p><!--ssr:2--></div><!--ssr:0-->

@ -1,4 +0,0 @@
<!--ssr:0-->
<!--ssr:1-->
<!--ssr:if:true-->x<!--ssr:1-->
<!--ssr:0-->

@ -1,4 +0,0 @@
<!--ssr:0-->
<!--ssr:1-->
<!--ssr:if:true--><!--ssr:1-->
<!--ssr:0-->

@ -1,3 +1,11 @@
import { test } from '../../test';
export default test({});
export default test({
server_props: {
foo: ''
},
props: {
foo: 'x'
}
});

@ -1,5 +0,0 @@
<!--ssr:0--><p>before</p>
<!--ssr:1-->
<!--ssr:if:false-->
<!--ssr:1-->
<p>after</p><!--ssr:0-->

@ -1 +0,0 @@
<!--ssr:0--><!--ssr:1--><!--ssr:if:false--><p>foo</p><!--ssr:1--><!--ssr:0-->

@ -1 +0,0 @@
<!--ssr:0--><!--ssr:1--><!--ssr:if:false--><p>bar</p><!--ssr:1--><!--ssr:0-->

@ -3,5 +3,13 @@ import { test } from '../../test';
// even {#if true} or {#if false} should be kept as an if block, because it could be {#if browser} originally,
// which is then different between client and server.
export default test({
server_props: {
condition: false
},
props: {
condition: true
},
trim_whitespace: false
});

@ -1,3 +1,7 @@
<script>
let { condition } = $props();
</script>
{#if true}
<p>foo</p>
{:else}

@ -1 +0,0 @@
<!--ssr:0--><!--ssr:1--><!--ssr:if:true--><p>foo!</p><!--ssr:1--> <!--ssr:2--><!--ssr:2--><!--ssr:0-->

@ -1,4 +0,0 @@
<!--ssr:0-->
<!--ssr:1-->
<!--ssr:if:true--><p>foo!</p><!--ssr:1-->
<!--ssr:0-->

@ -1 +0,0 @@
<!--ssr:0--><a href="/bar">foo</a><!--ssr:0-->

@ -1,6 +1,14 @@
import { test } from '../../test';
export default test({
server_props: {
browser: false
},
props: {
browser: true
},
test(assert, target) {
assert.equal(target.querySelector('a')?.getAttribute('href'), '/bar');
}

@ -1,5 +1,5 @@
<script>
let browser = typeof window !== 'undefined';
let { browser } = $props();
</script>
<a href={browser ? '/foo': '/bar'}>foo</a>

@ -1,2 +0,0 @@
<!--ssr:0--><noscript>JavaScript is required for this site.</noscript>
<h1>Hello!</h1><p>Count: 0</p><!--ssr:0-->

@ -1 +0,0 @@
<!--ssr:0--><!--ssr:1--><p>foo</p><!--ssr:1--><!--ssr:0-->

@ -1 +0,0 @@
<!--ssr:0--><!--ssr:1--><p>bar</p><!--ssr:1--><!--ssr:0-->

@ -1,2 +0,0 @@
<p><p>invalid</p></p>
<p><p>invalid</p></p>

@ -1,6 +0,0 @@
<!--ssr:0--><!--ssr:1--><p><!--ssr:2--></p>
<p>invalid</p><!--ssr:2--><p></p><!--ssr:1-->
<p><!--ssr:3--></p>
<p>invalid</p>
<!--ssr:3-->
<p></p><!--ssr:0-->

@ -1 +0,0 @@
<!--ssr:0--><svg><!--ssr:1--><circle cx="200" cy="500" r="200"></circle><!--ssr:1--></svg><!--ssr:0-->

Before

Width:  |  Height:  |  Size: 103 B

@ -1,10 +0,0 @@
<!--ssr:0--><div>before</div>
<br />
<!--ssr:1-->
<!--ssr:2--><!--ssr:3-->a<!--ssr:3--><!--ssr:2-->
<!--ssr:4--><!--ssr:5-->b<!--ssr:5--><!--ssr:4-->
<!--ssr:6--><!--ssr:7-->c<!--ssr:7--><!--ssr:6-->
<!--ssr:1-->
<div>after</div><!--ssr:0-->

@ -1,4 +0,0 @@
<!--ssr:0-->
<!--ssr:1--><p>this is some html</p>
<p>and so is this</p><!--ssr:1-->
<!--ssr:0-->

@ -1,3 +1,11 @@
import { test } from '../../test';
export default test({});
export default test({
server_props: {
x: ''
},
props: {
x: 'x'
}
});

@ -1,5 +1,5 @@
<script>
let x = typeof window === 'undefined' ? '' : 'x'
let { x } = $props();
</script>
{x}

@ -1 +0,0 @@
<!--ssr:0--><!--ssr:1--><div><!--ssr:2--><div slot="foo">foo override</div><!--ssr:2--> <!--ssr:3-->default<!--ssr:3--></div><!--ssr:1--><!--ssr:0-->

@ -4,14 +4,14 @@ import * as fs from 'node:fs';
import { assert } from 'vitest';
import { compile_directory, should_update_expected } from '../helpers.js';
import { assert_html_equal } from '../html_equal.js';
import { suite, assert_ok } from '../suite.js';
import { suite, assert_ok, type BaseTest } from '../suite.js';
import { createClassComponent } from 'svelte/legacy';
import { render } from 'svelte/server';
import type { CompileOptions } from '#compiler';
interface HydrationTest {
solo?: boolean;
skip?: boolean;
interface HydrationTest extends BaseTest {
load_compiled?: boolean;
server_props?: Record<string, any>;
props?: Record<string, any>;
compileOptions?: Partial<CompileOptions>;
/**
@ -53,14 +53,16 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
const target = window.document.body;
const head = window.document.head;
target.innerHTML = read_html(`${cwd}/_before.html`);
const rendered = render((await import(`${cwd}/_output/server/main.svelte.js`)).default, {
props: config.server_props ?? config.props ?? {}
});
let before_head;
try {
before_head = read_html(`${cwd}/_before_head.html`);
head.innerHTML = before_head;
} catch (err) {
// continue regardless of error
fs.writeFileSync(`${cwd}/_output/body.html`, rendered.html + '\n');
target.innerHTML = rendered.html;
if (rendered.head) {
fs.writeFileSync(`${cwd}/_output/head.html`, rendered.head + '\n');
head.innerHTML = rendered.head;
}
config.before_test?.();
@ -95,29 +97,16 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
assert.ok(!got_hydration_error, 'Unexpected hydration error');
}
try {
assert_html_equal(target.innerHTML, read_html(`${cwd}/_after.html`, `${cwd}/_before.html`));
} catch (error) {
if (should_update_expected()) {
fs.writeFileSync(`${cwd}/_after.html`, target.innerHTML);
console.log(`Updated ${cwd}/_after.html.`);
} else {
throw error;
}
}
const expected = fs.existsSync(`${cwd}/_expected.html`)
? read_html(`${cwd}/_expected.html`)
: rendered.html;
assert_html_equal(target.innerHTML, expected);
if (before_head) {
try {
const after_head = read_html(`${cwd}/_after_head.html`, `${cwd}/_before_head.html`);
assert_html_equal(head.innerHTML, after_head);
} catch (error) {
if (should_update_expected()) {
fs.writeFileSync(`${cwd}/_after_head.html`, head.innerHTML);
console.log(`Updated ${cwd}/_after_head.html.`);
} else {
throw error;
}
}
if (rendered.head) {
const expected = fs.existsSync(`${cwd}/_expected_head.html`)
? read_html(`${cwd}/_expected_head.html`)
: rendered.head;
assert_html_equal(head.innerHTML, expected);
}
if (config.snapshot) {

Loading…
Cancel
Save