Exemples Testing Library

Exemples complets de Testing Library couvrant les tests de composants React et Vue, les interactions utilisateur, le mocking et les patterns de test avancés

Key Facts

Category
Testing
Items
4
Format Families
text

Sample Overview

Exemples complets de Testing Library couvrant les tests de composants React et Vue, les interactions utilisateur, le mocking et les patterns de test avancés This sample set belongs to Testing and can be used to test related workflows inside Elysia Tools.

💻 Bases React Testing Library text

🟢 simple ⭐⭐

Tests de composants React fondamentaux avec des requêtes centrées sur l'utilisateur, des assertions et des tests d'interaction

⏱️ 20 min 🏷️ testing, react, component testing
Prerequisites: React, JavaScript, Jest, DOM basics
// React Testing Library Basics
// File: Button.test.js

import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom'
import React from 'react'
import Button from '../Button'

// 1. Basic component rendering
describe('Button Component', () => {
  test('renders button with text', () => {
    render(<Button>Click me</Button>)

    // Using user-centric queries
    const button = screen.getByRole('button', { name: /click me/i })
    expect(button).toBeInTheDocument()
    expect(button).toHaveTextContent('Click me')
  })

  // 2. Testing accessibility attributes
  test('applies correct accessibility props', () => {
    render(<Button disabled aria-label="Submit form">Submit</Button>)

    const button = screen.getByRole('button', { name: /submit form/i })
    expect(button).toBeDisabled()
    expect(button).toHaveAttribute('aria-label', 'Submit form')
  })

  // 3. Testing CSS classes and styling
  test('applies custom CSS classes', () => {
    const { container } = render(<Button className="custom-btn primary">Styled Button</Button>)

    const button = screen.getByRole('button', { name: /styled button/i })
    expect(button).toHaveClass('custom-btn', 'primary')
    expect(button).toHaveStyle({ backgroundColor: '#0070f3' })
  })

  // 4. Testing conditional rendering
  test('renders different variants correctly', () => {
    const { rerender } = render(<Button variant="primary">Primary</Button>)

    let button = screen.getByRole('button', { name: /primary/i })
    expect(button).toHaveClass('btn-primary')

    rerender(<Button variant="secondary">Secondary</Button>)
    button = screen.getByRole('button', { name: /secondary/i })
    expect(button).toHaveClass('btn-secondary')
  })

  // 5. Testing click events with fireEvent
  test('handles click events with fireEvent', () => {
    const handleClick = jest.fn()
    render(<Button onClick={handleClick}>Click me</Button>)

    const button = screen.getByRole('button', { name: /click me/i })
    fireEvent.click(button)

    expect(handleClick).toHaveBeenCalledTimes(1)
  })

  // 6. Testing user interactions with userEvent (preferred)
  test('handles user interactions more realistically', async () => {
    const user = userEvent.setup()
    const handleClick = jest.fn()

    render(<Button onClick={handleClick}>Click me</Button>)

    const button = screen.getByRole('button', { name: /click me/i })
    await user.click(button)

    expect(handleClick).toHaveBeenCalledTimes(1)
  })

  // 7. Testing hover and focus states
  test('handles hover and focus states', async () => {
    const user = userEvent.setup()
    render(<Button>Interactive Button</Button>)

    const button = screen.getByRole('button', { name: /interactive button/i })

    // Test hover
    await user.hover(button)
    expect(button).toHaveStyle({ backgroundColor: '#0051cc' })

    // Test focus
    await user.tab()
    expect(button).toHaveFocus()
  })

  // 8. Testing loading state
  test('shows loading state correctly', () => {
    render(<Button loading>Loading Button</Button>)

    const button = screen.getByRole('button', { name: /loading button/i })
    expect(button).toBeDisabled()
    expect(button.querySelector('[data-testid="spinner"]')).toBeInTheDocument()
  })

  // 9. Testing async behavior
  test('handles async actions', async () => {
    const asyncAction = jest.fn(() => Promise.resolve('success'))
    const user = userEvent.setup()

    render(<Button onClick={asyncAction}>Async Button</Button>)

    const button = screen.getByRole('button', { name: /async button/i })
    await user.click(button)

    await waitFor(() => {
      expect(asyncAction).toHaveBeenCalled()
    })
  })

  // 10. Testing custom hooks integration
  test('integrates with custom hooks', () => {
    const TestComponent = () => {
      const [count, setCount] = React.useState(0)
      return (
        <Button onClick={() => setCount(c => c + 1)}>
          Count: {count}
        </Button>
      )
    }

    render(<TestComponent />)

    const button = screen.getByRole('button')
    expect(button).toHaveTextContent('Count: 0')

    fireEvent.click(button)
    expect(button).toHaveTextContent('Count: 1')
  })
})

// File: Form.test.js
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Form from '../Form'

describe('Form Component', () => {
  test('renders all form fields', () => {
    render(<Form />)

    expect(screen.getByLabelText(/name/i)).toBeInTheDocument()
    expect(screen.getByLabelText(/email/i)).toBeInTheDocument()
    expect(screen.getByLabelText(/password/i)).toBeInTheDocument()
    expect(screen.getByRole('button', { name: /submit/i })).toBeInTheDocument()
  })

  test('validates required fields', async () => {
    const user = userEvent.setup()
    const handleSubmit = jest.fn()

    render(<Form onSubmit={handleSubmit} />)

    const submitButton = screen.getByRole('button', { name: /submit/i })
    await user.click(submitButton)

    // Should show validation errors
    await waitFor(() => {
      expect(screen.getByText(/name is required/i)).toBeInTheDocument()
      expect(screen.getByText(/email is required/i)).toBeInTheDocument()
    })

    // Form should not be submitted
    expect(handleSubmit).not.toHaveBeenCalled()
  })

  test('submits form with valid data', async () => {
    const user = userEvent.setup()
    const handleSubmit = jest.fn()

    render(<Form onSubmit={handleSubmit} />)

    // Fill out form
    await user.type(screen.getByLabelText(/name/i), 'John Doe')
    await user.type(screen.getByLabelText(/email/i), '[email protected]')
    await user.type(screen.getByLabelText(/password/i), 'password123')

    // Submit form
    await user.click(screen.getByRole('button', { name: /submit/i }))

    await waitFor(() => {
      expect(handleSubmit).toHaveBeenCalledWith({
        name: 'John Doe',
        email: '[email protected]',
        password: 'password123'
      })
    })
  })

  test('shows submission feedback', async () => {
    const user = userEvent.setup()

    render(<Form />)

    // Fill and submit form
    await user.type(screen.getByLabelText(/name/i), 'John Doe')
    await user.type(screen.getByLabelText(/email/i), '[email protected]')
    await user.type(screen.getByLabelText(/password/i), 'password123')

    await user.click(screen.getByRole('button', { name: /submit/i }))

    // Should show loading state
    await waitFor(() => {
      expect(screen.getByText(/submitting/i)).toBeInTheDocument()
    })
  })
})

