test: add async tests that already pass on main, unskip async-overlap-multiple-2 (#18981)

These tests from #18880 already pass on `main`. Moving them out of that
PR makes it clearer what behaviour is actually changing over there
pull/18176/merge
Rich Harris 2 days ago committed by GitHub
parent 6480a9737f
commit d9acd2da90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,33 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = '<button>x</button><button>show</button><button>shift</button>';
export default test({
async test({ assert, target, logs }) {
await tick();
const [x, show, shift] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, `<p>true</p><p>1</p>${buttons}`);
assert.deepEqual(logs, []);
// write x — the batch is pending on its async expression, and claims
// the `positive` derived (marked through x)
x.click();
await tick();
assert.deepEqual(logs, []);
// an independent batch runs the effect, which reads the claimed derived
// through the pending batch's overlay (x = 1, so `positive` is true)
show.click();
await tick();
assert.deepEqual(logs, ['positive true']);
// the pending batch settles and commits x = 2 — `positive` recomputes
// to the same value (true), so the effect should not re-run
shift.click();
await tick();
assert.deepEqual(logs, ['positive true']);
assert.htmlEqual(target.innerHTML, `<p>true</p><p>2</p>${buttons}`);
}
});

@ -0,0 +1,26 @@
<script>
let x = $state(1);
let show = $state(false);
let pend = false;
const deferred = [];
let positive = $derived(x > 0);
function delay(value) {
if (!pend) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
$effect(() => {
if (show) {
console.log(`positive ${positive}`);
}
});
</script>
<p>{positive}</p>
<p>{await delay(x)}</p>
<button onclick={() => { pend = true; x += 1; }}>x</button>
<button onclick={() => (show = true)}>show</button>
<button onclick={() => deferred.shift()?.()}>shift</button>

@ -0,0 +1,33 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
await tick();
const [revert, y, shift] = target.querySelectorAll('button');
assert.htmlEqual(
target.innerHTML,
'<p>0</p><button>revert</button><button>y</button><button>shift</button>'
);
assert.deepEqual(logs, []);
// write x and revert it within the same batch — the batch is pending
// (its async expression re-runs), with previous === current for `x`
revert.click();
await tick();
assert.deepEqual(logs, []);
// an independent batch runs the effect, which now reads `x` through
// the pending batch's overlay (seeing the held-back value 0)
y.click();
await tick();
assert.deepEqual(logs, ['effect 0 1']);
// the pending batch settles and commits x === 0, i.e. exactly the value
// the effect already saw — it should not re-run
shift.click();
await tick();
assert.deepEqual(logs, ['effect 0 1']);
}
});

@ -0,0 +1,23 @@
<script>
let x = $state(0);
let y = $state(0);
let pend = false;
const deferred = [];
function delay(value) {
if (!pend) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
$effect(() => {
if (y > 0) {
console.log(`effect ${x} ${y}`);
}
});
</script>
<p>{await delay(x)}</p>
<button onclick={() => { pend = true; x += 1; x -= 1; }}>revert</button>
<button onclick={() => y++}>y</button>
<button onclick={() => deferred.shift()?.()}>shift</button>

@ -0,0 +1,29 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
async test({ assert, target, logs }) {
await tick();
const [update, resolve_a, resolve_b] = target.querySelectorAll('button');
assert.deepEqual(logs, [0]);
update.click();
await tick();
assert.deepEqual(logs, [0]);
// The new if block evaluates parity while the text effect is still deferred.
resolve_b.click();
await tick();
assert.deepEqual(logs, [0, 2]);
// The batch must forget its earlier deferred DIRTY entry for parity.
resolve_a.click();
await tick();
assert.deepEqual(logs, [0, 2]);
assert.htmlEqual(
target.innerHTML,
'<button>update</button><button>resolve a</button><button>resolve b</button><p>2:0</p>'
);
}
});

