الوحدة 3 · اختبار واجهة المستخدم
الاستعلام عن الـ DOM كما يراه المستخدم
18 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
تجد العناصر في اختبارات الـ DOM بالطريقة التي يجدها بها المستخدمون والتقنيات المساعدة، باستخدام استعلامات Testing Library بالدور والتسمية والنص، وفق ترتيب الأولوية الموصى به.
- 1المبدأ الموجّه
- 2ترتيب الأولوية
- 3اختبار DOM باستخدام Testing Library
- 4getBy وqueryBy وfindBy
- 5حين لا يجد الـ query عنصرك
- 6في ساحة التجربة
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
إليك اختبارًا وجدته في قاعدة كود العام الماضي: document.querySelector('.card > div:nth-child(2) span.amount-red'). انكسر حين غيّر مصمّم لون المبالغ السالبة. لم ينكسر شيء مما يراه المستخدم، باستثناء اللون، الذي لم يختبره أحد أصلًا. كان الاختبار مرتبطًا بشكل الـ DOM وبالـ CSS، لا بما تقوله الصفحة.
المبدأ الموجّه
فكرة Testing Library الجوهرية تتّسع لها جملة واحدة: كلما أشبهت اختباراتك الطريقة التي يُستخدم بها برنامجك، زادت الثقة التي تمنحك إياها. لا يرى المستخدمون أسماء الأصناف ولا nth-child. بل يرون زرًّا عنوانه “Add expense”، وحقلًا تسميته “Amount”، وقائمة أرصدة. ويحصل مستخدمو قارئ الشاشة على هذه المعلومات بالضبط عبر شجرة إمكانية الوصول (accessibility tree): دور كل عنصر (button وlist وtextbox) واسمه القابل للوصول (تسميته أو نصّه).
لذا تجد استعلامات Testing Library العناصر بالدور والتسمية والنص. والاختبار الذي يجد زر “Settle up” بدوره واسمه يظلّ ينجح مع أي إعادة تصميم، ويفشل في اليوم الذي يتوقّف فيه الزر عن أن يكون متاحًا لمستخدمي لوحة المفاتيح وقارئ الشاشة. وهذا خطأ يستحق الالتقاط.
ترتيب الأولوية
يغطّي getByRole مع الخيار name معظم الحالات: الأزرار، والروابط، والعناوين، وحقول النماذج، والقوائم، والحوارات، والتنبيهات. وgetByLabelText هو الخيار الطبيعي لحقول النماذج. ويجد getByText المحتوى غير التفاعلي مثل رسالة أو سطر رصيد. أما الـ test ids فللعناصر التي لا دور لها ولا نص، مثل لوحة رسم بياني (canvas)، وهي لا تثبت شيئًا عن إمكانية الوصول.
اختبار DOM باستخدام Testing Library
يعرض Splitwise-lite الأرصدة كقائمة بسيطة. مع ضبط البيئة على jsdom وتثبيت @testing-library/dom، يبدو الاختبار هكذا:
// @vitest-environment jsdom
import { afterEach, test, expect } from 'vitest';
import { screen, within } from '@testing-library/dom';
import { renderBalances } from './render-balances.js';
afterEach(() => {
document.body.innerHTML = '';
});
test('lists each person with what they owe or are owed', () => {
renderBalances(document.body, { Ben: -500, Ana: 1200, Cai: 0 });
const list = screen.getByRole('list', { name: 'Balances' });
const items = within(list).getAllByRole('listitem');
expect(items.map((item) => item.textContent)).toEqual([
'Ana is owed $12.00',
'Ben owes $5.00',
'Cai is settled up',
]);
});
test('shows an empty state instead of an empty list', () => {
renderBalances(document.body, {});
expect(screen.getByText('No expenses yet')).toBeTruthy();
expect(screen.queryByRole('list')).toBeNull();
});يستعلم screen في المستند كله. ويحصر within(element) الاستعلامات في جزء منه، وهذا مهم بمجرد أن تحتوي الصفحة على قائمتين. وتنظيف document.body بعد كل اختبار يمنع الـ DOM الخاص باختبار من التسرّب إلى الاختبار التالي. (تستطيع React Testing Library أن تتولّى هذا التنظيف عنك؛ ويوضّح الدرس الرابع من هذا القسم متى يحدث ذلك.)
getBy وqueryBy وfindBy
لكل استعلام ثلاث نكهات، إضافة إلى نسخة All من كل منها:
| البادئة | لا يوجد تطابق | عدة تطابقات | استخدمها لـ |
|---|---|---|---|
getBy |
يرمي خطأً | يرمي خطأً | عناصر يجب أن تكون موجودة |
queryBy |
يعيد null |
يرمي خطأً | تأكيد أن شيئًا ما غائب |
findBy |
يُرفض بعد الانتظار | يُرفض | عناصر تظهر لاحقًا |
رمي getBy خطأً عند وجود عدة تطابقات ميزة لا عيب: إن كان استعلامك ملتبسًا، يخبرك الاختبار بدل أن يختار الأول بصمت.
حين لا يجد الـ query عنصرك
تفشل استعلامات الدور بصوت عالٍ، وفشلها مفيد. حين لا يجد getByRole('button', { name: 'Settle up' }) شيئًا، تطبع Testing Library كل دور متاح داخل الحاوية مع الأسماء التي حسبتها، فترى أن اسم الزر في الحقيقة “Settle up all debts”، أو أن الـ div القابل للنقر لديك بلا دور أصلًا. استدعِ screen.debug() لطباعة الـ DOM الحالي حين تحتاج إلى سياق أكثر. تأتي الأسماء المتاحة من بضعة مصادر، بهذا الترتيب تقريبًا: aria-labelledby، ثم aria-label، ثم <label> مرتبط، ثم نص العنصر نفسه (أو alt للصور). وإن لم يعطِ أيّ منها الاسم الذي تتوقّعه، فمستخدم قارئ الشاشة يسمع الشيء المربك نفسه الذي يراه اختبارك.
في ساحة التجربة
لا توجد حزم npm في ساحة التجربة، لذا تبدأ تمارين هذا القسم بنحو أربعين سطرًا من البدائل: getByRole وgetAllByRole وqueryByRole وgetByText وqueryByText، بالأسماء والسلوك نفسه في الحالات التي تحتاجها. ومثل الدوال المصدَّرة مباشرة من @testing-library/dom (لا عبر screen)، تأخذ هذه البدائل الحاوية معاملًا أول: getByRole(container, 'list'). تقرأ الدور من الوسم (أو من خاصية role) والاسم من aria-label أو <label> أو النص. أما Testing Library الحقيقية فتحسب شجرة إمكانية الوصول بشمول أكبر بكثير، لذا ثبّتها في مشاريعك.
الدرس القادم: النقر والكتابة كما يفعل المستخدم.
أهم النقاط
- استعلم بما يدركه المستخدمون (الدور والاسم القابل للوصول، والتسمية، والنص الظاهر)، لا بأصناف CSS أو بنية الـ DOM، كي لا تنكسر الاختبارات حين يتغيّر التصميم.
- ترتيب الأولوية هو:
getByRole، ثمgetByLabelTextوgetByPlaceholderTextوgetByTextوgetByDisplayValue، ثم النص البديل والعنوان، وgetByTestIdفي الأخير. - يرمي
getByخطأً حين يجد صفر تطابقات أو عدة تطابقات، ويعيدqueryByالقيمةnull(استخدمه لتأكيد الغياب)، وينتظرfindByظهور العنصر. - إن لم يجد استعلام الدور عنصرك، فغالبًا ما تكون البنية غير قابلة للوصول بعد؛ أصلح البنية بدل الانتقال إلى test id.