{"id":"l-tj-4-2","courseId":"testing-javascript","lang":"en","kind":"lesson","summary":"Keep end-to-end tests independent with Playwright’s isolated browser contexts, custom fixtures that create and clean up their own data, saved login state and targeted network routing.","html":"<p>The most common reason an end-to-end suite becomes untrustworthy isn’t timing; it’s <strong>shared state</strong>. Test 12 passes alone and fails in the full run because test 7 renamed the group it uses. Or a test passes only on Mondays because the seed data has a “this week” expense. Isolation is the cure, and Playwright gives you most of it for free.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Fresh context for every test</h2>\n<p>Each test receives its own <strong>browser context</strong>: an incognito-like profile with its own cookies, localStorage, sessionStorage and cache. Opening a context is cheap (milliseconds, not a new browser), so Playwright creates a new one per test. That’s why you can run with <code dir=\"ltr\">fullyParallel: true</code> across several workers: two tests can’t see each other’s logins or saved drafts.</p>\n<p>The browser side is isolated. <strong>Your backend is not.</strong> If two tests edit the same group on the same server, they collide. The rest of this lesson is about isolating data.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Fixtures: setup and cleanup in one place</h2>\n<p>You’ve been using fixtures already: <code dir=\"ltr\">page</code> in <code dir=\"ltr\">async ({ page }) =&gt; …</code> is one. Playwright also ships <code dir=\"ltr\">context</code>, <code dir=\"ltr\">browser</code>, <code dir=\"ltr\">browserName</code> and <code dir=\"ltr\">request</code> (an HTTP client that shares <code dir=\"ltr\">baseURL</code>). You define your own with <code dir=\"ltr\">test.extend</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">e2e/fixtures.ts</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 </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> base, expect } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '@playwright/test'</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\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Group</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</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\"> test</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> base.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">extend</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;{ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">group</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Group</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }>({</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  group</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:#953800;--shiki-dark:#FFA657\">request</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">use</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:#6E7781;--shiki-dark:#8B949E\">    // Setup: create a fresh group through the API</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> response</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> request.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">post</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'/api/groups'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      data: { name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Trip ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">crypto</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">randomUUID</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">slice</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">8</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">)</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, members: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Ana'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Ben'</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:#8250DF;--shiki-dark:#D2A8FF\">    expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(response.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">ok</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:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> group</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Group</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> response.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">json</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:#8250DF;--shiki-dark:#D2A8FF\"> use</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(group); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// the test runs here</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">    // Teardown: runs after the test, pass or fail</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> request.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">delete</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`/api/groups/${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">group</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\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { expect };</span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">e2e/settle-up.spec.ts</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\"> './fixtures'</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\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'settling up clears every balance'</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:#953800;--shiki-dark:#FFA657\">page</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">group</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\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">goto</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`/groups/${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">group</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\">);</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // …add an expense, click Settle up…</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(page.</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\">'status'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveText</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Everyone is settled up'</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>Three properties make this better than a <code dir=\"ltr\">beforeEach</code>:</p>\n<ul>\n<li><strong>On demand.</strong> Only tests that list <code dir=\"ltr\">group</code> in their parameters create one. Tests that don’t need it pay nothing.</li>\n<li><strong>Setup and teardown live together</strong>, around <code dir=\"ltr\">await use(...)</code>, so cleanup can’t drift out of sync with setup.</li>\n<li><strong>Composable.</strong> A <code dir=\"ltr\">group</code> fixture can depend on <code dir=\"ltr\">request</code>; an <code dir=\"ltr\">expense</code> fixture could depend on <code dir=\"ltr\">group</code>. Playwright resolves the order.</li>\n</ul>\n<p>Creating data <strong>through the API</strong> rather than clicking through the UI keeps each test focused on one flow and saves seconds per test. One test clicks through “create a group”; the other fifty get a group from the fixture.</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>Tests that rely on each other</span></p>\n<p>“Test 1 creates the group, test 2 adds an expense, test 3 settles up” reads like a story and behaves like a chain: it can’t run in parallel, can’t be rerun individually, and one failure cascades into three. Make each test create what it needs, with a unique name (a random suffix) so parallel workers never collide.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Logging in once</h2>\n<p>If Splitwise-lite requires login, clicking through the form in every test wastes time and hammers your auth server. Playwright’s pattern is a <strong>setup project</strong>: one test logs in and saves the browser’s storage state to a file, and the real projects declare a dependency on it and start already logged in:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">playwright.config.ts</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:#6E7781;--shiki-dark:#8B949E\">// …inside defineConfig</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">projects</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  { name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'setup'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, testMatch:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> /</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#116329;--shiki-light-font-weight:bold;--shiki-dark:#7EE787;--shiki-dark-font-weight:bold\">\\.</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">setup</span><span style=\"color:#116329;--shiki-light-font-weight:bold;--shiki-dark:#7EE787;--shiki-dark-font-weight:bold\">\\.</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">ts/</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\">    name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'chromium'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    use: { </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">devices[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Desktop Chrome'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">], storageState: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'playwright/.auth/user.json'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    dependencies: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'setup'</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>The setup test ends with <code dir=\"ltr\">await page.context().storageState({ path: 'playwright/.auth/user.json' })</code>. Add <code dir=\"ltr\">playwright/.auth</code> to <code dir=\"ltr\">.gitignore</code>: it contains real session cookies.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Routing: controlling what the server says</h2>\n<p>Some states are hard to produce with a real backend: a 500 on save, a slow response, an empty group from a legacy account. <code dir=\"ltr\">page.route</code> intercepts requests from the page and lets you answer them:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</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:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'explains a failed save'</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:#953800;--shiki-dark:#FFA657\">page</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">group</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\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">route</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'**/api/expenses'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">route</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\">    route.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fulfill</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ status: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">500</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, json: { error: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Database unavailable'</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\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">goto</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`/groups/${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">group</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\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByLabel</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Description'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fill</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Dinner'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByLabel</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Amount'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fill</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'30'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</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\">'button'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Add expense'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">click</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:#8250DF;--shiki-dark:#D2A8FF\"> expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(page.</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\">'alert'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveText</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Could not save the expense. Try again.'</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>Use routing for the rare and the broken. If you find yourself routing every request, you’ve rebuilt a component test in a slower tool; let the happy paths hit the real backend, because that integration is what end-to-end tests are for.</p>\n<aside class=\"callout callout-why\" 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\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">Why this matters</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Isolation is what makes parallelism safe</span></p>\n<p>A suite of 200 independent tests runs on 8 workers in an eighth of the time. A suite with hidden dependencies has to run serially, or it fails randomly. Isolation isn’t hygiene for its own sake; it’s what lets the suite stay fast as it grows.</p>\n</aside>\n<p>Isolation also makes failures cheaper to debug. When a test fails, you can rerun that single test with <code dir=\"ltr\">npx playwright test -g &quot;settling up&quot;</code> and get the same result, because nothing it depends on lives outside it. With hidden dependencies, the single rerun passes and the full run fails, and you’re left bisecting the suite to find which neighbour broke it.</p>\n<p>In the exercise you complete a fixture that creates and deletes a group. Next lesson: what to do when a test fails anyway.</p>\n","toc":[{"id":"sec-1","text":"Fresh context for every test","level":2},{"id":"sec-2","text":"Fixtures: setup and cleanup in one place","level":2},{"id":"sec-3","text":"Logging in once","level":2},{"id":"sec-4","text":"Routing: controlling what the server says","level":2}],"takeaways":["Every Playwright test gets a fresh browser context (its own cookies, storage and cache), so tests can run in parallel and in any order.","A fixture created with <code dir=\"ltr\">test.extend</code> sets something up, hands it to the test with <code dir=\"ltr\">use()</code>, and tears it down afterwards, only for tests that ask for it.","Each test should create the data it needs (ideally through the API) instead of depending on shared seed data or on another test having run first.","Log in once in a setup project, save the storage state, and reuse it, rather than clicking through the login form in every test.","Use <code dir=\"ltr\">page.route</code> to simulate rare server responses, and the real backend for the happy paths."],"further":[{"title":"Playwright — Fixtures","url":"https://playwright.dev/docs/test-fixtures"},{"title":"Playwright — Isolation","url":"https://playwright.dev/docs/browser-contexts"},{"title":"Playwright — Authentication","url":"https://playwright.dev/docs/auth"},{"title":"Playwright — Mock APIs","url":"https://playwright.dev/docs/mock"}],"quiz":[{"q":"<p>Test A creates a group named “Lisbon trip”; test B opens “Lisbon trip” and checks its balances. B passes locally and fails in CI. What’s the root problem?</p>","options":[{"text":"CI machines are slower, so B needs a longer timeout.","why":"Speed isn’t the issue; B depends on data that only exists if A ran first, on the same server, before it."},{"text":"B depends on A’s data. With parallel workers or a different order, the group doesn’t exist yet when B runs.","why":"Correct. Each test should create its own group, for example through a fixture that calls the API and cleans up after."},{"text":"B should use <code dir=\"ltr\">test.describe.serial</code> so it always runs after A.","why":"Serial mode makes the dependency official and the suite slower; one failure in A then skips everything after it. Remove the dependency instead."}],"answer":1},{"q":"<p>In a fixture, what does the code after <code dir=\"ltr\">await use(group)</code> do?</p>","options":[{"text":"It runs before the test, to prepare data.","why":"Setup runs before <code dir=\"ltr\">use</code>; the line after it waits until the test is done."},{"text":"Nothing; code after <code dir=\"ltr\">use</code> is never reached.","why":"<code dir=\"ltr\">use</code> resolves when the test finishes, so the code after it runs, which is how fixtures clean up."},{"text":"It runs after the test finishes, even if the test failed, which makes it the place for cleanup.","why":"Correct. Setup, <code dir=\"ltr\">use(value)</code>, teardown: one function owns the whole lifecycle of the resource."}],"answer":2},{"q":"<p>What is the best use of <code dir=\"ltr\">page.route</code> in an end-to-end suite?</p>","options":[{"text":"Simulating a rare server response, such as a 500 when saving, to test the error message.","why":"Correct. Hard-to-trigger responses are where routing shines; the happy path should still hit the real backend."},{"text":"Mocking every API call so tests never depend on the backend.","why":"Then it isn’t end-to-end any more; you’d be re-testing the UI with fake data, which component tests do faster."},{"text":"Speeding up slow tests by skipping requests to your own API.","why":"Skipping your own API removes the integration the test exists to check; make the API or the test data faster instead."}],"answer":0}],"exercise":{"kind":"fill","language":"ts","template":"import { test as base, expect } from '@playwright/test';\n\ntype Group = { id: string; name: string };\n\nexport const test = base.{{0}}<{ group: Group }>({\n  group: async ({ {{1}} }, use) => {\n    const response = await request.post('/api/groups', {\n      data: { name: `Trip ${crypto.randomUUID().slice(0, 8)}` },\n    });\n    const group: Group = await response.json();\n\n    await {{2}}(group);\n\n    await request.{{3}}(`/api/groups/${group.id}`);\n  },\n});\n\ntest('a new group starts settled up', async ({ page, {{4}} }) => {\n  await page.goto(`/groups/${group.id}`);\n  await {{5}}(page.getByRole('status')).toHaveText('Everyone is settled up');\n});","blanks":[{"answers":["extend"]},{"answers":["request"]},{"answers":["use"]},{"answers":["delete"]},{"answers":["group"]},{"answers":["expect"]}],"title":"Complete a group fixture","prompt":"<p>Finish this Playwright fixture so every test that asks for a <code dir=\"ltr\">group</code> gets a <strong>fresh</strong> one created through the API, and the group is <strong>deleted</strong> after the test, pass or fail. Then finish the test so it receives the fixture and uses a web-first assertion.</p>\n","hints":["Custom fixtures are defined by calling a method on the base <code dir=\"ltr\">test</code> object that returns a new, extended <code dir=\"ltr\">test</code>.","The fixture needs the built-in HTTP client fixture, the one the body calls <code dir=\"ltr\">.post</code> on.","Hand the value to the test with the second parameter of the fixture function; afterwards, clean up with the HTTP method that removes a resource. The test lists the fixture by name in its parameters, and asserts with Playwright’s retrying <code dir=\"ltr\">expect</code>."],"explanation":"<p><code dir=\"ltr\">base.extend&lt;{ group: Group }&gt;({...})</code> returns a new <code dir=\"ltr\">test</code> that knows about the <code dir=\"ltr\">group</code> fixture. The fixture asks for the built-in <code dir=\"ltr\">request</code> fixture, creates a group with a unique name, and passes it to the test with <code dir=\"ltr\">await use(group)</code>. Everything after <code dir=\"ltr\">use</code> runs when the test finishes, whatever the outcome, so <code dir=\"ltr\">request.delete(...)</code> cleans up even after a failure. The test opts in by destructuring <code dir=\"ltr\">group</code> next to <code dir=\"ltr\">page</code>; tests that don’t mention it never create one. Finally, <code dir=\"ltr\">await expect(locator).toHaveText(...)</code> retries until the status appears, so the test doesn’t race the page load.</p>\n","checkLabels":{}},"words":660,"readMinutes":3}