Google 로그인을 단일 버튼으로 정리
This commit is contained in:
parent
7994175a3d
commit
471eb9621a
15 changed files with 78 additions and 85 deletions
|
|
@ -1094,6 +1094,8 @@ class AuthProviderScaffoldTest(unittest.IsolatedAsyncioTestCase):
|
|||
self.assertEqual(query["client_id"], ["google-client"])
|
||||
self.assertEqual(query["redirect_uri"], ["https://api-vignette.test/auth/callback"])
|
||||
self.assertEqual(query["response_type"], ["code"])
|
||||
self.assertEqual(query["prompt"], ["select_account"])
|
||||
self.assertNotIn("hd", query)
|
||||
self.assertEqual(query["code_challenge_method"], ["S256"])
|
||||
self.assertEqual(
|
||||
query["code_challenge"],
|
||||
|
|
|
|||
|
|
@ -67,13 +67,17 @@ test.describe("auth domain policy", () => {
|
|||
await page.goto("/login");
|
||||
|
||||
const googleButtons = page.locator(".lg-obtn");
|
||||
await expect(googleButtons).toHaveCount(2);
|
||||
await expect(googleButtons).toHaveCount(1);
|
||||
await expect(page.getByRole("button", { name: /^Google 계정으로 로그인/ })).toHaveCount(1);
|
||||
await expect(page.getByRole("button", { name: /다른 Google 계정 선택/ })).toHaveCount(0);
|
||||
expect(config.allowed_email_domains).toEqual([]);
|
||||
await expect(page.locator(".lg-policy b")).toHaveText("모든 Google 계정");
|
||||
const redirectHost = new URL(config.redirect_uri).hostname;
|
||||
const devOAuthUnavailable =
|
||||
config.dev_login_enabled &&
|
||||
!isLocalHostname(redirectHost);
|
||||
await expect(page.locator(".lg-policy b")).toHaveText(
|
||||
config.google_oauth_configured && !devOAuthUnavailable ? "모든 Google 계정" : "설정 필요",
|
||||
);
|
||||
if (config.dev_login_enabled) {
|
||||
await expect(page.locator(".lg-dev")).toBeVisible();
|
||||
} else {
|
||||
|
|
@ -85,7 +89,6 @@ test.describe("auth domain policy", () => {
|
|||
await expect(page.locator(".lg-config")).toHaveCount(0);
|
||||
} else {
|
||||
await expect(googleButtons.first()).toBeDisabled();
|
||||
await expect(googleButtons.nth(1)).toBeDisabled();
|
||||
await expect(page.locator(".lg-config")).toBeVisible();
|
||||
await expect(page).toHaveURL(/\/login$/);
|
||||
await expect(page.locator("body")).not.toContainText("Google OAuth is not configured");
|
||||
|
|
@ -239,7 +242,9 @@ test.describe("auth domain policy", () => {
|
|||
await page.goto(`${publicBase}/login`, { waitUntil: "domcontentloaded" });
|
||||
await page.waitForLoadState("networkidle", { timeout: 15_000 }).catch(() => undefined);
|
||||
|
||||
await expect(page.locator(".lg-obtn")).toHaveCount(2);
|
||||
await expect(page.locator(".lg-obtn")).toHaveCount(1);
|
||||
await expect(page.getByRole("button", { name: /^Google 계정으로 로그인/ })).toHaveCount(1);
|
||||
await expect(page.getByRole("button", { name: /다른 Google 계정 선택/ })).toHaveCount(0);
|
||||
await expect(page.locator(".lg-dev")).toHaveCount(0);
|
||||
await expect(page.getByText("로컬 테스트")).toHaveCount(0);
|
||||
await expect(page.getByText("로컬 테스트 계정으로 계속")).toHaveCount(0);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { completeOnboarding, expectNoHorizontalOverflow, useRealApi } from "./su
|
|||
const CONFIG_PATTERN = "**/api/auth/config";
|
||||
const ME_PATTERN = "**/api/auth/me";
|
||||
|
||||
/** 로컬 redirect_uri라서 두 OAuth 버튼이 모두 활성화되는 구성 fixture. */
|
||||
/** 로컬 redirect_uri라서 단일 Google OAuth 버튼이 활성화되는 구성 fixture. */
|
||||
const READY_AUTH_CONFIG = {
|
||||
google_oauth_configured: true,
|
||||
saml_configured: false,
|
||||
|
|
@ -153,7 +153,7 @@ test.describe("full sweep auth", () => {
|
|||
});
|
||||
|
||||
// 검증 checklist: auth-login-generic-google-button
|
||||
test("both Google buttons advertise unrestricted accounts and start the same OAuth flow", async ({
|
||||
test("single Google button advertises unrestricted accounts and starts the account chooser flow", async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockUnauthenticatedMe(page);
|
||||
|
|
@ -173,16 +173,16 @@ test.describe("full sweep auth", () => {
|
|||
});
|
||||
await expectNoHorizontalOverflow(page);
|
||||
|
||||
const secondary = page.locator(".lg-obtn.secondary");
|
||||
await expect(secondary).toBeEnabled();
|
||||
await expect(secondary).toContainText("다른 Google 계정 선택");
|
||||
await expect(secondary.locator(".sub")).toHaveText("이메일 확인 후 바로 시작");
|
||||
await expect(page.locator(".lg-obtn.primary .sub")).toHaveText("모든 Google 계정");
|
||||
const googleLogin = page.getByRole("button", { name: /^Google 계정으로 로그인/ });
|
||||
await expect(googleLogin).toHaveCount(1);
|
||||
await expect(googleLogin).toBeEnabled();
|
||||
await expect(googleLogin.locator(".sub")).toHaveText("모든 Google 계정");
|
||||
await expect(page.getByRole("button", { name: /다른 Google 계정 선택/ })).toHaveCount(0);
|
||||
|
||||
// 클릭 시 primary와 동일한 provider=google 시작 URL로 이동한다.
|
||||
// 단일 CTA가 provider=google OAuth 시작 URL로 이동한다.
|
||||
await Promise.all([
|
||||
page.waitForURL(/\/api\/auth\/login\?provider=google&next=%2F/),
|
||||
secondary.click(),
|
||||
googleLogin.click(),
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ test.describe("full sweep — shared shell, GNB, and routing guards", () => {
|
|||
await page.getByRole("button", { name: "로그아웃" }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/login$/);
|
||||
await expect(page.getByRole("button", { name: /학교 Google 계정으로 계속/ })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible();
|
||||
|
||||
// 서버 세션도 무효화되었는지 실 API로 확인
|
||||
const me = await page.request.get("/api/auth/me");
|
||||
|
|
@ -112,7 +112,7 @@ test.describe("full sweep — shared shell, GNB, and routing guards", () => {
|
|||
await page.getByRole("button", { name: "로그아웃" }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/login$/);
|
||||
await expect(page.getByRole("button", { name: /학교 Google 계정으로 계속/ })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible();
|
||||
expect(logoutCalled).toBeTruthy();
|
||||
});
|
||||
|
||||
|
|
@ -239,7 +239,7 @@ test.describe("full sweep — shared shell, GNB, and routing guards", () => {
|
|||
releaseMe();
|
||||
|
||||
await expect(page).toHaveURL(/\/login$/);
|
||||
await expect(page.getByRole("button", { name: /학교 Google 계정으로 계속/ })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible();
|
||||
});
|
||||
|
||||
// checklist: shell-suspense-fallback
|
||||
|
|
@ -269,7 +269,7 @@ test.describe("full sweep — shared shell, GNB, and routing guards", () => {
|
|||
|
||||
releaseChunk();
|
||||
|
||||
await expect(page.getByRole("button", { name: /학교 Google 계정으로 계속/ })).toBeVisible({
|
||||
await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await expect(page.getByText(BOOT_SCREEN_TEXT)).toHaveCount(0);
|
||||
|
|
@ -287,7 +287,7 @@ test.describe("full sweep — shared shell, GNB, and routing guards", () => {
|
|||
});
|
||||
|
||||
await page.goto("/login");
|
||||
await expect(page.getByRole("button", { name: /학교 Google 계정으로 계속/ })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible();
|
||||
expect(documentRequests).toBe(1);
|
||||
|
||||
const firstPrevented = await page.evaluate(() => {
|
||||
|
|
@ -300,7 +300,7 @@ test.describe("full sweep — shared shell, GNB, and routing guards", () => {
|
|||
expect(firstPrevented).toBeTruthy();
|
||||
|
||||
await expect.poll(() => documentRequests).toBe(2);
|
||||
await expect(page.getByRole("button", { name: /학교 Google 계정으로 계속/ })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible();
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => sessionStorage.getItem("vignette:chunk-recovery:/login")),
|
||||
|
|
@ -358,7 +358,7 @@ test.describe("full sweep — shared shell, GNB, and routing guards", () => {
|
|||
test("redirects unknown routes to the role-aware root destination", async ({ page }) => {
|
||||
await page.goto("/definitely/not-a-route");
|
||||
await expect(page).toHaveURL(/\/login$/);
|
||||
await expect(page.getByRole("button", { name: /학교 Google 계정으로 계속/ })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible();
|
||||
|
||||
await signInAsLearner(page);
|
||||
await page.goto("/no-such-page");
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ test.describe("learner app shell and session launcher", () => {
|
|||
await page.goto("/learn/practice");
|
||||
|
||||
await expect(page).toHaveURL(/\/login$/);
|
||||
await expect(page.getByRole("button", { name: /학교 Google 계정으로 계속/ })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible();
|
||||
expect(await page.evaluate(() => window.localStorage.getItem("vignette.dev-auth"))).toBeNull();
|
||||
});
|
||||
|
||||
|
|
@ -182,7 +182,7 @@ test.describe("learner app shell and session launcher", () => {
|
|||
await page.goto("/learn/practice");
|
||||
|
||||
await expect(page).toHaveURL(/\/login$/);
|
||||
await expect(page.getByRole("button", { name: /학교 Google 계정으로 계속/ })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible();
|
||||
await expect(page.getByText("내담자 목록을 불러오지 못했습니다.")).toHaveCount(0);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -905,7 +905,7 @@ async function openLogin(page: Page) {
|
|||
await routeLoginScreen(page);
|
||||
await page.goto("/login");
|
||||
await expect(page.locator(".lg-root, .lg-enter").first()).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByRole("button", { name: /^Google 계정으로 계속/ })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /^Google 계정으로 로그인/ })).toBeVisible();
|
||||
}
|
||||
|
||||
async function openLearnerHome(page: Page) {
|
||||
|
|
@ -970,16 +970,41 @@ test.describe("uc a11y-mobile — 주요 6화면 접근성·모바일 감사", (
|
|||
// usecase: 키보드 사용자가 Tab 으로 로그인 버튼에 도달하고 포커스 위치를 눈으로 확인한다
|
||||
test("로그인 화면에서 Tab 순회 시 포커스 표시가 눈에 보인다", async ({ page }) => {
|
||||
await openLogin(page);
|
||||
await expectFocusVisibleOnTab(page, "login", 5);
|
||||
const googleLogin = page.getByRole("button", { name: /^Google 계정으로 로그인/ });
|
||||
await expect(googleLogin).toHaveCount(1);
|
||||
await page.keyboard.press("Tab");
|
||||
await expect(googleLogin).toBeFocused();
|
||||
const focusReport = await googleLogin.evaluate((element) => {
|
||||
const target = element as HTMLElement;
|
||||
const snapshot = () => {
|
||||
const style = window.getComputedStyle(target);
|
||||
return [
|
||||
style.outlineStyle,
|
||||
style.outlineWidth,
|
||||
style.outlineColor,
|
||||
style.boxShadow,
|
||||
style.borderColor,
|
||||
style.backgroundColor,
|
||||
].join("|");
|
||||
};
|
||||
const focusVisible = target.matches(":focus-visible");
|
||||
const focusedStyle = snapshot();
|
||||
target.blur();
|
||||
const blurredStyle = snapshot();
|
||||
target.focus();
|
||||
return { focusVisible, styleChanged: focusedStyle !== blurredStyle };
|
||||
});
|
||||
expect(focusReport.focusVisible).toBeTruthy();
|
||||
expect(focusReport.styleChanged).toBeTruthy();
|
||||
});
|
||||
|
||||
// usecase: 저시력 사용자가 주 로그인 버튼의 문구를 읽는다
|
||||
test("로그인 주 CTA(Google 계정으로 계속)의 색 대비가 4.5:1 이상이다", async ({ page }) => {
|
||||
test("로그인 주 CTA(Google 계정으로 로그인)의 색 대비가 4.5:1 이상이다", async ({ page }) => {
|
||||
await openLogin(page);
|
||||
await expectCtaContrast(
|
||||
page.getByRole("button", { name: /^Google 계정으로 계속/ }),
|
||||
page.getByRole("button", { name: /^Google 계정으로 로그인/ }),
|
||||
"login",
|
||||
"Google 계정으로 계속",
|
||||
"Google 계정으로 로그인",
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -500,7 +500,7 @@ test.describe("uc error-resilience-shell", () => {
|
|||
|
||||
await expect(page).toHaveURL(/\/login$/);
|
||||
await expect(
|
||||
page.getByRole("button", { name: /Google 계정으로 계속/ }),
|
||||
page.getByRole("button", { name: /Google 계정으로 로그인/ }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -131,16 +131,11 @@ export default function Login() {
|
|||
!isLocalRedirectUri(authConfig.redirect_uri);
|
||||
const oauthReady =
|
||||
authConfig?.google_oauth_configured === true && !devOAuthUnavailable;
|
||||
const primaryAccountLabel = devOAuthUnavailable
|
||||
const accountLabel = devOAuthUnavailable
|
||||
? "로컬은 테스트 계정 사용"
|
||||
: oauthChecking
|
||||
? "도메인 확인 중"
|
||||
: "모든 Google 계정";
|
||||
const secondaryAccountLabel = devOAuthUnavailable
|
||||
? "공개 주소에서 사용"
|
||||
: oauthChecking
|
||||
? "계정 정책 확인 중"
|
||||
: "이메일 확인 후 바로 시작";
|
||||
const oauthStatusMessage = devOAuthUnavailable
|
||||
? LOCAL_OAUTH_UNAVAILABLE_MESSAGE
|
||||
: oauthChecking
|
||||
|
|
@ -182,8 +177,7 @@ export default function Login() {
|
|||
<LoginPanel
|
||||
oauth={{
|
||||
ready: oauthReady,
|
||||
primaryAccountLabel,
|
||||
secondaryAccountLabel,
|
||||
accountLabel,
|
||||
statusIcon: authConfigError ? "alert" : "info",
|
||||
statusMessage: oauthStatusMessage,
|
||||
onStart: startOAuth,
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ export interface LoginRoleOption {
|
|||
|
||||
interface LoginOAuthView {
|
||||
ready: boolean;
|
||||
primaryAccountLabel: string;
|
||||
secondaryAccountLabel: string;
|
||||
accountLabel: string;
|
||||
statusIcon: Extract<IconName, "alert" | "info">;
|
||||
statusMessage: string;
|
||||
onStart: () => void;
|
||||
|
|
@ -38,33 +37,27 @@ interface LoginPanelProps {
|
|||
error: LoginErrorView;
|
||||
}
|
||||
|
||||
function LoginProviderButton({
|
||||
variant,
|
||||
icon,
|
||||
label,
|
||||
function GoogleLoginButton({
|
||||
subLabel,
|
||||
disabled,
|
||||
onClick,
|
||||
}: {
|
||||
variant: "primary" | "secondary";
|
||||
icon: Extract<IconName, "google" | "school">;
|
||||
label: string;
|
||||
subLabel: string;
|
||||
disabled: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
className={surfaceClassName(`lg-obtn ${variant}`, { variant: "interactive", flat: true })}
|
||||
className={surfaceClassName("lg-obtn primary", { variant: "interactive", flat: true })}
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<span className="ic">
|
||||
<Icon name={icon} size={19} strokeWidth={1.8} />
|
||||
<Icon name="google" size={19} strokeWidth={1.8} />
|
||||
</span>
|
||||
<span className="txt">
|
||||
{label}
|
||||
Google 계정으로 로그인
|
||||
<span className="sub">{subLabel}</span>
|
||||
</span>
|
||||
<Icon name="chevron-right" size={18} strokeWidth={2} />
|
||||
|
|
@ -140,19 +133,8 @@ export function LoginPanel({ oauth, devAccess, error }: LoginPanelProps) {
|
|||
</p>
|
||||
|
||||
<div className="lg-actions">
|
||||
<LoginProviderButton
|
||||
variant="primary"
|
||||
icon="school"
|
||||
label="Google 계정으로 계속"
|
||||
subLabel={oauth.primaryAccountLabel}
|
||||
disabled={!oauth.ready}
|
||||
onClick={oauth.onStart}
|
||||
/>
|
||||
<LoginProviderButton
|
||||
variant="secondary"
|
||||
icon="google"
|
||||
label="다른 Google 계정 선택"
|
||||
subLabel={oauth.secondaryAccountLabel}
|
||||
<GoogleLoginButton
|
||||
subLabel={oauth.accountLabel}
|
||||
disabled={!oauth.ready}
|
||||
onClick={oauth.onStart}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -221,14 +221,6 @@
|
|||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.lg-obtn.secondary {
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
.lg-obtn.secondary:hover {
|
||||
border-color: color-mix(in srgb, var(--accent) 42%, var(--glass-inset-border));
|
||||
}
|
||||
|
||||
.lg-obtn:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 1;
|
||||
|
|
@ -261,10 +253,6 @@
|
|||
color: var(--accent-deep);
|
||||
}
|
||||
|
||||
.lg-obtn.secondary .ic {
|
||||
background: var(--bg-surface-2);
|
||||
}
|
||||
|
||||
.lg-obtn .txt {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue