React: اختبار الوحدات (Vitest + React Testing Library)

آخر تحديث: 2026-08-26

أثناء إعادة هيكلة مكون قديم، قام توم «عن غير قصد» بتغيير منطق الحالة الداخلية، مما تسبب في حدوث مشكلات في واجهة المستخدم في ثلاث صفحات تعتمد على ذلك المكون. ونظرًا لعدم وجود اختبارات وحدة، لم يتم اكتشاف هذه المشكلة إلا خلال اختبار ضمان الجودة، مما أدى إلى إهدار وقت الدورة الكاملة للفريق. قرر توم إدخال Vitest وReact Testing Library إلى المشروع للاستفادة من الاختبارات الآلية لضمان ألا يؤدي أي تغيير إلى تعطيل الوظائف الحالية.


1. ما ستتعلمه



2. المخططات المفاهيمية

يوضح الرسم البياني التالي دور الاختبارات الوحدوية وعلاقاتها في تطوير مكونات React:

100%
flowchart LR
    A[Creating Components] --> B[Writing Tests]
    B --> C{Run Test}

    C -->|Through| D[Submit Code]
    C -->|Failure| E[Positioning Bug]

    E --> F{Error Type}
    F -->|Rendering Issues| G[getByText / getByRole]
    F -->|Interaction Issues| H[userEvent.click]
    F -->|Asynchronous Issues| I[findByText / waitFor]
    F -->|External Dependencies| J[vi.mock / vi.fn]

    G --> A
    H --> A
    I --> A
    J --> A

    style B fill:#e3f2fd,stroke:#1565c0
    style D fill:#e8f5e9,stroke:#2e7d32
    style E fill:#fff3e0,stroke:#e65100


3. سيناريو واقعي

يحتوي فريق توم على مكون UserCard يعرض معلومات المستخدم. ويستقبل هذا المكون كائنًا من نوع user ووظيفة استدعاء مرتدة من نوع onFollow، ويقوم بتغيير نمط الزر بناءً على الحالة isFollowing.

أثناء عملية إعادة هيكلة الكود، قام توم بتغيير القيم الأولية للحالة الداخلية، مما أدى إلى تعيين isFollowing على القيمة الافتراضية true — ونتيجة لذلك، تم ضبط جميع بطاقات المستخدمين بحيث تعرض كلمة «متابعة» بشكل افتراضي. ولم يتم اكتشاف هذا الخطأ إلا خلال اختبار القبول الذي أجراه مدير المنتج.

لو كانت الاختبارات مطبقة في ذلك الوقت، لكان قد تم اكتشاف مشكلة التراجع هذه في غضون ثوانٍ خلال مرحلة npm test. قرر توم إضافة اختبارات وحدة لجميع المكونات الأساسية.


(1) إعداد البيئة — إعداد البنية التحتية للاختبار

أولاً، قم بتثبيت جميع المكونات التبعية:

BASH
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom

تكوين Vitest (إضافة الحقل test إلى vite.config.ts):

TS
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    // Usage jsdom Simulate a browser environment
    environment: 'jsdom',
    // Global Registration describe / test / expect,No manual import required
    globals: true,
    // Configuration files that run before the test starts
    setupFiles: './src/test/setup.ts',
  },
})

إنشاء ملف إعداد:

TS
// src/test/setup.ts
import '@testing-library/jest-dom/vitest'
// This line allows toBeInTheDocument()、toHaveTextContent() Assertions are available

أضف نصوصًا برمجية للاختبار إلى package.json:

JSON
{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest --coverage"
  }
}

(2) ثلاث واجهات برمجة تطبيقات أساسية

فلسفة الاختبار في مكتبة React Testing Library: لا تختبر تفاصيل التنفيذ؛ بل اختبر فقط ما يمكن للمستخدمين رؤيته والتفاعل معه.

واجهة برمجة التطبيقات (API) الغرض الميزات
render(component) عرض المكونات في DOM الافتراضي إرجاع مراجع الحاويات والأساليب المساعدة
screen نقطة الدخول للبحث عن العناصر العالمية توفر ثلاثة أنواع من الطرق: getBy و findBy و queryBy
userEvent محاكاة تصرفات المستخدم أكثر واقعية من fireEvent

