استراتيجيات محددات العناصر
محدِّد العنصر (Locator) هو الاستعلام الذي يستخدمه اختبار الواجهة للعثور على عنصر — زر أو حقل أو صف في جدول. أحسِن اختياره فيصمد الاختبار أمام إعادة التصميم وإعادة الهيكلة وإطار CSS جديد؛ وأسِئ اختياره فتنكسر الحزمة كل سبرنت لأسباب لا علاقة لها بالعيوب.
يقدّم هذا الدليل ترتيب أولوية يصلح في Playwright وSelenium وCypress وTesting Library، ويقارن بين CSS وXPath، ويشرح التعامل مع المعرّفات الديناميكية وإطارات iframe وShadow DOM، وينتهي بمثال عملي وقائمة مراجعة. وهو جزء من دليل أتمتة اختبار واجهة المستخدم الذي يغطي الانتظار والتأكيدات وكائنات الصفحات.
ما الذي يجعل المحدِّد جيداً
أفضل طريقة للحكم على المحدِّد هي النظر إلى ما يكسره. فالمحدِّد الجيد لا يتغير إلا إذا تغيّر السلوك الذي يعتمد عليه المستخدم — لا عندما يعيد مصمّم تسمية صنف أو يضيف مطوّر عنصر div مغلِّفاً.
- فريد — يطابق عنصراً واحداً بالضبط في الحالة التي يتوقعها الاختبار. محددات Playwright صارمة (strict): الإجراء على محدِّد يطابق عدة عناصر يرمي خطأ، ما يكشف الغموض مبكراً.
- مستقر — مستقل عن التخطيط والتنسيق وعمق DOM وترتيب العناصر والقيم المولّدة عند البناء أو العرض.
- مقروء — من يقرأ الاختبار يعرف أي عنصر يقصد دون أن يفتح الصفحة.
- قريب من نيّة المستخدم — يسمّي العنصر كما يدركه المستخدم أو التقنيات المساعدة: زر «إتمام الطلب»، وحقل «البريد الإلكتروني».
- رخيص الإصلاح — عندما ينكسر، توضّح رسالة الفشل السبب.
ترتيب الأولوية: ما يراه المستخدم أولاً وXPath أخيراً
يوصي توثيق Playwright بالمحددات المبنية على الدور أولاً، ويرتّب Testing Library استعلاماته بالطريقة نفسها: الاستعلامات التي تعكس كيف يدرك المستخدمون والتقنيات المساعدة الصفحة تأتي أولاً، ومعرّفات الاختبار تأتي أخيراً بين استعلاماته. وبإضافة CSS وXPath للحالات التي لا يصل إليها غيرهما يصبح الترتيب:
- 1. الدور والاسم الميسَّر — getByRole("button", { name: "Place order" }). يأتي الدور (Role) من عنصر HTML (button وlink وtextbox وcheckbox وheading) أو من دور ARIA، ويأتي الاسم من التسمية أو المحتوى أو aria-label، ويُحسب كما تصف مواصفة W3C لحساب الاسم الميسَّر (Accessible Name).
- 2. التسمية (Label) — getByLabel("Email") لحقول النماذج المرتبطة بعنصر <label> أو بـ aria-labelledby.
- 3. النص المرئي والنص البديل والعنوان ونص التلميح في الحقل — getByText وgetByAltText وgetByTitle (ومحدِّد مقابل لنص التلميح داخل الحقل) للعناصر التي ليس لها دور واسم مفيدان. وتناسب محددات النص المحتوى غير التفاعلي مثل العناوين والرسائل؛ ونص التلميح بديل ضعيف عن التسمية.
- 4. معرّف الاختبار (Test id) — getByTestId("order-total") يطابق data-testid افتراضياً في Playwright (والسمة قابلة للتهيئة)؛ ويوصي Cypress بـ data-cy أو سمة data-* مماثلة. استخدمه عندما تكون سمات المستخدم غامضة أو مفقودة أو كثيرة التغيّر، كالنصوص المترجمة.
- 5. محدِّد CSS — للحالات البنيوية: السمات والمُركِّبات وحقول الإدخال بالاسم. أبقِه قصيراً ومرتكزاً على سمات مستقرة.
- 6. XPath — عندما تحتاج إلى الصعود إلى عنصر أب أو سلف، أو مطابقة النص في أداة لا تملك محدِّداً للنص. أبقِه نسبياً وقصيراً.
Element: <button type="submit">Place order</button>
Playwright page.getByRole("button", { name: "Place order" })
Testing Library screen.getByRole("button", { name: "Place order" })
Selenium (Java) driver.findElement(By.xpath("//button[normalize-space()='Place order']"))
driver.findElement(By.cssSelector("[data-testid=place-order]"))
Cypress cy.contains("button", "Place order")
cy.get("[data-cy=place-order]")| المحدِّد | إعادة التنسيق | إضافة div مغلِّف | إضافة عناصر أو تغيير الفرز | بناء أو عرض جديد | واجهة مترجمة | يكشف غياب الاسم الميسَّرفائدة جانبية |
|---|---|---|---|---|---|---|
| 1 · الدور والاسم الميسَّر | يصمد | يصمد | يصمد | يصمد | ينكسريطابق النص الإنجليزي | نعم |
| 2 · التسمية | يصمد | يصمد | يصمد | يصمد | ينكسريطابق النص الإنجليزي | نعم |
| 3 · النص المرئي | يصمد | يصمد | يصمد | يصمد | ينكسريطابق النص الإنجليزي | لا |
| 4 · معرّف الاختبار | يصمد | يصمد | يصمد | يصمد | يصمد | لا |
| 5 · محدِّد CSSقصير ومرتكز على سمات مستقرة | يصمد | يصمد | يصمد | يصمد | يصمد | لا |
| 6 · XPathنسبي ويطابق النص | يصمد | يصمد | يصمد | يصمد | ينكسريطابق النص الإنجليزي | لا |
| صنف تنسيق | ينكسر | يصمد | يصمد | يصمد | يصمد | لا |
| معتمد على الفهرس | يصمد | يصمد | عنصر خاطئ | يصمد | يصمد | لا |
| قيمة مولّدة | لا ينطبق: ينكسر أصلاً في كل عرض | لا ينطبق: ينكسر أصلاً في كل عرض | لا ينطبق: ينكسر أصلاً في كل عرض | ينكسر | لا ينطبق: ينكسر أصلاً في كل عرض | لا |
| XPath المطلق | يصمد | ينكسر | قد ينكسرإذا تغيّر موضع أحد الأسلاف | يصمد | يصمد | لا |
- ينكسر، أو قد ينكسر، أو يجد عنصراً خاطئاً
- نمط خاطئ
- ترتيب الأولوية: أمثلة
- 1 · الدور والاسم الميسَّر
getByRole("button", { name: "Place order" }) - 2 · التسمية
getByLabel("Email") - 3 · النص المرئي
getByText("Sign in") - 4 · معرّف الاختبار
getByTestId("order-total") - 5 · محدِّد CSS
input[name=terms] - 6 · XPath
//button[normalize-space()='Place order']
- الأنماط الخاطئة: أمثلة
- صنف تنسيق
.btn-primary - معتمد على الفهرس
(//button)[2] - قيمة مولّدة
#input-4821 - XPath المطلق
/html/body/div[2]/div/div[3]/form/button
الأمثلة من نموذج إتمام الشراء وعناوين الأخطاء في الصفحة. وتعمل المحددات النصية على واجهة مترجمة أيضاً إذا كان لكل لغة نصوصها المتوقعة. والقيمة المولّدة تنكسر في كل عرض مهما تغيّر غيرها (—).
تطبيق الترتيب في Selenium وCypress
لا يملك Selenium محدِّداً مدمجاً للدور. استراتيجياته هي id وname وclass name وtag name وlink text وpartial link text وCSS selector وXPath، ويضيف Selenium 4 المحددات النسبية (Relative locators): فوق، وتحت، وإلى اليسار، وإلى اليمين، وقريب من. وعملياً يصبح الترتيب: معرّف اختبار أو id مستقر، ثم name، ثم محدِّد CSS قصير، ثم XPath — مع الوصول إلى النص المرئي عبر link text للروابط أو XPath يطابق النص لغيرها. وتغلّف فرق كثيرة ذلك في دوال كائن الصفحة المسمّاة باسم العنصر كما يراه المستخدم، فتبقى الاختبارات مقروءة: «انقر إتمام الطلب».
يستعلم Cypress بـ cy.get (CSS) وcy.contains (النص). ويوصي دليل أفضل الممارسات فيه بسمات data-* مخصصة بدلاً من محددات الوسم والمعرّف والصنف، وبـ cy.contains للعناصر التي يكون نصها جزءاً من السلوك المختبَر. والفرق التي تريد استعلامات مبنية على الدور في Cypress كثيراً ما تضيف تكامل Testing Library الذي يضيف findByRole وبقية استعلامات findBy وfindAllBy.
معرّفات الاختبار: عقد مع المطورين
معرّف الاختبار سمة تُضاف إلى الترميز لأغراض الأتمتة فقط، مثل data-testid أو data-test أو data-cy. وهو أكثر نقاط الربط استقراراً لأنه لا يعتمد عليه شيء سوى الاختبارات — ما دام الفريق يعامله كعقد لا كزينة.
- اتفقوا على اسم سمة واحد للمنتج كله واضبطوا الأداة لاستخدامه (مثل إعداد testIdAttribute في Playwright).
- سمِّ حسب الغرض لا الموضع أو الشكل: checkout-place-order لا button-3 ولا blue-btn.
- اجعل المعرّفات فريدة داخل الصفحة. وللعناصر المتكررة ضع المعرّف على الحاوية أو حدّد النطاق بالمحتوى، ثم ابحث عن العنصر الابن بالدور — صف الطلب ORD-2026-000184، ثم زر «إلغاء» فيه.
- أضف معرّفات الاختبار في طلب الدمج نفسه الذي يضيف الميزة، وراجع حذفها كما تراجع تغييراً في API.
- لا تستبدل دوراً واسماً واضحين بمعرّف اختبار؛ فالمحدِّد المبني على ما يراه المستخدم يُظهر أيضاً أن العنصر مكشوف للتقنيات المساعدة.
- قرار حذف معرّفات الاختبار من نسخ الإنتاج قرار للفريق. إبقاؤها يكلّف بضعة بايتات ويتيح تشغيل اختبارات دخان للقراءة فقط على الإنتاج.
// Playwright: scope to the row for one order, then find its button by role
const row = page.getByRole("row").filter({ hasText: "ORD-2026-000184" });
await row.getByRole("button", { name: "Cancel" }).click();
// playwright.config.ts: use the attribute your team agreed on
export default defineConfig({ use: { testIdAttribute: "data-test" } });CSS مقابل XPath
تعبّر اللغتان عن معظم الاستعلامات البنيوية، والفروق تظهر عند الأطراف. لا أحد منهما «خاطئ»، لكن CSS أقصر عادةً في الحالات الشائعة، وXPath هو الخيار الوحيد في حالات قليلة أخرى.
- المقروئية — CSS أقصر للمعرّفات والأصناف والسمات: [data-testid=cart] مقابل //*[@data-testid="cart"].
- الاتجاه — يستطيع XPath الصعود إلى الأب أو السلف (.. وancestor::tr) والانتقال إلى الأشقاء السابقين؛ أما CSS فيتحرك تقليدياً إلى الأسفل والأمام فقط. ويغطي الصنف الزائف (pseudo-class) :has()، المدعوم في المتصفحات الرئيسية الحالية، بعض حالات «أب العنصر».
- النص — يستطيع XPath مطابقة النص (normalize-space()="Pay now" وcontains(., "Pay"))؛ ولا يستطيع CSS مطابقة محتوى النص. ويضيف Playwright وCypress مطابقة نصية خاصة بهما فوق CSS.
- Shadow DOM — يمكن استخدام محددات CSS داخل جذور الظل المفتوحة، ومحددات CSS والدور في Playwright تخترقها تلقائياً؛ أما XPath فلا يعبر حدود الظل.
- الإصدار — تطبّق المتصفحات XPath 1.0، لذا فدوال XPath 2.0 مثل matches() وends-with() غير متاحة.
- السرعة — كلاهما سريع بما يكفي لاختبارات الواجهة ونادراً ما يكون عنق الزجاجة. اختر حسب المقروئية والاستقرار.
Goal CSS XPath
Element by test id [data-testid=cart] //*[@data-testid="cart"]
Input by name input[name=email] //input[@name="email"]
Id starting with a prefix [id^="order-row-"] //*[starts-with(@id,"order-row-")]
Attribute contains a value [class*="error"] //*[contains(@class,"error")]
Button with exact text (not possible in CSS) //button[normalize-space()="Pay now"]
Button in the row of an order (CSS cannot match the text) //tr[td[normalize-space()="ORD-2026-000184"]]//button
Parent of an element (only via :has()) //input[@name="email"]/..المعرّفات الديناميكية وأسماء الأصناف المولّدة
تولّد أطر ومكتبات مكوّنات كثيرة قيم السمات عند البناء أو العرض: id="input-4821" يتغير في كل عرض، وclass="Button_primary__3xK9a" من CSS modules، وclass="css-1q2w3e" من مكتبات CSS-in-JS. والمحدِّد الذي ينسخ قيمة كهذه ينجح اليوم ويفشل بعد البناء التالي أو التحميل التالي للصفحة.
- تعرّف عليها — أرقام أو قيم تجزئة تختلف بين تحميلين للصفحة نفسها. احفظ الترميز من جلستين وقارن بينهما لتكتشف الأجزاء المتحركة.
- فضّل محدِّداً مبنياً على ما يراه المستخدم أو معرّف اختبار — الحل الدائم نقطة ربط مستقرة، لا محدِّد أذكى.
- إن اضطررت إلى المطابقة فارتكز على الجزء المستقر — [id^="order-row-"] (يبدأ بـ) أو [class*="Button_primary"] (يحتوي) أو starts-with() في XPath. وأبقِ النمط ضيقاً بما يكفي ليظل فريداً.
- طابق النص المتغير بنمط — مثل getByText(/Order ORD-\d{4}-\d{6} confirmed/) في Playwright — واختبر النمط على عينات حقيقية قبل اعتماده.
- لا تكتب أبداً قيماً ثابتة تأتي من بيانات الاختبار — ابنِ المحدِّد من البيانات التي أنشأها الاختبار، مثل رقم الطلب الذي أعاده استدعاء API الذي هيّأ الاختبار.
إطارات iframe وShadow DOM
إطار iframe مستند منفصل، لذا لا يرى محدِّد الصفحة الرئيسية ما بداخله. وكثيراً ما تكون حقول الدفع والخرائط المضمّنة وأدوات المحادثة ومحررات النص المنسّق إطارات iframe.
يغلّف Shadow DOM الترميز الداخلي لمكوّن ويب. يمكن لأدوات الأتمتة الوصول إلى جذور الظل المفتوحة (open shadow roots). أما الجذور المغلقة فمخفية عمداً ولا تصل إليها المحددات القياسية؛ اختبرها عبر السلوك العام للمكوّن أو اطلب من المطورين نقطة ربط للاختبار.
- Selenium — ادخل إلى الإطار بـ driver.switchTo().frame(…)، ونفّذ، ثم عُد بـ driver.switchTo().defaultContent(). وفي Shadow DOM ابحث عن العنصر المضيف، واستدعِ getShadowRoot() (Java؛ وshadow_root في Python)، ثم ابحث داخله بمحددات CSS.
- Playwright — page.frameLocator(…) أو locator.contentFrame() يعطي نطاقاً للمحددات داخل الإطار دون تبديل. ومحددات Playwright تخترق جذور الظل المفتوحة افتراضياً، باستثناء XPath.
- Cypress — يُدعم Shadow DOM عبر الأمر .shadow() أو خيار الإعداد includeShadowDom. ولا يوجد أمر مدمج مخصص لإطارات iframe؛ والنهج الشائع هو الوصول إلى جسم مستند الإطار من عنصر iframe، والإطارات من نطاقات أخرى تضيف قيوداً إضافية.
- إطارات الأطراف الثالثة — لحقل البطاقة المستضاف لدى مزوّد الدفع، استخدم وضع الاختبار لدى المزوّد وبيانات الاختبار المنشورة لديه، وأبقِ عدد الاختبارات التي تكتب داخل الإطار صغيراً.
// Selenium (Java) — iframe: switch in, act, switch back
driver.switchTo().frame(driver.findElement(By.cssSelector("iframe[title='Card details']")));
driver.findElement(By.name("cardnumber")).sendKeys(testCardNumber);
driver.switchTo().defaultContent();
// Selenium (Java) — open shadow root, then CSS inside it
SearchContext shadow = driver.findElement(By.cssSelector("app-date-picker")).getShadowRoot();
shadow.findElement(By.cssSelector("button.next-month")).click();
// Playwright — frame scope, no switching; open shadow roots are pierced
const card = page.frameLocator("iframe[title='Card details']");
await card.getByLabel("Card number").fill(testCardNumber);
await page.locator("app-date-picker").getByRole("button", { name: "Next month" }).click();أنماط خاطئة تجعل الحزم هشّة
- XPath المطلق — /html/body/div[2]/div/div[3]/form/button ينكسر عندما يتغير أي سلف. وأمر «Copy XPath» في المتصفح كثيراً ما ينتج هذا بالضبط.
- المحددات المعتمدة على الفهرس — :nth-child(3) أو (//button)[2] أو .first() دون سبب. تعتمد على الترتيب وتنكسر عند إضافة عنصر أو حذفه أو تغيير الفرز.
- أصناف التنسيق — .btn-primary و.mt-4 و.text-red-500 تتغير مع كل إعادة تصميم مرئي أو إعادة هيكلة لأصناف CSS المساعدة.
- القيم المولّدة — المعرّفات وقيم تجزئة الأصناف الناتجة عند البناء أو العرض.
- السلاسل الطويلة — div.container > div.row > div.col-md-6 > form > div:nth-child(2) > input يرمّز التخطيط كاملاً.
- نص بلغة خاطئة — الاختبار الذي يبحث عن «Place order» يفشل عندما تعمل الحزمة نفسها على الواجهة العربية («إتمام الطلب»). إما أن تعطي كل لغة نصوصها المتوقعة، وإما أن تستخدم معرّفات الاختبار للعناصر ذات النص المترجم.
- المحددات المكررة — المحدِّد نفسه ملصوق في عشرين اختباراً. احفظه في كائن صفحة أو مكوّن واحد.
- مطابقة نص واسعة جداً — getByText("Save") يطابق الزر وإشعاراً منبثقاً وفقرة مساعدة. حدّد النطاق بمنطقة أو استخدم الدور.
Automation defects caused by locators (written like bug titles)
- checkout.spec fails after restyle: .btn-primary no longer matches "Place order"
- orders.spec cancels the wrong order when the list is sorted by date: locator (//button[text()="Cancel"])[1]
- login.spec fails on the Arabic build: getByText("Sign in") not found
- profile.spec fails intermittently: #input-4821 is regenerated on every render
- search.spec breaks after a wrapper div is added: /html/body/div[2]/div/div[3]/form/buttonالمحددات وإمكانية الوصول
تستخدم محددات الدور والتسمية المعلومات نفسها التي يعلنها قارئ الشاشة: دور العنصر، من عنصر HTML أو WAI-ARIA، واسمه الميسَّر. فعندما لا يجد getByRole("button", { name: "Delete" }) زر حذف يتكون من أيقونة فقط، فالسبب المرجّح أن الزر بلا اسم ميسَّر — وهذا عيب حقيقي في إمكانية الوصول، لا مشكلة في الاختبار. أصلح الترميز (نص مرئي أو aria-label)، لا المحدِّد.
وينطبق الأمر نفسه عندما تعرض مكتبة مكوّنات عنصر div يتصرف كزر: أبلغ عن عيب غياب الدلالة بدلاً من كتابة محددات CSS للالتفاف عليه.
هذه فائدة جانبية لا اختبار لإمكانية الوصول. فنجاح المحددات المبنية على الدور لا يثبت أن الصفحة ميسَّرة؛ وتبقى فحوص لوحة المفاتيح وقارئ الشاشة والفحوص الآلية لإمكانية الوصول ضرورية.
مثال عملي: محددات نموذج إتمام الشراء
الترميز أدناه من صفحة إتمام شراء خيالية. لكل عنصر تعرض القائمة المحدِّد الموصى به، ومقابله في Selenium، وما يجب تجنبه.
- حقل البريد — getByLabel("Email"). في Selenium: By.name("email"). تجنّب #email-1293 (مولّد) وform > input:nth-of-type(1).
- مربع الموافقة على الشروط — getByRole("checkbox", { name: "I accept the terms" }). في Selenium: By.cssSelector("input[name=terms]").
- إجمالي الطلب — getByTestId("order-total")؛ نصه المرئي يتغير مع المبلغ، فالنص هو ما تتحقق منه لا ما تبحث به. في Selenium: By.cssSelector("[data-testid=order-total]").
- زر الإرسال — getByRole("button", { name: "Place order" }). في Selenium: XPath على النص بعد تطبيعه، أو معرّف اختبار إن أضافه الفريق. تجنّب .btn-primary و//form/button[1].
- النموذج نفسه — لا تستخدم #checkout-7f3a ولا .Checkout_form__x9Kp2. وإن احتجت إلى نطاق فامنح النموذج اسماً ميسَّراً واستخدم getByRole("form", { name: … })، أو أضف معرّف اختبار.
<form id="checkout-7f3a" class="Checkout_form__x9Kp2">
<label for="email-1293">Email</label>
<input id="email-1293" name="email" type="email">
<label><input type="checkbox" name="terms"> I accept the terms</label>
<div class="summary">
<span>Total</span> <span data-testid="order-total">199.20 SAR</span>
</div>
<button type="submit" class="btn btn-primary mt-4">Place order</button>
</form>قائمة مراجعة المحددات
- هل يطابق عنصراً واحداً بالضبط في الحالة التي يتوقعها الاختبار؟
- هل يصمد أمام إعادة التنسيق وإعادة ترتيب العناصر وتغيّر عمق DOM؟
- هل هو الدور/التسمية/النص أولاً، ثم معرّف الاختبار، ثم CSS، ثم XPath — وإن لم يكن كذلك فهل السبب مكتوب؟
- هل هو خالٍ من المعرّفات المولّدة والأصناف المجزّأة والفهارس والمسارات المطلقة؟
- هل يعيش في كائن صفحة أو مكوّن واحد لا في جسم الاختبار؟
- هل يعمل في كل لغة تعمل بها الحزمة؟
- لو فشل غداً، هل ستخبرك رسالة الخطأ أي عنصر كان مفقوداً؟
الأسئلة الشائعة
ما أفضل استراتيجية محددات لاختبارات الواجهة؟
ابحث عن العناصر كما يدركها المستخدمون — بالدور والاسم الميسَّر أو التسمية أو النص المرئي — ثم انتقل إلى معرّف اختبار مخصص، ثم محدِّد CSS قصير، واستخدم XPath فقط حين لا ينفع غيره. ويوصي Playwright وTesting Library بالجزء الأول منه (ما يراه المستخدم ثم معرّف الاختبار)، وبإضافة CSS ثم XPath يصلح الترتيب مع أي أداة.
هل XPath أبطأ من CSS؟
نادراً ما يكون الفرق مهماً في اختبارات الواجهة التي يهيمن عليها تحميل الصفحات واستدعاءات الشبكة. والسبب الحقيقي لتفضيل CSS أنه أقصر وأسهل قراءة؛ ويبقى XPath مفيداً للصعود إلى الآباء ومطابقة النص.
هل أستخدم data-testid أم getByRole؟
استخدم getByRole عندما يكون للعنصر دور واسم واضحان، لأنه يطابق ما يراه المستخدم ويكشف الأسماء الميسَّرة المفقودة. واستخدم معرّف الاختبار عندما يكون الدور والاسم غامضين أو مفقودين أو كثيري التغير، كالنصوص المترجمة أو العناصر المتكررة.
كيف أتعامل مع المعرّفات الديناميكية؟
اطلب نقطة ربط مستقرة (معرّف اختبار) أو استخدم محدِّداً مبنياً على ما يراه المستخدم. وإن اضطررت إلى مطابقة القيمة المولّدة فارتكز على جزئها المستقر بـ starts-with أو contains، وابنِ المحددات من بيانات الاختبار نفسه بدلاً من القيم الثابتة.
كيف أحدد عناصر داخل iframe؟
في Selenium ادخل إلى الإطار بـ switchTo().frame وعُد بـ switchTo().defaultContent. وفي Playwright استخدم frameLocator أو contentFrame للحصول على نطاق للمحددات داخل الإطار دون حاجة إلى التبديل.
لماذا يقول Playwright إن المحدِّد طابق عدة عناصر؟
محددات Playwright صارمة: يتطلب الإجراء تطابقاً واحداً بالضبط. اجعل المحدِّد أدق — أضف الاسم الميسَّر، أو حدّد نطاقه بمنطقة أو صف، أو استخدم filter — بدلاً من إضافة .first() التي تُخفي الغموض.
هل أستخدم المحددات المنسوخة من أدوات المطور في المتصفح؟
عاملها كنقطة بداية فقط. فـ «Copy XPath» و«Copy selector» تنتجان عادةً مسارات مطلقة أو محددات مبنية على الفهرس تنكسر مع تغييرات صغيرة في التخطيط؛ أعد كتابتها وفق ترتيب الأولوية قبل اعتمادها.
المراجع
- Locators — Playwrightكيف يعثر Playwright على العناصر، ولماذا تُعاد تحديد المحدِّدات وتنتظر تلقائياً، ولماذا يُوصى بالمحدِّدات المبنية على الدور أولاً.
- About Queries — Testing Libraryأنواع الاستعلامات وأولويتها — الدور والتسمية قبل النص، ومعرّفات الاختبار أخيراً — لتعثر الاختبارات على العناصر كما يفعل المستخدمون والتقنيات المساعدة.
- Locator strategies — Seleniumأنواع المحدِّدات التي يدعمها WebDriver (المعرّف والاسم ومحدِّد CSS وXPath ونص الرابط وغيرها) وكيف يُختار بينها.
- Best Practices — Cypressإرشادات Cypress لاختيار العناصر بسمات مخصّصة، وتجنّب الانتظار الثابت، وإبقاء الاختبارات مستقلة.
- Best Practices — Playwrightاختبر السلوك الذي يراه المستخدم، واجعل الاختبارات معزولة، وفضّل المحدِّدات المتينة والتأكيدات التي تنتظر، واستخدم التتبّع لتصحيح الأخطاء.
- Accessible Name and Description Computation 1.2كيف تحسب المتصفحات الاسم والوصف الميسَّرين لعنصرٍ من التسميات وسمات ARIA والمحتوى (مسودة عمل من W3C).
- Accessible Rich Internet Applications (WAI-ARIA) 1.2توصية W3C التي تعرّف أدوار ARIA وحالاتها وخصائصها — أي كيف تُعلن عناصر الواجهة المخصّصة معناها للتقنيات المساعدة.
تمرين عملي
راجع المحددات في طلب دمج
أنت مراجع الأتمتة في فريق متجر إلكترونيات على الويب (مثال خيالي) بواجهة إنجليزية وأخرى عربية. فتح زميلك طلب الدمج (Pull request) رقم 412 بأول اختبارات للواجهة في صفحة «طلباتي»: ترشيح القائمة، وتتبّع طلب وإلغاؤه، وتنزيل فاتورة، وفتح محادثة الدعم. والمحددات (Locators) الثمانية التي تستخدمها هذه الاختبارات مذكورة في المادة أدناه مع ترميز الصفحة مختصراً. سبعة من المحددات الثمانية تنجح على جهاز الكاتب مقابل النسخة الإنجليزية، أما L8 فتنتهي مهلته، ويطلب الكاتب المساعدة فيه.
راجع المحددات على الورق مستعيناً بترتيب الأولوية في الدليل، والأنماط الخاطئة، وقائمة المراجعة. اكتب كل ملاحظة كما تكتبها في طلب الدمج: ما الذي يكسر المحدِّد، وما البديل، وما الذي يجب أن يتغير في الترميز لا في الاختبار. خصّص لذلك 20–30 دقيقة.
متجر إلكترونيات على الويب (مثال خيالي) - واجهة إنجليزية وعربية
صفحة «طلباتي»، ترميز النسخة الإنجليزية مختصراً. حزمة اختبارات الواجهة الليلية:
Playwright (TypeScript)، وتعمل باللغتين.
قيم مولّدة عند البناء: المعرّفات مثل search-5521 وrow-7f21c تتغير مع كل نشر؛
والأصناف مثل Filters_bar__9kQ2z تأتي من CSS modules.
زر الأيقونة في كل صف ينزّل فاتورة الطلب (اسمه في التصميم «Download invoice»).
الواجهة العربية: Status = «الحالة»، Apply = «تطبيق»، Track = «تتبّع»، Cancel = «إلغاء»،
Processing = «قيد المعالجة»، Download invoice = «تنزيل الفاتورة»،
Search orders = «البحث في الطلبات»، Support chat = «محادثة الدعم».
<main>
<h1>My orders</h1>
<form class="Filters_bar__9kQ2z">
<input id="search-5521" name="q" type="search" placeholder="Search orders">
<label for="status-5522">Status</label>
<select id="status-5522" name="status">
<option value="all">All</option>
<option value="open">Open</option>
<option value="delivered">Delivered</option>
</select>
<button type="submit" class="btn btn-outline">Apply</button>
</form>
<table class="Table_root__x1Ab9">
<thead>
<tr><th>Order</th><th>Date</th><th>Total</th><th>Status</th><th>Actions</th></tr>
</thead>
<tbody>
<tr id="row-7f21c" class="Table_row__2kX9q">
<td>ORD-2026-004417</td><td>12 Sep 2026</td><td>249.00 SAR</td>
<td><span class="badge badge-green">Delivered</span></td>
<td>
<button type="button" class="btn btn-link">Track</button>
<button type="button" class="btn-icon"><svg aria-hidden="true"></svg></button>
</td>
</tr>
<tr id="row-7f21d" class="Table_row__2kX9q">
<td>ORD-2026-004431</td><td>28 Sep 2026</td><td>89.50 SAR</td>
<td><span class="badge badge-amber">Processing</span></td>
<td>
<button type="button" class="btn btn-link">Track</button>
<button type="button" class="btn btn-link">Cancel</button>
<button type="button" class="btn-icon"><svg aria-hidden="true"></svg></button>
</td>
</tr>
</tbody>
</table>
<iframe title="Support chat" src="https://chat.example.com/widget"></iframe>
</main>
طلب الدمج رقم 412 «orders.spec: أول اختبارات واجهة لصفحة طلباتي» - محدداته الثمانية
L1 page.locator('#search-5521') // حقل البحث
L2 page.locator('.Filters_bar__9kQ2z select') // مرشّح الحالة
L3 page.locator('.btn-outline') // زر «Apply»
L4 page.locator('xpath=/html/body/main/table/tbody/tr[2]/td[5]/button[2]')
// «Cancel»، ORD-2026-004431
L5 page.getByText('Track').first() // «Track»، ORD-2026-004417
L6 page.locator('.badge-amber') // الحالة، ORD-2026-004431
L7 page.locator('tr:nth-child(2) .btn-icon') // زر الفاتورة، ORD-2026-004431
L8 page.getByRole('button', { name: 'Start chat' }) // داخل أداة المحادثة
ملاحظة الكاتب: L1-L7 تنجح على جهازي (النسخة الإنجليزية). L8 تنتهي مهلته، أحتاج مساعدة.- لكل محدِّد من L1–L8 اكتب ما الذي سيكسره أو لماذا يفشل، مسمّياً السبب من الدليل (قيمة مولّدة، أو صنف تنسيق، أو XPath مطلق، أو فهرس، أو مطابقة نص واسعة جداً، أو نص بلغة خاطئة، أو إطار iframe). وبيّن أيّها ينجح اليوم رغم ذلك.
- أعد كتابة كل محدِّد وفق ترتيب الأولوية بصيغة Playwright: حدّد نطاق العناصر المتكررة بصف الطلب الذي أنشأ الاختبار رقمه بنفسه، واجعل النتيجة تعمل في التشغيل بالواجهة العربية أيضاً.
- حدّد المواضع التي لا يكون فيها المحدِّد هو الإصلاح الصحيح: ما الذي تطلب من المطورين إضافته (معرّف اختبار، أو تسمية، أو اسم ميسَّر)، وأيّ هذه الطلبات تبلّغ عنه كعيب.
- اكتب المقابل في Selenium (Java) لمرشّح الحالة ولزر «Cancel» الخاص بطلب معيّن.
- مرّر نتيجتك على قائمة مراجعة الدليل: التفرّد، واللغة، وهل تذكر رسالة الفشل العنصر المفقود.
إظهار الإجابة النموذجية
L1–L3، نموذج الترشيح. المحددات من L1 إلى L7 كلها تنجح اليوم كما يقول الكاتب، والمراجعة تبحث فيما سيكسرها. المحدِّد L1 #search-5521 معرّف مولّد يتغير مع كل نشر، فينجح الاختبار اليوم ويفشل بعد الإصدار التالي. وليس للحقل عنصر <label>، واسمه الميسَّر الوحيد يأتي من نص التلميح: فالمتصفحات تستخدم placeholder اسماً أخيراً حين لا يسمّي شيءٌ آخر حقلَ النص، وPlaywright يتبع القاعدة نفسها. لذا يجده page.getByRole("searchbox", { name: "Search orders" }) اليوم، وكذلك page.getByPlaceholder("Search orders")، وهو المستوى 3 في ترتيب الدليل. وفي الحالتين يرتكز الاختبار على نص تلميح يختفي بمجرد أن يكتب المستخدم، وهو ما يسمّيه الدليل بديلاً ضعيفاً عن التسمية: استخدم محدِّد الدور واطلب من المطورين تسمية مرئية (ويبقى المحدِّد صالحاً إن كان نص التسمية «Search orders»). والمحدِّد L2 .Filters_bar__9kQ2z select ينسخ صنفاً من CSS modules تتغير قيمة تجزئته مع كل بناء؛ وللقائمة تسمية، فالمحدِّد هو page.getByLabel("Status"). والمحدِّد L3 .btn-outline صنف تنسيق يختفي عند أول إعادة تنسيق، مع أنه يطابق عنصراً واحداً بالضبط اليوم؛ استخدم page.getByRole("button", { name: "Apply" }).
L4 وL5، صفوف الجدول. المحدِّد L4 مسار XPath مطلق ومعتمد على الفهرس مرتين: tr[2] يتوقف على ترتيب الفرز وعلى الطلبات الموجودة في الحساب، وbutton[2] هو «Cancel» فقط في الصفوف التي فيها زر إلغاء؛ ففي صف الطلب المُسلَّم يكون الزر الثاني أيقونة الفاتورة، وبذلك يلغي تغيّرُ الترتيب الطلبَ الخطأ أو ينقر الزرَّ الخطأ بصمت. وأمر «Copy full XPath» في أدوات المطور في Chrome ينتج هذا المسار بالضبط؛ أما «Copy XPath» فيرتكز بدلاً منه على معرّف الصف المولّد (//*[@id="row-7f21d"]/td[5]/button[2])، وهذا ليس أفضل. أعد الكتابة بتحديد نطاق الصف بمحتواه ثم البحث عن الزر بالدور: const row = page.getByRole("row").filter({ hasText: orderNumber }) ثم row.getByRole("button", { name: "Cancel" })، حيث orderNumber هو القيمة التي أنشأتها تهيئة الاختبار نفسه، لا قيمة ثابتة مثل ORD-2026-004431. والمحدِّد L5 getByText("Track").first(): يطابق getByText("Track") زرّين، فلولا .first() لرفض الوضع الصارم (strict mode) في Playwright النقر؛ و.first() تُخفي الغموض بدلاً من حلّه، وتنقر أيّ طلب يأتي أولاً في الفرز. وفي التشغيل الليلي بالواجهة العربية يفشل تماماً، لأن نص الزر هناك «تتبّع». والإصلاح نفسه: row.getByRole("button", { name: "Track" }) على صف الطلب ORD-2026-004417.
L6 وL7، شارة الحالة وزر الأيقونة. المحدِّد L6 .badge-amber يحدد الحالة بصنف لونها: ينكسر عند أول إعادة تنسيق، وليس محدود النطاق بطلب معيّن (فطلب ثانٍ قيد المعالجة يجعله غامضاً)، واللون يتغير مع الحالة — فشارة الطلب المُسلَّم صنفها badge-green — لذا فبعد أن يلغي الاختبار الطلب لا يعود المحدِّد يطابق الشارة التي يريد التحقق منها، وتقول رسالة الفشل إن العنصر غير موجود بدلاً من أن تذكر الحالة التي ظهرت. ونص الحالة هو ما يتحقق منه الاختبار، لذا، كما في إجمالي الطلب في المثال العملي بالدليل، لا يجوز أن يكون هو ما يبحث به الاختبار: اطلب data-testid="order-status" على الشارة، واستخدمه داخل نطاق الصف فقط لأنه يتكرر مرة في كل صف، وتحقق بـ row.getByTestId("order-status") مع toHaveText(...) والحالة المتوقعة بلغة التشغيل. والمحدِّد L7 tr:nth-child(2) .btn-icon معتمد على الفهرس وعلى التنسيق، لكن المشكلة الأعمق في الترميز: الزر لا يحوي إلا أيقونة aria-hidden وليس له اسم ميسَّر، فلا يجده getByRole("button", { name: "Download invoice" }) ويعلنه قارئ الشاشة «زر» فقط. هذا عيب في إمكانية الوصول يجب الإبلاغ عنه؛ وبعد أن يضيف المطورون aria-label="Download invoice" (أو نصاً مخفياً بصرياً) يصبح المحدِّد row.getByRole("button", { name: "Download invoice" }). ولا تكتب محدِّد CSS للالتفاف على الاسم المفقود.
L8، أداة المحادثة. تنتهي مهلة page.getByRole("button", { name: "Start chat" }) لأن الزر داخل إطار iframe، ومحدِّد الصفحة الرئيسية لا يرى ما بداخل مستند آخر. يحتاج Playwright إلى نطاق للإطار: page.frameLocator("iframe[title='Support chat']").getByRole("button", { name: "Start chat" })؛ وسمة title نقطة ربط مستقرة يراها المستخدم للإطار نفسه. والأداة مستند لطرف ثالث، فاكتفِ باختبار أو اثنين يتفاعلان معها، ولا تتحقق من ترميزها الداخلي.
ما يتغير في الترميز لا في الاختبار. تذهب ثلاثة طلبات إلى المطورين، في طلب الدمج نفسه إن أمكن: تسمية <label> مرئية لحقل البحث؛ وaria-label="Download invoice" على زر الأيقونة؛ وdata-testid="order-status" على شارة الحالة (بالسمة التي اتفق عليها الفريق، مسمّاة حسب الغرض، ومستخدمة داخل الصف). الطلبان الأولان يُسجَّلان في المتتبّع عيبين في إمكانية الوصول، فالاختبار كشفهما فقط: زر الأيقونة بلا اسم ميسَّر أصلاً، وحقل البحث لا يسمّيه إلا نص تلميح يختفي حين يكتب المستخدم. ومعرّف الاختبار عقد، فيُراجَع حذفه من الآن كما يُراجَع تغيير في API. ولا يحتاج شيء آخر إلى معرّف اختبار: فلأزرار «Cancel» و«Track» و«Apply» وقائمة «Status» أدوار وأسماء واضحة، والدليل يقول ألا تُستبدل بمعرّفات اختبار.
التشغيل بالواجهة العربية. كل اسم تستخدمه المحددات مترجم: «Status» تصبح «الحالة»، و«Apply» «تطبيق»، و«Track» «تتبّع»، و«Cancel» «إلغاء»، و«Download invoice» «تنزيل الفاتورة»، ونص الحالة «Processing» «قيد المعالجة» كذلك. يقدّم الدليل طريقتين ويعدّ ثالثة نمطاً خاطئاً: الاحتفاظ بجدول نصوص واحد لكل لغة وقراءة الأسماء منه، أو استخدام معرّفات الاختبار للعناصر ذات النص المترجم؛ أما ما يفشل فهو نص إنجليزي ثابت في حزمة تعمل بالعربية أيضاً. والدور مع الاسم من جدول اللغة هو الخيار الافتراضي الأفضل هنا لأنه يتحقق أيضاً مما يراه المستخدم العربي؛ وdata-testid="order-cancel" مقبول إن سجّل الفريق السبب. وعنوان title للإطار ونص التلميح في حقل البحث مترجمان أيضاً، فمكانهما الجدول نفسه.
Selenium (Java). مرشّح الحالة: new Select(driver.findElement(By.name("status"))).selectByValue("open")؛ فالسمة name وقيمة الخيار مستقرتان وغير مترجمتين، فيعمل السطر نفسه باللغتين، أما By.id("status-5522") فمولّد. زر «Cancel» لطلب معيّن: لا يملك Selenium محدِّداً للدور، فارتكز بمسار XPath نسبي على محتوى الصف ونص الزر: By.xpath("//tr[td[normalize-space()='" + orderNumber + "']]//button[normalize-space()='" + cancelLabel + "']")، حيث تُقرأ cancelLabel من جدول النصوص («Cancel» أو «إلغاء»)؛ ومع معرّف اختبار على الزر يصبح //tr[td[normalize-space()='…']]//*[@data-testid='order-cancel']. وكلاهما نسبي ومرتكز على بيانات يملكها الاختبار ومقروء، ودالة في كائن الصفحة مثل cancelOrder(orderNumber) تُبقيهما في مكان واحد.
قائمة المراجعة على النتيجة. التفرّد: كل محدِّد معاد كتابته يطابق عنصراً واحداً في الحالة المتوقعة؛ ونطاق الصف هو ما يجعل «Cancel» و«Track» وشارة الحالة فريدة، والوضع الصارم في Playwright سيخبرك إن لم تكن كذلك. الصمود أمام إعادة التنسيق وإعادة الفرز وعنصر div مغلِّف: نعم، لأن شيئاً لا يشير إلى أصناف أو فهارس أو عمق. ترتيب الأولوية: الدور أو التسمية في كل مكان، ومعرّف اختبار واحد بسبب مكتوب (نص الحالة يُتحقق منه ولا يُبحث به)، وXPath في Selenium فقط حيث لا يوجد محدِّد للدور؛ واسم حقل البحث يرتكز على نص التلميح إلى أن تُضاف التسمية، وتعليق المراجعة يذكر ذلك. اللغة: كل اسم يراه المستخدم يُقرأ من جدول النصوص الخاص بكل لغة، فيستخدم التشغيل بالعربية المحددات نفسها. العيش في كائن صفحة واحد: OrdersPage.row(orderNumber) وcancel(orderNumber) وما شابه، لا في جسم الاختبار. رسائل الخطأ: عندما يفشل row.getByRole("button", { name: "Cancel" }) يذكر Playwright مرشّح الصف وزر «Cancel» المفقود؛ وعندما يفشل /html/body/main/table/tbody/tr[2]/td[5]/button[2] يذكر مساراً لا يعرف أحد أيّ عنصر كان يقصده.
إن اختلفت إجاباتك فراجعها بسؤالي الدليل: هل يتغير المحدِّد فقط عندما يتغير السلوك الذي يعتمد عليه المستخدم؟ وهل يطابق عنصراً واحداً في الحالة المتوقعة؟ والاختيار المختلف مع سبب مسجَّل مقبول، مثل getByPlaceholder("Search orders") إلى أن تُضاف التسمية، أو معرّف اختبار على «Cancel» للحزمة ثنائية اللغة، أو getByRole("combobox", { name: "Status" }) بدلاً من getByLabel؛ أما المعرّف المولّد أو صنف التنسيق أو الفهرس أو المسار المطلق فغير مقبول، وكذلك المحدِّد الذي يلتف على اسم ميسَّر مفقود.
تقدّمك في هذا الموضوع
سجّل الدخول لتتابع تقدّمك
علّم الموضوعات مكتملة، وتابع من حيث توقفت، وافتح امتحانات التخصص.
جزء من مسار تعلم ضمان الجودة
قبل هذا الموضوع
الموضوعات التالية
الأدوات المذكورة في هذا الدليل
تجميل HTML بمسافات بادئة صحيحة للعناصر المتداخلة، مع الحفاظ على كتل pre وscript وstyle.
اختبر التعابير النمطية مباشرةً مع إبراز التطابقات ومجموعات الالتقاط ومعاينة الاستبدال.
قارن نصّين سطراً بسطر أو كلمةً بكلمة مع إبراز الاختلافات.