// File: List.test.js
import { render, screen, fireEvent } from '@testing-library/react'
import List from '../List'

describe('List Component', () => {
  const items = [
    { id: 1, name: 'Item 1', completed: false },
    { id: 2, name: 'Item 2', completed: true },
    { id: 3, name: 'Item 3', completed: false }
  ]

  test('renders list items correctly', () => {
    render(<List items={items} />)

    items.forEach(item => {
      expect(screen.getByText(item.name)).toBeInTheDocument()
    })
  })

  test('applies correct styling to completed items', () => {
    render(<List items={items} />)

    const completedItem = screen.getByText('Item 2')
    expect(completedItem).toHaveClass('completed')
  })

  test('filters items correctly', () => {
    const { rerender } = render(<List items={items} filter="completed" />)

    expect(screen.queryByText('Item 1')).not.toBeInTheDocument()
    expect(screen.getByText('Item 2')).toBeInTheDocument()
    expect(screen.queryByText('Item 3')).not.toBeInTheDocument()

    rerender(<List items={items} filter="active" />)

    expect(screen.getByText('Item 1')).toBeInTheDocument()
    expect(screen.queryByText('Item 2')).not.toBeInTheDocument()
    expect(screen.getByText('Item 3')).toBeInTheDocument()
  })

  test('handles item interactions', async () => {
    const onToggleComplete = jest.fn()
    const onDeleteItem = jest.fn()
    const user = userEvent.setup()

    render(
      <List
        items={items}
        onToggleComplete={onToggleComplete}
        onDeleteItem={onDeleteItem}
      />
    )

    // Toggle completion
    const firstItemCheckbox = screen.getAllByRole('checkbox')[0]
    await user.click(firstItemCheckbox)

    expect(onToggleComplete).toHaveBeenCalledWith(1)

    // Delete item
    const deleteButton = screen.getAllByRole('button', { name: /delete/i })[0]
    await user.click(deleteButton)

    expect(onDeleteItem).toHaveBeenCalledWith(1)
  })
})

// File: Modal.test.js
import { render, screen, fireEvent } from '@testing-library/react'
import Modal from '../Modal'

describe('Modal Component', () => {
  test('renders modal when open', () => {
    render(
      <Modal isOpen={true} onClose={jest.fn()}>
        <h1>Modal Content</h1>
        <p>This is a modal</p>
      </Modal>
    )

    expect(screen.getByRole('dialog')).toBeInTheDocument()
    expect(screen.getByText('Modal Content')).toBeInTheDocument()
    expect(screen.getByText('This is a modal')).toBeInTheDocument()
  })

  test('does not render when closed', () => {
    render(
      <Modal isOpen={false} onClose={jest.fn()}>
        <h1>Modal Content</h1>
      </Modal>
    )

    expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
  })

  test('calls onClose when close button is clicked', async () => {
    const onClose = jest.fn()
    const user = userEvent.setup()

    render(
      <Modal isOpen={true} onClose={onClose}>
        <h1>Modal Content</h1>
      </Modal>
    )

    const closeButton = screen.getByRole('button', { name: /close/i })
    await user.click(closeButton)

    expect(onClose).toHaveBeenCalledTimes(1)
  })

  test('calls onClose when overlay is clicked', async () => {
    const onClose = jest.fn()
    const user = userEvent.setup()

    render(
      <Modal isOpen={true} onClose={onClose}>
        <h1>Modal Content</h1>
      </Modal>
    )

    const overlay = screen.getByTestId('modal-overlay')
    await user.click(overlay)

    expect(onClose).toHaveBeenCalledTimes(1)
  })

  test('focuses on first focusable element when opened', () => {
    render(
      <Modal isOpen={true} onClose={jest.fn()}>
        <button>First Button</button>
        <button>Second Button</button>
      </Modal>
    )

    const firstButton = screen.getByRole('button', { name: /first button/i })
    expect(firstButton).toHaveFocus()
  })

  test('traps focus within modal', async () => {
    const user = userEvent.setup()
    render(
      <Modal isOpen={true} onClose={jest.fn()}>
        <button>First Button</button>
        <button>Second Button</button>
        <button>Third Button</button>
      </Modal>
    )

    const buttons = screen.getAllByRole('button')

    // Tab through buttons
    await user.tab()
    expect(buttons[1]).toHaveFocus()

    await user.tab()
    expect(buttons[2]).toHaveFocus()

    // Should cycle back to first element
    await user.tab()
    expect(buttons[0]).toHaveFocus()
  })
})

💻 Bases Vue Testing Library text

🟢 simple ⭐⭐

Tests de composants Vue essentiels avec Vue 3 Composition API, les interactions de composants et les utilitaires de test

⏱️ 20 min 🏷️ testing, vue, component testing
Prerequisites: Vue 3, JavaScript, Testing basics, Composition API
// Vue Testing Library Basics
// File: Button.spec.js

