Claude Code Plugins

Community-maintained marketplace

Feedback

testing-coverage

@ferryhinardi/supertool
1
0

Guide for writing comprehensive tests and achieving >= 95% coverage. Use this when asked to write tests, fix test coverage, or debug failing tests.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name testing-coverage
description Guide for writing comprehensive tests and achieving >= 95% coverage. Use this when asked to write tests, fix test coverage, or debug failing tests.
license MIT

Testing and Coverage Guide

This skill ensures all code meets the MANDATORY >= 95% test coverage requirement using Vitest with browser mode.

Critical Requirements

MANDATORY: All code MUST achieve >= 95% coverage for:

  • Lines
  • Functions
  • Branches
  • Statements

CI/CD will FAIL if coverage drops below 95%

Test Environment Setup

First-Time Setup

# Install Chromium for browser tests
pnpm exec playwright install chromium

Running Tests

# Watch mode
CI=true pnpm test

# Single test file
pnpm test -- path/to/test.tsx

# With coverage report
CI=true pnpm test run --coverage

# Visual UI
CI=true pnpm test:ui

# Browser mode (for component tests)
CI=true pnpm test:browser

Test File Locations

app/
  tools/
    your-tool/
      __tests__/
        page.test.tsx
        logic.test.ts
  api/
    endpoint/
      __tests__/
        route.test.ts
components/
  ui/
    __tests__/
      button.test.tsx
hooks/
  __tests__/
    useSomeHook.test.ts
lib/
  __tests__/
    utility.test.ts

Test Patterns by File Type

1. Component Tests (*.test.tsx)

import { describe, expect, it, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import { userEvent } from '@testing-library/user-event'
import Component from '../Component'

// Mock dependencies
vi.mock('@/lib/analytics', () => ({
  trackToolEvent: vi.fn(),
}))

vi.mock('sonner', () => ({
  toast: {
    success: vi.fn(),
    error: vi.fn(),
  },
}))

describe('Component', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  describe('Rendering', () => {
    it('renders initial state correctly', () => {
      render(<Component />)
      expect(screen.getByText('Expected Text')).toBeInTheDocument()
    })

    it('renders with props', () => {
      render(<Component prop="value" />)
      expect(screen.getByText('value')).toBeInTheDocument()
    })

    it('applies correct CSS classes', () => {
      render(<Component />)
      const element = screen.getByRole('button')
      expect(element).toHaveClass('expected-class')
    })
  })

  describe('User Interactions', () => {
    it('handles button clicks', async () => {
      const user = userEvent.setup()
      render(<Component />)
      
      const button = screen.getByRole('button', { name: /click me/i })
      await user.click(button)
      
      expect(screen.getByText('Clicked')).toBeInTheDocument()
    })

    it('handles text input', async () => {
      const user = userEvent.setup()
      render(<Component />)
      
      const input = screen.getByRole('textbox')
      await user.type(input, 'test input')
      
      expect(input).toHaveValue('test input')
    })

    it('handles file uploads', async () => {
      const user = userEvent.setup()
      render(<Component />)
      
      const file = new File(['content'], 'test.txt', { type: 'text/plain' })
      const input = screen.getByLabelText(/upload/i)
      
      await user.upload(input, file)
      
      expect(screen.getByText('test.txt')).toBeInTheDocument()
    })

    it('handles form submission', async () => {
      const user = userEvent.setup()
      const onSubmit = vi.fn()
      render(<Component onSubmit={onSubmit} />)
      
      await user.type(screen.getByRole('textbox'), 'value')
      await user.click(screen.getByRole('button', { name: /submit/i }))
      
      expect(onSubmit).toHaveBeenCalledWith({ field: 'value' })
    })
  })

  describe('State Management', () => {
    it('updates state on user action', async () => {
      const user = userEvent.setup()
      render(<Component />)
      
      expect(screen.queryByText('Updated')).not.toBeInTheDocument()
      
      await user.click(screen.getByRole('button'))
      
      expect(screen.getByText('Updated')).toBeInTheDocument()
    })

    it('resets state correctly', async () => {
      const user = userEvent.setup()
      render(<Component />)
      
      await user.type(screen.getByRole('textbox'), 'value')
      await user.click(screen.getByRole('button', { name: /reset/i }))
      
      expect(screen.getByRole('textbox')).toHaveValue('')
    })
  })

  describe('Error Handling', () => {
    it('displays error messages', async () => {
      const user = userEvent.setup()
      render(<Component />)
      
      // Trigger error condition
      await user.click(screen.getByRole('button'))
      
      await waitFor(() => {
        expect(screen.getByText(/error/i)).toBeInTheDocument()
      })
    })

    it('handles invalid input', async () => {
      const user = userEvent.setup()
      const { toast } = await import('sonner')
      render(<Component />)
      
      await user.type(screen.getByRole('textbox'), 'invalid')
      await user.click(screen.getByRole('button'))
      
      expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('Invalid'))
    })
  })

  describe('Loading States', () => {
    it('shows loading indicator', async () => {
      render(<Component />)
      
      expect(screen.queryByText(/loading/i)).not.toBeInTheDocument()
      
      // Trigger async action
      const user = userEvent.setup()
      await user.click(screen.getByRole('button'))
      
      expect(screen.getByText(/loading/i)).toBeInTheDocument()
    })
  })

  describe('Conditional Rendering', () => {
    it('renders based on state', () => {
      render(<Component showSection={true} />)
      expect(screen.getByText('Section Content')).toBeInTheDocument()
    })

    it('hides content when condition is false', () => {
      render(<Component showSection={false} />)
      expect(screen.queryByText('Section Content')).not.toBeInTheDocument()
    })
  })

  describe('Analytics', () => {
    it('tracks user events', async () => {
      const { trackToolEvent } = await import('@/lib/analytics')
      const user = userEvent.setup()
      render(<Component />)
      
      await user.click(screen.getByRole('button'))
      
      expect(trackToolEvent).toHaveBeenCalledWith(
        'tool-id',
        'action_name',
        expect.any(Object)
      )
    })
  })

  describe('Accessibility', () => {
    it('has accessible labels', () => {
      render(<Component />)
      expect(screen.getByLabelText('Input Label')).toBeInTheDocument()
    })

    it('supports keyboard navigation', async () => {
      const user = userEvent.setup()
      render(<Component />)
      
      await user.tab()
      expect(screen.getByRole('button')).toHaveFocus()
    })
  })
})

