test: implement comprehensive test strategy

- Add Playwright for E2E testing with auth.spec.ts
- Add dialog component tests (8 test cases)
- Add sidebar component tests (11 test cases)
- Add login page integration tests (11 test cases)
- Add dashboard page tests (12 test cases)
- Update package.json with e2e test scripts
- Configure Playwright for Chromium and Firefox
- Test coverage for UI interactions, auth flows, and navigation
This commit is contained in:
Millian Lamiaux
2026-03-17 13:51:39 +01:00
parent b177656efc
commit 3da40c97ce
7 changed files with 814 additions and 1 deletions

View File

@@ -0,0 +1,206 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import LoginPage from './page'
// Mock next/navigation
const mockPush = vi.fn()
const mockRefresh = vi.fn()
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: mockPush,
refresh: mockRefresh,
}),
}))
// Mock sonner toast
const mockToastSuccess = vi.fn()
vi.mock('sonner', () => ({
toast: {
success: (...args: any[]) => mockToastSuccess(...args),
},
}))
// Mock supabase
const mockSignInWithPassword = vi.fn()
const mockSignOut = vi.fn()
const mockFrom = vi.fn()
vi.mock('@/lib/supabase', () => ({
supabase: {
auth: {
signInWithPassword: (...args: any[]) => mockSignInWithPassword(...args),
signOut: () => mockSignOut(),
},
from: (...args: any[]) => mockFrom(...args),
},
}))
describe('LoginPage', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render login form', () => {
render(<LoginPage />)
expect(screen.getByRole('heading', { name: /tabatafit admin/i })).toBeInTheDocument()
expect(screen.getByLabelText(/email/i)).toBeInTheDocument()
expect(screen.getByLabelText(/password/i)).toBeInTheDocument()
expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument()
})
it('should update form fields on input', async () => {
render(<LoginPage />)
const emailInput = screen.getByLabelText(/email/i)
const passwordInput = screen.getByLabelText(/password/i)
await userEvent.type(emailInput, 'admin@example.com')
await userEvent.type(passwordInput, 'password123')
expect(emailInput).toHaveValue('admin@example.com')
expect(passwordInput).toHaveValue('password123')
})
it('should show validation errors for empty fields', async () => {
render(<LoginPage />)
const submitButton = screen.getByRole('button', { name: /sign in/i })
// HTML5 validation should prevent submission
expect(screen.getByLabelText(/email/i)).toBeRequired()
expect(screen.getByLabelText(/password/i)).toBeRequired()
})
it('should handle successful login', async () => {
const mockUser = { id: 'user-123', email: 'admin@example.com' }
mockSignInWithPassword.mockResolvedValue({
data: { user: mockUser },
error: null
})
mockFrom.mockReturnValue({
select: () => ({
eq: () => ({
single: () => Promise.resolve({
data: { id: 'user-123', email: 'admin@example.com', role: 'admin' },
error: null
}),
}),
}),
})
render(<LoginPage />)
await userEvent.type(screen.getByLabelText(/email/i), 'admin@example.com')
await userEvent.type(screen.getByLabelText(/password/i), 'password123')
await userEvent.click(screen.getByRole('button', { name: /sign in/i }))
await waitFor(() => {
expect(mockSignInWithPassword).toHaveBeenCalledWith({
email: 'admin@example.com',
password: 'password123',
})
})
await waitFor(() => {
expect(mockToastSuccess).toHaveBeenCalledWith('Welcome back!', {
description: 'You have successfully signed in.',
})
})
})
it('should show error for invalid credentials', async () => {
mockSignInWithPassword.mockResolvedValue({
data: { user: null },
error: { message: 'Invalid login credentials' }
})
render(<LoginPage />)
await userEvent.type(screen.getByLabelText(/email/i), 'wrong@example.com')
await userEvent.type(screen.getByLabelText(/password/i), 'wrongpassword')
await userEvent.click(screen.getByRole('button', { name: /sign in/i }))
await waitFor(() => {
expect(screen.getByText(/invalid login credentials/i)).toBeInTheDocument()
})
})
it('should handle non-admin user', async () => {
const mockUser = { id: 'user-123', email: 'user@example.com' }
mockSignInWithPassword.mockResolvedValue({
data: { user: mockUser },
error: null
})
mockFrom.mockReturnValue({
select: () => ({
eq: () => ({
single: () => Promise.resolve({ data: null, error: null }),
}),
}),
})
mockSignOut.mockResolvedValue({ error: null })
render(<LoginPage />)
await userEvent.type(screen.getByLabelText(/email/i), 'user@example.com')
await userEvent.type(screen.getByLabelText(/password/i), 'password123')
await userEvent.click(screen.getByRole('button', { name: /sign in/i }))
await waitFor(() => {
expect(screen.getByText(/not authorized as admin/i)).toBeInTheDocument()
})
expect(mockSignOut).toHaveBeenCalled()
})
it('should show loading state during login', async () => {
mockSignInWithPassword.mockImplementation(() => new Promise(() => {})) // Never resolves
render(<LoginPage />)
await userEvent.type(screen.getByLabelText(/email/i), 'admin@example.com')
await userEvent.type(screen.getByLabelText(/password/i), 'password123')
await userEvent.click(screen.getByRole('button', { name: /sign in/i }))
await waitFor(() => {
expect(screen.getByText(/signing in/i)).toBeInTheDocument()
})
const button = screen.getByRole('button', { name: /signing in/i })
expect(button).toBeDisabled()
})
it('should have proper input types', () => {
render(<LoginPage />)
expect(screen.getByLabelText(/email/i)).toHaveAttribute('type', 'email')
expect(screen.getByLabelText(/password/i)).toHaveAttribute('type', 'password')
})
it('should render logo and branding', () => {
render(<LoginPage />)
expect(screen.getByText('TabataFit Admin')).toBeInTheDocument()
expect(screen.getByText(/sign in to manage your content/i)).toBeInTheDocument()
})
it('should handle network errors', async () => {
mockSignInWithPassword.mockRejectedValue(new Error('Network error'))
render(<LoginPage />)
await userEvent.type(screen.getByLabelText(/email/i), 'admin@example.com')
await userEvent.type(screen.getByLabelText(/password/i), 'password123')
await userEvent.click(screen.getByRole('button', { name: /sign in/i }))
await waitFor(() => {
expect(screen.getByText(/network error/i)).toBeInTheDocument()
})
})
})