import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'
import Button from '../Button.vue'

describe('Button Component', () => {
  it('renders button with text', () => {
    const wrapper = mount(Button, {
      props: {
        text: 'Click me'
      }
    })

    expect(wrapper.text()).toContain('Click me')
    expect(wrapper.find('button').exists()).toBe(true)
  })

  it('applies correct CSS classes', () => {
    const wrapper = mount(Button, {
      props: {
        variant: 'primary',
        size: 'large'
      }
    })

    const button = wrapper.find('button')
    expect(button.classes()).toContain('btn-primary')
    expect(button.classes()).toContain('btn-large')
  })

  it('emits click event', async () => {
    const wrapper = mount(Button, {
      props: {
        text: 'Click me'
      }
    })

    await wrapper.find('button').trigger('click')

    expect(wrapper.emitted()).toHaveProperty('click')
    expect(wrapper.emitted('click')).toHaveLength(1)
  })

  it('is disabled when disabled prop is true', () => {
    const wrapper = mount(Button, {
      props: {
        text: 'Disabled Button',
        disabled: true
      }
    })

    const button = wrapper.find('button')
    expect(button.attributes('disabled')).toBeDefined()
    expect(button.element.disabled).toBe(true)
  })

  it('shows loading state', () => {
    const wrapper = mount(Button, {
      props: {
        text: 'Loading',
        loading: true
      }
    })

    const button = wrapper.find('button')
    expect(button.find('[data-testid="spinner"]').exists()).toBe(true)
    expect(button.attributes('disabled')).toBeDefined()
  })

  it('renders slot content when provided', () => {
    const wrapper = mount(Button, {
      slots: {
        default: '<span class="custom-content">Custom Content</span>'
      }
    })

    expect(wrapper.find('.custom-content').exists()).toBe(true)
    expect(wrapper.text()).toContain('Custom Content')
  })

  it('handles keyboard events', async () => {
    const wrapper = mount(Button, {
      props: {
        text: 'Keyboard Button'
      }
    })

    await wrapper.find('button').trigger('keydown.enter')

    expect(wrapper.emitted()).toHaveProperty('click')
  })
})

// File: TodoList.spec.js

import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach } from 'vitest'
import { nextTick } from 'vue'
import TodoList from '../TodoList.vue'

describe('TodoList Component', () => {
  let wrapper

  beforeEach(() => {
    wrapper = mount(TodoList, {
      props: {
        todos: [
          { id: 1, text: 'Learn Vue', completed: false },
          { id: 2, text: 'Build App', completed: true },
          { id: 3, text: 'Deploy App', completed: false }
        ]
      }
    })
  })

  it('renders all todos', () => {
    const todoItems = wrapper.findAll('[data-testid="todo-item"]')
    expect(todoItems).toHaveLength(3)
    expect(wrapper.text()).toContain('Learn Vue')
    expect(wrapper.text()).toContain('Build App')
    expect(wrapper.text()).toContain('Deploy App')
  })

  it('marks todo as completed', async () => {
    const firstTodo = wrapper.find('[data-testid="todo-item"]')
    const checkbox = firstTodo.find('input[type="checkbox"]')

    await checkbox.setChecked(true)
    await nextTick()

    expect(firstTodo.classes()).toContain('completed')
    expect(wrapper.emitted()).toHaveProperty('toggle-completed')
  })

  it('filters todos correctly', async () => {
    // Test active filter
    await wrapper.setData({ filter: 'active' })
    await nextTick()

    const activeTodos = wrapper.findAll('[data-testid="todo-item"]:not(.completed)')
    expect(activeTodos).toHaveLength(2)

    // Test completed filter
    await wrapper.setData({ filter: 'completed' })
    await nextTick()

    const completedTodos = wrapper.findAll('[data-testid="todo-item"].completed')
    expect(completedTodos).toHaveLength(1)
  })

  it('adds new todo', async () => {
    const newTodoInput = wrapper.find('[data-testid="new-todo-input"]')
    const addButton = wrapper.find('[data-testid="add-todo-button"]')

    await newTodoInput.setValue('New Todo Item')
    await addButton.trigger('click')
    await nextTick()

    expect(wrapper.emitted()).toHaveProperty('add-todo')
    expect(wrapper.emitted('add-todo')[0]).toEqual([{ text: 'New Todo Item' }])
  })

  it('deletes todo', async () => {
    const firstTodo = wrapper.find('[data-testid="todo-item"]')
    const deleteButton = firstTodo.find('[data-testid="delete-button"]')

    await deleteButton.trigger('click')
    await nextTick()

    expect(wrapper.emitted()).toHaveProperty('delete-todo')
    expect(wrapper.emitted('delete-todo')[0]).toEqual([1])
  })
})

// File: Form.spec.js

import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'
import { nextTick } from 'vue'
import ContactForm from '../ContactForm.vue'

describe('ContactForm Component', () => {
  it('renders form fields correctly', () => {
    const wrapper = mount(ContactForm)

    expect(wrapper.find('[name="name"]').exists()).toBe(true)
    expect(wrapper.find('[name="email"]').exists()).toBe(true)
    expect(wrapper.find('[name="message"]').exists()).toBe(true)
    expect(wrapper.find('button[type="submit"]').exists()).toBe(true)
  })

  it('validates required fields', async () => {
    const wrapper = mount(ContactForm)
    const submitButton = wrapper.find('button[type="submit"]')

    await submitButton.trigger('click')
    await nextTick()

    expect(wrapper.text()).toContain('Name is required')
    expect(wrapper.text()).toContain('Email is required')
    expect(wrapper.text()).toContain('Message is required')
  })

  it('validates email format', async () => {
    const wrapper = mount(ContactForm)

    await wrapper.find('[name="email"]').setValue('invalid-email')
    await wrapper.find('button[type="submit"]').trigger('click')
    await nextTick()

    expect(wrapper.text()).toContain('Please enter a valid email')
  })

  it('submits form with valid data', async () => {
    const wrapper = mount(ContactForm)

    await wrapper.find('[name="name"]').setValue('John Doe')
    await wrapper.find('[name="email"]').setValue('[email protected]')
    await wrapper.find('[name="message"]').setValue('This is a test message')
    await wrapper.find('button[type="submit"]').trigger('click')
    await nextTick()

    expect(wrapper.emitted()).toHaveProperty('submit')
    expect(wrapper.emitted('submit')[0]).toEqual({
      name: 'John Doe',
      email: '[email protected]',
      message: 'This is a test message'
    })
  })

  it('shows loading state during submission', async () => {
    const wrapper = mount(ContactForm)

    // Mock async submission
    const submitSpy = vi.fn(() => Promise.resolve())
    wrapper.vm.submit = submitSpy

    await wrapper.find('[name="name"]').setValue('John Doe')
    await wrapper.find('[name="email"]').setValue('[email protected]')
    await wrapper.find('[name="message"]').setValue('Test message')
    await wrapper.find('button[type="submit"]').trigger('click')

    expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBeDefined()
    expect(wrapper.text()).toContain('Submitting...')
  })
})