المبدأ الأساسي: استخدم getByRole أولاً (الدلالة)، ثم getByText ثانيًا، وأخيرًا getByTestId.

▶ المثال 1: اختبار عرض مكون «Counter» وتفاعليته

أولاً، دعونا نكتب مكون «Counter» بسيطًا:

TSX
// Counter.tsx
import { useState } from 'react'

interface CounterProps {
  initialCount?: number
  step?: number
  label?: string
}

export function Counter({
  initialCount = 0,
  step = 1,
  label = 'Count',
}: CounterProps) {
  const [count, setCount] = useState(initialCount)

  return (
    <div>
      <p>
        {label}:{count}
      </p>
      <button onClick={() => setCount(c => c + step)}>+{step}</button>
      <button onClick={() => setCount(c => c - step)} disabled={count <= 0}>
        -{step}
      </button>
      {count >= 10 && (
        <p role="alert">Maximum Value Reached Alert</p>
      )}
    </div>
  )
}

اختبارات الكتابة:

TSX
// Counter.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Counter } from './Counter'

describe('Counter Components', () => {
  // Test 1:Initial Rendering
  test('Display the initial counter value', () => {
    render(<Counter initialCount={5} />)
    // getByText — Search by text content
    expect(screen.getByText('Count:5')).toBeInTheDocument()
  })

  // Test 2:Click the "Add" button
  test('Click +1 The count increases after the button is pressed', async () => {
    const user = userEvent.setup()
    render(<Counter initialCount={0} step={1} />)

    const incrementBtn = screen.getByRole('button', { name: '+1' })
    await user.click(incrementBtn)

    expect(screen.getByText('Count:1')).toBeInTheDocument()
  })

  // Test 3:The count is 0 "Time Decrease" Button Disabled
  test('The count is 0 "Time Decrease" Button Disabled', () => {
    render(<Counter initialCount={0} />)

    const decrementBtn = screen.getByRole('button', { name: '-1' })
    expect(decrementBtn).toBeDisabled()
  })

  // Test 4:Display a reminder when the threshold is reached
  test('Count reached 10 Display reminders', async () => {
    const user = userEvent.setup()
    render(<Counter initialCount={9} step={1} />)

    // No reminder at the beginning
    expect(screen.queryByRole('alert')).not.toBeInTheDocument()

    // Click to add 1
    await user.click(screen.getByRole('button', { name: '+1' }))

    // Now there's a reminder
    expect(screen.getByRole('alert')).toHaveTextContent('Maximum Value Reached Alert')
  })

  // Test 5:Custom label and  step
  test('Supports customization label and  step', async () => {
    const user = userEvent.setup()
    render(<Counter initialCount={0} step={5} label="Number of steps" />)

    expect(screen.getByText('Number of steps:0')).toBeInTheDocument()

    await user.click(screen.getByRole('button', { name: '+5' }))
    expect(screen.getByText('Number of steps:5')).toBeInTheDocument()
  })
})

يوضح هذا الاختبار أربعة أوضاع:

  1. getByText — البحث عن العناصر التي تحتوي على النص المحدد (الطريقة الأبسط والأكثر مباشرة)
  2. getByRole — البحث حسب دور ARIA؛ يوفر الخيار name مطابقة تامة لنص الزر
  3. queryByRole — يبحث عن عنصر قد لا يكون موجودًا، ويعيد null بدلاً من إثارة استثناء
  4. toBeDisabled() / toHaveTextContent() — تأكيدات دلالية مقدمة من jest-dom

(3) اختبار الدعائم ووظائف الاستدعاء عند وقوع الأحداث

تتلقى المكونات عادةً البيانات ووظائف الاستدعاء المرتد عبر «props». والهدف من الاختبار هو التحقق من أن وظائف الاستدعاء المرتد يتم استدعاؤها بشكل صحيح، وأن المعلمات صحيحة.

▶ المثال 2: اختبار الخصائص والأحداث لمكون TodoItem

TSX
// TodoItem.tsx
interface Todo {
  id: number
  text: string
  completed: boolean
}

interface TodoItemProps {
  todo: Todo
  onToggle: (id: number) => void
  onDelete: (id: number) => void
}

