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:
135
admin-web/components/sidebar.test.tsx
Normal file
135
admin-web/components/sidebar.test.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { Sidebar } from './sidebar'
|
||||
|
||||
// Mock next/navigation
|
||||
const mockPush = vi.fn()
|
||||
const mockPathname = vi.fn()
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
}),
|
||||
usePathname: () => mockPathname(),
|
||||
}))
|
||||
|
||||
// Mock supabase
|
||||
const mockSignOut = vi.fn()
|
||||
vi.mock('@/lib/supabase', () => ({
|
||||
supabase: {
|
||||
auth: {
|
||||
signOut: () => mockSignOut(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
describe('Sidebar', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockPathname.mockReturnValue('/')
|
||||
})
|
||||
|
||||
it('should render sidebar with logo and navigation', () => {
|
||||
render(<Sidebar />)
|
||||
|
||||
expect(screen.getByText('TabataFit')).toBeInTheDocument()
|
||||
expect(screen.getByText('Admin Dashboard')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /logout/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render all navigation items', () => {
|
||||
render(<Sidebar />)
|
||||
|
||||
expect(screen.getByRole('link', { name: /dashboard/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /workouts/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /trainers/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /collections/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /media/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should highlight active navigation item', () => {
|
||||
mockPathname.mockReturnValue('/workouts')
|
||||
render(<Sidebar />)
|
||||
|
||||
const workoutsLink = screen.getByRole('link', { name: /workouts/i })
|
||||
expect(workoutsLink).toHaveClass('text-orange-500')
|
||||
expect(workoutsLink).toHaveClass('bg-orange-500/10')
|
||||
})
|
||||
|
||||
it('should not highlight inactive navigation items', () => {
|
||||
mockPathname.mockReturnValue('/workouts')
|
||||
render(<Sidebar />)
|
||||
|
||||
const dashboardLink = screen.getByRole('link', { name: /dashboard/i })
|
||||
expect(dashboardLink).toHaveClass('text-neutral-400')
|
||||
expect(dashboardLink).not.toHaveClass('bg-orange-500/10')
|
||||
})
|
||||
|
||||
it('should navigate to correct routes when clicking links', () => {
|
||||
render(<Sidebar />)
|
||||
|
||||
const workoutsLink = screen.getByRole('link', { name: /workouts/i })
|
||||
expect(workoutsLink).toHaveAttribute('href', '/workouts')
|
||||
|
||||
const trainersLink = screen.getByRole('link', { name: /trainers/i })
|
||||
expect(trainersLink).toHaveAttribute('href', '/trainers')
|
||||
})
|
||||
|
||||
it('should handle logout', async () => {
|
||||
mockSignOut.mockResolvedValue({ error: null })
|
||||
render(<Sidebar />)
|
||||
|
||||
const logoutButton = screen.getByRole('button', { name: /logout/i })
|
||||
await userEvent.click(logoutButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSignOut).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
expect(mockPush).toHaveBeenCalledWith('/login')
|
||||
})
|
||||
|
||||
it('should handle logout errors gracefully', async () => {
|
||||
mockSignOut.mockRejectedValueOnce(new Error('Network error'))
|
||||
|
||||
render(<Sidebar />)
|
||||
|
||||
const logoutButton = screen.getByRole('button', { name: /logout/i })
|
||||
await userEvent.click(logoutButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSignOut).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should have correct styling for navigation', () => {
|
||||
render(<Sidebar />)
|
||||
|
||||
const nav = screen.getByRole('navigation')
|
||||
expect(nav).toBeInTheDocument()
|
||||
|
||||
const links = screen.getAllByRole('link')
|
||||
links.forEach(link => {
|
||||
expect(link).toHaveClass('flex', 'items-center', 'gap-3')
|
||||
})
|
||||
})
|
||||
|
||||
it('should display icons for navigation items', () => {
|
||||
render(<Sidebar />)
|
||||
|
||||
// All nav links should have icons (lucide icons render as svg)
|
||||
const links = screen.getAllByRole('link')
|
||||
links.forEach(link => {
|
||||
const icon = link.querySelector('svg')
|
||||
expect(icon).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should be sticky positioned', () => {
|
||||
render(<Sidebar />)
|
||||
|
||||
const sidebar = screen.getByRole('complementary') || document.querySelector('aside')
|
||||
expect(sidebar).toHaveClass('sticky', 'top-0')
|
||||
})
|
||||
})
|
||||
184
admin-web/components/ui/dialog.test.tsx
Normal file
184
admin-web/components/ui/dialog.test.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from './dialog'
|
||||
import { Button } from './button'
|
||||
|
||||
describe('Dialog', () => {
|
||||
it('should render dialog trigger', () => {
|
||||
render(
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Test Title</DialogTitle>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: /open dialog/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should open dialog when trigger is clicked', async () => {
|
||||
render(
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Test Title</DialogTitle>
|
||||
<DialogDescription>Test Description</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /open dialog/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(screen.getByText('Test Title')).toBeInTheDocument()
|
||||
expect(screen.getByText('Test Description')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should close dialog when clicking outside', async () => {
|
||||
render(
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Test Title</DialogTitle>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /open dialog/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Click on the backdrop (outside the dialog content)
|
||||
const backdrop = document.querySelector('[data-state="open"]')
|
||||
if (backdrop) {
|
||||
await userEvent.click(backdrop)
|
||||
}
|
||||
})
|
||||
|
||||
it('should close dialog when pressing escape', async () => {
|
||||
render(
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Test Title</DialogTitle>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /open dialog/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
await userEvent.keyboard('{Escape}')
|
||||
})
|
||||
|
||||
it('should render dialog with footer', async () => {
|
||||
render(
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Item</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button>Delete</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /open dialog/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should support custom styling', async () => {
|
||||
render(
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="bg-neutral-900 border-neutral-800 text-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Styled Dialog</DialogTitle>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /open dialog/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
const dialog = screen.getByRole('dialog')
|
||||
expect(dialog).toHaveClass('bg-neutral-900')
|
||||
expect(dialog).toHaveClass('border-neutral-800')
|
||||
expect(dialog).toHaveClass('text-white')
|
||||
})
|
||||
})
|
||||
|
||||
it('should be accessible', async () => {
|
||||
render(
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Accessible Dialog</DialogTitle>
|
||||
<DialogDescription>This is a description</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button', { name: /open dialog/i })
|
||||
expect(trigger).toHaveAttribute('aria-haspopup', 'dialog')
|
||||
expect(trigger).toHaveAttribute('aria-expanded', 'false')
|
||||
|
||||
await userEvent.click(trigger)
|
||||
|
||||
await waitFor(() => {
|
||||
const dialog = screen.getByRole('dialog')
|
||||
expect(dialog).toHaveAttribute('aria-modal', 'true')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user