dograh/plans/frontend-testing-plan.md
Chris Briddock 20617db37a
Add Open AI Comatible API option in model configuration
Add unit tests for various UI components and context hooks

- Created tests for Progress, RadioGroup, Select, Separator, Sheet, Sidebar, Skeleton, Sonner, Switch, Table, Tabs, Textarea, Tooltip components.
- Implemented tests for UserConfigContext and other context hooks.
- Added utility tests for filters and utils functions.
- Set up MSW for API mocking in tests.
- Configured Vitest for testing environment with necessary setup.
2026-05-25 19:26:41 +01:00

22 KiB

Frontend Testing Plan — Dograh UI

Overview

The Dograh frontend is a Next.js 15 + React 19 + TypeScript application using:

  • UI: shadcn/ui + Radix UI primitives + Tailwind CSS
  • State: React Context (global), Zustand (workflow builder), React Hook Form (forms)
  • API: Auto-generated OpenAPI client (@hey-api/openapi-ts)
  • Auth: Stack Auth (@stackframe/stack)
  • Charts: Recharts
  • Flow: React Flow (@xyflow/react)

Current test coverage: Zero — no test framework, no test files, no CI test step.


Phase 1: Tooling Setup

1.1 Install Test Dependencies

cd ui
npm install -D vitest @vitest/ui @testing-library/react @testing-library/jest-dom \
  @testing-library/user-event @testing-library/dom jsdom msw@latest \
  @playwright/test @types/jest

Rationale:

  • Vitest: Fast, Vite-native, excellent TypeScript support, replaces Jest
  • @testing-library/react: Render components, fire events, query DOM
  • @testing-library/jest-dom: Custom matchers (toBeInTheDocument, toHaveValue)
  • jsdom: Browser-like environment for unit tests
  • MSW (Mock Service Worker): Intercept and mock HTTP requests at the network layer
  • Playwright: E2E testing with real browser automation

1.2 Configuration Files

ui/vitest.config.ts:

import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./src/test/setup.ts'],
    include: ['src/**/*.test.{ts,tsx}'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
      exclude: [
        'node_modules/',
        'src/test/',
        'src/client/',
        '**/*.d.ts',
      ],
    },
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});

ui/src/test/setup.ts:

import '@testing-library/jest-dom';
import { server } from './mocks/server';

// Start MSW before all tests
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));

// Reset handlers after each test
afterEach(() => server.resetHandlers());

// Close MSW after all tests
afterAll(() => server.close());

ui/src/test/mocks/server.ts:

import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);

ui/src/test/mocks/handlers.ts:

import { http, HttpResponse } from 'msw';

export const handlers = [
  // Auth
  http.get('/api/auth/session', () => {
    return HttpResponse.json({ user: { id: '1', email: 'test@example.com' } });
  }),

  // User config
  http.get('/api/v1/user/configurations/user', () => {
    return HttpResponse.json({
      llm: { provider: 'openai', model: 'gpt-4.1', base_url: 'https://api.openai.com/v1' },
      tts: { provider: 'openai', model: 'gpt-4o-mini-tts' },
      stt: { provider: 'deepgram', model: 'nova-2' },
    });
  }),

  http.put('/api/v1/user/configurations/user', async () => {
    return HttpResponse.json({ success: true });
  }),

  // Workflows
  http.get('/api/v1/workflows', () => {
    return HttpResponse.json({ items: [], total: 0 });
  }),

  // Add more handlers as needed
];

ui/src/test/test-utils.tsx — Custom render with providers:

import { render as rtlRender, RenderOptions } from '@testing-library/react';
import { ReactElement, ReactNode } from 'react';
import { UserConfigProvider } from '@/context/UserConfigContext';
import { AppConfigProvider } from '@/context/AppConfigContext';

function AllProviders({ children }: { children: ReactNode }) {
  return (
    <AppConfigProvider>
      <UserConfigProvider>
        {children}
      </UserConfigProvider>
    </AppConfigProvider>
  );
}

export function render(ui: ReactElement, options?: Omit<RenderOptions, 'wrapper'>) {
  return rtlRender(ui, { wrapper: AllProviders, ...options });
}

export * from '@testing-library/react';

ui/playwright.config.ts:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

1.3 Update package.json Scripts

{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest --coverage",
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui"
  }
}

Phase 2: Unit Tests

2.1 UI Primitive Components (src/components/ui/)

These are thin wrappers around Radix UI. Test focus: correct props forwarding, accessibility, styling.

