diff --git a/.changeset/eleven-weeks-dance.md b/.changeset/eleven-weeks-dance.md new file mode 100644 index 000000000000..91245df0eb6c --- /dev/null +++ b/.changeset/eleven-weeks-dance.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: support `await` in components when using the `experimental.async` compiler option diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0e1d3676041..046ad335f3ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,23 @@ jobs: - run: pnpm test env: CI: true + TestNoAsync: + permissions: {} + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm playwright install chromium + - run: pnpm test runtime-runes + env: + CI: true + SVELTE_NO_ASYNC: true Lint: permissions: {} runs-on: ubuntu-latest diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 3f33e37d2e3d..c7d6ec8ac9cd 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -1,5 +1,17 @@ +### async_derived_orphan + +``` +Cannot create a `$derived(...)` with an `await` expression outside of an effect tree +``` + +In Svelte there are two types of reaction — [`$derived`](/docs/svelte/$derived) and [`$effect`](/docs/svelte/$effect). Deriveds can be created anywhere, because they run _lazily_ and can be [garbage collected](https://developer.mozilla.org/en-US/docs/Glossary/Garbage_collection) if nothing references them. Effects, by contrast, keep running eagerly whenever their dependencies change, until they are destroyed. + +Because of this, effects can only be created inside other effects (or [effect roots](/docs/svelte/$effect#$effect.root), such as the one that is created when you first mount a component) so that Svelte knows when to destroy them. + +Some sleight of hand occurs when a derived contains an `await` expression: Since waiting until we read `{await getPromise()}` to call `getPromise` would be too late, we use an effect to instead call it proactively, notifying Svelte when the value is available. But since we're using an effect, we can only create asynchronous deriveds inside another effect. + ### bind_invalid_checkbox_value ``` @@ -68,12 +80,28 @@ Effect cannot be created inside a `$derived` value that was not itself created i `%rune%` can only be used inside an effect (e.g. during component initialisation) ``` +### effect_pending_outside_reaction + +``` +`$effect.pending()` can only be called inside an effect or derived +``` + ### effect_update_depth_exceeded ``` Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops ``` +### flush_sync_in_effect + +``` +Cannot use `flushSync` inside an effect +``` + +The `flushSync()` function can be used to flush any pending effects synchronously. It cannot be used if effects are currently being flushed — in other words, you can call it after a state change but _not_ inside an effect. + +This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6. + ### get_abort_signal_outside_reaction ``` @@ -116,6 +144,14 @@ Rest element properties of `$props()` such as `%property%` are readonly The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files ``` +### set_context_after_init + +``` +`setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression +``` + +This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6. + ### state_descriptors_fixed ``` diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md index fe90b0db3815..1c75faef5377 100644 --- a/documentation/docs/98-reference/.generated/client-warnings.md +++ b/documentation/docs/98-reference/.generated/client-warnings.md @@ -34,6 +34,67 @@ function add() { } ``` +### await_reactivity_loss + +``` +Detected reactivity loss when reading `%name%`. This happens when state is read in an async function after an earlier `await` +``` + +Svelte's signal-based reactivity works by tracking which bits of state are read when a template or `$derived(...)` expression executes. If an expression contains an `await`, Svelte transforms it such that any state _after_ the `await` is also tracked — in other words, in a case like this... + +```js +let total = $derived(await a + b); +``` + +...both `a` and `b` are tracked, even though `b` is only read once `a` has resolved, after the initial execution. + +This does _not_ apply to an `await` that is not 'visible' inside the expression. In a case like this... + +```js +async function sum() { + return await a + b; +} + +let total = $derived(await sum()); +``` + +...`total` will depend on `a` (which is read immediately) but not `b` (which is not). The solution is to pass the values into the function: + +```js +async function sum(a, b) { + return await a + b; +} + +let total = $derived(await sum(a, b)); +``` + +### await_waterfall + +``` +An async derived, `%name%` (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app +``` + +In a case like this... + +```js +let a = $derived(await one()); +let b = $derived(await two()); +``` + +...the second `$derived` will not be created until the first one has resolved. Since `await two()` does not depend on the value of `a`, this delay, often described as a 'waterfall', is unnecessary. + +(Note that if the values of `await one()` and `await two()` subsequently change, they can do so concurrently — the waterfall only occurs when the deriveds are first created.) + +You can solve this by creating the promises first and _then_ awaiting them: + +```js +let aPromise = $derived(one()); +let bPromise = $derived(two()); + +let a = $derived(await aPromise); +let b = $derived(await bPromise); +``` + ### binding_property_non_reactive ``` diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index db848a0299ee..20f57770d122 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -480,6 +480,12 @@ Expected token %token% Expected whitespace ``` +### experimental_async + +``` +Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true` +``` + ### export_undefined ``` @@ -534,6 +540,12 @@ The arguments keyword cannot be used within the template or at the top level of %message% ``` +### legacy_await_invalid + +``` +Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode +``` + ### legacy_export_invalid ``` diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md index 6c31aaafd0df..de34b3f5da7c 100644 --- a/documentation/docs/98-reference/.generated/shared-errors.md +++ b/documentation/docs/98-reference/.generated/shared-errors.md @@ -1,5 +1,25 @@ +### await_outside_boundary + +``` +Cannot await outside a `` with a `pending` snippet +``` + +The `await` keyword can only appear in a `$derived(...)` or template expression, or at the top level of a component's ` + + + + + +

