{"id":"l-tj-4-3","courseId":"testing-javascript","lang":"ar","kind":"lesson","summary":"تتتبّع اختبارات Playwright الفاشلة باستخدام UI mode والـ inspector وعارض الـ trace، ثم تجد الأسباب المعتادة للتقلّب وتزيلها بدل إخفائها بإعادة المحاولة.","html":"<p>في عمل سابق كانت لدينا مجموعة end-to-end حمراء في تشغيلة من كل خمس تقريبًا. كان الجميع يعرف أيّ الاختبارات “تفعل ذلك دائمًا”، فكان الجميع يعيد تشغيل الـ pipeline. ثم في يوم جمعة وصل إلى المستخدمين خطأ حقيقي في الدفع، لأن اختباره الفاشل كان من تلك التي “تفعل ذلك دائمًا”. الـ flaky test (الاختبار المتقلّب) ليس إزعاجًا بسيطًا؛ بل يدرّب الفريق كله على تجاهل جرس الإنذار.</p>\n<p>لهذا الدرس نصفان: أدوات رؤية سبب فشل الاختبار، والقائمة القصيرة من الأسباب التي تقف خلف كل اختبار متقلّب تقريبًا.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">رؤية ما حدث</h2>\n<p><strong>UI mode</strong> (وضع الواجهة) هو المكان الذي أقضي فيه معظم وقت تتبّع الأخطاء محليًا:</p>\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\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> playwright</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> test</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --ui</span></span></code></pre></div>\n<p>يفتح نافذة تسرد كل اختبار. شغّل واحدًا فتحصل على خط زمني لأفعاله؛ وانقر أي خطوة لترى لقطة DOM للصفحة في تلك اللحظة، إضافة إلى رسائل الـ console وطلبات الشبكة وكود الاختبار. يراقب الملفات ويعيد التشغيل عند الحفظ. وفيه أيضًا منتقي locators: مرّر المؤشّر فوق اللقطة فيقترح عليك locator.</p>\n<p><strong>الـ inspector</strong> (المفتّش) يمشي في الاختبار فعلًا بعد فعل في متصفّح ظاهر:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"1\"><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\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> playwright</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> test</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> e2e/settle-up.spec.ts</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --debug</span></span></code></pre></div>\n<p>أضف <code dir=\"ltr\">await page.pause()</code> في اختبار للتوقّف عند نقطة محدّدة.</p>\n<p><strong>الـ traces</strong> (سجلّات التتبّع) للحالات التي فشلت دون أن تراها، وعادةً في CI. مع <code dir=\"ltr\">trace: 'on-first-retry'</code> في الإعدادات، تسجّل Playwright ملف trace عند أول إعادة محاولة لاختبار فاشل: كل فعل، ولقطة DOM قبله وبعده، ومخرجات الـ console، واستدعاءات الشبكة، وسطر الكود. نزّل التقرير من CI وافتح الـ trace:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"2\"><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\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> playwright</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> show-report</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">            # HTML report, with traces attached</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> playwright</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> show-trace</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> trace.zip</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">   # a single trace</span></span></code></pre></div>\n<p>في تسعين بالمئة من الحالات، تخبرك اللقطة عند الخطوة الفاشلة بالإجابة فورًا: مؤشّر تحميل يغطّي الزر، أو رسالة خطأ منبثقة، أو صفحة غير التي توقّعتها.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 160\" role=\"img\" aria-labelledby=\"l-tj-4-3-t1\"><defs><marker id=\"arrow-l-tj-4-3-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-3-t1\">أربع خطوات: يفشل الاختبار في CI، فتعيد Playwright محاولته وتسجّل trace، فتفتح الـ trace وتفحص اللقطة، ثم تصلح السبب وتتأكّد بـ repeat-each.</title>\n  <rect class=\"d-box-warn\" x=\"10\" y=\"40\" width=\"150\" height=\"70\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"85\" y=\"70\" text-anchor=\"middle\">يفشل في CI</text>\n  <text class=\"d-label-muted\" x=\"85\" y=\"94\" text-anchor=\"middle\">المحاولة الأولى</text>\n  <path class=\"d-arrow\" d=\"M160 75 L184 75\" marker-end=\"url(#arrow-l-tj-4-3-1)\"/>\n  <rect class=\"d-box-accent\" x=\"186\" y=\"40\" width=\"150\" height=\"70\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"261\" y=\"70\" text-anchor=\"middle\">إعادة + trace</text>\n  <text class=\"d-code\" x=\"261\" y=\"94\" text-anchor=\"middle\">on-first-retry</text>\n  <path class=\"d-arrow\" d=\"M336 75 L360 75\" marker-end=\"url(#arrow-l-tj-4-3-1)\"/>\n  <rect class=\"d-box-primary\" x=\"362\" y=\"40\" width=\"150\" height=\"70\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"437\" y=\"70\" text-anchor=\"middle\">اقرأ الـ trace</text>\n  <text class=\"d-code\" x=\"437\" y=\"94\" text-anchor=\"middle\">show-trace</text>\n  <path class=\"d-arrow\" d=\"M512 75 L536 75\" marker-end=\"url(#arrow-l-tj-4-3-1)\"/>\n  <rect class=\"d-box-success\" x=\"538\" y=\"40\" width=\"132\" height=\"70\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"604\" y=\"70\" text-anchor=\"middle\">أصلح وأثبت</text>\n  <text class=\"d-code\" x=\"604\" y=\"94\" text-anchor=\"middle\">--repeat-each</text>\n</svg><figcaption>من فشل في CI إلى إصلاح</figcaption></figure>\n<h2 id=\"sec-2\" tabindex=\"-1\">المشتبه بهم المعتادون</h2>\n<p>كل اختبار end-to-end متقلّب حذفته أو أصلحته تقريبًا كان له أحد هذه الأسباب.</p>\n<p><strong>1. التسابق مع الصفحة.</strong> الانتظار الثابت (<code dir=\"ltr\">page.waitForTimeout(2000)</code>)، أو قيم تُقرأ مرة واحدة وتُؤكَّد بـ matcher لا يعيد المحاولة (<code dir=\"ltr\">expect(await locator.textContent()).toBe(...)</code>، <code dir=\"ltr\">expect(await locator.count()).toBe(3)</code>)، أو فروع <code dir=\"ltr\">if (await locator.isVisible())</code> تفحص في لحظة واحدة عشوائية. الإصلاح: assertions موجّهة للويب (<code dir=\"ltr\">toHaveText</code> و<code dir=\"ltr\">toHaveCount</code> و<code dir=\"ltr\">toBeVisible</code>) وأفعال تنتظر تلقائيًا.</p>\n<p><strong>2. البيانات المشتركة.</strong> اختبارات تستخدم المجموعة نفسها، أو المستخدم نفسه، أو السجلّات الأولية نفسها، ولا تنجح إلا بترتيب معيّن أو مع عامل واحد. الإصلاح: كل اختبار ينشئ بياناته الخاصة، بأسماء فريدة، عبر الـ fixtures.</p>\n<p><strong>3. الوقت والعشوائية غير المتحكَّم فيهما.</strong> منطق “هذا الأسبوع” الذي يفشل في أيام الاثنين، ومناطق زمنية تختلف بين حاسوبك وخادم CI، ورسوم متحرّكة ما زالت تعمل، ومعرّفات عشوائية في الـ assertions. الإصلاح: <code dir=\"ltr\">page.clock</code> لضبط الوقت (<code dir=\"ltr\">await page.clock.setFixedTime(new Date('2026-03-04T10:00:00'))</code>)، و<code dir=\"ltr\">timezoneId</code> ثابت في كتلة <code dir=\"ltr\">use</code> في الإعدادات، وassertions لا تعتمد على قيم مولّدة.</p>\n<p><strong>4. <code dir=\"ltr\">await</code> المفقودة.</strong> <code dir=\"ltr\">page.getByRole('button', { name: 'Add expense' }).click()</code> بلا <code dir=\"ltr\">await</code> يعود فورًا؛ فيتسابق السطر التالي مع النقر. الإصلاح: قاعدة الـ lint <code dir=\"ltr\">@typescript-eslint/no-floating-promises</code> تلتقط هذه الحالات لحظة الكتابة.</p>\n<p>السكربتات الخارجية (التحليلات، وأدوات الدردشة، وإطارات iframe الخاصة بالدفع) سبب خامس، خارجي: احجبها بـ <code dir=\"ltr\">page.route</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\">retries: 2</code> في CI معقول كشبكة أمان: تعلّم Playwright الاختبار الذي فشل ثم نجح بأنه <strong>flaky</strong>، فتعرف بأمره. أما رفع إعادة المحاولة إلى 5 وتجاهل قائمة الاختبارات المتقلّبة فيحوّل شبكة الأمان إلى عصابة على العينين. فالأخطاء المتقطّعة الحقيقية (سباق في كودك <em>أنت</em>، لا في الاختبار) تضيع هي الأخرى بإعادة المحاولة.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">إثبات الإصلاح</h2>\n<p>الاختبار المتقلّب الذي نجح مرة واحدة لا يثبت شيئًا. أعد إنتاج المشكلة أولًا، ثم أثبت الإصلاح تحت الضغط نفسه:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"3\"><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\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> playwright</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> test</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> e2e/settle-up.spec.ts</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --repeat-each=30</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --workers=4</span></span></code></pre></div>\n<p>إن فشل بضع مرات من 30 قبل الإصلاح وصفرًا بعده، فقد انتهيت. وفي CI، يحوّل <code dir=\"ltr\">--fail-on-flaky-tests</code> وسم flaky إلى فشل، وتفعّله بعض الفرق بعد أن تصبح المجموعة نظيفة كي لا يتسلّل تقلّب جديد بهدوء.</p>\n<p>يعرض التمرين اختبارًا من مجموعة تبدو حقيقية فيه عدة مشكلات من هذه. جدها كلها. وبهذا يُغلق قسم الـ end-to-end؛ ويتراجع القسم 5 خطوة إلى الوراء ليسأل كيف تحكم على مجموعة اختبارات ككل.</p>\n","toc":[{"id":"sec-1","text":"رؤية ما حدث","level":2},{"id":"sec-2","text":"المشتبه بهم المعتادون","level":2},{"id":"sec-3","text":"إثبات الإصلاح","level":2}],"takeaways":["وضع الواجهة UI mode (<code dir=\"ltr\">npx playwright test --ui</code>) هو أسرع حلقة عمل محلية: وضع مراقبة، وخط زمني لكل فعل، ولقطة DOM عند كل خطوة.","مع <code dir=\"ltr\">trace: 'on-first-retry'</code>، يترك الاختبار الذي يفشل في CI ملف trace تستطيع فتحه بـ <code dir=\"ltr\">npx playwright show-trace</code> لإعادة تشغيله خطوة بخطوة.","معظم التقلّب يأتي من أربعة أسباب: الانتظار الثابت أو الـ assertions التي لا تعيد المحاولة، والبيانات المشتركة بين الاختبارات، والوقت أو العشوائية غير المتحكَّم فيهما، و<code dir=\"ltr\">await</code> المفقودة.","أعد إنتاج التقلّب بـ <code dir=\"ltr\">--repeat-each</code>، وأصلح السبب، وتعامل مع الاختبارات الموسومة بـ flaky كأخطاء؛ فإعادة المحاولة كاشف دخان، لا إصلاح."],"further":[{"title":"Playwright — Trace viewer","url":"https://playwright.dev/docs/trace-viewer"},{"title":"Playwright — UI Mode","url":"https://playwright.dev/docs/test-ui-mode"},{"title":"Playwright — Debugging tests","url":"https://playwright.dev/docs/debug"},{"title":"Playwright — Clock","url":"https://playwright.dev/docs/clock"}],"quiz":[{"q":"<p>فشل اختبار مرة واحدة في CI ونجح عند إعادة المحاولة. إعداداتك تحتوي <code dir=\"ltr\">trace: 'on-first-retry'</code>. ما أسرع طريقة لرؤية شكل الصفحة لحظة الفشل؟</p>","options":[{"text":"أعد تشغيل مهمّة CI حتى يفشل مرة أخرى، وراقب السجلّات.","why":"قد تنتظر تشغيلات كثيرة، والسجلّات لا تُظهر الصفحة؛ والدليل من الفشل الأول موجود أصلًا."},{"text":"أضف استدعاءات <code dir=\"ltr\">console.log</code> حول كل فعل وادفع الكود مرة أخرى.","why":"التسجيل تخمين بطيء مقارنة بـ trace سجّل كل فعل ولقطة وطلب."},{"text":"نزّل artifact التقرير وافتح الـ trace بـ <code dir=\"ltr\">npx playwright show-trace</code> (أو بتقرير HTML).","why":"صحيح. يحتوي الـ trace لقطات DOM قبل كل فعل وبعده، ومخرجات الـ console، واستدعاءات الشبكة، فترى بالضبط ما رآه الاختبار."}],"answer":2},{"q":"<p>أيّ تغيير يصلح اختبارًا يفشل أحيانًا عند <code dir=\"ltr\">expect(await page.getByRole('listitem').count()).toBe(3)</code>؟</p>","options":[{"text":"<code dir=\"ltr\">await expect(page.getByRole('listitem')).toHaveCount(3)</code>","why":"صحيح. يعيد الـ assertion الموجّه للويب العدّ حتى تصبح العناصر ثلاثة أو تنتهي المهلة؛ أما <code dir=\"ltr\">count()</code> فيقرأ مرة واحدة."},{"text":"أضف <code dir=\"ltr\">await page.waitForTimeout(1000)</code> قبل السطر.","why":"الانتظار يجعل الاختبار أبطأ في كل مكان، ويظلّ يفشل حين يكون CI أبطأ من ثانية."},{"text":"اضبط <code dir=\"ltr\">retries</code> على 3 في الإعدادات.","why":"إعادة المحاولة تُخفي السباق بدل أن تزيله؛ سيظلّ الاختبار يفشل أحيانًا، لكن ببطء أكبر الآن."}],"answer":0},{"q":"<p>يتحقّق اختبار من أن مصروفًا أُضيف اليوم يظهر تحت “This week”. يفشل كل يوم اثنين في الساعة 00:30 في CI. ما الإصلاح الأمتن؟</p>","options":[{"text":"تخطَّ الاختبار في أيام الاثنين.","why":"هذا يُخفي سلوكًا حقيقيًا تريد تغطيته، ويترك الاعتماد على الوقت كما هو."},{"text":"تحكّم في الوقت بـ <code dir=\"ltr\">page.clock</code> (مثلًا <code dir=\"ltr\">setFixedTime</code>) كي يعمل الاختبار دائمًا في تاريخ معروف.","why":"صحيح. تثبيت الساعة يجعل النتيجة مستقلّة عن وقت تشغيل المجموعة؛ ويمكنك أيضًا إضافة اختبار لحدّ يوم الاثنين عن قصد."},{"text":"شغّل CI خلال ساعات العمل فقط.","why":"هذا ينقل الفشل إلى من يشغّل المجموعة ليلًا أو في منطقة زمنية أخرى."}],"answer":1},{"q":"<p>تبلّغ Playwright عن اختبار بأنه “flaky”. ماذا يعني هذا الوسم؟</p>","options":[{"text":"الاختبار بطيء وقريب من مهلته.","why":"البطء وحده ليس تقلّبًا؛ تستخدم Playwright وسمًا منفصلًا هو “slow” لذلك."},{"text":"تُخطّي الاختبار لأن المتصفّح انهار.","why":"الانهيارات تُنتج حالات فشل أو انقطاع، لا وسم flaky."},{"text":"فشل مرة واحدة على الأقل ثم نجح عند إعادة المحاولة ضمن التشغيل نفسه.","why":"صحيح. مع تفعيل إعادة المحاولة، تميّز Playwright بين “passed” و&quot;failed&quot; و&quot;flaky&quot;، فتستطيع إيجادها وإصلاحها."}],"answer":2}],"exercise":{"kind":"spot-bug","language":"ts","code":"import { test, expect } from '@playwright/test';\n\ntest('settling up clears the balances', async ({ page }) => {\n  await page.goto('/groups/lisbon-trip');\n  await page.getByLabel('Description').fill('Dinner');\n  await page.getByLabel('Amount').fill('30.00');\n  await page.getByRole('button', { name: 'Add expense' }).click();\n  page.getByRole('button', { name: 'Settle up' }).click();\n  await page.waitForTimeout(2000);\n  const status = await page.getByRole('status').textContent();\n  expect(status).toBe('Everyone is settled up');\n  await expect(page.getByRole('listitem')).toHaveCount(0);\n});","bugLines":[4,8,9,10,11],"title":"جد ما يجعل هذا الاختبار متقلّبًا","prompt":"<p>يفشل اختبار Playwright هذا مرة تقريبًا من كل عشر تشغيلات في CI، ولا يفشل أبدًا على حاسوب كاتبه. اختر <strong>كل سطر</strong> يجعله متقلّبًا أو غير موثوق. (افترض أن التطبيق نفسه يعمل.)</p>\n","hints":["ابحث عن سطر لا يُنتظَر فعله أبدًا.","ابحث عن انتظار ثابت، وعن قيمة تُقرأ مرة واحدة بدل أن تُؤكَّد بـ matcher يعيد المحاولة.","فكّر في الاختبارات الأخرى التي قد تستخدم مجموعة بهذا الاسم بالضبط، في الوقت نفسه، على أربعة عمّال."],"explanation":"<p>خمسة أسطر تسبّب المشكلات، لأربعة أسباب.</p>\n<ul>\n<li><strong>السطر 4</strong> يفتح مجموعة مشتركة معبّأة مسبقًا، <code dir=\"ltr\">lisbon-trip</code>. أي اختبار آخر يضيف إليها مصروفات، بالتوازي أو في وقت أبكر من التشغيل، يغيّر الأرصدة التي يراها هذا الاختبار. استخدم fixture تنشئ مجموعة جديدة لكل اختبار.</li>\n<li><strong>السطر 8</strong> بلا <code dir=\"ltr\">await</code>، فيتسابق النقر مع السطر التالي. أحيانًا تحدث التسوية قبل قراءة الحالة، وأحيانًا بعدها.</li>\n<li><strong>السطر 9</strong> انتظار ثابت مدته ثانيتان: وقت ضائع في التشغيل السريع، وغير كافٍ في البطيء.</li>\n<li><strong>السطران 10 و11</strong> يقرآن الحالة مرة واحدة بـ <code dir=\"ltr\">textContent()</code> ويقارنانها بـ <code dir=\"ltr\">toBe</code> عادي، فلا شيء يعيد المحاولة إن تحدّثت الحالة بعد لحظة.</li>\n</ul>\n<p>النسخة المصحّحة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><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:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'settling up clears 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:#953800;--shiki-dark:#FFA657\">group</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/${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">group</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">id</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</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\">'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 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\">'Settle up'</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 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:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>أما السطر 12 فكان صحيحًا أصلًا: <code dir=\"ltr\">toHaveCount</code> assertion موجّه للويب يعيد المحاولة.</p>\n","checkLabels":{}},"words":599,"readMinutes":4}