export function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
  return (
    <div
      style={{
        display: 'flex',
        alignItems: 'center',
        gap: 12,
        padding: '8px 12px',
        background: todo.completed ? '#f6ffed' : '#fff',
        borderRadius: 6,
        border: '1px solid #f0f0f0',
      }}
    >
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
        aria-label={`Mark ${todo.text}`}
      />
      <span
        style={{
          flex: 1,
          textDecoration: todo.completed ? 'line-through' : 'none',
          color: todo.completed ? '#999' : '#333',
        }}
      >
        {todo.text}
      </span>
      <button
        onClick={() => onDelete(todo.id)}
        aria-label={`Delete ${todo.text}`}
        style={{
          border: 'none',
          background: '#ff4d4f',
          color: '#fff',
          borderRadius: 4,
          padding: '2px 8px',
          cursor: 'pointer',
          fontSize: 12,
        }}
      >
        Delete
      </button>
    </div>
  )
}
TSX
// TodoItem.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { TodoItem } from './TodoItem'

describe('TodoItem Components', () => {
  const mockTodo = {
    id: 42,
    text: 'Study React Test',
    completed: false,
  }

  test('Format the to-do list text', () => {
    render(
      <TodoItem
        todo={mockTodo}
        onToggle={vi.fn()}
        onDelete={vi.fn()}
      />
    )

    expect(screen.getByText('Study React Test')).toBeInTheDocument()
  })

  test('Completed items are displayed with a strikethrough', () => {
    render(
      <TodoItem
        todo={{ ...mockTodo, completed: true }}
        onToggle={vi.fn()}
        onDelete={vi.fn()}
      />
    )

    const text = screen.getByText('Study React Test')
    expect(text).toHaveStyle('text-decoration: line-through')
  })

  test('Triggered by clicking the checkbox onToggle', async () => {
    const onToggle = vi.fn()
    const user = userEvent.setup()

    render(
      <TodoItem
        todo={mockTodo}
        onToggle={onToggle}
        onDelete={vi.fn()}
      />
    )

    await user.click(screen.getByRole('checkbox'))
    expect(onToggle).toHaveBeenCalledTimes(1)
    expect(onToggle).toHaveBeenCalledWith(42)  // The validation parameters are todo.id
  })

  test('Triggered by clicking the Delete button onDelete', async () => {
    const onDelete = vi.fn()
    const user = userEvent.setup()

    render(
      <TodoItem
        todo={mockTodo}
        onToggle={vi.fn()}
        onDelete={onDelete}
      />
    )

    await user.click(screen.getByRole('button', { name: /Delete/ }))
    expect(onDelete).toHaveBeenCalledWith(42)
  })

  test('Unfinished items are not struck through', () => {
    render(
      <TodoItem
        todo={mockTodo}
        onToggle={vi.fn()}
        onDelete={vi.fn()}
      />
    )

    const text = screen.getByText('Study React Test')
    // Note:Inline styles text-decoration as  'none',rather than not having this property
    expect(text).not.toHaveStyle('text-decoration: line-through')
  })
})

الاستخدامات الرئيسية لدالة Mock vi.fn():

واجهة برمجة التطبيقات (API) الدالة
vi.fn() إنشاء دالة وهمية فارغة
toHaveBeenCalledTimes(n) تم التحقق منه n مرات
toHaveBeenCalledWith(...) التحقق من المعلمات أثناء المكالمة
vi.fn().mockResolvedValue(x) محاكاة استجابة نجاح غير متزامنة
vi.fn().mockRejectedValue(e) محاكاة استجابات الفشل غير المتزامنة

(4) اختبار المكونات غير المتزامنة

تعرض العديد من المكونات في البداية عبارة «جاري التحميل...» أثناء التحميل، ثم تعرض المحتوى بمجرد وصول البيانات. يتعين عليك استخدام findBy (الانتظار غير المتزامن) لاختبار هذه الأنواع من السيناريوهات.

▶ المثال 3: اختبار مكون تحميل البيانات غير المتزامن

TSX
// UserProfile.tsx
interface UserProfileProps {
  userId: number
}

interface UserData {
  id: number
  name: string
  email: string
}

// Simulation API Call
async function fetchUser(id: number): Promise<UserData> {
  const res = await fetch(`/api/users/${id}`)
  if (!res.ok) throw new Error('Failed to load')
  return res.json()
}

