Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,42 @@ describe('EnterpriseSsoForm validation + save', () => {
});
});

it('does not log uncontrolled→controlled warnings when config resolves after load', async () => {
// Regression (#2095): the Select/Switch fields must be controlled from the
// first render. When `config` is undefined the seeded `data` is undefined,
// so `field.value` is undefined — without a defined fallback the controls
// mount uncontrolled, then warn once `config` hydrates them.
// Radix's `useControllableState` reports the transition via `console.warn`
// (React's native uncontrolled→controlled warning targets DOM inputs; these
// are button-based Radix controls), so spy on `warn`.
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
try {
const { rerender } = renderForm(undefined);

// Resolve the config — the form seeds its real values.
rerender(
<AbilityContext.Provider value={adminAbility}>
<ActiveEditorProvider>
<HeaderSlot />
<EnterpriseSsoForm organizationId="org-1" config={connectedOidc} />
</ActiveEditorProvider>
</AbilityContext.Provider>,
);

await screen.findByDisplayValue('Acme SSO');

const warned = warnSpy.mock.calls.some((call) =>
call.some(
(arg) =>
typeof arg === 'string' && /uncontrolled.*controlled/i.test(arg),
),
);
expect(warned).toBe(false);
} finally {
warnSpy.mockRestore();
}
});

it('blocks the test action and does not call testConnection when invalid', async () => {
testConnMock.mockClear();
const { user } = renderForm(unconfigured);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,10 @@ export function EnterpriseSsoForm({ organizationId, config }: Props) {
id="sso-protocol"
label={t('integrations.enterpriseSso.protocolLabel')}
description={t('integrations.enterpriseSso.protocolHelp')}
value={field.value}
// Default to a defined value so the Select is controlled from
// the first render — `field.value` is undefined while `data`
// is still loading (avoids the uncontrolled→controlled warning).
value={field.value ?? ''}
onValueChange={(value) => {
const next = narrowStringUnion<UiProtocol>(
value,
Expand Down Expand Up @@ -670,7 +673,9 @@ export function EnterpriseSsoForm({ organizationId, config }: Props) {
name="pkce"
render={({ field }) => (
<Switch
checked={field.value}
// `false` until `data` loads so the Switch stays controlled
// from the first render (no uncontrolled→controlled warning).
checked={field.value ?? false}
onCheckedChange={field.onChange}
label={t('integrations.enterpriseSso.pkceLabel')}
/>
Expand Down Expand Up @@ -749,7 +754,10 @@ export function EnterpriseSsoForm({ organizationId, config }: Props) {
<Select
id="sso-default-role"
label={t('integrations.enterpriseSso.defaultRoleLabel')}
value={field.value}
// Default to a defined value so the Select is controlled from
// the first render — `field.value` is undefined while `data`
// is still loading (avoids the uncontrolled→controlled warning).
value={field.value ?? ''}
onValueChange={(value) => {
const r = narrowStringUnion<PlatformRole>(value, [
'admin',
Expand All @@ -768,7 +776,9 @@ export function EnterpriseSsoForm({ organizationId, config }: Props) {
name="autoRole"
render={({ field }) => (
<Switch
checked={field.value}
// `false` until `data` loads so the Switch stays controlled
// from the first render (no uncontrolled→controlled warning).
checked={field.value ?? false}
onCheckedChange={field.onChange}
label={t('integrations.enterpriseSso.autoRoleLabel')}
/>
Expand All @@ -779,7 +789,9 @@ export function EnterpriseSsoForm({ organizationId, config }: Props) {
name="autoTeam"
render={({ field }) => (
<Switch
checked={field.value}
// `false` until `data` loads so the Switch stays controlled
// from the first render (no uncontrolled→controlled warning).
checked={field.value ?? false}
onCheckedChange={field.onChange}
label={t('integrations.enterpriseSso.autoTeamLabel')}
/>
Expand Down