# Testing Strategy

This document outlines the testing approach for the MetaMuLate decomposition.

## Testing Stack

| Type | Tool | Purpose |
|------|------|---------|
| Unit | Vitest | Fast unit tests for utilities |
| Component | Testing Library | React component testing |
| Integration | Vitest | Service/hook integration |
| E2E | Playwright | Full user flow testing |
| Visual | Storybook | Component documentation |

## Directory Structure

```
src/
├── components/
│   └── ComponentName/
│       ├── ComponentName.tsx
│       ├── ComponentName.test.tsx      # Component tests
│       └── index.ts
├── hooks/
│   └── __tests__/
│       └── hookName.test.ts
├── services/
│   └── __tests__/
│       └── ServiceName.test.ts
├── utils/
│   └── authorityScore/
│       ├── authorityScore.ts
│       └── __tests__/
│           └── authorityScore.spec.ts
└── __e2e__/
    └── flows/
        └── trackResolution.spec.ts
```

---

## Unit Tests

### Utilities

Test pure functions in isolation:

```typescript
// utils/authorityScore/__tests__/authorityScore.spec.ts
import { calculateAuthorityScore, cleanName } from '../authorityScore';

describe('calculateAuthorityScore', () => {
  it('should return 50 for ISRC presence', () => {
    const track = createTrack({ isrc: 'USRC11234567' });
    const result = calculateAuthorityScore(track);
    expect(result.isrcBonus).toBe(50);
  });

  it('should add vintage bonus for pre-2000 releases', () => {
    const track = createTrack({ year: '1997' });
    const result = calculateAuthorityScore(track);
    expect(result.vintageBonus).toBe(30);
  });
});

describe('cleanName', () => {
  it('should remove featuring variations', () => {
    expect(cleanName('Song (feat. Artist)')).toBe('Song');
    expect(cleanName('Song ft. Artist')).toBe('Song');
  });
});
```

### Services

Test service methods with mocked dependencies:

```typescript
// services/__tests__/RateLimiter.test.ts
import { RateLimiter } from '../RateLimiter';

describe('RateLimiter', () => {
  beforeEach(() => {
    RateLimiter.reset();
  });

  it('should throttle requests to specified interval', async () => {
    const start = Date.now();
    
    await RateLimiter.throttle('test', 100);
    await RateLimiter.throttle('test', 100);
    
    const elapsed = Date.now() - start;
    expect(elapsed).toBeGreaterThanOrEqual(100);
  });
});
```

---

## Component Tests

Use React Testing Library for component tests:

```typescript
// components/TrackList/__tests__/TrackListItem.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { TrackListItem } from '../TrackListItem';
import { createTestTrack } from '@/test/factories';

describe('TrackListItem', () => {
  it('should display track name and artist', () => {
    const track = createTestTrack({
      track_name: { val: 'Test Song', url: null },
      artist: { val: 'Test Artist', url: null },
    });

    render(
      <TrackListItem
        track={track}
        isSelected={false}
        isResolved={false}
        onClick={() => {}}
      />
    );

    expect(screen.getByText('Test Song')).toBeInTheDocument();
    expect(screen.getByText('Test Artist')).toBeInTheDocument();
  });

  it('should call onClick when clicked', () => {
    const onClick = vi.fn();
    const track = createTestTrack();

    render(
      <TrackListItem
        track={track}
        isSelected={false}
        isResolved={false}
        onClick={onClick}
      />
    );

    fireEvent.click(screen.getByRole('article'));
    expect(onClick).toHaveBeenCalledTimes(1);
  });

  it('should show ISRC badge when ISRC is present', () => {
    const track = createTestTrack({
      isrc: { val: 'USRC11234567', url: null },
    });

    render(
      <TrackListItem
        track={track}
        isSelected={false}
        isResolved={false}
        onClick={() => {}}
      />
    );

    expect(screen.getByText('ISRC')).toBeInTheDocument();
  });

  it('should show resolved indicator when isResolved', () => {
    const track = createTestTrack();

    render(
      <TrackListItem
        track={track}
        isSelected={false}
        isResolved={true}
        onClick={() => {}}
      />
    );

    expect(screen.getByTitle('Resolved')).toBeInTheDocument();
  });
});
```

---

## Hook Tests

Test custom hooks with renderHook:

```typescript
// hooks/__tests__/useGoldenRecord.test.ts
import { renderHook, act } from '@testing-library/react';
import { useGoldenRecord } from '../useGoldenRecord';
import { createTestTrack } from '@/test/factories';

describe('useGoldenRecord', () => {
  it('should initialize from track', () => {
    const { result } = renderHook(() => useGoldenRecord());
    const track = createTestTrack({
      track_name: { val: 'Test', url: null },
    });

    act(() => {
      result.current.initFromTrack(track);
    });

    expect(result.current.goldenRecord?.track_name?.val).toBe('Test');
  });

  it('should track source selections', () => {
    const { result } = renderHook(() => useGoldenRecord());
    const track = createTestTrack();

    act(() => {
      result.current.initFromTrack(track);
    });

    act(() => {
      result.current.selectField('isrc', 'apple', { val: 'ISRC123', url: 'http://apple.com' });
    });

    expect(result.current.sourceSelections).toHaveLength(1);
    expect(result.current.sourceSelections[0].sourceKey).toBe('apple');
  });
});
```

