diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index b473339c70..2d72760985 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -2777,24 +2777,22 @@ function applySearchMiddleware( } const routeValidateSearch = routeOptions.validateSearch - if (routeValidateSearch) { + if (includeValidateSearch && routeValidateSearch) { const validate: SearchMiddleware = ({ search, next, meta }) => { const result = next(search) - if (includeValidateSearch) { - try { - const validated = validateSearch(routeValidateSearch, result) as any - - if (meta && validated) { - for (const key in validated) { - if (!(key in result)) { - ;(meta.defaulted ||= new Map()).set(key, validated[key]) - } + try { + const validated = validateSearch(routeValidateSearch, result) as any + + if (meta && validated) { + for (const key in validated) { + if (!(key in result)) { + ;(meta.defaulted ||= new Map()).set(key, validated[key]) } } - return { ...result, ...validated } - } catch { - // ignore errors here because they are already handled in matchRoutes } + return { ...result, ...validated } + } catch { + // ignore errors here because they are already handled in matchRoutes } return result } diff --git a/packages/router-core/tests/build-location.test.ts b/packages/router-core/tests/build-location.test.ts index 14ab661ff6..1164636429 100644 --- a/packages/router-core/tests/build-location.test.ts +++ b/packages/router-core/tests/build-location.test.ts @@ -6,6 +6,7 @@ import { retainSearchParams, stripSearchParams, } from '../src' +import type { SearchMiddleware } from '../src' import { _getUserHistoryState } from '../src/router' import { createTestRouter } from './routerTestUtils' @@ -233,6 +234,67 @@ describe('buildLocation - params function receives parsed params', () => { }) describe('buildLocation - search params', () => { + test('only applies route validation when requested', async () => { + const events: Array = [] + const validateSearch = vi.fn((search: Record) => { + events.push('validate') + return { ...search, validated: true } + }) + const middleware = vi.fn>( + ({ search, next }) => { + events.push('middleware:before') + const result = next(search) + events.push('middleware:after') + return { ...result, middleware: true } + }, + ) + const rootRoute = new BaseRootRoute({ + validateSearch, + search: { middlewares: [middleware] }, + }) + const indexRoute = new BaseRoute({ + getParentRoute: () => rootRoute, + path: '/', + }) + const router = createTestRouter({ + routeTree: rootRoute.addChildren([indexRoute]), + history: createMemoryHistory({ initialEntries: ['/'] }), + }) + await router.load() + + router.buildLocation({ to: '/' }) + validateSearch.mockClear() + middleware.mockClear() + events.length = 0 + + const unvalidated = router.buildLocation({ + to: '/', + search: { explicit: true }, + }) + expect(validateSearch).not.toHaveBeenCalled() + expect(middleware).toHaveBeenCalledOnce() + expect(events).toEqual(['middleware:before', 'middleware:after']) + expect(unvalidated.search).toEqual({ explicit: true, middleware: true }) + + events.length = 0 + const validated = router.buildLocation({ + to: '/', + search: { explicit: true }, + _includeValidateSearch: true, + }) + expect(validateSearch).toHaveBeenCalledOnce() + expect(events).toEqual([ + 'middleware:before', + 'validate', + 'middleware:after', + ]) + expect(validated.search).toEqual({ + explicit: true, + middleware: true, + validated: true, + }) + }) + test('retainSearchParams should preserve current search over defaults during navigation', async () => { const rootRoute = new BaseRootRoute({ validateSearch: (search: Record) => ({