Testing Standards¶
This document outlines the testing standards and best practices for the Delta project to ensure consistent, maintainable, and automation-ready code.
Table of Contents¶
Test Attributes¶
data-testid Convention¶
Use data-testid attributes to provide stable selectors for test automation. Follow these naming conventions:
Pages¶
// Page container
<main data-testid="[page-name]-page">
// Sections
<section data-testid="[page-name]-[section-name]">
// Examples:
<main data-testid="login-page">
<section data-testid="projects-header">
<div data-testid="projects-grid">
Components¶
// Component root
<div data-testid="[component-name]">
// Component states
<div data-testid="[component-name]-[state]">
// Component items (for lists/grids)
<div data-testid="[component-name]-item-[id]">
// Examples:
<div data-testid="auth-flow">
<div data-testid="auth-flow-phone-step">
<div data-testid="project-tile-${project.id}">
Interactive Elements¶
// Buttons
<button data-testid="[action]-button">
// Forms
<form data-testid="[form-name]-form">
<input data-testid="[field-name]-input">
// Examples:
<button data-testid="submit-button">
<button data-testid="sign-out-button">
<form data-testid="login-form">
<input data-testid="phone-input">
Loading and Error States¶
// Loading states
<div data-testid="[component]-loading">
// Error states
<div data-testid="[component]-error">
// Empty states
<div data-testid="[component]-empty">
// Examples:
<div data-testid="projects-loading">
<div data-testid="auth-error">
<div data-testid="projects-empty">
When to Use data-testid¶
- Always use for:
- Page containers
- Major sections
- Forms and form fields
- Dynamic content (lists, grids)
- Loading/error/empty states
-
Custom interactive components
-
Avoid using for:
- Static text content
- Decorative elements
- When semantic queries (role, label) are more appropriate
Accessibility Standards¶
ARIA Labels¶
Always provide descriptive ARIA labels for interactive elements:
// Buttons without visible text
<button aria-label="Close dialog" data-testid="close-button">
<X className="h-4 w-4" />
</button>
// Form inputs
<input
aria-label="Phone number"
aria-describedby="phone-error"
data-testid="phone-input"
/>
// Navigation
<nav aria-label="Main navigation">
<nav aria-label="Breadcrumb">
// Sections
<section aria-labelledby="projects-heading">
<h1 id="projects-heading">Projects</h1>
</section>
Roles¶
Use semantic HTML first, then add roles when needed:
// Lists and grids
<div role="grid" aria-label="Projects grid">
<div role="row">
<div role="gridcell">
// Status messages
<div role="status" aria-live="polite">
Loading projects...
</div>
// Alerts
<div role="alert" aria-live="assertive">
Error loading data
</div>
Live Regions¶
For dynamic content updates:
// Polite updates (wait for user to stop)
<div aria-live="polite" aria-atomic="true">
{itemCount} items found
</div>
// Assertive updates (announce immediately)
<div aria-live="assertive" role="alert">
Connection lost
</div>
Component Structure¶
Pages¶
export default function ProjectsPage() {
return (
<main data-testid="projects-page" className="container mx-auto p-6">
<header data-testid="projects-header">
<h1 id="page-title">Projects</h1>
</header>
<section aria-labelledby="page-title" data-testid="projects-content">
{/* Content */}
</section>
</main>
);
}
Components¶
import { ReactNode } from 'react';
interface ButtonProps {
children: ReactNode;
onClick?: () => void;
loading?: boolean;
'data-testid'?: string;
}
export function Button({
children,
onClick,
loading,
'data-testid': testId = 'button',
}: ButtonProps) {
return (
<button
data-testid={testId}
onClick={onClick}
disabled={loading}
aria-busy={loading}
>
{children}
</button>
);
}
Forms¶
export function LoginForm() {
return (
<form data-testid="login-form" aria-label="Login form">
<div>
<label htmlFor="phone">Phone Number</label>
<input
id="phone"
data-testid="phone-input"
aria-describedby="phone-error"
aria-invalid={!!errors.phone}
/>
{errors.phone && (
<span id="phone-error" role="alert">
{errors.phone}
</span>
)}
</div>
<button
type="submit"
data-testid="submit-button"
aria-label="Submit login form"
>
Login
</button>
</form>
);
}
Mocking Patterns¶
Mock File Organization¶
The project uses a structured approach to organizing test mocks across different __mocks__ directories:
1. Root-level Mocks (packages/app/__mocks__/)¶
External libraries and Next.js modules:
@supabase/supabase-js.ts- Supabase client mocks@supabase/auth-helpers-nextjs.ts- Supabase auth helpersnext/navigation.ts- Next.js navigation hooksnext/headers.ts- Next.js headers/cookiesnext/link.tsx- Next.js Link componentnext/server.ts- NextResponse/NextRequestlucide-react.tsx- Icon componentswinston.ts- Logger libraryinput-otp.tsx- OTP input librarygoogle-libphonenumber.ts- Phone number utilitiesnext-themes.tsx- Theme provider
2. Component Mocks¶
src/components/ui/__mocks__/- UI component mockssrc/components/layout/__mocks__/- Layout component mocks (AppHeader, AppFooter, etc.)src/components/auth/__mocks__/- Auth component mocks
3. Hook and Utility Mocks¶
src/hooks/__mocks__/- Custom hook mockssrc/lib/__mocks__/- Utility function mockssrc/providers/__mocks__/- Context provider mockssrc/app/api/lib/__mocks__/- API utility mocks
Mock Implementation Guidelines¶
1. Use External Mock Files¶
Always prefer external mock files over inline mocks:
// Good - Using external mock
jest.mock('@/components/ui/Button', () =>
jest.requireActual('@/components/ui/__mocks__/Button')
);
// Avoid - Inline mock (except for layout components)
jest.mock('@/components/ui/Button', () => ({
Button: () => <div>Mock Button</div>,
}));
2. Path Aliases¶
Use the appropriate path aliases:
@/- For src directory paths@mocks/- For root-level mock paths
// Component mock
jest.mock('@/components/ui/Logo', () =>
jest.requireActual('@/components/ui/__mocks__/Logo')
);
// Root-level mock
jest.mock('next/navigation', () =>
jest.requireActual('@mocks/next/navigation')
);
3. Layout Component Exception¶
Due to Jest's hoisting behavior, layout component mocks must be defined inline:
// Layout components require inline mocks
jest.mock('@/components/layout/AppLayout', () => {
const React = require('react');
return {
AppLayout: ({ children }: { children: React.ReactNode }) =>
React.createElement('div', { 'data-testid': 'app-layout' }, children),
};
});
Always use React.createElement instead of JSX in these mocks to avoid transpilation issues.
4. Creating New Mocks¶
When creating new mock files:
- Place them in the appropriate
__mocks__directory - Match the export pattern of the real module
- Include appropriate
data-testidattributes - Provide minimal but functional implementations
Example mock structure:
// src/components/ui/__mocks__/NewComponent.tsx
import { ReactNode } from 'react';
export const NewComponent = ({
children,
...props
}: {
children?: ReactNode;
}) => {
return (
<div data-testid="new-component" {...props}>
{children}
</div>
);
};
5. Complex Mock Patterns¶
For complex mocks with state or behavior:
// __mocks__/complex-library.tsx
import { ChangeEvent, useState } from 'react';
export const ComplexComponent = ({
onChange,
value,
}: {
onChange?: (val: string) => void;
value?: string;
}) => {
const [internalValue, setInternalValue] = useState(value || '');
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setInternalValue(e.target.value);
onChange?.(e.target.value);
};
return (
<input
data-testid="complex-input"
value={internalValue}
onChange={handleChange}
/>
);
};
Mock Usage Best Practices¶
- Reset mocks between tests:
beforeEach(() => {
jest.clearAllMocks();
});
- Mock return values when needed:
const mockUseAuth = jest.requireMock('@/providers/AuthProvider').useAuth;
mockUseAuth.mockReturnValue({ user: null, loading: false });
- Avoid over-mocking: Only mock what's necessary for the test to run
- Keep mocks simple: Provide minimal functionality needed for tests
- Document complex mocks: Add comments explaining why certain mock behavior is needed
Testing Patterns¶
Query Priority¶
Follow this priority when selecting elements in tests:
- getByRole - For interactive elements
- getByLabelText - For form controls
- getByPlaceholderText - When label isn't available
- getByText - For non-interactive content
- getByTestId - When above options aren't suitable
Example Test¶
describe('ProjectsPage', () => {
it('displays projects grid', async () => {
render(<ProjectsPage />);
// Use semantic queries first
expect(
screen.getByRole('heading', { name: 'Projects' })
).toBeInTheDocument();
// Use data-testid for complex components
const projectsGrid = screen.getByTestId('projects-grid');
expect(projectsGrid).toBeInTheDocument();
// Check accessibility
expect(projectsGrid).toHaveAttribute('role', 'grid');
expect(projectsGrid).toHaveAttribute('aria-label', 'Projects grid');
});
});
TypeScript Standards in Tests¶
Import Conventions¶
Always use direct imports instead of React. prefix:
// Good - Direct imports
import { ReactNode, useState, useEffect } from 'react';
interface Props {
children: ReactNode;
}
// Bad - React.* prefix
import React from 'react';
interface Props {
children: React.ReactNode;
}
No any Type¶
Never use any in test files. Create proper types instead:
// Good - Proper typing
const mockData: BudgetView = {
id: 'budget-1',
title: 'Main Budget',
status: 'Active',
};
// Bad - Using any
const mockData: any = { id: 'budget-1' };
No require() in Mock Factories¶
Use top-level imports and reference them in mock factories. Only use require('react') as a last resort for layout component mocks where Jest hoisting prevents normal imports:
// Good - Import at top level, use in mock
import { createElement } from 'react';
jest.mock('@/components/ui/SomeComponent', () => ({
SomeComponent: ({ children, ...props }: { children?: ReactNode }) =>
createElement(
'div',
{ 'data-testid': 'some-component', ...props },
children
),
}));
// Acceptable ONLY for layout components (Jest hoisting issue)
jest.mock('@/components/layout/AppLayout', () => {
const React = require('react');
return {
AppLayout: ({ children }: { children: React.ReactNode }) =>
React.createElement('div', { 'data-testid': 'app-layout' }, children),
};
});
Console Output in Tests¶
Tests should run cleanly without console.error, console.warn, or other console noise:
- Mock
console.errorandconsole.warnwhen testing error boundaries or error states - Use
jest.spyOn(console, 'error').mockImplementation(() => {})with targeted restoration - Never globally suppress console output — only suppress expected messages
- Always restore console mocks in
afterEachor after the specific test
// Good - Targeted suppression for expected errors
it('handles error state', () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
// ... test that triggers expected console.error ...
consoleSpy.mockRestore();
});
Best Practices¶
1. Consistent Naming¶
- Use kebab-case for data-testid values
- Be descriptive but concise
- Include component name in the testid
2. Avoid Over-Testing¶
- Don't add data-testid to every element
- Use semantic queries when possible
- Only add testids where needed for reliable selection
3. Maintainability¶
- Keep testids stable across refactors
- Document any complex testid patterns
- Use TypeScript for testid constants if shared
4. Performance¶
- Don't use testids in production CSS selectors
- Keep testid values short
- Consider removing testids in production builds (optional)
5. Documentation¶
- Document any unique testing patterns in component files
- Keep test files next to components
- Use descriptive test names
Examples¶
Page with Dynamic Content¶
export default function ProjectsPage() {
const { projects, loading, error } = useProjects();
if (loading) {
return (
<div data-testid="projects-loading" role="status" aria-live="polite">
<Skeleton data-testid="projects-skeleton" />
<span className="sr-only">Loading projects...</span>
</div>
);
}
if (error) {
return (
<div data-testid="projects-error" role="alert" aria-live="assertive">
<p>Error loading projects: {error.message}</p>
</div>
);
}
if (projects.length === 0) {
return (
<div data-testid="projects-empty" role="status">
<p>No projects found</p>
</div>
);
}
return (
<main data-testid="projects-page">
<div data-testid="projects-grid" role="grid" aria-label="Projects grid">
{projects.map((project) => (
<div
key={project.id}
data-testid={`project-tile-${project.id}`}
role="gridcell"
>
{project.name}
</div>
))}
</div>
</main>
);
}
Interactive Component¶
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button
data-testid="theme-toggle"
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
aria-pressed={theme === 'dark'}
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
{theme === 'dark' ? <Sun /> : <Moon />}
</button>
);
}