diff --git a/llm-fe/src/llm-fe/components/SsoButtons/SsoButtons.test.tsx b/llm-fe/src/llm-fe/components/SsoButtons/SsoButtons.test.tsx new file mode 100644 index 0000000..86b68eb --- /dev/null +++ b/llm-fe/src/llm-fe/components/SsoButtons/SsoButtons.test.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import SsoButtons from './SsoButtons'; +import { startOAuth } from '../../auth/sso'; + +jest.mock('../../auth/sso', () => ({ + startOAuth: jest.fn(), +})); + +describe('SsoButtons', () => { + beforeEach(() => { + jest.mocked(startOAuth).mockReset(); + }); + + it('renders nothing when no providers enabled', () => { + const { container } = render( + + ); + expect(container).toBeEmptyDOMElement(); + }); + + it('uses brand-compliant labels and logos for login', () => { + render(); + + expect(screen.getByRole('button', { name: 'Continue with Google' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Sign in with Microsoft' })).toBeInTheDocument(); + expect(document.querySelectorAll('svg').length).toBeGreaterThanOrEqual(2); + }); + + it('uses Sign up with Google for signup intent', () => { + render(); + expect(screen.getByRole('button', { name: 'Sign up with Google' })).toBeInTheDocument(); + }); + + it('starts OAuth for the selected provider', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('button', { name: 'Continue with Google' })); + expect(startOAuth).toHaveBeenCalledWith('google', 'login'); + + await user.click(screen.getByRole('button', { name: 'Sign in with Microsoft' })); + expect(startOAuth).toHaveBeenCalledWith('microsoft', 'login'); + }); +}); diff --git a/llm-fe/src/llm-fe/components/SsoButtons/SsoButtons.tsx b/llm-fe/src/llm-fe/components/SsoButtons/SsoButtons.tsx index 2917a94..fc935e5 100644 --- a/llm-fe/src/llm-fe/components/SsoButtons/SsoButtons.tsx +++ b/llm-fe/src/llm-fe/components/SsoButtons/SsoButtons.tsx @@ -20,21 +20,32 @@ const Divider = styled.div` } `; -const SsoButton = styled.button` +const ButtonStack = styled.div` + display: flex; + flex-direction: column; width: 100%; - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(255, 255, 255, 0.18); - border-radius: 0.5rem; - color: #fff; - padding: 0.85rem 1rem; - font-size: 0.95rem; - font-weight: 600; - cursor: pointer; - transition: background 0.2s ease, transform 0.2s ease; + gap: 0.6rem; margin-top: 0.6rem; +`; - &:hover { - background: rgba(255, 255, 255, 0.12); +/** Shared layout for IdP buttons — equal size / visual weight. */ +const SsoButtonBase = styled.button` + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + width: 100%; + min-height: 2.75rem; + border-radius: 0.25rem; + padding: 0.65rem 1rem; + font-size: 0.875rem; + font-weight: 500; + font-family: 'Roboto', 'Google Sans', system-ui, sans-serif; + letter-spacing: 0.01em; + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease; + + &:hover:not(:disabled) { transform: translateY(-1px); } @@ -45,6 +56,93 @@ const SsoButton = styled.button` } `; +/** + * Google dark-theme button per + * https://developers.google.com/identity/branding-guidelines + * Fill #131314, stroke #8E918F, text #E3E3E3; multicolor G on white. + */ +const GoogleButton = styled(SsoButtonBase)` + background: #131314; + border: 1px solid #8e918f; + color: #e3e3e3; + + &:hover:not(:disabled) { + background: #1e1f20; + border-color: #a8aba9; + } +`; + +/** + * Microsoft dark-theme button — logo + "Sign in with Microsoft" + * https://learn.microsoft.com/en-us/entra/identity-platform/howto-add-branding-in-apps + */ +const MicrosoftButton = styled(SsoButtonBase)` + background: #2f2f2f; + border: 1px solid transparent; + color: #ffffff; + + &:hover:not(:disabled) { + background: #3b3b3b; + } +`; + +const GoogleLogoBadge = styled.span` + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 1.25rem; + height: 1.25rem; + background: #ffffff; + border-radius: 0.125rem; +`; + +const LogoMark = styled.span` + display: inline-flex; + flex-shrink: 0; + width: 1.25rem; + height: 1.25rem; + + svg { + width: 100%; + height: 100%; + display: block; + } +`; + +/** Official multicolor Google "G" (standard color Super G). */ +const GoogleGIcon = (): JSX.Element => ( + +); + +/** Official Microsoft four-square logo — do not recolor. */ +const MicrosoftLogoIcon = (): JSX.Element => ( + +); + export type OAuthProviderFlags = { google?: boolean; microsoft?: boolean; @@ -56,6 +154,9 @@ type SsoButtonsProps = { disabled?: boolean; }; +const googleLabel = (intent: 'login' | 'signup'): string => + intent === 'signup' ? 'Sign up with Google' : 'Continue with Google'; + const SsoButtons = ({ intent, providers, disabled = false }: SsoButtonsProps): JSX.Element | null => { const google = Boolean(providers.google); const microsoft = Boolean(providers.microsoft); @@ -66,24 +167,34 @@ const SsoButtons = ({ intent, providers, disabled = false }: SsoButtonsProps): J return ( <> or continue with - {google && ( - startOAuth('google', intent)} - > - Continue with Google - - )} - {microsoft && ( - startOAuth('microsoft', intent)} - > - Continue with Microsoft - - )} + + {google && ( + startOAuth('google', intent)} + > + + + + + + {googleLabel(intent)} + + )} + {microsoft && ( + startOAuth('microsoft', intent)} + > + + + + Sign in with Microsoft + + )} + ); }; diff --git a/llm-fe/src/llm-fe/pages/SignIn/SignIn.test.tsx b/llm-fe/src/llm-fe/pages/SignIn/SignIn.test.tsx index ba5fa14..8c978df 100644 --- a/llm-fe/src/llm-fe/pages/SignIn/SignIn.test.tsx +++ b/llm-fe/src/llm-fe/pages/SignIn/SignIn.test.tsx @@ -85,7 +85,7 @@ describe('SignIn', () => { renderSignIn(); expect(await screen.findByRole('button', { name: 'Continue with Google' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Continue with Microsoft' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Sign in with Microsoft' })).toBeInTheDocument(); }); it('hides SSO buttons when oauth not configured', async () => { @@ -99,6 +99,7 @@ describe('SignIn', () => { await waitFor(() => { expect(screen.queryByRole('button', { name: 'Continue with Google' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Sign in with Microsoft' })).not.toBeInTheDocument(); }); });