diff --git a/packages/svelte/tests/runtime-runes/samples/async-branch-commit-order/_config.js b/packages/svelte/tests/runtime-runes/samples/async-branch-commit-order/_config.js new file mode 100644 index 0000000000..11c3bce995 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-branch-commit-order/_config.js @@ -0,0 +1,27 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = ''; + +export default test({ + async test({ assert, target }) { + await tick(); + const [a, b, shift] = target.querySelectorAll('button'); + + // batch A waits on its key; batch B registers its key right away + a.click(); + await tick(); + b.click(); + await tick(); + + // the older batch commits first + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `

0

1

${buttons}`); + + // the newer batch commits with its own branch + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `

1

B

${buttons}`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-branch-commit-order/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-branch-commit-order/main.svelte new file mode 100644 index 0000000000..d0b1a57053 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-branch-commit-order/main.svelte @@ -0,0 +1,24 @@ + + +

{await delay(b)}

+ +{#key k} +

{k}

+{/key} + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-assign-keeps-maybe-dirty/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-assign-keeps-maybe-dirty/_config.js new file mode 100644 index 0000000000..ae9ef996f6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-assign-keeps-maybe-dirty/_config.js @@ -0,0 +1,39 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = + ''; + +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 `` 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}

1

0

1010`); + + resolve_c.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

1

1

9999`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-assign-keeps-maybe-dirty/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-assign-keeps-maybe-dirty/main.svelte new file mode 100644 index 0000000000..08c00a4911 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-assign-keeps-maybe-dirty/main.svelte @@ -0,0 +1,42 @@ + + + + + + + + + +

{await delay('a', s)}

+

{await delay('c', w)}

+ +{#if show} + {d} +{/if} + +{#if await delay('b', u)} + {await probe(d)} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-equal-real-value-hidden/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-equal-real-value-hidden/_config.js new file mode 100644 index 0000000000..d319831f2e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-equal-real-value-hidden/_config.js @@ -0,0 +1,33 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = + ''; + +export default test({ + mode: ['client'], + async test({ assert, target }) { + await tick(); + const [x, s, resolve_a, resolve_b] = target.querySelectorAll('button'); + assert.htmlEqual(target.innerHTML, `${buttons}

0:5

5

`); + + // batch A: `t` becomes truthy (pending) + x.click(); + await tick(); + + // batch B: `d` becomes 0 (pending) + s.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

0:5

5

`); + + // In A's world, `d` is recomputed to 0. That's the real value, but not the value + // A sees for it (the one from before B's change), so A has to record it + resolve_a.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

1:0

5

`); + + resolve_b.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

1:0

0

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-equal-real-value-hidden/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-equal-real-value-hidden/main.svelte new file mode 100644 index 0000000000..9fe34a5e12 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-equal-real-value-hidden/main.svelte @@ -0,0 +1,25 @@ + + + + + + + +{#if d >= 0} +

{t}:{d}

+{/if} + +

{await delay('b', s, 5)}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-previous-effect-batch/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-previous-effect-batch/_config.js new file mode 100644 index 0000000000..8f021032e0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-previous-effect-batch/_config.js @@ -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]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-previous-effect-batch/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-previous-effect-batch/main.svelte new file mode 100644 index 0000000000..5cd0876884 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-previous-effect-batch/main.svelte @@ -0,0 +1,38 @@ + + + + + + +

{y ? await push(y) : y}

+ +{#if x} + +

{await push(x)}

+ {#snippet pending()} +

loading

+ {/snippet} +
+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-eager-batch-no-merge/_config.js b/packages/svelte/tests/runtime-runes/samples/async-eager-batch-no-merge/_config.js new file mode 100644 index 0000000000..f90497846c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-eager-batch-no-merge/_config.js @@ -0,0 +1,44 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = + ''; + +// 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}

page: 1

result: 1/a

`); + + // filter = 'b' -> eager batch re-runs load(1, 'b'), pending + filter.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

page: 1

result: 1/a

`); + + // resolve load(1, 'b') -> the eager batch commits on its own + pop.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

page: 1

result: 1/b

`); + + // 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}

page: 2

result: 2/b

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-eager-batch-no-merge/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-eager-batch-no-merge/main.svelte new file mode 100644 index 0000000000..008fabe2e5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-eager-batch-no-merge/main.svelte @@ -0,0 +1,20 @@ + + + + + + + +

page: {page}

+

result: {await load(page, $state.eager(filter))}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-resume-no-rerun/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-resume-no-rerun/Child.svelte new file mode 100644 index 0000000000..6f48eaf129 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-resume-no-rerun/Child.svelte @@ -0,0 +1,9 @@ + + +

child

diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-resume-no-rerun/_config.js b/packages/svelte/tests/runtime-runes/samples/async-effect-resume-no-rerun/_config.js new file mode 100644 index 0000000000..4fd60a98ce --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-resume-no-rerun/_config.js @@ -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, + '

1

child

' + ); + + // 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']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-resume-no-rerun/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-resume-no-rerun/main.svelte new file mode 100644 index 0000000000..30d68f614b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-resume-no-rerun/main.svelte @@ -0,0 +1,29 @@ + + + + + + + +

{await push(x)}

+ +{#if show} +
+ +
+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-write-keeps-batch-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-effect-write-keeps-batch-derived/_config.js new file mode 100644 index 0000000000..fb0b6e8a4f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-write-keeps-batch-derived/_config.js @@ -0,0 +1,33 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = ''; + +// While batch A (x) commits, a pre effect writes to `s`. That marks `label` (via `big`) as +// maybe dirty, but it doesn't actually change. `label` must still show the value from A's +// point of view, not the one from the later, still-pending batch B (t). +export default test({ + async test({ assert, target }) { + await tick(); + const [x, t, shift] = target.querySelectorAll('button'); + + // A + x.click(); + await tick(); + + // B + t.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

0

0

0 false-0

`); + + // A commits, B is still pending + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

1

0

1 false-0

`); + + // B commits + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