@ -0,0 +1,22 @@
<script>
let count = $state(0);
const parity = $derived.by(() => {
console.log(count);
return count % 2;
});
const pending = new Map();
function delay(name, value) {
if (value === 0) return value;
return new Promise(resolve => pending.set(name, () => resolve(value)));
}
</script>
<button onclick={() => count += 2}>update</button>
<button onclick={() => pending.get('a')?.()}>resolve a</button>
<button onclick={() => pending.get('b')?.()}>resolve b</button>
<p>{await delay('a', count)}:{parity}</p>
{#if await delay('b', count)}
{#if parity}<p>odd</p>{/if}
{/if}

@ -0,0 +1,36 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
await tick();
const [a, b, resolve, read] = target.querySelectorAll('button');
const [p] = target.querySelectorAll('p');
assert.htmlEqual(p.innerHTML, '0 0 0');
// A waits for its async expression, so its updated input is not yet rendered.
a.click();
await tick();
assert.htmlEqual(p.innerHTML, '0 0 0');
// B commits with A's old input. Its derived values must remain batch-local.
b.click();
await tick();
assert.htmlEqual(p.innerHTML, '0 1 2');
// Outside reactivity, both deriveds should reflect the real inputs, without
// changing the DOM until A resolves.
read.click();
await tick();
assert.htmlEqual(p.innerHTML, '0 1 2');
resolve.click();
await tick();
assert.htmlEqual(p.innerHTML, '1 2 4');
read.click();
assert.deepEqual(logs, [
{ a: 1, b: 1, c: 2, doubled: 4 },
{ a: 1, b: 1, c: 2, doubled: 4 }
]);
}
});

@ -0,0 +1,18 @@
<script>
let a = $state(0);
let b = $state(0);
const c = $derived(a + b);
const doubled = $derived(c * 2);
const queued = [];
function delay(value) {
if (!value) return value;
return new Promise((resolve) => queued.push(() => resolve(value)));
}
</script>
<p>{await delay(a)} {c} {doubled}</p>
<button onclick={() => a++}>a</button>
<button onclick={() => b++}>b</button>
<button onclick={() => queued.shift()?.()}>resolve</button>
<button onclick={() => console.log({ a, b, c, doubled })}>read</button>

@ -0,0 +1,38 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
await tick();
const [increment_a, increment_a_b, shift_second, shift] = target.querySelectorAll('button');
const [p] = target.querySelectorAll('p');
increment_a.click();
await tick();
assert.deepEqual(logs, ['a_d: 1']);
logs.length = 0;
increment_a_b.click();
await tick();
assert.deepEqual(logs, ['a_d: 2', 'c: 1']);
logs.length = 0;
shift_second.click(); // a_d of second batch
await tick();
assert.deepEqual(logs, ['c: 3']);
logs.length = 0;
shift.click(); // a_d of first batch - should not rerun second batch
await tick();
assert.deepEqual(logs, ['c: 1']);
logs.length = 0;
shift.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(p.innerHTML, '3');
assert.deepEqual(logs, []);
}
});

@ -0,0 +1,21 @@
<script>
let a = $state(0);
let b = $state(0);
let a_d = $derived(await delay(a, 'a_d'));
let c = $derived(await delay(a_d + b, 'c'));
function delay(value, source) {
if (!value) return value;
console.log(source + ': ' + value);
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
let deferred = [];
</script>
<button onclick={() => a++}>increment a</button>
<button onclick={() => {a++; b++}}>increment a & b</button>
<button onclick={() => deferred.splice(1,1)[0]?.()}>shift second</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<p>{c}</p>

@ -0,0 +1,16 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [increment] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>0</p>');
increment.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>2</p>');
}
});

@ -0,0 +1,14 @@
<script>
let count = $state(0);
let runs = 0;
function delay(value, eager_value) {
if (runs++ > 10) throw new Error('infinite loop detected')
if (value === 0) return 0;
return Promise.resolve(value + eager_value);
}
</script>
<button onclick={() => count += 1}>increment</button>
<p>{await delay(count, $state.eager(count))}</p>

