mirror of https://github.com/sveltejs/svelte
add tests from #18983 that fail on main but pass here
parent
4e314973d3
commit
cea4875a2f
@ -0,0 +1,39 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons =
|
||||
'<button>s</button><button>u</button><button>d</button><button>resolve a</button><button>resolve b</button><button>resolve c</button>';
|
||||
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
const [s, u, d, resolve_a, resolve_b, resolve_c] = target.querySelectorAll('button');
|
||||
|
||||
// batch A: `d` becomes 10, but is not evaluated yet (the `<span>` is deferred)
|
||||
s.click();
|
||||
await tick();
|
||||
|
||||
// batch B
|
||||
u.click();
|
||||
await tick();
|
||||
|
||||
// batch C overrides `d` while A and B are pending. This must not mark `d` as
|
||||
// clean, because the other batches haven't seen its new value (10) yet
|
||||
d.click();
|
||||
await tick();
|
||||
|
||||
// B sees A's change (but not C's), so `d` is 10 in its world
|
||||
resolve_b.click();
|
||||
await tick();
|
||||
assert.equal(logs[0], 'probe 10');
|
||||
|
||||
resolve_a.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>1</p><p>0</p><span>10</span><i>10</i>`);
|
||||
|
||||
resolve_c.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>1</p><p>1</p><span>99</span><i>99</i>`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,42 @@
|
||||
<script>
|
||||
let s = $state(0);
|
||||
let u = $state(0);
|
||||
let w = $state(0);
|
||||
let show = $state(true);
|
||||
let d = $derived(s * 10);
|
||||
|
||||
const pending = new Map();
|
||||
|
||||
function delay(name, value) {
|
||||
if (!value) return value;
|
||||
return new Promise((resolve) => pending.set(name, () => resolve(value)));
|
||||
}
|
||||
|
||||
function probe(value) {
|
||||
console.log(`probe ${value}`);
|
||||
return value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => s++}>s</button>
|
||||
<button onclick={() => u++}>u</button>
|
||||
<button
|
||||
onclick={() => {
|
||||
d = 99;
|
||||
w++;
|
||||
}}>d</button
|
||||
>
|
||||
<button onclick={() => pending.get('a')?.()}>resolve a</button>
|
||||
<button onclick={() => pending.get('b')?.()}>resolve b</button>
|
||||
<button onclick={() => pending.get('c')?.()}>resolve c</button>
|
||||
|
||||
<p>{await delay('a', s)}</p>
|
||||
<p>{await delay('c', w)}</p>
|
||||
<!-- inside a branch, so that B's traversal doesn't reach (and stash) the effect reading `d` -->
|
||||
{#if show}
|
||||
<span>{d}</span>
|
||||
{/if}
|
||||
|
||||
{#if await delay('b', u)}
|
||||
<i>{await probe(d)}</i>
|
||||
{/if}
|
||||
@ -0,0 +1,33 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
// Batch B (x) commits while an earlier batch A (y) is still pending, but B stays around because
|
||||
// of the async work in the new boundary. While B flushes its effects, an effect creates a new batch C,
|
||||
// and `d` is computed for the first time afterwards (so it's captured in both B and C). When A
|
||||
// commits later, it sees B's previous values, which must be consistent (`d` must be `x * 2`).
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
await tick();
|
||||
const [y, x, shift] = target.querySelectorAll('button');
|
||||
assert.deepEqual(logs, [0, 0, 0]);
|
||||
|
||||
// A
|
||||
y.click();
|
||||
await tick();
|
||||
|
||||
// B
|
||||
x.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, [0, 0, 0, 0, 1, 2]);
|
||||
|
||||
// A commits
|
||||
shift.click();
|
||||
await tick();
|
||||
|
||||
for (let i = 0; i < logs.length; i += 3) {
|
||||
assert.equal(logs[i + 2], logs[i + 1] * 2, `torn read: ${logs.slice(i, i + 3)}`);
|
||||
}
|
||||
|
||||
assert.deepEqual(logs.slice(-3), [1, 1, 2]);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,38 @@
|
||||
<script>
|
||||
import { untrack } from 'svelte';
|
||||
|
||||
let x = $state(0);
|
||||
let y = $state(0);
|
||||
let z = $state(0);
|
||||
let d = $derived(x * 2);
|
||||
|
||||
const queue = [];
|
||||
function push(v) {
|
||||
return new Promise((r) => queue.push(() => r(v)));
|
||||
}
|
||||
|
||||
// creates a new batch while the batch that changed `x` flushes its effects...
|
||||
$effect(() => {
|
||||
if (x) untrack(() => z++);
|
||||
});
|
||||
|
||||
// ...before `d` is computed for the first time in that batch
|
||||
$effect(() => {
|
||||
console.log(y, x, d);
|
||||
});
|
||||
</script>
|
||||
|
||||
<button onclick={() => y++}>y</button>
|
||||
<button onclick={() => x++}>x</button>
|
||||
<button onclick={() => queue.shift()()}>shift</button>
|
||||
|
||||
<p>{y ? await push(y) : y}</p>
|
||||
|
||||
{#if x}
|
||||
<svelte:boundary>
|
||||
<p>{await push(x)}</p>
|
||||
{#snippet pending()}
|
||||
<p>loading</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
{/if}
|
||||
@ -0,0 +1,44 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
const buttons =
|
||||
'<button>page</button><button>filter</button><button>shift</button><button>pop</button>';
|
||||
|
||||
// The eager batch that re-runs `load` for the new `$state.eager(filter)` value computes it
|
||||
// with the old `page`, because eager batches don't see other batches' changes. Therefore it
|
||||
// must not be merged into the (pending) `page` batch — else its result would resolve that
|
||||
// batch's `load(2, ...)` and be shown next to `page: 2`
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
await tick();
|
||||
const [page, filter, shift, pop] = target.querySelectorAll('button');
|
||||
|
||||
// B1: page = 2 -> load(2, 'a') pending
|
||||
page.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>page: 1</p><p>result: 1/a</p>`);
|
||||
|
||||
// filter = 'b' -> eager batch re-runs load(1, 'b'), pending
|
||||
filter.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>page: 1</p><p>result: 1/a</p>`);
|
||||
|
||||
// resolve load(1, 'b') -> the eager batch commits on its own
|
||||
pop.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>page: 1</p><p>result: 1/b</p>`);
|
||||
|
||||
// resolve load(2, 'a') -> B1 commits (showing B1's own result is fine, as is
|
||||
// waiting for load(2, 'b'), as long as the result matches the page)
|
||||
shift.click();
|
||||
await tick();
|
||||
const [p1, p2] = target.querySelectorAll('p');
|
||||
assert.equal(p1.textContent, 'page: 2');
|
||||
assert.match(p2.textContent ?? '', /^result: 2\//);
|
||||
|
||||
// load(2, 'b') resolves
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `${buttons}<p>page: 2</p><p>result: 2/b</p>`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,20 @@
|
||||
<script>
|
||||
let page = $state(1);
|
||||
let filter = $state('a');
|
||||
|
||||
const deferred = [];
|
||||
|
||||
function load(page, filter) {
|
||||
const value = `${page}/${filter}`;
|
||||
if (value === '1/a') return value;
|
||||
return new Promise((resolve) => deferred.push(() => resolve(value)));
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => page++}>page</button>
|
||||
<button onclick={() => (filter = 'b')}>filter</button>
|
||||
<button onclick={() => deferred.shift()?.()}>shift</button>
|
||||
<button onclick={() => deferred.pop()?.()}>pop</button>
|
||||
|
||||
<p>page: {page}</p>
|
||||
<p>result: {await load(page, $state.eager(filter))}</p>
|
||||
@ -0,0 +1,9 @@
|
||||
<script>
|
||||
let { big } = $props();
|
||||
|
||||
$effect(() => {
|
||||
console.log(`effect ${big}`);
|
||||
});
|
||||
</script>
|
||||
|
||||
<p>child</p>
|
||||
@ -0,0 +1,37 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target, logs, raf }) {
|
||||
await tick();
|
||||
const [x, s, toggle, shift] = target.querySelectorAll('button');
|
||||
assert.deepEqual(logs, ['effect false']);
|
||||
|
||||
// keep an earlier batch pending, so that the next one commits while time travelling
|
||||
x.click();
|
||||
await tick();
|
||||
|
||||
// `big` stays `false`, so the effect must not re-run
|
||||
s.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['effect false']);
|
||||
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
'<button>x</button><button>s</button><button>toggle</button><button>shift</button><p>1</p><div><p>child</p></div>'
|
||||
);
|
||||
|
||||
// outro, then reintro before the outro finished: the branch is resumed
|
||||
toggle.click();
|
||||
await tick();
|
||||
raf.tick(50);
|
||||
toggle.click();
|
||||
await tick();
|
||||
raf.tick(200);
|
||||
|
||||
assert.deepEqual(logs, ['effect false']);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import { fade } from 'svelte/transition';
|
||||
import Child from './Child.svelte';
|
||||
|
||||
let x = $state(0);
|
||||
let s = $state(0);
|
||||
let show = $state(true);
|
||||
const big = $derived(s > 100);
|
||||
|
||||
const queued = [];
|
||||
|
||||
function push(v) {
|
||||
if (v === 0) return v;
|
||||
return new Promise((resolve) => queued.push(() => resolve(v)));
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => x++}>x</button>
|
||||
<button onclick={() => s++}>s</button>
|
||||
<button onclick={() => (show = !show)}>toggle</button>
|
||||
<button onclick={() => queued.shift()?.()}>shift</button>
|
||||
|
||||
<p>{await push(x)}</p>
|
||||
|
||||
{#if show}
|
||||
<div transition:fade={{ duration: 100 }}>
|
||||
<Child {big} />
|
||||
</div>
|
||||
{/if}
|
||||
@ -0,0 +1,9 @@
|
||||
<script>
|
||||
let { item } = $props();
|
||||
$effect(() => {
|
||||
console.log('mount ' + item);
|
||||
return () => console.log('destroy ' + item);
|
||||
});
|
||||
</script>
|
||||
|
||||
<span>{item}</span>
|
||||
@ -0,0 +1,47 @@
|
||||
import { flushSync, tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target, logs }) {
|
||||
const [a, push, clear, shift] = target.querySelectorAll('button');
|
||||
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, ['mount 1', 'mount 2', 'mount 3']);
|
||||
|
||||
// batch X: a=1, pending
|
||||
a.click();
|
||||
await tick();
|
||||
|
||||
// batch Y: list changes; the each block reads `a`, so Y merges into X.
|
||||
// The each block registers commit/discard callbacks with Y
|
||||
push.click();
|
||||
await tick();
|
||||
|
||||
// X resolves and throws while processing (not handled by any boundary), so it is discarded
|
||||
// rather than committed. Y's discard callbacks must still run
|
||||
let error = null;
|
||||
shift.click();
|
||||
try {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await Promise.resolve();
|
||||
flushSync();
|
||||
}
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
assert.match(/** @type {Error} */ (error)?.message, /^boom/);
|
||||
|
||||
logs.length = 0;
|
||||
clear.click();
|
||||
await tick();
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`<button>a</button><button>push</button><button>clear</button><button>shift</button><p>a: 0</p>`
|
||||
);
|
||||
// the removed items must be destroyed, not kept around for the discarded batch Y
|
||||
assert.deepEqual(logs, ['destroy 1', 'destroy 2', 'destroy 3']);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,39 @@
|
||||
<script>
|
||||
import Item from './Item.svelte';
|
||||
|
||||
let a = $state(0);
|
||||
let list = $state([1, 2, 3]);
|
||||
|
||||
let queue = [];
|
||||
function push(v) {
|
||||
const d = Promise.withResolvers();
|
||||
queue.push(() => d.resolve(v));
|
||||
return d.promise;
|
||||
}
|
||||
|
||||
function check(v) {
|
||||
if (v === 1) throw new Error('boom');
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => a++}>a</button>
|
||||
<button onclick={() => list.push(4)}>push</button>
|
||||
<button onclick={() => (list = [])}>clear</button>
|
||||
<button onclick={() => queue.shift()?.()}>shift</button>
|
||||
|
||||
<!-- no failed snippet/onerror: errors are not handled by the boundary -->
|
||||
<svelte:boundary>
|
||||
{#if check(await push(a))}
|
||||
<p>a: {a}</p>
|
||||
{/if}
|
||||
|
||||
<!-- reads `a`, so batches changing `list` depend on (and merge into) a pending batch changing `a` -->
|
||||
{#each list.slice(0, a + 10) as item (item)}
|
||||
<Item {item} />
|
||||
{/each}
|
||||
|
||||
{#snippet pending()}
|
||||
<p>loading...</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
Loading…
Reference in new issue