{"id":"l-tj-4-1","courseId":"testing-javascript","lang":"ar","kind":"lesson","summary":"تُعدّ Playwright لتطبيق Vite، وتجد العناصر بـ locators موجّهة للمستخدم، وتكتب assertions موجّهة للويب تنتظر الصفحة بدل أن تتسابق معها.","html":"<p>كل ما سبق عمل في Node، مع jsdom الذي يتظاهر بأنه متصفّح. هذا يغطّي معظم المنطق ومعظم سلوك الواجهة، لكن ليس كله: التخطيط الحقيقي، والتوجيه الحقيقي، والخادم الحقيقي، وخطأ CSS حقيقي يُخفي زر “Settle up” على Safari. وللمسارات القليلة التي يهمّ فيها ذلك، تقود متصفّحًا حقيقيًا. وفي 2026 الأداة الافتراضية لذلك في JavaScript هي Playwright.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">الإعداد</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> init</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> playwright@latest</span></span></code></pre></div>\n<p>يضيف المعالج <code dir=\"ltr\">@playwright/test</code>، وملف <code dir=\"ltr\">playwright.config.ts</code>، واختبارًا نموذجيًا، و(اختياريًا) workflow لـ GitHub Actions، وينزّل المتصفّحات. وجّهه إلى تطبيق Vite لديك ودعه يشغّل خادم التطوير نيابةً عنك:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">playwright.config.ts</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { defineConfig, devices } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '@playwright/test'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> default</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> defineConfig</span><span style=\"color:#953800;--shiki-dark:#FFA657\">({</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  testDir: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'./e2e'</span><span style=\"color:#953800;--shiki-dark:#FFA657\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  fullyParallel: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#953800;--shiki-dark:#FFA657\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  retries: process.env.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">CI</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#953800;--shiki-dark:#FFA657\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  use: {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    baseURL: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'http://localhost:5173'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    trace: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'on-first-retry'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span><span style=\"color:#953800;--shiki-dark:#FFA657\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  projects: [</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    { name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'chromium'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, use: { </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">devices[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Desktop Chrome'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] } },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    { name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'webkit'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, use: { </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">devices[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Desktop Safari'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] } },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  ]</span><span style=\"color:#953800;--shiki-dark:#FFA657\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  webServer: {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    command: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'npm run dev'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    url: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'http://localhost:5173'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    reuseExistingServer: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">process.env.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">CI</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span><span style=\"color:#953800;--shiki-dark:#FFA657\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">})</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span></code></pre></div>\n<p>يشغّل <code dir=\"ltr\">webServer</code> الأمر <code dir=\"ltr\">npm run dev</code>، وينتظر حتى يستجيب الـ URL، ثم يشغّل الاختبارات، ثم يوقفه. ويتيح <code dir=\"ltr\">baseURL</code> للاختبارات استدعاء <code dir=\"ltr\">page.goto('/groups/lisbon')</code>. شغّل المجموعة بـ <code dir=\"ltr\">npx playwright test</code>.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">أول اختبار end-to-end</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">e2e/settle-up.spec.ts</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { test, expect } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '@playwright/test'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'adding an expense updates the balances'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">page</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">goto</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'/groups/lisbon'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByLabel</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Description'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fill</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Dinner'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByLabel</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Amount'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fill</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'30.00'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByRole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'button'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Add expense'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">click</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> balances</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByRole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'list'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Balances'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(balances.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByRole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'listitem'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveText</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">([</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">    'Ana is owed $15.00'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">    'Ben owes $15.00'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  ]);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p><code dir=\"ltr\">page</code> تبويب متصفّح جديد، يُعطى للاختبار على شكل <strong>fixture</strong> (تجهيزة اختبار؛ المزيد عنها في الدرس القادم). يُفترض أن تبدو الـ locators (محدِّدات العناصر) مألوفة: <code dir=\"ltr\">getByRole</code> و<code dir=\"ltr\">getByLabel</code> و<code dir=\"ltr\">getByText</code> و<code dir=\"ltr\">getByPlaceholder</code> و<code dir=\"ltr\">getByAltText</code> و<code dir=\"ltr\">getByTitle</code> و<code dir=\"ltr\">getByTestId</code> في Playwright تتبع فكرة الأولوية نفسها في Testing Library، وللسبب نفسه.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">الـ locators كسولة وصارمة</h2>\n<p>الـ locator <strong>وصف</strong> لطريقة إيجاد عنصر، لا العنصر نفسه. لا يجد <code dir=\"ltr\">page.getByRole('button', { name: 'Add expense' })</code> شيئًا حتى تتعامل معه، ويجده من جديد في كل مرة، فلا تبقى ممسكًا بمرجع قديم حين تستبدل إعادة الـ render الزر.</p>\n<p>والـ locators أيضًا <strong>صارمة</strong>: إن طابق locator فعلٍ ما عنصرين، ترمي Playwright خطأً بدل أن تنقر الأول. ضيّقه بالتسلسل وبـ <code dir=\"ltr\">filter</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> taxiRow</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByRole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'listitem'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">filter</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ hasText: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Taxi'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> taxiRow.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByRole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'button'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Remove'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">click</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span></code></pre></div>\n<p>هذا يُقرأ كالتعليمات التي ستعطيها لإنسان (“احذف صف التاكسي”)، ويصمد أمام إعادة الترتيب. توجد <code dir=\"ltr\">nth(2)</code> و<code dir=\"ltr\">first()</code>، لكنهما تنكسران لحظة تغيّر الترتيب؛ فلا تستخدمهما إلا حين <em>يكون</em> الترتيب هو المقصود.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">الانتظار التلقائي: الأفعال</h2>\n<p>قبل <code dir=\"ltr\">click()</code>، تنتظر Playwright حتى يصبح العنصر <strong>ظاهرًا</strong>، و<strong>مستقرًا</strong> (لا في منتصف حركة)، و<strong>مفعّلًا</strong>، و<strong>يستقبل الأحداث</strong> فعلًا (غير مغطّى بمؤشّر تحميل أو بشريط ملفات تعريف الارتباط). وتنتظر <code dir=\"ltr\">fill()</code> أن يكون ظاهرًا ومفعّلًا وقابلًا للتحرير. ثم تنفّذ. وإن لم تتحقّق الشروط أبدًا، يفشل الفعل بعد انتهاء المهلة برسالة تذكر أيّ فحص فشل.</p>\n<p>لهذا نادرًا ما تحتاج اختبارات end-to-end في Playwright إلى انتظارات صريحة. فالزر الذي يظهر بعد تحميل المجموعة يُنقر حين يظهر.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 230\" role=\"img\" aria-labelledby=\"l-tj-4-1-t1\"><defs><marker id=\"arrow-l-tj-4-1-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-tj-4-1-t1\">في الأعلى: ينتظر click أن يكون العنصر ظاهرًا ومستقرًا ويستقبل الأحداث ومفعّلًا، ثم ينقر. في الأسفل: يعيد toHaveText قراءة العنصر مرارًا حتى يتطابق النص أو تمرّ 5 ثوانٍ.</title>\n  <text class=\"d-code\" x=\"20\" y=\"44\">click()</text>\n  <rect class=\"d-box-accent\" x=\"110\" y=\"24\" width=\"80\" height=\"32\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"150\" y=\"45\" text-anchor=\"middle\">ظاهر</text>\n  <rect class=\"d-box-accent\" x=\"200\" y=\"24\" width=\"80\" height=\"32\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"240\" y=\"45\" text-anchor=\"middle\">مستقر</text>\n  <rect class=\"d-box-accent\" x=\"290\" y=\"24\" width=\"130\" height=\"32\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"355\" y=\"45\" text-anchor=\"middle\">يستقبل الأحداث</text>\n  <rect class=\"d-box-accent\" x=\"430\" y=\"24\" width=\"90\" height=\"32\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"475\" y=\"45\" text-anchor=\"middle\">مفعّل</text>\n  <path class=\"d-arrow\" d=\"M524 40 L566 40\" marker-end=\"url(#arrow-l-tj-4-1-1)\"/>\n  <rect class=\"d-box-success\" x=\"570\" y=\"24\" width=\"90\" height=\"32\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"615\" y=\"45\" text-anchor=\"middle\">نقر</text>\n  <text class=\"d-code\" x=\"20\" y=\"144\">toHaveText()</text>\n  <path class=\"d-line\" d=\"M150 140 L520 140\"/>\n  <circle class=\"d-dot\" cx=\"170\" cy=\"140\" r=\"6\"/>\n  <circle class=\"d-dot\" cx=\"240\" cy=\"140\" r=\"6\"/>\n  <circle class=\"d-dot\" cx=\"310\" cy=\"140\" r=\"6\"/>\n  <text class=\"d-label-muted\" x=\"240\" y=\"120\" text-anchor=\"middle\">إعادة قراءة، لا تطابق</text>\n  <rect class=\"d-box-success\" x=\"350\" y=\"124\" width=\"110\" height=\"32\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"405\" y=\"145\" text-anchor=\"middle\">تطابق</text>\n  <text class=\"d-label-muted\" x=\"520\" y=\"180\" text-anchor=\"middle\">يستسلم عند 5 s</text>\n  <path class=\"d-line d-dashed\" d=\"M520 110 L520 165\"/>\n</svg><figcaption>تنتظر الأفعال حتى يجهز العنصر؛ وتعيد الـ assertions المحاولة حتى تنجح</figcaption></figure>\n<h2 id=\"sec-5\" tabindex=\"-1\">الـ assertions الموجّهة للويب: assertions تنتظر</h2>\n<p>تنطبق الفكرة نفسها على فحص النتائج. <code dir=\"ltr\">expect</code> في Playwright حين يُستخدم على <strong>locator</strong> يكون غير متزامن ويعيد المحاولة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByRole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'status'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveText</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Everyone is settled up'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByRole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'listitem'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveCount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(page).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveURL</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/</span><span style=\"color:#116329;--shiki-light-font-weight:bold;--shiki-dark:#7EE787;--shiki-dark-font-weight:bold\">\\/</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">groups</span><span style=\"color:#116329;--shiki-light-font-weight:bold;--shiki-dark:#7EE787;--shiki-dark-font-weight:bold\">\\/</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">lisbon</span><span style=\"color:#116329;--shiki-light-font-weight:bold;--shiki-dark:#7EE787;--shiki-dark-font-weight:bold\">\\/</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">settled/</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByRole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'button'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Settle up'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> })).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBeDisabled</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span></code></pre></div>\n<p>كل واحد منها يعيد فحص الصفحة حتى يتطابق، لمدة تصل إلى 5 ثوانٍ افتراضيًا. و<code dir=\"ltr\">toHaveText</code> مع مصفوفة (كما في الاختبار الأول) يفحص نص كل عنصر مطابق، بالترتيب، وهذه طريقة موجزة لتأكيد قائمة كاملة.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>قراءة القيم ثم تأكيدها</span></p>\n<p>يبدو <code dir=\"ltr\">expect(await page.getByRole('status').textContent()).toBe('Everyone is settled up')</code> مكافئًا، لكنه ليس كذلك. يقرأ <code dir=\"ltr\">textContent()</code> مرة واحدة، الآن؛ و<code dir=\"ltr\">toBe</code> العام لا يعيد المحاولة أبدًا. إن تحدّثت الحالة بعد 50 ms، يفشل الاختبار، أحيانًا. وهذه “الأحيانًا” هي الطريقة التي تولد بها المجموعات المتقلّبة. أبقِ <code dir=\"ltr\">await</code> <strong>خارج</strong> <code dir=\"ltr\">expect</code>، على locator، كي يتولّى الـ assertion نفسه الانتظار.</p>\n</aside>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>دع Playwright تكتب المسوّدة الأولى</span></p>\n<p>يفتح <code dir=\"ltr\">npx playwright codegen localhost:5173</code> متصفّحًا ويسجّل نقراتك ككود اختبار، مختارًا locators مبنيّة على الأدوار حيثما استطاع. إنها طريقة سريعة لاكتشاف الـ locator المناسب لعنصر صعب؛ ثم عدّل النتيجة كي تؤكّد النتائج، لا كل نقرة.</p>\n</aside>\n<h2 id=\"sec-6\" tabindex=\"-1\">ما الذي ينتمي إلى اختبار end-to-end</h2>\n<p>لأن كل اختبار هنا يبدأ سياق متصفّح، ويحمّل التطبيق، ويتحدّث إلى خادم حقيقي، فإن اختبار الـ end-to-end يكلّف ثوانيَ حيث يكلّف الـ unit test أجزاءً من الثانية. أنفق هذه الميزانية على المسارات التي يكون فيها <em>الاتصال</em> بين القطع هو الخطر: تسجيل الدخول، وإضافة مصروف ورؤية الأرصدة تتغيّر، وتسوية الحسابات، وكل ما يتعلّق بالمال أو بفقدان البيانات. لا تستخدمها لفحص كل رسالة تحقّق في حقل المبلغ؛ فاختبارات المكوّنات من القسم 3 تفعل ذلك أسرع بمئة مرة. قد تحتوي مجموعة Splitwise-lite السليمة على خمسة إلى خمسة عشر اختبار end-to-end، وبضع مئات من اختبارات الوحدات والمكوّنات. وإن فشل اختبار end-to-end، فاسأل أولًا إن كان على اختبار في مستوى أدنى أن يلتقطه.</p>\n<p>يطلب منك التمرين اختيار الـ locator الذي سيصمد أمام إعادة التصميم القادمة. الدرس القادم: إبقاء الاختبارات مستقلّة بعضها عن بعض.</p>\n","toc":[{"id":"sec-1","text":"الإعداد","level":2},{"id":"sec-2","text":"أول اختبار end-to-end","level":2},{"id":"sec-3","text":"الـ locators كسولة وصارمة","level":2},{"id":"sec-4","text":"الانتظار التلقائي: الأفعال","level":2},{"id":"sec-5","text":"الـ assertions الموجّهة للويب: assertions تنتظر","level":2},{"id":"sec-6","text":"ما الذي ينتمي إلى اختبار end-to-end","level":2}],"takeaways":["تقود Playwright متصفّحات Chromium وFirefox وWebKit حقيقية، وخيارها <code dir=\"ltr\">webServer</code> يشغّل خادم التطوير لديك قبل أن تعمل الاختبارات.","فضّل الـ locators الموجّهة للمستخدم (<code dir=\"ltr\">getByRole</code> و<code dir=\"ltr\">getByLabel</code> و<code dir=\"ltr\">getByText</code>)، وضيّقها بـ <code dir=\"ltr\">filter</code> أو بالتسلسل بدل مسارات CSS.","تنتظر الأفعال مثل <code dir=\"ltr\">click</code> و<code dir=\"ltr\">fill</code> تلقائيًا حتى يصبح العنصر ظاهرًا، ومستقرًا، ومفعّلًا، وقادرًا على استقبال الأحداث.","الـ assertions الموجّهة للويب مثل <code dir=\"ltr\">await expect(locator).toHaveText(...)</code> تعيد المحاولة حتى تنجح أو تنتهي المهلة (5 ثوانٍ افتراضيًا)؛ أما قراءة قيمة ثم تأكيدها مرة واحدة فلا تفعل ذلك."],"further":[{"title":"Playwright — Locators","url":"https://playwright.dev/docs/locators"},{"title":"Playwright — Assertions","url":"https://playwright.dev/docs/test-assertions"},{"title":"Playwright — Auto-waiting","url":"https://playwright.dev/docs/actionability"},{"title":"Playwright — Web server","url":"https://playwright.dev/docs/test-webserver"}],"quiz":[{"q":"<p>أيّ assertion يظلّ يعمل حين يتحدّث الرصيد بعد 300 ms من النقر؟</p>","options":[{"text":"<code dir=\"ltr\">expect(await page.getByRole('status').textContent()).toBe('Ben owes $5.00')</code>","why":"هذا يقرأ النص مرة واحدة، فورًا؛ وإن لم يكن التحديث قد وصل بعد، يفشل. لا شيء يعيد المحاولة."},{"text":"<code dir=\"ltr\">await expect(page.getByRole('status')).toHaveText('Ben owes $5.00')</code>","why":"صحيح. الـ assertion الموجّه للويب يعيد قراءة العنصر حتى يتطابق النص أو تنتهي المهلة."},{"text":"<code dir=\"ltr\">await page.waitForTimeout(500); expect(await page.getByRole('status').isVisible()).toBe(true)</code>","why":"الانتظار هنا تخمين، والـ assertion لا يفحص النص أصلًا."}],"answer":1},{"q":"<p>يفشل <code dir=\"ltr\">await page.getByRole('button', { name: 'Remove' }).click()</code> بخطأ strict mode violation. ماذا يعني ذلك؟</p>","options":[{"text":"الزر معطّل، فترفض Playwright النقر.","why":"الزر المعطّل يجعل النقر ينتظر ثم تنتهي مهلته؛ أما الوضع الصارم (strict mode) فيتعلّق بعدد العناصر المطابقة."},{"text":"الاختبار يعمل في متصفّح بإعدادات أمان صارمة.","why":"الوضع الصارم قاعدة خاصة بالـ locators، لا إعداد في المتصفّح."},{"text":"الـ locator يطابق أكثر من عنصر، فلا تخمّن Playwright أيّها تتعامل معه.","why":"صحيح. ضيّقه، مثلًا <code dir=\"ltr\">page.getByRole('listitem').filter({ hasText: 'Taxi' }).getByRole('button', { name: 'Remove' })</code>."}],"answer":2},{"q":"<p>ما الذي تفحصه Playwright قبل أن تنفّذ <code dir=\"ltr\">click()</code> على locator؟</p>","options":[{"text":"أن العنصر ظاهر، ومستقر (لا يتحرّك)، ومفعّل، وغير مغطّى بعنصر آخر.","why":"صحيح. فحوص قابلية التنفيذ هذه هي ما يجعل معظم الانتظارات الصريحة غير ضرورية."},{"text":"أن العنصر موجود في الـ DOM فقط.","why":"الوجود لا يكفي؛ فالمستخدم لا يستطيع النقر على زر مخفيّ أو مغطّى، وPlaywright تنتظر حتى يصبح قابلًا للنقر."},{"text":"لا شيء؛ تنقر فورًا، وعلى الاختبار أن ينتظر قبل ذلك.","why":"هكذا كانت تعمل الأدوات القديمة؛ أما Playwright فتنتظر تلقائيًا قبل الأفعال."}],"answer":0}],"exercise":{"kind":"choice","multiple":false,"options":["css-path","nth","role-filter","text-only","test-id"],"correct":["role-filter"],"title":"اختر الـ locator الذي يصمد أمام إعادة التصميم","prompt":"<p>تُعرض قائمة المصروفات في Splitwise-lite هكذا:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">HTML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">ul</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-label</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Expenses\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"mt-4 space-y-2\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">li</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"row\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Dinner: $45.50 &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"btn btn-ghost\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Remove&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">li</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">li</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"row\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Taxi: $18.00 &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"btn btn-ghost\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> data-testid</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"remove-e2\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Remove&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">li</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">li</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"row\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Museum: $24.00 &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"btn btn-ghost\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Remove&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">li</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">ul</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>في الـ sprint القادم، ستحوّل مصمّمة القائمة إلى شبكة بطاقات، وتغيّر كل الأصناف، وترتّب المصروفات حسب التاريخ بدل ترتيب الإضافة. أيّ locator لـ <strong>زر Remove الخاص بمصروف Taxi</strong> هو الأرجح أن يظلّ يعمل، وألا يفشل إلا إن عجز المستخدمون فعلًا عن حذف التاكسي؟</p>\n","hints":["استبعد كل ما يعتمد على الأصناف، أو تداخل الـ DOM، أو الموقع: فالثلاثة على وشك التغيّر.","فضّل ما يدركه المستخدم: دور الزر واسمه، محصورًا في الصف الذي يذكر Taxi."],"explanation":"<p>يصف <code dir=\"ltr\">page.getByRole('listitem').filter({ hasText: 'Taxi' }).getByRole('button', { name: 'Remove' })</code> الهدف كما سيصفه إنسان: زر Remove في صف التاكسي. لا يهتمّ بالأصناف ولا بالتداخل ولا بالترتيب، فتتركه إعادة التصميم سليمًا، ويفشل إن توقّف الصف عن أن يكون عنصر قائمة فيه زر Remove يمكن الوصول إليه، وهذا انتكاس حقيقي لمستخدمي لوحة المفاتيح وقارئ الشاشة. وإن استخدمت إعادة التصميم بطاقات لم تعد عناصر قائمة، يفشل الاختبار بصوت عالٍ ويشير إلى تغيير حقيقي في إمكانية الوصول، وهذا يستحق نقاشًا. أما مسار CSS و<code dir=\"ltr\">nth(1)</code> فينكسران مع أول تغيير في الأصناف أو إعادة ترتيب؛ والـ test id يصمد أمام تغيير التنسيق لكنه لا يثبت شيئًا عمّا يستطيع المستخدمون رؤيته أو الوصول إليه، ولا يملكه إلا صف واحد.</p>\n","checkLabels":{},"optionText":{"css-path":{"text":"<code dir=\"ltr\">page.locator('ul.space-y-2 &gt; li.row:nth-child(2) &gt; button.btn-ghost')</code>","why":"يرمّز الأصناف والتداخل والموقع، وكلها ستغيّرها إعادة التصميم، فينكسر رغم أن حذف التاكسي ما زال يعمل."},"nth":{"text":"<code dir=\"ltr\">page.getByRole('button', { name: 'Remove' }).nth(1)</code>","why":"يعتمد على أن التاكسي في المرتبة الثانية. وبعد الترتيب حسب التاريخ سينقر بصمت زر Remove لمصروف آخر، وهذا أسوأ من الفشل."},"role-filter":{"text":"<code dir=\"ltr\">page.getByRole('listitem').filter({ hasText: 'Taxi' }).getByRole('button', { name: 'Remove' })</code>","why":"صحيح. يجد الزر بدوره واسمه داخل الصف الذي يذكر Taxi، فتتغيّر الأصناف والتخطيط والترتيب بحرّية."},"text-only":{"text":"<code dir=\"ltr\">page.getByText('Remove')</code>","why":"يطابق ثلاثة أزرار، فيفشل أي فعل عليه بخطأ strict mode violation، ولا يقول شيئًا عن المصروف الذي تقصده."},"test-id":{"text":"<code dir=\"ltr\">page.getByTestId('remove-e2')</code>","why":"يصمد أمام تغيير التنسيق، لكن الـ test ids غير مرئية للمستخدمين وهي الملاذ الأخير: قد يفقد الزر تسميته أو يُخفى عن التقنيات المساعدة ويظلّ هذا ينجح."}}},"words":639,"readMinutes":4}