diff --git a/frontend/src/components/ui/alert.tsx b/frontend/src/components/ui/alert.tsx new file mode 100644 index 0000000..1fe3176 --- /dev/null +++ b/frontend/src/components/ui/alert.tsx @@ -0,0 +1,76 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground", + className + )} + {...props} + /> + ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription, AlertAction } diff --git a/frontend/src/pages/LoginPage.test.tsx b/frontend/src/pages/LoginPage.test.tsx new file mode 100644 index 0000000..b0e80b8 --- /dev/null +++ b/frontend/src/pages/LoginPage.test.tsx @@ -0,0 +1,65 @@ +import { render, screen, act } from '@testing-library/react' +import { describe, it, expect, vi } from 'vitest' +import { LoginPage } from './LoginPage' + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})) + +const mockAuth = { + user: null, + loading: false, + login: vi.fn(), + register: vi.fn(), + logout: vi.fn(), + token: null, + refetch: vi.fn(), +} + +describe('LoginPage branding', () => { + it('AUTH-01: wrapper div has a linear-gradient background', () => { + const { container } = render( + + ) + const wrapper = container.firstElementChild as HTMLElement + expect(wrapper?.style.background).toContain('linear-gradient') + }) + + it('AUTH-02: renders a wordmark element with a gradient background style', () => { + render() + const wordmark = screen.getByTestId('wordmark') + expect(wordmark).toBeTruthy() + expect(wordmark.style.background).toBeTruthy() + }) + + it('AUTH-04: renders role="alert" when an error is present', async () => { + const failingLogin = vi.fn().mockRejectedValue(new Error('Invalid credentials')) + const authWithError = { ...mockAuth, login: failingLogin } + + render() + + const form = document.querySelector('form')! + await act(async () => { + form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })) + }) + + const alertEl = await screen.findByRole('alert') + expect(alertEl).toBeTruthy() + }) + + it('AUTH-04: the alert contains an SVG icon (AlertCircle)', async () => { + const failingLogin = vi.fn().mockRejectedValue(new Error('Invalid credentials')) + const authWithError = { ...mockAuth, login: failingLogin } + + render() + + const form = document.querySelector('form')! + await act(async () => { + form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })) + }) + + const alertEl = await screen.findByRole('alert') + const svgEl = alertEl.querySelector('svg') + expect(svgEl).toBeTruthy() + }) +}) diff --git a/frontend/src/pages/RegisterPage.test.tsx b/frontend/src/pages/RegisterPage.test.tsx new file mode 100644 index 0000000..44243e0 --- /dev/null +++ b/frontend/src/pages/RegisterPage.test.tsx @@ -0,0 +1,33 @@ +import { render, screen } from '@testing-library/react' +import { describe, it, expect, vi } from 'vitest' +import { RegisterPage } from './RegisterPage' + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})) + +const mockAuth = { + user: null, + loading: false, + login: vi.fn(), + register: vi.fn(), + logout: vi.fn(), + token: null, + refetch: vi.fn(), +} + +describe('RegisterPage branding', () => { + it('AUTH-03: wrapper div has a linear-gradient background', () => { + const { container } = render( + + ) + const wrapper = container.firstElementChild as HTMLElement + expect(wrapper?.style.background).toContain('linear-gradient') + }) + + it('AUTH-03: renders a wordmark element with data-testid="wordmark"', () => { + render() + const wordmark = screen.getByTestId('wordmark') + expect(wordmark).toBeTruthy() + }) +})