From 541cfb4cf5245bcd0b961ddf3288cec5ab219d07 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 28 Oct 2026 18:32:03 -0400 Subject: [PATCH 1/2] feat(mosaic): add getSearchParams to the Mosaic router hook --- .changeset/mosaic-router-search-params.md | 2 + .../use-mosaic-router.feature.test.tsx | 47 ++++++++++++++++++- .../__tests__/use-mosaic-router.ssr.test.tsx | 43 +++++++++++++++++ .../mosaic/src/hooks/use-mosaic-router.ts | 2 + 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 .changeset/mosaic-router-search-params.md create mode 100644 packages/mosaic/src/hooks/__tests__/use-mosaic-router.ssr.test.tsx diff --git a/.changeset/mosaic-router-search-params.md b/.changeset/mosaic-router-search-params.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/mosaic-router-search-params.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/mosaic/src/hooks/__tests__/use-mosaic-router.feature.test.tsx b/packages/mosaic/src/hooks/__tests__/use-mosaic-router.feature.test.tsx index b78080cd141..5df07ef02c7 100644 --- a/packages/mosaic/src/hooks/__tests__/use-mosaic-router.feature.test.tsx +++ b/packages/mosaic/src/hooks/__tests__/use-mosaic-router.feature.test.tsx @@ -1,9 +1,10 @@ import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { serveFapi } from '../../__tests__/feature/fake-fapi'; import { renderWithClerk } from '../../__tests__/feature/render'; +import type { MosaicRouter } from '../use-mosaic-router'; import { useMosaicRouter } from '../use-mosaic-router'; function Navigation({ to, hard = false }: { to: string; hard?: boolean }) { @@ -18,6 +19,21 @@ function Navigation({ to, hard = false }: { to: string; hard?: boolean }) { ); } +function CaptureRouter({ onRouter }: { onRouter: (router: MosaicRouter) => void }) { + onRouter(useMosaicRouter()); + return null; +} + +async function renderRouter() { + serveFapi(); + let captured: MosaicRouter | undefined; + await renderWithClerk( (captured = router)} />); + if (!captured) { + throw new Error('Router was not rendered'); + } + return captured; +} + describe('Mosaic router', () => { it('uses the host router for ordinary same-origin navigation', async () => { serveFapi(); @@ -87,4 +103,33 @@ describe('Mosaic router', () => { } } }); + + describe('getSearchParams', () => { + const originalUrl = window.location.href; + + afterEach(() => { + window.history.replaceState(null, '', originalUrl); + }); + + it('reads the current query with values decoded', async () => { + const router = await renderRouter(); + window.history.replaceState(null, '', '?email_address=bob%40example.com'); + expect(router.getSearchParams().get('email_address')).toBe('bob@example.com'); + }); + + it('returns null for a key that is not in the query', async () => { + const router = await renderRouter(); + window.history.replaceState(null, '', '?email_address=bob%40example.com'); + expect(router.getSearchParams().get('missing')).toBeNull(); + }); + + it('returns a fresh instance on each call', async () => { + const router = await renderRouter(); + window.history.replaceState(null, '', '?step=one'); + const first = router.getSearchParams(); + first.set('step', 'two'); + first.append('extra', 'value'); + expect(router.getSearchParams().toString()).toBe('step=one'); + }); + }); }); diff --git a/packages/mosaic/src/hooks/__tests__/use-mosaic-router.ssr.test.tsx b/packages/mosaic/src/hooks/__tests__/use-mosaic-router.ssr.test.tsx new file mode 100644 index 00000000000..84d249966f2 --- /dev/null +++ b/packages/mosaic/src/hooks/__tests__/use-mosaic-router.ssr.test.tsx @@ -0,0 +1,43 @@ +// @vitest-environment node +import { Clerk } from '@clerk/clerk-js'; +import { ClerkContextProvider } from '@clerk/shared/react'; +import { renderToString } from 'react-dom/server'; +import { describe, expect, it, vi } from 'vitest'; + +import type { MosaicRouter } from '../use-mosaic-router'; +import { useMosaicRouter } from '../use-mosaic-router'; + +function CaptureRouter({ onRouter }: { onRouter: (router: MosaicRouter) => void }) { + onRouter(useMosaicRouter()); + return null; +} + +function renderRouterOnServer() { + const clerk = new Clerk(`pk_test_${btoa('clerk.example.com$')}`); + let captured: MosaicRouter | undefined; + renderToString( + + (captured = router)} /> + , + ); + if (!captured) { + throw new Error('Router was not rendered'); + } + return captured; +} + +describe('Mosaic router on the server', () => { + it('returns empty search params when there is no window', () => { + const router = renderRouterOnServer(); + expect(router.getSearchParams().toString()).toBe(''); + }); + + it('returns empty search params when the window has no location', () => { + const router = renderRouterOnServer(); + vi.stubGlobal('window', {}); + expect(router.getSearchParams().toString()).toBe(''); + }); +}); diff --git a/packages/mosaic/src/hooks/use-mosaic-router.ts b/packages/mosaic/src/hooks/use-mosaic-router.ts index b8e5959f0de..f27567bb109 100644 --- a/packages/mosaic/src/hooks/use-mosaic-router.ts +++ b/packages/mosaic/src/hooks/use-mosaic-router.ts @@ -4,6 +4,7 @@ import { useClerk } from '@clerk/shared/react'; export interface MosaicRouter { navigate: (to: string) => void | Promise; windowNavigate: (to: URL | string) => void; + getSearchParams: () => URLSearchParams; } /** @@ -25,5 +26,6 @@ export function useMosaicRouter(): MosaicRouter { windowNavigate(to); } }, + getSearchParams: () => new URLSearchParams(typeof window !== 'undefined' ? (window.location?.search ?? '') : ''), }; } From 31644e6fd4ca7b026d09cd67510b07aff38a523c Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 28 Oct 2026 19:13:56 -0400 Subject: [PATCH 2/2] test(mosaic): cover getSearchParams reading the latest URL --- .../hooks/__tests__/use-mosaic-router.feature.test.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/mosaic/src/hooks/__tests__/use-mosaic-router.feature.test.tsx b/packages/mosaic/src/hooks/__tests__/use-mosaic-router.feature.test.tsx index 5df07ef02c7..2d15ba6a553 100644 --- a/packages/mosaic/src/hooks/__tests__/use-mosaic-router.feature.test.tsx +++ b/packages/mosaic/src/hooks/__tests__/use-mosaic-router.feature.test.tsx @@ -123,6 +123,14 @@ describe('Mosaic router', () => { expect(router.getSearchParams().get('missing')).toBeNull(); }); + it('reads the latest query after the URL changes', async () => { + const router = await renderRouter(); + window.history.replaceState(null, '', '?step=one'); + expect(router.getSearchParams().get('step')).toBe('one'); + window.history.replaceState(null, '', '?step=two'); + expect(router.getSearchParams().get('step')).toBe('two'); + }); + it('returns a fresh instance on each call', async () => { const router = await renderRouter(); window.history.replaceState(null, '', '?step=one');