test(02-01): add failing tests for AUTH-01 through AUTH-04 and install shadcn Alert

- Install shadcn Alert component (alert.tsx)
- LoginPage.test.tsx: 4 test cases for gradient bg, wordmark, Alert error, SVG icon
- RegisterPage.test.tsx: 2 test cases for gradient bg, wordmark
- All tests fail as expected (features not yet implemented)
This commit is contained in:
2026-03-11 21:47:57 +01:00
parent c6db87d4b8
commit dfd88de03e
3 changed files with 174 additions and 0 deletions

View File

@@ -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<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"font-medium group-has-[>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 (
<div
data-slot="alert-description"
className={cn(
"text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
className
)}
{...props}
/>
)
}
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-action"
className={cn("absolute top-2 right-2", className)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription, AlertAction }

View File

@@ -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(
<LoginPage auth={mockAuth} onToggle={vi.fn()} />
)
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(<LoginPage auth={mockAuth} onToggle={vi.fn()} />)
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(<LoginPage auth={authWithError} onToggle={vi.fn()} />)
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(<LoginPage auth={authWithError} onToggle={vi.fn()} />)
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()
})
})

View File

@@ -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(
<RegisterPage auth={mockAuth} onToggle={vi.fn()} />
)
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(<RegisterPage auth={mockAuth} onToggle={vi.fn()} />)
const wordmark = screen.getByTestId('wordmark')
expect(wordmark).toBeTruthy()
})
})