// File: Counter.spec.js

import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach } from 'vitest'
import Counter from '../Counter.vue'

describe('Counter Component', () => {
  let wrapper

  beforeEach(() => {
    wrapper = mount(Counter, {
      props: {
        initialValue: 5
      }
    })
  })

  it('renders initial value', () => {
    expect(wrapper.text()).toContain('Count: 5')
  })

  it('increments count when increment button is clicked', async () => {
    const incrementButton = wrapper.find('[data-testid="increment"]')

    await incrementButton.trigger('click')
    await wrapper.vm.$nextTick()

    expect(wrapper.text()).toContain('Count: 6')
  })

  it('decrements count when decrement button is clicked', async () => {
    const decrementButton = wrapper.find('[data-testid="decrement"]')

    await decrementButton.trigger('click')
    await wrapper.vm.$nextTick()

    expect(wrapper.text()).toContain('Count: 4')
  })

  it('resets count when reset button is clicked', async () => {
    const incrementButton = wrapper.find('[data-testid="increment"]')
    const resetButton = wrapper.find('[data-testid="reset"]')

    await incrementButton.trigger('click')
    await incrementButton.trigger('click')
    await wrapper.vm.$nextTick()

    expect(wrapper.text()).toContain('Count: 7')

    await resetButton.trigger('click')
    await wrapper.vm.$nextTick()

    expect(wrapper.text()).toContain('Count: 5')
  })

  it('does not go below minimum value', async () => {
    wrapper = mount(Counter, {
      props: {
        initialValue: 0,
        min: 0
      }
    })

    const decrementButton = wrapper.find('[data-testid="decrement"]')

    await decrementButton.trigger('click')
    await wrapper.vm.$nextTick()

    expect(wrapper.text()).toContain('Count: 0')
    expect(wrapper.find('[data-testid="decrement"]').attributes('disabled')).toBeDefined()
  })

  it('does not go above maximum value', async () => {
    wrapper = mount(Counter, {
      props: {
        initialValue: 10,
        max: 10
      }
    })

    const incrementButton = wrapper.find('[data-testid="increment"]')

    await incrementButton.trigger('click')
    await wrapper.vm.$nextTick()

    expect(wrapper.text()).toContain('Count: 10')
    expect(wrapper.find('[data-testid="increment"]').attributes('disabled')).toBeDefined()
  })

  it('emits change event when count changes', async () => {
    const incrementButton = wrapper.find('[data-testid="increment"]')

    await incrementButton.trigger('click')
    await wrapper.vm.$nextTick()

    expect(wrapper.emitted()).toHaveProperty('change')
    expect(wrapper.emitted('change')[0]).toEqual(6)
  })
})

// File: Modal.spec.js

import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Modal from '../Modal.vue'

describe('Modal Component', () => {
  it('renders when open prop is true', () => {
    const wrapper = mount(Modal, {
      props: {
        open: true,
        title: 'Test Modal'
      },
      slots: {
        default: '<p>Modal content</p>'
      }
    })

    expect(wrapper.find('[data-testid="modal"]').exists()).toBe(true)
    expect(wrapper.text()).toContain('Test Modal')
    expect(wrapper.text()).toContain('Modal content')
  })

  it('does not render when open prop is false', () => {
    const wrapper = mount(Modal, {
      props: {
        open: false
      }
    })

    expect(wrapper.find('[data-testid="modal"]').exists()).toBe(false)
  })

  it('emits close event when close button is clicked', async () => {
    const wrapper = mount(Modal, {
      props: {
        open: true
      }
    })

    const closeButton = wrapper.find('[data-testid="close-button"]')
    await closeButton.trigger('click')

    expect(wrapper.emitted()).toHaveProperty('close')
  })

  it('emits close event when overlay is clicked', async () => {
    const wrapper = mount(Modal, {
      props: {
        open: true
      }
    })

    const overlay = wrapper.find('[data-testid="modal-overlay"]')
    await overlay.trigger('click')

    expect(wrapper.emitted()).toHaveProperty('close')
  })

  it('prevents closing when preventClose is true', async () => {
    const wrapper = mount(Modal, {
      props: {
        open: true,
        preventClose: true
      }
    })

    const overlay = wrapper.find('[data-testid="modal-overlay"]')
    await overlay.trigger('click')

    expect(wrapper.emitted('close')).toBeUndefined()
  })

  it('applies custom size class', () => {
    const wrapper = mount(Modal, {
      props: {
        open: true,
        size: 'large'
      }
    })

    const modal = wrapper.find('[data-testid="modal"]')
    expect(modal.classes()).toContain('modal-large')
  })
})

💻 Patterns Avancés de Testing React text

🔴 complex ⭐⭐⭐⭐

Scénarios de test complexes incluant le mocking d'API, les tests de hooks personnalisés, les tests d'intégration et les tests de performance