export function UserProfile({ userId }: UserProfileProps) {
  const [user, setUser] = useState<UserData | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    let cancelled = false

    async function load() {
      setLoading(true)
      setError(null)
      try {
        const data = await fetchUser(userId)
        if (!cancelled) setUser(data)
      } catch (err) {
        if (!cancelled) {
          setError(err instanceof Error ? err.message : 'Unknown error')
        }
      } finally {
        if (!cancelled) setLoading(false)
      }
    }

    load()
    return () => { cancelled = true }
  }, [userId])

  if (loading) return <div aria-label="Loading...">Loading......</div>
  if (error) return <div role="alert">Error:{error}</div>
  if (!user) return <div>No data</div>

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  )
}
TSX
// UserProfile.test.tsx
import { render, screen } from '@testing-library/react'
import { UserProfile } from './UserProfile'

// Mock out fetchUser module
vi.mock('./UserProfile', async (importOriginal) => {
  const actual = await importOriginal()
  return {
    ...actual,
    // Rewrite fetchUser Implementation
    fetchUser: vi.fn(),
  }
})

// A Better Approach:Separately mock API Module
// vi.mock('../api', () => ({
//   fetchUser: vi.fn()
// }))

describe('UserProfile Asynchronous Components', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  test('Display "Loading" while loading', () => {
    // let  fetch All along pending
    vi.spyOn(global, 'fetch').mockImplementation(
      () => new Promise(() => {})  // Never resolve
    )

    render(<UserProfile userId={1} />)
    expect(screen.getByLabelText('Loading...')).toBeInTheDocument()
  })

  test('Display user information after successful loading', async () => {
    const mockUser = { id: 1, name: 'Alice', email: 'alice@example.com' }

    // Mock fetch Return successful data
    vi.spyOn(global, 'fetch').mockResolvedValue({
      ok: true,
      json: async () => mockUser,
    } as Response)

    render(<UserProfile userId={1} />)

    // findByText — Waiting Asynchronously for an Element to Appear(Default Timeout 1000ms)
    expect(await screen.findByText('Alice')).toBeInTheDocument()
    expect(screen.getByText('alice@example.com')).toBeInTheDocument()
  })

  test('Display an error message when loading fails', async () => {
    // Mock fetch Back 500
    vi.spyOn(global, 'fetch').mockResolvedValue({
      ok: false,
      status: 500,
      statusText: 'Internal Server Error',
    } as Response)

    render(<UserProfile userId={1} />)

    // Wait for an error message to appear
    expect(await screen.findByRole('alert')).toHaveTextContent('Error:')
  })

  test('The state is not updated when the component is unmounted(Preventing Memory Leaks)', async () => {
    const mockUser = { id: 1, name: 'Alice', email: 'alice@example.com' }
    let resolvePromise!: (value: any) => void

    vi.spyOn(global, 'fetch').mockReturnValue(
      new Promise((resolve) => {
        resolvePromise = resolve
      })
    )

    const { unmount } = render(<UserProfile userId={1} />)
    // in  fetch Uninstall Components Before Completion
    unmount()

    // At this moment resolve,But the component has been uninstalled,Probably not. setState
    resolvePromise({
      ok: true,
      json: async () => mockUser,
    } as Response)

    // I didn't make a mistake = Test Passed
  })
})

مقارنة بين الطرق الثلاث الأساسية للاختبار غير المتزامن:

الطريقة متزامنة/غير متزامنة في حالة عدم وجود العنصر المهلة الافتراضية حالة الاستخدام
getByText متزامن إصدار خطأ فوري - يجب أن يكون العنصر موجودًا
queryByText مزامنة العودة إلى null - العنصر غير موجود
findByText غير متزامن (Promise) إصدار خطأ بعد انتهاء المهلة 1000 مللي ثانية انتظار العرض غير المتزامن

(5) أفضل الممارسات في محاكاة التبعيات الخارجية

في المشاريع العملية، غالبًا ما تعتمد المكونات على واجهات برمجة التطبيقات (APIs) والتوجيه وإدارة الحالة. ويُعد محاكاة هذه التبعيات أمرًا أساسيًّا في عملية الاختبار.

الطريقة الأولى: محاكاة المتغير العام fetch

TS
// Replace the global variable before each test fetch
beforeEach(() => {
  vi.spyOn(global, 'fetch').mockResolvedValue({
    ok: true,
    json: async () => ({ data: 'mock' }),
  } as Response)
})