@ -0,0 +1,36 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = '<button>count</button><button>eager</button><button>shift</button>';
export default test({
// running more than once per bump
async test({ assert, target, logs }) {
await tick();
const [count, eager, shift] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, `<p>-1</p>${buttons}`);
assert.deepEqual(logs, []);
// count++ creates a pending batch; the inner block (containing the
// $state.eager expression) is created inside it and belongs to it.
// it does not depend on `count`
count.click();
await tick();
assert.deepEqual(logs, ['inner 0']);
// an eager version bump re-runs the inner block in the eager batch's
// world — since the block doesn't depend on the pending batch's
// changes, it sees exactly the same values the owner's world would
eager.click();
await tick();
assert.deepEqual(logs, ['inner 0', 'inner 1']);
// the pending batch settles — nothing the inner block sees has
// changed since its eager run, so it should not be re-run
shift.click();
await tick();
assert.deepEqual(logs, ['inner 0', 'inner 1']);
assert.htmlEqual(target.innerHTML, `<p>0</p><span>1</span>${buttons}`);
}
});

@ -0,0 +1,26 @@
<script>
let count = $state(-1);
let eag = $state(0);
const deferred = [];
function delay(value) {
if (value < 0) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
function log(value) {
console.log(`inner ${value}`);
return value >= 0;
}
</script>
<p>{await delay(count)}</p>
{#if count >= 0}
{#if log($state.eager(eag))}
<span>{eag}</span>
{/if}
{/if}
<button onclick={() => count++}>count</button>
<button onclick={() => eag++}>eager</button>
<button onclick={() => deferred.shift()?.()}>shift</button>

@ -0,0 +1,10 @@
<script>
let { delay } = $props();
let result = $derived(await delay());
$effect(() => {
console.log(result);
});
</script>
<strong>{result}</strong>

@ -0,0 +1,47 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = `
<button>start A: x = 1</button>
<button>start B: show boundary</button>
<button>resolve A</button>
<button>resolve boundary</button>
<button>reset</button>
`;
export default test({
mode: ['client'],
async test({ assert, target, logs }) {
await tick();
const [start_a, start_b, resolve_a, resolve_b, reset] = target.querySelectorAll('button');
for (const boundary_first of [false, true]) {
logs.length = 0;
start_a.click();
await tick();
start_b.click();
await tick();
// B's pending boundary must transfer effects to A regardless of whether
// it resolves before or after A renders.
if (boundary_first) {
resolve_b.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>0</p>`);
assert.deepEqual(logs, []);
resolve_a.click();
} else {
resolve_a.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>1</p><span>pending</span>`);
resolve_b.click();
}
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>1</p><strong>ready</strong>`);
assert.deepEqual(logs, ['ready']);
reset.click();
await tick();
}
}
});

@ -0,0 +1,31 @@
<script>
import Child from './Child.svelte';
let x = $state(0);
let show = $state(false);
let resolve_a;
let resolve_b;
function delay_a(value) {
if (value === 0) return 0;
return new Promise(resolve => { resolve_a = () => resolve(value); });
}
function delay_b() {
return new Promise(resolve => { resolve_b = () => resolve('ready'); });
}
</script>
<button onclick={() => x = 1}>start A: x = 1</button>
<button onclick={() => show = true}>start B: show boundary</button>
<button onclick={() => resolve_a()}>resolve A</button>
<button onclick={() => resolve_b()}>resolve boundary</button>
<button onclick={() => { show = false; x = 0; }}>reset</button>
<p>{await delay_a(x)}</p>
{#if show && x > 0}
<svelte:boundary>
{#snippet pending()}<span>pending</span>{/snippet}
<Child delay={delay_b} />
</svelte:boundary>
{/if}

@ -2,7 +2,6 @@ import { tick } from 'svelte';
import { test } from '../../test';
export default test({
skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971
async test({ assert, target }) {
await tick();
const [a_b, a_c, b_d, shift, pop] = target.querySelectorAll('button');

@ -0,0 +1,40 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = `
<button>flip</button>
<button>a</button>
<button>b</button>
<button>resolve</button>
`;
// A reaction that reads its existing dependencies in a different order than last time
// is not a "new reader" of those dependencies — it must keep seeing its own batch's view,
// not the latest (uncommitted) value of a later pending batch
export default test({
async test({ assert, target }) {
const [flip, a, , resolve] = target.querySelectorAll('button');
resolve.click();
await tick();
resolve.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>a0b0</p><p>true</p><p>a0</p>`);
flip.click(); // B1: cond = false, slow(cond) pending
await tick();
a.click(); // B2: a = 'a1', slow(a) pending
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>a0b0</p><p>true</p><p>a0</p>`);
// B1 commits while B2 is still pending. The flipped branch reads `b` then `a` (reordered
// deps) — `a` must still resolve to B1's view (a0), not B2's uncommitted a1
resolve.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>b0a0</p><p>false</p><p>a0</p>`);
resolve.click();
await tick();
assert.htmlEqual(target.innerHTML, `${buttons}<p>b0a1</p><p>false</p><p>a1</p>`);
}
});

@ -0,0 +1,22 @@
<script>
let cond = $state(true);
let a = $state('a0');
let b = $state('b0');
const resolvers = [];
function slow(v) {
return new Promise((r) => resolvers.push(() => r(v)));
}
</script>
<button onclick={() => (cond = !cond)}>flip</button>
<button onclick={() => (a = "a1")}>a</button>
<button onclick={() => (b = "b1")}>b</button>
<button onclick={() => resolvers.shift()?.()}>resolve</button>
<svelte:boundary>
{#snippet pending()}loading{/snippet}
<p>{cond ? a + b : b + a}</p>
<p>{await slow(cond)}</p>
<p>{await slow(a)}</p>
</svelte:boundary>

@ -0,0 +1,30 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [a_b, b, resolve] = target.querySelectorAll('button');
a_b.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>a_b 0_0</button> <button>b 0</button> <button>resolve</button> 0'
);
b.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>a_b 0_0</button> <button>b 0</button> <button>resolve</button> 0'
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>a_b 1_2</button> <button>b 2</button> <button>resolve</button> 1'
);
}
});

