Repository: sveltejs/kit
Pull request #8361 (2023-01-06 14:21:16)

fix some flaky tests
this is a subset of #8225 

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 900ce2d7a72192155032992b4e9544b43e4f4498

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/sveltejs/kit/new/fix-flaky-tests?filename=.changeset/tiny-suits-try.md&value=---%0A%22%40sveltejs%2Fkit%22%3A%20patch%0A%22test-basics%22%3A%20patch%0A---%0A%0Afix%20some%20flaky%20tests%0A)



Patch:
diff --git a/.changeset/quiet-feet-compare.md b/.changeset/quiet-feet-compare.md
new file mode 100644
index 000000000..0a9eb19da
--- /dev/null
+++ b/.changeset/quiet-feet-compare.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Guarantee that `$page.route` has the correct shape
diff --git a/packages/adapter-static/CHANGELOG.md b/packages/adapter-static/CHANGELOG.md
index a0731b9b1..dbdea306c 100644
--- a/packages/adapter-static/CHANGELOG.md
+++ b/packages/adapter-static/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @sveltejs/adapter-static
 
+## 1.0.1
+
+### Patch Changes
+
+- [fix] match `adapter-vercel` logic for serving prerendered content ([#8332](https://github.com/sveltejs/kit/pull/8332))
+
 ## 1.0.0
 
 ### Major Changes
diff --git a/packages/adapter-static/index.js b/packages/adapter-static/index.js
index b43956ae2..8f859dabc 100644
--- a/packages/adapter-static/index.js
+++ b/packages/adapter-static/index.js
@@ -71,9 +71,7 @@ See https://kit.svelte.dev/docs/page-options#prerender for more details`
 				assets = pages,
 				fallback,
 				precompress
-			} = options ??
-			platform?.defaults(builder.config) ??
-			/** @type {import('./index').AdapterOptions} */ ({});
+			} = options ?? platform?.defaults ?? /** @type {import('./index').AdapterOptions} */ ({});
 
 			builder.rimraf(assets);
 			builder.rimraf(pages);
diff --git a/packages/adapter-static/package.json b/packages/adapter-static/package.json
index 20f917c54..aa8f00318 100644
--- a/packages/adapter-static/package.json
+++ b/packages/adapter-static/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-static",
-	"version": "1.0.0",
+	"version": "1.0.1",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/adapter-static/platforms.js b/packages/adapter-static/platforms.js
index d92967d09..3ecf37cb3 100644
--- a/packages/adapter-static/platforms.js
+++ b/packages/adapter-static/platforms.js
@@ -4,26 +4,22 @@ import fs from 'fs';
  * @typedef {{
  *   name: string;
  *   test: () => boolean;
- *   defaults: (config: any) => import('./index').AdapterOptions; // TODO
+ *   defaults: import('./index').AdapterOptions;
  *   done: (builder: import('@sveltejs/kit').Builder) => void;
  * }}
  * Platform */
 
+// This function is duplicated in adapter-vercel
 /** @param {import('@sveltejs/kit').Builder} builder */
