Skip to content

Commit 2db01fb

Browse files
authored
[fix] fire navigation-end event only at end of navigation (#2649)
1 parent 8df532b commit 2db01fb

File tree

8 files changed

+64
-40
lines changed

8 files changed

+64
-40
lines changed

.changeset/chatty-apricots-fly.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
[fix] fire navigation-end event only at end of navigation

packages/kit/src/runtime/client/renderer.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -204,22 +204,27 @@ export class Renderer {
204204
this._init(result);
205205
}
206206

207-
/** @param {{ path: string, query: URLSearchParams }} destination */
208-
notify({ path, query }) {
209-
dispatchEvent(new CustomEvent('sveltekit:navigation-start'));
210-
207+
/**
208+
* @param {import('./types').NavigationInfo} info
209+
* @param {string[]} chain
210+
* @param {boolean} no_cache
211+
* @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean}} [opts]
212+
*/
213+
async handle_navigation(info, chain, no_cache, opts) {
211214
if (this.started) {
212215
this.stores.navigating.set({
213216
from: {
214217
path: this.current.page.path,
215218
query: this.current.page.query
216219
},
217220
to: {
218-
path,
219-
query
221+
path: info.path,
222+
query: info.query
220223
}
221224
});
222225
}
226+
227+
await this.update(info, chain, no_cache, opts);
223228
}
224229

225230
/**
@@ -291,11 +296,12 @@ export class Renderer {
291296
}
292297

293298
await 0;
294-
dispatchEvent(new CustomEvent('sveltekit:navigation-end'));
299+
295300
this.loading.promise = null;
296301
this.loading.id = null;
297302

298303
if (!this.router) return;
304+
299305
const leaf_node = navigation_result.state.branch[navigation_result.state.branch.length - 1];
300306
if (leaf_node && leaf_node.module.router === false) {
301307
this.router.disable();

packages/kit/src/runtime/client/router.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export class Router {
3939
this.base = base;
4040
this.routes = routes;
4141
this.trailing_slash = trailing_slash;
42+
/** Keeps tracks of multiple navigations caused by redirects during rendering */
43+
this.navigating = 0;
4244

4345
/** @type {import('./renderer').Renderer} */
4446
this.renderer = renderer;
@@ -253,6 +255,11 @@ export class Router {
253255
throw new Error('Attempted to navigate to a URL that does not belong to this app');
254256
}
255257

258+
if (!this.navigating) {
259+
dispatchEvent(new CustomEvent('sveltekit:navigation-start'));
260+
}
261+
this.navigating++;
262+
256263
// remove trailing slashes
257264
if (info.path !== '/') {
258265
const has_trailing_slash = info.path.endsWith('/');
@@ -269,11 +276,11 @@ export class Router {
269276
}
270277
}
271278

272-
this.renderer.notify({
273-
path: info.path,
274-
query: info.query
275-
});
279+
await this.renderer.handle_navigation(info, chain, false, { hash, scroll, keepfocus });
276280

277-
await this.renderer.update(info, chain, false, { hash, scroll, keepfocus });
281+
this.navigating--;
282+
if (!this.navigating) {
283+
dispatchEvent(new CustomEvent('sveltekit:navigation-end'));
284+
}
278285
}
279286
}

packages/kit/test/apps/basics/src/routes/redirect/_tests.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default function (test) {
55
test('redirect', '/redirect', async ({ base, page, clicknav }) => {
66
await clicknav('[href="/redirect/a"]');
77

8-
assert.equal(page.url(), `${base}/redirect/c`);
8+
await page.waitForURL(`${base}/redirect/c`);
99
assert.equal(await page.textContent('h1'), 'c');
1010
});
1111

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<h1>c</h1>
1+
<h1>c</h1>

packages/kit/test/apps/basics/src/routes/routing/_tests.js

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -233,14 +233,10 @@ export default function (test, is_dev) {
233233
}
234234
);
235235

236-
test(
237-
'ignores navigation to URLs the app does not own',
238-
'/routing',
239-
async ({ page, clicknav }) => {
240-
await clicknav('[href="https://www.google.com"]');
241-
assert.equal(page.url(), 'https://www.google.com/');
242-
}
243-
);
236+
test('ignores navigation to URLs the app does not own', '/routing', async ({ page }) => {
237+
await page.click('[href="https://www.google.com"]');
238+
assert.equal(page.url(), 'https://www.google.com/');
239+
});
244240

245241
// skipping this test because it causes a bunch of failures locally
246242
test.skip('watch new route in dev', '/routing', async ({ page, base, js, watcher }) => {

packages/kit/test/apps/basics/src/routes/routing/shadow/_tests.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as assert from 'uvu/assert';
22

33
/** @type {import('test').TestMaker} */
44
export default function (test) {
5-
test('endpoints can shadow pages', '/routing/shadow', async ({ page, clicknav }) => {
5+
test('endpoints can shadow pages', '/routing/shadow', async ({ page }) => {
66
const random = String(Math.random());
77

88
await page.evaluate((random) => {
@@ -11,7 +11,7 @@ export default function (test) {
1111
el.value = random;
1212
}, random);
1313

14-
await clicknav('button');
14+
await page.click('button');
1515

1616
assert.equal(await page.textContent('h1'), random);
1717
});

packages/kit/test/test.js

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -217,32 +217,42 @@ function duplicate(test_fn, config, is_build) {
217217
clicknav: async (selector) => {
218218
await context.pages.js.evaluate(() => {
219219
window.navigated = new Promise((fulfil, reject) => {
220-
addEventListener('sveltekit:navigation-end', function handler() {
221-
fulfil();
222-
removeEventListener('sveltekit:navigation-end', handler);
223-
});
224-
225-
setTimeout(() => reject(new Error('Timed out')), 2000);
220+
const timeout = setTimeout(() => reject(new Error('Timed out')), 2000);
221+
addEventListener(
222+
'sveltekit:navigation-end',
223+
() => {
224+
clearTimeout(timeout);
225+
fulfil();
226+
},
227+
{ once: true }
228+
);
226229
});
227230
});
228231

229-
await context.pages.js.click(selector);
230-
await context.pages.js.evaluate(() => window.navigated);
232+
await Promise.all([
233+
context.pages.js.click(selector),
234+
context.pages.js.evaluate(() => window.navigated)
235+
]);
231236
},
232237
back: async () => {
233238
await context.pages.js.evaluate(() => {
234239
window.navigated = new Promise((fulfil, reject) => {
235-
addEventListener('sveltekit:navigation-end', function handler() {
236-
fulfil();
237-
removeEventListener('sveltekit:navigation-end', handler);
238-
});
239-
240-
setTimeout(() => reject(new Error('Timed out')), 2000);
240+
const timeout = setTimeout(() => reject(new Error('Timed out')), 2000);
241+
addEventListener(
242+
'sveltekit:navigation-end',
243+
() => {
244+
clearTimeout(timeout);
245+
fulfil();
246+
},
247+
{ once: true }
248+
);
241249
});
242250
});
243251

244-
await context.pages.js.goBack();
245-
await context.pages.js.evaluate(() => window.navigated);
252+
await Promise.all([
253+
context.pages.js.goBack(),
254+
context.pages.js.evaluate(() => window.navigated)
255+
]);
246256
},
247257
js: true,
248258
// @ts-expect-error

0 commit comments

Comments
 (0)