{"id":"l-tj-1-2","courseId":"testing-javascript","lang":"ar","kind":"lesson","summary":"تضيف Vitest إلى مشروع Vite، وتكتب أول ملف اختبار وتشغّله، وتستخدم وضع المراقبة والمرشّحات كي تبقى دورة التغذية الراجعة أقل من ثانية.","html":"<p>أسرع طريقة للتوقّف عن كتابة الاختبارات هي إعداد بطيء ومزعج. إن كان تشغيل اختبار واحد يستغرق 15 ثانية ويتطلّب ثلاثة ملفات إعدادات، فستتخطّاه “هذه المرة فقط” إلى أن لا يبقى أي اختبار. وُجدت Vitest في الأساس لحلّ هذه المشكلة: فهي تتّصل بإعداد Vite الموجود أصلًا في تطبيقك، فلا يستغرق أول اختبار أكثر من دقيقتين تقريبًا.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">التثبيت والتشغيل</h2>\n<p>في مشروع Vite (React أو Vue أو JavaScript صرف، لا فرق):</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\">npm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> install</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --save-dev</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> vitest</span></span></code></pre></div>\n<p>أضف سكربتين إلى <code dir=\"ltr\">package.json</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">package.json</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\">{</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"scripts\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    \"test\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"vitest\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    \"test:run\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"vitest run\"</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\">npm test</code> <strong>وضع المراقبة</strong> (watch mode): تشغّل Vitest مجموعة الاختبارات ثم تنتظر. وحين تحفظ ملفًا، تعيد تشغيل الاختبارات التي تستورده فقط، معتمدةً على مخطّط الوحدات (module graph) في Vite. على حاسوب محمول يستغرق ذلك عادةً أقل بكثير من ثانية، وهذا ما يجعل الاختبار جزءًا من البرمجة لا عبئًا تؤجّله إلى النهاية. أما <code dir=\"ltr\">npm run test:run</code> فيشغّل كل شيء مرة واحدة ثم يخرج؛ وهذا هو الأمر المناسب لـ CI (كما تُطفئ Vitest وضع المراقبة تلقائيًا حين يكون متغيّر البيئة <code dir=\"ltr\">CI</code> مضبوطًا).</p>\n<p>إن كنت تفضّل مشغّلًا مرئيًا، فثبّت <code dir=\"ltr\">@vitest/ui</code> وشغّل <code dir=\"ltr\">vitest --ui</code>: سيفتح لوحة في المتصفّح تعرض كل ملف وكل اختبار، مع حالات الفشل ومخرجات الـ console ومخطّط الوحدات، ويعيد التشغيل عند الحفظ تمامًا كما تفعل الطرفية. وإضافة Vitest الرسمية لـ VS Code تعطيك أزرار التشغيل وتصحيح الأخطاء نفسها بجانب كل اختبار داخل المحرّر. اختر ما يُبقيك في حالة تركيز؛ فالاختبارات هي نفسها.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">أول ملف اختبار</h2>\n<p>يحفظ Splitwise-lite المبالغ المالية <strong>كسنتات صحيحة</strong> (سنشرح السبب في الدرس القادم)، لذا يحتاج إلى دالة تحوّل <code dir=\"ltr\">1234</code> إلى <code dir=\"ltr\">&quot;$12.34&quot;</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">src/money.js</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\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> formatMoney</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">cents</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\"> sign</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cents </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '-'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</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\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> abs</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Math.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">abs</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(cents);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> dollars</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Math.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">floor</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(abs </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 100</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\"> rest</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> String</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(abs </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 100</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">padStart</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'0'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">sign</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}$${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">dollars</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}.${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">rest</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></code></pre></div>\n<p>ضع الاختبار بجوارها، باسم يجعل Vitest تجده:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">src/money.test.js</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\"> { describe, test, expect } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'vitest'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { formatMoney } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './money.js'</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\">describe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'formatMoney'</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:#8250DF;--shiki-dark:#D2A8FF\">  test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'formats dollars and cents'</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:#8250DF;--shiki-dark:#D2A8FF\">    expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">formatMoney</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1234</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'$12.34'</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:#8250DF;--shiki-dark:#D2A8FF\">  test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'pads single-digit cents'</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:#8250DF;--shiki-dark:#D2A8FF\">    expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">formatMoney</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">505</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'$5.05'</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\">describe</code> الاختبارات المترابطة (ويظهر في التقرير بصيغة <code dir=\"ltr\">formatMoney &gt; pads single-digit cents</code>). ويعرّف <code dir=\"ltr\">test</code> (واسمه البديل <code dir=\"ltr\">it</code>) اختبارًا واحدًا. أما <code dir=\"ltr\">expect(value).toBe(expected)</code> فهو الـ assertion (التأكيد): يرمي خطأً حين تختلف القيمتان، تمامًا مثل <code dir=\"ltr\">expectEqual</code> التي كتبتها في الدرس السابق.</p>\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>ضع الاختبارات بجوار الكود</span></p>\n<p>احتفظ بـ <code dir=\"ltr\">money.test.js</code> بجانب <code dir=\"ltr\">money.js</code> بدل وضعه في مجلد <code dir=\"ltr\">tests/</code> بعيد. سترى بنظرة واحدة أيّ الوحدات لها اختبارات، وحين تنقل وحدةً ينتقل اختبارها معها.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">أين توجد الإعدادات</h2>\n<p>تقرأ Vitest ملف <code dir=\"ltr\">vite.config.js</code> الموجود لديك، فتعمل الـ aliases والـ plugins في الاختبارات دون أي جهد. وتوضع خيارات الاختبار تحت المفتاح <code dir=\"ltr\">test</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">vite.config.js</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\">/// &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">reference</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> types</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"vitest/config\"</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\"> /></span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { defineConfig } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'vite'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> react </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '@vitejs/plugin-react'</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\">  plugins: [</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">react</span><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\">  test: {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    environment: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'node'</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\">test</code>. و<code dir=\"ltr\">environment: 'node'</code> هو الافتراضي، كتبناه هنا صراحةً كي تعرف أن هذا المفتاح موجود. المنطق الصافي مثل تنسيق المبالغ يعمل في Node، وهو سريع. أما الاختبارات التي تحتاج إلى <code dir=\"ltr\">document</code> (وستكتبها في القسم 3) فتستخدم <code dir=\"ltr\">jsdom</code> أو <code dir=\"ltr\">happy-dom</code>، وكلاهما يُثبَّت على حدة، ويُضبط إمّا هنا أو لكل ملف بتعليق <code dir=\"ltr\">// @vitest-environment jsdom</code> في أعلاه.</p>\n<p>سترى أيضًا <code dir=\"ltr\">globals: true</code> في بعض المشاريع. هذا الخيار يجعل <code dir=\"ltr\">test</code> و<code dir=\"ltr\">expect</code> متاحين دون استيراد، على طريقة Jest. لكن الاستيراد الصريح هو الافتراضي، ويتوافق بشكل أفضل مع المحرّرات وأدوات الـ lint، لذا تستوردهما هذه الدورة.</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>بيئة DOM لكل شيء</span></p>\n<p>ضبط <code dir=\"ltr\">environment: 'jsdom'</code> على مستوى المشروع كله “من باب الاحتياط” يجعل كل ملف اختبار يدفع ثمن بناء متصفّح مزيّف، ويُخفي أي وصول غير مقصود إلى الـ DOM في كود يُفترض أن يكون صافيًا. اجعل Node هي الافتراضي، وفعّل الـ DOM لكل ملف على حدة حين يحتاجه الاختبار.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">تشغيل جزء من مجموعة الاختبارات</h2>\n<p>كلما كبرت مجموعة الاختبارات، سترغب في تشغيل جزء أصغر منها:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"5\"><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\"> vitest</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> money</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">            # only files whose path contains \"money\"</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> vitest</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -t</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"pads\"</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">        # only tests whose name matches \"pads\"</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> vitest</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> run</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --reporter=verbose</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">   # one pass, every test name listed</span></span></code></pre></div>\n<p>داخل الملف، يشغّل <code dir=\"ltr\">test.only(...)</code> ذلك الاختبار وحده، ويركن <code dir=\"ltr\">test.skip(...)</code> اختبارًا جانبًا. كلاهما مفيد أثناء تتبّع الأخطاء، وخطير إن دخل في commit، لأن <code dir=\"ltr\">.only</code> منسيًّا يُطفئ بقية الملف بصمت. وتحمي Vitest من ذلك في CI: فحين يكون متغيّر البيئة <code dir=\"ltr\">CI</code> مضبوطًا، يفشل أي اختبار عليه <code dir=\"ltr\">.only</code> برسالة “Unexpected .only modifier” بدل أن يتخطّى جيرانه بهدوء.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">في ساحة التجربة</h2>\n<p>تعمل التمارين التالية في بيئة معزولة داخل المتصفّح، فيها جزء متوافق مع Vitest: الدوال <code dir=\"ltr\">describe</code> و<code dir=\"ltr\">test</code> و<code dir=\"ltr\">expect</code> و<code dir=\"ltr\">vi</code> والـ hooks متاحة مباشرة، والكود الخاضع للاختبار (الـ <strong>subject</strong>) محمَّل لك، فلا حاجة إلى أي استيراد. حين تتحقّق من حلّك، تعمل اختباراتك على الـ subject الحقيقي ويجب أن تنجح، ثم على بضع نسخ معطوبة عمدًا ويجب أن تفشل على كل واحدة منها.</p>\n<p>ينظر الدرس القادم عن قرب في ما يوضع داخل الاختبار، وفي كيفية قراءة المخرجات حين يفشل.</p>\n","toc":[{"id":"sec-1","text":"التثبيت والتشغيل","level":2},{"id":"sec-2","text":"أول ملف اختبار","level":2},{"id":"sec-3","text":"أين توجد الإعدادات","level":2},{"id":"sec-4","text":"تشغيل جزء من مجموعة الاختبارات","level":2},{"id":"sec-5","text":"في ساحة التجربة","level":2}],"takeaways":["تعيد Vitest استخدام إعدادات Vite لديك، فتنطبق الـ aliases والـ plugins والتحويلات نفسها على اختباراتك دون إعداد يُذكر.","الملفات المسمّاة <code dir=\"ltr\">*.test.js</code> أو <code dir=\"ltr\">*.spec.js</code> (ونظيراتها بامتدادات ts وjsx وtsx) تُلتقط تلقائيًا؛ ضع كل ملف منها بجوار الكود الذي يختبره.","شغّل <code dir=\"ltr\">vitest</code> أثناء العمل (فوضع المراقبة يعيد تشغيل الاختبارات المتأثّرة عند الحفظ)، و<code dir=\"ltr\">vitest run</code> في CI لتشغيلة واحدة.","البيئة الافتراضية هي Node؛ لا تنتقل إلى <code dir=\"ltr\">jsdom</code> أو <code dir=\"ltr\">happy-dom</code> إلا في الاختبارات التي تحتاج إلى DOM."],"further":[{"title":"Vitest — Getting Started","url":"https://vitest.dev/guide/"},{"title":"Vitest — Configuring Vitest","url":"https://vitest.dev/config/"},{"title":"Vitest — Test Environment","url":"https://vitest.dev/guide/environment"}],"quiz":[{"q":"<p>تشغّل <code dir=\"ltr\">npx vitest</code> على حاسوبك، ولا ينتهي الأمر بعد انتهاء الاختبارات. ما الذي يحدث؟</p>","options":[{"text":"أحد الاختبارات عالق في انتظار promise لا يُحلّ أبدًا.","why":"كان ذلك سيظهر فشلًا بسبب انتهاء المهلة (timeout) بعد بضع ثوانٍ؛ أما هنا فقد انتهى كل اختبار وظهرت نتيجته."},{"text":"Vitest في وضع المراقبة، وسيعيد تشغيل الاختبارات المتأثّرة حين تحفظ ملفًا.","why":"صحيح. في الطرفية التفاعلية يعمل <code dir=\"ltr\">vitest</code> افتراضيًا في وضع المراقبة؛ أما <code dir=\"ltr\">vitest run</code> (أو <code dir=\"ltr\">CI=true</code>) فيعمل مرة واحدة ثم يخرج."},{"text":"تحتاج Vitest إلى الخيار <code dir=\"ltr\">--exit</code> على macOS وLinux.","why":"لا يوجد شرط كهذا؛ العملية تبقى حيّة عمدًا لأن وضع المراقبة هو الافتراضي محليًا."}],"answer":1},{"q":"<p>أيّ ملف ستلتقطه Vitest كملف اختبار بالإعدادات الافتراضية؟</p>","options":[{"text":"<code dir=\"ltr\">src/money.tests.js</code>","why":"النمط الافتراضي يطابق <code dir=\"ltr\">.test.</code> أو <code dir=\"ltr\">.spec.</code> قبل الامتداد؛ و<code dir=\"ltr\">.tests.</code> بحرف s لا يطابقه."},{"text":"<code dir=\"ltr\">src/__tests__/money.js</code>","why":"لا تعامل Vitest مجلدات <code dir=\"ltr\">__tests__</code> معاملة خاصة افتراضيًا؛ يجب أن يحتوي اسم الملف نفسه على <code dir=\"ltr\">.test.</code> أو <code dir=\"ltr\">.spec.</code>."},{"text":"<code dir=\"ltr\">src/money.spec.ts</code>","why":"صحيح. نمط التضمين الافتراضي هو <code dir=\"ltr\">**/*.{test,spec}.?(c|m)[jt]s?(x)</code>، وهو يشمل <code dir=\"ltr\">.spec.ts</code>."},{"text":"<code dir=\"ltr\">test/money.js</code>","why":"اسم مجلد <code dir=\"ltr\">test</code> لا يكفي؛ فبدون <code dir=\"ltr\">.test.</code> أو <code dir=\"ltr\">.spec.</code> في اسم الملف يُتجاهَل افتراضيًا."}],"answer":2},{"q":"<p>يفشل اختبار لـ <code dir=\"ltr\">formatMoney</code> بالرسالة <code dir=\"ltr\">ReferenceError: document is not defined</code>، مع أن <code dir=\"ltr\">formatMoney</code> لا تلمس الـ DOM أبدًا. ما السبب الأرجح؟</p>","options":[{"text":"ملف الاختبار يستورد وحدة تلمس <code dir=\"ltr\">document</code> لحظة استيرادها، والبيئة هي Node.","why":"صحيح. البيئة الافتراضية لا تحتوي على DOM؛ وهناك شيء في سلسلة الاستيراد يقرأ <code dir=\"ltr\">document</code>. إمّا أن تفصل تلك الوحدة، أو تضبط البيئة لذلك الملف."},{"text":"تحتاج Vitest إلى <code dir=\"ltr\">globals: true</code> قبل أن تعمل الاختبارات.","why":"الـ globals تتحكّم فقط في إتاحة <code dir=\"ltr\">test</code> و<code dir=\"ltr\">expect</code> دون استيرادهما؛ ولا علاقة لها بـ <code dir=\"ltr\">document</code>."},{"text":"لا تستطيع Vitest اختبار إلا الكود الذي يعمل في المتصفّح.","why":"العكس تمامًا: تشغّل Vitest الاختبارات افتراضيًا في Node، ولهذا بالذات لا يوجد <code dir=\"ltr\">document</code>."}],"answer":0}],"exercise":{"kind":"web","mode":"test","subject":"// Splitwise-lite stores money as integer cents.\n// formatMoney(1234) -> \"$12.34\", formatMoney(-505) -> \"-$5.05\"\nfunction formatMoney(cents) {\n  const sign = cents < 0 ? '-' : '';\n  const abs = Math.abs(cents);\n  const dollars = Math.floor(abs / 100);\n  const rest = String(abs % 100).padStart(2, '0');\n  return `${sign}$${dollars}.${rest}`;\n}\n","mutants":[{"id":"no-padding","code":"// Splitwise-lite stores money as integer cents.\n// formatMoney(1234) -> \"$12.34\", formatMoney(-505) -> \"-$5.05\"\nfunction formatMoney(cents) {\n  const sign = cents < 0 ? '-' : '';\n  const abs = Math.abs(cents);\n  const dollars = Math.floor(abs / 100);\n  const rest = String(abs % 100);\n  return `${sign}$${dollars}.${rest}`;\n}\n"},{"id":"sign-lost","code":"// Splitwise-lite stores money as integer cents.\n// formatMoney(1234) -> \"$12.34\", formatMoney(-505) -> \"-$5.05\"\nfunction formatMoney(cents) {\n  const sign = '';\n  const abs = Math.abs(cents);\n  const dollars = Math.floor(abs / 100);\n  const rest = String(abs % 100).padStart(2, '0');\n  return `${sign}$${dollars}.${rest}`;\n}\n"},{"id":"rounds-dollars","code":"// Splitwise-lite stores money as integer cents.\n// formatMoney(1234) -> \"$12.34\", formatMoney(-505) -> \"-$5.05\"\nfunction formatMoney(cents) {\n  const sign = cents < 0 ? '-' : '';\n  const abs = Math.abs(cents);\n  const dollars = Math.round(abs / 100);\n  const rest = String(abs % 100).padStart(2, '0');\n  return `${sign}$${dollars}.${rest}`;\n}\n"}],"starter":{"js":"describe('formatMoney', () => {\n  test('formats dollars and cents', () => {\n    expect(formatMoney(1234)).toBe('$12.34');\n  });\n\n  // Add tests for: single-digit cents, negative amounts,\n  // and amounts where the cents are 50 or more.\n});\n"},"solution":{"js":"describe('formatMoney', () => {\n  test('formats dollars and cents', () => {\n    expect(formatMoney(1234)).toBe('$12.34');\n  });\n\n  test('pads single-digit cents with a zero', () => {\n    expect(formatMoney(505)).toBe('$5.05');\n  });\n\n  test('puts the minus sign before the dollar sign', () => {\n    expect(formatMoney(-505)).toBe('-$5.05');\n  });\n\n  test('keeps the dollars when the cents are 50 or more', () => {\n    expect(formatMoney(199)).toBe('$1.99');\n  });\n\n  test('formats zero', () => {\n    expect(formatMoney(0)).toBe('$0.00');\n  });\n});\n"},"checks":[{"id":"passes","suite":"subject"},{"id":"catches-no-padding","suite":"no-padding"},{"id":"catches-sign-lost","suite":"sign-lost"},{"id":"catches-rounds-dollars","suite":"rounds-dollars"}],"title":"أول اختبارات لـ formatMoney","prompt":"<p>في تبويب الـ <strong>subject</strong> توجد الدالة <code dir=\"ltr\">formatMoney(cents)</code>، التي تحوّل السنتات الصحيحة إلى نص بالدولار: <code dir=\"ltr\">1234</code> تصبح <code dir=\"ltr\">&quot;$12.34&quot;</code> و<code dir=\"ltr\">-505</code> تصبح <code dir=\"ltr\">&quot;-$5.05&quot;</code>.</p>\n<p>في الكود المبدئي اختبار واحد. أضف اختبارات حتى تنجح مجموعتك على الكود الحقيقي <strong>وتفشل</strong> على كل واحدة من ثلاث نسخ معطوبة أخفاها المدقّق: واحدة تنسى إكمال السنتات المكوّنة من رقم واحد بصفر، وواحدة تُسقط إشارة السالب، وواحدة تقرّب الدولارات بدل أن تأخذ الجزء الصحيح منها. استخدم <code dir=\"ltr\">describe</code> و<code dir=\"ltr\">test</code> و<code dir=\"ltr\">expect(...).toBe(...)</code>؛ لا يوجد ما تستورده.</p>\n","hints":["كل خطأ مخفيّ لا يظهر إلا مع مدخلات معيّنة. أيّ مدخل يجعل إكمال الصفر مهمًّا؟ وأيّها يجعل الإشارة مهمّة؟","إكمال الصفر: جرّب 505 (وتوقّع “$5.05”). الإشارة: جرّب رقمًا سالبًا.","التقريب بدل أخذ الجزء الصحيح لا يغيّر النتيجة إلا حين تكون السنتات 50 أو أكثر، فمثلًا يجب أن تكون 199 هي “$1.99” لا “$2.99”."],"explanation":"<p>كل اختبار يستهدف قاعدة واحدة من قواعد التنسيق، ويختار مدخلًا يتغيّر فيه الناتج لو انكسرت تلك القاعدة. المدخل <code dir=\"ltr\">1234</code> وحده لا يميّز بين وجود إكمال الصفر وغيابه (فـ 34 مكوّنة من رقمين)، ولا بين أخذ الجزء الصحيح والتقريب (فـ 34 تُقرَّب إلى الأسفل في الحالتين)، ولهذا مرّت كل الأخطاء من المجموعة المبدئية. الأمثلة الجيدة <strong>مميِّزة</strong>: اختر مدخلات يؤدّي فيها خطأ معقول إلى إجابة مختلفة. <code dir=\"ltr\">505</code> تختبر إكمال الصفر، و<code dir=\"ltr\">-505</code> تختبر الإشارة، و<code dir=\"ltr\">199</code> تختبر أخذ الجزء الصحيح. والصفر إضافة رخيصة تحمي أكثر قيمة تُعرض في التطبيق.</p>\n","checkLabels":{"passes":"كل اختباراتك تنجح على formatMoney الحقيقية","catches-no-padding":"تلتقط النسخة التي تنسى إكمال السنتات المكوّنة من رقم واحد بصفر","catches-sign-lost":"تلتقط النسخة التي تُسقط إشارة السالب","catches-rounds-dollars":"تلتقط النسخة التي تقرّب الدولارات بدل أخذ الجزء الصحيح"}},"words":650,"readMinutes":4}