afterEach(() => {
  vi.restoreAllMocks()  // Restore to Original fetch
})

الطريقة الثانية: الوحدة النمطية الوهمية

TS
// api.ts — Real Module
export async function fetchUsers() {
  const res = await fetch('/api/users')
  return res.json()
}

// Testing... Mock
vi.mock('../api', () => ({
  fetchUsers: vi.fn().mockResolvedValue([
    { id: 1, name: 'Mock User' },
  ])
}))

الطريقة الثالثة: محاكاة React Router

TSX
import { MemoryRouter } from 'react-router-dom'

test('Rendering in the route context', () => {
  render(
    <MemoryRouter initialEntries={['/users/1']}>
      <UserDetailPage />
    </MemoryRouter>
  )
})

▶ المثال 4: اختبار «هوك» مخصص

JSX
import { renderHook, act } from '@testing-library/react'
import { useState, useCallback } from 'react'

function useCounter(initial = 0) {
  const [count, setCount] = useState(initial)
  const increment = useCallback(() => setCount(c => c + 1), [])
  const decrement = useCallback(() => setCount(c => c - 1), [])
  const reset = useCallback(() => setCount(initial), [initial])
  return { count, increment, decrement, reset }
}

describe('useCounter', () => {
  test('initializes with default value', () => {
    const { result } = renderHook(() => useCounter())
    expect(result.current.count).toBe(0)
  })

  test('initializes with custom value', () => {
    const { result } = renderHook(() => useCounter(10))
    expect(result.current.count).toBe(10)
  })

  test('increments counter', () => {
    const { result } = renderHook(() => useCounter())
    act(() => result.current.increment())
    expect(result.current.count).toBe(1)
  })

  test('decrements counter', () => {
    const { result } = renderHook(() => useCounter(5))
    act(() => result.current.decrement())
    expect(result.current.count).toBe(4)
  })

  test('resets to initial value', () => {
    const { result } = renderHook(() => useCounter(10))
    act(() => result.current.increment())
    act(() => result.current.reset())
    expect(result.current.count).toBe(10)
  })
})
▶ جرّب الكود

▶ المثال 5: اختبار التكامل — عملية إرسال النموذج

JSX
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

function LoginForm({ onSubmit }) {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState('')

  async function handleSubmit(e) {
    e.preventDefault()
    if (!email.includes('@')) { setError('Invalid email'); return }
    if (password.length < 6) { setError('Password too short'); return }
    setError('')
    await onSubmit({ email, password })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={e => setEmail(e.target.value)} placeholder="Email" data-testid="email" />
      <input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="Password" data-testid="password" />
      {error && <p data-testid="error">{error}</p>}
      <button type="submit">Login</button>
    </form>
  )
}

describe('LoginForm integration', () => {
  test('shows error for invalid email', async () => {
    render(<LoginForm onSubmit={jest.fn()} />)
    await userEvent.type(screen.getByTestId('email'), 'invalid')
    await userEvent.type(screen.getByTestId('password'), 'password123')
    await userEvent.click(screen.getByRole('button', { name: /login/i }))
    expect(screen.getByTestId('error')).toHaveTextContent('Invalid email')
  })

  test('calls onSubmit with valid data', async () => {
    const onSubmit = jest.fn().mockResolvedValue(undefined)
    render(<LoginForm onSubmit={onSubmit} />)
    await userEvent.type(screen.getByTestId('email'), 'alice@test.com')
    await userEvent.type(screen.getByTestId('password'), 'secure123')
    await userEvent.click(screen.getByRole('button', { name: /login/i }))
    await waitFor(() => {
      expect(onSubmit).toHaveBeenCalledWith({ email: 'alice@test.com', password: 'secure123' })
    })
  })
})


❓ أسئلة شائعة