-function vercel_routes(builder) {
+function static_vercel_config(builder) {
 	/** @type {any[]} */
-	const routes = [
-		{
-			src: `/${builder.config.kit.appDir}/immutable/.+`,
-			headers: {
-				'cache-control': 'public, immutable, max-age=31536000'
-			}
-		}
-	];
+	const prerendered_redirects = [];
+
+	/** @type {Record<string, { path: string }>} */
+	const overrides = {};
 
-	// explicit redirects
 	for (const [src, redirect] of builder.prerendered.redirects) {
-		routes.push({
+		prerendered_redirects.push({
 			src,
 			headers: {
 				Location: redirect.location
@@ -32,32 +28,35 @@ function vercel_routes(builder) {
 		});
 	}
 
-	// prerendered pages
-	for (const [src, page] of builder.prerendered.pages) {
-		routes.push({
-			src,
-			dest: `${builder.config.kit.appDir}/prerendered/${page.file}`
-		});
-	}
+	for (const [path, page] of builder.prerendered.pages) {
+		if (path.endsWith('/') && path !== '/') {
+			prerendered_redirects.push(
+				{ src: path, dest: path.slice(0, -1) },
+				{ src: path.slice(0, -1), status: 308, headers: { Location: path } }
+			);
 
-	// implicit redirects (trailing slashes)
-	for (const [src] of builder.prerendered.pages) {
-		if (src !== '/') {
-			routes.push({
-				src: src.endsWith('/') ? src.slice(0, -1) : src + '/',
-				headers: {
-					location: src
-				},
-				status: 308
-			});
+			overrides[page.file] = { path: path.slice(1, -1) };
+		} else {
+			overrides[page.file] = { path: path.slice(1) };
 		}
 	}
 
-	routes.push({
-		handle: 'filesystem'
-	});
-
-	return routes;
+	return {
+		version: 3,
+		routes: [
+			...prerendered_redirects,
+			{
+				src: `/${builder.getAppPath()}/immutable/.+`,
+				headers: {
+					'cache-control': 'public, immutable, max-age=31536000'
+				}
+			},
+			{
+				handle: 'filesystem'
+			}
+		],
+		overrides
+	};
 }
 
 /** @type {Platform[]} */
@@ -65,18 +64,12 @@ export const platforms = [
 	{
 		name: 'Vercel',
 		test: () => !!process.env.VERCEL,
-		defaults: (config) => ({
-			pages: `.vercel/output/static/${config.kit.appDir}/prerendered`,
-			assets: '.vercel/output/static'
-		}),
+		defaults: {
+			pages: '.vercel/output/static'
+		},
 		done: (builder) => {
-			fs.writeFileSync(
-				'.vercel/output/config.json',
-				JSON.stringify({
-					version: 3,
-					routes: vercel_routes(builder)
-				})
-			);
+			const config = static_vercel_config(builder);
+			fs.writeFileSync('.vercel/output/config.json', JSON.stringify(config, null, '  '));
 		}
 	}
 ];
diff --git a/packages/adapter-vercel/CHANGELOG.md b/packages/adapter-vercel/CHANGELOG.md
index 62c41d705..b969b1bbe 100644
--- a/packages/adapter-vercel/CHANGELOG.md
+++ b/packages/adapter-vercel/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @sveltejs/adapter-vercel
 
+## 1.0.1
+
+### Patch Changes
+
+- [fix] only apply immutable cache-control headers to immutable assets ([#8332](https://github.com/sveltejs/kit/pull/8332))
+
 ## 1.0.0
 
 ### Major Changes
diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js
index 69ab0a8ba..529225d7c 100644
--- a/packages/adapter-vercel/index.js
+++ b/packages/adapter-vercel/index.js
@@ -25,48 +25,7 @@ const plugin = function ({ external = [], edge, split } = {}) {
 				functions: `${dir}/functions`
 			};
 
-			/** @type {any[]} */
-			const prerendered_redirects = [];
-
-			/** @type {Record<string, { path: string }>} */
-			const overrides = {};
-
-			for (const [src, redirect] of builder.prerendered.redirects) {
-				prerendered_redirects.push({
-					src,
-					headers: {
-						Location: redirect.location
-					},
-					status: redirect.status
-				});
-			}
-
-			for (const [path, page] of builder.prerendered.pages) {
-				if (path.endsWith('/') && path !== '/') {
-					prerendered_redirects.push(
-						{ src: path, dest: path.slice(0, -1) },
-						{ src: path.slice(0, -1), status: 308, headers: { Location: path } }
-					);
-
-					overrides[page.file] = { path: path.slice(1, -1) };
-				} else {
-					overrides[page.file] = { path: path.slice(1) };
-				}
-			}
-
-			/** @type {any[]} */
-			const routes = [
-				...prerendered_redirects,
-				{
-					src: `/${builder.getAppPath()}/.+`,
-					headers: {
-						'cache-control': 'public, immutable, max-age=31536000'
-					}
-				},
-				{
-					handle: 'filesystem'
-				}
-			];
+			const config = static_vercel_config(builder);
 
 			builder.log.minor('Generating serverless function...');
 
@@ -97,7 +56,7 @@ const plugin = function ({ external = [], edge, split } = {}) {
 					`nodejs${node_version.major}.x`
 				);
 
-				routes.push({ src: pattern, dest: `/${name}` });
+				config.routes.push({ src: pattern, dest: `/${name}` });
 			}
 
 			/**
@@ -142,7 +101,7 @@ const plugin = function ({ external = [], edge, split } = {}) {
 					})
 				);
 
-				routes.push({ src: pattern, dest: `/${name}` });
+				config.routes.push({ src: pattern, dest: `/${name}` });
 			}
 
 			const generate_function = edge ? generate_edge_function : generate_serverless_function;
@@ -182,18 +141,7 @@ const plugin = function ({ external = [], edge, split } = {}) {
 
 			builder.log.minor('Writing routes...');
 
-			write(
-				`${dir}/config.json`,
-				JSON.stringify(
-					{
-						version: 3,
-						routes,
-						overrides
-					},
-					null,
-					'  '
-				)
-			);
+			write(`${dir}/config.json`, JSON.stringify(config, null, '  '));
 		}
 	};
 };
@@ -225,6 +173,56 @@ function get_node_version() {
 	return { major, full };
 }
 
+// This function is duplicated in adapter-static
+/** @param {import('@sveltejs/kit').Builder} builder */
+function static_vercel_config(builder) {
+	/** @type {any[]} */
+	const prerendered_redirects = [];
+
+	/** @type {Record<string, { path: string }>} */
+	const overrides = {};
+
+	for (const [src, redirect] of builder.prerendered.redirects) {
+		prerendered_redirects.push({
+			src,
+			headers: {
+				Location: redirect.location
+			},
+			status: redirect.status
+		});
+	}
+
+	for (const [path, page] of builder.prerendered.pages) {
+		if (path.endsWith('/') && path !== '/') {
+			prerendered_redirects.push(
+				{ src: path, dest: path.slice(0, -1) },
+				{ src: path.slice(0, -1), status: 308, headers: { Location: path } }
+			);
+
+			overrides[page.file] = { path: path.slice(1, -1) };
+		} else {
+			overrides[page.file] = { path: path.slice(1) };
+		}
+	}
+
+	return {
+		version: 3,
+		routes: [
+			...prerendered_redirects,
+			{
+				src: `/${builder.getAppPath()}/immutable/.+`,
+				headers: {
+					'cache-control': 'public, immutable, max-age=31536000'
+				}
+			},
+			{
+				handle: 'filesystem'
+			}
+		],
+		overrides
+	};
+}
+
 /**
  * @param {import('@sveltejs/kit').Builder} builder
  * @param {string} entry
diff --git a/packages/adapter-vercel/package.json b/packages/adapter-vercel/package.json
index 68fce47b2..e815060a5 100644
--- a/packages/adapter-vercel/package.json
+++ b/packages/adapter-vercel/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-vercel",
-	"version": "1.0.0",
+	"version": "1.0.1",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index c85004811..6ef4cc0ed 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -286,7 +286,7 @@ export function create_client({ target, base }) {
 				);
 				return false;
 			}
-		} else if (navigation_result.props?.page?.status >= 400) {
+		} else if (/** @type {number} */ (navigation_result.props?.page?.status) >= 400) {
 			const updated = await stores.updated.check();
 			if (updated) {
 				await native_navigation(url);
@@ -369,7 +369,7 @@ export function create_client({ target, base }) {
 		const style = document.querySelector('style[data-sveltekit]');
 		if (style) style.remove();
 
-		page = result.props.page;
+		page = /** @type {import('types').Page} */ (result.props.page);
 
 		root = new Root({
 			target,
@@ -435,6 +435,7 @@ export function create_client({ target, base }) {
 				route
 			},
 			props: {
+				// @ts-ignore Somehow it's getting SvelteComponent and SvelteComponentDev mixed up
 				components: filtered.map((branch_node) => branch_node.node.component)
 			}
 		};
@@ -473,7 +474,9 @@ export function create_client({ target, base }) {
 			result.props.page = {
 				error,
 				params,
-				route,
+				route: {
+					id: route?.id ?? null
+				},
 				status,
 				url: new URL(url),
 				form: form ?? null,
diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts
index 757752cc1..b1bea7c13 100644
--- a/packages/kit/src/runtime/client/types.d.ts
+++ b/packages/kit/src/runtime/client/types.d.ts
@@ -8,7 +8,8 @@ import {
 	preloadCode,
 	preloadData
 } from '$app/navigation';
-import { CSRPageNode, CSRPageNodeLoader, CSRRoute, TrailingSlash, Uses } from 'types';
+import { SvelteComponent } from 'svelte';
+import { CSRPageNode, CSRPageNodeLoader, CSRRoute, Page, TrailingSlash, Uses } from 'types';
 
 export interface Client {
 	// public API, exposed via $app/navigation
@@ -58,7 +59,12 @@ export type NavigationRedirect = {
 export type NavigationFinished = {
 	type: 'loaded';
 	state: NavigationState;
-	props: Record<string, any>;
+	props: {
+		components: Array<typeof SvelteComponent>;
+		page?: Page;
+		form?: Record<string, any> | null;
+		[key: `data_${number}`]: Record<string, any>;
+	};
 };
 
 export type BranchNode = {


Test patch:
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index 82dc87df0..613133031 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -238,7 +238,7 @@ test.describe('Scrolling', () => {
 
 	test('scroll is restored after hitting the back button', async ({ baseURL, clicknav, page }) => {
 		await page.goto('/anchor');
-		await page.click('#scroll-anchor');
+		await page.locator('#scroll-anchor').click();
 		const originalScrollY = /** @type {number} */ (await page.evaluate(() => scrollY));
 		await clicknav('#routing-page');
 		await page.goBack();
@@ -266,7 +266,7 @@ test.describe('Scrolling', () => {
 		const target_scroll_y = rect.y + rect.height - height;
 		await page.evaluate((y) => scrollTo(0, y), target_scroll_y);
 
-		await page.click('[href="/scroll/cross-document/b"]');
+		await page.locator('[href="/scroll/cross-document/b"]').click();
 		expect(await page.textContent('h1')).toBe('b');
 		await page.waitForSelector('body.started');
 
@@ -394,7 +394,7 @@ test.describe('CSS', () => {
 test.describe('Endpoints', () => {
 	test('calls a delete handler', async ({ page }) => {
 		await page.goto('/delete-route');
-		await page.click('.del');
+		await page.locator('.del').click();
 		expect(await page.innerHTML('h1')).toBe('deleted 42');
 	});
 });
@@ -556,43 +556,35 @@ test.describe('Load', () => {
 		test('load does not call fetch if max-age allows it', async ({ page, request }) => {
 			await request.get('/load/cache-control/reset');
 
-			page.addInitScript(`
-			window.now = 0;
-			window.performance.now = () => now;
-		`);
-
 			await page.goto('/load/cache-control');
-			expect(await page.textContent('p')).toBe('Count is 0');
-			await page.waitForTimeout(500);
-			await page.click('button.default');
-			await page.waitForTimeout(500);
-			expect(await page.textContent('p')).toBe('Count is 0');
-
-			await page.evaluate(() => (window.now = 2500));
-			await page.click('button.default');
-			await expect(page.locator('p')).toHaveText('Count is 2');
+			await expect(page.getByText('Count is 0')).toBeVisible();
+			await page.locator('button.default').click();
+			await expect(page.getByText('Count is 0')).toBeVisible();
+
+			// use a cache expiration value high enough that we're confident the earlier
+			// portions of the test will complete before the cache expires
+			await page.waitForTimeout(4000);
+
+			await page.locator('button.default').click();
+			await expect(page.getByText('Count is 2')).toBeVisible();
 		});
 
 		test('load does ignore ttl if fetch cache options says so', async ({ page, request }) => {
 			await request.get('/load/cache-control/reset');
 
 			await page.goto('/load/cache-control');
-			expect(await page.textContent('p')).toBe('Count is 0');
-			await page.waitForTimeout(500);
-			await page.click('button.force');
-			await page.waitForTimeout(500);
-			expect(await page.textContent('p')).toBe('Count is 1');
+			await expect(page.getByText('Count is 0')).toBeVisible();
+			await page.locator('button.force').click();
+			await expect(page.getByText('Count is 1')).toBeVisible();
 		});
 
 		test('load busts cache if non-GET request to resource is made', async ({ page, request }) => {
 			await request.get('/load/cache-control/reset');
 
 			await page.goto('/load/cache-control');
-			expect(await page.textContent('p')).toBe('Count is 0');
-			await page.waitForTimeout(500);
-			await page.click('button.bust');
-			await page.waitForTimeout(500);
-			expect(await page.textContent('p')).toBe('Count is 1');
+			await expect(page.getByText('Count is 0')).toBeVisible();
+			await page.locator('button.bust').click();
+			await expect(page.getByText('Count is 1')).toBeVisible();
 		});
 	});
 
@@ -792,10 +784,10 @@ test.describe('Routing', () => {
 		await page.goto('/routing/hashes/pagestore');
 		expect(await page.textContent('#window-hash')).toBe('');
 		expect(await page.textContent('#page-url-hash')).toBe('');
-		await page.click('[href="#target"]');
+		await page.locator('[href="#target"]').click();
 		expect(await page.textContent('#window-hash')).toBe('#target');
 		expect(await page.textContent('#page-url-hash')).toBe('#target');
-		await page.click('[href="/routing/hashes/pagestore"]');
+		await page.locator('[href="/routing/hashes/pagestore"]').click();
 		await expect(page.locator('#window-hash')).toHaveText('#target'); // hashchange doesn't fire for these
 		await expect(page.locator('#page-url-hash')).toHaveText('');
 	});
@@ -811,7 +803,7 @@ test.describe('Routing', () => {
 
 		try {
 			await page.goto(`/routing/slashes?port=${port}`);
-			await page.click(`a[href="http://localhost:${port}/with-slash/"]`);
+			await page.locator(`a[href="http://localhost:${port}/with-slash/"]`).click();
 
 			expect(urls).toEqual(['/with-slash/']);
 		} finally {
@@ -823,7 +815,7 @@ test.describe('Routing', () => {
 		await page.goto('/routing/external-popstate');
 		expect(await page.textContent('h1')).toBe('hello');
 
-		await page.click('button');
+		await page.locator('button').click();
 		expect(await page.textContent('h1')).toBe('hello');
 
 		await page.goBack();
@@ -836,7 +828,7 @@ test.describe('Routing', () => {
 	test('recognizes clicks outside the app target', async ({ page }) => {
 		await page.goto('/routing/link-outside-app-target/source');
 
-		await page.click('[href="/routing/link-outside-app-target/target"]');
+		await page.locator('[href="/routing/link-outside-app-target/target"]').click();
 		await expect(page.locator('h1')).toHaveText('target: 1');
 	});
 
@@ -850,7 +842,7 @@ test.describe('Routing', () => {
 		page.on('request', (request) => requests.push(request.url()));
 
 		await page.locator('input').fill('updated');
-		await page.click('button');
+		await page.locator('button').click();
 
 		expect(requests).toEqual([]);
 		expect(await page.textContent('h1')).toBe('updated');
@@ -921,7 +913,7 @@ test.describe('$app/stores', () => {
 	test('can use $app/stores from anywhere on client', async ({ page }) => {
 		await page.goto('/store/client-access');
 		await expect(page.locator('h1')).toHaveText('undefined');
-		await page.click('button');
+		await page.locator('button').click();
 		await expect(page.locator('h1')).toHaveText('/store/client-access');
 	});
 
@@ -1014,7 +1006,7 @@ test.describe.serial('Invalidation', () => {
 
 	test('multiple invalidations run concurrently', async ({ page, request }) => {
 		await page.goto('/load/invalidation/multiple');
-		expect(await page.textContent('p')).toBe('layout: 0, page: 0');
+		await expect(page.getByText('layout: 0, page: 0')).toBeVisible();
 
 		await page.click('button.layout');
 		await page.click('button.layout');
@@ -1023,12 +1015,12 @@ test.describe.serial('Invalidation', () => {
 		await page.click('button.layout');
 		await page.click('button.page');
 		await page.click('button.all');
-		await expect(page.locator('p')).toHaveText('layout: 4, page: 4');
+		await expect(page.getByText('layout: 4, page: 4')).toBeVisible();
 	});
 
 	test('invalidateAll persists through redirects', async ({ page }) => {
 		await page.goto('/load/invalidation/multiple/redirect');
-		await page.click('button.redirect');
+		await page.locator('button.redirect').click();
 		await expect(page.locator('p.redirect-state')).toHaveText('Redirect state: done');
 	});
 
@@ -1096,10 +1088,10 @@ test.describe.serial('Invalidation', () => {
 
 	test('$page.url can safely be mutated', async ({ page }) => {
 		await page.goto('/load/mutated-url?q=initial');
-		expect(await page.textContent('h1')).toBe('initial');
+		await expect(page.getByText('initial')).toBeVisible();
 
-		await page.click('button');
-		expect(await page.textContent('h1')).toBe('updated');
+		await page.locator('button').click();
+		await expect(page.getByText('updated')).toBeVisible();
 	});
 });
 
@@ -1146,12 +1138,12 @@ test.describe('data-sveltekit attributes', () => {
 		page.on('request', (r) => requests.push(r.url()));
 
 		await page.goto('/data-sveltekit/reload');
-		await page.click('#one');
+		await page.locator('#one').click();
 		expect(requests).toContain(`${baseURL}/data-sveltekit/reload/target`);
 
 		requests.length = 0;
 		await page.goto('/data-sveltekit/reload');
-		await page.click('#two');
+		await page.locator('#two').click();
 		expect(requests).toContain(`${baseURL}/data-sveltekit/reload/target`);
 
 		requests.length = 0;
@@ -1207,7 +1199,7 @@ test.describe('cookies', () => {
 	test('etag forwards cookies', async ({ page }) => {
 		await page.goto('/cookies/forwarded-in-etag');
 		await expect(page.locator('p')).toHaveText('foo=bar');
-		await Promise.all([page.waitForNavigation(), page.click('button')]);
+		await page.locator('button').click();
 		await expect(page.locator('p')).toHaveText('foo=bar');
 	});
 });
@@ -1224,11 +1216,11 @@ test.describe('Interactivity', () => {
 		await page.goto('/interactivity/toggle-element');
 		expect(await page.textContent('button')).toBe('remove');
 
-		await page.click('button');
+		await page.locator('button').click();
 		expect(await page.textContent('button')).toBe('add');
 		expect(await page.textContent('a')).toBe('add');
 
-		await page.click('a');
+		await page.locator('a').filter({ hasText: 'add' }).click();
 		expect(await page.textContent('a')).toBe('remove');
 
 		expect(errored).toBe(false);