diff --git a/.changeset/fifty-plants-repeat.md b/.changeset/fifty-plants-repeat.md new file mode 100644 index 000000000000..63484f619c13 --- /dev/null +++ b/.changeset/fifty-plants-repeat.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: onNavigate lifecycle function diff --git a/packages/kit/package.json b/packages/kit/package.json index 6d356b4a2920..35930a953521 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -32,7 +32,7 @@ "@types/node": "^16.18.6", "@types/sade": "^1.7.4", "@types/set-cookie-parser": "^2.4.2", - "dts-buddy": "^0.1.9", + "dts-buddy": "^0.2.4", "marked": "^7.0.0", "rollup": "^3.7.0", "svelte": "^4.0.5", diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 4b8287f37a2a..409c8025ed9b 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -730,7 +730,7 @@ export interface LoadEvent< * * Setting the same header multiple times (even in separate `load` functions) is an error — you can only set a given header once. * - * You cannot add a `set-cookie` header with `setHeaders` — use the [`cookies`](https://kit.svelte.dev/docs/types#public-types-cookies) API in a server-only `load` function instead. + * You cannot add a `set-cookie` header with `setHeaders` — use the [`cookies`](https://kit.svelte.dev/docs/types#public-types-cookies) API in a server-only `load` function instead. * * `setHeaders` has no effect when a `load` function runs in the browser. */ @@ -860,6 +860,11 @@ export interface Navigation { * In case of a history back/forward navigation, the number of steps to go back/forward */ delta?: number; + /** + * A promise that resolves once the navigation is complete, and rejects if the navigation + * fails or is aborted. In the case of a `willUnload` navigation, the promise will never resolve + */ + complete: Promise; } /** @@ -872,6 +877,24 @@ export interface BeforeNavigate extends Navigation { cancel(): void; } +/** + * The argument passed to [`onNavigate`](https://kit.svelte.dev/docs/modules#$app-navigation-onnavigate) callbacks. + */ +export interface OnNavigate extends Navigation { + /** + * The type of navigation: + * - `form`: The user submitted a `
` + * - `link`: Navigation was triggered by a link click + * - `goto`: Navigation was triggered by a `goto(...)` call or a redirect + * - `popstate`: Navigation was triggered by back/forward navigation + */ + type: Exclude; + /** + * Since `onNavigate` callbacks are called immediately before a client-side navigation, they will never be called with a navigation that unloads the page. + */ + willUnload: false; +} + /** * The argument passed to [`afterNavigate`](https://kit.svelte.dev/docs/modules#$app-navigation-afternavigate) callbacks. */ @@ -886,7 +909,7 @@ export interface AfterNavigate extends Omit { */ type: Exclude; /** - * Since `afterNavigate` is called after a navigation completes, it will never be called with a navigation that unloads the page. + * Since `afterNavigate` callbacks are called after a navigation completes, they will never be called with a navigation that unloads the page. */ willUnload: false; } @@ -1007,7 +1030,7 @@ export interface RequestEvent< * * Setting the same header multiple times (even in separate `load` functions) is an error — you can only set a given header once. * - * You cannot add a `set-cookie` header with `setHeaders` — use the [`cookies`](https://kit.svelte.dev/docs/types#public-types-cookies) API instead. + * You cannot add a `set-cookie` header with `setHeaders` — use the [`cookies`](https://kit.svelte.dev/docs/types#public-types-cookies) API instead. */ setHeaders(headers: Record): void; /** @@ -1064,7 +1087,7 @@ export interface RouteDefinition { methods: HttpMethod[]; }; page: { - methods: Extract[]; + methods: Array>; }; pattern: RegExp; prerender: PrerenderOption; diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js index bea51d1b12c1..0da8f93e1599 100644 --- a/packages/kit/src/runtime/app/navigation.js +++ b/packages/kit/src/runtime/app/navigation.js @@ -101,6 +101,20 @@ export const preloadCode = /* @__PURE__ */ client_method('preload_code'); */ export const beforeNavigate = /* @__PURE__ */ client_method('before_navigate'); +/** + * A lifecycle function that runs the supplied `callback` immediately before we navigate to a new URL. + * + * If you return a `Promise`, SvelteKit will wait for it to resolve before completing the navigation. This allows you to — for example — use `document.startViewTransition`. Avoid promises that are slow to resolve, since navigation will appear stalled to the user. + * + * If a function (or a `Promise` that resolves to a function) is returned from the callback, it will be called once the DOM has updated. + * + * `onNavigate` must be called during a component initialization. It remains active as long as the component is mounted. + * @type {(callback: (navigation: import('@sveltejs/kit').OnNavigate) => import('../../types/internal.js').MaybePromise<(() => void) | void>) => void} + * @param {(navigation: import('@sveltejs/kit').OnNavigate) => void} callback + * @returns {void} + */ +export const onNavigate = /* @__PURE__ */ client_method('on_navigate'); + /** * A lifecycle function that runs the supplied `callback` when the current component mounts, and also whenever we navigate to a new URL. * diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index b1225ec3ce70..537913613bf5 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -89,6 +89,9 @@ export function create_client(app, target) { /** @type {Array<(navigation: import('@sveltejs/kit').BeforeNavigate) => void>} */ before_navigate: [], + /** @type {Array<(navigation: import('@sveltejs/kit').OnNavigate) => import('types').MaybePromise<(() => void) | void>>} */ + on_navigate: [], + /** @type {Array<(navigation: import('@sveltejs/kit').AfterNavigate) => void>} */ after_navigate: [] }; @@ -299,7 +302,8 @@ export function create_client(app, target) { url: new URL(location.href) }, willUnload: false, - type: 'enter' + type: 'enter', + complete: Promise.resolve() }; callbacks.after_navigate.forEach((fn) => fn(navigation)); @@ -910,30 +914,17 @@ export function create_client(app, target) { function before_navigate({ url, type, intent, delta }) { let should_block = false; - /** @type {import('@sveltejs/kit').Navigation} */ - const navigation = { - from: { - params: current.params, - route: { id: current.route?.id ?? null }, - url: current.url - }, - to: { - params: intent?.params ?? null, - route: { id: intent?.route?.id ?? null }, - url - }, - willUnload: !intent, - type - }; + const nav = create_navigation(current, intent, url, type); if (delta !== undefined) { - navigation.delta = delta; + nav.navigation.delta = delta; } const cancellable = { - ...navigation, + ...nav.navigation, cancel: () => { should_block = true; + nav.reject(new Error('navigation was cancelled')); } }; @@ -942,7 +933,7 @@ export function create_client(app, target) { callbacks.before_navigate.forEach((fn) => fn(cancellable)); } - return should_block ? null : navigation; + return should_block ? null : nav; } /** @@ -975,9 +966,9 @@ export function create_client(app, target) { blocked }) { const intent = get_navigation_intent(url, false); - const navigation = before_navigate({ url, type, delta, intent }); + const nav = before_navigate({ url, type, delta, intent }); - if (!navigation) { + if (!nav) { blocked(); return; } @@ -990,7 +981,7 @@ export function create_client(app, target) { navigating = true; if (started) { - stores.navigating.set(navigation); + stores.navigating.set(nav.navigation); } token = nav_token; @@ -1017,7 +1008,10 @@ export function create_client(app, target) { url = intent?.url || url; // abort if user navigated during update - if (token !== nav_token) return false; + if (token !== nav_token) { + nav.reject(new Error('navigation was aborted')); + return false; + } if (navigation_result.type === 'redirect') { if (redirect_chain.length > 10 || redirect_chain.includes(url.pathname)) { @@ -1093,6 +1087,28 @@ export function create_client(app, target) { navigation_result.props.page.url = url; } + const after_navigate = ( + await Promise.all( + callbacks.on_navigate.map((fn) => + fn(/** @type {import('@sveltejs/kit').OnNavigate} */ (nav.navigation)) + ) + ) + ).filter((value) => typeof value === 'function'); + + if (after_navigate.length > 0) { + function cleanup() { + callbacks.after_navigate = callbacks.after_navigate.filter( + // @ts-ignore + (fn) => !after_navigate.includes(fn) + ); + } + + after_navigate.push(cleanup); + + // @ts-ignore + callbacks.after_navigate.push(...after_navigate); + } + root.$set(navigation_result.props); } else { initialize(navigation_result); @@ -1142,8 +1158,10 @@ export function create_client(app, target) { restore_snapshot(current_history_index); } + nav.fulfil(undefined); + callbacks.after_navigate.forEach((fn) => - fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (navigation)) + fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation)) ); stores.navigating.set(null); @@ -1339,6 +1357,17 @@ export function create_client(app, target) { }); }, + on_navigate: (fn) => { + onMount(() => { + callbacks.on_navigate.push(fn); + + return () => { + const i = callbacks.on_navigate.indexOf(fn); + callbacks.on_navigate.splice(i, 1); + }; + }); + }, + disable_scroll_handling: () => { if (DEV && started && !updating) { throw new Error('Can only disable scroll handling during navigation'); @@ -1444,19 +1473,17 @@ export function create_client(app, target) { persist_state(); if (!navigating) { + const nav = create_navigation(current, undefined, null, 'leave'); + // If we're navigating, beforeNavigate was already called. If we end up in here during navigation, // it's due to an external or full-page-reload link, for which we don't want to call the hook again. /** @type {import('@sveltejs/kit').BeforeNavigate} */ const navigation = { - from: { - params: current.params, - route: { id: current.route?.id ?? null }, - url: current.url - }, - to: null, - willUnload: true, - type: 'leave', - cancel: () => (should_block = true) + ...nav.navigation, + cancel: () => { + should_block = true; + nav.reject(new Error('navigation was cancelled')); + } }; callbacks.before_navigate.forEach((fn) => fn(navigation)); @@ -1990,6 +2017,50 @@ function reset_focus() { } } +/** + * @param {import('./types').NavigationState} current + * @param {import('./types').NavigationIntent | undefined} intent + * @param {URL | null} url + * @param {Exclude} type + */ +function create_navigation(current, intent, url, type) { + /** @type {(value: any) => void} */ + let fulfil; + + /** @type {(error: any) => void} */ + let reject; + + const complete = new Promise((f, r) => { + fulfil = f; + reject = r; + }); + + /** @type {import('@sveltejs/kit').Navigation} */ + const navigation = { + from: { + params: current.params, + route: { id: current.route?.id ?? null }, + url: current.url + }, + to: url && { + params: intent?.params ?? null, + route: { id: intent?.route?.id ?? null }, + url + }, + willUnload: !intent, + type, + complete + }; + + return { + navigation, + // @ts-expect-error + fulfil, + // @ts-expect-error + reject + }; +} + if (DEV) { // Nasty hack to silence harmless warnings the user can do nothing about const console_warn = console.warn; diff --git a/packages/kit/src/runtime/client/singletons.js b/packages/kit/src/runtime/client/singletons.js index 9e1e7c1779a1..2d2d15095f1b 100644 --- a/packages/kit/src/runtime/client/singletons.js +++ b/packages/kit/src/runtime/client/singletons.js @@ -21,7 +21,7 @@ export function init(opts) { */ export function client_method(key) { if (!BROWSER) { - if (key === 'before_navigate' || key === 'after_navigate') { + if (key === 'before_navigate' || key === 'after_navigate' || key === 'on_navigate') { // @ts-expect-error doesn't recognize that both keys here return void so expects a async function return () => {}; } else { diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index 8fba78e51e0b..2517eb8ad052 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -2,6 +2,7 @@ import { applyAction } from '../app/forms'; import { afterNavigate, beforeNavigate, + onNavigate, goto, invalidate, invalidateAll, @@ -43,6 +44,7 @@ export interface Client { // public API, exposed via $app/navigation after_navigate: typeof afterNavigate; before_navigate: typeof beforeNavigate; + on_navigate: typeof onNavigate; disable_scroll_handling(): void; goto: typeof goto; invalidate: typeof invalidate; diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index ddd074bf290a..d341409b8886 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -266,7 +266,7 @@ export interface ServerMetadataRoute { }; methods: HttpMethod[]; prerender: PrerenderOption | undefined; - entries: Array | undefined; + entries: string[] | undefined; } export interface ServerMetadata { diff --git a/packages/kit/test/apps/basics/src/routes/before-navigate/redirect/+page.js b/packages/kit/test/apps/basics/src/routes/before-navigate/redirect/+page.js deleted file mode 100644 index a5b9dcc347e4..000000000000 --- a/packages/kit/test/apps/basics/src/routes/before-navigate/redirect/+page.js +++ /dev/null @@ -1,5 +0,0 @@ -import { redirect } from '@sveltejs/kit'; - -export function load() { - throw redirect(307, '/before-navigate/prevent-navigation'); -} diff --git a/packages/kit/test/apps/basics/src/routes/after-navigate/a/+page.svelte b/packages/kit/test/apps/basics/src/routes/navigation-lifecycle/after-navigate/a/+page.svelte similarity index 86% rename from packages/kit/test/apps/basics/src/routes/after-navigate/a/+page.svelte rename to packages/kit/test/apps/basics/src/routes/navigation-lifecycle/after-navigate/a/+page.svelte index 70209b7046fa..7e9eeeaa4b09 100644 --- a/packages/kit/test/apps/basics/src/routes/after-navigate/a/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/navigation-lifecycle/after-navigate/a/+page.svelte @@ -14,4 +14,4 @@

{from?.url.pathname} -> {to?.url.pathname}

-/b +/b diff --git a/packages/kit/test/apps/basics/src/routes/after-navigate/b/+page.svelte b/packages/kit/test/apps/basics/src/routes/navigation-lifecycle/after-navigate/b/+page.svelte similarity index 86% rename from packages/kit/test/apps/basics/src/routes/after-navigate/b/+page.svelte rename to packages/kit/test/apps/basics/src/routes/navigation-lifecycle/after-navigate/b/+page.svelte index 3db6a4022ac5..6481e3090c15 100644 --- a/packages/kit/test/apps/basics/src/routes/after-navigate/b/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/navigation-lifecycle/after-navigate/b/+page.svelte @@ -14,4 +14,4 @@

{from?.url.pathname} -> {to?.url.pathname}

-/a +/a diff --git a/packages/kit/test/apps/basics/src/routes/before-navigate/a/+page.svelte b/packages/kit/test/apps/basics/src/routes/navigation-lifecycle/before-navigate/a/+page.svelte similarity index 100% rename from packages/kit/test/apps/basics/src/routes/before-navigate/a/+page.svelte rename to packages/kit/test/apps/basics/src/routes/navigation-lifecycle/before-navigate/a/+page.svelte diff --git a/packages/kit/test/apps/basics/src/routes/before-navigate/hash-links/+page.svelte b/packages/kit/test/apps/basics/src/routes/navigation-lifecycle/before-navigate/hash-links/+page.svelte similarity index 100% rename from packages/kit/test/apps/basics/src/routes/before-navigate/hash-links/+page.svelte rename to packages/kit/test/apps/basics/src/routes/navigation-lifecycle/before-navigate/hash-links/+page.svelte diff --git a/packages/kit/test/apps/basics/src/routes/before-navigate/prevent-navigation/+page.svelte b/packages/kit/test/apps/basics/src/routes/navigation-lifecycle/before-navigate/prevent-navigation/+page.svelte similarity index 73% rename from packages/kit/test/apps/basics/src/routes/before-navigate/prevent-navigation/+page.svelte rename to packages/kit/test/apps/basics/src/routes/navigation-lifecycle/before-navigate/prevent-navigation/+page.svelte index 8a3884c54c53..6d6a56cc5020 100644 --- a/packages/kit/test/apps/basics/src/routes/before-navigate/prevent-navigation/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/navigation-lifecycle/before-navigate/prevent-navigation/+page.svelte @@ -15,9 +15,9 @@

prevent navigation

-a -redirect -self +a +redirect +self _blank external external diff --git a/packages/kit/test/apps/basics/src/routes/navigation-lifecycle/before-navigate/redirect/+page.js b/packages/kit/test/apps/basics/src/routes/navigation-lifecycle/before-navigate/redirect/+page.js new file mode 100644 index 000000000000..80f189a06a13 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/navigation-lifecycle/before-navigate/redirect/+page.js @@ -0,0 +1,5 @@ +import { redirect } from '@sveltejs/kit'; + +export function load() { + throw redirect(307, '/navigation-lifecycle/before-navigate/prevent-navigation'); +} diff --git a/packages/kit/test/apps/basics/src/routes/navigation-lifecycle/on-navigate/[x]/+page.svelte b/packages/kit/test/apps/basics/src/routes/navigation-lifecycle/on-navigate/[x]/+page.svelte new file mode 100644 index 000000000000..35e8ebb73349 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/navigation-lifecycle/on-navigate/[x]/+page.svelte @@ -0,0 +1,21 @@ + + +

{from?.url.pathname} -> {to?.url.pathname} ({type ?? '...'})

+/b diff --git a/packages/kit/test/apps/basics/test/cross-platform/client.test.js b/packages/kit/test/apps/basics/test/cross-platform/client.test.js index 77e8d2a4877c..0e8f32c556fb 100644 --- a/packages/kit/test/apps/basics/test/cross-platform/client.test.js +++ b/packages/kit/test/apps/basics/test/cross-platform/client.test.js @@ -111,19 +111,19 @@ test.describe('a11y', () => { }); }); -test.describe('beforeNavigate', () => { - test('prevents navigation triggered by link click', async ({ page, baseURL }) => { - await page.goto('/before-navigate/prevent-navigation'); +test.describe('Navigation lifecycle functions', () => { + test('beforeNavigate prevents navigation triggered by link click', async ({ page, baseURL }) => { + await page.goto('/navigation-lifecycle/before-navigate/prevent-navigation'); - await page.click('[href="/before-navigate/a"]'); + await page.click('[href="/navigation-lifecycle/before-navigate/a"]'); await page.waitForLoadState('networkidle'); - expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation'); + expect(page.url()).toBe(baseURL + '/navigation-lifecycle/before-navigate/prevent-navigation'); expect(await page.innerHTML('pre')).toBe('1 false link'); }); - test('prevents navigation to external', async ({ page, baseURL }) => { - await page.goto('/before-navigate/prevent-navigation'); + test('beforeNavigate prevents navigation to external', async ({ page, baseURL }) => { + await page.goto('/navigation-lifecycle/before-navigate/prevent-navigation'); await page.click('h1'); // The browsers block attempts to prevent navigation on a frame that's never had a user gesture. page.on('dialog', (dialog) => dialog.dismiss()); @@ -131,35 +131,43 @@ test.describe('beforeNavigate', () => { page.click('a[href="https://google.de"]'); // do NOT await this, promise only resolves after successful navigation, which never happens await page.waitForTimeout(500); await expect(page.locator('pre')).toHaveText('1 true link'); - expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation'); + expect(page.url()).toBe(baseURL + '/navigation-lifecycle/before-navigate/prevent-navigation'); }); - test('prevents navigation triggered by goto', async ({ page, app, baseURL }) => { - await page.goto('/before-navigate/prevent-navigation'); - await app.goto('/before-navigate/a'); - expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation'); + test('beforeNavigate prevents navigation triggered by goto', async ({ page, app, baseURL }) => { + await page.goto('/navigation-lifecycle/before-navigate/prevent-navigation'); + await app.goto('/navigation-lifecycle/before-navigate/a'); + expect(page.url()).toBe(baseURL + '/navigation-lifecycle/before-navigate/prevent-navigation'); expect(await page.innerHTML('pre')).toBe('1 false goto'); }); - test('prevents external navigation triggered by goto', async ({ page, app, baseURL }) => { - await page.goto('/before-navigate/prevent-navigation'); + test('beforeNavigate prevents external navigation triggered by goto', async ({ + page, + app, + baseURL + }) => { + await page.goto('/navigation-lifecycle/before-navigate/prevent-navigation'); await app.goto('https://google.de'); - expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation'); + expect(page.url()).toBe(baseURL + '/navigation-lifecycle/before-navigate/prevent-navigation'); expect(await page.innerHTML('pre')).toBe('1 true goto'); }); - test('prevents navigation triggered by back button', async ({ page, app, baseURL }) => { - await page.goto('/before-navigate/a'); - await app.goto('/before-navigate/prevent-navigation'); + test('beforeNavigate prevents navigation triggered by back button', async ({ + page, + app, + baseURL + }) => { + await page.goto('/navigation-lifecycle/before-navigate/a'); + await app.goto('/navigation-lifecycle/before-navigate/prevent-navigation'); await page.click('h1'); // The browsers block attempts to prevent navigation on a frame that's never had a user gesture. await page.goBack(); expect(await page.innerHTML('pre')).toBe('1 false popstate'); - expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation'); + expect(page.url()).toBe(baseURL + '/navigation-lifecycle/before-navigate/prevent-navigation'); }); - test('prevents unload', async ({ page }) => { - await page.goto('/before-navigate/prevent-navigation'); + test('beforeNavigate prevents unload', async ({ page }) => { + await page.goto('/navigation-lifecycle/before-navigate/prevent-navigation'); await page.click('h1'); // The browsers block attempts to prevent navigation on a frame that's never had a user gesture. const type = new Promise((fulfil) => { page.on('dialog', async (dialog) => { @@ -173,58 +181,80 @@ test.describe('beforeNavigate', () => { expect(await page.innerHTML('pre')).toBe('1 true leave'); }); - test('is not triggered on redirect', async ({ page, baseURL }) => { - await page.goto('/before-navigate/prevent-navigation'); + test('beforeNavigate is not triggered on redirect', async ({ page, baseURL }) => { + await page.goto('/navigation-lifecycle/before-navigate/prevent-navigation'); - await page.click('[href="/before-navigate/redirect"]'); + await page.click('[href="/navigation-lifecycle/before-navigate/redirect"]'); await page.waitForLoadState('networkidle'); - expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation'); + expect(page.url()).toBe(baseURL + '/navigation-lifecycle/before-navigate/prevent-navigation'); expect(await page.innerHTML('pre')).toBe('1 false link'); }); - test('is not triggered on target=_blank', async ({ page, baseURL }) => { - await page.goto('/before-navigate/prevent-navigation'); + test('beforeNavigate is not triggered on target=_blank', async ({ page, baseURL }) => { + await page.goto('/navigation-lifecycle/before-navigate/prevent-navigation'); await page.click('a[href="https://google.com"]'); await page.waitForTimeout(500); - expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation'); + expect(page.url()).toBe(baseURL + '/navigation-lifecycle/before-navigate/prevent-navigation'); expect(await page.innerHTML('pre')).toBe('0 false undefined'); }); - test('is not triggered on click or popstate for hash links', async ({ page }) => { - await page.goto('/before-navigate/hash-links'); + test('beforeNavigate is not triggered on click or popstate for hash links', async ({ page }) => { + await page.goto('/navigation-lifecycle/before-navigate/hash-links'); await page.click('a[href="#x"]'); await page.goBack(); expect(await page.textContent('h1')).toBe('before_navigate_ran: false'); }); - test('cancel() on an unloading navigation does not prevent subsequent beforeNavigate callbacks', async ({ + test('beforeNavigate cancel() on an unloading navigation does not prevent subsequent beforeNavigate callbacks', async ({ page, app }) => { - await page.goto('/before-navigate/prevent-navigation'); + await page.goto('/navigation-lifecycle/before-navigate/prevent-navigation'); await page.click('h1'); // The browsers block attempts to prevent navigation on a frame that's never had a user gesture. await app.goto('https://google.de'); - await app.goto('/before-navigate/prevent-navigation?x=1'); + await app.goto('/navigation-lifecycle/before-navigate/prevent-navigation?x=1'); expect(await page.innerHTML('pre')).toBe('2 false goto'); }); - test('is triggered after clicking a download link', async ({ page, baseURL }) => { - await page.goto('/before-navigate/prevent-navigation'); + test('beforeNavigate is triggered after clicking a download link', async ({ page, baseURL }) => { + await page.goto('/navigation-lifecycle/before-navigate/prevent-navigation'); await page.click('a[download]'); expect(await page.innerHTML('pre')).toBe('0 false undefined'); - await page.click('a[href="/before-navigate/a"]'); + await page.click('a[href="/navigation-lifecycle/before-navigate/a"]'); - expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation'); + expect(page.url()).toBe(baseURL + '/navigation-lifecycle/before-navigate/prevent-navigation'); expect(await page.innerHTML('pre')).toBe('1 false link'); }); + + test('afterNavigate calls callback', async ({ page, clicknav }) => { + await page.goto('/navigation-lifecycle/after-navigate/a'); + expect(await page.textContent('h1')).toBe( + 'undefined -> /navigation-lifecycle/after-navigate/a' + ); + + await clicknav('[href="/navigation-lifecycle/after-navigate/b"]'); + expect(await page.textContent('h1')).toBe( + '/navigation-lifecycle/after-navigate/a -> /navigation-lifecycle/after-navigate/b' + ); + }); + + test('onNavigate calls callback', async ({ page, clicknav }) => { + await page.goto('/navigation-lifecycle/on-navigate/a'); + expect(await page.textContent('h1')).toBe('undefined -> undefined (...)'); + + await clicknav('[href="/navigation-lifecycle/on-navigate/b"]'); + expect(await page.textContent('h1')).toBe( + '/navigation-lifecycle/on-navigate/a -> /navigation-lifecycle/on-navigate/b (link)' + ); + }); }); test.describe('Scrolling', () => { @@ -416,16 +446,6 @@ test.describe('Scrolling', () => { }); }); -test.describe('afterNavigate', () => { - test('calls callback', async ({ page, clicknav }) => { - await page.goto('/after-navigate/a'); - expect(await page.textContent('h1')).toBe('undefined -> /after-navigate/a'); - - await clicknav('[href="/after-navigate/b"]'); - expect(await page.textContent('h1')).toBe('/after-navigate/a -> /after-navigate/b'); - }); -}); - test.describe('CSS', () => { test('applies generated component styles (hides announcer)', async ({ page, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a74ab29884a..eca5670f0452 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,13 +22,13 @@ importers: version: 15.0.1(rollup@3.7.0) '@sveltejs/eslint-config': specifier: ^6.0.4 - version: 6.0.4(@typescript-eslint/eslint-plugin@6.0.0)(@typescript-eslint/parser@6.3.0)(eslint-config-prettier@9.0.0)(eslint-plugin-svelte@2.31.0)(eslint-plugin-unicorn@48.0.0)(eslint@8.45.0)(typescript@4.9.4) + version: 6.0.4(@typescript-eslint/eslint-plugin@6.0.0)(@typescript-eslint/parser@6.5.0)(eslint-config-prettier@9.0.0)(eslint-plugin-svelte@2.31.0)(eslint-plugin-unicorn@48.0.0)(eslint@8.45.0)(typescript@4.9.4) '@svitejs/changesets-changelog-github-compact': specifier: ^1.1.0 version: 1.1.0 '@typescript-eslint/eslint-plugin': specifier: ^6.0.0 - version: 6.0.0(@typescript-eslint/parser@6.3.0)(eslint@8.45.0)(typescript@4.9.4) + version: 6.0.0(@typescript-eslint/parser@6.5.0)(eslint@8.45.0)(typescript@4.9.4) eslint: specifier: ^8.45.0 version: 8.45.0 @@ -429,8 +429,8 @@ importers: specifier: ^2.4.2 version: 2.4.2 dts-buddy: - specifier: ^0.1.9 - version: 0.1.9 + specifier: ^0.2.4 + version: 0.2.4 marked: specifier: ^7.0.0 version: 7.0.0 @@ -2014,7 +2014,7 @@ packages: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true - /@sveltejs/eslint-config@6.0.4(@typescript-eslint/eslint-plugin@6.0.0)(@typescript-eslint/parser@6.3.0)(eslint-config-prettier@9.0.0)(eslint-plugin-svelte@2.31.0)(eslint-plugin-unicorn@48.0.0)(eslint@8.45.0)(typescript@4.9.4): + /@sveltejs/eslint-config@6.0.4(@typescript-eslint/eslint-plugin@6.0.0)(@typescript-eslint/parser@6.5.0)(eslint-config-prettier@9.0.0)(eslint-plugin-svelte@2.31.0)(eslint-plugin-unicorn@48.0.0)(eslint@8.45.0)(typescript@4.9.4): resolution: {integrity: sha512-U9pwmDs+DbmsnCgTfu6Bacdwqn0DuI1IQNSiQqTgzVyYfaaj+zy9ZoQCiJfxFBGXHkklyXuRHp0KMx346N0lcQ==} peerDependencies: '@typescript-eslint/eslint-plugin': '>= 5' @@ -2025,8 +2025,8 @@ packages: eslint-plugin-unicorn: '>= 47' typescript: '>= 4' dependencies: - '@typescript-eslint/eslint-plugin': 6.0.0(@typescript-eslint/parser@6.3.0)(eslint@8.45.0)(typescript@4.9.4) - '@typescript-eslint/parser': 6.3.0(eslint@8.45.0)(typescript@4.9.4) + '@typescript-eslint/eslint-plugin': 6.0.0(@typescript-eslint/parser@6.5.0)(eslint@8.45.0)(typescript@4.9.4) + '@typescript-eslint/parser': 6.5.0(eslint@8.45.0)(typescript@4.9.4) eslint: 8.45.0 eslint-config-prettier: 9.0.0(eslint@8.45.0) eslint-plugin-svelte: 2.31.0(eslint@8.45.0)(svelte@4.1.2) @@ -2222,7 +2222,7 @@ packages: '@types/node': 16.18.6 dev: true - /@typescript-eslint/eslint-plugin@6.0.0(@typescript-eslint/parser@6.3.0)(eslint@8.45.0)(typescript@4.9.4): + /@typescript-eslint/eslint-plugin@6.0.0(@typescript-eslint/parser@6.5.0)(eslint@8.45.0)(typescript@4.9.4): resolution: {integrity: sha512-xuv6ghKGoiq856Bww/yVYnXGsKa588kY3M0XK7uUW/3fJNNULKRfZfSBkMTSpqGG/8ZCXCadfh8G/z/B4aqS/A==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -2234,7 +2234,7 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.6.2 - '@typescript-eslint/parser': 6.3.0(eslint@8.45.0)(typescript@4.9.4) + '@typescript-eslint/parser': 6.5.0(eslint@8.45.0)(typescript@4.9.4) '@typescript-eslint/scope-manager': 6.0.0 '@typescript-eslint/type-utils': 6.0.0(eslint@8.45.0)(typescript@4.9.4) '@typescript-eslint/utils': 6.0.0(eslint@8.45.0)(typescript@4.9.4) @@ -2253,8 +2253,8 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@6.3.0(eslint@8.45.0)(typescript@4.9.4): - resolution: {integrity: sha512-ibP+y2Gr6p0qsUkhs7InMdXrwldjxZw66wpcQq9/PzAroM45wdwyu81T+7RibNCh8oc0AgrsyCwJByncY0Ongg==} + /@typescript-eslint/parser@6.5.0(eslint@8.45.0)(typescript@4.9.4): + resolution: {integrity: sha512-LMAVtR5GN8nY0G0BadkG0XIe4AcNMeyEy3DyhKGAh9k4pLSMBO7rF29JvDBpZGCmp5Pgz5RLHP6eCpSYZJQDuQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -2263,10 +2263,10 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 6.3.0 - '@typescript-eslint/types': 6.3.0 - '@typescript-eslint/typescript-estree': 6.3.0(typescript@4.9.4) - '@typescript-eslint/visitor-keys': 6.3.0 + '@typescript-eslint/scope-manager': 6.5.0 + '@typescript-eslint/types': 6.5.0 + '@typescript-eslint/typescript-estree': 6.5.0(typescript@4.9.4) + '@typescript-eslint/visitor-keys': 6.5.0 debug: 4.3.4 eslint: 8.45.0 typescript: 4.9.4 @@ -2282,12 +2282,12 @@ packages: '@typescript-eslint/visitor-keys': 6.0.0 dev: true - /@typescript-eslint/scope-manager@6.3.0: - resolution: {integrity: sha512-WlNFgBEuGu74ahrXzgefiz/QlVb+qg8KDTpknKwR7hMH+lQygWyx0CQFoUmMn1zDkQjTBBIn75IxtWss77iBIQ==} + /@typescript-eslint/scope-manager@6.5.0: + resolution: {integrity: sha512-A8hZ7OlxURricpycp5kdPTH3XnjG85UpJS6Fn4VzeoH4T388gQJ/PGP4ole5NfKt4WDVhmLaQ/dBLNDC4Xl/Kw==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.3.0 - '@typescript-eslint/visitor-keys': 6.3.0 + '@typescript-eslint/types': 6.5.0 + '@typescript-eslint/visitor-keys': 6.5.0 dev: true /@typescript-eslint/type-utils@6.0.0(eslint@8.45.0)(typescript@4.9.4): @@ -2315,8 +2315,8 @@ packages: engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/types@6.3.0: - resolution: {integrity: sha512-K6TZOvfVyc7MO9j60MkRNWyFSf86IbOatTKGrpTQnzarDZPYPVy0oe3myTMq7VjhfsUAbNUW8I5s+2lZvtx1gg==} + /@typescript-eslint/types@6.5.0: + resolution: {integrity: sha512-eqLLOEF5/lU8jW3Bw+8auf4lZSbbljHR2saKnYqON12G/WsJrGeeDHWuQePoEf9ro22+JkbPfWQwKEC5WwLQ3w==} engines: {node: ^16.0.0 || >=18.0.0} dev: true @@ -2341,8 +2341,8 @@ packages: - supports-color dev: true - /@typescript-eslint/typescript-estree@6.3.0(typescript@4.9.4): - resolution: {integrity: sha512-Xh4NVDaC4eYKY4O3QGPuQNp5NxBAlEvNQYOqJquR2MePNxO11E5K3t5x4M4Mx53IZvtpW+mBxIT0s274fLUocg==} + /@typescript-eslint/typescript-estree@6.5.0(typescript@4.9.4): + resolution: {integrity: sha512-q0rGwSe9e5Kk/XzliB9h2LBc9tmXX25G0833r7kffbl5437FPWb2tbpIV9wAATebC/018pGa9fwPDuvGN+LxWQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: typescript: '*' @@ -2350,13 +2350,13 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 6.3.0 - '@typescript-eslint/visitor-keys': 6.3.0 + '@typescript-eslint/types': 6.5.0 + '@typescript-eslint/visitor-keys': 6.5.0 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 - ts-api-utils: 1.0.1(typescript@4.9.4) + ts-api-utils: 1.0.2(typescript@4.9.4) typescript: 4.9.4 transitivePeerDependencies: - supports-color @@ -2390,12 +2390,12 @@ packages: eslint-visitor-keys: 3.4.2 dev: true - /@typescript-eslint/visitor-keys@6.3.0: - resolution: {integrity: sha512-kEhRRj7HnvaSjux1J9+7dBen15CdWmDnwrpyiHsFX6Qx2iW5LOBUgNefOFeh2PjWPlNwN8TOn6+4eBU3J/gupw==} + /@typescript-eslint/visitor-keys@6.5.0: + resolution: {integrity: sha512-yCB/2wkbv3hPsh02ZS8dFQnij9VVQXJMN/gbQsaaY+zxALkZnxa/wagvLEFsAWMPv7d7lxQmNsIzGU1w/T/WyA==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.3.0 - eslint-visitor-keys: 3.4.2 + '@typescript-eslint/types': 6.5.0 + eslint-visitor-keys: 3.4.3 dev: true /@typescript/twoslash@3.1.0: @@ -2852,7 +2852,7 @@ packages: normalize-path: 3.0.0 readdirp: 3.6.0 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 /chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -3214,8 +3214,8 @@ packages: resolution: {integrity: sha512-QgA6BUh2SoBYE/dSuMmeGhNdoGtGewt3Rn66xKyXoGNyjrKRXf163wuM+xeQ83p87l/3ALoB6Il1dgKyGS5pEw==} dev: true - /dts-buddy@0.1.9: - resolution: {integrity: sha512-I12BngU+NFY2njbTFCpiDqyvEFSYoMxqMpwBt1pykBEbWY6DbyDd9m0GOzd1hhpSDHDulgmQUntw5EZPNaptNA==} + /dts-buddy@0.2.4: + resolution: {integrity: sha512-41d7aGv2DXJYlzeKSKHf0GtpCC8OdpEHhz+aqjylKV5aP3fl4APzNmQ5hL5vSKZMaO/lrkrKWC+HQrxl+mcgUw==} hasBin: true dependencies: '@jridgewell/source-map': 0.3.5 @@ -3223,11 +3223,11 @@ packages: globrex: 0.1.2 kleur: 4.1.5 locate-character: 3.0.0 - magic-string: 0.30.2 + magic-string: 0.30.3 sade: 1.8.1 tiny-glob: 0.2.9 - ts-api-utils: 0.0.46(typescript@5.1.6) - typescript: 5.1.6 + ts-api-utils: 1.0.2(typescript@5.0.4) + typescript: 5.0.4 dev: true /eastasianwidth@0.2.0: @@ -3493,6 +3493,11 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /eslint@8.45.0: resolution: {integrity: sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3758,8 +3763,8 @@ packages: /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - /fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true @@ -4574,6 +4579,13 @@ packages: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 + /magic-string@0.30.3: + resolution: {integrity: sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -5405,14 +5417,14 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 /rollup@3.28.0: resolution: {integrity: sha512-d7zhvo1OUY2SXSM6pfNjgD5+d0Nz87CUp4mt8l/GgVP3oBsPwzNvSzyu1me6BSG9JIgWNTVcafIXBIyM8yQ3yw==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /rollup@3.7.0: @@ -5420,7 +5432,7 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -6171,17 +6183,17 @@ packages: regexparam: 1.3.0 dev: true - /ts-api-utils@0.0.46(typescript@5.1.6): - resolution: {integrity: sha512-YKJeSx39n0mMk+hrpyHKyTgxA3s7Pz/j1cXYR+t8HcwwZupzOR5xDGKnOEw3gmLaUeFUQt3FJD39AH9Ajn/mdA==} + /ts-api-utils@1.0.1(typescript@4.9.4): + resolution: {integrity: sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==} engines: {node: '>=16.13.0'} peerDependencies: typescript: '>=4.2.0' dependencies: - typescript: 5.1.6 + typescript: 4.9.4 dev: true - /ts-api-utils@1.0.1(typescript@4.9.4): - resolution: {integrity: sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==} + /ts-api-utils@1.0.2(typescript@4.9.4): + resolution: {integrity: sha512-Cbu4nIqnEdd+THNEsBdkolnOXhg0I8XteoHaEKgvsxpsbWda4IsUut2c187HxywQCvveojow0Dgw/amxtSKVkQ==} engines: {node: '>=16.13.0'} peerDependencies: typescript: '>=4.2.0' @@ -6189,6 +6201,15 @@ packages: typescript: 4.9.4 dev: true + /ts-api-utils@1.0.2(typescript@5.0.4): + resolution: {integrity: sha512-Cbu4nIqnEdd+THNEsBdkolnOXhg0I8XteoHaEKgvsxpsbWda4IsUut2c187HxywQCvveojow0Dgw/amxtSKVkQ==} + engines: {node: '>=16.13.0'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.0.4 + dev: true + /ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: true @@ -6451,7 +6472,7 @@ packages: postcss: 8.4.27 rollup: 3.27.2 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 /vite@4.4.9(@types/node@20.4.9)(lightningcss@1.21.5): resolution: {integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==} @@ -6487,7 +6508,7 @@ packages: postcss: 8.4.27 rollup: 3.28.0 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /vitefu@0.2.4(vite@4.4.8):