⏱️ 35 min 🏷️ testing, react, advanced, integration
Prerequisites: React Testing Library, MSW, Jest, Async/await, Performance concepts
// Advanced React Testing Patterns
// File: APITest.test.js

import { render, screen, waitFor, act } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { rest } from 'msw'
import { setupServer } from 'msw/node'
import UserProfile from '../UserProfile'
import '@testing-library/jest-dom'

// Mock API server setup
const server = setupServer(
  rest.get('/api/users/:id', (req, res, ctx) => {
    const { id } = req.params

    if (id === '1') {
      return res(
        ctx.json({
          id: 1,
          name: 'John Doe',
          email: '[email protected]',
          avatar: 'https://example.com/avatar1.jpg',
          bio: 'Software Developer'
        })
      )
    }

    if (id === '999') {
      return res(
        ctx.status(404),
        ctx.json({ error: 'User not found' })
      )
    }

    return res(ctx.status(500), ctx.json({ error: 'Server error' }))
  }),

  rest.put('/api/users/:id', (req, res, ctx) => {
    return res(
      ctx.json({
        ...req.body,
        updatedAt: new Date().toISOString()
      })
    )
  })
)

// Setup and cleanup
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

describe('UserProfile API Integration', () => {
  test('loads and displays user data', async () => {
    render(<UserProfile userId="1" />)

    // Should show loading state initially
    expect(screen.getByText(/loading/i)).toBeInTheDocument()

    // Wait for data to load
    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument()
      expect(screen.getByText('[email protected]')).toBeInTheDocument()
      expect(screen.getByText('Software Developer')).toBeInTheDocument()
    })

    // Should not show loading state anymore
    expect(screen.queryByText(/loading/i)).not.toBeInTheDocument()
  })

  test('handles user not found error', async () => {
    render(<UserProfile userId="999" />)

    await waitFor(() => {
      expect(screen.getByText(/user not found/i)).toBeInTheDocument()
    })
  })

  test('handles server error gracefully', async () => {
    render(<UserProfile userId="500" />)

    await waitFor(() => {
      expect(screen.getByText(/something went wrong/i)).toBeInTheDocument()
      expect(screen.getByRole('button', { name: /retry/i })).toBeInTheDocument()
    })
  })

  test('allows retrying failed requests', async () => {
    server.use(
      rest.get('/api/users/:id', (req, res, ctx) => {
        return res(ctx.status(500), ctx.json({ error: 'Server error' }))
      })
    )

    render(<UserProfile userId="1" />)

    // Wait for error state
    await waitFor(() => {
      expect(screen.getByText(/something went wrong/i)).toBeInTheDocument()
    })

    // Reset server to respond successfully
    server.resetHandlers()

    // Click retry button
    const retryButton = screen.getByRole('button', { name: /retry/i })
    await userEvent.click(retryButton)

    // Should load successfully now
    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument()
    })
  })
})

// File: CustomHook.test.js
import { renderHook, act, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import useUser from '../hooks/useUser'
import { rest } from 'msw'
import { setupServer } from 'msw/node'

const server = setupServer(
  rest.get('/api/users/:id', (req, res, ctx) => {
    return res(
      ctx.json({
        id: req.params.id,
        name: 'Test User',
        email: '[email protected]'
      })
    )
  })
)

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

function createWrapper(queryClient) {
  return ({ children }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

describe('useUser Hook', () => {
  test('loads user data successfully', async () => {
    const queryClient = new QueryClient({
      defaultOptions: {
        queries: { retry: false },
        mutations: { retry: false }
      }
    })

    const { result } = renderHook(() => useUser('1'), {
      wrapper: createWrapper(queryClient)
    })

    expect(result.current.isLoading).toBe(true)
    expect(result.current.user).toBeNull()
    expect(result.current.error).toBeNull()

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false)
      expect(result.current.user).toEqual({
        id: '1',
        name: 'Test User',
        email: '[email protected]'
      })
      expect(result.current.error).toBeNull()
    })
  })

  test('handles loading state', () => {
    const queryClient = new QueryClient({
      defaultOptions: {
        queries: { retry: false },
        mutations: { retry: false }
      }
    })

    const { result } = renderHook(() => useUser('1'), {
      wrapper: createWrapper(queryClient)
    })

    expect(result.current.isLoading).toBe(true)
    expect(result.current.user).toBeNull()
  })

  test('handles error state', async () => {
    server.use(
      rest.get('/api/users/:id', (req, res, ctx) => {
        return res(ctx.status(404), ctx.json({ error: 'Not found' }))
      })
    )

    const queryClient = new QueryClient({
      defaultOptions: {
        queries: { retry: false },
        mutations: { retry: false }
      }
    })

    const { result } = renderHook(() => useUser('999'), {
      wrapper: createWrapper(queryClient)
    })

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false)
      expect(result.current.user).toBeNull()
      expect(result.current.error).toBeTruthy()
    })
  })

  test('refetches data', async () => {
    const queryClient = new QueryClient({
      defaultOptions: {
        queries: { retry: false },
        mutations: { retry: false }
      }
    })

    const { result } = renderHook(() => useUser('1'), {
      wrapper: createWrapper(queryClient)
    })

    // Wait for initial load
    await waitFor(() => {
      expect(result.current.user).toBeTruthy()
    })

    const initialUser = result.current.user

    // Mock different data for refetch
    server.use(
      rest.get('/api/users/:id', (req, res, ctx) => {
        return res(
          ctx.json({
            id: req.params.id,
            name: 'Updated User',
            email: '[email protected]'
          })
        )
      })
    )

    // Refetch data
    await act(async () => {
      await result.current.refetch()
    })

    await waitFor(() => {
      expect(result.current.user).toEqual({
        id: '1',
        name: 'Updated User',
        email: '[email protected]'
      })
    })

    expect(result.current.user).not.toEqual(initialUser)
  })
})