{await load(deferred)}

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/_config.js b/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/_config.js new file mode 100644 index 000000000000..3de81a507b59 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/_config.js @@ -0,0 +1,18 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` +

pending

+ `, + + async test({ assert, target }) { + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + flushSync(); + + assert.htmlEqual(target.innerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/main.svelte new file mode 100644 index 000000000000..00a11cac438a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/main.svelte @@ -0,0 +1,7 @@ + +

hello

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js b/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js new file mode 100644 index 000000000000..0a647384095c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js @@ -0,0 +1,29 @@ +import { tick } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target }) { + const [cool, neat, reset] = target.querySelectorAll('button'); + + cool.click(); + await tick(); + + const p = target.querySelector('p'); + ok(p); + assert.htmlEqual(p.outerHTML, '

hello

'); + + reset.click(); + assert.htmlEqual(p.outerHTML, '

hello

'); + + neat.click(); + await tick(); + assert.htmlEqual(p.outerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-attribute/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-attribute/main.svelte new file mode 100644 index 000000000000..6332a9802d5c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute/main.svelte @@ -0,0 +1,15 @@ + + + + + + + +

hello

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/_config.js new file mode 100644 index 000000000000..a29c99860dab --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/_config.js @@ -0,0 +1,27 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [increment, shift] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + + shift.click(); + await tick(); + + shift.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

false

+

1

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/main.svelte new file mode 100644 index 000000000000..a93eb7dc254c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/main.svelte @@ -0,0 +1,28 @@ + + + + + + + {#if count % 2 === 0} +

true

+

{await push()}

+ {:else} +

false

+

{await push()}

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

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/_config.js new file mode 100644 index 000000000000..e2718a35d27c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/_config.js @@ -0,0 +1,10 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + assert.htmlEqual(target.innerHTML, 'loading'); + await tick(); + assert.htmlEqual(target.innerHTML, 'nope'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/main.svelte new file mode 100644 index 000000000000..412da7268ed1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/main.svelte @@ -0,0 +1,8 @@ + + {#if await Promise.reject(new Error('nope'))} + hi + {/if} + + {#snippet pending()}loading{/snippet} + {#snippet failed(e)}{e.message}{/snippet} + diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js new file mode 100644 index 000000000000..cd89439a7220 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js @@ -0,0 +1,28 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, errors }) { + const [increment, resolve, reject] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + + reject.click(); + await tick(); + + resolve.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

false

+

1

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/main.svelte new file mode 100644 index 000000000000..1ad6cb84decc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/main.svelte @@ -0,0 +1,29 @@ + + + + + + + + {#if count % 2 === 0} +

true

+ {#each await push() as count}

{count}

{/each} + {:else} +

false

+ {#each await push() as count}

{count}

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

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-rerun/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-rerun/_config.js new file mode 100644 index 000000000000..18175de4dcb6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-rerun/_config.js @@ -0,0 +1,53 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [override, release, resolve] = target.querySelectorAll('button'); + + resolve.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

before

+

before

+ ` + ); + + override.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

during

+

during

+ ` + ); + + release.click(); + await tick(); + + resolve.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

after

+

after

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-rerun/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-rerun/main.svelte new file mode 100644 index 000000000000..256ad68f4af7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-rerun/main.svelte @@ -0,0 +1,45 @@ + + + + + + + + + + {#each await indirect() as entry} +

{entry}

+ {/each} + + {#each current as entry} +

{entry}

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

pending...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-child-effect/_config.js b/packages/svelte/tests/runtime-runes/samples/async-child-effect/_config.js new file mode 100644 index 000000000000..325cb1dcd644 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-child-effect/_config.js @@ -0,0 +1,55 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + +

loading

+ `, + + async test({ assert, target }) { + target.querySelector('button')?.click(); + await tick(); + + const [button1, button2] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + +

A

+

a

+ ` + ); + + flushSync(() => button2.click()); + flushSync(() => button2.click()); + + button1.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

AA

+

aa

+ ` + ); + + button1.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

AAA

+

aaa

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-child-effect/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-child-effect/main.svelte new file mode 100644 index 000000000000..edb0eaea44fd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-child-effect/main.svelte @@ -0,0 +1,26 @@ + + + + + + +

{await push(input.toUpperCase())}

+ + {#if true} +

{input}

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

loading

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-class-directive/_config.js b/packages/svelte/tests/runtime-runes/samples/async-class-directive/_config.js new file mode 100644 index 000000000000..3186ed2069da --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-class-directive/_config.js @@ -0,0 +1,20 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `loading`, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +
one
+
two
+
red
+
blue
+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-class-directive/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-class-directive/main.svelte new file mode 100644 index 000000000000..f0f27e483005 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-class-directive/main.svelte @@ -0,0 +1,11 @@ + +
one
+
two
+ +
red
+
blue
+ + {#snippet pending()} + loading + {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/Child.svelte new file mode 100644 index 000000000000..fb47377513a7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/Child.svelte @@ -0,0 +1,5 @@ + + +

{n}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js new file mode 100644 index 000000000000..914b311c97ce --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js @@ -0,0 +1,19 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const button = target.querySelector('button'); + + button?.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

1

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/main.svelte new file mode 100644 index 000000000000..a53381c2d5f3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/main.svelte @@ -0,0 +1,17 @@ + + + + + + {#if show} + + {/if} + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/Child.svelte new file mode 100644 index 000000000000..ffcd8b46b408 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/Child.svelte @@ -0,0 +1,9 @@ + + +

{(await d).value}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/_config.js new file mode 100644 index 000000000000..fee8e2e6bfee --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/_config.js @@ -0,0 +1,33 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target, errors }) { + const [toggle, resolve1, resolve2] = target.querySelectorAll('button'); + + toggle.click(); + resolve1.click(); + resolve2.click(); + + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

two

+ ` + ); + + assert.deepEqual(errors, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/main.svelte new file mode 100644 index 000000000000..9babdb2fe274 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/main.svelte @@ -0,0 +1,20 @@ + + + + + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-module/Child.svelte new file mode 100644 index 000000000000..f803a30c37f9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/Child.svelte @@ -0,0 +1,20 @@ + + +

{derived.value}{console.log(`template ${derived.value} ${num}`)}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js new file mode 100644 index 000000000000..b16ef652aee2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js @@ -0,0 +1,63 @@ +import { flushSync, settled, tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise, + num: 1 + }; + }, + + async test({ assert, target, component, logs }) { + d.resolve(42); + + // TODO why is this necessary? why isn't `await tick()` enough? + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + flushSync(); + await tick(); + assert.htmlEqual(target.innerHTML, '

42

'); + + component.num = 2; + await tick(); + assert.htmlEqual(target.innerHTML, '

84

'); + + d = deferred(); + component.promise = d.promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

84

'); + + d.resolve(43); + await tick(); + assert.htmlEqual(target.innerHTML, '

86

'); + + assert.deepEqual(logs, [ + 'outside boundary 1', + '$effect.pre 42 1', + 'template 42 1', + '$effect 42 1', + '$effect.pre 84 2', + 'template 84 2', + 'outside boundary 2', + '$effect 84 2', + '$effect.pre 86 2', + 'template 86 2', + '$effect 86 2' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-module/main.svelte new file mode 100644 index 000000000000..e90bbf720ed3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/main.svelte @@ -0,0 +1,15 @@ + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
+ +{console.log(`outside boundary ${num}`)} diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/async-derived-module/state.svelte.js new file mode 100644 index 000000000000..a53fbb8c6fc5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/state.svelte.js @@ -0,0 +1,9 @@ +export async function create_derived(get_promise, get_num) { + let value = $derived((await get_promise()) * get_num()); + + return { + get value() { + return value; + } + }; +} diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/Component.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/Component.svelte new file mode 100644 index 000000000000..a90a9dedf724 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/Component.svelte @@ -0,0 +1,29 @@ + + + + + +

{n}: {Math.min(current, 3)}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js new file mode 100644 index 000000000000..016c311f989a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js @@ -0,0 +1,33 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending...

`, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

0: 0

+ ` + ); + + const [shift, increment] = target.querySelectorAll('button'); + const [p] = target.querySelectorAll('p'); + + for (let i = 1; i < 5; i += 1) { + flushSync(() => increment.click()); + } + + for (let i = 1; i < 5; i += 1) { + shift.click(); + await tick(); + + assert.equal(p.innerHTML, `${i}: ${Math.min(i, 3)}`); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/main.svelte new file mode 100644 index 000000000000..2d5ddca4dbfc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/main.svelte @@ -0,0 +1,11 @@ + + + + + + {#snippet pending()} +

pending...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte new file mode 100644 index 000000000000..b59fd7c08fc3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte @@ -0,0 +1,15 @@ + + +

{value}{console.log(`template ${value} ${num}`)}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js new file mode 100644 index 000000000000..72396434642e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js @@ -0,0 +1,48 @@ +import { flushSync, settled, tick } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + html: ` + + + + +

pending

+ `, + + async test({ assert, target, logs }) { + const [resolve_a, resolve_b, reset, increment] = target.querySelectorAll('button'); + + flushSync(() => resolve_a.click()); + await tick(); + + const p = target.querySelector('p'); + ok(p); + assert.htmlEqual(p.innerHTML, '1a'); + + flushSync(() => increment.click()); + await tick(); + assert.htmlEqual(p.innerHTML, '2a'); + + reset.click(); + assert.htmlEqual(p.innerHTML, '2a'); + + resolve_b.click(); + await tick(); + assert.htmlEqual(p.innerHTML, '2b'); + + assert.deepEqual(logs, [ + 'outside boundary 1', + '$effect.pre 1a 1', + 'template 1a 1', + '$effect 1a 1', + '$effect.pre 2a 2', + 'template 2a 2', + 'outside boundary 2', + '$effect 2a 2', + '$effect.pre 2b 2', + 'template 2b 2', + '$effect 2b 2' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived/main.svelte new file mode 100644 index 000000000000..1404ae0299d0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/main.svelte @@ -0,0 +1,21 @@ + + + + + + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
+ +{console.log(`outside boundary ${num}`)} diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js new file mode 100644 index 000000000000..54aa68eeb294 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js @@ -0,0 +1,31 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending

`, + + async test({ assert, target }) { + const [button1, button2, button3] = target.querySelectorAll('button'); + + button1.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

a

b

c

' + ); + + button2.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

a

b

c

' + ); + + button3.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

b

c

d

e

' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-await-item/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/main.svelte new file mode 100644 index 000000000000..eddcf2b749d7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/main.svelte @@ -0,0 +1,39 @@ + + + + + + + + + + {#each items as deferred} +

{await deferred.promise}

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

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js new file mode 100644 index 000000000000..43d3a0f8760d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js @@ -0,0 +1,41 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + html: ` + + + + +

pending

+ `, + + async test({ assert, target }) { + const [reset, one, two, three] = target.querySelectorAll('button'); + + one.click(); + await tick(); + + const [div] = target.querySelectorAll('div'); + assert.htmlEqual(div.innerHTML, '

a

b

c

'); + + reset.click(); + await tick(); + assert.htmlEqual(div.innerHTML, '

a

b

c

'); + + two.click(); + await tick(); + assert.htmlEqual(div.innerHTML, '

d

e

f

g

'); + + reset.click(); + await tick(); + three.click(); + await tick(); + + assert.include(target.innerHTML, '

each_key_duplicate'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-keyed/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/main.svelte new file mode 100644 index 000000000000..e2f826378017 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/main.svelte @@ -0,0 +1,24 @@ + + + + + + + + +

+ {#each await deferred.promise as item (item)} +

{item}

+ {/each} +
+ + {#snippet failed(e)} +

{e.message}

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

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-each/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each/_config.js new file mode 100644 index 000000000000..9dde2beb3926 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each/_config.js @@ -0,0 +1,33 @@ +import { tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target, component }) { + d.resolve(['a', 'b', 'c']); + await tick(); + assert.htmlEqual(target.innerHTML, '

a

b

c

'); + + d = deferred(); + component.promise = d.promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

a

b

c

'); + + d.resolve(['d', 'e', 'f', 'g']); + await tick(); + assert.htmlEqual(target.innerHTML, '

d

e

f

g

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each/main.svelte new file mode 100644 index 000000000000..9b59d57b055a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each/main.svelte @@ -0,0 +1,13 @@ + + + + {#each await promise as item} +

{item}

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

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/_config.js new file mode 100644 index 000000000000..2679785cff3b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `loading`, + + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, 'oops'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/main.svelte new file mode 100644 index 000000000000..a49a5c9540d2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/main.svelte @@ -0,0 +1,8 @@ + + {#each (await Promise.reject(new Error('oops'))) as x} + hi + {/each} + + {#snippet pending()}loading{/snippet} + {#snippet failed()}oops{/snippet} + diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js new file mode 100644 index 000000000000..1613bf9c6124 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js @@ -0,0 +1,87 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + +

pending...

+ `, + + compileOptions: { + // this tests some behaviour that was broken in dev + dev: true + }, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

0

+ ` + ); + + let [button] = target.querySelectorAll('button'); + let [p] = target.querySelectorAll('p'); + + button.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + +

1

+ ` + ); + + button.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + +

2

+ ` + ); + + button.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + + const [button1, button2] = target.querySelectorAll('button'); + + button1.click(); + await tick(); + + button2.click(); + await tick(); + + [p] = target.querySelectorAll('p'); + + assert.htmlEqual( + target.innerHTML, + ` + +

4

+ ` + ); + + button1.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + +

5

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-recovery/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/main.svelte new file mode 100644 index 000000000000..d5246d330e25 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/main.svelte @@ -0,0 +1,24 @@ + + + + + +

{await process(count)}

+ + {#snippet pending()} +

pending...

+ {/snippet} + + {#snippet failed(error, reset)} + + {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-error/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js new file mode 100644 index 000000000000..dfbd238eeb67 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js @@ -0,0 +1,34 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending

`, + + async test({ assert, target }) { + let [button1, button2, button3] = target.querySelectorAll('button'); + + button1.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

oops!

' + ); + + button2.click(); + + const reset = /** @type {HTMLButtonElement} */ (target.querySelector('[data-id="reset"]')); + reset.click(); + + assert.htmlEqual( + target.innerHTML, + '

pending

' + ); + + button3.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

wheee

' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte new file mode 100644 index 000000000000..9af5bbaa16a5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte @@ -0,0 +1,20 @@ + + + + + + + +

{await deferred.promise}

+ + {#snippet pending()} +

pending

+ {/snippet} + + {#snippet failed(error, reset)} +

{error.message}

+ + {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js new file mode 100644 index 000000000000..d626569ba250 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js @@ -0,0 +1,56 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target, raf }) { + const [reset, hello, goodbye] = target.querySelectorAll('button'); + + hello.click(); + raf.tick(0); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello

+ ` + ); + + reset.click(); + raf.tick(0); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello

+

updating...

+ ` + ); + + goodbye.click(); + await Promise.resolve(); + raf.tick(0); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

goodbye

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte new file mode 100644 index 000000000000..42536ab02a82 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte @@ -0,0 +1,19 @@ + + + + + + + +

{await deferred.promise}

+ + {#if $effect.pending()} +

updating...

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

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-html-tag/_config.js b/packages/svelte/tests/runtime-runes/samples/async-html-tag/_config.js new file mode 100644 index 000000000000..22b8b2a1c462 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-html-tag/_config.js @@ -0,0 +1,32 @@ +import { tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target, component }) { + d.resolve('hello'); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + component.promise = (d = deferred()).promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + d.resolve('wheee'); + await tick(); + assert.htmlEqual(target.innerHTML, '

wheee

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-html-tag/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-html-tag/main.svelte new file mode 100644 index 000000000000..f5aa363731c2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-html-tag/main.svelte @@ -0,0 +1,11 @@ + + + +

{@html await promise}

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-if/_config.js new file mode 100644 index 000000000000..a4bee8c9956f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if/_config.js @@ -0,0 +1,43 @@ +import { tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target }) { + const [reset, t, f] = target.querySelectorAll('button'); + + t.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

yes

' + ); + + reset.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

yes

' + ); + + f.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

no

' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-if/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-if/main.svelte new file mode 100644 index 000000000000..21a4cbef97f2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if/main.svelte @@ -0,0 +1,19 @@ + + + + + + + + {#if await deferred.promise} +

yes

+ {:else} +

no

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

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-key/_config.js b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js new file mode 100644 index 000000000000..bda922705464 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js @@ -0,0 +1,46 @@ +import { tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target, component }) { + d.resolve(1); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + const h1 = target.querySelector('h1'); + + d = deferred(); + component.promise = d.promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + d.resolve(1); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + assert.equal(target.querySelector('h1'), h1); + + d = deferred(); + component.promise = d.promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + d.resolve(2); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + assert.notEqual(target.querySelector('h1'), h1); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-key/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-key/main.svelte new file mode 100644 index 000000000000..7cac0f854240 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-key/main.svelte @@ -0,0 +1,13 @@ + + + + {#key await promise} +

hello

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

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/_config.js b/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/_config.js new file mode 100644 index 000000000000..cb8e0cfca90c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/_config.js @@ -0,0 +1,35 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

loading...

`, + + async test({ assert, target }) { + const [both, a, b] = target.querySelectorAll('button'); + + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

1 * 2 = 2

+

2 * 2 = 4

+ ` + ); + + both.click(); + b.click(); + + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

2 * 2 = 4

+

4 * 2 = 8

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/main.svelte new file mode 100644 index 000000000000..432eed976c47 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/main.svelte @@ -0,0 +1,17 @@ + + + + + + + +

{a} * 2 = {await (a * 2)}

+

{b} * 2 = {b * 2}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js new file mode 100644 index 000000000000..5e522ebdb536 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js @@ -0,0 +1,30 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [a, b, reset1, reset2, resolve1, resolve2] = target.querySelectorAll('button'); + + resolve1.click(); + await tick(); + + const p = /** @type {HTMLElement} */ (target.querySelector('#test')); + + assert.htmlEqual(p.innerHTML, '1 + 2 = 3'); + + flushSync(() => reset1.click()); + flushSync(() => a.click()); + flushSync(() => reset2.click()); + flushSync(() => b.click()); + + resolve2.click(); + await tick(); + + assert.htmlEqual(p.innerHTML, '1 + 2 = 3'); + + resolve1.click(); + await tick(); + + assert.htmlEqual(p.innerHTML, '2 + 3 = 5'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/main.svelte new file mode 100644 index 000000000000..cc82db0d7559 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/main.svelte @@ -0,0 +1,31 @@ + + + + + + + + + + + + +

{a} + {b} = {await add(a, b)}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-derived/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/Child.svelte new file mode 100644 index 000000000000..546494f4c3d6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/Child.svelte @@ -0,0 +1,11 @@ + + +

{indirect}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/_config.js new file mode 100644 index 000000000000..172b44e6e322 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/_config.js @@ -0,0 +1,14 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

0

'); + + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, '

1

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/main.svelte new file mode 100644 index 000000000000..f6b0afe98cba --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/main.svelte @@ -0,0 +1,15 @@ + + + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte new file mode 100644 index 000000000000..85d212b1a835 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte @@ -0,0 +1,5 @@ + + +

{value}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js b/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js new file mode 100644 index 000000000000..ef4c453b26ce --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js @@ -0,0 +1,33 @@ +import { tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target, component }) { + d.resolve('hello'); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + d = deferred(); + component.promise = d.promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + d.resolve('hello again'); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello again

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-prop/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-prop/main.svelte new file mode 100644 index 000000000000..cb5d00b3d374 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/main.svelte @@ -0,0 +1,13 @@ + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js new file mode 100644 index 000000000000..f8a7cfd479af --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js @@ -0,0 +1,24 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + html: `

pending

`, + + async test({ assert, target, warnings }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

3

3

'); + + assert.equal( + warnings[0], + 'Detected reactivity loss when reading `b`. This happens when state is read in an async function after an earlier `await`' + ); + + assert.equal(warnings[1].name, 'TracedAtError'); + + assert.equal(warnings.length, 2); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte new file mode 100644 index 000000000000..bdb1b095c9bc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte @@ -0,0 +1,20 @@ + + + + + + +

{await a_plus_b()}

+

{await a + await b}

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/_config.js b/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/_config.js new file mode 100644 index 000000000000..17bb79af086f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/_config.js @@ -0,0 +1,51 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [a, b, c, ok] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` +

b

+ + + + +

pending...

+ ` + ); + + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

c

+ + + + +

c

+ ` + ); + + ok.click(); + + b.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

b

+ + + + +

b

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/main.svelte new file mode 100644 index 000000000000..b1bb291e2e76 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/main.svelte @@ -0,0 +1,43 @@ + + +

{route}

+ + + + + + + {#if route === 'a'} +

a

+ {/if} + + {#if route === 'b'} + {#if ok} +

b

+ {:else} + {await goto('c')} + {/if} + {/if} + + {#if route === 'c'} +

c

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

pending...

+ {/snippet} + + {#snippet failed(error, reset)} + + {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js b/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js new file mode 100644 index 000000000000..ebbe642860d0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js @@ -0,0 +1,52 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + assert.htmlEqual( + target.innerHTML, + ` +

a

+ + + + +

a

+ ` + ); + + const [a, b, c, ok] = target.querySelectorAll('button'); + + b.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

c

+ + + + +

c

+ ` + ); + + ok.click(); + + b.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

b

+ + + + +

b

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-redirect/main.svelte new file mode 100644 index 000000000000..bf5fdf9ed395 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect/main.svelte @@ -0,0 +1,43 @@ + + +

{route}

+ + + + + + + {#if route === 'a'} +

a

+ {/if} + + {#if route === 'b'} + {#if ok} +

b

+ {:else} + {await goto('c')} + {/if} + {/if} + + {#if route === 'c'} +

c

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

pending...

+ {/snippet} + + {#snippet failed(error, reset)} + + {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js b/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js new file mode 100644 index 000000000000..22b8b2a1c462 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js @@ -0,0 +1,32 @@ +import { tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target, component }) { + d.resolve('hello'); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + component.promise = (d = deferred()).promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + d.resolve('wheee'); + await tick(); + assert.htmlEqual(target.innerHTML, '

wheee

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-render-tag/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-render-tag/main.svelte new file mode 100644 index 000000000000..e98738567112 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-render-tag/main.svelte @@ -0,0 +1,15 @@ + + +{#snippet hello(message)} +

{message}

+{/snippet} + + + {@render hello(await promise)} + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-slot/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-slot/Child.svelte new file mode 100644 index 000000000000..c32f869f63c6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-slot/Child.svelte @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-slot/_config.js b/packages/svelte/tests/runtime-runes/samples/async-slot/_config.js new file mode 100644 index 000000000000..3d54d242598c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-slot/_config.js @@ -0,0 +1,13 @@ +import { tick } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + html: ` +

loading...

+ `, + + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-slot/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-slot/main.svelte new file mode 100644 index 000000000000..badd60746d85 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-slot/main.svelte @@ -0,0 +1,13 @@ + + + + +

{message}

+
+ + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-2/_config.js new file mode 100644 index 000000000000..8276c5be419b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-2/_config.js @@ -0,0 +1,58 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO this one is tricky + + async test({ assert, target }) { + const [increment, a, b] = target.querySelectorAll('button'); + + a.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

a: 0

+ ` + ); + + increment.click(); + await tick(); + + increment.click(); + await tick(); + + increment.click(); + await tick(); + + a.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

a: 0

+ ` + ); + + b.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

b: 0

+ let count = $state(0); + + let a = []; + let b = []; + + function push(deferreds, value) { + const deferred = Promise.withResolvers(); + deferreds.push({ deferred, value }); + return deferred.promise; + } + + + + + + + + {#if count % 2 === 0} +

a: {await push(a, count)}

+ {:else} +

b: {await push(b, count)}

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

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-stale-derived/_config.js new file mode 100644 index 000000000000..884b27d865ed --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived/_config.js @@ -0,0 +1,52 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [increment, shift] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + +

0

+ ` + ); + + increment.click(); + await tick(); + + increment.click(); + await tick(); + + increment.click(); + await tick(); + + shift.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

2

+ ` + ); + + shift.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

delayed: 3

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-stale-derived/main.svelte new file mode 100644 index 000000000000..eeefffbee6c7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived/main.svelte @@ -0,0 +1,26 @@ + + + + + + + {#if count % 2} +

delayed: {await push()}

+ {:else} +

{await count}

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

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js new file mode 100644 index 000000000000..558caa629231 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js @@ -0,0 +1,32 @@ +import { tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target, component }) { + d.resolve('h1'); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + component.promise = (d = deferred()).promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + d.resolve('h2'); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte new file mode 100644 index 000000000000..52852b549c8e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte @@ -0,0 +1,11 @@ + + + + hello + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/Child.svelte new file mode 100644 index 000000000000..7ad618f13003 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/Child.svelte @@ -0,0 +1,7 @@ + + +

{value}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/_config.js new file mode 100644 index 000000000000..73c9b50a6962 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/_config.js @@ -0,0 +1,40 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [toggle, hello] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + + toggle.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + + hello.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

condition is true

+

hello

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/main.svelte new file mode 100644 index 000000000000..d111ce6fe3db --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/main.svelte @@ -0,0 +1,20 @@ + + + + + + + {#if condition} +

condition is {condition}

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

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level/Child.svelte new file mode 100644 index 000000000000..7ad618f13003 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level/Child.svelte @@ -0,0 +1,7 @@ + + +

{value}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js new file mode 100644 index 000000000000..b2200201c611 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js @@ -0,0 +1,14 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending

`, + + async test({ assert, target }) { + const [hello] = target.querySelectorAll('button'); + + hello.click(); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level/main.svelte new file mode 100644 index 000000000000..78ad3ba04a18 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level/main.svelte @@ -0,0 +1,15 @@ + + + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/_config.js b/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/_config.js new file mode 100644 index 000000000000..e9ccbba2b66b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/_config.js @@ -0,0 +1,32 @@ +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, + ` + +

0

+ ` + ); + + increment.click(); + await tick(); + + increment.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

2

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/main.svelte new file mode 100644 index 000000000000..e0619a1fe4d4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/main.svelte @@ -0,0 +1,19 @@ + + + + + + {#if count % 2} +

{await new Promise(() => {})}

+ {:else} +

{await count}

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

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/_config.js new file mode 100644 index 000000000000..e2c8b851c1e5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/_config.js @@ -0,0 +1,42 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + +
+

pending

+ `, + + async test({ assert, target }) { + const [button1, button2] = target.querySelectorAll('button'); + + button1.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +
+

pending

+ ` + ); + + button2.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +
+ +

true

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/main.svelte new file mode 100644 index 000000000000..86af9bb07eab --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/main.svelte @@ -0,0 +1,22 @@ + + + + + +
+ + + {#if await d1.promise} + +

{await d2.promise}

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

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js new file mode 100644 index 000000000000..837dd976e2fb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js @@ -0,0 +1,53 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

loading...

`, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

1

+

1

+

1

+ ` + ); + + const [log, x, other] = target.querySelectorAll('button'); + + flushSync(() => x.click()); + flushSync(() => other.click()); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

1

+

1

+

1

+ ` + ); + + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

2

+

2

+

2

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/main.svelte new file mode 100644 index 000000000000..764007e082a3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/main.svelte @@ -0,0 +1,19 @@ + + + + + + + +

{x}

+

{await x}

+

{y}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js index e55733c14810..416f61d23a9b 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js @@ -1,3 +1,4 @@ +import { async_mode } from '../../../helpers'; import { test } from '../../test'; import { flushSync } from 'svelte'; @@ -10,6 +11,12 @@ export default test({ flushSync(() => { b1.click(); }); - assert.deepEqual(logs, ['init 0']); + + // With async mode (which is on by default for runtime-runes) this works as expected, without it + // it works differently: https://github.com/sveltejs/svelte/pull/15564 + assert.deepEqual( + logs, + async_mode ? ['init 0', 'cleanup 0', null, 'init 2', 'cleanup 2', null, 'init 4'] : ['init 0'] + ); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte index 2cdcfdfb58f2..da38374f8232 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte @@ -14,4 +14,4 @@ }) - + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js index e092d0e7c752..f34668ec45b7 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js @@ -2,6 +2,8 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ + skip: true, // TODO unskip once tagged values are in and we can fix this properly + test({ assert, target }) { let btn = target.querySelector('button'); diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-await/Child.svelte b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/Child.svelte new file mode 100644 index 000000000000..122a31672661 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/Child.svelte @@ -0,0 +1,11 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js new file mode 100644 index 000000000000..1bf7e71176d4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js @@ -0,0 +1,12 @@ +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, logs }) { + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + + assert.ok(logs[0].startsWith('set_context_after_init')); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/main.svelte new file mode 100644 index 000000000000..65d0e623cf38 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/main.svelte @@ -0,0 +1,11 @@ + + + + + + {#snippet pending()} + ... + {/snippet} + diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js new file mode 100644 index 000000000000..4569f42a7379 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js @@ -0,0 +1,16 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; +import { async_mode } from '../../../helpers'; + +export default test({ + async test({ target, assert, logs }) { + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + assert.ok( + async_mode + ? logs[0].startsWith('set_context_after_init') + : logs[0] === 'works without experimental async but really shouldnt' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte new file mode 100644 index 000000000000..0c3b6c3a0fba --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte @@ -0,0 +1,20 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/tick-timing/_config.js b/packages/svelte/tests/runtime-runes/samples/tick-timing/_config.js index 339cec55c5a2..25414d4b4710 100644 --- a/packages/svelte/tests/runtime-runes/samples/tick-timing/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/tick-timing/_config.js @@ -3,6 +3,8 @@ import { test, ok } from '../../test'; // Tests that tick only resolves after all pending effects have been cleared export default test({ + skip: true, // weirdly, this works if you run it by itself + async test({ assert, target }) { const btn = target.querySelector('button'); ok(btn); diff --git a/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js index 18062b86fb43..b728c3c0bead 100644 --- a/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js @@ -2,6 +2,8 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ + // In async mode we _do_ want to run effects that react to their own state changing + skip_async: true, test({ assert, target, logs }) { const button = target.querySelector('button'); diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 719c936df002..937324727b16 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -6,16 +6,18 @@ import { effect, effect_root, render_effect, - user_effect + user_effect, + user_pre_effect } from '../../src/internal/client/reactivity/effects'; import { state, set, update, update_pre } from '../../src/internal/client/reactivity/sources'; -import type { Derived, Effect, Value } from '../../src/internal/client/types'; +import type { Derived, Effect, Source, Value } from '../../src/internal/client/types'; import { proxy } from '../../src/internal/client/proxy'; import { derived } from '../../src/internal/client/reactivity/deriveds'; import { snapshot } from '../../src/internal/shared/clone.js'; import { SvelteSet } from '../../src/reactivity/set'; import { DESTROYED } from '../../src/internal/client/constants'; import { noop } from 'svelte/internal/client'; +import { disable_async_mode_flag, enable_async_mode_flag } from '../../src/internal/flags'; /** * @param runes runes mode @@ -557,7 +559,7 @@ describe('signals', () => { }; }); - test('schedules rerun when writing to signal before reading it', (runes) => { + test.skip('schedules rerun when writing to signal before reading it', (runes) => { if (!runes) return () => {}; const error = console.error; @@ -1049,31 +1051,85 @@ describe('signals', () => { }; }); - test('effects do not depend on state they own', () => { + test('effects do depend on state they own', (runes) => { + // This behavior is important for use cases like a Resource class + // which shares its instance between multiple effects and triggers + // rerenders by self-invalidating its state. + const log: number[] = []; + + let count: any; + + if (runes) { + // We will make this the new default behavior once it's stable but until then + // we need to keep the old behavior to not break existing code. + enable_async_mode_flag(); + } + + effect(() => { + if (!count || $.get(count) < 2) { + count ||= state(0); + log.push($.get(count)); + set(count, $.get(count) + 1); + } + }); + + return () => { + try { + flushSync(); + if (runes) { + assert.deepEqual(log, [0, 1]); + } else { + assert.deepEqual(log, [0]); + } + } finally { + disable_async_mode_flag(); + } + }; + }); + + test('nested effects depend on state of upper effects', () => { + const logs: number[] = []; + let raw: Source; + let proxied: { current: number }; + user_effect(() => { - const value = state(0); - set(value, $.get(value) + 1); + raw = state(0); + proxied = proxy({ current: 0 }); + + // We need those separate, else one working and rerunning the effect + // could mask the other one not rerunning + user_effect(() => { + logs.push($.get(raw)); + }); + + user_effect(() => { + logs.push(proxied.current); + }); }); return () => { flushSync(); + set(raw, $.get(raw) + 1); + proxied.current += 1; + flushSync(); + assert.deepEqual(logs, [0, 0, 1, 1]); }; }); test('nested effects depend on state of upper effects', () => { const logs: number[] = []; - user_effect(() => { + user_pre_effect(() => { const raw = state(0); const proxied = proxy({ current: 0 }); // We need those separate, else one working and rerunning the effect // could mask the other one not rerunning - user_effect(() => { + user_pre_effect(() => { logs.push($.get(raw)); }); - user_effect(() => { + user_pre_effect(() => { logs.push(proxied.current); }); @@ -1081,7 +1137,7 @@ describe('signals', () => { // together with the reading effects flushSync(); - user_effect(() => { + user_pre_effect(() => { $.untrack(() => { set(raw, $.get(raw) + 1); proxied.current += 1; diff --git a/packages/svelte/tests/store/test.ts b/packages/svelte/tests/store/test.ts index 77cecca7e525..ecb22c1be6f6 100644 --- a/packages/svelte/tests/store/test.ts +++ b/packages/svelte/tests/store/test.ts @@ -11,6 +11,7 @@ import { } from 'svelte/store'; import { source, set } from '../../src/internal/client/reactivity/sources'; import * as $ from '../../src/internal/client/runtime'; +import { flushSync } from '../../src/internal/client/reactivity/batch'; import { effect_root, render_effect } from 'svelte/internal/client'; describe('writable', () => { @@ -602,7 +603,7 @@ describe('toStore', () => { assert.deepEqual(log, [0]); set(count, 1); - $.flushSync(); + flushSync(); assert.deepEqual(log, [0, 1]); unsubscribe(); @@ -625,7 +626,7 @@ describe('toStore', () => { assert.deepEqual(log, [0]); set(count, 1); - $.flushSync(); + flushSync(); assert.deepEqual(log, [0, 1]); store.set(2); @@ -654,11 +655,11 @@ describe('fromStore', () => { assert.deepEqual(log, [0]); store.set(1); - $.flushSync(); + flushSync(); assert.deepEqual(log, [0, 1]); count.current = 2; - $.flushSync(); + flushSync(); assert.deepEqual(log, [0, 1, 2]); assert.equal(get(store), 2); diff --git a/packages/svelte/tests/suite.ts b/packages/svelte/tests/suite.ts index 0ae06e727f87..6954b8b683f6 100644 --- a/packages/svelte/tests/suite.ts +++ b/packages/svelte/tests/suite.ts @@ -35,7 +35,7 @@ export function suite(fn: (config: Test, test_dir: string export function suite_with_variants( variants: Variants[], - should_skip_variant: (variant: Variants, config: Test) => boolean | 'no-test', + should_skip_variant: (variant: Variants, config: Test, test_name: string) => boolean | 'no-test', common_setup: (config: Test, test_dir: string) => Promise | Common, fn: (config: Test, test_dir: string, variant: Variants, common: Common) => void ) { @@ -46,11 +46,11 @@ export function suite_with_variants void): void; + /** + * Synchronously flush any pending updates. + * Returns void if no callback is provided, otherwise returns the result of calling the callback. + * */ + export function flushSync(fn?: (() => T) | undefined): T; /** * Create a snippet programmatically * */ @@ -443,29 +448,6 @@ declare module 'svelte' { }): Snippet; /** Anything except a function */ type NotFunction = T extends Function ? never : T; - /** - * Synchronously flush any pending updates. - * Returns void if no callback is provided, otherwise returns the result of calling the callback. - * */ - export function flushSync(fn?: (() => T) | undefined): T; - /** - * Returns a promise that resolves once any pending state changes have been applied. - * */ - export function tick(): Promise; - /** - * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect), - * any state read inside `fn` will not be treated as a dependency. - * - * ```ts - * $effect(() => { - * // this will run when `data` changes, but not when `time` changes - * save(data, { - * timestamp: untrack(() => time) - * }); - * }); - * ``` - * */ - export function untrack(fn: () => T): T; /** * Retrieves the context that belongs to the closest parent component with the specified `key`. * Must be called during component initialisation. @@ -539,6 +521,29 @@ declare module 'svelte' { export function unmount(component: Record, options?: { outro?: boolean; } | undefined): Promise; + /** + * Returns a promise that resolves once any pending state changes have been applied. + * */ + export function tick(): Promise; + /** + * Returns a promise that resolves once any state changes, and asynchronous work resulting from them, + * have resolved and the DOM has been updated + * */ + export function settled(): Promise; + /** + * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect), + * any state read inside `fn` will not be treated as a dependency. + * + * ```ts + * $effect(() => { + * // this will run when `data` changes, but not when `time` changes + * save(data, { + * timestamp: untrack(() => time) + * }); + * }); + * ``` + * */ + export function untrack(fn: () => T): T; type Getters = { [K in keyof T]: () => T[K]; }; @@ -1111,6 +1116,11 @@ declare module 'svelte/compiler' { * Use this to filter out warnings. Return `true` to keep the warning, `false` to discard it. */ warningFilter?: (warning: Warning) => boolean; + /** Experimental options */ + experimental?: { + /** Allow `await` keyword in deriveds, template expressions, and the top level of components */ + async?: boolean; + }; } /** * - `html` — the default, for e.g. `
` or `` @@ -3021,6 +3031,11 @@ declare module 'svelte/types/compiler/interfaces' { * Use this to filter out warnings. Return `true` to keep the warning, `false` to discard it. */ warningFilter?: (warning: Warning_1) => boolean; + /** Experimental options */ + experimental?: { + /** Allow `await` keyword in deriveds, template expressions, and the top level of components */ + async?: boolean; + }; } /** * - `html` — the default, for e.g. `
` or `` diff --git a/playgrounds/sandbox/index.html b/playgrounds/sandbox/index.html index 845538abf073..d70409ffb63a 100644 --- a/playgrounds/sandbox/index.html +++ b/playgrounds/sandbox/index.html @@ -14,6 +14,12 @@ import { mount, hydrate, unmount } from 'svelte'; import App from '/src/App.svelte'; + globalThis.delayed = (v, ms = 1000) => { + return new Promise((f) => { + setTimeout(() => f(v), ms); + }); + }; + const root = document.getElementById('root'); const render = root.firstChild?.nextSibling ? hydrate : mount; diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index 2029937f52dc..639b75502044 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -76,7 +76,10 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { dev: false, filename: input, generate, - runes: argv.values.runes + runes: argv.values.runes, + experimental: { + async: true + } }); for (const warning of compiled.warnings) { @@ -94,7 +97,10 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { filename: input, generate, runes: argv.values.runes, - fragments: 'tree' + fragments: 'tree', + experimental: { + async: true + } }); const output_js = `${cwd}/output/${generate}/${file}.tree.js`; @@ -116,7 +122,10 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { const compiled = compileModule(source, { dev: false, filename: input, - generate + generate, + experimental: { + async: true + } }); const output_js = `${cwd}/output/${generate}/${file}`; diff --git a/playgrounds/sandbox/ssr-common.js b/playgrounds/sandbox/ssr-common.js new file mode 100644 index 000000000000..db3e08550868 --- /dev/null +++ b/playgrounds/sandbox/ssr-common.js @@ -0,0 +1,17 @@ +Promise.withResolvers ??= () => { + let resolve; + let reject; + + const promise = new Promise((f, r) => { + resolve = f; + reject = r; + }); + + return { promise, resolve, reject }; +}; + +globalThis.delayed = (v, ms = 1000) => { + return new Promise((f) => { + setTimeout(() => f(v), ms); + }); +}; diff --git a/playgrounds/sandbox/ssr-dev.js b/playgrounds/sandbox/ssr-dev.js index 01ce14e2664d..e019b234a613 100644 --- a/playgrounds/sandbox/ssr-dev.js +++ b/playgrounds/sandbox/ssr-dev.js @@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url'; import polka from 'polka'; import { createServer as createViteServer } from 'vite'; import { render } from 'svelte/server'; +import './ssr-common.js'; const PORT = process.env.PORT || '5173'; diff --git a/playgrounds/sandbox/ssr-prod.js b/playgrounds/sandbox/ssr-prod.js index 1ed9435249ea..e8f74ee93ae7 100644 --- a/playgrounds/sandbox/ssr-prod.js +++ b/playgrounds/sandbox/ssr-prod.js @@ -3,6 +3,7 @@ import path from 'node:path'; import polka from 'polka'; import { render } from 'svelte/server'; import App from './src/App.svelte'; +import './ssr-common.js'; const { head, body } = render(App); diff --git a/playgrounds/sandbox/svelte.config.js b/playgrounds/sandbox/svelte.config.js index 65f739bdb608..68ac605385aa 100644 --- a/playgrounds/sandbox/svelte.config.js +++ b/playgrounds/sandbox/svelte.config.js @@ -1,5 +1,9 @@ export default { compilerOptions: { - hmr: true + hmr: false, + + experimental: { + async: true + } } };