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-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-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} +