2. API Route Tests (route.test.ts)

import { describe, expect, it, vi, beforeEach } from 'vitest'
import { POST, GET, PUT, DELETE } from '../route'

// Mock external services
vi.mock('@/lib/supabaseClient', () => ({
  supabase: {
    from: vi.fn(),
  },
}))

describe('POST /api/endpoint', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  describe('Validation', () => {
    it('returns 400 for missing required fields', async () => {
      const request = new Request('http://localhost/api/endpoint', {
        method: 'POST',
        body: JSON.stringify({}),
      })
      
      const response = await POST(request as any)
      const data = await response.json()
      
      expect(response.status).toBe(400)
      expect(data.error).toContain('required')
    })

    it('returns 400 for invalid field types', async () => {
      const request = new Request('http://localhost/api/endpoint', {
        method: 'POST',
        body: JSON.stringify({ field: 123 }), // Should be string
      })
      
      const response = await POST(request as any)
      
      expect(response.status).toBe(400)
    })

    it('returns 400 for malformed JSON', async () => {
      const request = new Request('http://localhost/api/endpoint', {
        method: 'POST',
        body: 'invalid json',
      })
      
      const response = await POST(request as any)
      
      expect(response.status).toBe(400)
    })
  })

  describe('Success Cases', () => {
    it('processes valid request successfully', async () => {
      const request = new Request('http://localhost/api/endpoint', {
        method: 'POST',
        body: JSON.stringify({ field: 'value' }),
      })
      
      const response = await POST(request as any)
      const data = await response.json()
      
      expect(response.status).toBe(200)
      expect(data).toHaveProperty('result')
    })

    it('returns correct data structure', async () => {
      const request = new Request('http://localhost/api/endpoint', {
        method: 'POST',
        body: JSON.stringify({ field: 'value' }),
      })
      
      const response = await POST(request as any)
      const data = await response.json()
      
      expect(data).toMatchObject({
        success: true,
        data: expect.any(Object),
      })
    })
  })

  describe('Error Handling', () => {
    it('returns 404 for not found resources', async () => {
      const request = new Request('http://localhost/api/endpoint', {
        method: 'POST',
        body: JSON.stringify({ id: 'nonexistent' }),
      })
      
      const response = await POST(request as any)
      
      expect(response.status).toBe(404)
    })

    it('returns 409 for conflicts', async () => {
      // Create duplicate entry
      const request = new Request('http://localhost/api/endpoint', {
        method: 'POST',
        body: JSON.stringify({ slug: 'existing' }),
      })
      
      const response = await POST(request as any)
      
      expect(response.status).toBe(409)
    })

    it('returns 500 for server errors', async () => {
      // Mock database error
      const { supabase } = await import('@/lib/supabaseClient')
      vi.mocked(supabase.from).mockImplementation(() => {
        throw new Error('Database error')
      })
      
      const request = new Request('http://localhost/api/endpoint', {
        method: 'POST',
        body: JSON.stringify({ field: 'value' }),
      })
      
      const response = await POST(request as any)
      
      expect(response.status).toBe(500)
    })
  })

  describe('Rate Limiting', () => {
    it('returns 429 for too many requests', async () => {
      // Make multiple requests
      const requests = Array(10).fill(null).map(() =>
        new Request('http://localhost/api/endpoint', {
          method: 'POST',
          body: JSON.stringify({ field: 'value' }),
        })
      )
      
      const responses = await Promise.all(
        requests.map(req => POST(req as any))
      )
      
      const tooManyRequests = responses.filter(r => r.status === 429)
      expect(tooManyRequests.length).toBeGreaterThan(0)
    })
  })

  describe('External API Integration', () => {
    it('handles external API success', async () => {
      global.fetch = vi.fn().mockResolvedValueOnce({
        ok: true,
        json: async () => ({ result: 'success' }),
      })
      
      const request = new Request('http://localhost/api/endpoint', {
        method: 'POST',
        body: JSON.stringify({ field: 'value' }),
      })
      
      const response = await POST(request as any)
      
      expect(response.status).toBe(200)
    })

    it('handles external API failure', async () => {
      global.fetch = vi.fn().mockRejectedValueOnce(new Error('API Error'))
      
      const request = new Request('http://localhost/api/endpoint', {
        method: 'POST',
        body: JSON.stringify({ field: 'value' }),
      })
      
      const response = await POST(request as any)
      
      expect(response.status).toBe(503)
    })
  })
})