Component Test File Key Assertions
Button button.test.tsx Renders children, handles click, disabled state, variants
Input input.test.tsx Accepts value, onChange, placeholder, disabled
Select select.test.tsx Opens dropdown, selects option, displays value
Dialog dialog.test.tsx Opens/closes, renders title, focus trap, ESC to close
Checkbox checkbox.test.tsx Checked/unchecked state, onChange fires
Tabs tabs.test.tsx Switches panels, active tab indicator
Switch switch.test.tsx Toggles on click, respects checked prop
Badge badge.test.tsx Renders text, applies variant classes
Card card.test.tsx Renders header, content, footer slots
Table table.test.tsx Renders rows/columns, sortable headers
Textarea textarea.test.tsx Multi-line input, resize behavior
Calendar calendar.test.tsx Date selection, month navigation
DropdownMenu dropdown-menu.test.tsx Opens on click, item selection, keyboard nav
Popover popover.test.tsx Opens/closes, positions content
Tooltip tooltip.test.tsx Shows on hover, hides on leave
RadioGroup radio-group.test.tsx Single selection, keyboard navigation
Progress progress.test.tsx Renders correct width for value
Separator separator.test.tsx Renders horizontal/vertical
Skeleton skeleton.test.tsx Renders with pulse animation class
Sonner sonner.test.tsx Toast appears, auto-dismisses
Sheet sheet.test.tsx Slides in from side, closes on overlay click
Sidebar sidebar.test.tsx Expands/collapses, shows/hides items
AlertDialog alert-dialog.test.tsx Confirm/cancel actions, focus management
Collapsible collapsible.test.tsx Expands/collapses content
ChoiceChips choice-chips.test.tsx Single/multi selection, removable chips
JSONEditor json-editor.test.tsx Validates JSON, shows error state

Example — button.test.tsx:

import { render, screen, fireEvent } from '@/test/test-utils';
import { Button } from './button';

