diff --git a/packages/kit/test/apps/async/.env b/packages/kit/test/apps/async/.env
new file mode 100644
index 000000000000..b12773447030
--- /dev/null
+++ b/packages/kit/test/apps/async/.env
@@ -0,0 +1,5 @@
+PRIVATE_STATIC="accessible to server-side code/replaced at build time"
+PRIVATE_DYNAMIC="accessible to server-side code/evaluated at run time"
+
+PUBLIC_STATIC="accessible anywhere/replaced at build time"
+PUBLIC_DYNAMIC="accessible anywhere/evaluated at run time"
\ No newline at end of file
diff --git a/packages/kit/test/apps/async/.gitignore b/packages/kit/test/apps/async/.gitignore
new file mode 100644
index 000000000000..d5868cb0ca8a
--- /dev/null
+++ b/packages/kit/test/apps/async/.gitignore
@@ -0,0 +1 @@
+!/.env
\ No newline at end of file
diff --git a/packages/kit/test/apps/async/README.md b/packages/kit/test/apps/async/README.md
new file mode 100644
index 000000000000..75842c404de0
--- /dev/null
+++ b/packages/kit/test/apps/async/README.md
@@ -0,0 +1,38 @@
+# sv
+
+Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
+
+## Creating a project
+
+If you're seeing this, you've probably already done this step. Congrats!
+
+```sh
+# create a new project in the current directory
+npx sv create
+
+# create a new project in my-app
+npx sv create my-app
+```
+
+## Developing
+
+Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
+
+```sh
+npm run dev
+
+# or start the server and open the app in a new browser tab
+npm run dev -- --open
+```
+
+## Building
+
+To create a production version of your app:
+
+```sh
+npm run build
+```
+
+You can preview the production build with `npm run preview`.
+
+> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
diff --git a/packages/kit/test/apps/async/package.json b/packages/kit/test/apps/async/package.json
new file mode 100644
index 000000000000..7afccae4847a
--- /dev/null
+++ b/packages/kit/test/apps/async/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "test-async",
+ "private": true,
+ "version": "0.0.1",
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "preview": "vite preview",
+ "prepare": "svelte-kit sync || echo ''",
+ "check": "svelte-kit sync && tsc && svelte-check",
+ "test": "pnpm test:dev && pnpm test:build",
+ "test:dev": "DEV=true playwright test",
+ "test:build": "playwright test"
+ },
+ "devDependencies": {
+ "@sveltejs/kit": "workspace:^",
+ "@sveltejs/vite-plugin-svelte": "catalog:",
+ "svelte": "catalog:",
+ "svelte-check": "catalog:",
+ "typescript": "^5.5.4",
+ "valibot": "catalog:",
+ "vite": "catalog:"
+ }
+}
diff --git a/packages/kit/test/apps/async/playwright.config.js b/packages/kit/test/apps/async/playwright.config.js
new file mode 100644
index 000000000000..f3ef088b1b6f
--- /dev/null
+++ b/packages/kit/test/apps/async/playwright.config.js
@@ -0,0 +1,11 @@
+import process from 'node:process';
+import { config } from '../../utils.js';
+import { defineConfig } from '@playwright/test';
+
+export default defineConfig({
+ ...config,
+ webServer: {
+ command: process.env.DEV ? `pnpm dev` : `pnpm build && pnpm preview`,
+ port: process.env.DEV ? 5173 : 4173
+ }
+});
diff --git a/packages/kit/test/apps/async/src/app.html b/packages/kit/test/apps/async/src/app.html
new file mode 100644
index 000000000000..f273cc58f7eb
--- /dev/null
+++ b/packages/kit/test/apps/async/src/app.html
@@ -0,0 +1,11 @@
+
+
+
@@ -110,4 +109,8 @@
-/remote/event
+
+/remote/event
diff --git a/packages/kit/test/apps/basics/src/routes/remote/accessing-env.remote.js b/packages/kit/test/apps/async/src/routes/remote/accessing-env.remote.js
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/accessing-env.remote.js
rename to packages/kit/test/apps/async/src/routes/remote/accessing-env.remote.js
diff --git a/packages/kit/test/apps/basics/src/routes/remote/batch/+page.js b/packages/kit/test/apps/async/src/routes/remote/batch/+page.js
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/batch/+page.js
rename to packages/kit/test/apps/async/src/routes/remote/batch/+page.js
diff --git a/packages/kit/test/apps/basics/src/routes/remote/batch/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/batch/+page.svelte
similarity index 73%
rename from packages/kit/test/apps/basics/src/routes/remote/batch/+page.svelte
rename to packages/kit/test/apps/async/src/routes/remote/batch/+page.svelte
index 0d801a2dde21..9c4ab7aa73cc 100644
--- a/packages/kit/test/apps/basics/src/routes/remote/batch/+page.svelte
+++ b/packages/kit/test/apps/async/src/routes/remote/batch/+page.svelte
@@ -1,4 +1,4 @@
-
+
+
route: {event.route.id}
+
pathname: {event.url.pathname}
diff --git a/packages/kit/test/apps/basics/src/routes/remote/event/data.remote.ts b/packages/kit/test/apps/async/src/routes/remote/event/data.remote.ts
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/event/data.remote.ts
rename to packages/kit/test/apps/async/src/routes/remote/event/data.remote.ts
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/[test_name]/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/form/[test_name]/+page.svelte
similarity index 95%
rename from packages/kit/test/apps/basics/src/routes/remote/form/[test_name]/+page.svelte
rename to packages/kit/test/apps/async/src/routes/remote/form/[test_name]/+page.svelte
index 9607ab161ce5..485cfab34a11 100644
--- a/packages/kit/test/apps/basics/src/routes/remote/form/[test_name]/+page.svelte
+++ b/packages/kit/test/apps/async/src/routes/remote/form/[test_name]/+page.svelte
@@ -16,10 +16,7 @@
message.current: {message.current}
-
-{#await message then m}
-
await get_message(): {m}
-{/await}
+
await get_message(): {await message}
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/[test_name]/form.remote.ts b/packages/kit/test/apps/async/src/routes/remote/form/[test_name]/form.remote.ts
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/form/[test_name]/form.remote.ts
rename to packages/kit/test/apps/async/src/routes/remote/form/[test_name]/form.remote.ts
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/file-upload/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/form/file-upload/+page.svelte
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/form/file-upload/+page.svelte
rename to packages/kit/test/apps/async/src/routes/remote/form/file-upload/+page.svelte
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/file-upload/form.remote.ts b/packages/kit/test/apps/async/src/routes/remote/form/file-upload/form.remote.ts
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/form/file-upload/form.remote.ts
rename to packages/kit/test/apps/async/src/routes/remote/form/file-upload/form.remote.ts
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/imperative/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/form/imperative/+page.svelte
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/form/imperative/+page.svelte
rename to packages/kit/test/apps/async/src/routes/remote/form/imperative/+page.svelte
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/preflight-only/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/form/preflight-only/+page.svelte
similarity index 82%
rename from packages/kit/test/apps/basics/src/routes/remote/form/preflight-only/+page.svelte
rename to packages/kit/test/apps/async/src/routes/remote/form/preflight-only/+page.svelte
index 6de838b61e63..0b2cb866102e 100644
--- a/packages/kit/test/apps/basics/src/routes/remote/form/preflight-only/+page.svelte
+++ b/packages/kit/test/apps/async/src/routes/remote/form/preflight-only/+page.svelte
@@ -11,12 +11,9 @@
});
-
-{#await data then { a, b, c }}
-
a: {a}
-
b: {b}
-
c: {c}
-{/await}
+
a: {(await data).a}
+
b: {(await data).b}
+
c: {(await data).c}
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/preflight-only/form.remote.ts b/packages/kit/test/apps/async/src/routes/remote/form/preflight-only/form.remote.ts
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/form/preflight-only/form.remote.ts
rename to packages/kit/test/apps/async/src/routes/remote/form/preflight-only/form.remote.ts
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/preflight/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/form/preflight/+page.svelte
similarity index 91%
rename from packages/kit/test/apps/basics/src/routes/remote/form/preflight/+page.svelte
rename to packages/kit/test/apps/async/src/routes/remote/form/preflight/+page.svelte
index 38d1248d4515..85269d87cff4 100644
--- a/packages/kit/test/apps/basics/src/routes/remote/form/preflight/+page.svelte
+++ b/packages/kit/test/apps/async/src/routes/remote/form/preflight/+page.svelte
@@ -13,10 +13,7 @@
number.current: {number.current}
-
-{#await number then n}
-
await get_number(): {n}
-{/await}
+
await get_number(): {await number}
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/preflight/form.remote.ts b/packages/kit/test/apps/async/src/routes/remote/form/preflight/form.remote.ts
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/form/preflight/form.remote.ts
rename to packages/kit/test/apps/async/src/routes/remote/form/preflight/form.remote.ts
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/select-untouched/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/form/select-untouched/+page.svelte
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/form/select-untouched/+page.svelte
rename to packages/kit/test/apps/async/src/routes/remote/form/select-untouched/+page.svelte
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/select-untouched/form.remote.ts b/packages/kit/test/apps/async/src/routes/remote/form/select-untouched/form.remote.ts
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/form/select-untouched/form.remote.ts
rename to packages/kit/test/apps/async/src/routes/remote/form/select-untouched/form.remote.ts
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/submitter/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/form/submitter/+page.svelte
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/form/submitter/+page.svelte
rename to packages/kit/test/apps/async/src/routes/remote/form/submitter/+page.svelte
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/submitter/form.remote.ts b/packages/kit/test/apps/async/src/routes/remote/form/submitter/form.remote.ts
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/form/submitter/form.remote.ts
rename to packages/kit/test/apps/async/src/routes/remote/form/submitter/form.remote.ts
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/underscore/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/form/underscore/+page.svelte
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/form/underscore/+page.svelte
rename to packages/kit/test/apps/async/src/routes/remote/form/underscore/+page.svelte
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/underscore/form.remote.ts b/packages/kit/test/apps/async/src/routes/remote/form/underscore/form.remote.ts
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/form/underscore/form.remote.ts
rename to packages/kit/test/apps/async/src/routes/remote/form/underscore/form.remote.ts
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/validate/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/form/validate/+page.svelte
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/form/validate/+page.svelte
rename to packages/kit/test/apps/async/src/routes/remote/form/validate/+page.svelte
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/validate/form.remote.ts b/packages/kit/test/apps/async/src/routes/remote/form/validate/form.remote.ts
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/form/validate/form.remote.ts
rename to packages/kit/test/apps/async/src/routes/remote/form/validate/form.remote.ts
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/value/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/form/value/+page.svelte
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/form/value/+page.svelte
rename to packages/kit/test/apps/async/src/routes/remote/form/value/+page.svelte
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/value/value.remote.ts b/packages/kit/test/apps/async/src/routes/remote/form/value/value.remote.ts
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/form/value/value.remote.ts
rename to packages/kit/test/apps/async/src/routes/remote/form/value/value.remote.ts
diff --git a/packages/kit/test/apps/basics/src/routes/remote/prerender/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/prerender/+page.svelte
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/prerender/+page.svelte
rename to packages/kit/test/apps/async/src/routes/remote/prerender/+page.svelte
diff --git a/packages/kit/test/apps/basics/src/routes/remote/prerender/functions-only/+page.js b/packages/kit/test/apps/async/src/routes/remote/prerender/functions-only/+page.js
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/prerender/functions-only/+page.js
rename to packages/kit/test/apps/async/src/routes/remote/prerender/functions-only/+page.js
diff --git a/packages/kit/test/apps/basics/src/routes/remote/prerender/functions-only/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/prerender/functions-only/+page.svelte
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/prerender/functions-only/+page.svelte
rename to packages/kit/test/apps/async/src/routes/remote/prerender/functions-only/+page.svelte
diff --git a/packages/kit/test/apps/basics/src/routes/remote/prerender/prerender.remote.js b/packages/kit/test/apps/async/src/routes/remote/prerender/prerender.remote.js
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/prerender/prerender.remote.js
rename to packages/kit/test/apps/async/src/routes/remote/prerender/prerender.remote.js
diff --git a/packages/kit/test/apps/basics/src/routes/remote/prerender/test.txt b/packages/kit/test/apps/async/src/routes/remote/prerender/test.txt
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/prerender/test.txt
rename to packages/kit/test/apps/async/src/routes/remote/prerender/test.txt
diff --git a/packages/kit/test/apps/basics/src/routes/remote/prerender/whole-page/+page.js b/packages/kit/test/apps/async/src/routes/remote/prerender/whole-page/+page.js
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/prerender/whole-page/+page.js
rename to packages/kit/test/apps/async/src/routes/remote/prerender/whole-page/+page.js
diff --git a/packages/kit/test/apps/basics/src/routes/remote/prerender/whole-page/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/prerender/whole-page/+page.svelte
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/prerender/whole-page/+page.svelte
rename to packages/kit/test/apps/async/src/routes/remote/prerender/whole-page/+page.svelte
diff --git a/packages/kit/test/apps/basics/src/routes/remote/query-command.remote.js b/packages/kit/test/apps/async/src/routes/remote/query-command.remote.js
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/query-command.remote.js
rename to packages/kit/test/apps/async/src/routes/remote/query-command.remote.js
diff --git a/packages/kit/test/apps/async/src/routes/remote/query-non-exported/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/query-non-exported/+page.svelte
new file mode 100644
index 000000000000..d69be7f2c775
--- /dev/null
+++ b/packages/kit/test/apps/async/src/routes/remote/query-non-exported/+page.svelte
@@ -0,0 +1,5 @@
+
+
+
{await total()}
diff --git a/packages/kit/test/apps/basics/src/routes/remote/query-non-exported/data.remote.ts b/packages/kit/test/apps/async/src/routes/remote/query-non-exported/data.remote.ts
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/query-non-exported/data.remote.ts
rename to packages/kit/test/apps/async/src/routes/remote/query-non-exported/data.remote.ts
diff --git a/packages/kit/test/apps/async/src/routes/remote/query-redirect/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/query-redirect/+page.svelte
new file mode 100644
index 000000000000..efc180ae5bd7
--- /dev/null
+++ b/packages/kit/test/apps/async/src/routes/remote/query-redirect/+page.svelte
@@ -0,0 +1,4 @@
+
+from page
+from layout
diff --git a/packages/kit/test/apps/async/src/routes/remote/query-redirect/from-common-layout/+layout.svelte b/packages/kit/test/apps/async/src/routes/remote/query-redirect/from-common-layout/+layout.svelte
new file mode 100644
index 000000000000..1eebf5f10c74
--- /dev/null
+++ b/packages/kit/test/apps/async/src/routes/remote/query-redirect/from-common-layout/+layout.svelte
@@ -0,0 +1,12 @@
+
+
+
+ on page {await layoutRedirect(page.url.pathname)} (== {page.url.pathname})
+
+
+{@render children()}
diff --git a/packages/kit/test/apps/basics/src/routes/remote/query-redirect/from-common-layout/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/query-redirect/from-common-layout/+page.svelte
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/query-redirect/from-common-layout/+page.svelte
rename to packages/kit/test/apps/async/src/routes/remote/query-redirect/from-common-layout/+page.svelte
diff --git a/packages/kit/test/apps/basics/src/routes/remote/query-redirect/from-common-layout/redirected/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/query-redirect/from-common-layout/redirected/+page.svelte
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/query-redirect/from-common-layout/redirected/+page.svelte
rename to packages/kit/test/apps/async/src/routes/remote/query-redirect/from-common-layout/redirected/+page.svelte
diff --git a/packages/kit/test/apps/async/src/routes/remote/query-redirect/from-page/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/query-redirect/from-page/+page.svelte
new file mode 100644
index 000000000000..8cad914e4f02
--- /dev/null
+++ b/packages/kit/test/apps/async/src/routes/remote/query-redirect/from-page/+page.svelte
@@ -0,0 +1,8 @@
+
+
+
+ {await pageRedirect()}
+
should never see this
+
diff --git a/packages/kit/test/apps/basics/src/routes/remote/query-redirect/redirect.remote.js b/packages/kit/test/apps/async/src/routes/remote/query-redirect/redirect.remote.js
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/query-redirect/redirect.remote.js
rename to packages/kit/test/apps/async/src/routes/remote/query-redirect/redirect.remote.js
diff --git a/packages/kit/test/apps/basics/src/routes/remote/query-redirect/redirected/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/query-redirect/redirected/+page.svelte
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/query-redirect/redirected/+page.svelte
rename to packages/kit/test/apps/async/src/routes/remote/query-redirect/redirected/+page.svelte
diff --git a/packages/kit/test/apps/basics/src/routes/remote/server-endpoint/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/server-endpoint/+page.svelte
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/server-endpoint/+page.svelte
rename to packages/kit/test/apps/async/src/routes/remote/server-endpoint/+page.svelte
diff --git a/packages/kit/test/apps/basics/src/routes/remote/server-endpoint/api/+server.ts b/packages/kit/test/apps/async/src/routes/remote/server-endpoint/api/+server.ts
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/server-endpoint/api/+server.ts
rename to packages/kit/test/apps/async/src/routes/remote/server-endpoint/api/+server.ts
diff --git a/packages/kit/test/apps/basics/src/routes/remote/server-endpoint/internal.remote.ts b/packages/kit/test/apps/async/src/routes/remote/server-endpoint/internal.remote.ts
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/server-endpoint/internal.remote.ts
rename to packages/kit/test/apps/async/src/routes/remote/server-endpoint/internal.remote.ts
diff --git a/packages/kit/test/apps/async/src/routes/remote/transport/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/transport/+page.svelte
new file mode 100644
index 000000000000..43a5205219ac
--- /dev/null
+++ b/packages/kit/test/apps/async/src/routes/remote/transport/+page.svelte
@@ -0,0 +1,5 @@
+
+
+
{(await greeting()).bar()}
diff --git a/packages/kit/test/apps/basics/src/routes/remote/transport/data.remote.ts b/packages/kit/test/apps/async/src/routes/remote/transport/data.remote.ts
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/transport/data.remote.ts
rename to packages/kit/test/apps/async/src/routes/remote/transport/data.remote.ts
diff --git a/packages/kit/test/apps/basics/src/routes/remote/validation/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/validation/+page.svelte
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/validation/+page.svelte
rename to packages/kit/test/apps/async/src/routes/remote/validation/+page.svelte
diff --git a/packages/kit/test/apps/basics/src/routes/remote/validation/validation.remote.js b/packages/kit/test/apps/async/src/routes/remote/validation/validation.remote.js
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/remote/validation/validation.remote.js
rename to packages/kit/test/apps/async/src/routes/remote/validation/validation.remote.js
diff --git a/packages/kit/test/apps/async/static/robots.txt b/packages/kit/test/apps/async/static/robots.txt
new file mode 100644
index 000000000000..b6dd6670cbb0
--- /dev/null
+++ b/packages/kit/test/apps/async/static/robots.txt
@@ -0,0 +1,3 @@
+# allow crawling everything by default
+User-agent: *
+Disallow:
diff --git a/packages/kit/test/apps/async/svelte.config.js b/packages/kit/test/apps/async/svelte.config.js
new file mode 100644
index 000000000000..d5098a971446
--- /dev/null
+++ b/packages/kit/test/apps/async/svelte.config.js
@@ -0,0 +1,16 @@
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ compilerOptions: {
+ experimental: {
+ async: true
+ }
+ },
+
+ kit: {
+ experimental: {
+ remoteFunctions: true
+ }
+ }
+};
+
+export default config;
diff --git a/packages/kit/test/apps/async/test/client.test.js b/packages/kit/test/apps/async/test/client.test.js
new file mode 100644
index 000000000000..8266b5ac8338
--- /dev/null
+++ b/packages/kit/test/apps/async/test/client.test.js
@@ -0,0 +1,310 @@
+import process from 'node:process';
+import { expect } from '@playwright/test';
+import { test } from '../../../utils.js';
+
+test.skip(({ javaScriptEnabled }) => !javaScriptEnabled);
+
+test.describe('remote functions', () => {
+ test('preloading data works when the page component and server load both import a remote function', async ({
+ page
+ }) => {
+ test.skip(!process.env.DEV, 'remote functions are only analysed in dev mode');
+ await page.goto('/remote/dev');
+ await page.locator('a[href="/remote/dev/preload"]').hover();
+ await Promise.all([
+ page.waitForTimeout(100), // wait for preloading to start
+ page.waitForLoadState('networkidle') // wait for preloading to finish
+ ]);
+ await page.click('a[href="/remote/dev/preload"]');
+ await expect(page.locator('p')).toHaveText('foobar');
+ });
+});
+
+// have to run in serial because commands mutate in-memory data on the server (should fix this at some point)
+test.describe('remote function mutations', () => {
+ test.afterEach(async ({ page }) => {
+ if (page.url().endsWith('/remote')) {
+ await page.click('#reset-btn');
+ }
+ });
+
+ test('query.set works', async ({ page }) => {
+ await page.goto('/remote');
+ let request_count = 0;
+ page.on('request', (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0));
+
+ await page.click('#set-btn');
+ await expect(page.locator('#count-result')).toHaveText('999 / 999 (false)');
+ await page.waitForTimeout(100); // allow all requests to finish (in case there are query refreshes which shouldn't happen)
+ expect(request_count).toBe(0);
+ });
+
+ test('hydrated data is reused', async ({ page }) => {
+ let request_count = 0;
+ page.on('request', (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0));
+
+ await page.goto('/remote');
+ await expect(page.locator('#count-result')).toHaveText('0 / 0 (false)');
+ // only the calls in the template are done, not the one in the load function
+ expect(request_count).toBe(2);
+ });
+
+ test('command returns correct sum but does not refresh data by default', async ({ page }) => {
+ await page.goto('/remote');
+ await expect(page.locator('#count-result')).toHaveText('0 / 0 (false)');
+
+ let request_count = 0;
+ page.on('request', (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0));
+
+ await page.click('#multiply-btn');
+ await expect(page.locator('#command-result')).toHaveText('2');
+ await expect(page.locator('#count-result')).toHaveText('0 / 0 (false)');
+ await page.waitForTimeout(100); // allow all requests to finish
+ expect(request_count).toBe(1); // 1 for the command, no refreshes
+ });
+
+ test('command returns correct sum and does client-initiated single flight mutation', async ({
+ page
+ }) => {
+ await page.goto('/remote');
+ await expect(page.locator('#count-result')).toHaveText('0 / 0 (false)');
+
+ let request_count = 0;
+ page.on('request', (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0));
+
+ await page.click('#multiply-refresh-btn');
+ await expect(page.locator('#command-result')).toHaveText('3');
+ await expect(page.locator('#count-result')).toHaveText('3 / 3 (false)');
+ await page.waitForTimeout(100); // allow all requests to finish
+ expect(request_count).toBe(1); // no query refreshes, since that happens as part of the command response
+ });
+
+ test('command does server-initiated single flight mutation (refresh)', async ({ page }) => {
+ await page.goto('/remote');
+ await expect(page.locator('#count-result')).toHaveText('0 / 0 (false)');
+
+ let request_count = 0;
+ page.on('request', (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0));
+
+ await page.click('#multiply-server-refresh-btn');
+ await expect(page.locator('#command-result')).toHaveText('4');
+ await expect(page.locator('#count-result')).toHaveText('4 / 4 (false)');
+ await page.waitForTimeout(100); // allow all requests to finish (in case there are query refreshes which shouldn't happen)
+ expect(request_count).toBe(1); // no query refreshes, since that happens as part of the command response
+ });
+
+ test('command refresh after reading query reruns the query', async ({ page }) => {
+ await page.goto('/remote');
+ await expect(page.locator('#count-result')).toHaveText('0 / 0 (false)');
+
+ let request_count = 0;
+ page.on('request', (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0));
+
+ await page.click('#multiply-server-refresh-after-read-btn');
+ await expect(page.locator('#command-result')).toHaveText('6');
+ await expect(page.locator('#count-result')).toHaveText('6 / 6 (false)');
+ await page.waitForTimeout(100); // allow all requests to finish (in case there are query refreshes which shouldn't happen)
+ expect(request_count).toBe(1);
+ });
+
+ test('command does server-initiated single flight mutation (set)', async ({ page }) => {
+ await page.goto('/remote');
+ await expect(page.locator('#count-result')).toHaveText('0 / 0 (false)');
+
+ let request_count = 0;
+ page.on('request', (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0));
+
+ await page.click('#multiply-server-set-btn');
+ await expect(page.locator('#command-result')).toHaveText('8');
+ await expect(page.locator('#count-result')).toHaveText('8 / 8 (false)');
+ await page.waitForTimeout(100); // allow all requests to finish (in case there are query refreshes which shouldn't happen)
+ expect(request_count).toBe(1); // no query refreshes, since that happens as part of the command response
+ });
+
+ test('command does client-initiated single flight mutation with override', async ({ page }) => {
+ await page.goto('/remote');
+ await expect(page.locator('#count-result')).toHaveText('0 / 0 (false)');
+
+ let request_count = 0;
+ page.on('request', (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0));
+
+ page.click('#multiply-override-refresh-btn');
+ await expect(page.locator('#count-result')).toHaveText('6 / 6 (false)');
+ await expect(page.locator('#command-result')).toHaveText('5');
+ await expect(page.locator('#count-result')).toHaveText('5 / 5 (false)');
+ await page.waitForTimeout(100); // allow all requests to finish (in case there are query refreshes which shouldn't happen)
+ expect(request_count).toBe(1); // no query refreshes, since that happens as part of the command response
+ });
+
+ test('query/command inside endpoint works', async ({ page }) => {
+ await page.goto('/remote/server-endpoint');
+
+ await page.getByRole('button', { name: 'get' }).click();
+ await expect(page.locator('p')).toHaveText('get');
+
+ await page.getByRole('button', { name: 'post' }).click();
+ await expect(page.locator('p')).toHaveText('post');
+ });
+
+ test('prerendered entries not called in prod', async ({ page }) => {
+ let request_count = 0;
+ page.on('request', (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0));
+ await page.goto('/remote/prerender');
+
+ await page.click('#fetch-prerendered');
+ await expect(page.locator('#fetch-prerendered')).toHaveText('yes');
+
+ await page.click('#fetch-not-prerendered');
+ await expect(page.locator('#fetch-not-prerendered')).toHaveText('d');
+ });
+
+ test('refreshAll reloads remote functions and load functions', async ({ page }) => {
+ await page.goto('/remote');
+ await expect(page.locator('#count-result')).toHaveText('0 / 0 (false)');
+
+ let request_count = 0;
+ page.on('request', (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0));
+
+ await page.click('#refresh-all');
+ await page.waitForTimeout(100); // allow things to rerun
+ expect(request_count).toBe(3);
+ });
+
+ test('refreshAll({ includeLoadFunctions: false }) reloads remote functions only', async ({
+ page
+ }) => {
+ await page.goto('/remote');
+ await expect(page.locator('#count-result')).toHaveText('0 / 0 (false)');
+
+ let request_count = 0;
+ page.on('request', (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0));
+
+ await page.click('#refresh-remote-only');
+ await page.waitForTimeout(100); // allow things to rerun
+ expect(request_count).toBe(2);
+ });
+
+ test('command tracks pending state', async ({ page }) => {
+ await page.goto('/remote');
+
+ // Initial pending should be 0
+ await expect(page.locator('#command-pending')).toHaveText('Command pending: 0');
+
+ // Start a slow command - this will hang until we resolve it
+ await page.click('#command-deferred-btn');
+
+ // Check that pending has incremented to 1
+ await expect(page.locator('#command-pending')).toHaveText('Command pending: 1');
+
+ // Resolve the deferred command
+ await page.click('#resolve-deferreds');
+
+ // Wait for the command to complete and pending to go back to 0
+ await expect(page.locator('#command-pending')).toHaveText('Command pending: 0');
+ });
+
+ test('validation works', async ({ page }) => {
+ await page.goto('/remote/validation');
+ await expect(page.locator('p')).toHaveText('pending');
+
+ await page.click('button:nth-of-type(1)');
+ await expect(page.locator('p')).toHaveText('success');
+
+ await page.click('button:nth-of-type(2)');
+ await expect(page.locator('p')).toHaveText('success');
+
+ await page.click('button:nth-of-type(3)');
+ await expect(page.locator('p')).toHaveText('success');
+
+ await page.click('button:nth-of-type(4)');
+ await expect(page.locator('p')).toHaveText('success');
+ });
+
+ test('fields.set updates DOM before validate', async ({ page }) => {
+ await page.goto('/remote/form/imperative');
+
+ const input = page.locator('input[name="message"]');
+ await input.fill('123');
+
+ await page.locator('#set-and-validate').click();
+
+ await expect(input).toHaveValue('hello');
+ await expect(page.locator('#issue')).toHaveText('ok');
+ });
+
+ test('command pending state is tracked correctly', async ({ page }) => {
+ await page.goto('/remote');
+
+ // Initially no pending commands
+ await expect(page.locator('#command-pending')).toHaveText('Command pending: 0');
+
+ // Start a slow command - this will hang until we resolve it
+ await page.click('#command-deferred-btn');
+
+ // Check that pending has incremented to 1
+ await expect(page.locator('#command-pending')).toHaveText('Command pending: 1');
+
+ // Resolve the deferred command
+ await page.click('#resolve-deferreds');
+
+ // Wait for the command to complete and verify results
+ await expect(page.locator('#command-result')).toHaveText('7');
+
+ // Verify pending count returns to 0
+ await expect(page.locator('#command-pending')).toHaveText('Command pending: 0');
+ });
+
+ // TODO once we have async SSR adjust the test and move this into test.js
+ test('query.batch works', async ({ page }) => {
+ await page.goto('/remote/batch');
+
+ await expect(page.locator('#batch-result-1')).toHaveText('Buy groceries');
+ await expect(page.locator('#batch-result-2')).toHaveText('Walk the dog');
+ await expect(page.locator('#batch-result-3')).toHaveText('Buy groceries');
+ await expect(page.locator('#batch-result-4')).toHaveText('Error loading todo error: Not found');
+
+ let request_count = 0;
+ page.on('request', (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0));
+
+ await page.click('button');
+ await page.waitForTimeout(100); // allow all requests to finish
+ expect(request_count).toBe(1);
+ });
+
+ test('query.batch set updates cache without extra request', async ({ page }) => {
+ await page.goto('/remote/batch');
+ await page.click('#batch-reset-btn');
+ await expect(page.locator('#batch-result-1')).toHaveText('Buy groceries');
+
+ let request_count = 0;
+ const handler = (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0);
+ page.on('request', handler);
+
+ await page.click('#batch-set-btn');
+ await expect(page.locator('#batch-result-1')).toHaveText('Buy cat food');
+ await page.waitForTimeout(100); // allow all requests to finish
+ expect(request_count).toBe(1); // only the command request
+ });
+
+ test('query.batch refresh in command reuses single flight', async ({ page }) => {
+ await page.goto('/remote/batch');
+ await page.click('#batch-reset-btn');
+ await expect(page.locator('#batch-result-2')).toHaveText('Walk the dog');
+
+ let request_count = 0;
+ const handler = (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0);
+ page.on('request', handler);
+
+ await page.click('#batch-refresh-btn');
+ await expect(page.locator('#batch-result-2')).toHaveText('Walk the dog (refreshed)');
+ await page.waitForTimeout(100); // allow all requests to finish
+ expect(request_count).toBe(1); // only the command request
+ });
+
+ // TODO ditto
+ test('query works with transport', async ({ page }) => {
+ await page.goto('/remote/transport');
+
+ await expect(page.locator('h1')).toHaveText('hello from remote function!');
+ });
+});
diff --git a/packages/kit/test/apps/async/test/server.test.js b/packages/kit/test/apps/async/test/server.test.js
new file mode 100644
index 000000000000..69412eadbcbe
--- /dev/null
+++ b/packages/kit/test/apps/async/test/server.test.js
@@ -0,0 +1,17 @@
+import process from 'node:process';
+import { expect } from '@playwright/test';
+import { test } from '../../../utils.js';
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+test.skip(({ javaScriptEnabled }) => javaScriptEnabled);
+
+const root = path.resolve(fileURLToPath(import.meta.url), '..', '..');
+
+test.describe('remote functions', () => {
+ test("doesn't write bundle to disk when treeshaking prerendered remote functions", () => {
+ test.skip(!!process.env.DEV, 'skip when in dev mode');
+ expect(fs.existsSync(path.join(root, 'dist'))).toBe(false);
+ });
+});
diff --git a/packages/kit/test/apps/async/test/test.js b/packages/kit/test/apps/async/test/test.js
new file mode 100644
index 000000000000..bf647f6e4e38
--- /dev/null
+++ b/packages/kit/test/apps/async/test/test.js
@@ -0,0 +1,486 @@
+import { expect } from '@playwright/test';
+import { test } from '../../../utils.js';
+
+test.describe('remote functions', () => {
+ test('query returns correct data', async ({ page, javaScriptEnabled }) => {
+ await page.goto('/remote');
+ await expect(page.locator('#echo-result')).toHaveText('Hello world');
+ if (javaScriptEnabled) {
+ await expect(page.locator('#count-result')).toHaveText('0 / 0 (false)');
+ }
+ });
+
+ test('query redirects on page load (query in common layout)', async ({ page }) => {
+ await page.goto('/remote/query-redirect');
+ await page.click('a[href="/remote/query-redirect/from-common-layout"]');
+ await expect(page.locator('#redirected')).toHaveText('redirected');
+ await expect(page.locator('#layout-query')).toHaveText(
+ 'on page /remote/query-redirect/from-common-layout/redirected (== /remote/query-redirect/from-common-layout/redirected)'
+ );
+ });
+
+ test('query redirects on page load (query on page)', async ({ page }) => {
+ await page.goto('/remote/query-redirect');
+ await page.click('a[href="/remote/query-redirect/from-page"]');
+ await expect(page.locator('#redirected')).toHaveText('redirected');
+ });
+
+ test('non-exported queries do not clobber each other', async ({ page }) => {
+ await page.goto('/remote/query-non-exported');
+
+ await expect(page.locator('h1')).toHaveText('3');
+ });
+
+ test('queries can access the route/url of the page they were called from', async ({
+ page,
+ clicknav
+ }) => {
+ await page.goto('/remote');
+
+ await clicknav('[href="/remote/event"]');
+
+ await expect(page.locator('[data-id="route"]')).toHaveText('route: /remote/event');
+ await expect(page.locator('[data-id="pathname"]')).toHaveText('pathname: /remote/event');
+ });
+
+ test('form works', async ({ page, javaScriptEnabled }) => {
+ await page.goto(`/remote/form/basic-${javaScriptEnabled}`);
+
+ if (javaScriptEnabled) {
+ await expect(page.getByText('message.current:')).toHaveText('message.current: initial');
+ }
+ await expect(page.getByText('await get_message():')).toHaveText('await get_message(): initial');
+
+ await page.fill('[data-unscoped] input', 'hello');
+ await page.getByText('set message').click();
+
+ if (javaScriptEnabled) {
+ await expect(page.getByText('set_message.pending:')).toHaveText('set_message.pending: 1');
+ await page.getByText('resolve deferreds').click();
+ await expect(page.getByText('set_message.pending:')).toHaveText('set_message.pending: 0');
+ await expect(page.getByText('message.current:')).toHaveText('message.current: hello');
+ }
+
+ await expect(page.getByText('await get_message():')).toHaveText('await get_message(): hello');
+
+ await expect(page.getByText('set_message.result')).toHaveText('set_message.result: hello');
+ await expect(page.locator('[data-unscoped] input[name="message"]')).toHaveValue('');
+ });
+
+ test('form submitters work', async ({ page }) => {
+ await page.goto('/remote/form/submitter');
+
+ await page.locator('button').click();
+
+ await expect(page.locator('#result')).toHaveText('hello');
+ });
+
+ test('form updates inputs live', async ({ page, javaScriptEnabled }) => {
+ await page.goto('/remote/form/live-update');
+
+ await page.fill('input', 'hello');
+
+ if (javaScriptEnabled) {
+ await expect(page.getByText('set_message.input.message:')).toHaveText(
+ 'set_message.input.message: hello'
+ );
+ }
+
+ await page.getByText('set message').click();
+
+ if (javaScriptEnabled) {
+ await page.getByText('resolve deferreds').click();
+ }
+
+ await expect(page.getByText('set_message.input.message:')).toHaveText(
+ 'set_message.input.message:'
+ );
+ });
+
+ test('form reports validation issues', async ({ page }) => {
+ await page.goto('/remote/form/validation-issues');
+
+ await page.fill('input', 'invalid');
+ await page.getByText('set message').click();
+
+ await page.getByText('message is invalid').waitFor();
+ });
+
+ test('form handles unexpected error', async ({ page }) => {
+ await page.goto('/remote/form/unexpected-error');
+
+ await page.fill('input', 'unexpected error');
+ await page.getByText('set message').click();
+
+ await page
+ .getByText('This is your custom error page saying: "oops (500 Internal Error)"')
+ .waitFor();
+ });
+
+ test('form handles expected error', async ({ page }) => {
+ await page.goto('/remote/form/expected-error');
+
+ await page.fill('input', 'expected error');
+ await page.getByText('set message').click();
+
+ await page.getByText('This is your custom error page saying: "oops"').waitFor();
+ });
+
+ test('form redirects', async ({ page }) => {
+ await page.goto('/remote/form/redirect');
+
+ await page.fill('input', 'redirect');
+ await page.getByText('set message').click();
+
+ await page.waitForURL('/remote');
+ });
+
+ test('form.buttonProps works', async ({ page, javaScriptEnabled }) => {
+ await page.goto('/remote/form/button-props');
+
+ await page.fill('[data-unscoped] input', 'backwards');
+ await page.getByText('set reverse message').click();
+
+ if (javaScriptEnabled) {
+ await page.getByText('message.current: sdrawkcab').waitFor();
+ await expect(page.getByText('await get_message():')).toHaveText(
+ 'await get_message(): sdrawkcab'
+ );
+ }
+
+ await expect(page.getByText('set_reverse_message.result')).toHaveText(
+ 'set_reverse_message.result: sdrawkcab'
+ );
+ });
+
+ test('form scoping with for(...) works', async ({ page, javaScriptEnabled }) => {
+ await page.goto('/remote/form/form-scoped');
+
+ await page.fill('[data-scoped] input', 'hello');
+ await page.getByText('set scoped message').click();
+
+ if (javaScriptEnabled) {
+ await expect(page.getByText('scoped.pending:')).toHaveText('scoped.pending: 1');
+ await page.getByText('resolve deferreds').click();
+ await expect(page.getByText('scoped.pending:')).toHaveText('scoped.pending: 0');
+
+ await page.getByText('message.current: hello').waitFor();
+ await expect(page.getByText('await get_message():')).toHaveText('await get_message(): hello');
+ }
+
+ await expect(page.getByText('scoped.result')).toHaveText(
+ 'scoped.result: hello (from: scoped:form-scoped)'
+ );
+ await expect(page.locator('[data-scoped] input[name="message"]')).toHaveValue('');
+ });
+
+ test('form enhance(...) works', async ({ page, javaScriptEnabled }) => {
+ await page.goto('/remote/form/enhanced');
+
+ await page.fill('[data-enhanced] input', 'hello');
+
+ // Click on the span inside the button to test the event.target vs event.currentTarget issue (#14159)
+ await page.locator('[data-enhanced] span').click();
+
+ if (javaScriptEnabled) {
+ await expect(page.getByText('enhanced.pending:')).toHaveText('enhanced.pending: 1');
+
+ await page.getByText('message.current: hello (override)').waitFor();
+
+ await page.getByText('resolve deferreds').click();
+ await expect(page.getByText('enhanced.pending:')).toHaveText('enhanced.pending: 0');
+ await expect(page.getByText('await get_message():')).toHaveText('await get_message(): hello');
+
+ // enhanced submission should not clear the input; the developer must do that at the appropriate time
+ await expect(page.locator('[data-enhanced] input[name="message"]')).toHaveValue('hello');
+ } else {
+ await expect(page.locator('[data-enhanced] input[name="message"]')).toHaveValue('');
+ }
+
+ await expect(page.getByText('enhanced.result')).toHaveText(
+ 'enhanced.result: hello (from: enhanced:enhanced)'
+ );
+ });
+
+ test('form preflight works', async ({ page, javaScriptEnabled }) => {
+ if (!javaScriptEnabled) return;
+
+ await page.goto('/remote/form/preflight');
+
+ for (const enhanced of [true, false]) {
+ const input = page.locator(enhanced ? '[data-enhanced] input' : '[data-default] input');
+ const button = page.getByText(enhanced ? 'set enhanced number' : 'set number');
+
+ await input.fill('21');
+ await button.click();
+ await page.getByText('too big').waitFor();
+
+ await input.fill('9');
+ await button.click();
+ await page.getByText('too small').waitFor();
+
+ await input.fill('15');
+ await button.click();
+ await expect(page.getByText('number.current')).toHaveText('number.current: 15');
+ }
+ });
+
+ test('form preflight-only validation works', async ({ page, javaScriptEnabled }) => {
+ if (!javaScriptEnabled) return;
+
+ await page.goto('/remote/form/preflight-only');
+
+ const a = page.locator('[name="a"]');
+ const button = page.locator('button');
+ const issues = page.locator('.issues');
+
+ await button.click();
+ await expect(issues).toContainText('a is too short');
+ await expect(issues).toContainText('b is too short');
+ await expect(issues).toContainText('c is too short');
+
+ await a.fill('aaaaaaaa');
+ await expect(issues).toContainText('a is too long');
+
+ // server issues should be preserved...
+ await expect(issues).toContainText('b is too short');
+ await expect(issues).toContainText('c is too short');
+
+ // ...unless overridden by client issues
+ await expect(issues).not.toContainText('a is too short');
+ });
+
+ test('form validate works', async ({ page, javaScriptEnabled }) => {
+ if (!javaScriptEnabled) return;
+
+ await page.goto('/remote/form/validate');
+
+ const myForm = page.locator('form#my-form');
+ const foo = page.locator('input[name="foo"]');
+ const bar = page.locator('input[name="bar"]');
+ const submit = page.locator('button:has-text("imperative validation")');
+
+ await foo.fill('a');
+ await expect(myForm).not.toContainText('Invalid type: Expected');
+
+ await bar.fill('g');
+ await expect(myForm).toContainText('Invalid type: Expected ("d" | "e") but received "g"');
+
+ await bar.fill('d');
+ await expect(myForm).not.toContainText('Invalid type: Expected');
+
+ await page.locator('#trigger-validate').click();
+ await expect(myForm).toContainText(
+ 'Invalid type: Expected "submitter" but received "incorrect_value"'
+ );
+
+ // Test imperative validation
+ await foo.fill('c');
+ await bar.fill('d');
+ await submit.click();
+ await expect(myForm).toContainText('Imperative: foo cannot be c');
+
+ const nestedValue = page.locator('input[name="nested.value"]');
+ const validate = page.locator('button#validate');
+ const allIssues = page.locator('#allIssues');
+
+ await nestedValue.fill('in');
+ await validate.click();
+ await expect(allIssues).toContainText('"path":["nested","value"]');
+ });
+
+ test('form validation issues cleared', async ({ page, javaScriptEnabled }) => {
+ if (!javaScriptEnabled) return;
+
+ await page.goto('/remote/form/validate');
+
+ const baz = page.locator('input[name="baz"]');
+ const submit = page.locator('#my-form-2 button');
+
+ await baz.fill('c');
+ await submit.click();
+ await expect(page.locator('#my-form-2')).toContainText('Invalid type: Expected');
+
+ await baz.fill('a');
+ await submit.click();
+ await expect(page.locator('#my-form-2')).not.toContainText('Invalid type: Expected');
+ await expect(page.locator('[data-error]')).toHaveText('An error occurred');
+
+ await baz.fill('c');
+ await submit.click();
+ await expect(page.locator('#my-form-2')).toContainText('Invalid type: Expected');
+
+ await baz.fill('b');
+ await submit.click();
+ await expect(page.locator('#my-form-2')).not.toContainText('Invalid type: Expected');
+ await expect(page.locator('[data-error]')).toHaveText('No error');
+ });
+
+ test('form inputs excludes underscore-prefixed fields', async ({ page, javaScriptEnabled }) => {
+ if (javaScriptEnabled) return;
+
+ await page.goto('/remote/form/underscore');
+
+ await page.fill('input[name="username"]', 'abcdefg');
+ await page.fill('input[name="_password"]', 'pqrstuv');
+ await page.locator('button').click();
+
+ await expect(page.locator('input[name="username"]')).toHaveValue('abcdefg');
+ await expect(page.locator('input[name="_password"]')).toHaveValue('');
+ });
+
+ test('prerendered entries not called in prod', async ({ page, clicknav }) => {
+ await page.goto('/remote/prerender');
+ await clicknav('[href="/remote/prerender/whole-page"]');
+ await expect(page.locator('#prerendered-data')).toHaveText('a c 中文 yes');
+
+ await page.goto('/remote/prerender');
+ await clicknav('[href="/remote/prerender/functions-only"]');
+ await expect(page.locator('#prerendered-data')).toHaveText('a c 中文 yes');
+ });
+
+ test('form.fields.value() returns correct nested object structure', async ({
+ page,
+ javaScriptEnabled
+ }) => {
+ if (!javaScriptEnabled) return;
+
+ await page.goto('/remote/form/value');
+
+ // Initially should be empty object or undefined values
+ const initialValue = await page.locator('#full-value').textContent();
+ expect(JSON.parse(initialValue)).toEqual({});
+
+ // Fill leaf field
+ await page.fill('input[name="leaf"]', 'leaf-value');
+ const afterLeaf = await page.locator('#full-value').textContent();
+ expect(JSON.parse(afterLeaf)).toEqual({
+ leaf: 'leaf-value'
+ });
+
+ // Fill object.leaf field
+ await page.fill('input[name="object.leaf"]', 'object-leaf-value');
+ const afterObjectLeaf = await page.locator('#full-value').textContent();
+ expect(JSON.parse(afterObjectLeaf)).toEqual({
+ leaf: 'leaf-value',
+ object: {
+ leaf: 'object-leaf-value'
+ }
+ });
+
+ // Fill object.array fields
+ await page.fill('input[name="object.array[0]"]', 'array-item-1');
+ const afterArrayItem1 = await page.locator('#full-value').textContent();
+ expect(JSON.parse(afterArrayItem1)).toEqual({
+ leaf: 'leaf-value',
+ object: {
+ leaf: 'object-leaf-value',
+ array: ['array-item-1']
+ }
+ });
+
+ await page.fill('input[name="object.array[1]"]', 'array-item-2');
+ const afterArrayItem2 = await page.locator('#full-value').textContent();
+ expect(JSON.parse(afterArrayItem2)).toEqual({
+ leaf: 'leaf-value',
+ object: {
+ leaf: 'object-leaf-value',
+ array: ['array-item-1', 'array-item-2']
+ }
+ });
+
+ // Fill array[0].leaf field
+ await page.fill('input[name="array[0].leaf"]', 'array-0-leaf');
+ const afterArray0 = await page.locator('#full-value').textContent();
+ expect(JSON.parse(afterArray0)).toEqual({
+ leaf: 'leaf-value',
+ object: {
+ leaf: 'object-leaf-value',
+ array: ['array-item-1', 'array-item-2']
+ },
+ array: [{ leaf: 'array-0-leaf' }]
+ });
+
+ // Fill array[1].leaf field
+ await page.fill('input[name="array[1].leaf"]', 'array-1-leaf');
+ const afterArray1 = await page.locator('#full-value').textContent();
+ expect(JSON.parse(afterArray1)).toEqual({
+ leaf: 'leaf-value',
+ object: {
+ leaf: 'object-leaf-value',
+ array: ['array-item-1', 'array-item-2']
+ },
+ array: [{ leaf: 'array-0-leaf' }, { leaf: 'array-1-leaf' }]
+ });
+
+ // Test nested object value access
+ const objectValue = await page.locator('#object-value').textContent();
+ expect(JSON.parse(objectValue)).toEqual({
+ leaf: 'object-leaf-value',
+ array: ['array-item-1', 'array-item-2']
+ });
+
+ // Test array value access
+ const arrayValue = await page.locator('#array-value').textContent();
+ expect(JSON.parse(arrayValue)).toEqual([{ leaf: 'array-0-leaf' }, { leaf: 'array-1-leaf' }]);
+ });
+
+ test('selects are not nuked when unrelated controls change', async ({
+ page,
+ javaScriptEnabled
+ }) => {
+ if (!javaScriptEnabled) return;
+
+ await page.goto('/remote/form/select-untouched');
+
+ await page.fill('input', 'hello');
+ await expect(page.locator('select')).toHaveValue('one');
+ });
+ test('file uploads work', async ({ page }) => {
+ await page.goto('/remote/form/file-upload');
+
+ await page.locator('input[name="file1"]').setInputFiles({
+ name: 'a.txt',
+ mimeType: 'text/plain',
+ buffer: Buffer.from('a')
+ });
+ await page.locator('input[name="file2"]').setInputFiles({
+ name: 'b.txt',
+ mimeType: 'text/plain',
+ buffer: Buffer.from('b')
+ });
+ await page.locator('input[type="checkbox"]').check();
+ await page.locator('button').click();
+
+ await expect(page.locator('pre')).toHaveText(
+ JSON.stringify({
+ text: 'Hello world',
+ file1: 'a',
+ file2: 'b'
+ })
+ );
+ });
+ test('large file uploads work', async ({ page }) => {
+ await page.goto('/remote/form/file-upload');
+
+ await page.locator('input[name="file1"]').setInputFiles({
+ name: 'a.txt',
+ mimeType: 'text/plain',
+ buffer: Buffer.alloc(1024 * 1024 * 10)
+ });
+ await page.locator('input[name="file2"]').setInputFiles({
+ name: 'b.txt',
+ mimeType: 'text/plain',
+ buffer: Buffer.from('b')
+ });
+ await page.locator('button').click();
+
+ await expect(page.locator('pre')).toHaveText(
+ JSON.stringify({
+ text: 'Hello world',
+ file1: 1024 * 1024 * 10,
+ file2: 1
+ })
+ );
+ });
+});
diff --git a/packages/kit/test/apps/async/tsconfig.json b/packages/kit/test/apps/async/tsconfig.json
new file mode 100644
index 000000000000..1d665886266b
--- /dev/null
+++ b/packages/kit/test/apps/async/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "esModuleInterop": true,
+ "noEmit": true,
+ "resolveJsonModule": true
+ },
+ "extends": "./.svelte-kit/tsconfig.json"
+}
diff --git a/packages/kit/test/apps/async/vite.config.js b/packages/kit/test/apps/async/vite.config.js
new file mode 100644
index 000000000000..69200cdb7cd8
--- /dev/null
+++ b/packages/kit/test/apps/async/vite.config.js
@@ -0,0 +1,18 @@
+import * as path from 'node:path';
+import { sveltekit } from '@sveltejs/kit/vite';
+
+/** @type {import('vite').UserConfig} */
+const config = {
+ build: {
+ minify: false
+ },
+ clearScreen: false,
+ plugins: [sveltekit()],
+ server: {
+ fs: {
+ allow: [path.resolve('../../../src')]
+ }
+ }
+};
+
+export default config;
diff --git a/packages/kit/test/apps/basics/src/hooks.server.js b/packages/kit/test/apps/basics/src/hooks.server.js
index 34d0e9cba874..c988355d8ece 100644
--- a/packages/kit/test/apps/basics/src/hooks.server.js
+++ b/packages/kit/test/apps/basics/src/hooks.server.js
@@ -62,11 +62,6 @@ export const handleError = ({ event, error: e, status, message }) => {
: { message: `${error.message} (${status} ${message})` };
};
-/** @type {import('@sveltejs/kit').HandleValidationError} */
-export const handleValidationError = ({ issues }) => {
- return { message: issues[0].message };
-};
-
export const handle = sequence(
// eslint-disable-next-line prefer-arrow-callback -- this needs a name for tests
function set_tracing_test_id({ event, resolve }) {
diff --git a/packages/kit/test/apps/basics/src/routes/remote/event/+page.svelte b/packages/kit/test/apps/basics/src/routes/remote/event/+page.svelte
deleted file mode 100644
index 3aa05f322ddc..000000000000
--- a/packages/kit/test/apps/basics/src/routes/remote/event/+page.svelte
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-{#await get_event() then event}
-