Skip to content

Commit d3a7fbe

Browse files
committed
test(ui): assert composed provider wiring via rendered output
Convert appearance/localization wiring tests to assert on rendered styles and text a user would see; drop implementation-detail context probes. Keep the moduleManager getter-resolution and fallback tests, which pin a bundle-boundary contract with no user-visible surface.
1 parent 04936da commit d3a7fbe

1 file changed

Lines changed: 26 additions & 269 deletions

File tree

packages/ui/src/composed/__tests__/composed-provider-wiring.test.tsx

Lines changed: 26 additions & 269 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
1-
import { act } from '@testing-library/react';
2-
import { useContext } from 'react';
3-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
42

53
import { bindCreateFixtures } from '@/test/create-fixtures';
64
import { render, screen, waitFor } from '@/test/utils';
7-
import { useModuleManager, useOptions } from '@/ui/contexts';
8-
import { useAppearance } from '@/ui/customizables/AppearanceContext';
9-
import { useRouter } from '@/ui/router';
5+
import { useModuleManager } from '@/ui/contexts';
6+
import { Box } from '@/ui/customizables';
107

11-
import { OrganizationProfileContext } from '../../contexts/components/OrganizationProfile';
12-
import { UserProfileContext } from '../../contexts/components/UserProfile';
8+
import { computedColors } from '../../customizables/__tests__/test-utils';
139
import { clearFetchCache } from '../../hooks';
1410
import { OrganizationProfileProvider } from '../OrganizationProfile/OrganizationProfileProvider';
1511
import { fallbackModuleManager } from '../ProfileProviderShell';
12+
import { UserProfileAccountPanel } from '../UserProfile/Account';
13+
import { UserProfileProfileSection } from '../UserProfile/AccountProfile';
1614
import { UserProfileSecurityPanel } from '../UserProfile/Security';
1715
import { UserProfilePasswordSection } from '../UserProfile/SecurityPassword';
1816
import { UserProfileProvider } from '../UserProfile/UserProfileProvider';
@@ -21,51 +19,13 @@ function patchEnvironment(clerk: any, env: any) {
2119
Object.defineProperty(clerk, '__internal_environment', { value: env, configurable: true });
2220
}
2321