describe('Button', () => {
  it('renders children', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
  });

  it('handles click events', () => {
    const handleClick = vi.fn();
    render(<Button onClick={handleClick}>Click</Button>);
    fireEvent.click(screen.getByRole('button'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('is disabled when disabled prop is true', () => {
    render(<Button disabled>Disabled</Button>);
    expect(screen.getByRole('button')).toBeDisabled();
  });

  it('applies variant classes', () => {
    const { container } = render(<Button variant="destructive">Delete</Button>);
    expect(container.firstChild).toHaveClass('bg-destructive');
  });
});

2.2 Utility Functions (src/lib/)

File Test File Key Test Cases
utils.ts utils.test.ts cn() class merging, date formatting, slug generation
filters.ts filters.test.ts Filter predicate composition, attribute extraction
filterAttributes.ts filterAttributes.test.ts Attribute parsing, type coercion
files.ts files.test.ts File upload validation, size/format checks
logger.ts logger.test.ts Log level filtering, structured output
apiClient.ts apiClient.test.ts Base URL resolution (server vs client), interceptor registration

2.3 Custom Hooks (src/hooks/)

Hook Test File Key Test Cases
use-mobile.ts use-mobile.test.ts Detects mobile viewport, updates on resize
useAudioPlayback.ts useAudioPlayback.test.ts Play/pause/stop, progress tracking, error handling
useLatestReleaseVersion.ts useLatestReleaseVersion.test.ts Fetches version, caches result, handles errors

Example — use-mobile.test.ts:

import { renderHook } from '@testing-library/react';
import { useIsMobile } from './use-mobile';

describe('useIsMobile', () => {
  it('returns false for desktop viewport', () => {
    window.innerWidth = 1024;
    const { result } = renderHook(() => useIsMobile());
    expect(result.current).toBe(false);
  });

  it('returns true for mobile viewport', () => {
    window.innerWidth = 375;
    const { result } = renderHook(() => useIsMobile());
    expect(result.current).toBe(true);
  });
});

2.4 Constants & Types (src/constants/, src/types/)

  • Verify constant values match expected enums
  • Type tests using tsd or vitest typecheck

Phase 3: Integration Tests

3.1 Context Providers (src/context/)

Test with real provider wrappers, mock API calls via MSW.

Context Test File Key Test Cases
UserConfigContext UserConfigContext.test.tsx Fetches config on mount, saves config, handles errors, loading states
AppConfigContext AppConfigContext.test.tsx Provides app config, handles feature flags
OnboardingContext OnboardingContext.test.tsx Tracks onboarding step, persists state
UnsavedChangesContext UnsavedChangesContext.test.tsx Tracks dirty state, warns on navigation
TelephonyConfigWarningsContext TelephonyConfigWarningsContext.test.tsx Displays warnings for missing telephony config

Example — UserConfigContext.test.tsx:

import { render, screen, waitFor } from '@/test/test-utils';
import { useUserConfig } from './UserConfigContext';

function TestComponent() {
  const { userConfig, loading, saveUserConfig } = useUserConfig();
  if (loading) return <div>Loading</div>;
  return (
    <div>
      <div data-testid="provider">{userConfig?.llm?.provider}</div>
      <button onClick={() => saveUserConfig({ llm: { provider: 'openai', model: 'gpt-4' } })}>
        Save
      </button>
    </div>
  );
}

describe('UserConfigContext', () => {
  it('fetches user config on mount', async () => {
    render(<TestComponent />);
    expect(screen.getByText('Loading')).toBeInTheDocument();
    await waitFor(() => {
      expect(screen.getByTestId('provider')).toHaveTextContent('openai');
    });
  });

  it('saves user config', async () => {
    render(<TestComponent />);
    await waitFor(() => screen.getByRole('button'));
    fireEvent.click(screen.getByRole('button'));
    await waitFor(() => {
      // Assert API was called
    });
  });
});

3.2 Complex Components (src/components/)

Component Test File Key Test Cases
ServiceConfigurationForm ServiceConfigurationForm.test.tsx Renders all tabs, switches providers, shows/hides fields, validates, submits
LLMConfigSelector LLMConfigSelector.test.tsx Provider dropdown, model selection, custom input toggle
VoiceSelector VoiceSelector.test.tsx Voice list, selection, preview
WorkflowCard WorkflowCard.test.tsx Renders workflow data, edit/delete actions
WorkflowTable WorkflowTable.test.tsx Sorting, pagination, row actions
CreateWorkflowButton CreateWorkflowButton.test.tsx Opens modal, creates workflow, shows error
TemplateCard TemplateCard.test.tsx Renders template, applies on click
UploadWorkflowButton UploadWorkflowButton.test.tsx File upload, validation, error handling
DailyUsageTable DailyUsageTable.test.tsx Renders usage data, date formatting
MCPSection MCPSection.test.tsx Tool list, add/remove, configuration
MediaPreviewDialog MediaPreviewDialog.test.tsx Opens with media, closes, audio playback
ChatwootWidget ChatwootWidget.test.tsx Loads widget, handles events
SignInClient SignInClient.test.tsx Form validation, auth flow, error display
SpinLoader SpinLoader.test.tsx Shows/hides based on loading prop
ThemeSwitcher ThemeSwitcher.test.tsx Toggles theme, persists preference
Footer Footer.test.tsx Renders links, version info
PostHogIdentify PostHogIdentify.test.tsx Identifies user on auth
SentryErrorBoundary SentryErrorBoundary.test.tsx Catches errors, shows fallback UI
TelemetrySection TelemetrySection.test.tsx Toggle telemetry, persists setting
CallTypeCell CallTypeCell.test.tsx Renders call type badge

3.3 Workflow Builder Components (src/components/workflow/, src/components/flow/)

These use React Flow and Zustand — test with mocked stores.

Component Test File Key Test Cases
Flow canvas flow/*.test.tsx Node rendering, edge connections, drag-and-drop
Node configuration panels workflow/*.test.tsx Form validation, state updates, node types
Conversation components workflow/conversation/*.test.tsx Message rendering, send/receive
Folder management workflow/folders/*.test.tsx CRUD operations, drag-drop

3.4 Page Components (src/app/)

Test page-level integration with routing and data fetching.

Page Test File Key Test Cases
/model-configurations model-configurations/page.test.tsx Loads config, renders form, saves changes
/workflow/create workflow/create/page.test.tsx Creates workflow, validates, redirects
/workflow/[id] workflow/[id]/page.test.tsx Loads workflow, edits, saves
/settings settings/page.test.tsx Renders settings, updates preferences
/auth/login auth/login/page.test.tsx Form validation, login flow, error handling
/overview overview/page.test.tsx Dashboard stats, recent activity
/recordings recordings/page.test.tsx List recordings, play, filter
/tools tools/page.test.tsx Tool list, create/edit/delete
/campaigns campaigns/page.test.tsx Campaign list, status, actions
/usage usage/page.test.tsx Usage charts, date range selection
/api-keys api-keys/page.test.tsx Key list, generate, revoke
/telephony-configurations telephony-configurations/page.test.tsx Provider config, validation
/files files/page.test.tsx File list, upload, delete
/reports reports/page.test.tsx Report generation, export
/automation automation/page.test.tsx Automation rules, triggers

Phase 4: E2E Tests (Playwright)

4.1 Authentication Flows

Test File Scenario
auth/login.spec.ts User logs in with valid credentials, sees dashboard
auth/signup.spec.ts New user signs up, completes onboarding
auth/logout.spec.ts User logs out, redirected to login
auth/session.spec.ts Session persists across page reloads

4.2 Core User Journeys

Test File Scenario
workflow/create.spec.ts Create voice agent from scratch, configure LLM, save
workflow/edit.spec.ts Open existing workflow, modify nodes, save changes
workflow/deploy.spec.ts Deploy workflow, verify webhook URL generated
model-configurations.spec.ts Configure OpenAI with custom base URL, save, verify persisted
campaigns/create.spec.ts Create campaign, upload contacts, launch
recordings/play.spec.ts Navigate to recordings, play audio, verify playback
tools/mcp.spec.ts Add MCP tool, configure server URL, test connection
telephony/configure.spec.ts Add telephony provider, configure inbound webhook

4.3 Critical Path — Voice Agent Creation

// e2e/workflow/create-voice-agent.spec.ts
import { test, expect } from '@playwright/test';

test('create and configure a voice agent end-to-end', async ({ page }) => {
  // Login
  await page.goto('/auth/login');
  await page.fill('[name="email"]', 'test@example.com');
  await page.fill('[name="password"]', 'password');
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL('/overview');

  // Navigate to create workflow
  await page.click('text=Voice Agents');
  await page.click('text=Create Agent');
  await expect(page).toHaveURL(/\/workflow\/create/);

  // Configure LLM with custom base URL
  await page.click('text=Models');
  await page.selectOption('select[name="llm_provider"]', 'openai');
  await page.fill('input[name="llm_base_url"]', 'https://custom.example.com/v1');
  await page.fill('input[name="llm_api_key"]', 'sk-test-key');
  await page.selectOption('select[name="llm_model"]', 'gpt-4.1');

  // Save configuration
  await page.click('text=Save Configuration');
  await expect(page.locator('.sonner')).toContainText('Saved');

  // Verify persistence
  await page.reload();
  await expect(page.locator('input[name="llm_base_url"]')).toHaveValue('https://custom.example.com/v1');
});

4.4 Cross-Browser & Responsive

  • Run E2E suite on Chromium and Firefox
  • Test key flows at mobile viewport (375px width)
  • Verify sidebar collapses, tables scroll, modals fit screen

Phase 5: Backend Test Expansion

5.1 Service Factory Tests

Test File Coverage
test_openai_service_factory.py OpenAI LLM with/without custom base_url, gpt-5 reasoning params
test_openrouter_service_factory.py OpenRouter with custom base_url
test_speaches_service_factory.py Local model endpoint configuration
test_minimax_service_factory.py MiniMax with custom base_url and temperature
test_azure_service_factory.py Azure endpoint configuration
test_google_vertex_service_factory.py Vertex AI project/location/credentials
test_bedrock_service_factory.py AWS credentials and region

5.2 Configuration Registry Tests

Test File Coverage
test_configuration_registry.py All provider schemas validate, defaults correct, required fields
test_configuration_merge.py Override merging, fallback to global, partial overrides
test_configuration_validity.py API key validation per provider, masked key rejection
test_configuration_defaults_endpoint.py /configurations/defaults returns complete schemas

5.3 Integration Tests for New Feature

Test File Coverage
test_openai_base_url_persistence.py Save/load user config with custom base_url
test_openai_base_url_runtime.py Pipeline uses custom base_url at runtime
test_openai_base_url_workflow_override.py Workflow-level override of base_url

Phase 6: CI/CD Integration

6.1 GitHub Actions Workflow

# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '24' }
      - run: cd ui && npm ci
      - run: cd ui && npm run test:coverage
      - uses: codecov/codecov-action@v4

  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '24' }
      - run: cd ui && npm ci
      - run: cd ui && npx playwright install
      - run: cd ui && npm run test:e2e
      - uses: actions/upload-artifact@v4
        if: failure()
        with: { name: playwright-report, path: ui/playwright-report/ }

6.2 Coverage Gates

  • Unit tests: minimum 70% line coverage
  • Integration tests: minimum 50% line coverage
  • E2E tests: all critical user journeys must pass
  • Block PR merge on test failure

Implementation Priority

Priority Phase Effort Impact
P0 1. Tooling Setup Medium Enables all testing
P0 2.1 UI Primitives Medium Foundation for component tests
P0 2.2 Utilities + Hooks Low Quick wins, high confidence
P1 3.1 Context Providers Medium Core app logic
P1 3.2 Complex Components High User-facing features
P1 5.1 Service Factory Tests Medium Backend stability
P2 3.3 Workflow Builder High Complex React Flow + Zustand
P2 3.4 Page Components High Full page integration
P2 4.1-4.3 E2E Core Journeys High Regression prevention
P3 4.4 Cross-browser Medium Quality assurance
P3 5.2-5.3 Backend Expansion Medium Complete backend coverage

Estimated Test Count

Layer Estimated Tests
Unit (UI primitives) ~150
Unit (utilities/hooks) ~30
Integration (contexts) ~25
Integration (components) ~80
Integration (pages) ~45
E2E (core journeys) ~20
E2E (edge cases) ~15
Backend unit ~60
Backend integration ~25
Total ~450