{"id":"l-tj-2-2","courseId":"testing-javascript","lang":"ar","kind":"lesson","summary":"تجد المدخلات التي تكشف الأخطاء باستخدام فئات التكافؤ والقيم الحدّية، ثم تغطّيها بإيجاز باختبارات معتمدة على الجداول عبر test.each.","html":"<p>معظم الأخطاء لا تعيش في منتصف نطاق المدخلات. لا ينكسر كود أحد مع “$12.34”. بل ينكسر مع “”، ومع “0”، ومع “12.5” حين تُقرأ اثني عشر دولارًا وخمسة سنتات، ومع &quot; 7.10 &quot; حين تُلصق بمسافة في آخرها. إيجاد هذه المدخلات عن قصد هو المهارة التي يعلّمها هذا الدرس.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">الدالة الخاضعة للاختبار</h2>\n<p>يقبل حقل المبلغ في 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\"> parseAmount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">input</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\"> text</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> String</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">trim</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:#0A3069;--shiki-dark:#A5D6FF\">/</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">^</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">\\d</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#116329;--shiki-light-font-weight:bold;--shiki-dark:#7EE787;--shiki-dark-font-weight:bold\">\\.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">\\d</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{1,2}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?$</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(text)) {</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\"> Error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Not a valid amount: \"${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">input</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}\"`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">whole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">fraction</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> ''</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> text.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">split</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'.'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> cents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(whole) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 100</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> +</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(fraction.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">padEnd</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'0'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">));</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (cents </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</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\">'Amount must be greater than zero'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cents;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> input</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> of</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'12'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'12.5'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">' 7.10 '</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'0.01'</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:#0550AE;--shiki-dark:#79C0FF\">JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">stringify</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'->'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">parseAmount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input));</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\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> input</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> of</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">''</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'0'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'-5'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'12.345'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'ten'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  try</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">parseAmount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input); } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">catch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (e) { console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">stringify</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'->'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, e.message); }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<h2 id=\"sec-2\" tabindex=\"-1\">فئات التكافؤ والحدود</h2>\n<p>لا تستطيع اختبار كل نص ممكن، ولا تحتاج إلى ذلك. <strong>تقسيم التكافؤ</strong> (equivalence partitioning) يعني جمع المدخلات التي يُفترض أن يعاملها الكود بالطريقة نفسها، واختبار ممثّل واحد من كل مجموعة:</p>\n<ul>\n<li>أعداد صحيحة: <code dir=\"ltr\">'12'</code></li>\n<li>منزلة عشرية واحدة: <code dir=\"ltr\">'12.5'</code> (وهي الخطرة: هل هي 1250 أم 1205؟)</li>\n<li>منزلتان عشريتان: <code dir=\"ltr\">'12.50'</code></li>\n<li>محاطة بمسافات: <code dir=\"ltr\">' 7.10 '</code></li>\n<li>نص غير صالح: <code dir=\"ltr\">'ten'</code>، <code dir=\"ltr\">''</code>، <code dir=\"ltr\">'-5'</code></li>\n</ul>\n<p>ثم يضيف <strong>تحليل القيم الحدّية</strong> (boundary value analysis) المدخلات الواقعة على الحواف بين المجموعات، لأن هناك بالذات يختلط <code dir=\"ltr\">&lt;</code> بـ <code dir=\"ltr\">&lt;=</code>، ويتحوّل <code dir=\"ltr\">{1,2}</code> إلى <code dir=\"ltr\">{1,3}</code>. لكل حدّ، اختبر ما تحته مباشرة، وعليه، وما فوقه مباشرة:</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 200\" role=\"img\" aria-labelledby=\"l-tj-2-2-t1\"><defs><marker id=\"arrow-l-tj-2-2-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-2-t1\">خطّا أرقام. المبلغ: 0 مرفوض، و0.01 أصغر قيمة مقبولة. المنازل العشرية: منزلتان مقبولتان، وثلاث مرفوضة.</title>\n  <text class=\"d-label-strong\" x=\"20\" y=\"40\">المبلغ</text>\n  <path class=\"d-line\" d=\"M130 60 L660 60\"/>\n  <rect class=\"d-box-warn\" x=\"130\" y=\"46\" width=\"150\" height=\"28\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"205\" y=\"65\" text-anchor=\"middle\">مرفوض</text>\n  <rect class=\"d-box-success\" x=\"290\" y=\"46\" width=\"370\" height=\"28\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"475\" y=\"65\" text-anchor=\"middle\">مقبول</text>\n  <text class=\"d-code\" x=\"250\" y=\"98\" text-anchor=\"middle\">'0'</text>\n  <text class=\"d-code\" x=\"330\" y=\"98\" text-anchor=\"middle\">'0.01'</text>\n  <text class=\"d-label-strong\" x=\"20\" y=\"140\">العشريات</text>\n  <path class=\"d-line\" d=\"M130 160 L660 160\"/>\n  <rect class=\"d-box-success\" x=\"130\" y=\"146\" width=\"330\" height=\"28\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"295\" y=\"165\" text-anchor=\"middle\">0 أو 1 أو 2: مقبول</text>\n  <rect class=\"d-box-warn\" x=\"470\" y=\"146\" width=\"190\" height=\"28\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"565\" y=\"165\" text-anchor=\"middle\">3 فأكثر: مرفوض</text>\n  <text class=\"d-code\" x=\"420\" y=\"194\" text-anchor=\"middle\">'12.50'</text>\n  <text class=\"d-code\" x=\"520\" y=\"194\" text-anchor=\"middle\">'12.505'</text>\n</svg><figcaption>اختبر جهتي كل حدّ، لا المنتصف فقط</figcaption></figure>\n<p>الحدود هنا هي الصفر (مرفوض) مقابل سنت واحد (مقبول)، ومنزلتان عشريتان (مقبول) مقابل ثلاث (مرفوض). وإليك قائمة أمرّ عليها مع أي مدخل:</p>\n<ul>\n<li><strong>الفارغ والمسافات فقط</strong>: <code dir=\"ltr\">''</code>، <code dir=\"ltr\">'   '</code></li>\n<li><strong>صفر، وواحد، وكثير</strong>: <code dir=\"ltr\">'0'</code>، <code dir=\"ltr\">'0.01'</code>، <code dir=\"ltr\">'12345.67'</code></li>\n<li><strong>كل حدّ، من الجهتين</strong>: منزلتان مقابل ثلاث</li>\n<li><strong>أنواع خاطئة</strong>: حروف، وإشارة سالب، وفاصلة (<code dir=\"ltr\">'1,000'</code>)</li>\n<li><strong>ضجيج التنسيق</strong>: مسافات في البداية أو النهاية، ونقطة في الآخر (<code dir=\"ltr\">'12.'</code>)</li>\n</ul>\n<h2 id=\"sec-3\" tabindex=\"-1\">الاختبارات المعتمدة على الجداول باستخدام test.each</h2>\n<p>كتابة <code dir=\"ltr\">test(...)</code> منفصل لكل واحد من هذه المدخلات تزحم الملف. يأخذ <code dir=\"ltr\">test.each</code> جدولًا وينتج <strong>اختبارًا مسمّى لكل صف</strong>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">src/parse-amount.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\"> { parseAmount } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './parse-amount.js'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">describe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'parseAmount'</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\">  test.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">each</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">([</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'12'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1200</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'12.5'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1250</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'12.50'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1250</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'0.01'</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:#1F2328;--shiki-dark:#E6EDF3\">    [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">' 7.10 '</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">710</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  ])(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'parses %s as %i cents'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">input</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">cents</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span 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\">parseAmount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(cents);</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\">  test.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">each</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">([</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">''</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'0'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'0.00'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'-5'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'12.505'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'ten'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'1,000'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">])(</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">    'rejects %s'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">input</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\"> parseAmount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toThrow</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  );</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>يُملأ <code dir=\"ltr\">%s</code> و<code dir=\"ltr\">%i</code> من كل صف، فيقرأ التقرير <code dir=\"ltr\">parses 12.5 as 1250 cents</code>، ويسمّي الفشل المدخل بالضبط. وفي الجداول الأعرض، يمكن أن تكون الصفوف كائنات، ويمكن أن يستخدم الاسم <code dir=\"ltr\">$field</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:#1F2328;--shiki-dark:#E6EDF3\">test.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">each</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">([</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  { input: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'12.5'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, cents: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1250</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  { input: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'0.01'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, cents: </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:#1F2328;--shiki-dark:#E6EDF3\">])(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'parses $input as $cents cents'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, ({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">input</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">cents</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }) </span><span 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\">parseAmount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(cents);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>المكسب الحقيقي في الصيانة. حين يبلّغ مستخدم أن <code dir=\"ltr\">'12.'</code> يعطّل النموذج، يكون اختبار الانتكاس (regression test) صفًّا جديدًا واحدًا، لا كتلة كود جديدة.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">من أين تأتي الحالات الحدّية</h2>\n<p>لا تجد الحالات الحدّية (edge cases) بالتحديق في الكود. ثلاثة مصادر تقوم بمعظم العمل:</p>\n<ol>\n<li><strong>المواصفات، مقروءةً بعين الخصم.</strong> “حتى منزلتين عشريتين” تقترح فورًا منزلتين وثلاثًا. “يجب أن يكون موجبًا” تقترح الصفر. كل حدّ في المتطلّبات حدّ يُختبر من جهتيه.</li>\n<li><strong>المستخدمون الحقيقيون.</strong> يلصق الناس المبالغ من كشوف الحساب البنكية مع مسافات ورموز عملات. ويكتب الناس في ألمانيا الفاصلة كفاصل عشري، فـ <code dir=\"ltr\">'12,50'</code> ليست خطأً مطبعيًا بالنسبة إليهم؛ قرّر إن كنت سترفضها برسالة مفيدة أو تقبلها، واكتب الصف في الحالتين.</li>\n<li><strong>تقارير الأخطاء.</strong> كل خطأ وصل إلى بيئة الإنتاج حالة حدّية فاتها جدولك. أضف الصف أولًا، وشاهده يفشل، ثم أصلح الكود. هكذا يصبح الجدول سجلًّا لكل ما سار على نحو خاطئ في أي وقت.</li>\n</ol>\n<p>كم صفًّا يكفي؟ صف لكل فئة، وجهتا كل حدّ، وصف لكل خطأ سابق. بالنسبة إلى <code dir=\"ltr\">parseAmount</code> هذا نحو خمسة عشر صفًّا، وبضعة أجزاء من الثانية وقت تشغيل، ونوم هانئ جدًا. وإن وجدت نفسك تريد آلاف المدخلات العشوائية، فانظر في مكتبات الاختبار القائم على الخصائص (property-based testing) مثل fast-check، التي تولّد المدخلات وتقلّص حالات الفشل إلى أصغر مثال؛ لكن في معظم أكواد التطبيقات، يكون الجدول المنتقى يدويًا أوضح.</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>جدول من عشرة مبالغ صالحة يبدو شاملًا لكنه لا يلتقط شيئًا تقريبًا: كلها في الفئة نفسها. تأكّد أن في كل جدول صفوفًا من <strong>كل</strong> فئة، ومن <strong>كل جهة</strong> من كل حدّ. خمسة صفوف منتقاة جيدًا تتفوّق على خمسين صفًّا متشابهًا.</p>\n</aside>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>أكّد الرسالة حين تهمّ</span></p>\n<p><code dir=\"ltr\">toThrow()</code> بلا معامل ينجح مع أي خطأ، بما في ذلك <code dir=\"ltr\">TypeError</code> ناتج عن خطأ مطبعي داخل دالتك. حين يهمّ سبب الرفض للمستخدم، طابقه: <code dir=\"ltr\">toThrow('greater than zero')</code>. ومع قائمة طويلة من المدخلات غير الصالحة، يكفي <code dir=\"ltr\">toThrow()</code> عادي ما دام هناك اختبار واحد يثبّت كل رسالة مختلفة.</p>\n</aside>\n<p>في التمرين، ابنِ جدولًا يكشف أربعة أخطاء تحليل واقعية. وبعده: دوال ترمي أخطاءً ودوال تعيد promises.</p>\n","toc":[{"id":"sec-1","text":"الدالة الخاضعة للاختبار","level":2},{"id":"sec-2","text":"فئات التكافؤ والحدود","level":2},{"id":"sec-3","text":"الاختبارات المعتمدة على الجداول باستخدام test.each","level":2},{"id":"sec-4","text":"من أين تأتي الحالات الحدّية","level":2}],"takeaways":["اجمع المدخلات في فئات يُفترض أن يعاملها الكود بالطريقة نفسها، ثم اختبر قيمة نموذجية واحدة من كل فئة، إضافة إلى الحواف بين الفئات.","تتجمّع الأخطاء عند الحدود، لذا اختبر ما تحت كل حدّ مباشرة، وعليه، وما فوقه مباشرة (الصفر، والطول الأقصى، وحدّ المنازل العشرية).","يحوّل <code dir=\"ltr\">test.each</code> جدولًا من المدخلات والمخرجات المتوقّعة إلى اختبار مسمّى لكل صف، فتصبح إضافة حالة جديدة سطرًا واحدًا.","سمِّ كل صف باستخدام عناصر نائبة مثل <code dir=\"ltr\">%s</code> أو <code dir=\"ltr\">$field</code> كي يخبرك الفشل أيّ مدخل انكسر."],"further":[{"title":"Vitest — test.each","url":"https://vitest.dev/api/test#test-each"},{"title":"MDN — Regular expressions","url":"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions"}],"quiz":[{"q":"<p>تقبل <code dir=\"ltr\">parseAmount</code> حتى منزلتين عشريتين. أيّ مجموعة مدخلات تختبر هذه القاعدة على أفضل وجه؟</p>","options":[{"text":"<code dir=\"ltr\">'12'</code>، <code dir=\"ltr\">'12.5'</code>، <code dir=\"ltr\">'12.50'</code>، <code dir=\"ltr\">'12.505'</code>","why":"صحيح. بلا منازل عشرية، ومنزلة واحدة، ومنزلتان (على الحدّ)، وثلاث (فوقه مباشرة)؛ وهكذا تُغطّى جهتا الحدّ."},{"text":"<code dir=\"ltr\">'12.50'</code>، <code dir=\"ltr\">'99.99'</code>، <code dir=\"ltr\">'0.01'</code>","why":"كلها في الفئة نفسها (منزلتان عشريتان بالضبط)، فالخطأ الذي يقبل ثلاث منازل أيضًا سينجح فيها جميعًا."},{"text":"<code dir=\"ltr\">'abc'</code>، <code dir=\"ltr\">''</code>، <code dir=\"ltr\">'-5'</code>","why":"مدخلات غير صالحة جيدة، لكن لا شيء منها قريب من حدّ المنازل العشرية، فلا تستطيع التمييز بين منزلتين وثلاث."}],"answer":0},{"q":"<p>ما أسماء الاختبارات التي ينتجها هذا الكود؟</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"0\"><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:#1F2328;--shiki-dark:#E6EDF3\">test.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">each</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">([</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'12'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1200</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'0.5'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">50</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">])(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'parses %s as %i cents'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">input</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">cents</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span 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\">parseAmount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(cents);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n","options":[{"text":"<code dir=\"ltr\">parses %s as %i cents</code>، مرتين","why":"العناصر النائبة تُملأ من كل صف، وهذا بالضبط ما يجعل حالات الفشل في الاختبارات المعتمدة على الجداول مقروءة."},{"text":"<code dir=\"ltr\">parses 12 as 1200 cents</code> و<code dir=\"ltr\">parses 0.5 as 50 cents</code>","why":"صحيح. يأخذ <code dir=\"ltr\">%s</code> القيمة الأولى من الصف، و<code dir=\"ltr\">%i</code> الثانية، فيُنتج اختبارًا مسمّى لكل صف."},{"text":"اختبار واحد اسمه <code dir=\"ltr\">parses 12, 0.5 as 1200, 50 cents</code>","why":"يُنشئ <code dir=\"ltr\">test.each</code> اختبارًا لكل صف، لا اختبارًا واحدًا للجدول كله؛ وكل صف ينجح أو يفشل وحده."}],"answer":1},{"q":"<p>حقل المبلغ في نموذج ما يرفض الصفر. أيّ خطأ يلتقطه اختبار بالمدخل <code dir=\"ltr\">'0'</code> ويفوت المدخلين <code dir=\"ltr\">'1'</code> و<code dir=\"ltr\">'-1'</code>؟</p>","options":[{"text":"تعبير نمطي (regex) لا يسمح بالأرقام السالبة.","why":"<code dir=\"ltr\">'-1'</code> يغطّي الأرقام السالبة أصلًا؛ والصفر لا يضيف شيئًا هنا."},{"text":"محلّل ينسى حذف المسافات الزائدة.","why":"حذف المسافات يتعلّق بالفراغات، ولا يحتوي أيّ من هذه المدخلات الثلاثة على فراغات."},{"text":"شرط مكتوب <code dir=\"ltr\">cents &lt; 0</code> بدل <code dir=\"ltr\">cents &lt;= 0</code>، فيمرّ الصفر.","why":"صحيح. الصفر يقع على الحدّ تمامًا، ولا يميّز <code dir=\"ltr\">&lt;</code> من <code dir=\"ltr\">&lt;=</code> إلا مدخل يقع على الحدّ."}],"answer":2}],"exercise":{"kind":"web","mode":"test","subject":"// Parses what a user typed in the amount field and returns integer cents.\n// Accepts: digits, optionally a dot and 1 or 2 decimals, surrounding spaces.\n//   \"12\" -> 1200, \"12.5\" -> 1250, \" 7.10 \" -> 710\n// Throws an Error for: empty input, zero, negatives, letters,\n// commas and more than 2 decimals.\nfunction parseAmount(input) {\n  const text = String(input).trim();\n  if (!/^\\d+(\\.\\d{1,2})?$/.test(text)) {\n    throw new Error(`Not a valid amount: \"${input}\"`);\n  }\n  const [whole, fraction = ''] = text.split('.');\n  const cents = Number(whole) * 100 + Number(fraction.padEnd(2, '0'));\n  if (cents === 0) throw new Error('Amount must be greater than zero');\n  return cents;\n}\n","mutants":[{"id":"three-decimals","code":"// Parses what a user typed in the amount field and returns integer cents.\n// Accepts: digits, optionally a dot and 1 or 2 decimals, surrounding spaces.\n//   \"12\" -> 1200, \"12.5\" -> 1250, \" 7.10 \" -> 710\n// Throws an Error for: empty input, zero, negatives, letters,\n// commas and more than 2 decimals.\nfunction parseAmount(input) {\n  const text = String(input).trim();\n  if (!/^\\d+(\\.\\d{1,3})?$/.test(text)) {\n    throw new Error(`Not a valid amount: \"${input}\"`);\n  }\n  const [whole, fraction = ''] = text.split('.');\n  const cents = Number(whole) * 100 + Number(fraction.padEnd(2, '0'));\n  if (cents === 0) throw new Error('Amount must be greater than zero');\n  return cents;\n}\n"},{"id":"pads-wrong-side","code":"// Parses what a user typed in the amount field and returns integer cents.\n// Accepts: digits, optionally a dot and 1 or 2 decimals, surrounding spaces.\n//   \"12\" -> 1200, \"12.5\" -> 1250, \" 7.10 \" -> 710\n// Throws an Error for: empty input, zero, negatives, letters,\n// commas and more than 2 decimals.\nfunction parseAmount(input) {\n  const text = String(input).trim();\n  if (!/^\\d+(\\.\\d{1,2})?$/.test(text)) {\n    throw new Error(`Not a valid amount: \"${input}\"`);\n  }\n  const [whole, fraction = ''] = text.split('.');\n  const cents = Number(whole) * 100 + Number(fraction.padStart(2, '0'));\n  if (cents === 0) throw new Error('Amount must be greater than zero');\n  return cents;\n}\n"},{"id":"accepts-zero","code":"// Parses what a user typed in the amount field and returns integer cents.\n// Accepts: digits, optionally a dot and 1 or 2 decimals, surrounding spaces.\n//   \"12\" -> 1200, \"12.5\" -> 1250, \" 7.10 \" -> 710\n// Throws an Error for: empty input, zero, negatives, letters,\n// commas and more than 2 decimals.\nfunction parseAmount(input) {\n  const text = String(input).trim();\n  if (!/^\\d+(\\.\\d{1,2})?$/.test(text)) {\n    throw new Error(`Not a valid amount: \"${input}\"`);\n  }\n  const [whole, fraction = ''] = text.split('.');\n  const cents = Number(whole) * 100 + Number(fraction.padEnd(2, '0'));\n  if (cents < 0) throw new Error('Amount must be greater than zero');\n  return cents;\n}\n"},{"id":"no-trim","code":"// Parses what a user typed in the amount field and returns integer cents.\n// Accepts: digits, optionally a dot and 1 or 2 decimals, surrounding spaces.\n//   \"12\" -> 1200, \"12.5\" -> 1250, \" 7.10 \" -> 710\n// Throws an Error for: empty input, zero, negatives, letters,\n// commas and more than 2 decimals.\nfunction parseAmount(input) {\n  const text = String(input);\n  if (!/^\\d+(\\.\\d{1,2})?$/.test(text)) {\n    throw new Error(`Not a valid amount: \"${input}\"`);\n  }\n  const [whole, fraction = ''] = text.split('.');\n  const cents = Number(whole) * 100 + Number(fraction.padEnd(2, '0'));\n  if (cents === 0) throw new Error('Amount must be greater than zero');\n  return cents;\n}\n"}],"starter":{"js":"describe('parseAmount', () => {\n  test.each([\n    ['12', 1200],\n    ['12.50', 1250],\n  ])('parses %s as %i cents', (input, cents) => {\n    expect(parseAmount(input)).toBe(cents);\n  });\n\n  test.each(['', 'ten', '-5'])('rejects %s', (input) => {\n    expect(() => parseAmount(input)).toThrow();\n  });\n});\n"},"solution":{"js":"describe('parseAmount', () => {\n  test.each([\n    ['12', 1200],\n    ['12.5', 1250],\n    ['12.50', 1250],\n    ['0.01', 1],\n    [' 7.10 ', 710],\n  ])('parses %s as %i cents', (input, cents) => {\n    expect(parseAmount(input)).toBe(cents);\n  });\n\n  test.each(['', 'ten', '-5', '1,000', '12.505'])('rejects %s', (input) => {\n    expect(() => parseAmount(input)).toThrow();\n  });\n\n  test.each(['0', '0.00'])('rejects a zero amount: %s', (input) => {\n    expect(() => parseAmount(input)).toThrow('greater than zero');\n  });\n});\n"},"checks":[{"id":"passes","suite":"subject"},{"id":"catches-three-decimals","suite":"three-decimals"},{"id":"catches-pads-wrong-side","suite":"pads-wrong-side"},{"id":"catches-accepts-zero","suite":"accepts-zero"},{"id":"catches-no-trim","suite":"no-trim"}],"title":"جدول يكسر parseAmount","prompt":"<p>تحوّل <code dir=\"ltr\">parseAmount(input)</code> ما كتبه المستخدم إلى سنتات صحيحة. قواعدها مذكورة في تعليق الـ subject.</p>\n<p>في الكود المبدئي جدولا <code dir=\"ltr\">test.each</code> ينجحان بالكامل لكنهما لا يلتقطان شيئًا مثيرًا للاهتمام: كل صف يقع بأمان داخل فئة واحدة. أضف صفوفًا (وجداول إن شئت) حتى تلتقط مجموعتك أربعة أخطاء مخفيّة: نسخة تقبل <strong>ثلاث</strong> منازل عشرية، ونسخة تقرأ <code dir=\"ltr\">'12.5'</code> على أنها 1205 سنتًا، ونسخة تقبل <strong>الصفر</strong>، ونسخة تنسى <strong>حذف</strong> المسافات.</p>\n","hints":["لكل قاعدة، اسأل: ما المدخل الواقع على الجهة الأخرى من الحدّ مباشرة؟ المنزلتان مسموح بهما، فجرّب ثلاثًا.","المنزلة العشرية الواحدة فئة قائمة بذاتها: يجب أن تكون <code dir=\"ltr\">'12.5'</code> هي 1250 لا 1205. والصفر يقع على الحدّ بين المرفوض والمقبول: جرّب <code dir=\"ltr\">'0'</code>.","أضف <code dir=\"ltr\">['12.5', 1250]</code> و<code dir=\"ltr\">[' 7.10 ', 710]</code> إلى جدول المدخلات الصالحة، و<code dir=\"ltr\">'12.505'</code> و<code dir=\"ltr\">'0'</code> إلى جدول المدخلات غير الصالحة."],"explanation":"<p>كل خطأ هنا يختبئ عند حدّ، أو في فئة لم يزرها الكود المبدئي قط. <code dir=\"ltr\">'12.505'</code> تقع بعد حدّ المنزلتين بخطوة واحدة، فلا يستطيع غيرها التمييز بين <code dir=\"ltr\">{1,2}</code> و<code dir=\"ltr\">{1,3}</code>. و<code dir=\"ltr\">'12.5'</code> هي فئة المنزلة الواحدة، حيث يحوّل إكمال الصفر في الجهة الخطأ $12.50 إلى $12.05. و<code dir=\"ltr\">'0'</code> تقع على حدّ الصفر تمامًا، وهي المدخل الوحيد الذي يفصل <code dir=\"ltr\">=== 0</code> عن <code dir=\"ltr\">&lt; 0</code>. و<code dir=\"ltr\">' 7.10 '</code> ضجيج تنسيق يلصقه المستخدمون كل يوم. ومطابقة الرسالة (<code dir=\"ltr\">'greater than zero'</code>) في اختبار الصفر تثبت أيضًا أن القاعدة الصحيحة هي التي رفضته، لا مصادفة في الـ regex.</p>\n","checkLabels":{"passes":"كل اختباراتك تنجح على parseAmount الحقيقية","catches-three-decimals":"تلتقط النسخة التي تقبل ثلاث منازل عشرية","catches-pads-wrong-side":"تلتقط النسخة التي تقرأ ‘12.5’ على أنها 1205 سنتًا","catches-accepts-zero":"تلتقط النسخة التي تقبل مبلغًا صفريًا","catches-no-trim":"تلتقط النسخة التي لا تحذف المسافات"}},"words":597,"readMinutes":4}