@ -0,0 +1,18 @@
<script>
let a = $state(0);
let b = $state(0);
let deferreds = [];
const value = () => (`${a}_${b}`);
function push(value) {
if (!value) return value;
return new Promise((resolve) => {
deferreds.push(() => resolve(value));
});
}
</script>
<button onclick={() => {a++;b++}}>a_b {value()}</button>
<button onclick={() => (b++)}>b {b}</button>
<button onclick={() => (deferreds.shift()?.())}>resolve</button>
{await push(a)}

@ -0,0 +1,31 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = '<button>a</button><button>b</button><button>shift</button>';
export default test({
async test({ assert, target }) {
await tick();
const [a, b, shift] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, `<p>0</p><p>0</p>${buttons}`);
// batch A writes `a` and pends on its async expression
a.click();
await tick();
assert.htmlEqual(target.innerHTML, `<p>0</p><p>0</p>${buttons}`);
// batch B writes `b` — it only shares the `{add(a, b)}` template expression
// (a leaf) with batch A, so the two are independent: B commits right away,
// rendering its own view of the world (in which `a` is still 0)
b.click();
await tick();
assert.htmlEqual(target.innerHTML, `<p>0</p><p>1</p>${buttons}`);
// the async expression settles — A commits and brings the shared
// template expression up to date
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, `<p>1</p><p>2</p>${buttons}`);
}
});

@ -0,0 +1,23 @@
<script>
let a = $state(0);
let b = $state(0);
const c = $derived(add(a, b))
let pend = false;
const deferred = [];
function delay(value) {
if (!pend) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
function add(x, y) {
return x + y;
}
</script>
<p>{await delay(a)}</p>
<p>{c}</p>
<button onclick={() => { pend = true; a++; }}>a</button>
<button onclick={() => b++}>b</button>
<button onclick={() => deferred.shift()?.()}>shift</button>

@ -0,0 +1,38 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = '<button>z</button><button>w</button><button>shift</button>';
export default test({
async test({ assert, target, logs }) {
await tick();
const [z, w, shift] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, `<p>true</p><p>0</p>${buttons}`);
assert.deepEqual(logs, ['eval true']);
// with no other batch pending, the equality cut-off works: `c`
// recomputes to the same value, the template expression is not
// re-evaluated
z.click();
await tick();
assert.deepEqual(logs, ['eval true']);
// write w — the batch is pending on its async expression
w.click();
await tick();
assert.deepEqual(logs, ['eval true']);
// z++ recomputes `c` to the same value again — the template expression
// should still not be re-evaluated, even though another batch is pending
z.click();
await tick();
assert.deepEqual(logs, ['eval true']);
// the pending batch settles — nothing `c` depends on changed
shift.click();
await tick();
assert.deepEqual(logs, ['eval true']);
assert.htmlEqual(target.innerHTML, `<p>true</p><p>1</p>${buttons}`);
}
});

