diff --git a/.changeset/new-candles-marry.md b/.changeset/new-candles-marry.md deleted file mode 100644 index 4d55980c72..0000000000 --- a/.changeset/new-candles-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: simplify internal component `pop()` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0e1d36760..046ad335f3 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/.github/workflows/ecosystem-ci-trigger.yml b/.github/workflows/ecosystem-ci-trigger.yml index 71df3242e8..9be1f00104 100644 --- a/.github/workflows/ecosystem-ci-trigger.yml +++ b/.github/workflows/ecosystem-ci-trigger.yml @@ -8,9 +8,17 @@ jobs: trigger: runs-on: ubuntu-latest if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') + permissions: + issues: write # to add / delete reactions + pull-requests: write # to read PR data, and to add labels + actions: read # to check workflow status + contents: read # to clone the repo steps: - - uses: GitHubSecurityLab/actions-permissions/monitor@v1 - - uses: actions/github-script@v6 + - name: monitor action permissions + uses: GitHubSecurityLab/actions-permissions/monitor@v1 + - name: check user authorization # user needs triage permission + uses: actions/github-script@v7 + id: check-permissions with: script: | const user = context.payload.sender.login @@ -29,7 +37,7 @@ jobs: } if (hasTriagePermission) { - console.log('Allowed') + console.log('User is allowed. Adding +1 reaction.') await github.rest.reactions.createForIssueComment({ owner: context.repo.owner, repo: context.repo.repo, @@ -37,16 +45,18 @@ jobs: content: '+1', }) } else { - console.log('Not allowed') + console.log('User is not allowed. Adding -1 reaction.') await github.rest.reactions.createForIssueComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: context.payload.comment.id, content: '-1', }) - throw new Error('not allowed') + throw new Error('User does not have the necessary permissions.') } - - uses: actions/github-script@v6 + + - name: Get PR Data + uses: actions/github-script@v7 id: get-pr-data with: script: | @@ -59,21 +69,27 @@ jobs: return { num: context.issue.number, branchName: pr.head.ref, + commit: pr.head.sha, repo: pr.head.repo.full_name } - - id: generate-token - uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 #keep pinned for security reasons, currently 1.8.0 + + - name: Generate Token + id: generate-token + uses: actions/create-github-app-token@v2 with: - app_id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }} - private_key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }} - repository: '${{ github.repository_owner }}/svelte-ecosystem-ci' - - uses: actions/github-script@v6 + app-id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }} + private-key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }} + repositories: | + svelte + svelte-ecosystem-ci + + - name: Trigger Downstream Workflow + uses: actions/github-script@v7 id: trigger env: COMMENT: ${{ github.event.comment.body }} with: github-token: ${{ steps.generate-token.outputs.token }} - result-encoding: string script: | const comment = process.env.COMMENT.trim() const prData = ${{ steps.get-pr-data.outputs.result }} @@ -89,6 +105,7 @@ jobs: prNumber: '' + prData.num, branchName: prData.branchName, repo: prData.repo, + commit: prData.commit, suite: suite === '' ? '-' : suite } }) diff --git a/.prettierignore b/.prettierignore index 72cd10aca8..5e1d9b1aa7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -28,15 +28,6 @@ packages/svelte/types packages/svelte/compiler/index.js playgrounds/sandbox/src/* -# sites/svelte.dev -sites/svelte.dev/static/svelte-app.json -sites/svelte.dev/scripts/svelte-app/ -sites/svelte.dev/src/routes/_components/Supporters/contributors.jpg -sites/svelte.dev/src/routes/_components/Supporters/contributors.js -sites/svelte.dev/src/routes/_components/Supporters/donors.jpg -sites/svelte.dev/src/routes/_components/Supporters/donors.js -sites/svelte.dev/src/lib/generated - **/node_modules **/.svelte-kit **/.vercel diff --git a/.prettierrc b/.prettierrc index c4fd5d9f2f..c2d09a4289 100644 --- a/.prettierrc +++ b/.prettierrc @@ -17,12 +17,6 @@ "useTabs": false, "tabWidth": 2 } - }, - { - "files": ["sites/svelte-5-preview/src/routes/docs/content/**/*.md"], - "options": { - "printWidth": 60 - } } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 21a2a11c84..4d360cbc8a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,3 @@ { - "search.exclude": { - "sites/svelte-5-preview/static/*": true - }, "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e2628f84f..c2d3e45049 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,10 +105,10 @@ Test samples are kept in `/test/xxx/samples` folder. pnpm test validator ``` -1. To filter tests _within_ a test suite, use `pnpm test -- -t `, for example: +1. To filter tests _within_ a test suite, use `pnpm test -t `, for example: ```bash - pnpm test validator -- -t a11y-alt-text + pnpm test validator -t a11y-alt-text ``` (You can also do `FILTER= pnpm test ` which removes other tests rather than simply skipping them — this will result in faster and more compact test results, but it's non-idiomatic. Choose your fighter.) diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index 8e6c91fad7..741e24fde0 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -50,7 +50,7 @@ todos.push({ }); ``` -> [!NOTE] When you update properties of proxies, the original object is _not_ mutated. +> [!NOTE] When you update properties of proxies, the original object is _not_ mutated. If you need to use your own proxy handlers in a state proxy, [you should wrap the object _after_ wrapping it in `$state`](https://svelte.dev/playground/hello-world?version=latest#H4sIAAAAAAAACpWR3WoDIRCFX2UqhWyIJL3erAulL9C7XnQLMe5ksbUqOpsfln33YuyGFNJC8UKdc2bOhw7Myk9kJXsJ0nttO9jcR5KEG9AWJDwHdzwxznbaYGTl68Do5JM_FRifuh-9X8Y9Gkq1rYx4q66cJbQUWcmqqIL2VDe2IYMEbvuOikBADi-GJDSkXG-phId0G-frye2DO2psQYDFQ0Ys8gQO350dUkEydEg82T0GOs0nsSG9g2IqgxACZueo2ZUlpdvoDC6N64qsg1QKY8T2bpZp8gpIfbCQ85Zn50Ud82HkeY83uDjspenxv3jXcSDyjPWf9L1vJf0GH666J-jLu1ery4dV257IWXBWGa0-xFDMQdTTn2ScxWKsn86ROsLwQxqrVR5QM84Ij8TKFD2-cUZSm4O2LSt30kQcvwCgCmfZnAIAAA==). Note that if you destructure a reactive value, the references are not reactive — as in normal JavaScript, they are evaluated at the point of destructuring: @@ -119,7 +119,9 @@ class Todo { } ``` -> Svelte provides reactive implementations of built-in classes like `Set` and `Map` that can be imported from [`svelte/reactivity`](svelte-reactivity). +### Built-in classes + +Svelte provides reactive implementations of built-in classes like `Set`, `Map`, `Date` and `URL` that can be imported from [`svelte/reactivity`](svelte-reactivity). ## `$state.raw` diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md index 2464aa9295..0123868c4e 100644 --- a/documentation/docs/02-runes/03-$derived.md +++ b/documentation/docs/02-runes/03-$derived.md @@ -94,6 +94,27 @@ let selected = $derived(items[index]); ...you can change (or `bind:` to) properties of `selected` and it will affect the underlying `items` array. If `items` was _not_ deeply reactive, mutating `selected` would have no effect. +## Destructuring + +If you use destructuring with a `$derived` declaration, the resulting variables will all be reactive — this... + +```js +function stuff() { return { a: 1, b: 2, c: 3 } } +// ---cut--- +let { a, b, c } = $derived(stuff()); +``` + +...is roughly equivalent to this: + +```js +function stuff() { return { a: 1, b: 2, c: 3 } } +// ---cut--- +let _stuff = $derived(stuff()); +let a = $derived(_stuff.a); +let b = $derived(_stuff.b); +let c = $derived(_stuff.c); +``` + ## Update propagation Svelte uses something called _push-pull reactivity_ — when state is updated, everything that depends on the state (whether directly or indirectly) is immediately notified of the change (the 'push'), but derived values are not re-evaluated until they are actually read (the 'pull'). diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index 0e129973d5..5820e178a0 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -221,6 +221,21 @@ The `$effect.tracking` rune is an advanced feature that tells you whether or not It is used to implement abstractions like [`createSubscriber`](/docs/svelte/svelte-reactivity#createSubscriber), which will create listeners to update reactive values but _only_ if those values are being tracked (rather than, for example, read inside an event handler). +## `$effect.pending` + +When using [`await`](await-expressions) in components, the `$effect.pending()` rune tells you how many promises are pending in the current [boundary](svelte-boundary), not including child boundaries ([demo](/playground/untitled#H4sIAAAAAAAAE3WRMU_DMBCF_8rJdHDUqilILGkaiY2RgY0yOPYZWbiOFV8IleX_jpMUEAIWS_7u-d27c2ROnJBV7B6t7WDsequAozKEqmAbpo3FwKqnyOjsJ90EMr-8uvN-G97Q0sRaEfAvLjtH6CjbsDrI3nhqju5IFgkEHGAVSBDy62L_SdtvejPTzEU4Owl6cJJM50AoxcUG2gLiVM31URgChyM89N3JBORcF3BoICA9mhN2A3G9gdvdrij2UJYgejLaSCMsKLTivNj0SEOf7WEN7ZwnHV1dfqd2dTsQ5QCdk9bI10PkcxexXqcmH3W51Jt_le2kbH8os9Y3UaTcNLYpDx-Xab6GTHXpZ128MhpWqDVK2np0yrgXXqQpaLa4APDLBkIF8bd2sYql0Sn_DeE7sYr6AdNzvgljR-MUq7SwAdMHeUtgHR4CAAA=)): + +```svelte + + + +

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

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