---

## Integration Tests

Test service interactions:

```typescript
// services/__tests__/ScrapeManager.integration.test.ts
import { ScrapeManager } from '../ScrapeManager';
import { createTestTrack } from '@/test/factories';

describe('ScrapeManager Integration', () => {
  const mockScrapers = {
    apple: vi.fn().mockResolvedValue({ track_name: { val: 'Apple Track', url: null } }),
    discogs: vi.fn().mockResolvedValue({ label: { val: 'Test Label', url: null } }),
  };

  it('should orchestrate concurrent scraping', async () => {
    const track = createTestTrack();
    
    const results = await ScrapeManager.startScraping(
      track,
      ['apple', 'discogs'],
      mockScrapers
    );

    expect(results.size).toBe(2);
    expect(results.get('apple')?.success).toBe(true);
    expect(results.get('discogs')?.success).toBe(true);
  });

  it('should handle cancellation', async () => {
    const slowScraper = vi.fn().mockImplementation(
      () => new Promise((resolve) => setTimeout(resolve, 5000))
    );

    const promise = ScrapeManager.startScraping(
      createTestTrack(),
      ['slow'],
      { slow: slowScraper }
    );

    // Cancel after 100ms
    setTimeout(() => ScrapeManager.cancelAll(), 100);

    const results = await promise;
    expect(results.get('slow')?.error).toContain('cancelled');
  });
});
```

---

## E2E Tests

Use Playwright for full user flows:

```typescript
// __e2e__/flows/trackResolution.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Track Resolution Flow', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/');
  });

  test('should search for artist and display tracks', async ({ page }) => {
    await page.fill('[data-testid="search-input"]', 'Daft Punk');
    await page.click('[data-testid="search-button"]');
    
    await expect(page.locator('[data-testid="track-item"]')).toHaveCount.greaterThan(0);
  });

  test('should select track and show matrix', async ({ page }) => {
    // Search and wait
    await page.fill('[data-testid="search-input"]', 'Daft Punk');
    await page.click('[data-testid="search-button"]');
    await page.waitForSelector('[data-testid="track-item"]');

    // Select first track
    await page.click('[data-testid="track-item"]:first-child');

    // Matrix should appear
    await expect(page.locator('[data-testid="metadata-matrix"]')).toBeVisible();
  });

  test('should select cells and build golden record', async ({ page }) => {
    // Setup: select a track
    await page.fill('[data-testid="search-input"]', 'Daft Punk');
    await page.click('[data-testid="search-button"]');
    await page.waitForSelector('[data-testid="track-item"]');
    await page.click('[data-testid="track-item"]:first-child');

    // Click a matrix cell
    await page.click('[data-testid="matrix-cell-isrc-apple"]');

    // Golden record should update
    await expect(page.locator('[data-testid="golden-isrc"]')).not.toBeEmpty();
  });

  test('should resolve track and show success', async ({ page }) => {
    // Complete flow...
    await page.click('[data-testid="resolve-button"]');
    
    await expect(page.locator('.toast-success')).toBeVisible();
  });
});
```

---

## Test Factories

Create reusable test data factories:

```typescript
// test/factories.ts
import { TrackMetadata, createEmptyTrackMetadata } from '@/types/TrackMetadata';
import { DataValue } from '@/types/DataValue';

let trackIdCounter = 0;

export function createTestTrack(
  overrides: Partial<Record<string, DataValue | string>> = {}
): TrackMetadata {
  const id = `test-track-${++trackIdCounter}`;
  const track = createEmptyTrackMetadata(id);

  // Apply overrides
  for (const [key, value] of Object.entries(overrides)) {
    if (typeof value === 'string') {
      (track as Record<string, unknown>)[key] = value;
    } else {
      (track as Record<string, unknown>)[key] = value;
    }
  }

  return track;
}

export function createDataValue(val: string, url?: string): DataValue {
  return { val, url: url ?? null };
}
```

---

## Coverage Goals

| Category | Target | Notes |
|----------|--------|-------|
| Utilities | 90%+ | Pure functions, easy to test |
| Hooks | 80%+ | Logic-heavy, critical |
| Components | 70%+ | UI behavior coverage |
| Services | 80%+ | Business logic |
| E2E | Core flows | Happy path coverage |

---

## CI Configuration

```yaml
# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '20'
      
      - run: npm ci
      - run: npm run test:coverage
      - run: npm run test:e2e
      
      - uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info
```
