Skip to content

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

  1. Always use for:
  2. Page containers
  3. Major sections
  4. Forms and form fields
  5. Dynamic content (lists, grids)
  6. Loading/error/empty states
  7. Custom interactive components

  8. Avoid using for:

  9. Static text content
  10. Decorative elements
  11. 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 helpers
  • next/navigation.ts - Next.js navigation hooks
  • next/headers.ts - Next.js headers/cookies
  • next/link.tsx - Next.js Link component
  • next/server.ts - NextResponse/NextRequest
  • lucide-react.tsx - Icon components
  • winston.ts - Logger library
  • input-otp.tsx - OTP input library
  • google-libphonenumber.ts - Phone number utilities
  • next-themes.tsx - Theme provider

2. Component Mocks

  • src/components/ui/__mocks__/ - UI component mocks
  • src/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 mocks
  • src/lib/__mocks__/ - Utility function mocks
  • src/providers/__mocks__/ - Context provider mocks
  • src/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:

  1. Place them in the appropriate __mocks__ directory
  2. Match the export pattern of the real module
  3. Include appropriate data-testid attributes
  4. 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

  1. Reset mocks between tests:
beforeEach(() => {
  jest.clearAllMocks();
});
  1. Mock return values when needed:
const mockUseAuth = jest.requireMock('@/providers/AuthProvider').useAuth;
mockUseAuth.mockReturnValue({ user: null, loading: false });
  1. Avoid over-mocking: Only mock what's necessary for the test to run
  2. Keep mocks simple: Provide minimal functionality needed for tests
  3. Document complex mocks: Add comments explaining why certain mock behavior is needed

Testing Patterns

Query Priority

Follow this priority when selecting elements in tests:

  1. getByRole - For interactive elements
  2. getByLabelText - For form controls
  3. getByPlaceholderText - When label isn't available
  4. getByText - For non-interactive content
  5. 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.error and console.warn when 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 afterEach or 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>
  );
}