Skip to content

Commit 9fca3bb

Browse files
feat(mosaic): add getSearchParams to the Mosaic router hook (#10503)
1 parent 6857df6 commit 9fca3bb

4 files changed

Lines changed: 101 additions & 1 deletion

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

‎packages/mosaic/src/hooks/__tests__/use-mosaic-router.feature.test.tsx‎

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { screen, waitFor } from '@testing-library/react';
22
import userEvent from '@testing-library/user-event';
3-
import { describe, expect, it, vi } from 'vitest';
3+
import { afterEach, describe, expect, it, vi } from 'vitest';
44

55
import { serveFapi } from '../../__tests__/feature/fake-fapi';
66
import { renderWithClerk } from '../../__tests__/feature/render';
7+
import type { MosaicRouter } from '../use-mosaic-router';
78
import { useMosaicRouter } from '../use-mosaic-router';
89

910
function Navigation({ to, hard = false }: { to: string; hard?: boolean }) {
@@ -18,6 +19,21 @@ function Navigation({ to, hard = false }: { to: string; hard?: boolean }) {
1819
);
1920
}
2021

22+
function CaptureRouter({ onRouter }: { onRouter: (router: MosaicRouter) => void }) {
23+
onRouter(useMosaicRouter());
24+
return null;
25+
}
26+
27+
async function renderRouter() {
28+
serveFapi();
29+
let captured: MosaicRouter | undefined;
30+
await renderWithClerk(<CaptureRouter onRouter={router => (captured = router)} />);
31+
if (!captured) {
32+
throw new Error('Router was not rendered');
33+
}
34+
return captured;
35+
}
36+
2137
describe('Mosaic router', () => {
2238
it('uses the host router for ordinary same-origin navigation', async () => {
2339
serveFapi();
@@ -87,4 +103,41 @@ describe('Mosaic router', () => {
87103
}
88104
}
89105
});
106+
107+
describe('getSearchParams', () => {
108+
const originalUrl = window.location.href;
109+
110+
afterEach(() => {
111+
window.history.replaceState(null, '', originalUrl);
112+
});
113+
114+
it('reads the current query with values decoded', async () => {
115+
const router = await renderRouter();
116+
window.history.replaceState(null, '', '?email_address=bob%40example.com');
117+
expect(router.getSearchParams().get('email_address')).toBe('bob@example.com');
118+
});
119+
120+
it('returns null for a key that is not in the query', async () => {
121+
const router = await renderRouter();
122+
window.history.replaceState(null, '', '?email_address=bob%40example.com');
123+
expect(router.getSearchParams().get('missing')).toBeNull();
124+
});
125+
126+
it('reads the latest query after the URL changes', async () => {
127+
const router = await renderRouter();
128+
window.history.replaceState(null, '', '?step=one');
129+
expect(router.getSearchParams().get('step')).toBe('one');
130+
window.history.replaceState(null, '', '?step=two');
131+
expect(router.getSearchParams().get('step')).toBe('two');
132+
});
133+
134+
it('returns a fresh instance on each call', async () => {
135+
const router = await renderRouter();
136+
window.history.replaceState(null, '', '?step=one');
137+
const first = router.getSearchParams();
138+
first.set('step', 'two');
139+
first.append('extra', 'value');
140+
expect(router.getSearchParams().toString()).toBe('step=one');
141+
});
142+
});
90143
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// @vitest-environment node
2+
import { Clerk } from '@clerk/clerk-js';
3+
import { ClerkContextProvider } from '@clerk/shared/react';
4+
import { renderToString } from 'react-dom/server';
5+
import { describe, expect, it, vi } from 'vitest';
6+
7+
import type { MosaicRouter } from '../use-mosaic-router';
8+
import { useMosaicRouter } from '../use-mosaic-router';
9+
10+
function CaptureRouter({ onRouter }: { onRouter: (router: MosaicRouter) => void }) {
11+
onRouter(useMosaicRouter());
12+
return null;
13+
}
14+
15+
function renderRouterOnServer() {
16+
const clerk = new Clerk(`pk_test_${btoa('clerk.example.com$')}`);
17+
let captured: MosaicRouter | undefined;
18+
renderToString(
19+
<ClerkContextProvider
20+
clerk={clerk}
21+
clerkStatus={clerk.status}
22+
>
23+
<CaptureRouter onRouter={router => (captured = router)} />
24+
</ClerkContextProvider>,
25+
);
26+
if (!captured) {
27+
throw new Error('Router was not rendered');
28+
}
29+
return captured;
30+
}
31+
32+
describe('Mosaic router on the server', () => {
33+
it('returns empty search params when there is no window', () => {
34+
const router = renderRouterOnServer();
35+
expect(router.getSearchParams().toString()).toBe('');
36+
});
37+
38+
it('returns empty search params when the window has no location', () => {
39+
const router = renderRouterOnServer();
40+
vi.stubGlobal('window', {});
41+
expect(router.getSearchParams().toString()).toBe('');
42+
});
43+
});

‎packages/mosaic/src/hooks/use-mosaic-router.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useClerk } from '@clerk/shared/react';
44
export interface MosaicRouter {
55
navigate: (to: string) => void | Promise<unknown>;
66
windowNavigate: (to: URL | string) => void;
7+
getSearchParams: () => URLSearchParams;
78
}
89

910
/**
@@ -25,5 +26,6 @@ export function useMosaicRouter(): MosaicRouter {
2526
windowNavigate(to);
2627
}
2728
},
29+
getSearchParams: () => new URLSearchParams(typeof window !== 'undefined' ? (window.location?.search ?? '') : ''),
2830
};
2931
}

0 commit comments

Comments
 (0)