{"id":"l-tj-2-2","courseId":"testing-javascript","lang":"en","kind":"lesson","summary":"Find the inputs that expose bugs with equivalence classes and boundary values, then cover them compactly with table-driven tests using test.each.","html":"<p>Most bugs don’t live in the middle of the input range. Nobody’s code breaks on “$12.34”. It breaks on “”, on “0”, on “12.5” read as twelve dollars and five cents, on &quot; 7.10 &quot; pasted with a trailing space. Finding those inputs on purpose is the skill this lesson teaches.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The function under test</h2>\n<p>Splitwise-lite’s amount field accepts what people type and turns it into integer cents:</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=\"Run this example\"><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>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><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>Copy</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\">Equivalence classes and boundaries</h2>\n<p>You can’t test every string, and you don’t need to. <strong>Equivalence partitioning</strong> means grouping inputs the code should treat the same way, and testing one representative of each group:</p>\n<ul>\n<li>whole numbers: <code dir=\"ltr\">'12'</code></li>\n<li>one decimal: <code dir=\"ltr\">'12.5'</code> (the dangerous one: is it 1250 or 1205?)</li>\n<li>two decimals: <code dir=\"ltr\">'12.50'</code></li>\n<li>surrounded by spaces: <code dir=\"ltr\">' 7.10 '</code></li>\n<li>invalid text: <code dir=\"ltr\">'ten'</code>, <code dir=\"ltr\">''</code>, <code dir=\"ltr\">'-5'</code></li>\n</ul>\n<p><strong>Boundary value analysis</strong> then adds the inputs at the edges between groups, because that is where <code dir=\"ltr\">&lt;</code> and <code dir=\"ltr\">&lt;=</code> get mixed up and where <code dir=\"ltr\">{1,2}</code> becomes <code dir=\"ltr\">{1,3}</code>. For each limit, test just below, on, and just above:</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\">Two number lines. Amount: 0 is rejected, 0.01 is the smallest accepted value. Decimal places: two are accepted, three are rejected.</title>\n  <text class=\"d-label-strong\" x=\"20\" y=\"40\">Amount</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\">rejected</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\">accepted</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\">Decimals</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 or 2 accepted</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+ rejected</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>Test each side of every boundary, not only the middle</figcaption></figure>\n<p>Here the boundaries are zero (rejected) versus one cent (accepted), and two decimals (accepted) versus three (rejected). A checklist I run through for any input:</p>\n<ul>\n<li><strong>Empty and blank</strong>: <code dir=\"ltr\">''</code>, <code dir=\"ltr\">'   '</code></li>\n<li><strong>Zero, one, many</strong>: <code dir=\"ltr\">'0'</code>, <code dir=\"ltr\">'0.01'</code>, <code dir=\"ltr\">'12345.67'</code></li>\n<li><strong>Each limit, from both sides</strong>: two versus three decimals</li>\n<li><strong>Wrong kinds</strong>: letters, a minus sign, a comma (<code dir=\"ltr\">'1,000'</code>)</li>\n<li><strong>Formatting noise</strong>: leading or trailing spaces, a trailing dot (<code dir=\"ltr\">'12.'</code>)</li>\n</ul>\n<h2 id=\"sec-3\" tabindex=\"-1\">Table-driven tests with test.each</h2>\n<p>Writing a separate <code dir=\"ltr\">test(...)</code> for each of those is noisy. <code dir=\"ltr\">test.each</code> takes a table and produces <strong>one named test per row</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=\"Copy code\"><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>Copy</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> and <code dir=\"ltr\">%i</code> are filled from each row, so the report reads <code dir=\"ltr\">parses 12.5 as 1250 cents</code>, and a failure names the exact input. For wider tables, rows can be objects and the name can use <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=\"Copy code\"><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>Copy</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>The real win is maintenance. When a user reports that <code dir=\"ltr\">'12.'</code> crashes the form, the regression test is one new row, not a new block of code.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Where edge cases come from</h2>\n<p>You don’t find edge cases by staring at the code. Three sources do most of the work:</p>\n<ol>\n<li><strong>The spec, read adversarially.</strong> “Up to two decimals” immediately suggests two and three. “Must be positive” suggests zero. Every limit in a requirement is a boundary to test from both sides.</li>\n<li><strong>Real users.</strong> People paste amounts from bank statements with spaces and currency signs. People in Germany type a comma as the decimal separator, so <code dir=\"ltr\">'12,50'</code> is not a typo for them; decide whether you reject it with a helpful message or accept it, and write the row either way.</li>\n<li><strong>Bug reports.</strong> Every bug that reached production is an edge case your table missed. Add the row first, watch it fail, then fix the code. The table becomes a record of everything that ever went wrong.</li>\n</ol>\n<p>How many rows are enough? One per class, plus both sides of each boundary, plus one row per past bug. For <code dir=\"ltr\">parseAmount</code> that is about fifteen rows, a few milliseconds of runtime and a very good night’s sleep. If you find yourself wanting thousands of random inputs, look at property-based testing libraries such as fast-check, which generate inputs and shrink failures to the smallest example; for most app code, a hand-picked table is clearer.</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\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Only testing the happy path in the table</span></p>\n<p>A table of ten valid amounts feels thorough and catches almost nothing: they are all in the same class. Make sure every table has rows from <strong>each</strong> class and <strong>each side</strong> of each boundary. Five well-chosen rows beat fifty similar ones.</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\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Assert the message when it matters</span></p>\n<p><code dir=\"ltr\">toThrow()</code> with no argument passes for any error, including a <code dir=\"ltr\">TypeError</code> from a typo inside your function. When the rejection reason matters to the user, match it: <code dir=\"ltr\">toThrow('greater than zero')</code>. For a long list of invalid inputs, a plain <code dir=\"ltr\">toThrow()</code> is fine as long as one test pins each distinct message.</p>\n</aside>\n<p>In the exercise, build a table that exposes four realistic parsing bugs. Next up: functions that throw and functions that return promises.</p>\n","toc":[{"id":"sec-1","text":"The function under test","level":2},{"id":"sec-2","text":"Equivalence classes and boundaries","level":2},{"id":"sec-3","text":"Table-driven tests with test.each","level":2},{"id":"sec-4","text":"Where edge cases come from","level":2}],"takeaways":["Group inputs into classes that the code should treat the same way, then test one typical value per class plus the edges between classes.","Bugs cluster at boundaries, so test just below, on and just above every limit (zero, the maximum length, the decimal limit).","<code dir=\"ltr\">test.each</code> turns a table of inputs and expected outputs into one named test per row, so adding a case is one line.","Name each row with <code dir=\"ltr\">%s</code> placeholders or <code dir=\"ltr\">$field</code> so a failure tells you which input broke."],"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> accepts up to two decimal places. Which set of inputs best tests that rule?</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":"Correct. No decimals, one, two (on the limit) and three (just over) cover both sides of the boundary."},{"text":"<code dir=\"ltr\">'12.50'</code>, <code dir=\"ltr\">'99.99'</code>, <code dir=\"ltr\">'0.01'</code>","why":"These are all in the same class (exactly two decimals), so a bug that also accepted three decimals would pass every one."},{"text":"<code dir=\"ltr\">'abc'</code>, <code dir=\"ltr\">''</code>, <code dir=\"ltr\">'-5'</code>","why":"Good invalid inputs, but none of them is near the decimal limit, so they can’t tell two decimals from three."}],"answer":0},{"q":"<p>What are the names of the tests this produces?</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=\"Copy code\"><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>Copy</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>, twice","why":"The placeholders are filled in from each row, which is exactly what makes table-driven failures readable."},{"text":"<code dir=\"ltr\">parses 12 as 1200 cents</code> and <code dir=\"ltr\">parses 0.5 as 50 cents</code>","why":"Correct. <code dir=\"ltr\">%s</code> takes the first value of the row and <code dir=\"ltr\">%i</code> the second, giving one named test per row."},{"text":"One test named <code dir=\"ltr\">parses 12, 0.5 as 1200, 50 cents</code>","why":"<code dir=\"ltr\">test.each</code> creates one test per row, not one test for the whole table; each row passes or fails on its own."}],"answer":1},{"q":"<p>A form’s amount field rejects zero. Which bug does a test with input <code dir=\"ltr\">'0'</code> catch that <code dir=\"ltr\">'1'</code> and <code dir=\"ltr\">'-1'</code> miss?</p>","options":[{"text":"A regex that doesn’t allow negative numbers.","why":"<code dir=\"ltr\">'-1'</code> already covers negatives; zero adds nothing there."},{"text":"A parser that forgets to trim spaces.","why":"Trimming is about whitespace; none of these three inputs has any."},{"text":"A check written as <code dir=\"ltr\">cents &lt; 0</code> instead of <code dir=\"ltr\">cents &lt;= 0</code>, which lets zero through.","why":"Correct. Zero sits exactly on the boundary, and only an input on the boundary tells <code dir=\"ltr\">&lt;</code> from <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":"A table that breaks parseAmount","prompt":"<p><code dir=\"ltr\">parseAmount(input)</code> turns what a user typed into integer cents. Its rules are in the subject’s comment.</p>\n<p>The starter has two <code dir=\"ltr\">test.each</code> tables that all pass but catch nothing interesting: every row sits safely inside a class. Add rows (and tables if you like) until your suite catches four hidden bugs: one accepts <strong>three</strong> decimals, one reads <code dir=\"ltr\">'12.5'</code> as 1205 cents, one accepts <strong>zero</strong>, and one forgets to <strong>trim</strong> spaces.</p>\n","hints":["For each rule, ask: what is the input just on the other side of the limit? Two decimals is allowed, so try three.","One decimal is its own class: <code dir=\"ltr\">'12.5'</code> must be 1250, not 1205. Zero sits on the boundary between rejected and accepted: try <code dir=\"ltr\">'0'</code>.","Add <code dir=\"ltr\">['12.5', 1250]</code> and <code dir=\"ltr\">[' 7.10 ', 710]</code> to the valid table, and <code dir=\"ltr\">'12.505'</code> and <code dir=\"ltr\">'0'</code> to the invalid ones."],"explanation":"<p>Every bug here hides at a boundary or in a class the starter never visited. <code dir=\"ltr\">'12.505'</code> sits one step past the two-decimal limit, so only it can tell <code dir=\"ltr\">{1,2}</code> from <code dir=\"ltr\">{1,3}</code>. <code dir=\"ltr\">'12.5'</code> is the one-decimal class, where padding on the wrong side turns $12.50 into $12.05. <code dir=\"ltr\">'0'</code> sits exactly on the zero boundary, the only input that separates <code dir=\"ltr\">=== 0</code> from <code dir=\"ltr\">&lt; 0</code>. <code dir=\"ltr\">' 7.10 '</code> is formatting noise that users paste every day. Matching the message (<code dir=\"ltr\">'greater than zero'</code>) in the zero test also proves the right rule rejected it, not a regex accident.</p>\n","checkLabels":{"passes":"All your tests pass against the real parseAmount","catches-three-decimals":"Catches the version that accepts three decimals","catches-pads-wrong-side":"Catches the version that reads ‘12.5’ as 1205 cents","catches-accepts-zero":"Catches the version that accepts a zero amount","catches-no-trim":"Catches the version that does not trim spaces"}},"words":669,"readMinutes":3}