3. Utility Function Tests (*.test.ts)

import { describe, expect, it } from 'vitest'
import { utilityFunction } from '../utility'

describe('utilityFunction', () => {
  describe('Valid Inputs', () => {
    it('handles normal case', () => {
      expect(utilityFunction('input')).toBe('expected')
    })

    it('handles edge case: empty string', () => {
      expect(utilityFunction('')).toBe('')
    })

    it('handles edge case: special characters', () => {
      expect(utilityFunction('!@#$')).toBe('!@#$')
    })
  })

  describe('Invalid Inputs', () => {
    it('throws error for null', () => {
      expect(() => utilityFunction(null as any)).toThrow()
    })

    it('throws error for undefined', () => {
      expect(() => utilityFunction(undefined as any)).toThrow()
    })
  })

  describe('Type Handling', () => {
    it('handles numbers', () => {
      expect(utilityFunction(123)).toBe('123')
    })

    it('handles arrays', () => {
      expect(utilityFunction(['a', 'b'])).toEqual(['a', 'b'])
    })

    it('handles objects', () => {
      expect(utilityFunction({ key: 'value' })).toEqual({ key: 'value' })
    })
  })

  describe('Async Operations', () => {
    it('resolves with correct value', async () => {
      const result = await asyncUtilityFunction('input')
      expect(result).toBe('expected')
    })

    it('rejects with error', async () => {
      await expect(asyncUtilityFunction('invalid')).rejects.toThrow()
    })
  })
})

4. Hook Tests (*.test.ts)

import { describe, expect, it, beforeEach } from 'vitest'
import { renderHook, act, waitFor } from '@testing-library/react'
import { useCustomHook } from '../useCustomHook'

describe('useCustomHook', () => {
  beforeEach(() => {
    // Reset state between tests
  })

  it('initializes with default values', () => {
    const { result } = renderHook(() => useCustomHook())
    
    expect(result.current.value).toBe(null)
    expect(result.current.loading).toBe(false)
  })

  it('updates value on action', () => {
    const { result } = renderHook(() => useCustomHook())
    
    act(() => {
      result.current.setValue('new value')
    })
    
    expect(result.current.value).toBe('new value')
  })

  it('handles async operations', async () => {
    const { result } = renderHook(() => useCustomHook())
    
    act(() => {
      result.current.fetchData()
    })
    
    expect(result.current.loading).toBe(true)
    
    await waitFor(() => {
      expect(result.current.loading).toBe(false)
      expect(result.current.value).toBeDefined()
    })
  })

  it('cleans up on unmount', () => {
    const { unmount } = renderHook(() => useCustomHook())
    
    unmount()
    
    // Verify cleanup happened
  })
})

Coverage Troubleshooting

Check Current Coverage

CI=true pnpm test run --coverage

Find Uncovered Lines

  1. Open coverage/index.html in browser
  2. Navigate to file with < 95% coverage
  3. Red/yellow lines indicate uncovered code

Common Coverage Issues

Issue: Uncovered branches

// Before (uncovered else branch)
if (condition) {
  doSomething()
}

// Test both branches
it('handles true condition', () => {
  // Test with condition = true
})

it('handles false condition', () => {
  // Test with condition = false
})

Issue: Uncovered error handlers

// Test the catch block
it('handles errors', async () => {
  // Mock to throw error
  vi.mocked(someFunction).mockRejectedValue(new Error('Test error'))
  
  // Verify error handling
})

Issue: Uncovered async callbacks

// Test async state updates
it('updates after async operation', async () => {
  // Trigger async operation
  
  await waitFor(() => {
    expect(result).toBeDefined()
  })
})

Test Quality Checklist

  • All user interactions tested
  • All error scenarios tested
  • All conditional branches tested
  • All async operations tested
  • Loading states tested
  • Analytics tracking verified
  • Accessibility features tested
  • Mocks cleared between tests (beforeEach)
  • Tests are isolated (no shared state)
  • = 95% coverage achieved

  • Coverage report reviewed

References

  • Test examples: app/tools/split-bill/__tests__/
  • API test examples: app/api/shorten/__tests__/
  • Setup file: vitest.setup.ts
  • Configuration: vitest.config.mts