// File: Integration.test.js
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { rest } from 'msw'
import { setupServer } from 'msw/node'
import App from '../App'
import '@testing-library/jest-dom'

const server = setupServer(
  rest.get('/api/posts', (req, res, ctx) => {
    return res(
      ctx.json([
        { id: 1, title: 'First Post', content: 'First content' },
        { id: 2, title: 'Second Post', content: 'Second content' }
      ])
    )
  }),

  rest.post('/api/posts', (req, res, ctx) => {
    return res(
      ctx.json({
        id: 3,
        ...req.body,
        createdAt: new Date().toISOString()
      })
    )
  })
)

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

describe('App Integration Tests', () => {
  test('renders full application flow', async () => {
    render(<App />)

    // Should show initial loading
    expect(screen.getByText(/loading posts/i)).toBeInTheDocument()

    // Should render posts after loading
    await waitFor(() => {
      expect(screen.getByText('First Post')).toBeInTheDocument()
      expect(screen.getByText('Second Post')).toBeInTheDocument()
    })
  })

  test('creates new post successfully', async () => {
    const user = userEvent.setup()
    render(<App />)

    // Wait for posts to load
    await waitFor(() => {
      expect(screen.getByText('First Post')).toBeInTheDocument()
    })

    // Open new post form
    const newPostButton = screen.getByRole('button', { name: /new post/i })
    await user.click(newPostButton)

    // Fill out form
    await user.type(screen.getByLabelText(/title/i), 'New Test Post')
    await user.type(screen.getByLabelText(/content/i), 'New test content')

    // Submit form
    await user.click(screen.getByRole('button', { name: /create post/i }))

    // Should show new post in list
    await waitFor(() => {
      expect(screen.getByText('New Test Post')).toBeInTheDocument()
      expect(screen.getByText('New test content')).toBeInTheDocument()
    })
  })

  test('handles API errors gracefully', async () => {
    server.use(
      rest.get('/api/posts', (req, res, ctx) => {
        return res(ctx.status(500), ctx.json({ error: 'Server error' }))
      })
    )

    render(<App />)

    await waitFor(() => {
      expect(screen.getByText(/failed to load posts/i)).toBeInTheDocument()
      expect(screen.getByRole('button', { name: /retry/i })).toBeInTheDocument()
    })
  })

  test('maintains state during navigation', async () => {
    const user = userEvent.setup()
    render(<App />)

    // Wait for initial load
    await waitFor(() => {
      expect(screen.getByText('First Post')).toBeInTheDocument()
    })

    // Navigate to a post
    const firstPostLink = screen.getByRole('link', { name: /first post/i })
    await user.click(firstPostLink)

    // Should show post details
    await waitFor(() => {
      expect(screen.getByText('First content')).toBeInTheDocument()
    })

    // Navigate back
    const backButton = screen.getByRole('button', { name: /back/i })
    await user.click(backButton)

    // Should show post list again
    await waitFor(() => {
      expect(screen.getByText('First Post')).toBeInTheDocument()
      expect(screen.getByText('Second Post')).toBeInTheDocument()
    })
  })
})

// File: Performance.test.js
import { render, screen } from '@testing-library/react'
import { measure } from 'v8-performance'
import LargeList from '../LargeList'

describe('Performance Tests', () => {
  // Generate large dataset for performance testing
  const generateLargeDataset = (size) => {
    return Array.from({ length: size }, (_, index) => ({
      id: index + 1,
      name: `Item ${index + 1}`,
      description: `Description for item ${index + 1}`,
      category: `Category ${(index % 10) + 1}`
    }))
  }

  test('renders large list efficiently', () => {
    const largeData = generateLargeDataset(1000)

    // Measure render time
    const startTime = performance.now()

    render(<LargeList items={largeData} />)

    const endTime = performance.now()
    const renderTime = endTime - startTime

    // Render should complete within reasonable time
    expect(renderTime).toBeLessThan(1000) // 1 second

    // Should render initial items
    expect(screen.getAllByRole('listitem').length).toBeGreaterThan(0)
  })

  test('handles filtering efficiently', () => {
    const largeData = generateLargeDataset(1000)
    const { rerender } = render(<LargeList items={largeData} />)

    const initialItems = screen.getAllByRole('listitem')
    expect(initialItems.length).toBeLessThan(1000) // Due to virtualization

    // Filter to specific category
    rerender(<LargeList items={largeData} filter="Category 1" />)

    const filteredItems = screen.getAllByRole('listitem')

    // Should update filter quickly
    const filterStartTime = performance.now()

    // Trigger filter logic (this would be handled by the component)
    fireEvent.change(screen.getByRole('textbox', { name: /filter/i }), {
      target: { value: 'Category 1' }
    })

    const filterEndTime = performance.now()
    const filterTime = filterEndTime - filterStartTime

    expect(filterTime).toBeLessThan(100) // Filter should be very fast
  })

  test('memory usage remains reasonable', () => {
    const largeData = generateLargeDataset(5000)

    const initialMemory = performance.memory?.usedJSHeapSize || 0

    const { unmount } = render(<LargeList items={largeData} />)

    const afterRenderMemory = performance.memory?.usedJSHeapSize || 0
    const memoryIncrease = afterRenderMemory - initialMemory

    // Memory increase should be reasonable (less than 50MB)
    expect(memoryIncrease).toBeLessThan(50 * 1024 * 1024)

    // Cleanup and check memory release
    unmount()

    // Force garbage collection if available
    if (global.gc) {
      global.gc()
    }
  })

  test('handles frequent updates efficiently', async () => {
    const initialData = generateLargeDataset(100)

    render(<LargeList items={initialData} />)

    const startTime = performance.now()

    // Simulate frequent updates
    for (let i = 0; i < 10; i++) {
      const newData = generateLargeDataset(100 + i * 10)

      // This would be handled by parent component updating props
      // For testing, we simulate the update time
      const updateStart = performance.now()

      // Trigger re-render (in real scenario, this would be props update)
      fireEvent.click(screen.getByRole('button', { name: /refresh/i }))

      const updateEnd = performance.now()
      const updateTime = updateEnd - updateStart

      // Each update should be fast
      expect(updateTime).toBeLessThan(100)
    }

    const totalTime = performance.now() - startTime

    // Total time for all updates should be reasonable
    expect(totalTime).toBeLessThan(2000) // 2 seconds
  })
})

