{"id":"l-tj-3-4","courseId":"testing-javascript","lang":"en","kind":"lesson","summary":"Test React components through props, rendered output and user interaction with React Testing Library, and mock the network at its boundary with an injected fetch or MSW.","html":"<p>Everything in this section so far applies to React unchanged, because React Testing Library is a thin layer over the same queries and user-event you already know. The genuinely new part is the network: React components love to fetch, and how you fake that decides whether your tests catch real bugs.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Rendering a component</h2>\n<p>Splitwise-lite’s <code dir=\"ltr\">ExpenseList</code> takes expenses as a prop and has one piece of state, a search filter:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/ExpenseList.jsx</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\"> { useState } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react'</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\"> { formatMoney } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './money.js'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> ExpenseList</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">expenses</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:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">query</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setQuery</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</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\"> visible</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> expenses.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">filter</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    e.description.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toLowerCase</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">includes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(query.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">trim</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toLowerCase</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">()),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  );</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">section</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-label</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Expenses\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">label</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        Search &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">input</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">query</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onChange</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> setQuery</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(e.target.value)</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\">      &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">label</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      {</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">visible.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">length</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</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\">        &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">p</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>No matching expenses&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">p</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:#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\">        &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">ul</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">          {</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">visible.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">map</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</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\">            &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">li</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> key</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">e.id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">e.description</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">formatMoney</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(e.amountCents)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">li</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:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">ul</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:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">section</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>With <code dir=\"ltr\">@testing-library/react</code> installed and <code dir=\"ltr\">environment: 'jsdom'</code>, tests read like the DOM tests you’ve written:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">src/ExpenseList.test.jsx</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\"> { 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\"> { render, screen } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '@testing-library/react'</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\"> userEvent </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '@testing-library/user-event'</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\"> { ExpenseList } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './ExpenseList.jsx'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> expenses</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\">  { id: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'e1'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, description: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Dinner'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amountCents: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">4550</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  { id: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'e2'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, description: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Taxi'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amountCents: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1800</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">];</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'filters expenses as you type'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> user</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> userEvent.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setup</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  render</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">ExpenseList</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> expenses</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">expenses</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> />);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> user.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(screen.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByRole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'textbox'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Search'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'tax'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> items</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> screen.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getAllByRole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'listitem'</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\">(items.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">map</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">li</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> li.textContent)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toEqual</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">([</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Taxi: $18.00'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'shows the new expense when the prop changes'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">rerender</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> render</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">ExpenseList</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> expenses</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">expenses</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</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\">  rerender</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">ExpenseList</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> expenses</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">expenses, { id: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'e3'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, description: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Museum'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amountCents: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2400</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>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(screen.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByText</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Museum: $24.00'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBeTruthy</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><strong>Props</strong> are tested by rendering with different values (and <code dir=\"ltr\">rerender</code> for updates). <strong>State</strong> is tested by interacting and checking the result; there’s no way to read <code dir=\"ltr\">query</code> from the test, and that’s a feature. <code dir=\"ltr\">render</code> wraps updates in React’s <code dir=\"ltr\">act()</code> for you, and React Testing Library unmounts components after each test when Vitest’s <code dir=\"ltr\">globals</code> option is on (otherwise call <code dir=\"ltr\">cleanup</code> in an <code dir=\"ltr\">afterEach</code>).</p>\n<aside class=\"callout callout-note\" 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\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/></svg><span><span class=\"callout-kind\">Note</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Components in a real browser</span></p>\n<p>Vitest 4 also has a stable Browser Mode, which runs component tests in a real browser (driven by a provider such as Playwright) instead of jsdom. It’s slower to start, but catches layout and browser-API differences jsdom can’t. Many teams keep jsdom for most component tests and use Browser Mode where real rendering matters.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">Where to fake the network</h2>\n<p>A component that loads a group calls a data function, which calls <code dir=\"ltr\">fetch</code>, which goes over the network. You can cut that chain at three points:</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 210\" role=\"img\" aria-labelledby=\"l-tj-3-4-t1\"><defs><marker id=\"arrow-l-tj-3-4-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-tj-3-4-t1\">A chain from component to loadGroup to fetch to network. vi.mock cuts early and skips loadGroup; injecting fetch cuts after loadGroup; MSW intercepts at the network edge.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"40\" width=\"140\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"80\" y=\"74\" text-anchor=\"middle\">Component</text>\n  <path class=\"d-arrow\" d=\"M150 68 L188 68\" marker-end=\"url(#arrow-l-tj-3-4-1)\"/>\n  <rect class=\"d-box\" x=\"190\" y=\"40\" width=\"140\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"260\" y=\"74\" text-anchor=\"middle\">loadGroup()</text>\n  <path class=\"d-arrow\" d=\"M330 68 L368 68\" marker-end=\"url(#arrow-l-tj-3-4-1)\"/>\n  <rect class=\"d-box\" x=\"370\" y=\"40\" width=\"140\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"440\" y=\"74\" text-anchor=\"middle\">fetch()</text>\n  <path class=\"d-arrow\" d=\"M510 68 L548 68\" marker-end=\"url(#arrow-l-tj-3-4-1)\"/>\n  <rect class=\"d-box\" x=\"550\" y=\"40\" width=\"120\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"610\" y=\"74\" text-anchor=\"middle\">Network</text>\n  <path class=\"d-line d-dashed\" d=\"M170 30 L170 150\"/>\n  <text class=\"d-label-muted\" x=\"170\" y=\"172\" text-anchor=\"middle\">vi.mock</text>\n  <path class=\"d-line d-dashed\" d=\"M350 30 L350 150\"/>\n  <text class=\"d-label-muted\" x=\"350\" y=\"172\" text-anchor=\"middle\">inject fetch</text>\n  <path class=\"d-line d-dashed\" d=\"M530 30 L530 150\"/>\n  <text class=\"d-label-muted\" x=\"530\" y=\"172\" text-anchor=\"middle\">MSW</text>\n  <text class=\"d-label\" x=\"350\" y=\"200\" text-anchor=\"middle\">later cut = more real code tested</text>\n</svg><figcaption>Cut the chain as late as you can</figcaption></figure>\n<ul>\n<li><strong>Mocking the data module</strong> (<code dir=\"ltr\">vi.mock('./load-group.js')</code>) skips the URL building, status handling and response shaping. Those are exactly where the bugs are.</li>\n<li><strong>Injecting <code dir=\"ltr\">fetch</code></strong> (<code dir=\"ltr\">loadGroup(id, fetchImpl)</code>) runs all of your code and fakes only the transport. Simple, no library, and what the exercise uses.</li>\n<li><strong>MSW</strong> (Mock Service Worker) intercepts requests at the network level in Node and in the browser. Your code calls the real <code dir=\"ltr\">fetch</code>, unchanged:</li>\n</ul>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">src/test/server.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\"> { setupServer } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'msw/node'</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\"> { http, HttpResponse } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'msw'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> server</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> setupServer</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  http.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">get</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'https://api.splitwise-lite.test/groups/:id'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, ({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">params</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    HttpResponse.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">json</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Group ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">params</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">id</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, members: [], expenses: [] }),</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>In a setup file, call <code dir=\"ltr\">server.listen({ onUnhandledRequest: 'error' })</code> before all tests, <code dir=\"ltr\">server.resetHandlers()</code> after each, and <code dir=\"ltr\">server.close()</code> at the end. A single test overrides a route with <code dir=\"ltr\">server.use(http.get(..., () =&gt; new HttpResponse(null, { status: 404 })))</code> to test the error path. The same handlers can power your app in development and in Storybook, so the fake API stays in one place.</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>Faking only the happy path</span></p>\n<p>A network mock that always returns 200 with perfect data tests the code you were least worried about. Every data function deserves a test for a non-OK status, and ideally for a network failure (<code dir=\"ltr\">HttpResponse.error()</code> in MSW, or a rejected promise from an injected fetch). Assert the specific error message, so a crash elsewhere doesn’t pass for correct error handling.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">What not to test in a component</h2>\n<p>Some component tests cost more than they catch. Skip them:</p>\n<ul>\n<li><strong>Styling details</strong>: whether a class is <code dir=\"ltr\">text-red-600</code> or the padding is 12px. Visual regressions are better caught by screenshot tests in a real browser, or a human review.</li>\n<li><strong>Third-party internals</strong>: you don’t need to prove that a date-picker library opens its calendar. Test that your component passes it the right value and reacts to its <code dir=\"ltr\">onChange</code>.</li>\n<li><strong>Whole-tree snapshots</strong>: <code dir=\"ltr\">expect(container).toMatchSnapshot()</code> on a large component fails on every harmless markup change, and reviewers learn to press “update snapshot” without reading. A small inline snapshot of one formatted value can be fine; a 300-line one is noise.</li>\n</ul>\n<h2 id=\"sec-4\" tabindex=\"-1\">Asserting on both sides of the request</h2>\n<p>A good network test checks what you <strong>sent</strong> as well as what you did with the answer. A wrong URL is a real bug (<code dir=\"ltr\">/group/</code> instead of <code dir=\"ltr\">/groups/</code>), and only an assertion on the request catches it, since your fake happily answers anything. With an injected fetch that’s <code dir=\"ltr\">expect(fetchMock).toHaveBeenCalledWith('https://api.splitwise-lite.test/groups/lisbon')</code>; with MSW, the handler only matches the right path, and <code dir=\"ltr\">onUnhandledRequest: 'error'</code> fails the test for anything else.</p>\n<p>In the exercise you test <code dir=\"ltr\">loadGroup</code> with an injected <code dir=\"ltr\">fetch</code> that returns real <code dir=\"ltr\">Response</code> objects, and catch four bugs in the request and the response shaping. That completes the UI section. Next comes the real browser, where layout, routing and the real server finally meet.</p>\n","toc":[{"id":"sec-1","text":"Rendering a component","level":2},{"id":"sec-2","text":"Where to fake the network","level":2},{"id":"sec-3","text":"What not to test in a component","level":2},{"id":"sec-4","text":"Asserting on both sides of the request","level":2}],"takeaways":["React Testing Library’s <code dir=\"ltr\">render</code> mounts a component into the jsdom document; you query and interact with it exactly as with plain DOM.","Test props by rendering with different props (or <code dir=\"ltr\">rerender</code>) and state by interacting; never reach into state or hooks directly.","Mock the network at its edge, either by injecting <code dir=\"ltr\">fetch</code> or by intercepting requests with MSW, so your own data-shaping code still runs in the test.","Assert on the request you sent (URL, method) as well as on what the UI or function does with the response, including error statuses."],"further":[{"title":"Testing Library — React Testing Library","url":"https://testing-library.com/docs/react-testing-library/intro"},{"title":"Testing Library — React Testing Library API","url":"https://testing-library.com/docs/react-testing-library/api"},{"title":"MDN — Response","url":"https://developer.mozilla.org/en-US/docs/Web/API/Response"},{"title":"Vitest — Mocking requests","url":"https://vitest.dev/guide/mocking/requests"}],"quiz":[{"q":"<p>How should a test check that <code dir=\"ltr\">ExpenseList</code> filters as you type in the Search box?</p>","options":[{"text":"Type into the Search field with user-event and assert which list items are on screen.","why":"Correct. That is the behaviour a user sees, and it keeps passing if you move the filter state into a reducer or a URL parameter."},{"text":"Read the component’s <code dir=\"ltr\">query</code> state after typing and check it equals the typed text.","why":"State is an implementation detail; a component can store the right text and still render the wrong list."},{"text":"Call the <code dir=\"ltr\">setQuery</code> function directly and snapshot the result.","why":"Tests can’t (and shouldn’t) reach a component’s state setters; and a snapshot wouldn’t say which items should be visible."}],"answer":0},{"q":"<p>Why do many teams prefer MSW over <code dir=\"ltr\">vi.mock('./api.js')</code> for components that fetch data?</p>","options":[{"text":"MSW makes requests to the real server faster.","why":"MSW doesn’t talk to the real server at all; it intercepts requests and answers them with your handlers."},{"text":"<code dir=\"ltr\">vi.mock</code> doesn’t work in files that render React components.","why":"<code dir=\"ltr\">vi.mock</code> works anywhere; the issue is how much real code it skips."},{"text":"MSW intercepts at the network level, so your real fetch and data-shaping code still run in the test.","why":"Correct. With a module mock, a bug in how you build the URL or parse the response is replaced away; with MSW it is exercised."}],"answer":2},{"q":"<p><code dir=\"ltr\">loadGroup</code> forgot to check <code dir=\"ltr\">response.ok</code>. For a 404, the JSON is <code dir=\"ltr\">{ &quot;error&quot;: &quot;Not found&quot; }</code>, so the code throws <code dir=\"ltr\">Cannot read properties of undefined</code>. Which assertion catches the missing check?</p>","options":[{"text":"<code dir=\"ltr\">await expect(loadGroup('nope', fetchMock)).rejects.toThrow()</code>","why":"The buggy version rejects too, with a <code dir=\"ltr\">TypeError</code>, so an assertion that accepts any error passes."},{"text":"<code dir=\"ltr\">await expect(loadGroup('nope', fetchMock)).rejects.toThrow('HTTP 404')</code>","why":"Correct. Matching the message proves the status was checked, not that something else happened to blow up."},{"text":"<code dir=\"ltr\">expect(fetchMock).toHaveBeenCalledTimes(1)</code>","why":"Both versions call fetch exactly once; the difference is what they do with the response."}],"answer":1}],"exercise":{"kind":"web","mode":"test","subject":"const API = 'https://api.splitwise-lite.test';\n\n// Loads a group: GET `${API}/groups/<id>` (id URL-encoded).\n// The API answers with JSON like:\n//   { \"name\": \"Lisbon trip\",\n//     \"members\": [{ \"name\": \"Ana\" }, { \"name\": \"Ben\" }],\n//     \"expenses\": [{ \"description\": \"Dinner\", \"amount\": 45.5,\n//                    \"paid_by\": \"Ana\", \"split_among\": [\"Ana\", \"Ben\"] }] }\n// and loadGroup resolves with Splitwise-lite's own shape:\n//   { name: \"Lisbon trip\", members: [\"Ana\", \"Ben\"],\n//     expenses: [{ description: \"Dinner\", amountCents: 4550,\n//                  paidBy: \"Ana\", splitAmong: [\"Ana\", \"Ben\"] }] }\n// If the response is not ok, it rejects with\n//   \"Could not load group <id> (HTTP <status>)\".\nasync function loadGroup(groupId, fetchImpl = fetch) {\n  const response = await fetchImpl(`${API}/groups/${encodeURIComponent(groupId)}`);\n  if (!response.ok) {\n    throw new Error(`Could not load group ${groupId} (HTTP ${response.status})`);\n  }\n  const data = await response.json();\n  return {\n    name: data.name,\n    members: data.members.map((member) => member.name),\n    expenses: data.expenses.map((e) => ({\n      description: e.description,\n      amountCents: Math.round(e.amount * 100),\n      paidBy: e.paid_by,\n      splitAmong: e.split_among,\n    })),\n  };\n}\n","mutants":[{"id":"ignores-status","code":"const API = 'https://api.splitwise-lite.test';\n\n// Loads a group: GET `${API}/groups/<id>` (id URL-encoded).\n// The API answers with JSON like:\n//   { \"name\": \"Lisbon trip\",\n//     \"members\": [{ \"name\": \"Ana\" }, { \"name\": \"Ben\" }],\n//     \"expenses\": [{ \"description\": \"Dinner\", \"amount\": 45.5,\n//                    \"paid_by\": \"Ana\", \"split_among\": [\"Ana\", \"Ben\"] }] }\n// and loadGroup resolves with Splitwise-lite's own shape:\n//   { name: \"Lisbon trip\", members: [\"Ana\", \"Ben\"],\n//     expenses: [{ description: \"Dinner\", amountCents: 4550,\n//                  paidBy: \"Ana\", splitAmong: [\"Ana\", \"Ben\"] }] }\n// If the response is not ok, it rejects with\n//   \"Could not load group <id> (HTTP <status>)\".\nasync function loadGroup(groupId, fetchImpl = fetch) {\n  const response = await fetchImpl(`${API}/groups/${encodeURIComponent(groupId)}`);\n  const data = await response.json();\n  return {\n    name: data.name,\n    members: data.members.map((member) => member.name),\n    expenses: data.expenses.map((e) => ({\n      description: e.description,\n      amountCents: Math.round(e.amount * 100),\n      paidBy: e.paid_by,\n      splitAmong: e.split_among,\n    })),\n  };\n}\n"},{"id":"wrong-url","code":"const API = 'https://api.splitwise-lite.test';\n\n// Loads a group: GET `${API}/groups/<id>` (id URL-encoded).\n// The API answers with JSON like:\n//   { \"name\": \"Lisbon trip\",\n//     \"members\": [{ \"name\": \"Ana\" }, { \"name\": \"Ben\" }],\n//     \"expenses\": [{ \"description\": \"Dinner\", \"amount\": 45.5,\n//                    \"paid_by\": \"Ana\", \"split_among\": [\"Ana\", \"Ben\"] }] }\n// and loadGroup resolves with Splitwise-lite's own shape:\n//   { name: \"Lisbon trip\", members: [\"Ana\", \"Ben\"],\n//     expenses: [{ description: \"Dinner\", amountCents: 4550,\n//                  paidBy: \"Ana\", splitAmong: [\"Ana\", \"Ben\"] }] }\n// If the response is not ok, it rejects with\n//   \"Could not load group <id> (HTTP <status>)\".\nasync function loadGroup(groupId, fetchImpl = fetch) {\n  const response = await fetchImpl(`${API}/group/${encodeURIComponent(groupId)}`);\n  if (!response.ok) {\n    throw new Error(`Could not load group ${groupId} (HTTP ${response.status})`);\n  }\n  const data = await response.json();\n  return {\n    name: data.name,\n    members: data.members.map((member) => member.name),\n    expenses: data.expenses.map((e) => ({\n      description: e.description,\n      amountCents: Math.round(e.amount * 100),\n      paidBy: e.paid_by,\n      splitAmong: e.split_among,\n    })),\n  };\n}\n"},{"id":"no-cents","code":"const API = 'https://api.splitwise-lite.test';\n\n// Loads a group: GET `${API}/groups/<id>` (id URL-encoded).\n// The API answers with JSON like:\n//   { \"name\": \"Lisbon trip\",\n//     \"members\": [{ \"name\": \"Ana\" }, { \"name\": \"Ben\" }],\n//     \"expenses\": [{ \"description\": \"Dinner\", \"amount\": 45.5,\n//                    \"paid_by\": \"Ana\", \"split_among\": [\"Ana\", \"Ben\"] }] }\n// and loadGroup resolves with Splitwise-lite's own shape:\n//   { name: \"Lisbon trip\", members: [\"Ana\", \"Ben\"],\n//     expenses: [{ description: \"Dinner\", amountCents: 4550,\n//                  paidBy: \"Ana\", splitAmong: [\"Ana\", \"Ben\"] }] }\n// If the response is not ok, it rejects with\n//   \"Could not load group <id> (HTTP <status>)\".\nasync function loadGroup(groupId, fetchImpl = fetch) {\n  const response = await fetchImpl(`${API}/groups/${encodeURIComponent(groupId)}`);\n  if (!response.ok) {\n    throw new Error(`Could not load group ${groupId} (HTTP ${response.status})`);\n  }\n  const data = await response.json();\n  return {\n    name: data.name,\n    members: data.members.map((member) => member.name),\n    expenses: data.expenses.map((e) => ({\n      description: e.description,\n      amountCents: e.amount,\n      paidBy: e.paid_by,\n      splitAmong: e.split_among,\n    })),\n  };\n}\n"},{"id":"snake-case-leak","code":"const API = 'https://api.splitwise-lite.test';\n\n// Loads a group: GET `${API}/groups/<id>` (id URL-encoded).\n// The API answers with JSON like:\n//   { \"name\": \"Lisbon trip\",\n//     \"members\": [{ \"name\": \"Ana\" }, { \"name\": \"Ben\" }],\n//     \"expenses\": [{ \"description\": \"Dinner\", \"amount\": 45.5,\n//                    \"paid_by\": \"Ana\", \"split_among\": [\"Ana\", \"Ben\"] }] }\n// and loadGroup resolves with Splitwise-lite's own shape:\n//   { name: \"Lisbon trip\", members: [\"Ana\", \"Ben\"],\n//     expenses: [{ description: \"Dinner\", amountCents: 4550,\n//                  paidBy: \"Ana\", splitAmong: [\"Ana\", \"Ben\"] }] }\n// If the response is not ok, it rejects with\n//   \"Could not load group <id> (HTTP <status>)\".\nasync function loadGroup(groupId, fetchImpl = fetch) {\n  const response = await fetchImpl(`${API}/groups/${encodeURIComponent(groupId)}`);\n  if (!response.ok) {\n    throw new Error(`Could not load group ${groupId} (HTTP ${response.status})`);\n  }\n  const data = await response.json();\n  return {\n    name: data.name,\n    members: data.members.map((member) => member.name),\n    expenses: data.expenses.map((e) => ({\n      description: e.description,\n      amountCents: Math.round(e.amount * 100),\n      paidBy: e.paidBy,\n      splitAmong: e.split_among,\n    })),\n  };\n}\n"}],"starter":{"js":"// Builds a real Response, like the one fetch resolves with.\nconst jsonResponse = (body, status = 200) =>\n  new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });\n\nconst apiGroup = {\n  name: 'Lisbon trip',\n  members: [{ name: 'Ana' }, { name: 'Ben' }],\n  expenses: [{ description: 'Dinner', amount: 45.5, paid_by: 'Ana', split_among: ['Ana', 'Ben'] }],\n};\n\ndescribe('loadGroup', () => {\n  test('resolves with the group name', async () => {\n    const fetchMock = vi.fn().mockResolvedValue(jsonResponse(apiGroup));\n\n    const group = await loadGroup('lisbon', fetchMock);\n\n    expect(group.name).toBe('Lisbon trip');\n  });\n\n  // Add: the URL that was requested, the full converted shape,\n  // and what happens with a 404.\n});\n"},"solution":{"js":"// Builds a real Response, like the one fetch resolves with.\nconst jsonResponse = (body, status = 200) =>\n  new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });\n\nconst apiGroup = {\n  name: 'Lisbon trip',\n  members: [{ name: 'Ana' }, { name: 'Ben' }],\n  expenses: [{ description: 'Dinner', amount: 45.5, paid_by: 'Ana', split_among: ['Ana', 'Ben'] }],\n};\n\ndescribe('loadGroup', () => {\n  test('requests the group by id', async () => {\n    const fetchMock = vi.fn().mockResolvedValue(jsonResponse(apiGroup));\n\n    await loadGroup('lisbon', fetchMock);\n\n    expect(fetchMock).toHaveBeenCalledWith('https://api.splitwise-lite.test/groups/lisbon');\n  });\n\n  test(\"converts the API's shape into Splitwise-lite's\", async () => {\n    const fetchMock = vi.fn().mockResolvedValue(jsonResponse(apiGroup));\n\n    const group = await loadGroup('lisbon', fetchMock);\n\n    expect(group).toEqual({\n      name: 'Lisbon trip',\n      members: ['Ana', 'Ben'],\n      expenses: [{ description: 'Dinner', amountCents: 4550, paidBy: 'Ana', splitAmong: ['Ana', 'Ben'] }],\n    });\n  });\n\n  test('rejects with the status when the response is not ok', async () => {\n    const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ error: 'Not found' }, 404));\n\n    await expect(loadGroup('nope', fetchMock)).rejects.toThrow('Could not load group nope (HTTP 404)');\n  });\n});\n"},"checks":[{"id":"passes","suite":"subject"},{"id":"catches-ignores-status","suite":"ignores-status"},{"id":"catches-wrong-url","suite":"wrong-url"},{"id":"catches-no-cents","suite":"no-cents"},{"id":"catches-snake-case-leak","suite":"snake-case-leak"}],"title":"Fake the network at its edge","prompt":"<p><code dir=\"ltr\">loadGroup(groupId, fetchImpl)</code> requests <code dir=\"ltr\">https://api.splitwise-lite.test/groups/&lt;id&gt;</code>, converts the API’s JSON (dollars, snake_case) into Splitwise-lite’s shape (cents, camelCase), and rejects with <code dir=\"ltr\">Could not load group &lt;id&gt; (HTTP &lt;status&gt;)</code> when the response isn’t ok. The exact shapes are in the subject’s comment.</p>\n<p>The starter gives you <code dir=\"ltr\">jsonResponse(body, status)</code>, which builds a real <code dir=\"ltr\">Response</code>, and an <code dir=\"ltr\">apiGroup</code> sample. Pass a <code dir=\"ltr\">vi.fn().mockResolvedValue(jsonResponse(...))</code> as <code dir=\"ltr\">fetchImpl</code> and add tests until your suite catches four hidden bugs: one <strong>ignores</strong> the HTTP status, one requests the <strong>wrong URL</strong>, one forgets to convert dollars to <strong>cents</strong>, and one reads <code dir=\"ltr\">paidBy</code> from the wrong field.</p>\n","hints":["Check the request: <code dir=\"ltr\">expect(fetchMock).toHaveBeenCalledWith('https://api.splitwise-lite.test/groups/lisbon')</code>.","Compare the whole result with <code dir=\"ltr\">toEqual</code>; the expected object is spelled out in the subject comment (45.5 dollars is 4550 cents).","For the error path, use <code dir=\"ltr\">jsonResponse({ error: 'Not found' }, 404)</code> and assert the message, not just that it rejects: the buggy version rejects too, with a TypeError."],"explanation":"<p>Because only the transport is faked, every line of <code dir=\"ltr\">loadGroup</code> runs in the test. <code dir=\"ltr\">toHaveBeenCalledWith</code> on the fetch mock catches the wrong URL, which no assertion on the result could see, since a fake answers any URL. One <code dir=\"ltr\">toEqual</code> on the converted group catches both shaping bugs: dollars left as 45.5 and <code dir=\"ltr\">paidBy</code> coming back <code dir=\"ltr\">undefined</code>. The 404 test is the subtle one: without the <code dir=\"ltr\">ok</code> check, the code still rejects, with <code dir=\"ltr\">Cannot read properties of undefined</code>, so only matching the message <code dir=\"ltr\">HTTP 404</code> proves the status was handled on purpose. This is the same discipline MSW gives you, without the library.</p>\n","checkLabels":{"passes":"All your tests pass against the real loadGroup","catches-ignores-status":"Catches the version that ignores the HTTP status","catches-wrong-url":"Catches the version that requests the wrong URL","catches-no-cents":"Catches the version that keeps dollars instead of cents","catches-snake-case-leak":"Catches the version that reads paidBy from the wrong field"}},"words":671,"readMinutes":3}