{"id":"l-tj-2-1","courseId":"testing-javascript","lang":"ar","kind":"lesson","summary":"تكتب اختبارات تثبّت عقد الدالة (المدخلات والمخرجات والثوابت) كي تصمد أمام إعادة الهيكلة ولا تفشل إلا حين يتغيّر السلوك فعلًا.","html":"<p>لمجموعة الاختبارات مهمّتان: <strong>أن تفشل حين ينكسر السلوك</strong>، و<strong>أن تبقى خضراء حين لا ينكسر</strong>. معظم الفرق تركّز على الأولى وتنسى الثانية. ثم يعيد أحدهم تسمية دالة خاصة، فيتحوّل أربعون اختبارًا إلى الأحمر، دون أن يتغيّر أي سلوك، والدرس الذي يتعلّمه الفريق هو “الاختبارات تبطّئنا”.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">السلوك مقابل التفاصيل الداخلية</h2>\n<p>إليك <code dir=\"ltr\">splitEvenly</code> في Splitwise-lite، التي تقسّم فاتورة بالسنتات بين الأشخاص. تذهب السنتات المتبقّية إلى أوائل الأشخاص في القائمة، فتكون النتيجة عادلة (لا تختلف الحصص بأكثر من سنت واحد) وحتمية:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-run=\"js\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"js\" data-index=\"0\" 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\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>تشغيل</span></button><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\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> splitEvenly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">totalCents</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">people</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\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">Number.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">isInteger</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(people) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">||</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> people </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;</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:#CF222E;--shiki-dark:#FF7B72\">    throw</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> RangeError</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'people must be a positive integer'</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:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> base</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\">(totalCents </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> people);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> remainder</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> totalCents </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> people;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Array.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ length: people }, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">_</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">i</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (i </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> remainder </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> base </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> base));</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:#1F2328;--shiki-dark:#E6EDF3\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">splitEvenly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// $10.00 three ways</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">splitEvenly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1001</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">splitEvenly</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:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">));</span></span></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p><strong>سلوكها</strong> هو كل ما يستطيع المستدعي ملاحظته: المصفوفة التي تعيدها، والخطأ الذي ترميه حين يكون عدد الأشخاص صفرًا. أما <strong>تفاصيلها الداخلية</strong> فهي الطريقة: <code dir=\"ltr\">Math.floor</code>، والعامل <code dir=\"ltr\">%</code>، و<code dir=\"ltr\">Array.from</code>، وأسماء المتغيّرات. تستطيع إعادة كتابة جسم الدالة بحلقة، أو بتوزيع السنتات واحدًا تلو الآخر، دون أن يلاحظ أيّ مستدعٍ شيئًا.</p>\n<p>الاختبار الذي يفحص السلوك يصمد أمام كل إعادات الكتابة هذه. أما الاختبار الذي يفحص التفاصيل الداخلية فينكسر مع كل واحدة منها، رغم أن شيئًا مما يهمّ المستخدم لم يتغيّر. هذا <em>إنذار كاذب</em>، والإنذارات الكاذبة مكلفة: كل واحد منها يكلّف جلسة تتبّع أخطاء، وتراكمها يعلّم الناس إعادة تشغيل الاختبارات الحمراء أو حذفها دون قراءتها.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 640 220\" role=\"img\" aria-labelledby=\"l-tj-2-1-t1\"><defs><marker id=\"arrow-l-tj-2-1-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-tj-2-1-t1\">ترسل الاختبارات مدخلات إلى splitEvenly وتفحص المخرجات؛ والدوال المساعدة الداخلية داخل المربّع المتقطّع لا تلمسها الاختبارات.</title>\n  <rect class=\"d-box-accent\" x=\"20\" y=\"70\" width=\"150\" height=\"80\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"95\" y=\"105\" text-anchor=\"middle\">الاختبارات</text>\n  <text class=\"d-label-muted\" x=\"95\" y=\"128\" text-anchor=\"middle\">مدخلات، مخرجات</text>\n  <path class=\"d-arrow\" d=\"M170 95 L258 95\" marker-end=\"url(#arrow-l-tj-2-1-1)\"/>\n  <path class=\"d-arrow\" d=\"M258 128 L172 128\" marker-end=\"url(#arrow-l-tj-2-1-1)\"/>\n  <rect class=\"d-box-primary\" x=\"260\" y=\"30\" width=\"360\" height=\"160\" rx=\"14\"/>\n  <text class=\"d-label-strong\" x=\"300\" y=\"62\">splitEvenly(total, people)</text>\n  <rect class=\"d-box d-dashed\" x=\"290\" y=\"80\" width=\"300\" height=\"90\" rx=\"10\"/>\n  <text class=\"d-label-muted\" x=\"440\" y=\"112\" text-anchor=\"middle\">الداخل: floor و% وArray.from</text>\n  <text class=\"d-label-muted\" x=\"440\" y=\"140\" text-anchor=\"middle\">حرّ في التغيّر</text>\n</svg><figcaption>الاختبارات تخاطب العقد العام، لا الأجزاء الداخلية</figcaption></figure>\n<h2 id=\"sec-2\" tabindex=\"-1\">كيف تبدو الاختبارات المرتبطة بالتفاصيل الداخلية</h2>\n<p>هذه أنماط حقيقية حذفتها بنفسي من قواعد كود:</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:#6E7781;--shiki-dark:#8B949E\">// Spying on Math.floor: breaks if you switch to integer division another way</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> floor</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\">(Math, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'floor'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">splitEvenly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</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\">(floor).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveBeenCalledWith</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1000</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> /</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Testing an exported private helper: breaks when you inline or rename it</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\">_remainderFor</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</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:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Snapshotting the source-shaped output of a debug helper nobody calls</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\">describeSplit</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toMatchSnapshot</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span></code></pre></div>\n<p>كل واحد منها ينجح اليوم ولا يخبرك بشيء لا يخبرك به اختبار السلوك. وكل واحد منها سيفشل في اليوم الذي يحسّن فيه أحدهم الكود.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">اختبار العقد بدلًا من ذلك</h2>\n<p>ابدأ مما يعتمد عليه المستدعي. بالنسبة إلى <code dir=\"ltr\">splitEvenly</code> فهو:</p>\n<ol>\n<li>حصة واحدة لكل شخص.</li>\n<li><strong>مجموع الحصص يساوي الإجمالي.</strong> لا يضيع سنت، ولا يُختلَق سنت.</li>\n<li>لا تختلف الحصص بأكثر من سنت واحد.</li>\n<li>تذهب السنتات الإضافية إلى أوائل الأشخاص (وهذا موثّق، فقد يعتمد عليه المستدعون).</li>\n<li>عدد الأشخاص صفر خطأ.</li>\n</ol>\n<p>النقطتان 2 و3 <em>ثوابت</em> (invariants): تصحّ مع كل مدخل صالح، لذا تستطيع فحصها على عدة مدخلات دفعة واحدة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">src/split.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\"> { splitEvenly } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './split.js'</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:#8250DF;--shiki-dark:#D2A8FF\"> sum</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">xs</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> xs.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">reduce</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">a</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">b</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> a </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> b, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</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\">'splitEvenly'</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\">'never loses or invents a cent'</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\">    for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">total</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">people</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">of</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">], [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1001</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">4</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:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">], [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">5</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\">sum</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">splitEvenly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(total, people))).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(total);</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:#8250DF;--shiki-dark:#D2A8FF\">  test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'gives the leftover cents to the first people'</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\">splitEvenly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1001</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toEqual</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">([</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">251</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">250</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">250</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">250</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\">'refuses to split between zero people'</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:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> splitEvenly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toThrow</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(RangeError);</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>ثابت المجموع هو أثمن سطر في الملف. النسخة التي تُسقط الباقي تفشل فيه. وكذلك الخطأ بفارق واحد (off-by-one) الذي يوزّع سنتًا زائدًا. لا يهمّه كيف تُحسب الحصص، بل يهمّه فقط أن المال محفوظ.</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>إن شعرت أن دالة مساعدة مهمّة بما يكفي لتُختبر وحدها، فاسأل: أيّ سلوك عام يعتمد عليها؟ ثم اختبر ذلك السلوك بمدخل يشغّل الدالة المساعدة. وإن كانت الدالة المساعدة فعلًا مفهومًا مستقلًّا يُستخدم في عدة أماكن، فارفعها إلى وحدة برمجية حقيقية لها عقدها الخاص. أما “مُصدَّرة من أجل الاختبارات فقط” فهي أسوأ الحالين: عامة، لكن دون أي وعود.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">ما مدى الدقّة المطلوبة في الـ assertions؟</h2>\n<p>يمكن لاختبارات السلوك أن تبقى دقيقة. <code dir=\"ltr\">toEqual([251, 250, 250, 250])</code> يثبّت الترتيب الموثّق للسنتات الإضافية، وهذا سلوك لأن المستدعين يعتمدون عليه. السؤال ليس أبدًا “دقيق أم مرن؟” بل “هل سيلاحظ مستخدم أو مستدعٍ لو تغيّر هذا؟” إن كانت الإجابة نعم، فأكّده بدقّة. وإن كانت لا، فلا تؤكّده أصلًا.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">حين تكسر إعادة الهيكلة اختبارًا فعلًا</h2>\n<p>أحيانًا <em>يجب</em> أن يغيّر تعديل الكود اختبارًا ما، ومن المفيد أن تعرف في أيّ حالة أنت. إن تغيّر العقد العام (صارت الدالة تعيد كائنات بدل أرقام، أو صارت السنتات الإضافية تذهب إلى الدافع)، فالاختبار محقّ في فشله، وتحدّثه عن قصد، ويُفضَّل أن يكون ذلك قبل تغيير الكود. أما إن بقي العقد كما هو وفشل اختبار رغم ذلك، فقد كان الاختبار مرتبطًا بالتفاصيل الداخلية: أعد كتابته على أساس العقد واحذف القديم. المجموعة السليمة تجعل الإجابة عن هذا السؤال سهلة، لأن اسم كل اختبار يذكر وعدًا يقطعه الكود.</p>\n<p>في التمرين، اكتب اختبارات سلوك لـ <code dir=\"ltr\">splitEvenly</code> تلتقط أربعة أخطاء واقعية. والدرس القادم عن إيجاد المدخلات التي تكشف أخطاءً كهذه.</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":"ما مدى الدقّة المطلوبة في الـ assertions؟","level":2},{"id":"sec-5","text":"حين تكسر إعادة الهيكلة اختبارًا فعلًا","level":2}],"takeaways":["السلوك هو ما يستطيع المستدعي ملاحظته (القيم المعادة، والأخطاء المرمية، والآثار الجانبية الظاهرة)؛ أما التفاصيل الداخلية (implementation) فهي الطريقة التي يصل بها الكود إلى ذلك.","الاختبار الذي ينكسر حين تعيد الهيكلة دون تغيير السلوك إنذار كاذب، والإنذارات الكاذبة تعلّم الفريق تجاهل البناء الأحمر.","الثوابت (invariants) مثل “مجموع الحصص يساوي الإجمالي” تلتقط عائلات كاملة من الأخطاء بـ assertion واحد.","لا تصدّر الدوال المساعدة الخاصة لمجرّد اختبارها؛ اختبرها من خلال الدالة العامة التي تستخدمها."],"further":[{"title":"Testing Library — Guiding principles","url":"https://testing-library.com/docs/guiding-principles"},{"title":"Vitest — expect API","url":"https://vitest.dev/api/expect"}],"quiz":[{"q":"<p>تعيد كتابة <code dir=\"ltr\">splitEvenly</code> من <code dir=\"ltr\">Array.from</code> إلى حلقة <code dir=\"ltr\">for</code> عادية. مخرجاتها متطابقة مع كل مدخل. فيتحوّل اختباران إلى الأحمر. ماذا يخبرك ذلك عن هذين الاختبارين؟</p>","options":[{"text":"كانا يختبران التفاصيل الداخلية لا السلوك.","why":"صحيح. إن لم يتغيّر السلوك وفشلت الاختبارات، فالاختبارات كانت مرتبطة بطريقة عمل الكود، وهذا يجعل كل إعادة هيكلة مكلفة."},{"text":"أدخلت إعادة الهيكلة خطأً فاتَ بقية الاختبارات.","why":"الفرضية أن المخرجات متطابقة مع كل مدخل؛ والاختبار الذي يفشل رغم ذلك يبلّغ عن البنية، لا عن خطأ."},{"text":"الاختباران صحيحان ويجب التراجع عن الحلقة.","why":"التراجع عن إعادة هيكلة غير ضارّة إرضاءً لاختبار قلبٌ للأولويات، فالاختبار يخدم الكود لا العكس؛ أصلح الاختبارين كي يفحصا المخرجات بدلًا من ذلك."}],"answer":0},{"q":"<p>أيّ assertion واحد سيلتقط أكبر عدد من الأخطاء المختلفة في <code dir=\"ltr\">splitEvenly(totalCents, people)</code>؟</p>","options":[{"text":"<code dir=\"ltr\">expect(shares).toHaveLength(people)</code>","why":"مفيد، لكن النسخة التي تضيّع السنتات المتبقّية ما زالت تعيد العدد الصحيح من الحصص."},{"text":"<code dir=\"ltr\">expect(Math.max(...shares) - Math.min(...shares)).toBeLessThanOrEqual(1)</code>","why":"يلتقط التقسيم غير العادل، لكن النسخة التي تُسقط الباقي تعطي الجميع الحصة نفسها فتنجح فيه."},{"text":"<code dir=\"ltr\">expect(shares.reduce((a, b) =&gt; a + b, 0)).toBe(totalCents)</code>","why":"صحيح. ضياع سنت، واختلاق سنت، وسوء التعامل مع الباقي، كلها تكسر المجموع. هذا هو الثابت الذي لا يجوز لكود المال أن ينتهكه أبدًا."}],"answer":2},{"q":"<p>يصدّر زميلك <code dir=\"ltr\">_remainderFor(total, people)</code> من <code dir=\"ltr\">split.js</code> كي يختبرها كـ unit test مباشرة. ما الطريقة الأفضل؟</p>","options":[{"text":"أبقِ التصدير لكن ضع قبل الاسم شرطة سفلية كي يعرف الناس أنها خاصة.","why":"الشرطة السفلية لا تمنع الاستيراد؛ صارت الدالة المساعدة الآن جزءًا من الـ API العام سترتبط به الاختبارات (وأكواد أخرى)."},{"text":"اختبر سلوك الباقي من خلال <code dir=\"ltr\">splitEvenly</code>، باستخدام مدخلات تترك باقيًا.","why":"صحيح. لا تهمّ الدالة المساعدة إلا بسبب ما تعيده <code dir=\"ltr\">splitEvenly</code>، فالاختبار عبر الدالة العامة يغطّيها ويتركك حرًّا في حذفها أو إعادة تسميتها."},{"text":"انسخ الدالة المساعدة إلى ملف الاختبار واختبر النسخة.","why":"عندها يفحص الاختبار نسخة قد تبتعد عن الكود الحقيقي؛ وسيبقى أخضر بينما تنكسر الدالة الحقيقية."}],"answer":1}],"exercise":{"kind":"web","mode":"test","subject":"// Splits a bill in integer cents between people.\n// Contract:\n//   - one share per person\n//   - the shares add up exactly to totalCents\n//   - shares differ by at most one cent\n//   - leftover cents go to the FIRST people in the list\n//   - fewer than one person throws a RangeError\nfunction splitEvenly(totalCents, people) {\n  if (!Number.isInteger(people) || people < 1) {\n    throw new RangeError('people must be a positive integer');\n  }\n  const base = Math.floor(totalCents / people);\n  const remainder = totalCents % people;\n  return Array.from({ length: people }, (_, i) => (i < remainder ? base + 1 : base));\n}\n","mutants":[{"id":"drops-remainder","code":"// Splits a bill in integer cents between people.\n// Contract:\n//   - one share per person\n//   - the shares add up exactly to totalCents\n//   - shares differ by at most one cent\n//   - leftover cents go to the FIRST people in the list\n//   - fewer than one person throws a RangeError\nfunction splitEvenly(totalCents, people) {\n  if (!Number.isInteger(people) || people < 1) {\n    throw new RangeError('people must be a positive integer');\n  }\n  const base = Math.floor(totalCents / people);\n  const remainder = totalCents % people;\n  return Array.from({ length: people }, (_, i) => (base));\n}\n"},{"id":"extra-cent","code":"// Splits a bill in integer cents between people.\n// Contract:\n//   - one share per person\n//   - the shares add up exactly to totalCents\n//   - shares differ by at most one cent\n//   - leftover cents go to the FIRST people in the list\n//   - fewer than one person throws a RangeError\nfunction splitEvenly(totalCents, people) {\n  if (!Number.isInteger(people) || people < 1) {\n    throw new RangeError('people must be a positive integer');\n  }\n  const base = Math.floor(totalCents / people);\n  const remainder = totalCents % people;\n  return Array.from({ length: people }, (_, i) => (i <= remainder ? base + 1 : base));\n}\n"},{"id":"remainder-last","code":"// Splits a bill in integer cents between people.\n// Contract:\n//   - one share per person\n//   - the shares add up exactly to totalCents\n//   - shares differ by at most one cent\n//   - leftover cents go to the FIRST people in the list\n//   - fewer than one person throws a RangeError\nfunction splitEvenly(totalCents, people) {\n  if (!Number.isInteger(people) || people < 1) {\n    throw new RangeError('people must be a positive integer');\n  }\n  const base = Math.floor(totalCents / people);\n  const remainder = totalCents % people;\n  return Array.from({ length: people }, (_, i) => (i >= people - remainder ? base + 1 : base));\n}\n"},{"id":"zero-people","code":"// Splits a bill in integer cents between people.\n// Contract:\n//   - one share per person\n//   - the shares add up exactly to totalCents\n//   - shares differ by at most one cent\n//   - leftover cents go to the FIRST people in the list\n//   - fewer than one person throws a RangeError\nfunction splitEvenly(totalCents, people) {\n  if (!Number.isInteger(people) || people < 0) {\n    throw new RangeError('people must be a positive integer');\n  }\n  const base = Math.floor(totalCents / people);\n  const remainder = totalCents % people;\n  return Array.from({ length: people }, (_, i) => (i < remainder ? base + 1 : base));\n}\n"}],"starter":{"js":"const sum = (xs) => xs.reduce((a, b) => a + b, 0);\n\ndescribe('splitEvenly', () => {\n  test('returns one share per person', () => {\n    expect(splitEvenly(1000, 3)).toHaveLength(3);\n  });\n\n  // Test the rest of the contract in the subject's comment.\n});\n"},"solution":{"js":"const sum = (xs) => xs.reduce((a, b) => a + b, 0);\n\ndescribe('splitEvenly', () => {\n  test('returns one share per person', () => {\n    expect(splitEvenly(1000, 3)).toHaveLength(3);\n  });\n\n  test('never loses or invents a cent', () => {\n    for (const [total, people] of [[1000, 3], [1001, 4], [2, 3], [900, 3], [0, 4]]) {\n      expect(sum(splitEvenly(total, people))).toBe(total);\n    }\n  });\n\n  test('keeps shares within one cent of each other', () => {\n    const shares = splitEvenly(1001, 4);\n    expect(Math.max(...shares) - Math.min(...shares)).toBeLessThanOrEqual(1);\n  });\n\n  test('gives the leftover cents to the first people', () => {\n    expect(splitEvenly(1001, 4)).toEqual([251, 250, 250, 250]);\n    expect(splitEvenly(1000, 3)).toEqual([334, 333, 333]);\n  });\n\n  test('refuses to split between zero people', () => {\n    expect(() => splitEvenly(1000, 0)).toThrow(RangeError);\n  });\n});\n"},"checks":[{"id":"passes","suite":"subject"},{"id":"catches-drops-remainder","suite":"drops-remainder"},{"id":"catches-extra-cent","suite":"extra-cent"},{"id":"catches-remainder-last","suite":"remainder-last"},{"id":"catches-zero-people","suite":"zero-people"}],"title":"اختبارات سلوك لـ splitEvenly","prompt":"<p>يسرد التعليق في الـ subject عقد <code dir=\"ltr\">splitEvenly</code>. اكتب اختبارات لذلك العقد (لا لطريقة كتابة الدالة) حتى تنجح مجموعتك على الكود الحقيقي وتلتقط أربعة أخطاء مخفيّة:</p>\n<ul>\n<li>نسخة تُسقط السنتات المتبقّية،</li>\n<li>نسخة توزّع سنتًا متبقّيًا زائدًا،</li>\n<li>نسخة تعطي السنتات المتبقّية إلى <strong>آخر</strong> الأشخاص بدل أوّلهم،</li>\n<li>نسخة تعيد مصفوفة فارغة حين يكون عدد الأشخاص صفرًا بدل أن ترمي خطأً.</li>\n</ul>\n<p>الدالة المساعدة <code dir=\"ltr\">sum</code> موجودة مسبقًا في الكود المبدئي.</p>\n","hints":["المدخلات التي تنقسم بالتساوي (مثل 900 على 3) تُخفي كل أخطاء الباقي. استخدم إجماليات تترك باقيًا، مثل 1000 على 3 أو 1001 على 4.","ثابت المجموع يلتقط خطأين دفعة واحدة: <code dir=\"ltr\">expect(sum(splitEvenly(1000, 3))).toBe(1000)</code>.","لقاعدة الترتيب، قارن المصفوفة كلها: يجب أن تساوي <code dir=\"ltr\">splitEvenly(1001, 4)</code> القيمة <code dir=\"ltr\">[251, 250, 250, 250]</code>. ولعدد الأشخاص صفر، استخدم <code dir=\"ltr\">expect(() =&gt; splitEvenly(1000, 0)).toThrow(RangeError)</code>."],"explanation":"<p>ثابت المجموع يحمل العبء الأكبر: إسقاط الباقي يجعل الإجمالي أصغر من اللازم، والخطأ بفارق واحد يجعله أكبر، فيقضي assertion واحد على الاثنين. قاعدة الترتيب جزء من العقد الموثّق، لذا فإن تأكيد المصفوفة بالضبط اختبار سلوك لا اختبار تفاصيل داخلية؛ وهو الطريقة الوحيدة لالتقاط النسخة التي تفضّل آخر الأشخاص، لأنها تحفظ المال وتبقى عادلة. أما حالة الصفر فهي حدّ كانت النسخة المعطوبة تعيد فيه <code dir=\"ltr\">[]</code> بصمت، وكان المستدعي سيقسّم فاتورة على لا أحد. لا يذكر أيّ من هذه الاختبارات <code dir=\"ltr\">Math.floor</code> أو <code dir=\"ltr\">%</code> أو <code dir=\"ltr\">Array.from</code>، فتستطيع إعادة كتابة الدالة بحرّية وتبقى الاختبارات خضراء.</p>\n","checkLabels":{"passes":"كل اختباراتك تنجح على splitEvenly الحقيقية","catches-drops-remainder":"تلتقط النسخة التي تُسقط السنتات المتبقّية","catches-extra-cent":"تلتقط النسخة التي توزّع سنتًا زائدًا","catches-remainder-last":"تلتقط النسخة التي تعطي السنتات المتبقّية لآخر الأشخاص","catches-zero-people":"تلتقط النسخة التي تقبل عدد أشخاص صفرًا"}},"words":562,"readMinutes":3}