💻 Patterns Avancés de Testing Vue text

🔴 complex ⭐⭐⭐⭐

Scénarios de test Vue complexes incluant les tests de store Pinia, les tests de router, l'intégration d'API et les patterns de communication de composants

⏱️ 35 min 🏷️ testing, vue, advanced, integration
Prerequisites: Vue Testing Library, Pinia, Vue Router, Async/await, Component communication
// Advanced Vue Testing Patterns
// File: StoreIntegration.spec.js

import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { nextTick } from 'vue'
import UserProfile from '../UserProfile.vue'
import { useUserStore } from '../stores/userStore'

describe('Store Integration Tests', () => {
  let wrapper
  let pinia

  beforeEach(() => {
    pinia = createPinia()
    setActivePinia(pinia)
  })

  it('loads and displays user data from store', async () => {
    // Mock store data
    const userStore = useUserStore()
    userStore.user = {
      id: 1,
      name: 'John Doe',
      email: '[email protected]',
      avatar: 'avatar.jpg'
    }
    userStore.loading = false

    wrapper = mount(UserProfile, {
      global: {
        plugins: [pinia]
      }
    })

    expect(wrapper.text()).toContain('John Doe')
    expect(wrapper.text()).toContain('[email protected]')
  })

  it('shows loading state when store is loading', () => {
    const userStore = useUserStore()
    userStore.loading = true
    userStore.user = null

    wrapper = mount(UserProfile, {
      global: {
        plugins: [pinia]
      }
    })

    expect(wrapper.text()).toContain('Loading...')
  })

  it('dispatches store actions on user actions', async () => {
    const userStore = useUserStore()
    userStore.user = {
      id: 1,
      name: 'John Doe',
      email: '[email protected]'
    }
    userStore.loading = false

    // Mock store action
    const updateProfileSpy = vi.spyOn(userStore, 'updateProfile')

    wrapper = mount(UserProfile, {
      global: {
        plugins: [pinia]
      }
    })

    // Find and trigger edit mode
    const editButton = wrapper.find('[data-testid="edit-profile"]')
    await editButton.trigger('click')

    await nextTick()

    // Update form fields
    const nameInput = wrapper.find('[data-testid="name-input"]')
    await nameInput.setValue('Jane Doe')

    const saveButton = wrapper.find('[data-testid="save-profile"]')
    await saveButton.trigger('click')

    await nextTick()

    expect(updateProfileSpy).toHaveBeenCalledWith({
      name: 'Jane Doe'
    })
  })

  it('handles store errors correctly', async () => {
    const userStore = useUserStore()
    userStore.loading = false
    userStore.error = 'Failed to load user'

    wrapper = mount(UserProfile, {
      global: {
        plugins: [pinia]
      }
    })

    expect(wrapper.text()).toContain('Failed to load user')
    expect(wrapper.find('[data-testid="error-message"]').exists()).toBe(true)
  })
})

// File: RouterIntegration.spec.js

import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'
import { createRouter, createWebHistory } from 'vue-router'
import { nextTick } from 'vue'
import App from '../App.vue'

// Mock components
const Home = { template: '<div>Home Page</div>' }
const About = { template: '<div>About Page</div>' }
const UserProfile = { template: '<div>User Profile: {{ $route.params.id }}</div>' }

describe('Router Integration Tests', () => {
  let router
  let wrapper

  beforeEach(async () => {
    router = createRouter({
      history: createWebHistory(),
      routes: [
        { path: '/', component: Home },
        { path: '/about', component: About },
        { path: '/user/:id', component: UserProfile, name: 'user-profile' }
      ]
    })

    wrapper = mount(App, {
      global: {
        plugins: [router]
      }
    })

    await router.isReady()
  })

  it('renders home page by default', () => {
    expect(wrapper.text()).toContain('Home Page')
  })

  it('navigates to about page', async () => {
    const aboutLink = wrapper.find('[data-testid="about-link"]')
    await aboutLink.trigger('click')

    await nextTick()
    await new Promise(resolve => setTimeout(resolve, 0))

    expect(wrapper.text()).toContain('About Page')
  })

  it('navigates to user profile with dynamic route', async () => {
    await router.push('/user/123')
    await nextTick()

    expect(wrapper.text()).toContain('User Profile: 123')
  })

  it('updates route params correctly', async () => {
    await router.push('/user/123')
    await nextTick()
    expect(wrapper.text()).toContain('User Profile: 123')

    await router.push('/user/456')
    await nextTick()
    expect(wrapper.text()).toContain('User Profile: 456')
  })

  it('handles navigation guards', async () => {
    // Add navigation guard
    router.beforeEach((to, from, next) => {
      if (to.path === '/about') {
        next('/login')
      } else {
        next()
      }
    })

    const aboutLink = wrapper.find('[data-testid="about-link"]')
    await aboutLink.trigger('click')

    await nextTick()
    await new Promise(resolve => setTimeout(resolve, 0))

    expect(wrapper.vm.$route.path).toBe('/login')
  })

  it('passes route params as props', async () => {
    const TestComponent = {
      template: '<div>User ID: {{ userId }}</div>',
      props: ['userId']
    }

    router.addRoute({
      path: '/profile/:userId',
      component: TestComponent,
      props: true
    })

    await router.push('/profile/789')
    await nextTick()

    expect(wrapper.text()).toContain('User ID: 789')
  })
})

// File: ComponentCommunication.spec.js

import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'
import { nextTick } from 'vue'
import ParentComponent from '../ParentComponent.vue'
import ChildComponent from '../ChildComponent.vue'

