{"id":"l-tj-4-2","courseId":"testing-javascript","lang":"ar","kind":"lesson","summary":"تُبقي اختبارات الـ end-to-end مستقلّة باستخدام سياقات المتصفّح المعزولة في Playwright، وfixtures مخصّصة تنشئ بياناتها وتنظّفها، وحالة تسجيل دخول محفوظة، وتوجيه انتقائي لطلبات الشبكة.","html":"<p>أشهر سبب لفقدان الثقة في مجموعة end-to-end ليس التوقيت؛ بل <strong>الحالة المشتركة</strong>. ينجح الاختبار 12 وحده ويفشل في التشغيل الكامل لأن الاختبار 7 أعاد تسمية المجموعة التي يستخدمها. أو ينجح اختبار في أيام الاثنين فقط لأن البيانات الأولية تحتوي مصروفًا لـ “هذا الأسبوع”. العزل هو العلاج، وPlaywright تعطيك معظمه مجانًا.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">سياق جديد لكل اختبار</h2>\n<p>يستلم كل اختبار <strong>سياق متصفّح</strong> (browser context) خاصًّا به: ملفًّا شخصيًا يشبه وضع التصفّح الخفي، له ملفات تعريف الارتباط وlocalStorage وsessionStorage والذاكرة المؤقّتة الخاصة به. فتح سياق رخيص (أجزاء من الثانية، لا متصفّح جديد)، لذا تنشئ Playwright سياقًا جديدًا لكل اختبار. ولهذا تستطيع التشغيل مع <code dir=\"ltr\">fullyParallel: true</code> عبر عدة عمّال (workers): فلا يستطيع اختباران رؤية تسجيلات دخول بعضهما أو المسوّدات المحفوظة.</p>\n<p>جهة المتصفّح معزولة. <strong>أما الـ backend لديك فليس كذلك.</strong> إن عدّل اختباران المجموعة نفسها على الخادم نفسه، يتصادمان. وبقية هذا الدرس عن عزل البيانات.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">الـ Fixtures: التهيئة والتنظيف في مكان واحد</h2>\n<p>لقد كنت تستخدم الـ fixtures أصلًا: فـ <code dir=\"ltr\">page</code> في <code dir=\"ltr\">async ({ page }) =&gt; …</code> واحدة منها. وتأتي Playwright أيضًا بـ <code dir=\"ltr\">context</code> و<code dir=\"ltr\">browser</code> و<code dir=\"ltr\">browserName</code> و<code dir=\"ltr\">request</code> (عميل HTTP يشترك في <code dir=\"ltr\">baseURL</code>). وتعرّف fixtures خاصة بك عبر <code dir=\"ltr\">test.extend</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">e2e/fixtures.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 </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> base, 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:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Group</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</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\"> const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> test</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> base.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">extend</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;{ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">group</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Group</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }>({</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  group</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\">request</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">use</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:#6E7781;--shiki-dark:#8B949E\">    // Setup: create a fresh group through the API</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> response</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> request.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">post</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'/api/groups'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      data: { name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Trip ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">crypto</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">randomUUID</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">slice</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">8</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">)</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, members: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Ana'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Ben'</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:#8250DF;--shiki-dark:#D2A8FF\">    expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(response.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">ok</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">()).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBeTruthy</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> group</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Group</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> response.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">json</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:#8250DF;--shiki-dark:#D2A8FF\"> use</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(group); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// the test runs here</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">    // Teardown: runs after the test, pass or fail</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> request.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">delete</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`/api/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:#1F2328;--shiki-dark:#E6EDF3\">  },</span></span>\n<span class=\"line\"><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:#1F2328;--shiki-dark:#E6EDF3\"> { expect };</span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"1\"><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\"> './fixtures'</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\">'settling up clears every balance'</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:#6E7781;--shiki-dark:#8B949E\">  // …add an expense, click Settle up…</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:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>ثلاث خصائص تجعل هذا أفضل من <code dir=\"ltr\">beforeEach</code>:</p>\n<ul>\n<li><strong>عند الطلب.</strong> لا ينشئ مجموعةً إلا الاختبارات التي تذكر <code dir=\"ltr\">group</code> في معاملاتها. والاختبارات التي لا تحتاجها لا تدفع شيئًا.</li>\n<li><strong>التهيئة والهدم يعيشان معًا</strong>، حول <code dir=\"ltr\">await use(...)</code>، فلا يمكن أن يخرج التنظيف عن التزامن مع التهيئة.</li>\n<li><strong>قابلة للتركيب.</strong> يمكن لـ fixture الـ <code dir=\"ltr\">group</code> أن تعتمد على <code dir=\"ltr\">request</code>؛ ويمكن لـ fixture <code dir=\"ltr\">expense</code> أن تعتمد على <code dir=\"ltr\">group</code>. وتحلّ Playwright الترتيب.</li>\n</ul>\n<p>إنشاء البيانات <strong>عبر الـ API</strong> بدل النقر في الواجهة يُبقي كل اختبار مركّزًا على مسار واحد ويوفّر ثوانيَ في كل اختبار. اختبار واحد ينقر عبر “إنشاء مجموعة”؛ والخمسون الآخرون يحصلون على مجموعة من الـ fixture.</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>“الاختبار 1 ينشئ المجموعة، والاختبار 2 يضيف مصروفًا، والاختبار 3 يسوّي الحسابات” يُقرأ كقصة ويتصرّف كسلسلة: لا يمكن تشغيله بالتوازي، ولا يمكن إعادة تشغيل أحد اختباراته منفردًا، وفشل واحد يتتالى إلى ثلاثة. اجعل كل اختبار ينشئ ما يحتاجه، باسم فريد (لاحقة عشوائية) كي لا يتصادم العمّال المتوازيون أبدًا.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">تسجيل الدخول مرة واحدة</h2>\n<p>إن كان Splitwise-lite يتطلّب تسجيل الدخول، فالتنقّل في النموذج في كل اختبار يضيّع الوقت ويُرهق خادم المصادقة. نمط Playwright هو <strong>setup project</strong> (مشروع تهيئة): اختبار واحد يسجّل الدخول ويحفظ حالة تخزين المتصفّح في ملف، والمشاريع الحقيقية تعلن اعتمادها عليه وتبدأ وقد سُجّل دخولها مسبقًا:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"2\"><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:#6E7781;--shiki-dark:#8B949E\">// …inside defineConfig</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">projects</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\">'setup'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, testMatch:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> /</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</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\">setup</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\">ts/</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\">    name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'chromium'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><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\">], storageState: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'playwright/.auth/user.json'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    dependencies: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'setup'</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\">await page.context().storageState({ path: 'playwright/.auth/user.json' })</code>. أضف <code dir=\"ltr\">playwright/.auth</code> إلى <code dir=\"ltr\">.gitignore</code>: فهو يحتوي ملفات تعريف ارتباط لجلسات حقيقية.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">التوجيه: التحكّم في ما يقوله الخادم</h2>\n<p>بعض الحالات صعبة الإنتاج مع backend حقيقي: 500 عند الحفظ، أو استجابة بطيئة، أو مجموعة فارغة من حساب قديم. يعترض <code dir=\"ltr\">page.route</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:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'explains a failed save'</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\">route</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'**/api/expenses'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">route</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    route.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fulfill</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ status: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">500</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, json: { error: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Database unavailable'</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>\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'</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\">  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\">'alert'</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\">'Could not save the expense. Try again.'</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>استخدم التوجيه للنادر والمعطوب. وإن وجدت نفسك توجّه كل طلب، فقد أعدت بناء اختبار مكوّن بأداة أبطأ؛ دع المسارات السعيدة تصل إلى الـ backend الحقيقي، لأن هذا التكامل هو سبب وجود اختبارات الـ end-to-end.</p>\n<aside class=\"callout callout-why\" 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\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">لماذا يهمّ هذا</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>العزل هو ما يجعل التوازي آمنًا</span></p>\n<p>مجموعة من 200 اختبار مستقل تعمل على 8 عمّال في ثُمن الوقت. أما المجموعة ذات الاعتماديات الخفيّة فلا بدّ أن تعمل تسلسليًا، وإلا فشلت عشوائيًا. العزل ليس نظافة لذاتها؛ بل هو ما يُبقي المجموعة سريعة وهي تكبر.</p>\n</aside>\n<p>ويجعل العزل أيضًا تتبّع حالات الفشل أرخص. حين يفشل اختبار، تستطيع إعادة تشغيل ذلك الاختبار وحده بـ <code dir=\"ltr\">npx playwright test -g &quot;settling up&quot;</code> والحصول على النتيجة نفسها، لأن لا شيء مما يعتمد عليه يعيش خارجه. أما مع الاعتماديات الخفيّة، فتنجح إعادة التشغيل المنفردة ويفشل التشغيل الكامل، وتبقى تقسّم المجموعة نصفين بعد نصفين لتجد أيّ جار كسره.</p>\n<p>في التمرين تُكمل fixture تنشئ مجموعة وتحذفها. الدرس القادم: ماذا تفعل حين يفشل اختبار رغم كل ذلك.</p>\n","toc":[{"id":"sec-1","text":"سياق جديد لكل اختبار","level":2},{"id":"sec-2","text":"الـ Fixtures: التهيئة والتنظيف في مكان واحد","level":2},{"id":"sec-3","text":"تسجيل الدخول مرة واحدة","level":2},{"id":"sec-4","text":"التوجيه: التحكّم في ما يقوله الخادم","level":2}],"takeaways":["يحصل كل اختبار في Playwright على سياق متصفّح جديد (له ملفات تعريف الارتباط والتخزين والذاكرة المؤقّتة الخاصة به)، فتستطيع الاختبارات أن تعمل بالتوازي وبأي ترتيب.","الـ fixture (تجهيزة الاختبار) التي تُنشأ بـ <code dir=\"ltr\">test.extend</code> تُعدّ شيئًا ما، وتسلّمه للاختبار عبر <code dir=\"ltr\">use()</code>، ثم تهدمه بعد ذلك، وذلك للاختبارات التي تطلبها فقط.","يجب أن ينشئ كل اختبار البيانات التي يحتاجها (ويُفضَّل عبر الـ API) بدل الاعتماد على بيانات أولية مشتركة أو على أن اختبارًا آخر عمل قبله.","سجّل الدخول مرة واحدة في setup project، واحفظ حالة التخزين، وأعد استخدامها، بدل التنقّل في نموذج تسجيل الدخول في كل اختبار.","استخدم <code dir=\"ltr\">page.route</code> لمحاكاة استجابات الخادم النادرة، والـ backend الحقيقي للمسارات السعيدة."],"further":[{"title":"Playwright — Fixtures","url":"https://playwright.dev/docs/test-fixtures"},{"title":"Playwright — Isolation","url":"https://playwright.dev/docs/browser-contexts"},{"title":"Playwright — Authentication","url":"https://playwright.dev/docs/auth"},{"title":"Playwright — Mock APIs","url":"https://playwright.dev/docs/mock"}],"quiz":[{"q":"<p>ينشئ الاختبار A مجموعة اسمها “Lisbon trip”؛ ويفتح الاختبار B المجموعة “Lisbon trip” ويفحص أرصدتها. ينجح B محليًا ويفشل في CI. ما أصل المشكلة؟</p>","options":[{"text":"أجهزة CI أبطأ، فيحتاج B إلى مهلة أطول.","why":"السرعة ليست المشكلة؛ B يعتمد على بيانات لا توجد إلا إن عمل A أولًا، على الخادم نفسه، قبله."},{"text":"يعتمد B على بيانات A. ومع العمّال المتوازيين أو بترتيب مختلف، لا تكون المجموعة موجودة بعد حين يعمل B.","why":"صحيح. يجب أن ينشئ كل اختبار مجموعته الخاصة، مثلًا عبر fixture تستدعي الـ API وتنظّف بعد الانتهاء."},{"text":"يجب أن يستخدم B الدالة <code dir=\"ltr\">test.describe.serial</code> كي يعمل دائمًا بعد A.","why":"الوضع التسلسلي يجعل الاعتماد رسميًا والمجموعة أبطأ؛ ثم إن فشلًا واحدًا في A يتخطّى كل ما بعده. أزل الاعتماد بدلًا من ذلك."}],"answer":1},{"q":"<p>في الـ fixture، ماذا يفعل الكود الواقع بعد <code dir=\"ltr\">await use(group)</code>؟</p>","options":[{"text":"يعمل قبل الاختبار، لتجهيز البيانات.","why":"التهيئة تعمل قبل <code dir=\"ltr\">use</code>؛ أما السطر الذي بعده فينتظر حتى ينتهي الاختبار."},{"text":"لا شيء؛ الكود بعد <code dir=\"ltr\">use</code> لا يُبلغ أبدًا.","why":"يُحلّ <code dir=\"ltr\">use</code> حين ينتهي الاختبار، فيعمل الكود الذي بعده، وهكذا تنظّف الـ fixtures."},{"text":"يعمل بعد انتهاء الاختبار، حتى لو فشل الاختبار، وهذا ما يجعله مكان التنظيف.","why":"صحيح. التهيئة، ثم <code dir=\"ltr\">use(value)</code>، ثم الهدم: دالة واحدة تملك دورة حياة المورد كلها."}],"answer":2},{"q":"<p>ما أفضل استخدام لـ <code dir=\"ltr\">page.route</code> في مجموعة end-to-end؟</p>","options":[{"text":"محاكاة استجابة نادرة من الخادم، مثل 500 عند الحفظ، لاختبار رسالة الخطأ.","why":"صحيح. الاستجابات الصعبة الإحداث هي حيث يتألّق التوجيه؛ ويجب أن تظلّ المسارات السعيدة تصل إلى الـ backend الحقيقي."},{"text":"استبدال كل استدعاء API بـ mock كي لا تعتمد الاختبارات على الـ backend أبدًا.","why":"عندها لم يعد end-to-end؛ ستكون تعيد اختبار الواجهة ببيانات مزيّفة، وهذا ما تفعله اختبارات المكوّنات أسرع."},{"text":"تسريع الاختبارات البطيئة بتخطّي الطلبات إلى الـ API الخاص بك.","why":"تخطّي الـ API الخاص بك يُلغي التكامل الذي وُجد الاختبار لفحصه؛ اجعل الـ API أو بيانات الاختبار أسرع بدلًا من ذلك."}],"answer":0}],"exercise":{"kind":"fill","language":"ts","template":"import { test as base, expect } from '@playwright/test';\n\ntype Group = { id: string; name: string };\n\nexport const test = base.{{0}}<{ group: Group }>({\n  group: async ({ {{1}} }, use) => {\n    const response = await request.post('/api/groups', {\n      data: { name: `Trip ${crypto.randomUUID().slice(0, 8)}` },\n    });\n    const group: Group = await response.json();\n\n    await {{2}}(group);\n\n    await request.{{3}}(`/api/groups/${group.id}`);\n  },\n});\n\ntest('a new group starts settled up', async ({ page, {{4}} }) => {\n  await page.goto(`/groups/${group.id}`);\n  await {{5}}(page.getByRole('status')).toHaveText('Everyone is settled up');\n});","blanks":[{"answers":["extend"]},{"answers":["request"]},{"answers":["use"]},{"answers":["delete"]},{"answers":["group"]},{"answers":["expect"]}],"title":"أكمل fixture للمجموعة","prompt":"<p>أكمل هذه الـ fixture في Playwright كي يحصل كل اختبار يطلب <code dir=\"ltr\">group</code> على مجموعة <strong>جديدة</strong> تُنشأ عبر الـ API، وكي <strong>تُحذف</strong> المجموعة بعد الاختبار، نجح أو فشل. ثم أكمل الاختبار كي يستلم الـ fixture ويستخدم assertion موجّهًا للويب.</p>\n","hints":["تُعرَّف الـ fixtures المخصّصة باستدعاء دالة على كائن <code dir=\"ltr\">test</code> الأساسي تعيد <code dir=\"ltr\">test</code> جديدًا موسَّعًا.","تحتاج الـ fixture إلى fixture عميل HTTP المدمجة، تلك التي يستدعي الجسم <code dir=\"ltr\">.post</code> عليها.","سلّم القيمة للاختبار عبر المعامل الثاني لدالة الـ fixture؛ وبعد ذلك، نظّف بطريقة HTTP التي تزيل موردًا. يذكر الاختبار الـ fixture باسمها في معاملاته، ويؤكّد بـ <code dir=\"ltr\">expect</code> الخاص بـ Playwright الذي يعيد المحاولة."],"explanation":"<p>يعيد <code dir=\"ltr\">base.extend&lt;{ group: Group }&gt;({...})</code> كائن <code dir=\"ltr\">test</code> جديدًا يعرف الـ fixture المسمّاة <code dir=\"ltr\">group</code>. تطلب الـ fixture الـ fixture المدمجة <code dir=\"ltr\">request</code>، وتنشئ مجموعة باسم فريد، وتمرّرها للاختبار عبر <code dir=\"ltr\">await use(group)</code>. وكل ما بعد <code dir=\"ltr\">use</code> يعمل حين ينتهي الاختبار، أيًّا كانت النتيجة، فينظّف <code dir=\"ltr\">request.delete(...)</code> حتى بعد الفشل. ويشترك الاختبار فيها بتفكيك <code dir=\"ltr\">group</code> بجوار <code dir=\"ltr\">page</code>؛ أما الاختبارات التي لا تذكرها فلا تنشئ مجموعة أبدًا. وأخيرًا، يعيد <code dir=\"ltr\">await expect(locator).toHaveText(...)</code> المحاولة حتى تظهر الحالة، فلا يتسابق الاختبار مع تحميل الصفحة.</p>\n","checkLabels":{}},"words":598,"readMinutes":4}