1

1

1 false-1

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-write-keeps-batch-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-write-keeps-batch-derived/main.svelte new file mode 100644 index 0000000000..1791827eae --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-write-keeps-batch-derived/main.svelte @@ -0,0 +1,32 @@ + + + + + + +

{await push(x)}

+

{await push(t)}

+ +{#key label}{/key} + +

{x} {label}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-commit-new-reader/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-commit-new-reader/_config.js new file mode 100644 index 0000000000..a9e860bec7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-commit-new-reader/_config.js @@ -0,0 +1,35 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = + ''; + +// The template effect is deferred by the fork (it depends on `s` via `big`), but before the +// fork is committed, it re-runs in the real world and starts reading `s` directly. +// Committing the fork (while another batch is pending) must still update it. +export default test({ + async test({ assert, target }) { + await tick(); + const [u, f, cond, commit, shift] = target.querySelectorAll('button'); + + // unrelated pending batch + u.click(); + await tick(); + + f.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

0

false

`); + + cond.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

0

0

`); + + commit.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

0

1

`); + + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

1

1

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-commit-new-reader/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-commit-new-reader/main.svelte new file mode 100644 index 0000000000..a52d6b4dfd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-commit-new-reader/main.svelte @@ -0,0 +1,25 @@ + + + + + + + + +

{await push(u)}

+

{cond ? s : big}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-merge-blocking-pending/_config.js b/packages/svelte/tests/runtime-runes/samples/async-merge-blocking-pending/_config.js new file mode 100644 index 0000000000..9cd8eba0b3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-merge-blocking-pending/_config.js @@ -0,0 +1,34 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = ''; + +export default test({ + async test({ assert, target }) { + await tick(); + const [x, toggle, pop] = target.querySelectorAll('button'); + + // X is deferred on its own await + x.click(); + await tick(); + // Y is deferred on the await inside the new branch + toggle.click(); + await tick(); + // Z hides that branch again, merges into Y, which merges into X + toggle.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `

n = 0

x = 0

${buttons}`); + + // the branch's await resolves while it is hidden + pop.click(); + await tick(); + // W shows the branch again and merges into X + toggle.click(); + await tick(); + + // nothing is pending any more, so X commits + pop.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `

n = 4

x = 1

cond: e

${buttons}`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-merge-blocking-pending/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-merge-blocking-pending/main.svelte new file mode 100644 index 0000000000..d4cbd8c5b6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-merge-blocking-pending/main.svelte @@ -0,0 +1,29 @@ + + + +

n = {n}

+

x = {await delay(x)}

+ {#if cond} +

cond: {await delay('e')}

+ {/if} + + {#snippet pending()} +

loading

+ {/snippet} +
+ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-merge-discard-after-error/Item.svelte b/packages/svelte/tests/runtime-runes/samples/async-merge-discard-after-error/Item.svelte new file mode 100644 index 0000000000..7e30dd0125 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-merge-discard-after-error/Item.svelte @@ -0,0 +1,9 @@ + + +{item} diff --git a/packages/svelte/tests/runtime-runes/samples/async-merge-discard-after-error/_config.js b/packages/svelte/tests/runtime-runes/samples/async-merge-discard-after-error/_config.js new file mode 100644 index 0000000000..9bd1abce5c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-merge-discard-after-error/_config.js @@ -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, + `

a: 0

` + ); + // the removed items must be destroyed, not kept around for the discarded batch Y + assert.deepEqual(logs, ['destroy 1', 'destroy 2', 'destroy 3']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-merge-discard-after-error/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-merge-discard-after-error/main.svelte new file mode 100644 index 0000000000..ea1bfd8594 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-merge-discard-after-error/main.svelte @@ -0,0 +1,39 @@ + + + + + + + + + + {#if check(await push(a))} +

a: {a}

+ {/if} + + + {#each list.slice(0, a + 10) as item (item)} + + {/each} + + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-read-many-sources/_config.js b/packages/svelte/tests/runtime-runes/samples/async-stale-read-many-sources/_config.js new file mode 100644 index 0000000000..f5318b930a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-read-many-sources/_config.js @@ -0,0 +1,39 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = ''; +/** @param {number} offset */ +const list = (offset) => Array.from({ length: 2000 }, (_, i) => `${i + offset},`).join(''); + +// When the branch is created in B1, the each block reads all 2000 items, each of which B2 holds +// a newer value of. The each block must be scheduled to re-run in B2 once, not once per stale +// value — otherwise B2 is flushed 2000 times (each flush being O(size of B2)), which takes seconds +export default test({ + mode: ['client'], + async test({ assert, target }) { + await tick(); + const [show, items, shift] = target.querySelectorAll('button'); + + // B1: show = true -> pending + show.click(); + await tick(); + + // B2: increment all items -> pending + items.click(); + await tick(); + + // B1 resolves -> the branch is created, reading 2000 values that are stale relative to B2 + const start = performance.now(); + shift.click(); + await tick(); + const duration = performance.now() - start; + + assert.ok(duration < 1000, `took ${duration}ms`); + assert.htmlEqual(target.innerHTML, `${buttons}

0

${list(0)}
`); + + // B2 resolves + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}

1

${list(1)}
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-read-many-sources/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-stale-read-many-sources/main.svelte new file mode 100644 index 0000000000..f932241cc6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-read-many-sources/main.svelte @@ -0,0 +1,23 @@ + + + + + + +

{await delay(items[0])}

+ +{#if await delay(show)} +
+ {#each items as item}{item},{/each} +
+{/if}