{"id":"l-tj-2-4","courseId":"testing-javascript","lang":"ar","kind":"lesson","summary":"تستبدل المتعاونين بـ vi.fn وvi.spyOn، وتتحكّم في الوقت بالمؤقّتات المزيّفة، وتميّز الحالات التي يجعل فيها الـ mocking الاختبار ينجح بينما الكود الحقيقي معطّل.","html":"<p>في الدرس الماضي كتبت API بديلًا (stub) باليد، مع عدّاد. نجح الأمر، واستغرق نحو خمسة عشر سطرًا. أدوات <code dir=\"ltr\">vi</code> في Vitest تفعل الشيء نفسه في سطر واحد، وتضيف قوة كبيرة. وهذه القوة هي المشكلة: الـ mocking أسرع طريقة لكتابة اختبار ينجح بينما الكود الحقيقي معطّل. يغطّي هذا الدرس الأدوات، ويغطّي حسن التقدير في استخدامها.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">المصطلحات، باختصار</h2>\n<p>كل هذه <strong>test doubles</strong> (بدائل الاختبار)، أي أشياء تقوم مقام متعاون حقيقي:</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 190\" role=\"img\" aria-labelledby=\"l-tj-2-4-t1\"><defs><marker id=\"arrow-l-tj-2-4-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-2-4-t1\">أربعة أنواع من بدائل الاختبار في صف: الـ stub يعيد إجابات جاهزة، والـ spy يسجّل الاستدعاءات، والـ mock هو spy تؤكّد عليه، والـ fake تطبيق خفيف يعمل فعلًا.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"40\" width=\"155\" height=\"110\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"87\" y=\"72\" text-anchor=\"middle\">Stub</text>\n  <text class=\"d-label-muted\" x=\"87\" y=\"100\" text-anchor=\"middle\">إجابات جاهزة</text>\n  <text class=\"d-code\" x=\"87\" y=\"126\" text-anchor=\"middle\">returns {id}</text>\n  <rect class=\"d-box-accent\" x=\"180\" y=\"40\" width=\"155\" height=\"110\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"257\" y=\"72\" text-anchor=\"middle\">Spy</text>\n  <text class=\"d-label-muted\" x=\"257\" y=\"100\" text-anchor=\"middle\">يسجّل الاستدعاءات</text>\n  <text class=\"d-code\" x=\"257\" y=\"126\" text-anchor=\"middle\">.mock.calls</text>\n  <rect class=\"d-box-primary\" x=\"350\" y=\"40\" width=\"155\" height=\"110\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"427\" y=\"72\" text-anchor=\"middle\">Mock</text>\n  <text class=\"d-label-muted\" x=\"427\" y=\"100\" text-anchor=\"middle\">spy تؤكّد عليه</text>\n  <text class=\"d-code\" x=\"427\" y=\"126\" text-anchor=\"middle\">CalledWith</text>\n  <rect class=\"d-box-success\" x=\"520\" y=\"40\" width=\"155\" height=\"110\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"597\" y=\"72\" text-anchor=\"middle\">Fake</text>\n  <text class=\"d-label-muted\" x=\"597\" y=\"100\" text-anchor=\"middle\">يعمل، وأبسط</text>\n  <text class=\"d-code\" x=\"597\" y=\"126\" text-anchor=\"middle\">in-memory DB</text>\n</svg><figcaption>بدائل الاختبار، من الأبسط إلى الأغنى سلوكًا</figcaption></figure>\n<p>في Vitest، يعطيك <code dir=\"ltr\">vi.fn()</code> الـ stub والـ spy (الجاسوس) في كائن واحد، والناس يسمّون كل ذلك “mocks” (بدائل وهمية). الأسماء أقل أهمية من السؤال الذي يجيب عنه كل منها: <em>ماذا يعيد هذا المتعاون؟</em> (stub)، و<em>كيف استُدعي؟</em> (spy).</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">vi.fn: دالة تتذكّر</h2>\n<p>يرسل Splitwise-lite تذكيرًا إلى كل من عليه مال. دالة الإرسال تُمرَّر كمعامل، فيستطيع الاختبار استبدالها:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/remind.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\"> { test, expect, vi } </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\"> { remindDebtors } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './remind.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\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'reminds people who owe money, with the amount'</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\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> sendReminder</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> vi.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fn</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\"> sent</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> remindDebtors</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ Ana: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1500</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, Ben: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">500</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, Cai: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }, sendReminder);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(sent).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</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\">(sendReminder).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveBeenCalledTimes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</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\">(sendReminder).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveBeenCalledWith</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 style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'You owe $5.00 to the group'</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\">vi.fn()</code> القيمة <code dir=\"ltr\">undefined</code> افتراضيًا. أعطه سلوكًا حين يحتاج الكود الخاضع للاختبار إلى إجابة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</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\"> post</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> vi.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">mockResolvedValue</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ id: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'e1'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });          </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// async success</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> failing</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> vi.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">mockRejectedValue</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Offline'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// async failure</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> send</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> vi.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">mockImplementation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">name</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\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (name </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Ben'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">throw</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Mailbox full'</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\">send.mock.calls</code>، وهي مصفوفة من مصفوفات المعاملات، للحالات النادرة التي لا تغطّيها الـ matchers.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">vi.spyOn: مراقبة دالة حقيقية</h2>\n<p>أحيانًا يكون المتعاون دالة على كائن موجود، مثل <code dir=\"ltr\">console.warn</code>. يلفّها <code dir=\"ltr\">vi.spyOn</code>: افتراضيًا تظلّ الدالة الحقيقية تعمل، ويسجّل الـ spy الاستدعاءات. أضف <code dir=\"ltr\">mockImplementation</code> لإسكاتها أو استبدالها:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</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\"> { afterEach, test, expect, vi } </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>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">afterEach</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:#1F2328;--shiki-dark:#E6EDF3\">  vi.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">restoreAllMocks</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// put console.warn back</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\">'warns and carries on when one reminder fails'</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\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> warn</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> vi.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">spyOn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(console, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'warn'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">mockImplementation</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\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> send</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> vi.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">mockImplementationOnce</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">throw</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Mailbox full'</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\"> sent</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> remindDebtors</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ Ben: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">500</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, Dee: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">300</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }, send);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(sent).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</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\">(send).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveBeenCalledTimes</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>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(warn).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveBeenCalledWith</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Could not remind Ben: Mailbox full'</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>يستبدل الـ spy دالة على كائن <strong>مشترك</strong>. انسَ استعادتها، وسيعمل كل اختبار لاحق في الملف مع <code dir=\"ltr\">console.warn</code> صامت. وجود <code dir=\"ltr\">vi.restoreAllMocks()</code> داخل <code dir=\"ltr\">afterEach</code>، أو <code dir=\"ltr\">restoreMocks: true</code> في الإعدادات، يجعل التنظيف تلقائيًا.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">المؤقّتات المزيّفة: تخطّي الانتظار</h2>\n<p>الكود الذي ينتظر (الحفظ التلقائي المؤجَّل بـ debounce، والتباطؤ بين المحاولات، ورسائل “تراجع” المؤقّتة) اختباره بالوقت الحقيقي مؤلم. تستبدل المؤقّتات المزيّفة (fake timers) كلًّا من <code dir=\"ltr\">setTimeout</code> و<code dir=\"ltr\">setInterval</code> و<code dir=\"ltr\">Date</code> بساعة يتحكّم فيها الاختبار:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</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\"> { afterEach, test, expect, vi } </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>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">afterEach</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> vi.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">useRealTimers</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\">'autosaves the draft 2 seconds after the last change'</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:#1F2328;--shiki-dark:#E6EDF3\">  vi.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">useFakeTimers</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\"> save</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> vi.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fn</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\"> draft</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> createDraft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ save, delayMs: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// debounced</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  draft.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">update</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ description: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Din'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  draft.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">update</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ description: </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:#1F2328;--shiki-dark:#E6EDF3\">  vi.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">advanceTimersByTime</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1999</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\">(save).not.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveBeenCalled</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  vi.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">advanceTimersByTime</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</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\">(save).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveBeenCalledWith</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ description: </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:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>يشغّل <code dir=\"ltr\">vi.advanceTimersByTime(ms)</code> فورًا كل مؤقّت يحين موعده خلال تلك الفترة. ويثبّت <code dir=\"ltr\">vi.setSystemTime(date)</code> قيمة <code dir=\"ltr\">new Date()</code>، وهكذا تختبر أن “التذكيرات تُرسل في أول كل شهر”. وحين تجدول المؤقّتات promises، استخدم النسخ غير المتزامنة مثل <code dir=\"ltr\">vi.advanceTimersByTimeAsync</code>. (ساحة التجربة لا تشغّل إلا المؤقّتات الحقيقية، لذا يبقى هذا المثال على الصفحة فقط.)</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">متى يضرّ الـ mocking</h2>\n<p>كل mock <strong>ادّعاء</strong> عن طريقة عمل الشيء الحقيقي. وحين يبتعد الواقع عن الادّعاء، يستمرّ الاختبار في النجاح. الأنماط التي تؤذي:</p>\n<ul>\n<li><strong>استخدام الـ mocks مكان وحداتك أنت.</strong> استبدال <code dir=\"ltr\">parseAmount</code> داخل اختبار النموذج يعني أن النموذج والمحلّل لا يُختبران معًا أبدًا، وهذا كان الهدف من الاختبار. كودك الصافي سريع؛ استخدمه.</li>\n<li><strong>استخدام mock مكان الشيء الخاضع للاختبار نفسه.</strong> إن تجسّست على <code dir=\"ltr\">remindDebtors</code> وأنت تختبر <code dir=\"ltr\">remindDebtors</code>، فأنت تختبر الـ spy.</li>\n<li><strong>تأكيد كل استدعاء.</strong> فحص التسلسل الدقيق لأربعة عشر استدعاءً داخليًا يجمّد التفاصيل الداخلية. أكّد الاستدعاءات التي تهمّ المستدعي، مثل “وصل تذكير إلى Ben”.</li>\n<li><strong>mocks تكذب.</strong> الـ API البديل لديك يعيد <code dir=\"ltr\">{ id }</code>؛ والحقيقي يعيد <code dir=\"ltr\">{ data: { id } }</code>. الـ unit tests خضراء، والتطبيق معطّل. ومحاكاة الشبكة في القسم التالي واختبارات الـ end-to-end في القسم 4 موجودة لالتقاط هذا بالضبط.</li>\n</ul>\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>اللجوء إلى vi.mock أولًا</span></p>\n<p>يستبدل <code dir=\"ltr\">vi.mock('./api.js')</code> وحدة كاملة للملف، ويُرفع (hoisted) فوق الاستيرادات، وهذا يفاجئ الناس. إنه مفيد مع SDK خارجي لا تستطيع حقنه. أما مع كودك أنت، فتمرير المتعاون كمعامل (كما تفعل <code dir=\"ltr\">remindDebtors</code>) يُبقي الاختبارات أبسط والتصميم أصدق.</p>\n</aside>\n<p>فحص سريع لأي mock تكتبه: احذف سطرًا واحدًا من الكود الحقيقي الذي يقوم مقامه. إن لم يفشل أي اختبار في أي مكان من المجموعة، فذلك الـ mock كان يُخفي السلوك بدل أن يختبر ما حوله، وتحتاج في مكان ما إلى اختبار يشغّل الشيء الحقيقي.</p>\n<p>قاعدتي: <strong>استخدم الـ mocks عند حدود</strong> نظامك (الشبكة، والبريد، والدفع، والوقت، والعشوائية)، وشغّل الكود الحقيقي في كل ما بداخله.</p>\n<p>في التمرين، استخدم <code dir=\"ltr\">vi.fn</code> و<code dir=\"ltr\">vi.spyOn</code> لالتقاط أربعة أخطاء في <code dir=\"ltr\">remindDebtors</code>. وينتقل القسم 3 من الدوال إلى الـ DOM.</p>\n","toc":[{"id":"sec-1","text":"المصطلحات، باختصار","level":2},{"id":"sec-2","text":"vi.fn: دالة تتذكّر","level":2},{"id":"sec-3","text":"vi.spyOn: مراقبة دالة حقيقية","level":2},{"id":"sec-4","text":"المؤقّتات المزيّفة: تخطّي الانتظار","level":2},{"id":"sec-5","text":"متى يضرّ الـ mocking","level":2}],"takeaways":["ينشئ <code dir=\"ltr\">vi.fn()</code> دالة تسجّل كل استدعاء، وتستطيع أن تحدّد ما تعيده باستخدام <code dir=\"ltr\">mockReturnValue</code> أو <code dir=\"ltr\">mockResolvedValue</code> أو <code dir=\"ltr\">mockImplementation</code>.","يراقب <code dir=\"ltr\">vi.spyOn(object, 'method')</code> دالة حقيقية ويستطيع استبدالها؛ أعِد الـ spies دائمًا إلى أصلها، مثلًا بـ <code dir=\"ltr\">vi.restoreAllMocks()</code> داخل <code dir=\"ltr\">afterEach</code>.","استخدم الـ mocks عند حدود نظامك (الشبكة، والبريد، والوقت، والعشوائية)، واستخدم الكود الحقيقي لوحداتك أنت.","تسمح المؤقّتات المزيّفة (<code dir=\"ltr\">vi.useFakeTimers</code> و<code dir=\"ltr\">vi.advanceTimersByTime</code>) للاختبار بأن يقفز فوق فترات الانتظار بدل أن ينتظرها.","كل mock ادّعاء عن طريقة عمل الشيء الحقيقي؛ وحين يبتعد هذا الادّعاء عن الواقع، يبقى الاختبار أخضر بينما ينكسر الإنتاج."],"further":[{"title":"Vitest — Mocking guide","url":"https://vitest.dev/guide/mocking"},{"title":"Vitest — Mock functions API","url":"https://vitest.dev/api/mock"},{"title":"Vitest — Fake timers","url":"https://vitest.dev/guide/mocking/timers"}],"quiz":[{"q":"<p>تريد التحقّق من أن <code dir=\"ltr\">remindDebtors</code> تستدعي <code dir=\"ltr\">sendReminder('Ben', 'You owe $5.00 to the group')</code>. أيّ assertion هو المناسب؟</p>","options":[{"text":"<code dir=\"ltr\">expect(sendReminder).toHaveBeenCalledWith('Ben', 'You owe $5.00 to the group')</code>","why":"صحيح. ينجح <code dir=\"ltr\">toHaveBeenCalledWith</code> إن كان أيّ استدعاء مسجَّل قد استخدم هذه المعاملات بالضبط، وبهذا الترتيب."},{"text":"<code dir=\"ltr\">expect(sendReminder('Ben')).toBe('You owe $5.00 to the group')</code>","why":"هذا يستدعي الـ mock بنفسك ويفحص قيمته المعادة؛ ولا يقول شيئًا عمّا فعلته <code dir=\"ltr\">remindDebtors</code>."},{"text":"<code dir=\"ltr\">expect(sendReminder).toHaveBeenCalled()</code>","why":"ينجح حتى لو كانت المعاملات خاطئة أو متبادلة الموقع، فيفوته معظم الأخطاء التي تهمّك."}],"answer":0},{"q":"<p>اختبار لنموذج المصروفات يستخدم mock مكان الوحدة الداخلية <code dir=\"ltr\">parseAmount</code> بحيث تعيد دائمًا 1250. ثم يظهر خطأ في <code dir=\"ltr\">parseAmount</code> الحقيقية. ماذا يحدث لاختبار النموذج؟</p>","options":[{"text":"يفشل، لأن Vitest تكتشف أن الـ mock والوحدة الحقيقية لا يتّفقان.","why":"لا تعرف Vitest ما الذي كانت الوحدة الحقيقية ستعيده؛ فالـ mock يحلّ محلّها بالكامل."},{"text":"يرمي خطأً، لأن الـ mocks لا تستطيع استبدال وحدات تملكها أنت.","why":"يستطيع <code dir=\"ltr\">vi.mock</code> استبدال أي وحدة، بما فيها وحداتك، وهذا بالضبط مكمن الخطر هنا."},{"text":"يبقى أخضر، لأنه لا يشغّل المحلّل الحقيقي أبدًا؛ فلم يعد النموذج والمحلّل يُختبران معًا.","why":"صحيح. استخدام الـ mocks مكان وحداتك الصافية لا يوفّر سرعة تُذكر، ويُلغي التكامل الذي أردت فحصه."}],"answer":2},{"q":"<p>تُحفظ المسوّدة تلقائيًا بعد ثانيتين من آخر ضغطة مفتاح. مع تفعيل <code dir=\"ltr\">vi.useFakeTimers()</code>، كيف يجعل الاختبار الحفظ يحدث؟</p>","options":[{"text":"بالانتظار عبر <code dir=\"ltr\">await new Promise((r) =&gt; setTimeout(r, 2000))</code>.","why":"مع المؤقّتات المزيّفة يكون <code dir=\"ltr\">setTimeout</code> هذا مزيّفًا أيضًا، فلا يعمل من تلقاء نفسه أبدًا، ويعلق الاختبار حتى تنتهي مهلته."},{"text":"باستدعاء <code dir=\"ltr\">vi.advanceTimersByTime(2000)</code> بعد آخر ضغطة مفتاح، ثم تأكيد أن الحفظ حدث.","why":"صحيح. تقديم الساعة المزيّفة يشغّل فورًا كل مؤقّت يحين موعده خلال تلك الفترة."},{"text":"بضبط مهلة الاختبار على 3 ثوانٍ كي يجد المؤقّت الحقيقي وقتًا ليعمل.","why":"مع تثبيت المؤقّتات المزيّفة لا يوجد مؤقّت حقيقي؛ والمهلة الأطول تؤخّر الفشل فقط."}],"answer":1},{"q":"<p>لماذا تستدعي <code dir=\"ltr\">vi.restoreAllMocks()</code> (أو تضبط <code dir=\"ltr\">restoreMocks</code>) بعد الاختبارات التي تستخدم <code dir=\"ltr\">vi.spyOn</code>؟</p>","options":[{"text":"لإعادة عدّادات الاستدعاء في mocks الـ <code dir=\"ltr\">vi.fn()</code> إلى الصفر.","why":"مسح سجلّ الاستدعاءات هو ما يفعله <code dir=\"ltr\">mockClear</code>؛ أما الاستعادة فتتعلّق بإرجاع الدالة الأصلية."},{"text":"لإرجاع الدوال الأصلية كي لا يغيّر spy من اختبار ما السلوك في اختبار آخر.","why":"صحيح. يستبدل الـ spy دالة حقيقية على كائن مشترك مثل <code dir=\"ltr\">console</code>؛ وبدون الاستعادة يتسرّب الاستبدال إلى الاختبارات اللاحقة."},{"text":"لأن Vitest ترفض تشغيل اختبار ثانٍ ما دام هناك spy موجود.","why":"تعمل Vitest بلا مشكلة مع spies متسرّبة، ولهذا بالضبط تسبّب التسرّبات حالات فشل مربكة في اختبارات لا علاقة لها."}],"answer":1}],"exercise":{"kind":"web","mode":"test","subject":"function formatMoney(cents) {\n  const sign = cents < 0 ? '-' : '';\n  const abs = Math.abs(cents);\n  return `${sign}$${Math.floor(abs / 100)}.${String(abs % 100).padStart(2, '0')}`;\n}\n\n// Sends a reminder to everyone who owes money (a negative balance).\n// - Calls sendReminder(name, message) once per debtor, with a message\n//   such as \"You owe $5.00 to the group\".\n// - Skips debts smaller than minimumCents (default 100, i.e. $1.00);\n//   a debt of exactly minimumCents IS reminded.\n// - People who are owed money or are settled up get nothing.\n// - If sendReminder throws for someone, logs\n//   console.warn(\"Could not remind <name>: <error message>\")\n//   and carries on with the others.\n// Returns the number of reminders that were sent successfully.\nfunction remindDebtors(balances, sendReminder, { minimumCents = 100 } = {}) {\n  let sent = 0;\n  for (const [name, cents] of Object.entries(balances)) {\n    if (cents >= 0 || -cents < minimumCents) continue;\n    try {\n      sendReminder(name, `You owe ${formatMoney(-cents)} to the group`);\n      sent += 1;\n    } catch (error) {\n      console.warn(`Could not remind ${name}: ${error.message}`);\n    }\n  }\n  return sent;\n}\n","mutants":[{"id":"reminds-creditors","code":"function formatMoney(cents) {\n  const sign = cents < 0 ? '-' : '';\n  const abs = Math.abs(cents);\n  return `${sign}$${Math.floor(abs / 100)}.${String(abs % 100).padStart(2, '0')}`;\n}\n\n// Sends a reminder to everyone who owes money (a negative balance).\n// - Calls sendReminder(name, message) once per debtor, with a message\n//   such as \"You owe $5.00 to the group\".\n// - Skips debts smaller than minimumCents (default 100, i.e. $1.00);\n//   a debt of exactly minimumCents IS reminded.\n// - People who are owed money or are settled up get nothing.\n// - If sendReminder throws for someone, logs\n//   console.warn(\"Could not remind <name>: <error message>\")\n//   and carries on with the others.\n// Returns the number of reminders that were sent successfully.\nfunction remindDebtors(balances, sendReminder, { minimumCents = 100 } = {}) {\n  let sent = 0;\n  for (const [name, cents] of Object.entries(balances)) {\n    if (cents === 0 || Math.abs(cents) < minimumCents) continue;\n    try {\n      sendReminder(name, `You owe ${formatMoney(-cents)} to the group`);\n      sent += 1;\n    } catch (error) {\n      console.warn(`Could not remind ${name}: ${error.message}`);\n    }\n  }\n  return sent;\n}\n"},{"id":"swapped-args","code":"function formatMoney(cents) {\n  const sign = cents < 0 ? '-' : '';\n  const abs = Math.abs(cents);\n  return `${sign}$${Math.floor(abs / 100)}.${String(abs % 100).padStart(2, '0')}`;\n}\n\n// Sends a reminder to everyone who owes money (a negative balance).\n// - Calls sendReminder(name, message) once per debtor, with a message\n//   such as \"You owe $5.00 to the group\".\n// - Skips debts smaller than minimumCents (default 100, i.e. $1.00);\n//   a debt of exactly minimumCents IS reminded.\n// - People who are owed money or are settled up get nothing.\n// - If sendReminder throws for someone, logs\n//   console.warn(\"Could not remind <name>: <error message>\")\n//   and carries on with the others.\n// Returns the number of reminders that were sent successfully.\nfunction remindDebtors(balances, sendReminder, { minimumCents = 100 } = {}) {\n  let sent = 0;\n  for (const [name, cents] of Object.entries(balances)) {\n    if (cents >= 0 || -cents < minimumCents) continue;\n    try {\n      sendReminder(`You owe ${formatMoney(-cents)} to the group`, name);\n      sent += 1;\n    } catch (error) {\n      console.warn(`Could not remind ${name}: ${error.message}`);\n    }\n  }\n  return sent;\n}\n"},{"id":"skips-exact-minimum","code":"function formatMoney(cents) {\n  const sign = cents < 0 ? '-' : '';\n  const abs = Math.abs(cents);\n  return `${sign}$${Math.floor(abs / 100)}.${String(abs % 100).padStart(2, '0')}`;\n}\n\n// Sends a reminder to everyone who owes money (a negative balance).\n// - Calls sendReminder(name, message) once per debtor, with a message\n//   such as \"You owe $5.00 to the group\".\n// - Skips debts smaller than minimumCents (default 100, i.e. $1.00);\n//   a debt of exactly minimumCents IS reminded.\n// - People who are owed money or are settled up get nothing.\n// - If sendReminder throws for someone, logs\n//   console.warn(\"Could not remind <name>: <error message>\")\n//   and carries on with the others.\n// Returns the number of reminders that were sent successfully.\nfunction remindDebtors(balances, sendReminder, { minimumCents = 100 } = {}) {\n  let sent = 0;\n  for (const [name, cents] of Object.entries(balances)) {\n    if (cents >= 0 || -cents <= minimumCents) continue;\n    try {\n      sendReminder(name, `You owe ${formatMoney(-cents)} to the group`);\n      sent += 1;\n    } catch (error) {\n      console.warn(`Could not remind ${name}: ${error.message}`);\n    }\n  }\n  return sent;\n}\n"},{"id":"stops-on-failure","code":"function formatMoney(cents) {\n  const sign = cents < 0 ? '-' : '';\n  const abs = Math.abs(cents);\n  return `${sign}$${Math.floor(abs / 100)}.${String(abs % 100).padStart(2, '0')}`;\n}\n\n// Sends a reminder to everyone who owes money (a negative balance).\n// - Calls sendReminder(name, message) once per debtor, with a message\n//   such as \"You owe $5.00 to the group\".\n// - Skips debts smaller than minimumCents (default 100, i.e. $1.00);\n//   a debt of exactly minimumCents IS reminded.\n// - People who are owed money or are settled up get nothing.\n// - If sendReminder throws for someone, logs\n//   console.warn(\"Could not remind <name>: <error message>\")\n//   and carries on with the others.\n// Returns the number of reminders that were sent successfully.\nfunction remindDebtors(balances, sendReminder, { minimumCents = 100 } = {}) {\n  let sent = 0;\n  for (const [name, cents] of Object.entries(balances)) {\n    if (cents >= 0 || -cents < minimumCents) continue;\n    try {\n      sendReminder(name, `You owe ${formatMoney(-cents)} to the group`);\n      sent += 1;\n    } catch (error) {\n      throw error;\n    }\n  }\n  return sent;\n}\n"}],"starter":{"js":"describe('remindDebtors', () => {\n  let warn;\n\n  beforeEach(() => {\n    // Watch console.warn and keep the test output quiet.\n    warn = vi.spyOn(console, 'warn').mockImplementation(() => {});\n  });\n\n  afterEach(() => {\n    vi.restoreAllMocks(); // puts the real console.warn back\n  });\n\n  test('returns how many reminders were sent', () => {\n    const sendReminder = vi.fn();\n\n    const sent = remindDebtors({ Ana: 1500, Ben: -1500 }, sendReminder);\n\n    expect(sent).toBe(1);\n  });\n\n  // Add tests with vi.fn() and toHaveBeenCalledWith / toHaveBeenCalledTimes.\n});\n"},"solution":{"js":"describe('remindDebtors', () => {\n  let warn;\n\n  beforeEach(() => {\n    warn = vi.spyOn(console, 'warn').mockImplementation(() => {});\n  });\n\n  afterEach(() => {\n    vi.restoreAllMocks(); // puts the real console.warn back\n  });\n\n  test('returns how many reminders were sent', () => {\n    const sendReminder = vi.fn();\n\n    const sent = remindDebtors({ Ana: 1500, Ben: -1500 }, sendReminder);\n\n    expect(sent).toBe(1);\n  });\n\n  test('reminds only debtors, with their name and the amount', () => {\n    const sendReminder = vi.fn();\n\n    remindDebtors({ Ana: 1500, Ben: -500, Cai: 0 }, sendReminder);\n\n    expect(sendReminder).toHaveBeenCalledTimes(1);\n    expect(sendReminder).toHaveBeenCalledWith('Ben', 'You owe $5.00 to the group');\n  });\n\n  test('reminds a debt of exactly the minimum, but not less', () => {\n    const sendReminder = vi.fn();\n\n    remindDebtors({ Ben: -100, Dee: -99, Ana: 199 }, sendReminder);\n\n    expect(sendReminder).toHaveBeenCalledTimes(1);\n    expect(sendReminder).toHaveBeenCalledWith('Ben', 'You owe $1.00 to the group');\n  });\n\n  test('warns and carries on when one reminder fails', () => {\n    const sendReminder = vi.fn().mockImplementation((name) => {\n      if (name === 'Ben') throw new Error('Mailbox full');\n    });\n\n    const sent = remindDebtors({ Ben: -500, Dee: -300, Ana: 800 }, sendReminder);\n\n    expect(sent).toBe(1);\n    expect(sendReminder).toHaveBeenCalledWith('Dee', 'You owe $3.00 to the group');\n    expect(warn).toHaveBeenCalledWith('Could not remind Ben: Mailbox full');\n  });\n});\n"},"checks":[{"id":"passes","suite":"subject"},{"id":"catches-reminds-creditors","suite":"reminds-creditors"},{"id":"catches-swapped-args","suite":"swapped-args"},{"id":"catches-skips-exact-minimum","suite":"skips-exact-minimum"},{"id":"catches-stops-on-failure","suite":"stops-on-failure"}],"title":"Mocks لـ remindDebtors","prompt":"<p>تستدعي <code dir=\"ltr\">remindDebtors(balances, sendReminder)</code> الدالة <code dir=\"ltr\">sendReminder(name, message)</code> لكل شخص عليه $1.00 على الأقل، وتحذّر عبر <code dir=\"ltr\">console.warn</code> حين يفشل تذكير، وتعيد عدد التذكيرات التي أرسلتها. الأرصدة الموجبة لأشخاص لهم مال؛ والسالبة لأشخاص عليهم مال. القواعد مذكورة في تعليق الـ subject.</p>\n<p>يتجسّس الكود المبدئي على <code dir=\"ltr\">console.warn</code> مسبقًا (ويستعيده بعد كل اختبار). استخدم <code dir=\"ltr\">vi.fn()</code> مكان <code dir=\"ltr\">sendReminder</code> وأضف اختبارات حتى تلتقط مجموعتك أربعة أخطاء مخفيّة: نسخة تذكّر من <strong>لهم</strong> مال، ونسخة <strong>تبادل</strong> موقعي المعاملين، ونسخة تتخطّى دينًا قيمته $1.00 <strong>بالضبط</strong>، ونسخة <strong>تتوقّف</strong> عند أول تذكير فاشل.</p>\n","hints":["مرّر <code dir=\"ltr\">vi.fn()</code> وافحصه بـ <code dir=\"ltr\">toHaveBeenCalledWith('Ben', 'You owe $5.00 to the group')</code> و<code dir=\"ltr\">toHaveBeenCalledTimes(1)</code>. ترتيب المعاملات بالضبط مهم.","الحدّ: الرصيد -100 يجب أن يُذكَّر صاحبه، و-99 يجب ألا يُذكَّر.","لحالة الفشل، اجعل الـ mock يرمي خطأً لاسم واحد: <code dir=\"ltr\">vi.fn().mockImplementation((name) =&gt; { if (name === 'Ben') throw new Error('Mailbox full'); })</code>، ثم أكّد أن المدين التالي استُدعي رغم ذلك، وأن <code dir=\"ltr\">warn</code> استُدعيت بالرسالة."],"explanation":"<p>يفحص <code dir=\"ltr\">toHaveBeenCalledWith</code> المعاملات بالترتيب، فيلتقط النسخة التي تبادل موقعيهما، و<code dir=\"ltr\">toHaveBeenCalledTimes(1)</code> مع وجود دائن في الأرصدة يلتقط النسخة التي تذكّر الجميع. اختبار الحدّ يستخدم -100 و-99، واحدًا على كل جهة من الحدّ؛ و-100، الواقع على الحدّ تمامًا، هو المدخل الذي يميّز <code dir=\"ltr\">&lt;</code> من <code dir=\"ltr\">&lt;=</code>. واختبار الفشل يجعل الـ mock يرمي خطأً لشخص واحد ويتحقّق من أن الشخص <strong>التالي</strong> ذُكِّر رغم ذلك؛ أما النسخة التي تعيد رمي الخطأ فلا تصل إليه أبدًا فيفشل الاختبار. والـ spy على <code dir=\"ltr\">console.warn</code> يحوّل أثرًا جانبيًا كنت ستتجاهله عادةً إلى شيء تستطيع تأكيده، واستعادته في <code dir=\"ltr\">afterEach</code> تمنعه من التسرّب إلى اختبارات أخرى.</p>\n","checkLabels":{"passes":"كل اختباراتك تنجح على remindDebtors الحقيقية","catches-reminds-creditors":"تلتقط النسخة التي تذكّر من لهم مال","catches-swapped-args":"تلتقط النسخة التي تبادل موقعي الاسم والرسالة","catches-skips-exact-minimum":"تلتقط النسخة التي تتخطّى دينًا قيمته $1.00 بالضبط","catches-stops-on-failure":"تلتقط النسخة التي تتوقّف عند أول تذكير فاشل"}},"words":564,"readMinutes":3}