Skip to content

Commit a256748

Browse files
committed
test(ui): refresh Select Provider tests for createConnection flow
Mocks the ConfigureSSO flow context so the Continue handler can be verified end-to-end. Asserts setProvider is called with the backend provider id, followed by createConnection, then goNext, and adds coverage for the loading state and the rejection path where the wizard must not advance and the error must surface in the card.
1 parent 1f38f18 commit a256748

1 file changed

Lines changed: 142 additions & 12 deletions

File tree

packages/ui/src/components/ConfigureSSO/steps/__tests__/SelectProviderStep.test.tsx

Lines changed: 142 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import { ClerkRuntimeError } from '@clerk/shared/error';
2+
import type { ReactElement } from 'react';
13
import { describe, expect, it, vi } from 'vitest';
24

35
import { bindCreateFixtures } from '@/test/create-fixtures';
4-
import { render, screen } from '@/test/utils';
6+
import { render, screen, waitFor } from '@/test/utils';
7+
import { CardStateProvider } from '@/ui/elements/contexts';
58

69
const goNext = vi.fn();
710
const goPrev = vi.fn();
@@ -23,30 +26,64 @@ vi.mock('../../elements/Wizard', () => ({
2326
}),
2427
}));
2528

29+
const setProvider = vi.fn();
30+
const clearProvider = vi.fn();
31+
const createConnection = vi.fn();
32+
33+
vi.mock('../../ConfigureSSOContext', () => ({
34+
useConfigureSSOFlow: () => ({
35+
enterpriseConnection: undefined,
36+
isLoading: false,
37+
provider: undefined,
38+
setProvider,
39+
clearProvider,
40+
createConnection,
41+
}),
42+
}));
43+
2644
import { SelectProviderStep } from '../SelectProviderStep';
2745

2846
const { createFixtures } = bindCreateFixtures('ConfigureSSO');
2947

