Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Defer pushState until navigation succeeds #4070

Merged
merged 17 commits into from
Feb 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cyan-cobras-explode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

[breaking] referer header sent by fetch in load matches page's referer header, not the page itself
5 changes: 5 additions & 0 deletions .changeset/little-geckos-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

[breaking] remove sveltekit:navigation-{start,end} events
5 changes: 5 additions & 0 deletions .changeset/old-years-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

[breaking] defer pushState until navigation occurs
8 changes: 2 additions & 6 deletions documentation/docs/08-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@
title: Events
---

SvelteKit emits [CustomEvents](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) on the `window` object when certain things happen:
SvelteKit emits a `sveltekit:start` [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) on the `window` object once the app has hydrated.

- `sveltekit:start` — fired once the app has hydrated
- `sveltekit:navigation-start` — navigation has started
- `sveltekit:navigation-end` — navigation has ended

You probably won't need to use these, but they can be useful in the context of (for example) integration tests.
You probably won't need to use it, but it can be useful in the context of (for example) integration tests.
9 changes: 4 additions & 5 deletions packages/kit/src/runtime/client/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -335,11 +335,10 @@ export class Renderer {
});
} else {
if (this.router) {
this.router.goto(
new URL(navigation_result.redirect, info.url).href,
{ replaceState: true },
[...chain, info.url.pathname]
);
this.router.goto(new URL(navigation_result.redirect, info.url).href, {}, [
...chain,
info.url.pathname
]);
} else {
location.href = new URL(navigation_result.redirect, location.href).href;
}
Expand Down
20 changes: 10 additions & 10 deletions packages/kit/src/runtime/client/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -425,32 +425,32 @@ export class Router {

accepted();

if (!this.navigating) {
dispatchEvent(new CustomEvent('sveltekit:navigation-start'));
}
this.navigating++;

const pathname = normalize_path(url.pathname, this.trailing_slash);

info.url = new URL(url.origin + pathname + url.search + url.hash);

if (details) {
const change = details.replaceState ? 0 : 1;
details.state['sveltekit:index'] = this.current_history_index += change;
history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', info.url);
}
const token = (this.navigating_token = {});

await this.renderer.handle_navigation(info, chain, false, {
scroll,
keepfocus
});

this.navigating--;
if (!this.navigating) {
dispatchEvent(new CustomEvent('sveltekit:navigation-end'));

// navigation was aborted
if (this.navigating_token !== token) return;
if (!this.navigating) {
const navigation = { from, to: url };
this.callbacks.after_navigate.forEach((fn) => fn(navigation));
}

if (details) {
const change = details.replaceState ? 0 : 1;
details.state['sveltekit:index'] = this.current_history_index += change;
history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', info.url);
mrkishi marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
2 changes: 0 additions & 2 deletions packages/kit/src/runtime/server/page/load_node.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,6 @@ export async function load_node({
}
}

opts.headers.set('referer', event.url.href);

const resolved = resolve(event.url.pathname, requested.split('?')[0]);

/** @type {Response} */
Expand Down
1 change: 1 addition & 0 deletions packages/kit/test/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ declare global {
// used in tests
oops: string;
pageContext: any;
fulfil_navigation: (value: any) => void;
}

const goto: (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<slot></slot>

<a href="/routing/cancellation/a">a</a>
<a href="/routing/cancellation/b">b</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script context="module">
import { browser } from '$app/env';


/** @type {import('@sveltejs/kit').Load} */
export async function load() {
if (browser) {
await new Promise(f => {
window.fulfil_navigation = f;
});
}

return {};
}
</script>

<h1>this should not appear</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>b</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>cancellation</h1>
33 changes: 28 additions & 5 deletions packages/kit/test/apps/basics/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,10 @@ test.describe.parallel('beforeNavigate', () => {
await page.goto('/before-navigate/prevent-navigation');

try {
await clicknav('[href="/before-navigate/a"]');
await clicknav('[href="/before-navigate/a"]', { timeout: 1000 });
expect(false).toBe(true);
} catch (/** @type {any} */ e) {
expect(e.message).toMatch('Timed out');
expect(e.message).toMatch('page.waitForNavigation: Timeout 1000ms exceeded');
}

expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation');
Expand Down Expand Up @@ -1269,8 +1269,12 @@ test.describe.parallel('Load', () => {
await clicknav('[href="/load/fetch-headers"]');

const json = /** @type {string} */ (await page.textContent('pre'));
expect(JSON.parse(json)).toEqual({
referer: `${baseURL}/load/fetch-headers`,
const headers = JSON.parse(json);

expect(headers).toEqual({
// the referer will be the previous page in the client-side
// navigation case
referer: `${baseURL}/load`,
// these headers aren't particularly useful, but they allow us to verify
// that page headers are being forwarded
'sec-fetch-dest': javaScriptEnabled ? 'empty' : 'document',
Expand Down Expand Up @@ -1631,13 +1635,17 @@ test.describe.parallel('searchParams', () => {
});

test.describe.parallel('Redirects', () => {
test('redirect', async ({ page, clicknav }) => {
test('redirect', async ({ baseURL, page, clicknav, back }) => {
await page.goto('/redirect');

await clicknav('[href="/redirect/a"]');

await page.waitForURL('/redirect/c');
expect(await page.textContent('h1')).toBe('c');
expect(page.url()).toBe(`${baseURL}/redirect/c`);

await back();
expect(page.url()).toBe(`${baseURL}/redirect`);
});

test('prevents redirect loops', async ({ baseURL, page, javaScriptEnabled }) => {
Expand Down Expand Up @@ -2123,6 +2131,21 @@ test.describe.parallel('Routing', () => {
await clicknav('[href="/static.json"]');
expect(await page.textContent('body')).toBe('"static file"\n');
});

test('navigation is cancelled upon subsequent navigation', async ({
baseURL,
page,
clicknav
}) => {
await page.goto('/routing/cancellation');
await page.click('[href="/routing/cancellation/a"]');
await clicknav('[href="/routing/cancellation/b"]');

expect(await page.url()).toBe(`${baseURL}/routing/cancellation/b`);

await page.evaluate('window.fulfil_navigation && window.fulfil_navigation()');
expect(await page.url()).toBe(`${baseURL}/routing/cancellation/b`);
});
});

test.describe.parallel('Session', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/test/utils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const test: TestType<
prefetchRoutes: (urls: string[]) => Promise<void>;
};
back: () => Promise<void>;
clicknav: (selector: string) => Promise<void>;
clicknav: (selector: string, options?: { timeout?: number }) => Promise<void>;
in_view: (selector: string) => Promise<boolean>;
read_errors: (href: string) => string;
},
Expand Down
37 changes: 6 additions & 31 deletions packages/kit/test/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,6 @@ export const test = base.extend({
back: async ({ page, javaScriptEnabled }, use) => {
use(async () => {
if (javaScriptEnabled) {
await page.evaluate(() => {
window.navigated = new Promise((fulfil, reject) => {
const timeout = setTimeout(() => reject(new Error('Timed out')), 2000);
addEventListener(
'sveltekit:navigation-end',
() => {
clearTimeout(timeout);
fulfil();
},
{ once: true }
);
});
});

await Promise.all([page.goBack(), page.evaluate(() => window.navigated)]);
} else {
return page.goBack().then(() => void 0);
Expand All @@ -81,24 +67,13 @@ export const test = base.extend({

// @ts-expect-error
clicknav: async ({ page, javaScriptEnabled }, use) => {
/** @param {string} selector */
async function clicknav(selector) {
/**
* @param {string} selector
* @param {{ timeout: number }} options
*/
async function clicknav(selector, options) {
if (javaScriptEnabled) {
await page.evaluate(() => {
window.navigated = new Promise((fulfil, reject) => {
const timeout = setTimeout(() => reject(new Error('Timed out')), 2000);
addEventListener(
'sveltekit:navigation-end',
() => {
clearTimeout(timeout);
fulfil();
},
{ once: true }
);
});
});

await Promise.all([page.click(selector), page.evaluate(() => window.navigated)]);
await Promise.all([page.click(selector), page.waitForNavigation(options)]);
} else {
await page.click(selector);
}
Expand Down