س ما الفرق بالضبط بين getBy و findBy و queryBy؟
ج قاعدة عامة بسيطة: استخدم getBy عندما يكون وجود العنصر مؤكدًا (يتم إلقاء خطأ في حالة عدم العثور عليه)؛ واستخدم queryBy عندما يكون من المحتمل ألا يكون العنصر موجودًا (تُرجع القيمة null)؛ استخدم findBy عندما يتم عرض العنصر بشكل غير متزامن (تُرجع Promise؛ ويتم إصدار خطأ بعد انتهاء المهلة). لكل طريقة متغيرات Role/Text/TestId المقابلة، مثل getByRole وfindByText وqueryByTestId.
س أيهما يجب أن أستخدم، userEvent أم fireEvent؟
ج استخدم userEvent كلما أمكن ذلك. fireEvent هي واجهة برمجة تطبيقات (API) منخفضة المستوى تُطلق أحداث DOM مباشرةً. userEvent تحاكي تسلسلًا كاملاً من إجراءات المستخدم فوق fireEvent (على سبيل المثال، «النقر» يشمل mousedown → mouseup → click)، وهو ما يتطابق بشكل أوثق مع سلوك المتصفح الفعلي. لا يجب اللجوء إلى fireEvent إلا في حالة العمليات التي لا تدعمها userEvent.
س هل يجب محاكاة المكونات الفرعية في الاختبارات؟
ج تتمثل فلسفة React Testing Library في «عدم محاكاة المكونات الفرعية»، لأن الاختبارات يجب أن تحاكي منظور المستخدم — فما يراه المستخدم هو شجرة المكونات الكاملة. ولا ينبغي التفكير في محاكاة المكونات الفرعية إلا عندما تكون لها آثار جانبية ملحوظة (مثل مكتبات الرسوم المتحركة المعقدة أو مكونات الرسوم البيانية التابعة لجهات خارجية). ما عليك سوى استبدالها بـ vi.mock('./ExpensiveChart', () => () => <div>Mock Chart</div>).
س هل يجب عليّ تنظيف بيئة الاختبار في beforeEach؟
ج نعم. بعد كل اختبار، يجب عليك تنظيف DOM الذي تم عرضه والأنماذج الوهمية. نوصي باستخدام afterEach(() => { vi.clearAllMocks() }). يقوم Vitest تلقائيًا بإلغاء تحميل المكونات بعد كل عملية عرض، ولكن يجب عليك تنظيف حالة النموذج الوهمي يدويًّا. إذا كنت تستخدم نموذجًا وهميًّا عالميًّا للجلب، فاستخدم vi.restoreAllMocks() لاستعادة التنفيذ الأصلي.
س ما هو مستوى تغطية الاختبار الكافي؟
ج لا يوجد معيار واحد يناسب الجميع، لكن المعايير المرجعية في المجال تشير إلى: 80%+ للمنطق الأساسي للأعمال، و90%+ للوظائف المساعدة العامة، و60%+ لمكونات واجهة المستخدم. لا تسعَ إلى تحقيق تغطية بنسبة 100% — فبعض الأكواد (مثل أنماط CSS وتمرير المتغيرات البسيطة) لها عائد استثمار منخفض جدًا في الاختبار. المفتاح هو تغطية مسارات الكود التي سيكون لها «أكبر تأثير في حالة حدوث خطأ». يمكن لمعلمة --coverage في Vitest إنشاء تقرير تغطية Istanbul.

📖 ملخص


📝 تمارين

  1. اكتب اختبارات شاملة لمكون Button: تحقق من أن النقر عليه يؤدي إلى تشغيل onClick وdisabled، وأنه يصبح غير قابل للنقر، وأن النص الصحيح يُعرض، وأن اسم الفئة المخصص (className) يسري مفعوله. قم بتغطية 4 حالات اختبار على الأقل.
  2. اكتب اختبارات لمكون UserProfile: يجب أن يتحقق الاختبار من أن حالة التحميل تعرض عبارة «جاري التحميل»، وأن حالة النجاح تعرض معلومات المستخدم (مع المعالجة غير المتزامنة)، وأن حالة الفشل تعرض رسالة خطأ. استخدم vi.spyOn لمحاكاة واجهة برمجة التطبيقات (API) الخاصة بالاسترجاع.
  3. اكتب اختبارات التكامل لمكون TodoApp (بما في ذلك TodoList و TodoItem ونموذج AddTodo): اختبر الوظائف الثلاث — إضافة مهمة جديدة، ووضع علامة «مكتمل» على مهمة، وحذف مهمة — للتحقق من أن واجهة المستخدم الخاصة بالقائمة يتم تحديثها بشكل صحيح.
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%