From 66daf9d1fbbea937881e2ce9f67b8cd9d8d030fe Mon Sep 17 00:00:00 2001 From: Sheraff Date: Mon, 18 Aug 2025 21:36:31 +0200 Subject: [PATCH 01/12] refactor(router-core): rework the conditions under which a beforeLoad is executed or skipped --- packages/router-core/tests/load.test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 packages/router-core/tests/load.test.ts diff --git a/packages/router-core/tests/load.test.ts b/packages/router-core/tests/load.test.ts new file mode 100644 index 0000000000..865cc37b25 --- /dev/null +++ b/packages/router-core/tests/load.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test, vi } from "vitest" +import { BaseRootRoute, BaseRoute, RouterCore } from "../src" + + +describe('beforeLoad skip or exec', () => { + test('current behavior', async () => { + const rootRoute = new BaseRootRoute({}) + const beforeLoad = vi.fn(() => new Promise((resolve) => setTimeout(resolve, 100))) + const fooRoute = new BaseRoute({ + getParentRoute: () => rootRoute, + path: '/foo', + beforeLoad + }) + const routeTree = rootRoute.addChildren([fooRoute]) + const router = new RouterCore({ + routeTree + }) + router.preloadRoute({ to: '/foo' }) + await Promise.resolve() + await router.navigate({ to: '/foo' }) + + expect(beforeLoad).toHaveBeenCalledTimes(2) + }) +}) \ No newline at end of file From 98b5c3af7904bd66bf14242b75da25981f9f4bf0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 19:37:39 +0000 Subject: [PATCH 02/12] ci: apply automated fixes --- packages/router-core/tests/load.test.ts | 43 +++++++++++++------------ 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/router-core/tests/load.test.ts b/packages/router-core/tests/load.test.ts index 865cc37b25..8d12ea2472 100644 --- a/packages/router-core/tests/load.test.ts +++ b/packages/router-core/tests/load.test.ts @@ -1,24 +1,25 @@ -import { describe, expect, test, vi } from "vitest" -import { BaseRootRoute, BaseRoute, RouterCore } from "../src" - +import { describe, expect, test, vi } from 'vitest' +import { BaseRootRoute, BaseRoute, RouterCore } from '../src' describe('beforeLoad skip or exec', () => { - test('current behavior', async () => { - const rootRoute = new BaseRootRoute({}) - const beforeLoad = vi.fn(() => new Promise((resolve) => setTimeout(resolve, 100))) - const fooRoute = new BaseRoute({ - getParentRoute: () => rootRoute, - path: '/foo', - beforeLoad - }) - const routeTree = rootRoute.addChildren([fooRoute]) - const router = new RouterCore({ - routeTree - }) - router.preloadRoute({ to: '/foo' }) - await Promise.resolve() - await router.navigate({ to: '/foo' }) + test('current behavior', async () => { + const rootRoute = new BaseRootRoute({}) + const beforeLoad = vi.fn( + () => new Promise((resolve) => setTimeout(resolve, 100)), + ) + const fooRoute = new BaseRoute({ + getParentRoute: () => rootRoute, + path: '/foo', + beforeLoad, + }) + const routeTree = rootRoute.addChildren([fooRoute]) + const router = new RouterCore({ + routeTree, + }) + router.preloadRoute({ to: '/foo' }) + await Promise.resolve() + await router.navigate({ to: '/foo' }) - expect(beforeLoad).toHaveBeenCalledTimes(2) - }) -}) \ No newline at end of file + expect(beforeLoad).toHaveBeenCalledTimes(2) + }) +}) From fad30e82d0a7768bba64d7d4abe09047783f6daf Mon Sep 17 00:00:00 2001 From: Sheraff Date: Mon, 18 Aug 2025 22:16:16 +0200 Subject: [PATCH 03/12] more tests --- packages/router-core/tests/load.test.ts | 142 +++++++++++++++++++++++- 1 file changed, 136 insertions(+), 6 deletions(-) diff --git a/packages/router-core/tests/load.test.ts b/packages/router-core/tests/load.test.ts index 8d12ea2472..1d2909b64d 100644 --- a/packages/router-core/tests/load.test.ts +++ b/packages/router-core/tests/load.test.ts @@ -1,21 +1,151 @@ import { describe, expect, test, vi } from 'vitest' -import { BaseRootRoute, BaseRoute, RouterCore } from '../src' +import { BaseRootRoute, BaseRoute, RouterCore, notFound, redirect, } from '../src' +import type { RouteOptions } from '../src' + +type AnyRouteOptions = RouteOptions +type BeforeLoad = NonNullable describe('beforeLoad skip or exec', () => { - test('current behavior', async () => { + const setup = ({ beforeLoad }: { beforeLoad?: BeforeLoad }) => { const rootRoute = new BaseRootRoute({}) - const beforeLoad = vi.fn( - () => new Promise((resolve) => setTimeout(resolve, 100)), - ) + const fooRoute = new BaseRoute({ getParentRoute: () => rootRoute, path: '/foo', beforeLoad, }) - const routeTree = rootRoute.addChildren([fooRoute]) + + const barRoute = new BaseRoute({ + getParentRoute: () => rootRoute, + path: '/bar', + }) + + const routeTree = rootRoute.addChildren([fooRoute, barRoute]) + const router = new RouterCore({ routeTree, }) + + return router + } + + test('nothing happens if nothing happens', async () => { + const beforeLoad = vi.fn() + const router = setup({ beforeLoad }) + await router.load() + expect(beforeLoad).toHaveBeenCalledTimes(0) + }) + + test('exec on regular nav', async () => { + const beforeLoad = vi.fn() + const router = setup({ beforeLoad }) + await router.navigate({ to: '/foo' }) + expect(beforeLoad).toHaveBeenCalledTimes(1) + }) + + test('skip if preload is ongoing', async () => { + const beforeLoad = vi.fn( + () => new Promise((resolve) => setTimeout(resolve, 100)), + ) + const router = setup({ beforeLoad }) + router.preloadRoute({ to: '/foo' }) + await Promise.resolve() + await router.navigate({ to: '/foo' }) + + expect(beforeLoad).toHaveBeenCalledTimes(1) + }) + + test('skip if preload resolved successfully', async () => { + const beforeLoad = vi.fn(() => Promise.resolve()) + const router = setup({ beforeLoad }) + await router.preloadRoute({ to: '/foo' }) + await router.navigate({ to: '/foo' }) + + expect(beforeLoad).toHaveBeenCalledTimes(1) + }) + + test('exec if preload has rejected w/ notFound', async () => { + const beforeLoad = vi.fn(async ({ preload }) => { + if (preload) throw notFound() + await Promise.resolve() + }) + const router = setup({ + beforeLoad + }) + await router.preloadRoute({ to: '/foo' }) + await router.navigate({ to: '/foo' }) + + expect(beforeLoad).toHaveBeenCalledTimes(2) + }) + + test('exec if preload is pending, but will reject w/ notFound', async () => { + const beforeLoad = vi.fn(async ({ preload }) => { + await new Promise((resolve) => setTimeout(resolve, 100)) + if (preload) throw notFound() + }) + const router = setup({ + beforeLoad + }) + router.preloadRoute({ to: '/foo' }) + await Promise.resolve() + await router.navigate({ to: '/foo' }) + + expect(beforeLoad).toHaveBeenCalledTimes(2) + }) + + test('exec if preload has rejected w/ redirect', async () => { + const beforeLoad = vi.fn(async ({ preload }) => { + if (preload) throw redirect({ to: '/bar' }) + await Promise.resolve() + }) + const router = setup({ + beforeLoad + }) + await router.preloadRoute({ to: '/foo' }) + await router.navigate({ to: '/foo' }) + + expect(router.state.location.pathname).toBe('/foo') + expect(beforeLoad).toHaveBeenCalledTimes(2) + }) + + test('exec if preload is pending, but will reject w/ redirect', async () => { + const beforeLoad = vi.fn(async ({ preload }) => { + await new Promise((resolve) => setTimeout(resolve, 100)) + if (preload) throw redirect({ to: '/bar' }) + }) + const router = setup({ + beforeLoad + }) + router.preloadRoute({ to: '/foo' }) + await Promise.resolve() + await router.navigate({ to: '/foo' }) + + expect(router.state.location.pathname).toBe('/foo') + expect(beforeLoad).toHaveBeenCalledTimes(2) + }) + + test('exec if preload has rejected w/ regular Error', async () => { + const beforeLoad = vi.fn(async ({ preload }) => { + if (preload) throw new Error('error') + await Promise.resolve() + }) + const router = setup({ + beforeLoad + }) + await router.preloadRoute({ to: '/foo' }) + await router.navigate({ to: '/foo' }) + + expect(beforeLoad).toHaveBeenCalledTimes(2) + }) + + test('exec if preload is pending, but will reject w/ regular Error', async () => { + const beforeLoad = vi.fn(async ({ preload }) => { + await new Promise((resolve) => setTimeout(resolve, 100)) + if (preload) throw new Error('error') + }) + const router = setup({ + beforeLoad + }) router.preloadRoute({ to: '/foo' }) await Promise.resolve() await router.navigate({ to: '/foo' }) From c7291626d1c33165094faf35ca7c2ff3ee158d10 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 20:17:09 +0000 Subject: [PATCH 04/12] ci: apply automated fixes --- packages/router-core/tests/load.test.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/router-core/tests/load.test.ts b/packages/router-core/tests/load.test.ts index 1d2909b64d..66abc2e842 100644 --- a/packages/router-core/tests/load.test.ts +++ b/packages/router-core/tests/load.test.ts @@ -1,5 +1,11 @@ import { describe, expect, test, vi } from 'vitest' -import { BaseRootRoute, BaseRoute, RouterCore, notFound, redirect, } from '../src' +import { + BaseRootRoute, + BaseRoute, + RouterCore, + notFound, + redirect, +} from '../src' import type { RouteOptions } from '../src' type AnyRouteOptions = RouteOptions @@ -70,7 +76,7 @@ describe('beforeLoad skip or exec', () => { await Promise.resolve() }) const router = setup({ - beforeLoad + beforeLoad, }) await router.preloadRoute({ to: '/foo' }) await router.navigate({ to: '/foo' }) @@ -84,7 +90,7 @@ describe('beforeLoad skip or exec', () => { if (preload) throw notFound() }) const router = setup({ - beforeLoad + beforeLoad, }) router.preloadRoute({ to: '/foo' }) await Promise.resolve() @@ -99,7 +105,7 @@ describe('beforeLoad skip or exec', () => { await Promise.resolve() }) const router = setup({ - beforeLoad + beforeLoad, }) await router.preloadRoute({ to: '/foo' }) await router.navigate({ to: '/foo' }) @@ -114,7 +120,7 @@ describe('beforeLoad skip or exec', () => { if (preload) throw redirect({ to: '/bar' }) }) const router = setup({ - beforeLoad + beforeLoad, }) router.preloadRoute({ to: '/foo' }) await Promise.resolve() @@ -130,7 +136,7 @@ describe('beforeLoad skip or exec', () => { await Promise.resolve() }) const router = setup({ - beforeLoad + beforeLoad, }) await router.preloadRoute({ to: '/foo' }) await router.navigate({ to: '/foo' }) @@ -144,7 +150,7 @@ describe('beforeLoad skip or exec', () => { if (preload) throw new Error('error') }) const router = setup({ - beforeLoad + beforeLoad, }) router.preloadRoute({ to: '/foo' }) await Promise.resolve() From f68f3d6c210046a1eab4d854dd663dcc5b842640 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Mon, 18 Aug 2025 23:42:57 +0200 Subject: [PATCH 05/12] more tests --- packages/router-core/tests/load.test.ts | 28 ++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/router-core/tests/load.test.ts b/packages/router-core/tests/load.test.ts index 66abc2e842..53c83113eb 100644 --- a/packages/router-core/tests/load.test.ts +++ b/packages/router-core/tests/load.test.ts @@ -43,9 +43,25 @@ describe('beforeLoad skip or exec', () => { }) test('exec on regular nav', async () => { - const beforeLoad = vi.fn() + const beforeLoad = vi.fn(() => Promise.resolve({ hello: 'world' })) const router = setup({ beforeLoad }) - await router.navigate({ to: '/foo' }) + const navigation = router.navigate({ to: '/foo' }) + expect(beforeLoad).toHaveBeenCalledTimes(1) + expect(router.state.pendingMatches).toEqual( + expect.arrayContaining([expect.objectContaining({ id: '/foo' })]), + ) + await navigation + expect(router.state.location.pathname).toBe('/foo') + expect(router.state.matches).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: '/foo', + context: { + hello: 'world', + }, + }), + ]), + ) expect(beforeLoad).toHaveBeenCalledTimes(1) }) @@ -56,15 +72,21 @@ describe('beforeLoad skip or exec', () => { const router = setup({ beforeLoad }) router.preloadRoute({ to: '/foo' }) await Promise.resolve() + expect(router.state.cachedMatches).toEqual( + expect.arrayContaining([expect.objectContaining({ id: '/foo' })]), + ) await router.navigate({ to: '/foo' }) expect(beforeLoad).toHaveBeenCalledTimes(1) }) test('skip if preload resolved successfully', async () => { - const beforeLoad = vi.fn(() => Promise.resolve()) + const beforeLoad = vi.fn() const router = setup({ beforeLoad }) await router.preloadRoute({ to: '/foo' }) + expect(router.state.cachedMatches).toEqual( + expect.arrayContaining([expect.objectContaining({ id: '/foo' })]), + ) await router.navigate({ to: '/foo' }) expect(beforeLoad).toHaveBeenCalledTimes(1) From 0b56d04333081aa07386a6ed98346052dff797e8 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Tue, 19 Aug 2025 00:03:42 +0200 Subject: [PATCH 06/12] fix test setup: use new memory history for every test --- packages/router-core/tests/load.test.ts | 34 +++++++++++++------------ 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/router-core/tests/load.test.ts b/packages/router-core/tests/load.test.ts index 53c83113eb..ddf8c8a2fa 100644 --- a/packages/router-core/tests/load.test.ts +++ b/packages/router-core/tests/load.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test, vi } from 'vitest' +import { createMemoryHistory } from '@tanstack/history' import { BaseRootRoute, BaseRoute, @@ -30,12 +31,13 @@ describe('beforeLoad skip or exec', () => { const router = new RouterCore({ routeTree, + history: createMemoryHistory(), }) return router } - test('nothing happens if nothing happens', async () => { + test('baseline', async () => { const beforeLoad = vi.fn() const router = setup({ beforeLoad }) await router.load() @@ -65,13 +67,10 @@ describe('beforeLoad skip or exec', () => { expect(beforeLoad).toHaveBeenCalledTimes(1) }) - test('skip if preload is ongoing', async () => { - const beforeLoad = vi.fn( - () => new Promise((resolve) => setTimeout(resolve, 100)), - ) + test('skip if resolved preload (success)', async () => { + const beforeLoad = vi.fn() const router = setup({ beforeLoad }) - router.preloadRoute({ to: '/foo' }) - await Promise.resolve() + await router.preloadRoute({ to: '/foo' }) expect(router.state.cachedMatches).toEqual( expect.arrayContaining([expect.objectContaining({ id: '/foo' })]), ) @@ -80,10 +79,13 @@ describe('beforeLoad skip or exec', () => { expect(beforeLoad).toHaveBeenCalledTimes(1) }) - test('skip if preload resolved successfully', async () => { - const beforeLoad = vi.fn() + test('skip if pending preload (success)', async () => { + const beforeLoad = vi.fn( + () => new Promise((resolve) => setTimeout(resolve, 100)), + ) const router = setup({ beforeLoad }) - await router.preloadRoute({ to: '/foo' }) + router.preloadRoute({ to: '/foo' }) + await Promise.resolve() expect(router.state.cachedMatches).toEqual( expect.arrayContaining([expect.objectContaining({ id: '/foo' })]), ) @@ -92,7 +94,7 @@ describe('beforeLoad skip or exec', () => { expect(beforeLoad).toHaveBeenCalledTimes(1) }) - test('exec if preload has rejected w/ notFound', async () => { + test('exec if rejected preload (notFound)', async () => { const beforeLoad = vi.fn(async ({ preload }) => { if (preload) throw notFound() await Promise.resolve() @@ -106,7 +108,7 @@ describe('beforeLoad skip or exec', () => { expect(beforeLoad).toHaveBeenCalledTimes(2) }) - test('exec if preload is pending, but will reject w/ notFound', async () => { + test('exec if pending preload (notFound)', async () => { const beforeLoad = vi.fn(async ({ preload }) => { await new Promise((resolve) => setTimeout(resolve, 100)) if (preload) throw notFound() @@ -121,7 +123,7 @@ describe('beforeLoad skip or exec', () => { expect(beforeLoad).toHaveBeenCalledTimes(2) }) - test('exec if preload has rejected w/ redirect', async () => { + test('exec if rejected preload (redirect)', async () => { const beforeLoad = vi.fn(async ({ preload }) => { if (preload) throw redirect({ to: '/bar' }) await Promise.resolve() @@ -136,7 +138,7 @@ describe('beforeLoad skip or exec', () => { expect(beforeLoad).toHaveBeenCalledTimes(2) }) - test('exec if preload is pending, but will reject w/ redirect', async () => { + test('exec if pending preload (redirect)', async () => { const beforeLoad = vi.fn(async ({ preload }) => { await new Promise((resolve) => setTimeout(resolve, 100)) if (preload) throw redirect({ to: '/bar' }) @@ -152,7 +154,7 @@ describe('beforeLoad skip or exec', () => { expect(beforeLoad).toHaveBeenCalledTimes(2) }) - test('exec if preload has rejected w/ regular Error', async () => { + test('exec if rejected preload (error)', async () => { const beforeLoad = vi.fn(async ({ preload }) => { if (preload) throw new Error('error') await Promise.resolve() @@ -166,7 +168,7 @@ describe('beforeLoad skip or exec', () => { expect(beforeLoad).toHaveBeenCalledTimes(2) }) - test('exec if preload is pending, but will reject w/ regular Error', async () => { + test('exec if pending preload (error)', async () => { const beforeLoad = vi.fn(async ({ preload }) => { await new Promise((resolve) => setTimeout(resolve, 100)) if (preload) throw new Error('error') From 67c7e043d5ce33ffd031c3cee4d452bf70970803 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 23 Aug 2025 12:17:02 +0200 Subject: [PATCH 07/12] add loader tests for exec vs. skip --- packages/router-core/tests/load.test.ts | 201 ++++++++++++++++++++++-- 1 file changed, 191 insertions(+), 10 deletions(-) diff --git a/packages/router-core/tests/load.test.ts b/packages/router-core/tests/load.test.ts index ddf8c8a2fa..466d481a77 100644 --- a/packages/router-core/tests/load.test.ts +++ b/packages/router-core/tests/load.test.ts @@ -11,6 +11,7 @@ import type { RouteOptions } from '../src' type AnyRouteOptions = RouteOptions type BeforeLoad = NonNullable +type Loader = NonNullable describe('beforeLoad skip or exec', () => { const setup = ({ beforeLoad }: { beforeLoad?: BeforeLoad }) => { @@ -67,22 +68,21 @@ describe('beforeLoad skip or exec', () => { expect(beforeLoad).toHaveBeenCalledTimes(1) }) - test('skip if resolved preload (success)', async () => { + test('exec if resolved preload (success)', async () => { const beforeLoad = vi.fn() const router = setup({ beforeLoad }) await router.preloadRoute({ to: '/foo' }) expect(router.state.cachedMatches).toEqual( expect.arrayContaining([expect.objectContaining({ id: '/foo' })]), ) + await sleep(10) await router.navigate({ to: '/foo' }) - expect(beforeLoad).toHaveBeenCalledTimes(1) + expect(beforeLoad).toHaveBeenCalledTimes(2) }) - test('skip if pending preload (success)', async () => { - const beforeLoad = vi.fn( - () => new Promise((resolve) => setTimeout(resolve, 100)), - ) + test('exec if pending preload (success)', async () => { + const beforeLoad = vi.fn(() => sleep(100)) const router = setup({ beforeLoad }) router.preloadRoute({ to: '/foo' }) await Promise.resolve() @@ -91,7 +91,7 @@ describe('beforeLoad skip or exec', () => { ) await router.navigate({ to: '/foo' }) - expect(beforeLoad).toHaveBeenCalledTimes(1) + expect(beforeLoad).toHaveBeenCalledTimes(2) }) test('exec if rejected preload (notFound)', async () => { @@ -103,6 +103,7 @@ describe('beforeLoad skip or exec', () => { beforeLoad, }) await router.preloadRoute({ to: '/foo' }) + await sleep(10) await router.navigate({ to: '/foo' }) expect(beforeLoad).toHaveBeenCalledTimes(2) @@ -110,7 +111,7 @@ describe('beforeLoad skip or exec', () => { test('exec if pending preload (notFound)', async () => { const beforeLoad = vi.fn(async ({ preload }) => { - await new Promise((resolve) => setTimeout(resolve, 100)) + await sleep(100) if (preload) throw notFound() }) const router = setup({ @@ -132,6 +133,7 @@ describe('beforeLoad skip or exec', () => { beforeLoad, }) await router.preloadRoute({ to: '/foo' }) + await sleep(10) await router.navigate({ to: '/foo' }) expect(router.state.location.pathname).toBe('/foo') @@ -140,7 +142,7 @@ describe('beforeLoad skip or exec', () => { test('exec if pending preload (redirect)', async () => { const beforeLoad = vi.fn(async ({ preload }) => { - await new Promise((resolve) => setTimeout(resolve, 100)) + await sleep(100) if (preload) throw redirect({ to: '/bar' }) }) const router = setup({ @@ -163,6 +165,7 @@ describe('beforeLoad skip or exec', () => { beforeLoad, }) await router.preloadRoute({ to: '/foo' }) + await sleep(10) await router.navigate({ to: '/foo' }) expect(beforeLoad).toHaveBeenCalledTimes(2) @@ -170,7 +173,7 @@ describe('beforeLoad skip or exec', () => { test('exec if pending preload (error)', async () => { const beforeLoad = vi.fn(async ({ preload }) => { - await new Promise((resolve) => setTimeout(resolve, 100)) + await sleep(100) if (preload) throw new Error('error') }) const router = setup({ @@ -183,3 +186,181 @@ describe('beforeLoad skip or exec', () => { expect(beforeLoad).toHaveBeenCalledTimes(2) }) }) + +describe('loader skip or exec', () => { + const setup = ({ loader }: { loader?: Loader }) => { + const rootRoute = new BaseRootRoute({}) + + const fooRoute = new BaseRoute({ + getParentRoute: () => rootRoute, + path: '/foo', + loader, + }) + + const barRoute = new BaseRoute({ + getParentRoute: () => rootRoute, + path: '/bar', + }) + + const routeTree = rootRoute.addChildren([fooRoute, barRoute]) + + const router = new RouterCore({ + routeTree, + history: createMemoryHistory(), + }) + + return router + } + + test('baseline', async () => { + const loader = vi.fn() + const router = setup({ loader }) + await router.load() + expect(loader).toHaveBeenCalledTimes(0) + }) + + test('exec on regular nav', async () => { + const loader = vi.fn(() => Promise.resolve({ hello: 'world' })) + const router = setup({ loader }) + const navigation = router.navigate({ to: '/foo' }) + expect(loader).toHaveBeenCalledTimes(1) + expect(router.state.pendingMatches).toEqual( + expect.arrayContaining([expect.objectContaining({ id: '/foo' })]), + ) + await navigation + expect(router.state.location.pathname).toBe('/foo') + expect(router.state.matches).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: '/foo', + loaderData: { + hello: 'world', + }, + }), + ]), + ) + expect(loader).toHaveBeenCalledTimes(1) + }) + + test('exec if resolved preload (success)', async () => { + const loader = vi.fn() + const router = setup({ loader }) + await router.preloadRoute({ to: '/foo' }) + expect(router.state.cachedMatches).toEqual( + expect.arrayContaining([expect.objectContaining({ id: '/foo' })]), + ) + await sleep(10) + await router.navigate({ to: '/foo' }) + + expect(loader).toHaveBeenCalledTimes(2) + }) + + test('skip if pending preload (success)', async () => { + const loader = vi.fn(() => sleep(100)) + const router = setup({ loader }) + router.preloadRoute({ to: '/foo' }) + await Promise.resolve() + expect(router.state.cachedMatches).toEqual( + expect.arrayContaining([expect.objectContaining({ id: '/foo' })]), + ) + await router.navigate({ to: '/foo' }) + + expect(loader).toHaveBeenCalledTimes(1) + }) + + test('exec if rejected preload (notFound)', async () => { + const loader = vi.fn(async ({ preload }) => { + if (preload) throw notFound() + await Promise.resolve() + }) + const router = setup({ + loader, + }) + await router.preloadRoute({ to: '/foo' }) + await sleep(10) + await router.navigate({ to: '/foo' }) + + expect(loader).toHaveBeenCalledTimes(2) + }) + + test('skip if pending preload (notFound)', async () => { + const loader = vi.fn(async ({ preload }) => { + await sleep(100) + if (preload) throw notFound() + }) + const router = setup({ + loader, + }) + router.preloadRoute({ to: '/foo' }) + await Promise.resolve() + await router.navigate({ to: '/foo' }) + + expect(loader).toHaveBeenCalledTimes(1) + }) + + test('exec if rejected preload (redirect)', async () => { + const loader = vi.fn(async ({ preload }) => { + if (preload) throw redirect({ to: '/bar' }) + await Promise.resolve() + }) + const router = setup({ + loader, + }) + await router.preloadRoute({ to: '/foo' }) + await sleep(10) + await router.navigate({ to: '/foo' }) + + expect(router.state.location.pathname).toBe('/foo') + expect(loader).toHaveBeenCalledTimes(2) + }) + + test('skip if pending preload (redirect)', async () => { + const loader = vi.fn(async ({ preload }) => { + await sleep(100) + if (preload) throw redirect({ to: '/bar' }) + }) + const router = setup({ + loader, + }) + router.preloadRoute({ to: '/foo' }) + await Promise.resolve() + await router.navigate({ to: '/foo' }) + + expect(router.state.location.pathname).toBe('/bar') + expect(loader).toHaveBeenCalledTimes(1) + }) + + test('exec if rejected preload (error)', async () => { + const loader = vi.fn(async ({ preload }) => { + if (preload) throw new Error('error') + await Promise.resolve() + }) + const router = setup({ + loader, + }) + await router.preloadRoute({ to: '/foo' }) + await sleep(10) + await router.navigate({ to: '/foo' }) + + expect(loader).toHaveBeenCalledTimes(2) + }) + + test('skip if pending preload (error)', async () => { + const loader = vi.fn(async ({ preload }) => { + await sleep(100) + if (preload) throw new Error('error') + }) + const router = setup({ + loader, + }) + router.preloadRoute({ to: '/foo' }) + await Promise.resolve() + await router.navigate({ to: '/foo' }) + + expect(loader).toHaveBeenCalledTimes(1) + }) +}) + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} From c177e0b1c278917e05b21a7c313d7e83435a30a1 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 23 Aug 2025 12:55:16 +0200 Subject: [PATCH 08/12] add skip loader test when staleTime --- packages/router-core/tests/load.test.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/router-core/tests/load.test.ts b/packages/router-core/tests/load.test.ts index 466d481a77..511f301cb3 100644 --- a/packages/router-core/tests/load.test.ts +++ b/packages/router-core/tests/load.test.ts @@ -188,13 +188,15 @@ describe('beforeLoad skip or exec', () => { }) describe('loader skip or exec', () => { - const setup = ({ loader }: { loader?: Loader }) => { + const setup = ({ loader, staleTime }: { loader?: Loader, staleTime?: number }) => { const rootRoute = new BaseRootRoute({}) const fooRoute = new BaseRoute({ getParentRoute: () => rootRoute, path: '/foo', loader, + staleTime, + gcTime: staleTime, }) const barRoute = new BaseRoute({ @@ -255,6 +257,19 @@ describe('loader skip or exec', () => { expect(loader).toHaveBeenCalledTimes(2) }) + test('skip if resolved preload (success) within staleTime duration', async () => { + const loader = vi.fn() + const router = setup({ loader, staleTime: 1000 }) + await router.preloadRoute({ to: '/foo' }) + expect(router.state.cachedMatches).toEqual( + expect.arrayContaining([expect.objectContaining({ id: '/foo' })]), + ) + await sleep(10) + await router.navigate({ to: '/foo' }) + + expect(loader).toHaveBeenCalledTimes(1) + }) + test('skip if pending preload (success)', async () => { const loader = vi.fn(() => sleep(100)) const router = setup({ loader }) From 2a291e07bbc48f8f1ccd7235a08cb0554e1e2e14 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 23 Aug 2025 10:56:07 +0000 Subject: [PATCH 09/12] ci: apply automated fixes --- packages/router-core/tests/load.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/router-core/tests/load.test.ts b/packages/router-core/tests/load.test.ts index 511f301cb3..90e9ad8ecd 100644 --- a/packages/router-core/tests/load.test.ts +++ b/packages/router-core/tests/load.test.ts @@ -188,7 +188,13 @@ describe('beforeLoad skip or exec', () => { }) describe('loader skip or exec', () => { - const setup = ({ loader, staleTime }: { loader?: Loader, staleTime?: number }) => { + const setup = ({ + loader, + staleTime, + }: { + loader?: Loader + staleTime?: number + }) => { const rootRoute = new BaseRootRoute({}) const fooRoute = new BaseRoute({ From 1bfb9f4feef692a7f1c7de4173346ef6d668d3c1 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sun, 24 Aug 2025 10:16:45 +0200 Subject: [PATCH 10/12] add onStay test --- packages/router-core/tests/load.test.ts | 70 +++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/packages/router-core/tests/load.test.ts b/packages/router-core/tests/load.test.ts index 90e9ad8ecd..8656d50d3c 100644 --- a/packages/router-core/tests/load.test.ts +++ b/packages/router-core/tests/load.test.ts @@ -382,6 +382,76 @@ describe('loader skip or exec', () => { }) }) +test('exec on stay (beforeLoad & loader)', async () => { + let rootBeforeLoadResolved = false + const rootBeforeLoad = vi.fn(async () => { + await sleep(100) + rootBeforeLoadResolved = true + }) + const rootLoader = vi.fn(() => sleep(10)) + const rootRoute = new BaseRootRoute({ + beforeLoad: rootBeforeLoad, + loader: rootLoader, + }) + + let layoutBeforeLoadResolved = false + const layoutBeforeLoad = vi.fn(async () => { + await sleep(100) + layoutBeforeLoadResolved = true + }) + const layoutLoader = vi.fn(() => sleep(10)) + const layoutRoute = new BaseRoute({ + getParentRoute: () => rootRoute, + beforeLoad: layoutBeforeLoad, + loader: layoutLoader, + id: '/_layout', + }) + + const fooRoute = new BaseRoute({ + getParentRoute: () => layoutRoute, + path: '/foo', + }) + const barRoute = new BaseRoute({ + getParentRoute: () => layoutRoute, + path: '/bar', + }) + + const routeTree = rootRoute.addChildren([layoutRoute.addChildren([fooRoute, barRoute])]) + + const router = new RouterCore({ + routeTree, + history: createMemoryHistory(), + defaultStaleTime: 1000, + defaultGcTime: 1000, + }) + + await router.navigate({ to: '/foo' }) + expect(router.state.location.pathname).toBe('/foo') + + rootBeforeLoadResolved = false + layoutBeforeLoadResolved = false + vi.clearAllMocks() + + /* + * When navigating between sibling routes, + * do the parent routes get re-executed? + */ + + await router.navigate({ to: '/bar' }) + + // beforeLoad always re-executes + expect(rootBeforeLoad).toHaveBeenCalledTimes(1) + expect(layoutBeforeLoad).toHaveBeenCalledTimes(1) + + // loader is skipped because of staleTime + expect(rootLoader).toHaveBeenCalledTimes(0) + expect(layoutLoader).toHaveBeenCalledTimes(0) + + // beforeLoad calls were correctly awaited + expect(rootBeforeLoadResolved).toBe(true) + expect(layoutBeforeLoadResolved).toBe(true) +}) + function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } From be1b634dd1875ca244fbc7e3cbd1527913e8413f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 24 Aug 2025 08:17:33 +0000 Subject: [PATCH 11/12] ci: apply automated fixes --- packages/router-core/tests/load.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/router-core/tests/load.test.ts b/packages/router-core/tests/load.test.ts index 8656d50d3c..febaee7612 100644 --- a/packages/router-core/tests/load.test.ts +++ b/packages/router-core/tests/load.test.ts @@ -416,7 +416,9 @@ test('exec on stay (beforeLoad & loader)', async () => { path: '/bar', }) - const routeTree = rootRoute.addChildren([layoutRoute.addChildren([fooRoute, barRoute])]) + const routeTree = rootRoute.addChildren([ + layoutRoute.addChildren([fooRoute, barRoute]), + ]) const router = new RouterCore({ routeTree, From 360177fd65c6b14937df263e0e7703c154826a06 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sun, 24 Aug 2025 10:29:17 +0200 Subject: [PATCH 12/12] minor changes to onStay test --- packages/router-core/tests/load.test.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/router-core/tests/load.test.ts b/packages/router-core/tests/load.test.ts index febaee7612..5633c64abb 100644 --- a/packages/router-core/tests/load.test.ts +++ b/packages/router-core/tests/load.test.ts @@ -385,7 +385,7 @@ describe('loader skip or exec', () => { test('exec on stay (beforeLoad & loader)', async () => { let rootBeforeLoadResolved = false const rootBeforeLoad = vi.fn(async () => { - await sleep(100) + await sleep(10) rootBeforeLoadResolved = true }) const rootLoader = vi.fn(() => sleep(10)) @@ -396,7 +396,7 @@ test('exec on stay (beforeLoad & loader)', async () => { let layoutBeforeLoadResolved = false const layoutBeforeLoad = vi.fn(async () => { - await sleep(100) + await sleep(10) layoutBeforeLoadResolved = true }) const layoutLoader = vi.fn(() => sleep(10)) @@ -440,12 +440,18 @@ test('exec on stay (beforeLoad & loader)', async () => { */ await router.navigate({ to: '/bar' }) + expect(router.state.location.pathname).toBe('/bar') - // beforeLoad always re-executes + // beforeLoads always re-execute expect(rootBeforeLoad).toHaveBeenCalledTimes(1) expect(layoutBeforeLoad).toHaveBeenCalledTimes(1) - // loader is skipped because of staleTime + // beforeLoads are called in order + expect(rootBeforeLoad.mock.invocationCallOrder[0]).toBeLessThan( + layoutBeforeLoad.mock.invocationCallOrder[0]!, + ) + + // loaders are skipped because of staleTime expect(rootLoader).toHaveBeenCalledTimes(0) expect(layoutLoader).toHaveBeenCalledTimes(0)