describe('Component Communication Tests', () => {
  it('passes data from parent to child via props', () => {
    const wrapper = mount(ParentComponent, {
      props: {
        message: 'Hello from parent'
      }
    })

    const childComponent = wrapper.findComponent(ChildComponent)
    expect(childComponent.props('message')).toBe('Hello from parent')
    expect(wrapper.text()).toContain('Child says: Hello from parent')
  })

  it('emits events from child to parent', async () => {
    const wrapper = mount(ParentComponent)
    const childComponent = wrapper.findComponent(ChildComponent)

    await childComponent.vm.$emit('child-event', 'Hello from child')

    expect(wrapper.emitted('child-event')).toBeTruthy()
    expect(wrapper.emitted('child-event')[0]).toEqual(['Hello from child'])
  })

  it('updates parent state based on child events', async () => {
    const wrapper = mount(ParentComponent)
    const childComponent = wrapper.findComponent(ChildComponent)

    // Child component emits an update event
    await childComponent.vm.$emit('update-message', 'Updated message')

    await nextTick()

    // Parent should reflect the updated message
    expect(wrapper.vm.message).toBe('Updated message')
    expect(wrapper.text()).toContain('Child says: Updated message')
  })

  it('communicates via provide/inject', () => {
    const ParentComponent = {
      template: `
        <div>
          <ChildComponent />
        </div>
      `,
      provide: {
        sharedData: 'Shared from parent',
        sharedMethod: vi.fn()
      }
    }

    const ChildComponent = {
      template: `
        <div>{{ injectedData }}</div>
      `,
      inject: ['sharedData', 'sharedMethod']
    }

    const wrapper = mount(ParentComponent, {
      global: {
        components: { ChildComponent }
      }
    })

    const childComponent = wrapper.findComponent(ChildComponent)
    expect(childComponent.vm.sharedData).toBe('Shared from parent')
    expect(childComponent.vm.sharedMethod).toBeDefined()
  })

  it('communicates via event bus', async () => {
    const eventBus = mitt()

    const EmitterComponent = {
      template: '<button @click="emitEvent">Emit Event</button>',
      methods: {
        emitEvent() {
          eventBus.emit('test-event', 'Hello from emitter')
        }
      }
    }

    const ListenerComponent = {
      template: '<div>{{ receivedMessage }}</div>',
      data() {
        return {
          receivedMessage: ''
        }
      },
      mounted() {
        eventBus.on('test-event', (message) => {
          this.receivedMessage = message
        })
      }
    }

    const wrapper = mount({
      template: `
        <div>
          <EmitterComponent ref="emitter" />
          <ListenerComponent ref="listener" />
        </div>
      `,
      components: {
        EmitterComponent,
        ListenerComponent
      }
    })

    const emitter = wrapper.findComponent({ name: 'EmitterComponent' })
    await emitter.find('button').trigger('click')

    await nextTick()

    const listener = wrapper.findComponent({ name: 'ListenerComponent' })
    expect(listener.vm.receivedMessage).toBe('Hello from emitter')
  })
})

// File: AsyncComponent.spec.js

import { mount } from '@vue/test-utils'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { nextTick } from 'vue'
import AsyncDataLoader from '../AsyncDataLoader.vue'

describe('Async Component Tests', () => {
  let wrapper

  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('shows loading state initially', () => {
    wrapper = mount(AsyncDataLoader)
    expect(wrapper.text()).toContain('Loading...')
  })

  it('loads and displays data on mount', async () => {
    // Mock API call
    const mockFetch = vi.fn(() =>
      Promise.resolve({
        json: () => Promise.resolve({ data: ['item1', 'item2', 'item3'] })
      })
    )
    global.fetch = mockFetch

    wrapper = mount(AsyncDataLoader)

    // Wait for async operation
    await nextTick()
    await new Promise(resolve => setTimeout(resolve, 100))

    expect(mockFetch).toHaveBeenCalledTimes(1)
    expect(wrapper.text()).toContain('item1')
    expect(wrapper.text()).toContain('item2')
    expect(wrapper.text()).toContain('item3')
  })

  it('handles API errors gracefully', async () => {
    // Mock failed API call
    const mockFetch = vi.fn(() =>
      Promise.reject(new Error('Network error'))
    )
    global.fetch = mockFetch

    wrapper = mount(AsyncDataLoader)

    await nextTick()
    await new Promise(resolve => setTimeout(resolve, 100))

    expect(wrapper.text()).toContain('Failed to load data')
  })

  it('can refresh data', async () => {
    let callCount = 0
    const mockFetch = vi.fn(() => {
      callCount++
      return Promise.resolve({
        json: () => Promise.resolve({ data: [`item${callCount}`] })
      })
    })
    global.fetch = mockFetch

    wrapper = mount(AsyncDataLoader)

    // Initial load
    await nextTick()
    await new Promise(resolve => setTimeout(resolve, 100))
    expect(wrapper.text()).toContain('item1')

    // Trigger refresh
    const refreshButton = wrapper.find('[data-testid="refresh-button"]')
    await refreshButton.trigger('click')

    await nextTick()
    await new Promise(resolve => setTimeout(resolve, 100))

    expect(callCount).toBe(2)
    expect(wrapper.text()).toContain('item2')
  })

  it('debounces rapid requests', async () => {
    const mockFetch = vi.fn(() =>
      Promise.resolve({
        json: () => Promise.resolve({ data: ['debounced-item'] })
      })
    )
    global.fetch = mockFetch

    wrapper = mount(AsyncDataLoader)

    const searchInput = wrapper.find('[data-testid="search-input"]')

    // Trigger multiple rapid changes
    await searchInput.setValue('search1')
    await searchInput.setValue('search2')
    await searchInput.setValue('search3')

    // Wait for debounce timeout
    await new Promise(resolve => setTimeout(resolve, 300))

    // Should only call API once due to debouncing
    expect(mockFetch).toHaveBeenCalledTimes(1)
  })
})