pending promises: {$effect.pending()}

+{/if} +``` + ## `$effect.root` The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for nested effects that you want to manually control. This rune also allows for the creation of effects outside of the component initialisation phase. diff --git a/documentation/docs/03-template-syntax/19-await-expressions.md b/documentation/docs/03-template-syntax/19-await-expressions.md new file mode 100644 index 0000000000..4e5ec28b26 --- /dev/null +++ b/documentation/docs/03-template-syntax/19-await-expressions.md @@ -0,0 +1,144 @@ +--- +title: await +--- + +As of Svelte 5.36, you can use the `await` keyword inside your components in three places where it was previously unavailable: + +- at the top level of your component's ` + + + + +

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

+``` + +...if you increment `a`, the contents of the `

` will _not_ immediately update to read this — + +```html +

2 + 2 = 3

+``` + +— instead, the text will update to `2 + 2 = 4` when `add(a, b)` resolves. + +Updates can overlap — a fast update will be reflected in the UI while an earlier slow update is still ongoing. + +## Concurrency + +Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup... + +```svelte +

{await one()}

+

{await two()}

+``` + +...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential. + +This does not apply to sequential `await` expressions inside your ` + + + + (reset = r)}> + + + {#snippet failed(e)} +

oops! {e.message}

+ {/snippet} +
+``` + ### transition_slide_display ``` diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index db848a0299..20f57770d1 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 6c31aaafd0..de34b3f5da 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 ` + + + + (reset = r)}> + + + {#snippet failed(e)} +

oops! {e.message}

+ {/snippet} +
+``` + ## transition_slide_display > The `slide` transition does not work correctly for elements with `display: %value%` diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index e11975aef2..2b0c5eafdf 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -70,6 +70,10 @@ This turned out to be buggy and unpredictable, particularly when working with de > `$effect()` can only be used as an expression statement +## 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 > `%name%` is not defined @@ -98,6 +102,10 @@ This turned out to be buggy and unpredictable, particularly when working with de > The arguments keyword cannot be used within the template or at the top level of a component +## 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 > Cannot use `export let` in runes mode — use `$props()` instead diff --git a/packages/svelte/messages/shared-errors/errors.md b/packages/svelte/messages/shared-errors/errors.md index 4b4d332202..f9160671d3 100644 --- a/packages/svelte/messages/shared-errors/errors.md +++ b/packages/svelte/messages/shared-errors/errors.md @@ -1,3 +1,21 @@ +## 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 ` * diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index e9fa33b054..599d3e8248 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -170,6 +170,15 @@ export function effect_invalid_placement(node) { e(node, 'effect_invalid_placement', `\`$effect()\` can only be used as an expression statement\nhttps://svelte.dev/e/effect_invalid_placement`); } +/** + * Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true` + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function experimental_async(node) { + e(node, '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\`\nhttps://svelte.dev/e/experimental_async`); +} + /** * `%name%` is not defined * @param {null | number | NodeLike} node @@ -235,6 +244,15 @@ export function invalid_arguments_usage(node) { e(node, 'invalid_arguments_usage', `The arguments keyword cannot be used within the template or at the top level of a component\nhttps://svelte.dev/e/invalid_arguments_usage`); } +/** + * Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function legacy_await_invalid(node) { + e(node, 'legacy_await_invalid', `Cannot use \`await\` in deriveds and template expressions, or at the top level of a component, unless in runes mode\nhttps://svelte.dev/e/legacy_await_invalid`); +} + /** * Cannot use `export let` in runes mode — use `$props()` instead * @param {null | number | NodeLike} node diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js index 9ba23c1485..a378af34ee 100644 --- a/packages/svelte/src/compiler/index.js +++ b/packages/svelte/src/compiler/index.js @@ -20,7 +20,7 @@ export { default as preprocess } from './preprocess/index.js'; */ export function compile(source, options) { source = remove_bom(source); - state.reset_warnings(options.warningFilter); + state.reset({ warning: options.warningFilter, filename: options.filename }); const validated = validate_component_options(options, ''); let parsed = _parse(source); @@ -63,7 +63,7 @@ export function compile(source, options) { */ export function compileModule(source, options) { source = remove_bom(source); - state.reset_warnings(options.warningFilter); + state.reset({ warning: options.warningFilter, filename: options.filename }); const validated = validate_module_options(options, ''); const analysis = analyze_module(source, validated); @@ -111,7 +111,7 @@ export function compileModule(source, options) { */ export function parse(source, { modern, loose } = {}) { source = remove_bom(source); - state.reset_warnings(() => false); + state.reset({ warning: () => false, filename: undefined }); const ast = _parse(source, loose); return to_public_ast(source, ast, modern); diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index fdc9734f85..6b2e6cda70 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -9,7 +9,7 @@ import { parse } from '../phases/1-parse/index.js'; import { regex_valid_component_name } from '../phases/1-parse/state/element.js'; import { analyze_component } from '../phases/2-analyze/index.js'; import { get_rune } from '../phases/scope.js'; -import { reset, reset_warnings } from '../state.js'; +import { reset, UNKNOWN_FILENAME } from '../state.js'; import { extract_identifiers, extract_all_identifiers_from_expression, @@ -134,7 +134,7 @@ export function migrate(source, { filename, use_ts } = {}) { return start + style_placeholder + end; }); - reset_warnings(() => false); + reset({ warning: () => false, filename }); let parsed = parse(source); @@ -145,7 +145,10 @@ export function migrate(source, { filename, use_ts } = {}) { ...validate_component_options({}, ''), ...parsed_options, customElementOptions, - filename: filename ?? '(unknown)' + filename: filename ?? UNKNOWN_FILENAME, + experimental: { + async: true + } }; const str = new MagicString(source); diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 6b6c9160d8..87332f647d 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -295,6 +295,8 @@ export default function element(parser) { } else { element.tag = get_attribute_expression(definition); } + + element.metadata.expression = create_expression_metadata(); } if (is_top_level_script_or_style) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index d73e273ec2..d407b44556 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -1,4 +1,4 @@ -/** @import { Comment, Expression, Node, Program } from 'estree' */ +/** @import { Expression, Node, Program } from 'estree' */ /** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ /** @import { AnalysisState, Visitors } from './types' */ /** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */ @@ -22,6 +22,7 @@ import { AssignmentExpression } from './visitors/AssignmentExpression.js'; import { AttachTag } from './visitors/AttachTag.js'; import { Attribute } from './visitors/Attribute.js'; import { AwaitBlock } from './visitors/AwaitBlock.js'; +import { AwaitExpression } from './visitors/AwaitExpression.js'; import { BindDirective } from './visitors/BindDirective.js'; import { CallExpression } from './visitors/CallExpression.js'; import { ClassBody } from './visitors/ClassBody.js'; @@ -140,6 +141,7 @@ const visitors = { AttachTag, Attribute, AwaitBlock, + AwaitExpression, BindDirective, CallExpression, ClassBody, @@ -211,9 +213,14 @@ function js(script, root, allow_reactive_declarations, parent) { body: [] }; - const { scope, scopes } = create_scopes(ast, root, allow_reactive_declarations, parent); + const { scope, scopes, has_await } = create_scopes( + ast, + root, + allow_reactive_declarations, + parent + ); - return { ast, scope, scopes }; + return { ast, scope, scopes, has_await }; } /** @@ -244,7 +251,7 @@ export function analyze_module(source, options) { state.set_source(source); const ast = parse(source, comments, false, false); - const { scope, scopes } = create_scopes(ast, new ScopeRoot(), false, null); + const { scope, scopes, has_await } = create_scopes(ast, new ScopeRoot(), false, null); for (const [name, references] of scope.references) { if (name[0] !== '$' || RESERVED.includes(name)) continue; @@ -261,19 +268,19 @@ export function analyze_module(source, options) { /** @type {Analysis} */ const analysis = { - module: { ast, scope, scopes }, + module: { ast, scope, scopes, has_await }, name: options.filename, accessors: false, runes: true, immutable: true, tracing: false, + async_deriveds: new Set(), comments, classes: new Map() }; - state.reset({ + state.adjust({ dev: options.dev, - filename: options.filename, rootDir: options.rootDir, runes: true }); @@ -314,7 +321,12 @@ export function analyze_component(root, source, options) { const module = js(root.module, scope_root, false, null); const instance = js(root.instance, scope_root, true, module.scope); - const { scope, scopes } = create_scopes(root.fragment, scope_root, false, instance.scope); + const { scope, scopes, has_await } = create_scopes( + root.fragment, + scope_root, + false, + instance.scope + ); /** @type {Template} */ const template = { ast: root.fragment, scope, scopes }; @@ -422,7 +434,9 @@ export function analyze_component(root, source, options) { const component_name = get_component_name(options.filename); - const runes = options.runes ?? Array.from(module.scope.references.keys()).some(is_rune); + const runes = + options.runes ?? + (has_await || instance.has_await || Array.from(module.scope.references.keys()).some(is_rune)); if (!runes) { for (let check of synthetic_stores_legacy_check) { @@ -512,15 +526,15 @@ export function analyze_component(root, source, options) { source, undefined_exports: new Map(), snippet_renderers: new Map(), - snippets: new Set() + snippets: new Set(), + async_deriveds: new Set() }; - state.reset({ + state.adjust({ component_name: analysis.name, dev: options.dev, - filename: options.filename, rootDir: options.rootDir, - runes: true + runes }); if (!runes) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 773aa59744..b13f3f89b6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -192,8 +192,13 @@ function get_delegated_event(event_name, handler, context) { return unhoisted; } - // If we are referencing a binding that is shadowed in another scope then bail out. - if (local_binding !== null && binding !== null && local_binding.node !== binding.node) { + // If we are referencing a binding that is shadowed in another scope then bail out (unless it's declared within the function). + if ( + local_binding !== null && + binding !== null && + local_binding.node !== binding.node && + scope.declarations.get(reference) !== binding + ) { return unhoisted; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js new file mode 100644 index 0000000000..af7d0307e9 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -0,0 +1,30 @@ +/** @import { AwaitExpression } from 'estree' */ +/** @import { Context } from '../types' */ +import * as e from '../../../errors.js'; + +/** + * @param {AwaitExpression} node + * @param {Context} context + */ +export function AwaitExpression(node, context) { + let suspend = context.state.ast_type === 'instance' && context.state.function_depth === 1; + + if (context.state.expression) { + context.state.expression.has_await = true; + suspend = true; + } + + // disallow top-level `await` or `await` in template expressions + // unless a) in runes mode and b) opted into `experimental.async` + if (suspend) { + if (!context.state.options.experimental.async) { + e.experimental_async(node); + } + + if (!context.state.analysis.runes) { + e.legacy_await_invalid(node); + } + } + + context.next(); +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 33abb52cac..9b6337b9ed 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -7,6 +7,7 @@ import { get_parent } from '../../../utils/ast.js'; import { is_pure, is_safe_identifier } from './shared/utils.js'; import { dev, locate_node, source } from '../../../state.js'; import * as b from '#compiler/builders'; +import { create_expression_metadata } from '../../nodes.js'; /** * @param {CallExpression} node @@ -163,6 +164,13 @@ export function CallExpression(node, context) { break; + case '$effect.pending': + if (context.state.expression) { + context.state.expression.has_state = true; + } + + break; + case '$inspect': if (node.arguments.length < 1) { e.rune_invalid_arguments_length(node, rune, 'one or more arguments'); @@ -227,7 +235,19 @@ export function CallExpression(node, context) { } // `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning - if (rune === '$inspect' || rune === '$derived') { + if (rune === '$derived') { + const expression = create_expression_metadata(); + + context.next({ + ...context.state, + function_depth: context.state.function_depth + 1, + expression + }); + + if (expression.has_await) { + context.state.analysis.async_deriveds.add(node); + } + } else if (rune === '$inspect') { context.next({ ...context.state, function_depth: context.state.function_depth + 1 }); } else { context.next(); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js index 4b85894e52..5b8d9ba053 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js @@ -11,6 +11,17 @@ export function ExportNamedDeclaration(node, context) { // visit children, so bindings are correctly initialised context.next(); + if ( + context.state.ast_type && + node.specifiers.some((specifier) => + specifier.exported.type === 'Identifier' + ? specifier.exported.name === 'default' + : specifier.exported.value === 'default' + ) + ) { + e.module_illegal_default_export(node); + } + if (node.declaration?.type === 'VariableDeclaration') { // in runes mode, forbid `export let` if ( diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js index d5689e5d55..fab5d46e1b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js @@ -9,7 +9,7 @@ import * as e from '../../../errors.js'; import * as w from '../../../warnings.js'; import { create_attribute, is_custom_element_node } from '../../nodes.js'; import { regex_starts_with_newline } from '../../patterns.js'; -import { check_element } from './shared/a11y.js'; +import { check_element } from './shared/a11y/index.js'; import { validate_element } from './shared/element.js'; import { mark_subtree_dynamic } from './shared/fragment.js'; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/StyleDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/StyleDirective.js index 7d6eb5be99..9699d3c03b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/StyleDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/StyleDirective.js @@ -32,6 +32,7 @@ export function StyleDirective(node, context) { node.metadata.expression.has_state ||= chunk.metadata.expression.has_state; node.metadata.expression.has_call ||= chunk.metadata.expression.has_call; + node.metadata.expression.has_await ||= chunk.metadata.expression.has_await; } } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js index d50cb80cb8..35af96ba12 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js @@ -2,7 +2,7 @@ /** @import { Context } from '../types' */ import * as e from '../../../errors.js'; -const valid = ['onerror', 'failed']; +const valid = ['onerror', 'failed', 'pending']; /** * @param {AST.SvelteBoundary} node diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js index c45859408c..3f7b0ec6b8 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js @@ -2,7 +2,7 @@ /** @import { Context } from '../types' */ import { NAMESPACE_MATHML, NAMESPACE_SVG } from '../../../../constants.js'; import { is_text_attribute } from '../../../utils/ast.js'; -import { check_element } from './shared/a11y.js'; +import { check_element } from './shared/a11y/index.js'; import { validate_element } from './shared/element.js'; import { mark_subtree_dynamic } from './shared/fragment.js'; @@ -62,5 +62,17 @@ export function SvelteElement(node, context) { mark_subtree_dynamic(context.path); - context.next({ ...context.state, parent_element: null }); + context.visit(node.tag, { + ...context.state, + expression: node.metadata.expression + }); + + for (const attribute of node.attributes) { + context.visit(attribute); + } + + context.visit(node.fragment, { + ...context.state, + parent_element: null + }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteSelf.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteSelf.js index b87f082de0..652a447165 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteSelf.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteSelf.js @@ -3,7 +3,7 @@ import { visit_component } from './shared/component.js'; import * as e from '../../../errors.js'; import * as w from '../../../warnings.js'; -import { filename } from '../../../state.js'; +import { filename, UNKNOWN_FILENAME } from '../../../state.js'; /** * @param {AST.SvelteSelf} node @@ -23,9 +23,9 @@ export function SvelteSelf(node, context) { } if (context.state.analysis.runes) { - const name = filename === '(unknown)' ? 'Self' : context.state.analysis.name; + const name = filename === UNKNOWN_FILENAME ? 'Self' : context.state.analysis.name; const basename = - filename === '(unknown)' + filename === UNKNOWN_FILENAME ? 'Self.svelte' : /** @type {string} */ (filename.split(/[/\\]/).pop()); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/constants.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/constants.js new file mode 100644 index 0000000000..a1b70f2207 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/constants.js @@ -0,0 +1,319 @@ +/** @import { ARIARoleRelationConcept } from 'aria-query' */ +import { roles as roles_map, elementRoles } from 'aria-query'; +// @ts-expect-error package doesn't provide typings +import { AXObjects, elementAXObjects } from 'axobject-query'; + +export const aria_attributes = + 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split( + ' ' + ); + +/** @type {Record} */ +export const a11y_required_attributes = { + a: ['href'], + area: ['alt', 'aria-label', 'aria-labelledby'], + // html-has-lang + html: ['lang'], + // iframe-has-title + iframe: ['title'], + img: ['alt'], + object: ['title', 'aria-label', 'aria-labelledby'] +}; + +export const a11y_distracting_elements = ['blink', 'marquee']; + +// this excludes `` and ` \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js new file mode 100644 index 0000000000..a947a91ab8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js @@ -0,0 +1,23 @@ +import { settled, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const [reset, resolve] = target.querySelectorAll('button'); + + reset.click(); + await settled(); + assert.deepEqual(logs, ['aborted']); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

hello

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-abort-signal/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/main.svelte new file mode 100644 index 0000000000..d8d77bf0e9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/main.svelte @@ -0,0 +1,29 @@ + + + + + + +

{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 0000000000..3de81a507b --- /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 0000000000..00a11cac43 --- /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 0000000000..0a64738409 --- /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 0000000000..6332a9802d --- /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 0000000000..a29c99860d --- /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 0000000000..a93eb7dc25 --- /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 0000000000..e2718a35d2 --- /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 0000000000..412da7268e --- /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 0000000000..c5dae7fee2 --- /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 }) { + 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 0000000000..1ad6cb84de --- /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 0000000000..18175de4dc --- /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 0000000000..256ad68f4a --- /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 0000000000..325cb1dcd6 --- /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 0000000000..edb0eaea44 --- /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 0000000000..3186ed2069 --- /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 0000000000..f0f27e4830 --- /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-destructured/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte new file mode 100644 index 0000000000..39112b12a7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte @@ -0,0 +1,13 @@ + + + + +

{count} ** 2 = {squared}

+

{count} ** 3 = {cubed}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/_config.js new file mode 100644 index 0000000000..d444e8e1d9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/_config.js @@ -0,0 +1,31 @@ +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, + ` + +

1 ** 2 = 1

+

1 ** 3 = 1

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

2 ** 2 = 4

+

2 ** 3 = 8

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/main.svelte new file mode 100644 index 0000000000..c5d8a12a78 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/main.svelte @@ -0,0 +1,11 @@ + + + + + + {#snippet pending()} +

pending

+ {/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 0000000000..fb47377513 --- /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 0000000000..914b311c97 --- /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 0000000000..a53381c2d5 --- /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 0000000000..ffcd8b46b4 --- /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 0000000000..fee8e2e6bf --- /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 0000000000..9babdb2fe2 --- /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 0000000000..f803a30c37 --- /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 0000000000..f7d1d28fde --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js @@ -0,0 +1,93 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + + +

pending

+ `, + + async test({ assert, target, logs }) { + const [reset, a, b, increment] = target.querySelectorAll('button'); + + a.click(); + + // 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

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

84

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

84

+ ` + ); + + b.click(); + 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 0000000000..2c83e1d23d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/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-derived-module/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/async-derived-module/state.svelte.js new file mode 100644 index 0000000000..a53fbb8c6f --- /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 0000000000..a90a9dedf7 --- /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 0000000000..016c311f98 --- /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 0000000000..2d5ddca4db --- /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 0000000000..b59fd7c08f --- /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 0000000000..7239643464 --- /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 0000000000..1404ae0299 --- /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 0000000000..54aa68eeb2 --- /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 0000000000..eddcf2b749 --- /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 0000000000..43d3a0f876 --- /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 0000000000..e2f8263780 --- /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 0000000000..50aa055130 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each/_config.js @@ -0,0 +1,48 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

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

a

b

c

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

a

b

c

` + ); + + defg.click(); + 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 0000000000..8e4412811a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each/main.svelte @@ -0,0 +1,17 @@ + + + + + + + + {#each await deferred.promise as item} +

{item}

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

pending

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

0

+

0

+

0

+

pending: 0

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

0

+

0

+

0

+

pending: 3

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

0

+

0

+

0

+

pending: 2

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

0

+

0

+

0

+

pending: 1

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

1

+

1

+

1

+

pending: 0

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-pending/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-pending/main.svelte new file mode 100644 index 0000000000..89cead2cc6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-pending/main.svelte @@ -0,0 +1,32 @@ + + + + + + +

{await push(value)}

+

{await push(value)}

+

{await push(value)}

+ +

pending: {$effect.pending()}

+ + {#snippet pending()} +

loading...

+ {/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 0000000000..2679785cff --- /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 0000000000..a49a5c9540 --- /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 0000000000..1613bf9c61 --- /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 0000000000..d5246d330e --- /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 0000000000..dfbd238eeb --- /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 0000000000..9af5bbaa16 --- /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 0000000000..3a66ea709f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js @@ -0,0 +1,56 @@ +import { 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 0000000000..42536ab02a --- /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 0000000000..f5b1f3d2c4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-html-tag/_config.js @@ -0,0 +1,51 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

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

hello

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

hello

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

goodbye

+ ` + ); + } +}); 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 0000000000..980bb16d5c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-html-tag/main.svelte @@ -0,0 +1,15 @@ + + + + + + + +

{@html await deferred.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 0000000000..3cd67952c3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if/_config.js @@ -0,0 +1,31 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending

`, + + 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 0000000000..21a4cbef97 --- /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 0000000000..e7e5db3dd8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js @@ -0,0 +1,46 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target }) { + const [reset, one, two] = target.querySelectorAll('button'); + + const html = ` + + + +

hello

+ `; + + one.click(); + await tick(); + assert.htmlEqual(target.innerHTML, html); + + const h1 = target.querySelector('h1'); + + reset.click(); + await tick(); + assert.htmlEqual(target.innerHTML, html); + + one.click(); + await tick(); + assert.htmlEqual(target.innerHTML, html); + assert.equal(target.querySelector('h1'), h1); + + reset.click(); + await tick(); + assert.htmlEqual(target.innerHTML, html); + + two.click(); + await tick(); + assert.htmlEqual(target.innerHTML, html); + 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 0000000000..5fbdbd47d0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-key/main.svelte @@ -0,0 +1,17 @@ + + + + + + + + {#key await deferred.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 0000000000..cb8e0cfca9 --- /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 0000000000..432eed976c --- /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 0000000000..5e522ebdb5 --- /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 0000000000..cc82db0d75 --- /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 0000000000..546494f4c3 --- /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 0000000000..172b44e6e3 --- /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 0000000000..f6b0afe98c --- /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-ownership-validation/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/Child.svelte new file mode 100644 index 0000000000..d4d5cf7554 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/Child.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/_config.js b/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/_config.js new file mode 100644 index 0000000000..167eee8488 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/_config.js @@ -0,0 +1,22 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + async test({ assert, target, warnings }) { + await tick(); + + const [button] = target.querySelectorAll('button'); + + button.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, ''); + assert.deepEqual(warnings, [ + 'Mutating unbound props (`object`, at Child.svelte:7:23) is strongly discouraged. Consider using `bind:object={...}` in main.svelte (or using a callback) instead' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/main.svelte new file mode 100644 index 0000000000..ae6b43cbb1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/main.svelte @@ -0,0 +1,13 @@ + + + + + + {#snippet pending()} +

loading...

+ {/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 0000000000..85d212b1a8 --- /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 0000000000..66690c120c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js @@ -0,0 +1,51 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

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

hello

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

hello

+ ` + ); + + again.click(); + 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 0000000000..38388607ce --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/main.svelte @@ -0,0 +1,17 @@ + + + + + + + + + + {#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 0000000000..16318a3b44 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js @@ -0,0 +1,27 @@ +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, + '

6

6

' + ); + + 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 0000000000..03596ce051 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte @@ -0,0 +1,23 @@ + + + + + + + +

{await a_plus_b_plus_c()}

+

{await a + await b + await c}

+ + {#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 0000000000..17bb79af08 --- /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 0000000000..b1bb291e2e --- /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 0000000000..ebbe642860 --- /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 0000000000..bf5fdf9ed3 --- /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 0000000000..6f3473f592 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js @@ -0,0 +1,51 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

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

hello

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

hello

+ ` + ); + + wheee.click(); + 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 0000000000..b59bc319d8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-render-tag/main.svelte @@ -0,0 +1,19 @@ + + + + + + +{#snippet hello(message)} +

{message}

+{/snippet} + + + {@render hello(await deferred.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 0000000000..c32f869f63 --- /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 0000000000..3d54d24259 --- /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 0000000000..badd60746d --- /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 0000000000..8276c5be41 --- /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 0000000000..884b27d865 --- /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 0000000000..eeefffbee6 --- /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 0000000000..dc25be10c8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js @@ -0,0 +1,51 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

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

hello

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

hello

+ ` + ); + + h2.click(); + 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 0000000000..f8165784dc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte @@ -0,0 +1,15 @@ + + + + + + + + hello + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/_config.js new file mode 100644 index 0000000000..06437d2e31 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/_config.js @@ -0,0 +1,56 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [a, b, update] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

a

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

b

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

b

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

a

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/main.svelte new file mode 100644 index 0000000000..5fca286a78 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/main.svelte @@ -0,0 +1,26 @@ + + + + + + + + + {#if condition} +

a

+ {:else} +

b

+ {/if} + + {#snippet 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 0000000000..7ad618f130 --- /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 0000000000..73c9b50a69 --- /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 0000000000..d111ce6fe3 --- /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 0000000000..7ad618f130 --- /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 0000000000..b2200201c6 --- /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 0000000000..78ad3ba04a --- /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 0000000000..e9ccbba2b6 --- /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 0000000000..e0619a1fe4 --- /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 0000000000..e2c8b851c1 --- /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 0000000000..86af9bb07e --- /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 0000000000..837dd976e2 --- /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 0000000000..764007e082 --- /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/binding-update-while-focused/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused/_config.js new file mode 100644 index 0000000000..1d2e6dd470 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused/_config.js @@ -0,0 +1,40 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + + async test({ assert, target }) { + const [input] = target.querySelectorAll('input'); + + flushSync(() => { + input.focus(); + input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })); + }); + assert.equal(input.value, '2'); + assert.htmlEqual( + target.innerHTML, + ` + +

value = 2

+ ` + ); + + flushSync(() => { + input.focus(); + input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); + }); + assert.equal(input.value, '1'); + assert.htmlEqual( + target.innerHTML, + ` + +

value = 1

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused/main.svelte b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused/main.svelte new file mode 100644 index 0000000000..4cc174e404 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused/main.svelte @@ -0,0 +1,16 @@ + + + + +

value = {value}

diff --git a/packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/Component.svelte b/packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/Component.svelte new file mode 100644 index 0000000000..b184e0e9b3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/Component.svelte @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/_config.js b/packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/_config.js new file mode 100644 index 0000000000..6eb4b06712 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const btn = target.querySelector('button'); + flushSync(() => { + btn?.click(); + }); + assert.deepEqual(logs, ['abort']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/main.svelte b/packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/main.svelte new file mode 100644 index 0000000000..99ea3fabb4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/main.svelte @@ -0,0 +1,10 @@ + + + +{#if show} + +{/if} 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 e55733c148..416f61d23a 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 2cdcfdfb58..da38374f82 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/effect-loop-infinite/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js new file mode 100644 index 0000000000..400495050c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js @@ -0,0 +1,21 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + + compileOptions: { + dev: true + }, + + test({ assert, errors }) { + const [button] = document.querySelectorAll('button'); + + try { + flushSync(() => button.click()); + } catch (e) { + assert.equal(errors.length, 1); // for whatever reason we can't get the name which should be UpdatedAtError + assert.ok(/** @type {Error} */ (e).message.startsWith('effect_update_depth_exceeded')); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/main.svelte new file mode 100644 index 0000000000..ddb91a90ad --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/main.svelte @@ -0,0 +1,12 @@ + + + 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 e092d0e7c7..f34668ec45 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/error-boundary-20/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-20/_config.js index ccff614ade..e3a3b0c7d7 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-20/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-20/_config.js @@ -2,12 +2,14 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ - test({ assert, target }) { + test({ assert, target, errors }) { let btn = target.querySelector('button'); btn?.click(); flushSync(); + assert.equal(errors.length, 1); + assert.htmlEqual(target.innerHTML, `
An error occurred!
`); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-3/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-3/_config.js index 040e13676e..06da8f667c 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-3/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-3/_config.js @@ -9,6 +9,6 @@ export default test({ flushSync(); assert.deepEqual(logs, ['error caught']); - assert.htmlEqual(target.innerHTML, `
Fallback!
`); + assert.htmlEqual(target.innerHTML, `
oh no!
`); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-3/main.svelte index bad84666c0..bc7fe072c4 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-3/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-3/main.svelte @@ -1,6 +1,6 @@ + + reset()}> + {must_throw ? throw_error() : 'normal content'} + + {#snippet failed()} +
err
+ {/snippet} +
+ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/_config.js new file mode 100644 index 0000000000..687961e721 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/_config.js @@ -0,0 +1,28 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + normal content + + `, + + async test({ assert, target, warnings }) { + const [btn] = target.querySelectorAll('button'); + + flushSync(() => btn.click()); + assert.htmlEqual(target.innerHTML, `
err
`); + assert.deepEqual(warnings, []); + + flushSync(() => btn.click()); + assert.htmlEqual(target.innerHTML, `normal content `); + assert.deepEqual(warnings, []); + + flushSync(() => btn.click()); + assert.htmlEqual(target.innerHTML, `
err
`); + + assert.deepEqual(warnings, [ + 'A `` `reset` function only resets the boundary the first time it is called' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/main.svelte new file mode 100644 index 0000000000..c1462eaf09 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/main.svelte @@ -0,0 +1,26 @@ + + + + (reset = fn)}> + {must_throw ? throw_error() : 'normal content'} + + {#snippet failed()} +
err
+ {/snippet} +
+
+ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-with-error/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-with-error/_config.js new file mode 100644 index 0000000000..844e6981bd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-with-error/_config.js @@ -0,0 +1,27 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target, warnings }) { + const [toggle] = target.querySelectorAll('button'); + + flushSync(() => toggle.click()); + assert.htmlEqual( + target.innerHTML, + `

yikes!

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

yikes!

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

hello!

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-with-error/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-with-error/main.svelte new file mode 100644 index 0000000000..91479a631a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-with-error/main.svelte @@ -0,0 +1,20 @@ + + + + + +

{must_throw ? throw_error() : 'hello!'}

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

{error.message}

+ + {/snippet} +
+ + diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js index 0e6b12508b..49f1b5de41 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js @@ -13,17 +13,6 @@ export default test({ button?.click(); }); - assert.deepEqual(logs, [ - 'init', - [1, 2, 3, 7], - 'update', - [2, 2, 3, 7], - 'update', - [2, 3, 3, 7], - 'update', - [2, 3, 7, 7], - 'update', - [2, 3, 7] - ]); + assert.deepEqual(logs, ['init', [1, 2, 3, 7], 'update', [2, 3, 7]]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-exception/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-exception/_config.js index e155ff236a..83e0eb9443 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-exception/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-exception/_config.js @@ -11,7 +11,7 @@ export default test({ b1?.click(); flushSync(); - assert.ok(errors.length > 0); + assert.equal(errors.length, 0); assert.deepEqual(logs, ['init', 'a', 'init', 'b']); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace-store/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-trace-store/_config.js new file mode 100644 index 0000000000..4e16c0cf2a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-store/_config.js @@ -0,0 +1,30 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; +import { normalise_trace_logs } from '../../../helpers.js'; + +export default test({ + compileOptions: { + dev: true + }, + + test({ assert, target, logs }) { + assert.deepEqual(normalise_trace_logs(logs), [ + { log: 'effect' }, + { log: 'store', highlighted: true }, + { log: '$count', highlighted: false }, + { log: 0 } + ]); + + logs.length = 0; + + const [button] = target.querySelectorAll('button'); + flushSync(() => button.click()); + + assert.deepEqual(normalise_trace_logs(logs), [ + { log: 'effect' }, + { log: 'store', highlighted: true }, + { log: '$count', highlighted: false }, + { log: 1 } + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace-store/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-trace-store/main.svelte new file mode 100644 index 0000000000..af57513365 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-store/main.svelte @@ -0,0 +1,14 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/props-derived-teardown/Teardown.svelte b/packages/svelte/tests/runtime-runes/samples/props-derived-teardown/Teardown.svelte new file mode 100644 index 0000000000..f71890d31e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-derived-teardown/Teardown.svelte @@ -0,0 +1,11 @@ + + +
teardown
\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/props-derived-teardown/_config.js b/packages/svelte/tests/runtime-runes/samples/props-derived-teardown/_config.js new file mode 100644 index 0000000000..c4e7d1307b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-derived-teardown/_config.js @@ -0,0 +1,30 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + assert.htmlEqual( + target.innerHTML, + ` + +
teardown
+
1
+
2
+
3
+ ` + ); + const [increment] = target.querySelectorAll('button'); + + increment.click(); + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` + +
1
+
3
+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/props-derived-teardown/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-derived-teardown/main.svelte new file mode 100644 index 0000000000..ff9e19b2c6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-derived-teardown/main.svelte @@ -0,0 +1,25 @@ + + + + +{#if show} + +{/if} +{#each test.ids as id} +
{id}
+{/each} 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 0000000000..122a316726 --- /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 0000000000..1bf7e71176 --- /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 0000000000..65d0e623cf --- /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 0000000000..4569f42a73 --- /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 0000000000..0c3b6c3a0f --- /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/store-inside-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/store-inside-derived/_config.js new file mode 100644 index 0000000000..de078b1e75 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/store-inside-derived/_config.js @@ -0,0 +1,45 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test: ({ assert, target }) => { + const [loading, increment] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` +
$value: 0
+
valueFromStore.current: 0
+
valueDerivedCurrent: 0
+ + + ` + ); + + loading.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` +
$value: Loading...
+
valueFromStore.current: Loading...
+
valueDerivedCurrent: Loading...
+ + + ` + ); + + increment.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` +
$value: 1
+
valueFromStore.current: 1
+
valueDerivedCurrent: 1
+ + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/store-inside-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/store-inside-derived/main.svelte new file mode 100644 index 0000000000..06d0a0d4b4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/store-inside-derived/main.svelte @@ -0,0 +1,36 @@ + + +
+ $value: {isLoading ? 'Loading...' : $value} +
+ +
+ valueFromStore.current: {isLoading ? 'Loading...' : valueFromStore.current} +
+ +
+ valueDerivedCurrent: {isLoading ? 'Loading...' : valueDerivedCurrent} +
+ + + + 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 339cec55c5..25414d4b47 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 18062b86fb..b728c3c0be 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 719c936df0..937324727b 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/snapshot/samples/await-block-scope/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js index 4b6e32d58e..cc2628c852 100644 --- a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js @@ -8,7 +8,7 @@ export default function Await_block_scope($$payload) { counter.count += 1; } - $$payload.out += ` `; + $$payload.out.push(` `); $.await($$payload, promise, () => {}, (counter) => {}); - $$payload.out += ` ${$.escape(counter.count)}`; + $$payload.out.push(` ${$.escape(counter.count)}`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js index e2c0ee29a5..c0db7d2fd5 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js @@ -2,7 +2,7 @@ import * as $ from 'svelte/internal/server'; import TextInput from './Child.svelte'; function snippet($$payload) { - $$payload.out += `Something`; + $$payload.out.push(`Something`); } export default function Bind_component_snippet($$payload) { @@ -23,7 +23,7 @@ export default function Bind_component_snippet($$payload) { } }); - $$payload.out += ` value: ${$.escape(value)}`; + $$payload.out.push(` value: ${$.escape(value)}`); } do { diff --git a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js new file mode 100644 index 0000000000..0d95d8d335 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js @@ -0,0 +1,28 @@ +import 'svelte/internal/disclose-version'; +import 'svelte/internal/flags/legacy'; +import * as $ from 'svelte/internal/client'; + +var on_click = (e) => { + const index = Number(e.currentTarget.dataset.index); + + console.log(index); +}; + +var root_1 = $.from_html(``); + +export default function Delegated_locally_declared_shadowed($$anchor) { + var fragment = $.comment(); + var node = $.first_child(fragment); + + $.each(node, 0, () => ({ length: 1 }), $.index, ($$anchor, $$item, index) => { + var button = root_1(); + + $.set_attribute(button, 'data-index', index); + button.__click = [on_click]; + $.append($$anchor, button); + }); + + $.append($$anchor, fragment); +} + +$.delegate(['click']); \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js new file mode 100644 index 0000000000..ac3dfcd2cb --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js @@ -0,0 +1,13 @@ +import * as $ from 'svelte/internal/server'; + +export default function Delegated_locally_declared_shadowed($$payload) { + const each_array = $.ensure_array_like({ length: 1 }); + + $$payload.out.push(``); + + for (let index = 0, $$length = each_array.length; index < $$length; index++) { + $$payload.out.push(``); + } + + $$payload.out.push(``); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/index.svelte b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/index.svelte new file mode 100644 index 0000000000..d870a6b270 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/index.svelte @@ -0,0 +1,12 @@ + + +{#each { length: 1 }, index} + +{/each} diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js index cf731d8187..9c837d4e1d 100644 --- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js @@ -6,5 +6,5 @@ export default function Main($$payload) { let y = () => 'test'; - $$payload.out += ` `; + $$payload.out.push(` `); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js index 3431e36833..8fa0c5f28c 100644 --- a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js @@ -3,11 +3,11 @@ import * as $ from 'svelte/internal/server'; export default function Each_index_non_null($$payload) { const each_array = $.ensure_array_like(Array(10)); - $$payload.out += ``; + $$payload.out.push(``); for (let i = 0, $$length = each_array.length; i < $$length; i++) { - $$payload.out += `

index: ${$.escape(i)}

`; + $$payload.out.push(`

index: ${$.escape(i)}

`); } - $$payload.out += ``; + $$payload.out.push(``); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js index 4386c22ebe..6dbe8130da 100644 --- a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js @@ -3,13 +3,13 @@ import * as $ from 'svelte/internal/server'; export default function Each_string_template($$payload) { const each_array = $.ensure_array_like(['foo', 'bar', 'baz']); - $$payload.out += ``; + $$payload.out.push(``); for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { let thing = each_array[$$index]; - $$payload.out += `${$.escape(thing)}, `; + $$payload.out.push(`${$.escape(thing)}, `); } - $$payload.out += ``; + $$payload.out.push(``); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js index 7d37abd97b..ce4f09ed1d 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js @@ -15,7 +15,7 @@ export default function Function_prop_no_getter($$payload) { onmouseenter: () => count = plusOne(count), children: ($$payload) => { - $$payload.out += `clicks: ${$.escape(count)}`; + $$payload.out.push(`clicks: ${$.escape(count)}`); }, $$slots: { default: true } diff --git a/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js index dc49c0c213..b1a0a5f9e6 100644 --- a/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js @@ -1,5 +1,5 @@ import * as $ from 'svelte/internal/server'; export default function Functional_templating($$payload) { - $$payload.out += `

hello

child element

another child element

`; + $$payload.out.push(`

hello

child element

another child element

`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/hello-world/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/hello-world/_expected/server/index.svelte.js index 8766fb1300..30f6d6b74a 100644 --- a/packages/svelte/tests/snapshot/samples/hello-world/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/hello-world/_expected/server/index.svelte.js @@ -1,5 +1,5 @@ import * as $ from 'svelte/internal/server'; export default function Hello_world($$payload) { - $$payload.out += `

hello world

`; + $$payload.out.push(`

hello world

`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/hmr/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/hmr/_expected/server/index.svelte.js index 959e0a403e..ea1d12c83b 100644 --- a/packages/svelte/tests/snapshot/samples/hmr/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/hmr/_expected/server/index.svelte.js @@ -1,5 +1,5 @@ import * as $ from 'svelte/internal/server'; export default function Hmr($$payload) { - $$payload.out += `

hello world

`; + $$payload.out.push(`

hello world

`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js index 3b23befcd4..18e01b4f72 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js @@ -4,5 +4,5 @@ export default function Nullish_coallescence_omittance($$payload) { let name = 'world'; let count = 0; - $$payload.out += `

Hello, world!

123

Hello, world

`; + $$payload.out.push(`

Hello, world!

123

Hello, world

`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js index 9457378c0d..29b0d0d594 100644 --- a/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js @@ -1,7 +1,7 @@ import * as $ from 'svelte/internal/server'; export default function Purity($$payload) { - $$payload.out += `

0

${$.escape(location.href)}

`; + $$payload.out.push(`

0

${$.escape(location.href)}

`); Child($$payload, { prop: encodeURIComponent('hello') }); - $$payload.out += ``; + $$payload.out.push(``); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js index 0532ec5aa9..bad475ec86 100644 --- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js @@ -3,5 +3,5 @@ import * as $ from 'svelte/internal/server'; export default function Skip_static_subtree($$payload, $$props) { let { title, content } = $$props; - $$payload.out += `

${$.escape(title)}

we don't need to traverse these nodes

or

these

ones

${$.html(content)}

these

trailing

nodes

can

be

completely

ignored

`; + $$payload.out.push(`

${$.escape(title)}

we don't need to traverse these nodes

or

these

ones

${$.html(content)}

these

trailing

nodes

can

be

completely

ignored

`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js index f814dd4f84..c2736b0f43 100644 --- a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js @@ -11,5 +11,5 @@ export default function State_proxy_literal($$payload) { tpl = ``; } - $$payload.out += ` `; + $$payload.out.push(` `); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js index 6f019647f5..f7dc586026 100644 --- a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js @@ -12,5 +12,5 @@ export default function Text_nodes_deriveds($$payload) { return count2; } - $$payload.out += `

${$.escape(text1())}${$.escape(text2())}

`; + $$payload.out.push(`

${$.escape(text1())}${$.escape(text2())}

`); } \ No newline at end of file diff --git a/packages/svelte/tests/store/test.ts b/packages/svelte/tests/store/test.ts index 77cecca7e5..ecb22c1be6 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 0ae06e727f..bbd252b8e1 100644 --- a/packages/svelte/tests/suite.ts +++ b/packages/svelte/tests/suite.ts @@ -20,6 +20,9 @@ const filter = process.env.FILTER ) : /./; +// this defaults to 10, which is too low for some of our tests +Error.stackTraceLimit = 100; + export function suite(fn: (config: Test, test_dir: string) => void) { return { test: (config: Test) => config, @@ -35,7 +38,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 +49,11 @@ export function suite_with_variants + diff --git a/packages/svelte/tests/validator/samples/a11y-no-autofocus/input.svelte b/packages/svelte/tests/validator/samples/a11y-no-autofocus/input.svelte index 769dbe8c5b..7b1dccd1e8 100644 --- a/packages/svelte/tests/validator/samples/a11y-no-autofocus/input.svelte +++ b/packages/svelte/tests/validator/samples/a11y-no-autofocus/input.svelte @@ -1 +1,6 @@ -
\ No newline at end of file +
+ + + + + diff --git a/packages/svelte/tests/validator/samples/default-export-indirect/errors.json b/packages/svelte/tests/validator/samples/default-export-indirect/errors.json new file mode 100644 index 0000000000..6a8b2b3c13 --- /dev/null +++ b/packages/svelte/tests/validator/samples/default-export-indirect/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "module_illegal_default_export", + "message": "A component cannot have a default export", + "start": { + "line": 3, + "column": 4 + }, + "end": { + "line": 3, + "column": 32 + } + } +] diff --git a/packages/svelte/tests/validator/samples/default-export-indirect/input.svelte b/packages/svelte/tests/validator/samples/default-export-indirect/input.svelte new file mode 100644 index 0000000000..72150edab1 --- /dev/null +++ b/packages/svelte/tests/validator/samples/default-export-indirect/input.svelte @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 432171ae0d..9ea45af7e6 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -434,6 +434,11 @@ declare module 'svelte' { * @deprecated Use [`$effect`](https://svelte.dev/docs/svelte/$effect) instead * */ export function afterUpdate(fn: () => 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,30 @@ 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 + * @since 5.36 + */ + 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 +1117,17 @@ 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 + * @since 5.36 + */ + experimental?: { + /** + * Allow `await` keyword in deriveds, template expressions, and the top level of components + * @since 5.36 + */ + async?: boolean; + }; } /** * - `html` — the default, for e.g. `
` or `` @@ -2318,10 +2335,13 @@ declare module 'svelte/reactivity' { constructor(query: string, fallback?: boolean | undefined); } /** - * Returns a `subscribe` function that, if called in an effect (including expressions in the template), - * calls its `start` callback with an `update` function. Whenever `update` is called, the effect re-runs. + * Returns a `subscribe` function that integrates external event-based systems with Svelte's reactivity. + * It's particularly useful for integrating with web APIs like `MediaQuery`, `IntersectionObserver`, or `WebSocket`. + * + * If `subscribe` is called inside an effect (including indirectly, for example inside a getter), + * the `start` callback will be called with an `update` function. Whenever `update` is called, the effect re-runs. * - * If `start` returns a function, it will be called when the effect is destroyed. + * If `start` returns a cleanup function, it will be called when the effect is destroyed. * * If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects * are active, and the returned teardown function will only be called when all effects are destroyed. @@ -2349,6 +2369,7 @@ declare module 'svelte/reactivity' { * } * * get current() { + * // This makes the getter reactive, if read in an effect * this.#subscribe(); * * // Return the current state of the query, whether or not we're in an effect @@ -3021,6 +3042,17 @@ 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 + * @since 5.36 + */ + experimental?: { + /** + * Allow `await` keyword in deriveds, template expressions, and the top level of components + * @since 5.36 + */ + async?: boolean; + }; } /** * - `html` — the default, for e.g. `
` or `` @@ -3297,6 +3329,13 @@ declare namespace $effect { */ export function pre(fn: () => void | (() => void)): void; + /** + * Returns the number of promises that are pending in the current boundary, not including child boundaries. + * + * https://svelte.dev/docs/svelte/$effect#$effect.pending + */ + export function pending(): number; + /** * The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template. * @@ -3330,13 +3369,13 @@ declare namespace $effect { * let count = $state(0); * * const cleanup = $effect.root(() => { - * $effect(() => { - * console.log(count); - * }) + * $effect(() => { + * console.log(count); + * }) * - * return () => { - * console.log('effect root cleanup'); - * } + * return () => { + * console.log('effect root cleanup'); + * } * }); * * diff --git a/playgrounds/sandbox/Wrapper.svelte b/playgrounds/sandbox/Wrapper.svelte new file mode 100644 index 0000000000..1fe82c1716 --- /dev/null +++ b/playgrounds/sandbox/Wrapper.svelte @@ -0,0 +1,9 @@ + + + + + + {#snippet pending()}{/snippet} + diff --git a/playgrounds/sandbox/index.html b/playgrounds/sandbox/index.html index 845538abf0..639409b877 100644 --- a/playgrounds/sandbox/index.html +++ b/playgrounds/sandbox/index.html @@ -12,7 +12,13 @@