تست تعامل کاربر: کلیک، تایپ و موارد مشابه

در درس قبل دیدیم چرا باید از دید کاربر تست بنویسیم. در این درس، وارد جزئیات عملی می‌شویم: چگونه عناصر را روی صفحه پیدا کنیم و چگونه تعاملات واقعی کاربر، مثل کلیک و تایپ، را در یک تست شبیه‌سازی کنیم.

پیدا کردن عناصر: getByRole به‌عنوان انتخاب اول

React Testing Library چند تابع برای پیدا کردن عناصر ارائه می‌دهد، اما توصیه رسمی این است که getByRole همیشه اولین انتخاب باشد؛ چون دقیقاً همان چیزی را جست‌وجو می‌کند که یک صفحه‌خوان هم می‌بیند: نقش عنصر (دکمه، لینک، فیلد متنی) و نام قابل‌دسترس آن.

javascript
const button = screen.getByRole("button", { name: "ارسال" });
const emailInput = screen.getByRole("textbox", { name: "ایمیل" });

توجه کنید که name در اینجا همان متن قابل‌مشاهده دکمه، یا متن Labelی است که در فصل هجدهم با htmlFor به فیلد متصل کردیم. این یعنی اگر یک فیلد Label درست نداشته باشد، پیدا کردنش در تست هم سخت‌تر می‌شود؛ دقیقاً همان ارتباطی که در درس قبل به آن اشاره کردیم.

شبیه‌سازی کلیک

jsx
import { render, screen, fireEvent } from "@testing-library/react";

test("کلیک روی دکمه، شمارنده را افزایش می‌دهد", () => {
  render(<Counter />);

  const button = screen.getByRole("button");
  fireEvent.click(button);

  expect(button).toHaveTextContent("1");
});

fireEvent.click، دقیقاً همان رویداد onClick را که در فصل چهارم دیدیم، روی عنصر فعال می‌کند؛ از دید کامپوننت، هیچ فرقی با یک کلیک واقعی ماوس ندارد.

شبیه‌سازی تایپ در یک فیلد

jsx
test("تایپ در فیلد ایمیل، مقدار آن را به‌روزرسانی می‌کند", () => {
  render(<EmailField />);

  const input = screen.getByRole("textbox", { name: "ایمیل" });
  fireEvent.change(input, { target: { value: "test@example.com" } });

  expect(input).toHaveValue("test@example.com");
});

fireEvent.change، رویداد onChange را فعال می‌کند؛ همان رویدادی که در فصل چهارم، پایه Controlled Components بود. این تست، بدون هیچ آگاهی از این‌که فیلد از useState یا useReducer استفاده می‌کند، فقط رفتار نهایی قابل‌مشاهده را بررسی می‌کند.

fireEvent در برابر user-event: یک تفاوت مهم

fireEvent یک رویداد DOM منفرد را شبیه‌سازی می‌کند؛ اما یک تعامل واقعی کاربر (مثل تایپ یک کلمه)، در واقع از چند رویداد پشت‌سرهم (فشردن هر کلید، فوکوس، تغییر مقدار) تشکیل شده است. کتابخانه مکمل @testing-library/user-event این رویدادها را به‌شکلی نزدیک‌تر به واقعیت شبیه‌سازی می‌کند و در عمل، برای تعاملات پیچیده‌تر، ترجیح داده می‌شود:

jsx
import userEvent from "@testing-library/user-event";

test("تایپ کامل یک کلمه در فیلد جست‌وجو", async () => {
  render(<SearchBox />);
  const input = screen.getByRole("textbox");

  await userEvent.type(input, "کیبورد");

  expect(input).toHaveValue("کیبورد");
});

بررسی نتیجه با Matcherهای معنادار

کتابخانه jest-dom، Matcherهایی مثل toHaveTextContent، toHaveValue، و toBeDisabled اضافه می‌کند که خواناتر از مقایسه مستقیم رشته‌ها یا بررسی دستی DOM هستند؛ این Matcherها هم، دقیقاً همان چیزهایی را بررسی می‌کنند که کاربر واقعی تجربه می‌کند (متن دیده‌شده، مقدار فعلی فیلد، فعال یا غیرفعال بودن دکمه).

مثال قابل اجرا

چون اجرای واقعی Jest و React Testing Library در این محیط ممکن نیست، در این مثال، همان ایده fireEvent با رویدادهای واقعی DOM (بدون فریمورک تست) شبیه‌سازی شده است.

شبیه‌سازی دستی تعامل کلیک و تایپ، بدون فریمورک تست
import { useState } from "react";

function SearchBox() {
  const [query, setQuery] = useState("");
  return (
    <input
      aria-label="جست‌وجو"
      value={query}
      onChange={(e) => setQuery(e.target.value)}
    />
  );
}

function App() {
  return (
    <div>
      <p>در فیلد زیر تایپ کنید؛ این دقیقاً همان رویدادی است که fireEvent.change شبیه‌سازی می‌کند.</p>
      <SearchBox />
    </div>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

getByRole بهترین روش پیدا کردن عناصر است، چون مستقیماً با نقش و نام قابل‌دسترس کار می‌کند؛ fireEvent یک رویداد منفرد را شبیه‌سازی می‌کند و user-event تعاملات واقعی‌تر و چندمرحله‌ای را. بررسی نتیجه با Matcherهای معنادار jest-dom انجام می‌شود که همگی روی آنچه کاربر واقعی می‌بیند تمرکز دارند. در درس بعدی، به Mocking، یعنی جایگزین کردن بخش‌هایی از برنامه برای تست‌پذیرتر کردن آن، می‌پردازیم.