Skip to content

Commit 925e860

Browse files
fix(auth): when SSO is the way in, the sign-in card offers the SSO email field and button on first paint (#29337)
domain:ui#5 · zhuangjianguo · session_01QsR2jQjvWkZ7is6GXbC6DB <!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbWNEBnsJNXch6gN8mBmyHTT)_ Fixes #29274 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:开了 SSO(features.sso)且密码表单被收起的部署(强制 SSO,或管理员关闭了邮箱密码登录),登录卡首屏直接给出邮箱输入框和「Sign in with SSO」按钮,不再出现密码框。 - 为什么改:之前首屏只有一句 SSO 提示和「Use a password instead」链接,唯一能用的 SSO 按钮藏在密码表单里,用户被引向一个可能根本没有的密码(JIT 开通的 SSO 用户没有密码);关闭密码登录时,那个链接打开的密码表单服务端直接拒绝。 - 风险与回滚:只改 ui/packages/auth 的 LoginForm 一个组件,未加文案键;只有「SSO 已开且密码表单收起」这一状态变化;另外,关闭了密码登录的部署,只有开了手机号+密码登录时才显示「Use a password instead」。其余状态不动。回滚即 revert 本 PR。 - 先看:ui/packages/auth/src/LoginForm.tsx(ssoEmailFormVisible 与 SSO-only 视图里新增的 form),ui/packages/auth/src/__tests__/ssoEnforcedFirstPaint-29274.test.tsx。 - 两分钟验证:OS_SSO_ENABLED=true OS_AUTH_SSO_ONLY=true 启动并注册一个 OIDC 提供方后打开 Console /login,预期首屏有邮箱框和「Sign in with SSO」按钮、无密码框,点「Use a password instead」才出现密码框。 - 需要你做的:无,席位落地。 ## What changes `ui/packages/auth/src/LoginForm.tsx`: whenever the password form is collapsed (declared enforcement, `features.ssoEnforced`, or the password door shut, `emailPassword.enabled: false`) and the server advertises SSO (`features.sso`), the SSO-only view now draws its own small form on first paint: the email field (reusing `emailLabel` / `emailPlaceholder`) and the "Sign in with SSO" button (reusing `ssoButton`), submitting through the same `handleSso` that routes the email to the IdP. No password field. Under enforcement, "Use a password instead" still reveals the password form (break-glass), which keeps its own SSO button, and "Back to single sign-on" returns. With the password door shut, that link is now offered only when phone+password sign-in is on (`breakGlassOffered = !passwordSignInOff || phonePasswordEnabled`): its `ssoEnabled` term existed only because the SSO button lived inside the password form, and without phone sign-in the form it opens is one the server refuses. The provider divider shows when providers sit above the SSO email field. No new label or i18n key. Unchanged: federated providers only (providers + link, no email field), a server whose password form is shown (password form with the SSO button inside), and the no-method statement from #29159. ## Tests - New `ui/packages/auth/src/__tests__/ssoEnforcedFirstPaint-29274.test.tsx`, 8 cases: under enforcement the first paint shows the email field and SSO button and no password field; submitting posts the trimmed email to `/api/v1/auth/sign-in/sso`; the break-glass link reveals the password field and comes back; divider with providers; with the password door shut and SSO advertised (`emailPassword.enabled: false`, `sso: true`, `ssoEnforced: false`) the first paint shows the email field and SSO button, no password field and no "Use a password instead" link; with phone+password also on, the link stays; controls for federated-only, a shown password form, and the no-method state. - `LoginForm.test.tsx`: the enforced-mode case with `sso: true` read "no Email field"; it now asserts the SSO button is present and the password field absent (same file surface). - `noSignInMethod-29159.test.tsx`: the password-off + SSO case asserted the SSO button is reached through the link; it now asserts the SSO button on the first paint and no link, with the reason in the test (the SSO way in is on the first paint since #29274). - `pnpm exec vitest run --maxWorkers=2 packages/auth/` from `ui/`, locked, at `86424840`: 42/42 files, 368/368 tests. Console and app-shell login suites naming `ssoEnforced` (2 files): 5/5 at `c567affc`. `turbo run type-check --filter=@object-ui/auth`: exit 0 at `c567affc`. - Ablations via `scripts/ablation-replace.mjs` at `86424840`, each over the three affected files (44 tests), each restored to blob `c35cd36c` == HEAD with `git diff HEAD` empty: - `const ssoEmailFormVisible = ssoOnlyView && ssoEnabled;` made always-false (the SSO path back behind the collapsed form): blob `c35cd36c` to `d5f586fc`; 8 red (both triggers' first-paint cases, the phone+password link case, the updated `LoginForm.test.tsx` and `noSignInMethod-29159.test.tsx` cases), controls green. - `breakGlassOffered` with its `ssoEnabled` term put back: blob `c35cd36c` to `94bc71d8`; 2 red (the password-shut first-paint case and the flipped #29159 case), the rest green. ## Acceptance notes - Both triggers draw the same first-paint SSO form: declared enforcement (`features.ssoEnforced`) and the password door shut (`emailPassword.enabled: false`), each with `features.sso`. The second was first noted as out of scope and folded in on the filing seat's ruling: it collapses the same form, and there the break-glass link opens a password door the server refuses. - On the filing seat's ruling this PR also drops the `ssoEnabled` term from the break-glass link's condition, which existed only because the SSO button lived inside the password form. The no-method state is unchanged. - The live staging run below measured the enforcement trigger; the password-shut trigger and the link condition are held by the unit cases and the ablations. ## Verification Live staging run at `e343cef8` (fix) against `f2bf2938` (main), Playwright Chromium, fresh context, en-US. 1. `OS_SSO_ENABLED=true OS_AUTH_SSO_ONLY=true pnpm dev -- --fresh -p PORT`; as the dev admin, `POST /api/v1/auth/sso/register` for OIDC provider `acme-sso`, domain `acme.example`: 200. `GET /api/v1/auth/config` then read `emailPassword {enabled:true, disableSignUp:true}`, `features.sso: true`, `features.ssoEnforced: true`, `socialProviders: []`. 2. Console Vite dev server from this tree (it aliases `@object-ui/auth` to source), `/login`. | | SSO buttons | email fields | password fields | break-glass link | |---|---|---|---|---| | before, 1280x800 first paint | 0 | 0 | 0 | 1 | | before, 390x844 first paint | 0 | 0 | 0 | 1 | | after, 1280x800 first paint | 1 | 1 | 0 | 1 | | after, 390x844 first paint | 1 | 1 | 0 | 1 | | after, 1280x800 after break-glass | 1 | 1 | 1 | 0 | 3. After: typing `jane@acme.example` on the first paint and clicking "Sign in with SSO" sent `POST /api/v1/auth/sign-in/sso`, which answered 200 with the IdP authorize URL (`https://idp.acme.example/authorize?response_type=code&client_id=acme-client...`), and the browser navigated to it. <details><summary>Gates</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `86424840`: 34 families (same list as at `69adc443`), every one exit 0, including `node scripts/check-sdui-manifest.mjs` and its `--self-test`, and `pnpm --dir ui exec vitest run scripts/` (169 passed, 2 skipped). `--ran` reconciliation: 34 derived, 34 run, 0 NOT-MEASURED. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jT8Pv5VNAwUnWGJWyK)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e5a24dd commit 925e860

5 files changed

Lines changed: 265 additions & 12 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@object-ui/auth': patch
3+
---
4+
5+
fix(auth): when SSO is the way in, the sign-in card offers the SSO email field and "Sign in with SSO" on first paint
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
On a deployment that advertises SSO (`features.sso`) and either enforces it (`features.ssoEnforced`) or has email+password sign-in turned off (`emailPassword.enabled: false`), the sign-in card first showed only the SSO-only sentence and a "Use a password instead" link. The email-routed "Sign in with SSO" button, the deployment's only working way in, sat inside the password form and appeared only after that link was clicked; with the password door shut, that link opened a form whose password half the server refuses.
11+
12+
`LoginForm` now draws the email field and the "Sign in with SSO" button in the SSO-only view itself, with no password field. Under enforcement, "Use a password instead" still reveals the password form for the break-glass admin, and "Back to single sign-on" returns to the SSO view. With email+password sign-in turned off, that link is now offered only when phone+password sign-in is on, since otherwise the form it opens is one the server refuses. A deployment with federated providers only, a server whose password form is shown, and the no-method state are unchanged. No new label: the field reuses `emailLabel` and `emailPlaceholder`, the button `ssoButton`.
13+
14+
What to do: nothing.

‎ui/packages/auth/src/LoginForm.tsx‎

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ export interface LoginFormLabels {
4141
/**
4242
* Divider label between the social provider buttons and the email/password
4343
* form (defaults to "or continue with email"). Rendered only when the server
44-
* reports at least one social provider, and only while that form is shown:
45-
* under SSO-only mode it appears once "Use a password instead" reveals it.
44+
* reports at least one social provider, and only while an email field is
45+
* shown: under SSO-only mode, the SSO email field (when the server also
46+
* advertises SSO) or the form "Use a password instead" reveals.
4647
*/
4748
orText?: string;
4849
/**
@@ -341,9 +342,10 @@ export function LoginForm({
341342
const passwordFormCollapsed = ssoEnforced || passwordSignInOff;
342343
const passwordFormVisible = !passwordFormCollapsed || showPasswordFallback;
343344
// The break-glass link reveals the password form, so it is offered only
344-
// while that form holds a door the server accepts: email+password itself, the
345-
// SSO button (routes the entered email), or phone+password sign-in.
346-
const breakGlassOffered = !passwordSignInOff || ssoEnabled || phonePasswordEnabled;
345+
// while that form holds a password door the server accepts: email+password
346+
// itself, or phone+password sign-in. The SSO button needs no link: the
347+
// SSO-only view draws it on first paint (`ssoEmailFormVisible` below).
348+
const breakGlassOffered = !passwordSignInOff || phonePasswordEnabled;
347349
// Nothing the server advertises lets anyone in: say so, instead of SSO copy
348350
// with no SSO affordance and a password link the server refuses.
349351
const noSignInMethod =
@@ -506,6 +508,12 @@ export function LoginForm({
506508
// its own enforcement declaration, or the SSO button. Never while the
507509
// server answers `features.sso: false` with no enforcement.
508510
const ssoOnlyCopy = ssoOnlyView && !noSignInMethod && (ssoEnforced || ssoEnabled);
511+
// When the password form is collapsed (declared enforcement, or the password
512+
// door shut) and the server advertises SSO, the SSO button IS the way in, so
513+
// the SSO-only view draws it on first paint with the email field it routes,
514+
// and no password field. It used to live only inside the password form,
515+
// reachable solely through the break-glass link.
516+
const ssoEmailFormVisible = ssoOnlyView && ssoEnabled;
509517

510518
// objectstack#29105 — a password sign-in waiting for its second factor,
511519
// whoever started it (this form, or another `signIn` caller on the page).
@@ -593,7 +601,7 @@ export function LoginForm({
593601
<SocialSignInButtons
594602
mode="sign-in"
595603
dividerText={labels.orText}
596-
hideDivider={!passwordFormVisible}
604+
hideDivider={!passwordFormVisible && !ssoEmailFormVisible}
597605
buttonText={labels.socialButton}
598606
onProvidersResolved={(hasProviders) => setHasSocialProviders(hasProviders)}
599607
/>
@@ -794,6 +802,43 @@ export function LoginForm({
794802
link to the password form for the env owner / local admin. */
795803
<div className="space-y-4">
796804
{error && <AuthErrorBanner message={error} />}
805+
{ssoEmailFormVisible && (
806+
<form
807+
onSubmit={(e) => {
808+
e.preventDefault();
809+
void handleSso();
810+
}}
811+
className="space-y-4"
812+
data-testid="login-sso-email-form"
813+
>
814+
<div className="space-y-2">
815+
<label htmlFor="login-sso-email" className={AUTH_FIELD_LABEL_CLASS}>
816+
{l.emailLabel}
817+
</label>
818+
<input
819+
id="login-sso-email"
820+
type="email"
821+
inputMode="email"
822+
placeholder={l.emailPlaceholder}
823+
value={email}
824+
onChange={(e) => setEmail(e.target.value)}
825+
required
826+
autoComplete="username"
827+
disabled={ssoSubmitting}
828+
className={AUTH_INPUT_CLASS}
829+
/>
830+
</div>
831+
<button
832+
type="submit"
833+
disabled={ssoSubmitting}
834+
aria-busy={ssoSubmitting}
835+
className={AUTH_PRIMARY_BUTTON_CLASS}
836+
>
837+
{ssoSubmitting && <AuthSpinner />}
838+
{l.ssoButton}
839+
</button>
840+
</form>
841+
)}
797842
{breakGlassOffered && (
798843
<button
799844
type="button"

‎ui/packages/auth/src/__tests__/LoginForm.test.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,10 @@ describe('LoginForm — SSO-only (enforced) mode', () => {
118118
registerUrl: '/register',
119119
});
120120

121-
// The break-glass link appears (federated buttons are the path)…
121+
// The break-glass link appears beside the SSO path itself…
122122
await screen.findByRole('button', BREAK_GLASS);
123-
// …the local password form is hidden…
124-
expect(screen.queryByLabelText('Email')).toBeNull();
123+
expect(screen.getByRole('button', { name: 'Sign in with SSO' })).toBeTruthy();
124+
// …the local password field is hidden…
125125
expect(screen.queryByLabelText('Password')).toBeNull();
126126
// …and the sign-up link is hidden (no self-registration under enforced).
127127
expect(screen.queryByText('Sign up')).toBeNull();

‎ui/packages/auth/src/__tests__/noSignInMethod-29159.test.tsx‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ describe('LoginForm — doors the server does advertise stay usable', () => {
145145
expect(screen.queryByTestId('login-no-sign-in-method')).toBeNull();
146146
});
147147

148-
it('with password off and SSO advertised, says SSO and reaches the SSO button through the link', async () => {
148+
it('with password off and SSO advertised, says SSO and offers the SSO button with no refused password link', async () => {
149149
renderLogin({
150150
socialProviders: [],
151151
emailPassword: { enabled: false },
@@ -155,8 +155,11 @@ describe('LoginForm — doors the server does advertise stay usable', () => {
155155

156156
expect(screen.getByText(SSO_ONLY_COPY)).toBeTruthy();
157157
expect(screen.queryByTestId('login-no-sign-in-method')).toBeNull();
158-
fireEvent.click(screen.getByRole('button', BREAK_GLASS));
159-
expect(await screen.findByRole('button', SSO_BUTTON)).toBeTruthy();
158+
// The SSO way in is on the first paint since #29274, so the link no longer
159+
// has a door to reach here: the password form it opens is one the server
160+
// refuses.
161+
expect(screen.getByRole('button', SSO_BUTTON)).toBeTruthy();
162+
expect(screen.queryByRole('button', BREAK_GLASS)).toBeNull();
160163
});
161164

162165
it('with password off and phone sign-in on, is not the no-method state', async () => {
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* Under declared SSO enforcement (`features.ssoEnforced` with
11+
* `features.sso`), the email-routed "Sign in with SSO" button is the way in.
12+
* It used to render only inside the password form, which enforcement keeps
13+
* collapsed until "Use a password instead" is clicked, so the first paint
14+
* offered no SSO control at all. The first paint now carries the email field
15+
* and the SSO button, and no password field; the break-glass link still
16+
* reveals the password form.
17+
*
18+
* The same holds when the password door is shut (`emailPassword.enabled:
19+
* false`) and the server advertises SSO without enforcing it: that collapses
20+
* the same form. There the break-glass link is no longer offered: the
21+
* password form it would open is one the server refuses, unless phone+password
22+
* sign-in is on.
23+
*
24+
* The controls: federated providers only, a server whose password form is
25+
* shown, and the no-method state keep their views.
26+
*/
27+
import React from 'react';
28+
import { afterEach, describe, it, expect, vi } from 'vitest';
29+
import { cleanup, render, screen, waitFor, fireEvent } from '@testing-library/react';
30+
import { AuthProvider } from '../AuthProvider';
31+
import { LoginForm } from '../LoginForm';
32+
import type { AuthClient, AuthPublicConfig } from '../types';
33+
34+
afterEach(() => {
35+
cleanup();
36+
vi.unstubAllGlobals();
37+
});
38+
39+
const SSO_BUTTON = { name: 'Sign in with SSO' } as const;
40+
const BREAK_GLASS = { name: 'Use a password instead' } as const;
41+
const BACK_TO_SSO = { name: 'Back to single sign-on' } as const;
42+
const PROVIDER_BUTTON = { name: /Continue with Acme IdP$/ } as const;
43+
const DIVIDER = 'or continue with email';
44+
const OIDC: AuthPublicConfig['socialProviders'] = [
45+
{ id: 'acme-idp', name: 'Acme IdP', enabled: true, type: 'oidc' },
46+
];
47+
48+
/** The card's staging reading of an SSO-enforced deployment with a registered OIDC provider. */
49+
const ENFORCED_SSO: AuthPublicConfig = {
50+
socialProviders: [],
51+
emailPassword: { enabled: true, disableSignUp: true },
52+
features: { sso: true, ssoEnforced: true },
53+
};
54+
55+
function renderLogin(config: AuthPublicConfig) {
56+
const client = {
57+
signIn: vi.fn(),
58+
getSession: vi.fn().mockResolvedValue(null),
59+
getConfig: vi.fn().mockResolvedValue(config),
60+
signInWithProvider: vi.fn().mockResolvedValue(undefined),
61+
} as unknown as AuthClient;
62+
return render(
63+
<AuthProvider authUrl="/api/auth" client={client}>
64+
<LoginForm registerUrl="/register" />
65+
</AuthProvider>,
66+
);
67+
}
68+
69+
const configResolved = () =>
70+
waitFor(() => expect(screen.queryByTestId('login-config-loading')).toBeNull());
71+
72+
describe('LoginForm — SSO enforcement with SSO advertised: the first paint is the SSO path', () => {
73+
it('draws the email field and the SSO button, and no password field', async () => {
74+
renderLogin(ENFORCED_SSO);
75+
await configResolved();
76+
77+
expect(screen.getByLabelText('Email')).toBeTruthy();
78+
expect(screen.getByRole('button', SSO_BUTTON)).toBeTruthy();
79+
expect(screen.queryByLabelText('Password')).toBeNull();
80+
expect(screen.getByRole('button', BREAK_GLASS)).toBeTruthy();
81+
expect(screen.queryByText('Sign up')).toBeNull();
82+
});
83+
84+
it('routes the entered email to the IdP from the first paint', async () => {
85+
const fetchMock = vi.fn().mockResolvedValue({
86+
ok: false,
87+
json: async () => ({ message: 'No SSO provider for that domain' }),
88+
});
89+
vi.stubGlobal('fetch', fetchMock);
90+
renderLogin(ENFORCED_SSO);
91+
await configResolved();
92+
93+
fireEvent.change(screen.getByLabelText('Email'), { target: { value: ' jane@acme.example ' } });
94+
fireEvent.click(screen.getByRole('button', SSO_BUTTON));
95+
96+
await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1));
97+
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
98+
expect(url).toBe('/api/v1/auth/sign-in/sso');
99+
expect(JSON.parse(String(init.body)).email).toBe('jane@acme.example');
100+
expect(await screen.findByText('No SSO provider for that domain')).toBeTruthy();
101+
});
102+
103+
it('keeps "Use a password instead" as the break-glass route to the password form, and back', async () => {
104+
renderLogin(ENFORCED_SSO);
105+
await configResolved();
106+
107+
fireEvent.click(screen.getByRole('button', BREAK_GLASS));
108+
expect(await screen.findByLabelText('Password')).toBeTruthy();
109+
expect(screen.getByRole('button', SSO_BUTTON)).toBeTruthy();
110+
111+
fireEvent.click(screen.getByRole('button', BACK_TO_SSO));
112+
await screen.findByRole('button', BREAK_GLASS);
113+
expect(screen.queryByLabelText('Password')).toBeNull();
114+
expect(screen.getByRole('button', SSO_BUTTON)).toBeTruthy();
115+
});
116+
117+
it('separates the provider buttons from the SSO email field with the divider', async () => {
118+
renderLogin({ ...ENFORCED_SSO, socialProviders: OIDC });
119+
await configResolved();
120+
121+
expect(await screen.findByRole('button', PROVIDER_BUTTON)).toBeTruthy();
122+
expect(screen.getByText(DIVIDER)).toBeTruthy();
123+
expect(screen.getByRole('button', SSO_BUTTON)).toBeTruthy();
124+
expect(screen.queryByLabelText('Password')).toBeNull();
125+
});
126+
});
127+
128+
describe('LoginForm — password door shut with SSO advertised: the first paint is the SSO path too', () => {
129+
it('draws the email field and the SSO button, and no password field, without enforcement', async () => {
130+
renderLogin({
131+
socialProviders: [],
132+
emailPassword: { enabled: false },
133+
features: { sso: true, ssoEnforced: false },
134+
});
135+
await configResolved();
136+
137+
expect(screen.getByLabelText('Email')).toBeTruthy();
138+
expect(screen.getByRole('button', SSO_BUTTON)).toBeTruthy();
139+
expect(screen.queryByLabelText('Password')).toBeNull();
140+
expect(screen.queryByTestId('login-no-sign-in-method')).toBeNull();
141+
// The password form the link would open is one the server refuses.
142+
expect(screen.queryByRole('button', BREAK_GLASS)).toBeNull();
143+
});
144+
145+
it('keeps the link when phone+password is a door the server still accepts', async () => {
146+
renderLogin({
147+
socialProviders: [],
148+
emailPassword: { enabled: false },
149+
features: { sso: true, ssoEnforced: false, phoneNumber: true },
150+
});
151+
await configResolved();
152+
153+
expect(screen.getByRole('button', SSO_BUTTON)).toBeTruthy();
154+
expect(screen.getByRole('button', BREAK_GLASS)).toBeTruthy();
155+
});
156+
});
157+
158+
describe('LoginForm — the views around it stay as they were', () => {
159+
it('enforcement with federated providers only: the providers and the link, no email field', async () => {
160+
renderLogin({ socialProviders: OIDC, features: { sso: false, ssoEnforced: true } });
161+
await configResolved();
162+
163+
expect(await screen.findByRole('button', PROVIDER_BUTTON)).toBeTruthy();
164+
expect(screen.getByRole('button', BREAK_GLASS)).toBeTruthy();
165+
expect(screen.queryByLabelText('Email')).toBeNull();
166+
expect(screen.queryByRole('button', SSO_BUTTON)).toBeNull();
167+
expect(screen.queryByText(DIVIDER)).toBeNull();
168+
});
169+
170+
it('no enforcement: the password form, with the SSO button inside it', async () => {
171+
renderLogin({ socialProviders: [], features: { sso: true, ssoEnforced: false } });
172+
await configResolved();
173+
174+
expect(screen.getByLabelText('Password')).toBeTruthy();
175+
expect(screen.getByRole('button', SSO_BUTTON)).toBeTruthy();
176+
expect(screen.queryByRole('button', BREAK_GLASS)).toBeNull();
177+
expect(screen.queryByTestId('login-sso-email-form')).toBeNull();
178+
});
179+
180+
it('password off and nothing else advertised: the no-method statement', async () => {
181+
renderLogin({
182+
socialProviders: [],
183+
emailPassword: { enabled: false },
184+
features: { sso: false, ssoEnforced: false },
185+
});
186+
187+
expect(await screen.findByTestId('login-no-sign-in-method')).toBeTruthy();
188+
expect(screen.queryByLabelText('Email')).toBeNull();
189+
expect(screen.queryByRole('button', SSO_BUTTON)).toBeNull();
190+
});
191+
});

0 commit comments

Comments
 (0)