48+
const renderStep = (
49+
wrapper: React.ComponentType<{ children?: React.ReactNode }>,
50+
ui: ReactElement = <SelectProviderStep />,
51+
) => {
52+
return render(<CardStateProvider>{ui}</CardStateProvider>, { wrapper });
53+
};
54+
55+
const resetMocks = () => {
56+
goNext.mockReset();
57+
goPrev.mockReset();
58+
setProvider.mockReset();
59+
clearProvider.mockReset();
60+
createConnection.mockReset();
61+
createConnection.mockResolvedValue(undefined);
62+
};
63+
3064
describe('SelectProviderStep', () => {
3165
it('mounts and renders the step header', async () => {
66+
resetMocks();
3267
const { wrapper } = await createFixtures();
33-
render(<SelectProviderStep />, { wrapper });
68+
renderStep(wrapper);
3469

3570
expect(screen.getByRole('heading', { name: 'Select provider' })).toBeInTheDocument();
3671
expect(screen.getByText('Select your identity provider')).toBeInTheDocument();
3772
});
3873

3974
it('renders both SAML provider tiles with their labels', async () => {
75+
resetMocks();
4076
const { wrapper } = await createFixtures();
41-
render(<SelectProviderStep />, { wrapper });
77+
renderStep(wrapper);
4278

4379
expect(screen.getByRole('button', { name: 'Okta Workforce' })).toBeInTheDocument();
4480
expect(screen.getByRole('button', { name: 'Custom SAML Provider' })).toBeInTheDocument();
4581
});
4682

4783
it('loads each tile icon from img.clerk.com', async () => {
84+
resetMocks();
4885
const { wrapper } = await createFixtures();
49-
const { container } = render(<SelectProviderStep />, { wrapper });
86+
const { container } = renderStep(wrapper);
5087

5188
// Emotion serializes sx into stylesheets, so we check both inline + the document's collected styles
5289
const iconSpans = Array.from(container.querySelectorAll('button span[aria-hidden]'));
@@ -62,15 +99,17 @@ describe('SelectProviderStep', () => {
6299
});
63100

64101
it('disables Continue when no provider is selected', async () => {
102+
resetMocks();
65103
const { wrapper } = await createFixtures();
66-
render(<SelectProviderStep />, { wrapper });
104+
renderStep(wrapper);
67105

68106
expect(screen.getByRole('button', { name: /Continue/i })).toBeDisabled();
69107
});
70108

71109
it('marks the clicked tile as pressed and enables Continue', async () => {
110+
resetMocks();
72111
const { wrapper } = await createFixtures();
73-
const { userEvent } = render(<SelectProviderStep />, { wrapper });
112+
const { userEvent } = renderStep(wrapper);
74113

75114
const oktaTile = screen.getByRole('button', { name: 'Okta Workforce' });
76115
expect(oktaTile).toHaveAttribute('aria-pressed', 'false');
@@ -82,8 +121,9 @@ describe('SelectProviderStep', () => {
82121
});
83122

84123
it('flips selection when a different tile is clicked', async () => {
124+
resetMocks();
85125
const { wrapper } = await createFixtures();
86-
const { userEvent } = render(<SelectProviderStep />, { wrapper });
126+
const { userEvent } = renderStep(wrapper);
87127

88128
const oktaTile = screen.getByRole('button', { name: 'Okta Workforce' });
89129
const customSamlTile = screen.getByRole('button', { name: 'Custom SAML Provider' });
@@ -97,20 +137,110 @@ describe('SelectProviderStep', () => {
97137
expect(customSamlTile).toHaveAttribute('aria-pressed', 'true');
98138
});
99139

100-
it('calls goNext when Continue is clicked after a selection', async () => {
101-
goNext.mockClear();
140+
it('calls setProvider, createConnection, then goNext when Continue is clicked', async () => {
141+
resetMocks();
142+
const callOrder: string[] = [];
143+
setProvider.mockImplementation(() => {
144+
callOrder.push('setProvider');
145+
});
146+
createConnection.mockImplementation(() => {
147+
callOrder.push('createConnection');
148+
return Promise.resolve();
149+
});
150+
goNext.mockImplementation(() => {
151+
callOrder.push('goNext');
152+
});
153+
154+
const { wrapper } = await createFixtures();
155+
const { userEvent } = renderStep(wrapper);
156+
157+
await userEvent.click(screen.getByRole('button', { name: 'Okta Workforce' }));
158+
await userEvent.click(screen.getByRole('button', { name: /Continue/i }));
159+
160+
await waitFor(() => {
161+
expect(goNext).toHaveBeenCalledTimes(1);
162+
});
163+
164+
expect(setProvider).toHaveBeenCalledWith('saml_okta');
165+
expect(createConnection).toHaveBeenCalledTimes(1);
166+
expect(callOrder).toEqual(['setProvider', 'createConnection', 'goNext']);
167+
});
168+
169+
it('forwards the Custom SAML backend provider id when selected', async () => {
170+
resetMocks();
171+
const { wrapper } = await createFixtures();
172+
const { userEvent } = renderStep(wrapper);
173+
174+
await userEvent.click(screen.getByRole('button', { name: 'Custom SAML Provider' }));
175+
await userEvent.click(screen.getByRole('button', { name: /Continue/i }));
176+
177+
await waitFor(() => {
178+
expect(goNext).toHaveBeenCalledTimes(1);
179+
});
180+
181+
expect(setProvider).toHaveBeenCalledWith('saml_custom');
182+
expect(createConnection).toHaveBeenCalledTimes(1);
183+
});
184+
185+
it('shows loading state while createConnection is pending', async () => {
186+
resetMocks();
187+
let resolveCreate: () => void = () => undefined;
188+
createConnection.mockImplementation(
189+
() =>
190+
new Promise<void>(resolve => {
191+
resolveCreate = resolve;
192+
}),
193+
);
194+
195+
const { wrapper } = await createFixtures();
196+
const { userEvent } = renderStep(wrapper);
197+
198+
await userEvent.click(screen.getByRole('button', { name: 'Okta Workforce' }));
199+
const continueButton = screen.getByRole('button', { name: /Continue/i });
200+
await userEvent.click(continueButton);
201+
202+
// While create is pending, Continue stays disabled and goNext hasn't fired.
203+
// The button's accessible name flips to the spinner's "Loading" label while pending.
204+
await waitFor(() => {
205+
expect(createConnection).toHaveBeenCalledTimes(1);
206+
});
207+
expect(continueButton).toBeDisabled();
208+
expect(goNext).not.toHaveBeenCalled();
209+
210+
resolveCreate();
211+
212+
await waitFor(() => {
213+
expect(goNext).toHaveBeenCalledTimes(1);
214+
});
215+
});
216+
217+
it('does not advance and surfaces the error when createConnection rejects', async () => {
218+
resetMocks();
219+
createConnection.mockRejectedValue(new ClerkRuntimeError('Backend unavailable', { code: 'create_failed' }));
220+
102221
const { wrapper } = await createFixtures();
103-
const { userEvent } = render(<SelectProviderStep />, { wrapper });
222+
const { userEvent, container } = renderStep(wrapper);
104223

105224
await userEvent.click(screen.getByRole('button', { name: 'Okta Workforce' }));
106225
await userEvent.click(screen.getByRole('button', { name: /Continue/i }));
107226

108-
expect(goNext).toHaveBeenCalledTimes(1);
227+
await waitFor(() => {
228+
expect(createConnection).toHaveBeenCalledTimes(1);
229+
});
230+
231+
expect(goNext).not.toHaveBeenCalled();
232+
expect(setProvider).toHaveBeenCalledWith('saml_okta');
233+
// Allow microtasks to flush so the rejection -> handleError -> setError chain settles
234+
await waitFor(() => {
235+
const text = container.textContent ?? '';
236+
expect(text).toContain('Backend unavailable');
237+
});
109238
});
110239

111240
it('disables Previous on the first step', async () => {
241+
resetMocks();
112242
const { wrapper } = await createFixtures();
113-
render(<SelectProviderStep />, { wrapper });
243+
renderStep(wrapper);
114244

115245
expect(screen.getByRole('button', { name: /Previous/i })).toBeDisabled();
116246
});

0 commit comments

Comments
 (0)