24-
function ModuleManagerProbe() {
25-
const mm = useModuleManager();
26-
return (
27-
<div
28-
data-testid='mm-probe'
29-
data-has-mm={!!mm}
30-
/>
31-
);
32-
}
33-
34-
function RouterProbe() {
35-
const router = useRouter();
36-
return (
37-
<div
38-
data-testid='router-probe'
39-
data-has-navigate={typeof router.navigate === 'function'}
40-
data-has-base-navigate={typeof router.baseNavigate === 'function'}
41-
/>
42-
);
43-
}
44-
4522
describe('UserProfileProvider wiring', () => {
4623
const { createFixtures } = bindCreateFixtures('UserProfile');
4724

4825
beforeEach(() => {
4926
clearFetchCache();
5027
});
5128

52-
it("provides the clerk instance's moduleManager to children", async () => {
53-
const { wrapper, fixtures } = await createFixtures(f => {
54-
f.withUser({ email_addresses: ['test@clerk.com'], first_name: 'Test', last_name: 'User' });
55-
});
56-
patchEnvironment(fixtures.clerk, fixtures.environment);
57-
58-
render(
59-
<UserProfileProvider>
60-
<ModuleManagerProbe />
61-
</UserProfileProvider>,
62-
{ wrapper },
63-
);
64-
65-
const probe = screen.getByTestId('mm-probe');
66-
expect(probe.dataset.hasMm).toBe('true');
67-
});
68-
6929
it('falls back to fallbackModuleManager when the clerk instance exposes no moduleManager', async () => {
7030
const { wrapper, fixtures } = await createFixtures(f => {
7131
f.withUser({ email_addresses: ['test@clerk.com'], first_name: 'Test', last_name: 'User' });
@@ -97,89 +57,6 @@ describe('UserProfileProvider wiring', () => {
9757
expect(screen.getByTestId('mm-fallback-probe').dataset.isFallback).toBe('true');
9858
});
9959

100-
it('provides a router that delegates to clerk.navigate', async () => {
101-
const { wrapper, fixtures } = await createFixtures(f => {
102-
f.withUser({ email_addresses: ['test@clerk.com'], first_name: 'Test', last_name: 'User' });
103-
});
104-
patchEnvironment(fixtures.clerk, fixtures.environment);
105-
106-
render(
107-
<UserProfileProvider>
108-
<RouterProbe />
109-
</UserProfileProvider>,
110-
{ wrapper },
111-
);
112-
113-
const probe = screen.getByTestId('router-probe');
114-
expect(probe.dataset.hasNavigate).toBe('true');
115-
expect(probe.dataset.hasBaseNavigate).toBe('true');
116-
});
117-
118-
// The shell does NOT track the host app URL. Composed has no Clerk-internal
119-
// navigation between sections (consumer remounts <UserProfile.X>), so there's
120-
// no meaningful currentPath to snapshot — and observing the consumer's URL
121-
// would just trigger spurious navigation-keyed effects.
122-
describe('router.currentPath is decoupled from the host URL', () => {
123-
let originalPath: string;
124-
125-
beforeEach(() => {
126-
originalPath = window.location.pathname;
127-
});
128-
129-
afterEach(() => {
130-
window.history.replaceState(null, '', originalPath);
131-
});
132-
133-
function CurrentPathProbe() {
134-
const router = useRouter();
135-
return (
136-
<div
137-
data-testid='path-probe'
138-
data-current={router.currentPath}
139-
/>
140-
);
141-
}
142-
143-
it('does not snapshot window.location.pathname into router.currentPath', async () => {
144-
const { wrapper, fixtures } = await createFixtures(f => {
145-
f.withUser({ email_addresses: ['test@clerk.com'] });
146-
});
147-
patchEnvironment(fixtures.clerk, fixtures.environment);
148-
149-
window.history.replaceState(null, '', '/page-a');
150-
151-
render(
152-
<UserProfileProvider>
153-
<CurrentPathProbe />
154-
</UserProfileProvider>,
155-
{ wrapper },
156-
);
157-
158-
expect(screen.getByTestId('path-probe').dataset.current).toBe('');
159-
});
160-
161-
it('does not update router.currentPath on popstate', async () => {
162-
const { wrapper, fixtures } = await createFixtures(f => {
163-
f.withUser({ email_addresses: ['test@clerk.com'] });
164-
});
165-
patchEnvironment(fixtures.clerk, fixtures.environment);
166-
167-
render(
168-
<UserProfileProvider>
169-
<CurrentPathProbe />
170-
</UserProfileProvider>,
171-
{ wrapper },
172-
);
173-
174-
act(() => {
175-
window.history.pushState(null, '', '/page-b');
176-
window.dispatchEvent(new PopStateEvent('popstate'));
177-
});
178-
179-
expect(screen.getByTestId('path-probe').dataset.current).toBe('');
180-
});
181-
});
182-
18360
// The end-to-end proof that resolution actually feeds a dynamic import: render
18461
// the real composed password section, enable zxcvbn strength, type a password,
18562
// and assert the resolved manager's `import` fires for the zxcvbn module. This
@@ -224,7 +101,7 @@ describe('UserProfileProvider wiring', () => {
224101
const { wrapper, fixtures } = await createFixtures();
225102
patchEnvironment(fixtures.clerk, fixtures.environment);
226103

227-
const { container } = render(
104+
render(
228105
<UserProfileProvider>
229106
<div data-testid='should-not-render' />
230107
</UserProfileProvider>,
@@ -234,81 +111,50 @@ describe('UserProfileProvider wiring', () => {
234111
expect(screen.queryByTestId('should-not-render')).not.toBeInTheDocument();
235112
});
236113

237-
it('cascades globalAppearance from ClerkProvider into composed theme', async () => {
114+
it('cascades globalAppearance from ClerkProvider into rendered composed styles', async () => {
238115
const { wrapper, fixtures } = await createFixtures(f => {
239116
f.withUser({ email_addresses: ['test@clerk.com'], first_name: 'Test', last_name: 'User' });
240117
});
241118
patchEnvironment(fixtures.clerk, fixtures.environment);
242119

243-
// Simulate ClerkProvider setting appearance with colorPrimary
244-
fixtures.clerk.__internal_getOption = vi.fn((key: string) => {
245-
if (key === 'appearance') {
246-
return { variables: { colorPrimary: '#ff0000' } };
247-
}
248-
return undefined;
249-
});
250-
251-
function AppearanceProbe() {
252-
const { parsedInternalTheme } = useAppearance();
253-
return (
254-
<div
255-
data-testid='appearance-probe'
256-
data-color-primary={parsedInternalTheme.colors.$primary500}
257-
/>
258-
);
259-
}
120+
// Simulate ClerkProvider setting appearance with colorPrimary.
121+
fixtures.clerk.__internal_getOption = vi.fn((key: string) =>
122+
key === 'appearance' ? { variables: { colorPrimary: 'red' } } : undefined,
123+
);
260124

261125
render(
262126
<UserProfileProvider>
263-
<AppearanceProbe />
127+
<Box
128+
data-testid='primary-swatch'
129+
sx={t => ({ backgroundColor: t.colors.$primary500 })}
130+
/>
264131
</UserProfileProvider>,
265132
{ wrapper },
266133
);
267134

268-
const probe = screen.getByTestId('appearance-probe');
269-
// #ff0000 = hsla(0, 100%, 50%, 1) — the global appearance should cascade
270-
expect(probe.dataset.colorPrimary).toBe('hsla(0, 100%, 50%, 1)');
135+
expect(getComputedStyle(screen.getByTestId('primary-swatch')).backgroundColor).toBe(computedColors.red);
271136
});
272137

273-
it('threads localization from ClerkProvider into composed OptionsProvider', async () => {
138+
it('threads ClerkProvider localization into rendered composed text', async () => {
274139
const { wrapper, fixtures } = await createFixtures(f => {
275140
f.withUser({ email_addresses: ['test@clerk.com'], first_name: 'Test', last_name: 'User' });
276141
});
277142
patchEnvironment(fixtures.clerk, fixtures.environment);
278143

279-
const expectedLocalization = { locale: 'fr-FR', signIn: { start: { title: 'Bienvenue' } } };
280-
const expectedSupportEmail = 'help@clerk.dev';
281-
fixtures.clerk.__internal_getOption = vi.fn((key: string) => {
282-
if (key === 'localization') {
283-
return expectedLocalization;
284-
}
285-
if (key === 'supportEmail') {
286-
return expectedSupportEmail;
287-
}
288-
return undefined;
289-
});
290-
291-
function OptionsProbe() {
292-
const options = useOptions();
293-
return (
294-
<div
295-
data-testid='options-probe'
296-
data-locale={(options.localization as any)?.locale ?? ''}
297-
data-support-email={options.supportEmail ?? ''}
298-
/>
299-
);
300-
}
144+
fixtures.clerk.__internal_getOption = vi.fn((key: string) =>
145+
key === 'localization' ? { userProfile: { start: { headerTitle__account: 'Détails du profil' } } } : undefined,
146+
);
301147

302148
render(
303149
<UserProfileProvider>
304-
<OptionsProbe />
150+
<UserProfileAccountPanel>
151+
<UserProfileProfileSection />
152+
</UserProfileAccountPanel>
305153
</UserProfileProvider>,
306154
{ wrapper },
307155
);
308156

309-
const probe = screen.getByTestId('options-probe');
310-
expect(probe.dataset.locale).toBe('fr-FR');
311-
expect(probe.dataset.supportEmail).toBe(expectedSupportEmail);
157+
await waitFor(() => screen.getByText('Détails du profil'));
312158
});
313159

314160
it('returns null when environment is missing', async () => {
@@ -326,32 +172,6 @@ describe('UserProfileProvider wiring', () => {
326172

327173
expect(screen.queryByTestId('should-not-render')).not.toBeInTheDocument();
328174
});
329-
330-
it('forwards apiKeysProps into the UserProfile context', async () => {
331-
const { wrapper, fixtures } = await createFixtures(f => {
332-
f.withUser({ email_addresses: ['test@clerk.com'], first_name: 'Test', last_name: 'User' });
333-
});
334-
patchEnvironment(fixtures.clerk, fixtures.environment);
335-
336-
function ApiKeysProbe() {
337-
const ctx = useContext(UserProfileContext);
338-
return (
339-
<div
340-
data-testid='apikeys-probe'
341-
data-api-keys={JSON.stringify(ctx?.apiKeysProps)}
342-
/>
343-
);
344-
}
345-
346-
render(
347-
<UserProfileProvider apiKeysProps={{ perPage: 5 }}>
348-
<ApiKeysProbe />
349-
</UserProfileProvider>,
350-
{ wrapper },
351-
);
352-
353-
expect(JSON.parse(screen.getByTestId('apikeys-probe').dataset.apiKeys || 'null')).toEqual({ perPage: 5 });
354-
});
355175
});
356176

357177
describe('OrganizationProfileProvider wiring', () => {
@@ -361,30 +181,6 @@ describe('OrganizationProfileProvider wiring', () => {
361181
clearFetchCache();
362182
});
363183

364-
it("provides the clerk instance's moduleManager to children", async () => {
365-
const { wrapper, fixtures } = await createFixtures(f => {
366-
f.withOrganizations();
367-
f.withUser({
368-
email_addresses: ['test@clerk.com'],
369-
first_name: 'Test',
370-
last_name: 'User',
371-
organization_memberships: [{ name: 'TestOrg' }],
372-
});
373-
});
374-
patchEnvironment(fixtures.clerk, fixtures.environment);
375-
fixtures.clerk.organization?.getDomains.mockReturnValue(Promise.resolve({ data: [], total_count: 0 }));
376-
377-
render(
378-
<OrganizationProfileProvider>
379-
<ModuleManagerProbe />
380-
</OrganizationProfileProvider>,
381-
{ wrapper },
382-
);
383-
384-
const probe = screen.getByTestId('mm-probe');
385-
expect(probe.dataset.hasMm).toBe('true');
386-
});
387-
388184
it('falls back to fallbackModuleManager when the clerk instance exposes no moduleManager', async () => {
389185
const { wrapper, fixtures } = await createFixtures(f => {
390186
f.withOrganizations();
@@ -428,7 +224,7 @@ describe('OrganizationProfileProvider wiring', () => {
428224
});
429225
patchEnvironment(fixtures.clerk, fixtures.environment);
430226

431-
const { container } = render(
227+
render(
432228
<OrganizationProfileProvider>
433229
<div data-testid='should-not-render' />
434230
</OrganizationProfileProvider>,
@@ -437,45 +233,6 @@ describe('OrganizationProfileProvider wiring', () => {
437233

438234
expect(screen.queryByTestId('should-not-render')).not.toBeInTheDocument();
439235
});
440-
441-
it('forwards afterLeaveOrganizationUrl and apiKeysProps into the OrganizationProfile context', async () => {
442-
const { wrapper, fixtures } = await createFixtures(f => {
443-
f.withOrganizations();
444-
f.withUser({
445-
email_addresses: ['test@clerk.com'],
446-
first_name: 'Test',
447-
last_name: 'User',
448-
organization_memberships: [{ name: 'TestOrg' }],
449-
});
450-
});
451-
patchEnvironment(fixtures.clerk, fixtures.environment);
452-
fixtures.clerk.organization?.getDomains.mockReturnValue(Promise.resolve({ data: [], total_count: 0 }));
453-
454-
function OrgProbe() {
455-
const ctx = useContext(OrganizationProfileContext);
456-
return (
457-
<div
458-
data-testid='org-probe'
459-
data-after-leave={ctx?.afterLeaveOrganizationUrl ?? ''}
460-
data-api-keys={JSON.stringify(ctx?.apiKeysProps)}
461-
/>
462-
);
463-
}
464-
465-
render(
466-
<OrganizationProfileProvider
467-
afterLeaveOrganizationUrl='/bye'
468-
apiKeysProps={{ showDescription: true }}
469-
>
470-
<OrgProbe />
471-
</OrganizationProfileProvider>,
472-
{ wrapper },
473-
);
474-
475-
const probe = screen.getByTestId('org-probe');
476-
expect(probe.dataset.afterLeave).toBe('/bye');
477-
expect(JSON.parse(probe.dataset.apiKeys || 'null')).toEqual({ showDescription: true });
478-
});
479236
});
480237

481238
// The fallback exists to fail loudly instead of silently resolving `undefined`.

0 commit comments

Comments
 (0)