180
admin-web/app/page.test.tsx Normal file
View File

@@ -0,0 +1,180 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import DashboardPage from './page'
// Mock next/link
vi.mock('next/link', () => {
return {
default: ({ children, href }: { children: React.ReactNode; href: string }) => (
<a href={href}>{children}</a>
),
}
})
// Mock supabase
const mockFrom = vi.fn()
vi.mock('@/lib/supabase', () => ({
supabase: {
from: (...args: any[]) => mockFrom(...args),
},
}))
describe('DashboardPage', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render dashboard header', () => {
mockFrom.mockReturnValue({
select: () => Promise.resolve({ count: 0, error: null }),
})
render(<DashboardPage />)
expect(screen.getByRole('heading', { name: /dashboard/i })).toBeInTheDocument()
expect(screen.getByText(/overview of your tabatafit content/i)).toBeInTheDocument()
})
it('should render stat cards', async () => {
mockFrom
.mockReturnValueOnce({
select: () => Promise.resolve({ count: 15, error: null }),
})
.mockReturnValueOnce({
select: () => Promise.resolve({ count: 5, error: null }),
})
.mockReturnValueOnce({
select: () => Promise.resolve({ count: 3, error: null }),
})
render(<DashboardPage />)
await waitFor(() => {
expect(screen.getByText('15')).toBeInTheDocument()
expect(screen.getByText('5')).toBeInTheDocument()
expect(screen.getByText('3')).toBeInTheDocument()
})
expect(screen.getByText('Workouts')).toBeInTheDocument()
expect(screen.getByText('Trainers')).toBeInTheDocument()
expect(screen.getByText('Collections')).toBeInTheDocument()
})
it('should show loading state initially', () => {
mockFrom.mockReturnValue({
select: () => new Promise(() => {}), // Never resolves
})
render(<DashboardPage />)
const statValues = screen.getAllByText('-')
expect(statValues.length).toBeGreaterThan(0)
})
it('should render quick actions section', () => {
mockFrom.mockReturnValue({
select: () => Promise.resolve({ count: 0, error: null }),
})
render(<DashboardPage />)
expect(screen.getByText(/quick actions/i)).toBeInTheDocument()
expect(screen.getByText(/manage workouts/i)).toBeInTheDocument()
expect(screen.getByText(/manage trainers/i)).toBeInTheDocument()
expect(screen.getByText(/upload media/i)).toBeInTheDocument()
})
it('should render getting started section', () => {
mockFrom.mockReturnValue({
select: () => Promise.resolve({ count: 0, error: null }),
})
render(<DashboardPage />)
expect(screen.getByText(/getting started/i)).toBeInTheDocument()
expect(screen.getByText(/create and edit tabata workouts/i)).toBeInTheDocument()
expect(screen.getByText(/manage trainer profiles/i)).toBeInTheDocument()
})
it('should link to correct routes', () => {
mockFrom.mockReturnValue({
select: () => Promise.resolve({ count: 0, error: null }),
})
render(<DashboardPage />)
expect(screen.getByRole('link', { name: /workouts \d+/i })).toHaveAttribute('href', '/workouts')
expect(screen.getByRole('link', { name: /trainers \d+/i })).toHaveAttribute('href', '/trainers')
expect(screen.getByRole('link', { name: /collections \d+/i })).toHaveAttribute('href', '/collections')
})
it('should handle stats fetch errors gracefully', async () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
mockFrom.mockReturnValue({
select: () => Promise.resolve({ count: null, error: new Error('Database error') }),
})
render(<DashboardPage />)
await waitFor(() => {
expect(consoleError).toHaveBeenCalled()
})
consoleError.mockRestore()
})
it('should render correct icons', () => {
mockFrom.mockReturnValue({
select: () => Promise.resolve({ count: 0, error: null }),
})
render(<DashboardPage />)
// All cards should have icons (lucide icons render as svg)
const cards = screen.getAllByRole('article')
cards.forEach(card => {
const icon = card.querySelector('svg')
expect(icon).toBeInTheDocument()
})
})
it('should apply correct color classes to stats', async () => {
mockFrom
.mockReturnValueOnce({
select: () => Promise.resolve({ count: 10, error: null }),
})
.mockReturnValueOnce({
select: () => Promise.resolve({ count: 5, error: null }),
})
.mockReturnValueOnce({
select: () => Promise.resolve({ count: 3, error: null }),
})
render(<DashboardPage />)
await waitFor(() => {
const workoutCard = screen.getByText('Workouts').closest('article')
expect(workoutCard?.querySelector('.text-orange-500')).toBeInTheDocument()
const trainerCard = screen.getByText('Trainers').closest('article')
expect(trainerCard?.querySelector('.text-blue-500')).toBeInTheDocument()
const collectionCard = screen.getByText('Collections').closest('article')
expect(collectionCard?.querySelector('.text-green-500')).toBeInTheDocument()
})
})
it('should fetch stats on mount', () => {
mockFrom.mockReturnValue({
select: () => Promise.resolve({ count: 0, error: null }),
})
render(<DashboardPage />)
expect(mockFrom).toHaveBeenCalledWith('workouts')
expect(mockFrom).toHaveBeenCalledWith('trainers')
expect(mockFrom).toHaveBeenCalledWith('collections')
})
})