@ -0,0 +1,25 @@
<script>
let z = $state(1);
let w = $state(0);
let pend = false;
const deferred = [];
let c = $derived(z > 0);
function delay(value) {
if (!pend) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
function log(value) {
console.log(`eval ${value}`);
return value;
}
</script>
<p>{log(c)}</p>
<p>{await delay(w)}</p>
<button onclick={() => z++}>z</button>
<button onclick={() => { pend = true; w += 1; }}>w</button>
<button onclick={() => deferred.shift()?.()}>shift</button>

@ -0,0 +1,36 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = '<button>b</button><button>z</button><button>shift</button>';
export default test({
async test({ assert, target, logs }) {
await tick();
const [b, z, shift] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, `<p>0</p><p>a</p>${buttons}`);
assert.deepEqual(logs, ['fetch a']);
// write x = 'b' — the batch is pending on its async expression
b.click();
await tick();
assert.deepEqual(logs, ['fetch a', 'fetch b']);
// an independent batch runs the effect, which writes x = 'b' — the
// real value is already 'b', so this is a no-op: the async expression
// must not re-run (no needless refetch), and the batch must not be
// entangled with the pending one (z commits immediately)
z.click();
await tick();
assert.deepEqual(logs, ['fetch a', 'fetch b']);
assert.htmlEqual(target.innerHTML, `<p>1</p><p>a</p>${buttons}`);
// the pending batch settles
shift.click();
await tick();
shift.click();
await tick();
assert.deepEqual(logs, ['fetch a', 'fetch b']);
assert.htmlEqual(target.innerHTML, `<p>1</p><p>b</p>${buttons}`);
}
});

@ -0,0 +1,25 @@
<script>
let x = $state('a');
let z = $state(0);
let pend = false;
const deferred = [];
function delay(value) {
console.log(`fetch ${value}`);
if (!pend) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
$effect(() => {
if (z > 0) {
x = 'b';
}
});
</script>
<p>{z}</p>
<p>{await delay(x)}</p>
<button onclick={() => { pend = true; x = 'b'; }}>b</button>
<button onclick={() => z++}>z</button>
<button onclick={() => deferred.shift()?.()}>shift</button>

@ -0,0 +1,33 @@
import { tick } from 'svelte';
import { test } from '../../test';
const buttons = '<button>b</button><button>reset</button><button>shift</button>';
export default test({
async test({ assert, target, logs }) {
await tick();
const [b, reset, shift] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, `<p>0</p><p>a</p>${buttons}`);
assert.deepEqual(logs, ['fetch a']);
// write x = 'b' — the batch is pending on its async expression
b.click();
await tick();
assert.deepEqual(logs, ['fetch a', 'fetch b']);
// an independent batch runs the effect, which resets x = 'a'. The real
// (pending) value is 'b', so this is a genuine change and must not be
// swallowed just because the effect's world still shows 'a'
reset.click();
await tick();
assert.deepEqual(logs, ['fetch a', 'fetch b', 'fetch a']);
// resolve all in-flight runs — the reset must win
shift.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, `<p>1</p><p>a</p>${buttons}`);
}
});

@ -0,0 +1,25 @@
<script>
let x = $state('a');
let z = $state(0);
let pend = false;
const deferred = [];
function delay(value) {
console.log(`fetch ${value}`);
if (!pend) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
$effect(() => {
if (z > 0) {
x = 'a';
}
});
</script>
<p>{z}</p>
<p>{await delay(x)}</p>
<button onclick={() => { pend = true; x = 'b'; }}>b</button>
<button onclick={() => z++}>reset</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
Loading…
Cancel
Save