{"id":"l-tj-5-1","courseId":"testing-javascript","lang":"ar","kind":"lesson","summary":"تستخدم دورة red-green-refactor لبناء منطق محدّد المواصفات ولإصلاح الأخطاء، وتميّز الحالات التي تبطّئك فيها كتابة الاختبارات أولًا.","html":"<p>للتطوير الموجّه بالاختبارات (Test-driven development أو TDD) مشكلة في سمعته. بعض الناس يعاملونه كدين؛ وآخرون يرفضونه بعد تمرين واحد مؤلم. أنا أعامله كأداة، ومثل أي أداة فهو ممتاز لبعض المهام وخاطئ لأخرى. أما بالنسبة إلى منطق المال الأساسي في Splitwise-lite، فهو أفضل أداة أعرفها.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">الدورة</h2>\n<p>الـ TDD حلقة قصيرة، تستغرق عادةً دقيقة أو اثنتين في كل جولة:</p>\n<ol>\n<li><strong>Red (الأحمر)</strong>: اكتب اختبارًا صغيرًا واحدًا لسلوك غير موجود بعد. شغّله. وشاهده يفشل، ويُفضَّل أن يكون بالفشل الذي توقّعته.</li>\n<li><strong>Green (الأخضر)</strong>: اكتب أبسط كود يجعله ينجح. بسيط، لا ذكي؛ والتثبيت الصريح للقيم (hard-coding) مسموح به إن كان صادقًا.</li>\n<li><strong>Refactor (إعادة الهيكلة)</strong>: نظّف الكود (والاختبارات) وكل شيء يبقى أخضر.</li>\n</ol>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 640 260\" role=\"img\" aria-labelledby=\"l-tj-5-1-t1\"><defs><marker id=\"arrow-l-tj-5-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-5-1-t1\">دورة من ثلاثة صناديق: الأحمر، اكتب اختبارًا فاشلًا؛ الأخضر، اجعله ينجح؛ إعادة الهيكلة، نظّف والاختبارات خضراء؛ ثم العودة إلى الأحمر.</title>\n  <rect class=\"d-box-warn\" x=\"240\" y=\"16\" width=\"160\" height=\"66\" rx=\"14\"/>\n  <text class=\"d-label-strong\" x=\"320\" y=\"44\" text-anchor=\"middle\">Red</text>\n  <text class=\"d-label-muted\" x=\"320\" y=\"68\" text-anchor=\"middle\">اختبار فاشل</text>\n  <rect class=\"d-box-success\" x=\"430\" y=\"160\" width=\"180\" height=\"66\" rx=\"14\"/>\n  <text class=\"d-label-strong\" x=\"520\" y=\"188\" text-anchor=\"middle\">Green</text>\n  <text class=\"d-label-muted\" x=\"520\" y=\"212\" text-anchor=\"middle\">أبسط كود ناجح</text>\n  <rect class=\"d-box-primary\" x=\"30\" y=\"160\" width=\"180\" height=\"66\" rx=\"14\"/>\n  <text class=\"d-label-strong\" x=\"120\" y=\"188\" text-anchor=\"middle\">Refactor</text>\n  <text class=\"d-label-muted\" x=\"120\" y=\"212\" text-anchor=\"middle\">نظّف، وابقَ أخضر</text>\n  <path class=\"d-arrow\" d=\"M400 60 Q500 80 515 155\" marker-end=\"url(#arrow-l-tj-5-1-1)\"/>\n  <path class=\"d-arrow\" d=\"M430 205 L214 205\" marker-end=\"url(#arrow-l-tj-5-1-1)\"/>\n  <path class=\"d-arrow\" d=\"M125 158 Q140 80 236 56\" marker-end=\"url(#arrow-l-tj-5-1-1)\"/>\n</svg><figcaption>Red ثم green ثم refactor</figcaption></figure>\n<p>لماذا الإصرار على رؤية الأحمر؟ لأن الاختبار الذي لم تره يفشل قط قد لا يكون قادرًا على الفشل. قد يستدعي الدالة الخاطئة، أو يؤكّد متغيّرًا قيمته truthy دائمًا، أو لا يفحص شيئًا في الحقيقة (<code dir=\"ltr\">await</code> مفقودة، أو <code dir=\"ltr\">.skip</code> منسيّة في مكانها). الأحمر أولًا دليل رخيص على أن الاختبار موصول بشكل صحيح.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">بناء settleUp بكتابة الاختبار أولًا</h2>\n<p>يحتاج Splitwise-lite إلى <code dir=\"ltr\">settleUp(balances)</code>: بمعرفة من له ومن عليه (بالسنتات)، تعيد قائمة قصيرة من التحويلات تعيد الجميع إلى الصفر. إليك كيف تسير الجولات الأولى، مع Vitest في وضع المراقبة كي يعيد كل حفظ تشغيل الاختبارات.</p>\n<p><strong>الجولة 1، أبسط حالة.</strong> لا أحد مدين بشيء:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/settle-up.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:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'needs no transfers when everyone is settled'</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\">settleUp</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ Ana: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, Ben: </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\">toEqual</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\">settleUp is not defined</code>. الأخضر: <code dir=\"ltr\">export function settleUp() { return []; }</code>. نعم، هذا هو التطبيق كله في الوقت الحالي. إنه صادق بشأن ما تطلبه الاختبارات.</p>\n<p><strong>الجولة 2، دين واحد.</strong> يفرض الاختبار التالي سلوكًا حقيقيًا:</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:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'the debtor pays the creditor'</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\">settleUp</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ Ana: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">500</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\"> })).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toEqual</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">([</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    { from: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Ben'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, to: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Ana'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amountCents: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">500</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>قد يقرن الأخضر الرصيد السالب الوحيد بالرصيد الموجب الوحيد. ما زالت خطوة صغيرة.</p>\n<p><strong>الجولة 3، عدة أشخاص.</strong> الآن ينكسر الاقتران المثبّت يدويًا، وتستحق الخوارزمية العامة مكانها: رتّب المدينين والدائنين حسب المبلغ، وانقل مرة بعد مرة الأصغر من المبلغين الحاليين، وتقدّم في الجهة التي تصل إلى الصفر. وفي الـ assertions، استخدم <strong>الخصائص</strong> حيث تكون عدة إجابات صالحة: تطبيق التحويلات يعيد كل رصيد إلى الصفر، وكل مبلغ موجب، وعدد التحويلات لا يتجاوز <code dir=\"ltr\">people - 1</code>.</p>\n<p><strong>الجولة 4، مدخل غير صالح.</strong> الأرصدة التي لا يساوي مجموعها صفرًا تعني خطأً في مرحلة سابقة؛ ويجب أن ترفضها <code dir=\"ltr\">settleUp</code> بدل أن تختلق مالًا: <code dir=\"ltr\">expect(() =&gt; settleUp({ Ana: 500, Ben: -400 })).toThrow()</code>.</p>\n<p>ثم <strong>أعد الهيكلة</strong>: سمِّ دالة المقارنة في الترتيب، واستخرج دالة مساعدة، واجعل الكود سهل القراءة. تتيح لك الاختبارات تغيير شكل الكود دون خوف، وهذا هو المكسب الحقيقي.</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>اختر الاختبار الذي يفرض <strong>أصغر</strong> خطوة من السلوك الجديد. وإن كان اختبار ما سيجعلك تكتب الخوارزمية كلها دفعة واحدة، فابحث عن اختبار أبسط أولًا. وإن لم تستطع التفكير في اختبار فاشل، فربما انتهيت.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">إصلاح الأخطاء بكتابة الاختبار أولًا</h2>\n<p>أثمن استخدام يومي للـ TDD لا علاقة له بالميزات الجديدة. حين يُبلَّغ عن خطأ:</p>\n<ol>\n<li>اكتب اختبارًا يعيد إنتاجه، و<strong>شاهده يفشل</strong>. الآن تعرف أنك فهمت الخطأ.</li>\n<li>أصلح الكود حتى ينجح الاختبار.</li>\n<li>احتفظ بالاختبار. لن يعود ذلك الخطأ بصمت أبدًا.</li>\n</ol>\n<p>ينجح هذا حتى في قواعد الكود التي لم تُكتب قط بالاختبار أولًا، ويحوّل كل تقرير خطأ إلى تحسين دائم لمجموعة الاختبارات.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">متى لا تستخدمه</h2>\n<p>يفترض الـ TDD أنك تعرف معنى “الصحيح”. وحين لا تعرفه، فكتابة الاختبارات أولًا مجرّد تدوين لتخمينات:</p>\n<ul>\n<li><strong>العمل الاستكشافي على الواجهة</strong>: تجربة التخطيطات والتفاعلات والنصوص. ابنِها، وانظر إليها، واعرضها على أحد؛ ثم اختبر ما يبقى منها.</li>\n<li><strong>التجارب المؤقّتة (spikes)</strong>: تجربة تُرمى بعد استخدامها لتعرف إن كان نهج ما ينجح. احذفها، ثم ابنِ الشيء الحقيقي (بالاختبار أولًا إن شئت).</li>\n<li><strong>كود الربط الخالي من المنطق</strong>: وصل دالة راجعة في مكتبة بالـ state لديك. غطِّه باختبار integration أو end-to-end بدلًا من ذلك.</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>كتابة اختبارات كثيرة قبل أي كود</span></p>\n<p>الـ TDD اختبار واحد في كل مرة. كتابة عشرين اختبارًا فاشلًا مقدّمًا تخطيط، لا TDD: تفقد التغذية الراجعة الضيّقة لكل جولة أحمر-أخضر، ويكون على التطبيق الأول أن يُرضي كل شيء دفعة واحدة، وهذا بالضبط القفزة الكبيرة التي وُجد الـ TDD لتجنّبها.</p>\n</aside>\n<p>في التمرين تؤدّي النصف الأخضر من الدورة: اختبارات <code dir=\"ltr\">settleUp</code> مكتوبة (وهي الفحوص)، وأنت تكتب الكود الذي يجعلها تنجح. وفي الدرسين القادمين، ستقيس مدى جودة تلك الاختبارات.</p>\n","toc":[{"id":"sec-1","text":"الدورة","level":2},{"id":"sec-2","text":"بناء settleUp بكتابة الاختبار أولًا","level":2},{"id":"sec-3","text":"إصلاح الأخطاء بكتابة الاختبار أولًا","level":2},{"id":"sec-4","text":"متى لا تستخدمه","level":2}],"takeaways":["Red ثم green ثم refactor: اكتب اختبارًا واحدًا فاشلًا، واجعله ينجح بأبسط كود، ثم نظّف الكود والاختبارات خضراء.","رؤية الاختبار يفشل أولًا تثبت أنه قادر على الفشل؛ أما الاختبار المكتوب بعد الكود فقد ينجح لسبب خاطئ.","اختر الاختبار التالي بحيث يفرض خطوة صغيرة واحدة من السلوك الجديد، بادئًا من أبسط حالة.","يتألّق الـ TDD مع المنطق ذي القواعد الواضحة ومع إصلاح الأخطاء (أعد إنتاج الخطأ باختبار فاشل أولًا)؛ ولا يناسب العمل الاستكشافي على الواجهة ولا التجارب المؤقّتة."],"further":[{"title":"Vitest — Watch mode and filtering","url":"https://vitest.dev/guide/filtering"},{"title":"Vitest — expect API","url":"https://vitest.dev/api/expect"}],"quiz":[{"q":"<p>تكتب اختبارًا لحالة جديدة فينجح فورًا، قبل أن تغيّر أي كود. ماذا يجب أن تفعل؟</p>","options":[{"text":"احفظه في commit؛ فالاختبار الناجح هو الهدف.","why":"الاختبار الذي لم تره يفشل قط قد لا يختبر ما تظنّه، مثلًا لأنه يستدعي الدالة الخاطئة أو يؤكّد القيمة الخاطئة."},{"text":"اكسر الكود (أو التوقّع) مؤقتًا لتتأكّد أن الاختبار قادر على الفشل، ثم قرّر إن كانت الحالة مغطّاة أصلًا.","why":"صحيح. إمّا أن السلوك موجود أصلًا (لا بأس، أبقِ الاختبار كتوثيق) أو أن الاختبار معطوب؛ وتحتاج إلى أن تعرف أيّهما."},{"text":"احذف الاختبار، لأن اختبارات الـ TDD يجب أن تبدأ حمراء.","why":"قد يكون الاختبار توثيقًا قيّمًا لحالة ما؛ المقصود هو التحقّق من قدرته على الفشل، لا التخلّص منه."}],"answer":1},{"q":"<p>يبلّغ مستخدم أن تسوية حسابات مجموعة يدين فيها شخص واحد بمبلغ $0.01 تؤدّي إلى انهيار. ما طريقة الـ TDD لإصلاح ذلك؟</p>","options":[{"text":"أصلح الكود، ثم أضف اختبارًا للحالة كي يبقى الإصلاح قائمًا.","why":"أفضل من غياب الاختبار، لكنك لم ترَ الاختبار يفشل قط، فلا تستطيع التأكّد من أنه يعيد إنتاج الخطأ المبلَّغ عنه."},{"text":"أضف <code dir=\"ltr\">try/catch</code> حول استدعاء التسوية كي لا يصل الانهيار إلى المستخدم.","why":"هذا يُخفي الخطأ بدل أن يصلحه، وتبقى الأرصدة خاطئة."},{"text":"اكتب اختبارًا يعيد إنتاج الانهيار وشاهده يفشل، ثم أصلح الكود حتى ينجح.","why":"صحيح. الاختبار الأحمر يثبت أنك أعدت إنتاج الخطأ؛ والأخضر يثبت الإصلاح؛ ثم يحرس الاختبار من الانتكاس إلى الأبد."}],"answer":2},{"q":"<p>أيّ مهمّة هي الأقل ملاءمة للتطوير الصارم القائم على كتابة الاختبار أولًا؟</p>","options":[{"text":"تطبيق قواعد التقريب لتقسيم فاتورة.","why":"المدخلات والمخرجات الواضحة مع حالات حدّية صعبة هي حيث يكون الـ TDD في أقوى حالاته."},{"text":"إصلاح خطأ مبلَّغ عنه في حساب الأرصدة.","why":"إعادة إنتاج الخطأ باختبار فاشل أولًا من أثمن استخدامات الـ TDD."},{"text":"تجربة ثلاثة تخطيطات لشاشة الأرصدة لمعرفة أيّها يفهمه المستخدمون.","why":"صحيح. حين لا تعرف بعد ما الذي تبنيه، فالاختبارات المكتوبة أولًا تصف تخمينات؛ جرّب أولًا، ثم اختبر ما تحتفظ به."}],"answer":2}],"exercise":{"kind":"web","mode":"js","starter":{"js":"// Make the tests (the checks) pass, one at a time.\n// balances: { name: cents }. Positive = is owed money, negative = owes.\n// Return transfers: [{ from, to, amountCents }].\nfunction settleUp(balances) {\n  return [];\n}\n\nconsole.log(settleUp({ Ana: 500, Ben: -500 }));\n"},"solution":{"js":"// Make the tests (the checks) pass, one at a time.\n// balances: { name: cents }. Positive = is owed money, negative = owes.\n// Return transfers: [{ from, to, amountCents }].\nfunction settleUp(balances) {\n  const entries = Object.entries(balances);\n  const sum = entries.reduce((total, [, cents]) => total + cents, 0);\n  if (sum !== 0) throw new Error('Balances must add up to zero');\n\n  const debtors = [];\n  const creditors = [];\n  for (const [name, cents] of entries) {\n    if (cents < 0) debtors.push({ name, cents: -cents });\n    else if (cents > 0) creditors.push({ name, cents });\n  }\n  const largestFirst = (a, b) => b.cents - a.cents || a.name.localeCompare(b.name);\n  debtors.sort(largestFirst);\n  creditors.sort(largestFirst);\n\n  const transfers = [];\n  let d = 0;\n  let c = 0;\n  while (d < debtors.length && c < creditors.length) {\n    const amountCents = Math.min(debtors[d].cents, creditors[c].cents);\n    transfers.push({ from: debtors[d].name, to: creditors[c].name, amountCents });\n    debtors[d].cents -= amountCents;\n    creditors[c].cents -= amountCents;\n    if (debtors[d].cents === 0) d += 1;\n    if (creditors[c].cents === 0) c += 1;\n  }\n  return transfers;\n}\n\nconsole.log(settleUp({ Ana: 500, Ben: -500 }));\nconsole.log(settleUp({ Ana: 1500, Ben: -1000, Cai: -500 }));\n"},"checks":[{"id":"settled","test":"const t = settleUp({ Ana: 0, Ben: 0 }); return Array.isArray(t) && t.length === 0 && Array.isArray(settleUp({})) && settleUp({}).length === 0;"},{"id":"two-people","test":"const t = settleUp({ Ana: 500, Ben: -500 }); return Array.isArray(t) && t.length === 1 && t[0].from === 'Ben' && t[0].to === 'Ana' && t[0].amountCents === 500;"},{"id":"settles-everyone","test":"const cases = [\n  { Ana: 500, Ben: -500 },\n  { Ana: 1500, Ben: -1000, Cai: -500 },\n  { Ana: -700, Ben: 300, Cai: 400, Dee: 0 },\n  { Ana: 2500, Ben: -1000, Cai: -1000, Dee: -1000, Eve: 500 },\n  { Ana: -1, Ben: -1, Cai: 2 },\n];\nfor (const b of cases) {\n  const left = { ...b };\n  const t = settleUp({ ...b });\n  if (!Array.isArray(t) || (t.length === 0)) return false;\n  for (const x of t) {\n    if (!Number.isInteger(x.amountCents) || x.amountCents <= 0) return false;\n    if (!(x.from in left) || !(x.to in left)) return false;\n    left[x.from] += x.amountCents;\n    left[x.to] -= x.amountCents;\n  }\n  if (Object.values(left).some((v) => v !== 0)) return false;\n}\nreturn true;"},{"id":"few-transfers","test":"const cases = [\n  { Ana: 500, Ben: -500 },\n  { Ana: 1500, Ben: -1000, Cai: -500 },\n  { Ana: -700, Ben: 300, Cai: 400, Dee: 0 },\n  { Ana: 2500, Ben: -1000, Cai: -1000, Dee: -1000, Eve: 500 },\n  { Ana: -1, Ben: -1, Cai: 2 },\n];\nreturn cases.every((b) => {\n  const people = Object.values(b).filter((v) => v !== 0).length;\n  const t = settleUp({ ...b });\n  return Array.isArray(t) && t.length >= 1 && t.length <= people - 1;\n});"},{"id":"ignores-settled","test":"const t = settleUp({ Ana: -700, Ben: 300, Cai: 400, Dee: 0 }); return Array.isArray(t) && t.length > 0 && t.every((x) => x.from !== 'Dee' && x.to !== 'Dee');"},{"id":"rejects-unbalanced","test":"try { settleUp({ Ana: 500, Ben: -400 }); return false; } catch (e) { return e instanceof Error; }"},{"id":"no-mutation","test":"const b = { Ana: 1500, Ben: -1000, Cai: -500 }; settleUp(b); return b.Ana === 1500 && b.Ben === -1000 && b.Cai === -500 && Object.keys(b).length === 3;"}],"title":"اجعل اختبارات settleUp تنجح","prompt":"<p>هذه هي خطوة <strong>الأخضر</strong> في الـ TDD: الاختبارات مكتوبة أصلًا (وهي الفحوص أدناه)، ومهمّتك أن تجعلها تنجح.</p>\n<p>تأخذ <code dir=\"ltr\">settleUp(balances)</code> أرصدة بالسنتات (<code dir=\"ltr\">{ Ana: 1500, Ben: -1000, Cai: -500 }</code>: الموجب يعني أن للشخص مالًا، والسالب يعني أن عليه مالًا) وتعيد تحويلات مثل <code dir=\"ltr\">[{ from: 'Ben', to: 'Ana', amountCents: 1000 }, { from: 'Cai', to: 'Ana', amountCents: 500 }]</code>.</p>\n<p>تشترط الاختبارات ما يلي: المجموعات المسوّاة لا تحتاج إلى تحويلات؛ والمدين الواحد يدفع للدائن الواحد؛ وتطبيق التحويلات يعيد <strong>الجميع إلى الصفر</strong> بمبالغ موجبة من سنتات صحيحة؛ وعدد التحويلات <strong>لا يتجاوز عدد الأشخاص − 1</strong> (بعدّ من لهم رصيد غير صفري)؛ ومن سوّوا حساباتهم لا يظهرون أبدًا؛ والأرصدة التي لا يساوي مجموعها صفرًا <strong>ترمي خطأً</strong>؛ وكائن المدخلات <strong>لا يُعدَّل</strong>.</p>\n<p>اعمل على الفحوص بالترتيب، وشغّل بعد كل تغيير. الكود المبدئي ينجح في الفحص الأول.</p>\n","hints":["ارفض المدخلات السيئة أولًا: اجمع كل الأرصدة وارمِ <code dir=\"ltr\">Error</code> إن لم يكن المجموع 0.","قسّم الأشخاص إلى مدينين (أرصدة سالبة، تُخزَّن كمبالغ موجبة يدينون بها) ودائنين (أرصدة موجبة). انسخ الأرقام إلى كائنات جديدة كي لا تغيّر المدخلات أبدًا.","كرّر ما دام في القائمتين أحد: حوّل <code dir=\"ltr\">Math.min(debtor.cents, creditor.cents)</code> من المدين الحالي إلى الدائن الحالي، واطرحه من كليهما، وانتقل إلى المدين أو الدائن التالي حين يصل رصيده إلى 0. ترتيب القائمتين من الأكبر إلى الأصغر يُبقي عدد التحويلات صغيرًا."],"explanation":"<p>يفحص الحلّ المرجعي المجموع أولًا، ثم يبني كائنات <code dir=\"ltr\">{ name, cents }</code> جديدة للمدينين والدائنين، ولهذا تبقى المدخلات دون مساس. وتحوّل الحلقة دائمًا الأصغر من المبلغين الحاليين، فبعد كل تحويل يصل شخص واحد على الأقل إلى الصفر ويُترك جانبًا؛ وهذا يضمن انتهاء الحلقة وألا يتجاوز عدد التحويلات <code dir=\"ltr\">people - 1</code>. الترتيب من الأكبر إلى الأصغر قاعدة تقريبية شائعة تُنتج غالبًا تحويلات أقل في الواقع، لكن الاختبارات لا تشترط عمدًا قائمة محدّدة بالضبط للمجموعات الأكبر من شخصين، لأن عدة إجابات صحيحة. وهذا قرار تصميمي يستحق التقليد: حين تكون مخرجات كثيرة صالحة، أكّد <strong>الخصائص</strong> التي تملكها كل المخرجات الصالحة (الجميع ينتهي عند الصفر، والمبالغ موجبة، والعدد صغير) بدل إجابة واحدة بعينها.</p>\n","checkLabels":{"settled":"المجموعات المسوّاة (والفارغة) لا تحتاج إلى تحويلات","two-people":"مع شخصين، يدفع المدين للدائن المبلغ كاملًا","settles-everyone":"تطبيق التحويلات يعيد الجميع إلى الصفر، بمبالغ موجبة من سنتات صحيحة","few-transfers":"لا يتجاوز عدد التحويلات عدد الأشخاص − 1","ignores-settled":"من رصيدهم صفر لا يظهرون أبدًا في أي تحويل","rejects-unbalanced":"ترمي خطأً حين لا يساوي مجموع الأرصدة صفرًا","no-mutation":"لا تعدّل كائن الأرصدة الذي استلمته"}},"words":634,"readMinutes":4}