<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Molar guides</title>
    <link>https://molar.it/blog</link>
    <description>Practical engineering guides on browser QA, integrations, release confidence, and agent workflows.</description>
    <language>en</language>
    <atom:link href="https://molar.it/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Payment succeeded. Why is the account still locked?</title>
      <link>https://molar.it/blog/testing-delayed-payment-webhooks-and-account-access</link>
      <guid isPermaLink="true">https://molar.it/blog/testing-delayed-payment-webhooks-and-account-access</guid>
      <description>A practical test plan for delayed payment webhooks, duplicate deliveries and the account access a paying customer expects.</description>
      <pubDate>Sat, 05 Sep 2026 00:00:00 GMT</pubDate>
      <category>Integrations</category>
      <dc:creator>Molar editorial</dc:creator>
      <content:encoded><![CDATA[<section><h2 id="check-the-access-your-customer-paid-for">Check the access your customer paid for</h2><p>A checkout can show a success screen while the application still treats the customer as unpaid. That gap is easy to miss when a test ends at the payment response. Start with a more useful outcome: the intended customer can use the feature they purchased, and the application records the correct order or subscription state.</p><p>Write down the separate observations before automating anything. You need the browser result, the payment record, delivery of the relevant event and the entitlement or order created by your application. Decide what the customer should see while processing is incomplete. A pending message can be correct behavior; an unexplained locked account needs investigation.</p><p class="article-source">Sources: <a href="https://molar.it/solutions/checkout-testing">Test a checkout beyond the payment</a></p></section><figure class="article-graphic article-graphic--payment" data-article-graphic data-article-family="payment" aria-label="A payment callback changes access"><div class="article-graphic__canvas is-payment"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M154 126H301"/><path class="" d="M459 126H606"/><path class="is-flow" d="M154 126H301"/><path class="is-flow" d="M459 126H606"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card is-browser"><div class="article-graphic__eyebrow">BROWSER</div><h3>Payment confirmed</h3><p>The customer sees the checkout result.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">screen state</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">CALLBACK</div><h3>Payment succeeded</h3><p>The app receives and verifies the provider event.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">delivery can lag</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">APPLICATION</div><h3>Access granted</h3><p>Account permissions change only after the callback is applied.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">assert here</span></span></div></div></div><figcaption><strong>A payment callback changes access</strong><span>The browser can show success before the application has processed the provider callback.</span></figcaption></figure><section><h2 id="control-the-delay-then-watch-both-sides">Control the delay, then watch both sides</h2><p>Use a dedicated test account and a controlled test environment. Complete the browser checkout, then hold back the payment notification using a test fixture or delivery mechanism you control. At this point, inspect the payment state and the application state independently. Do not make the test pass merely because the browser reached a thank-you page.</p><p>Release the event and wait for the specific expected change in the application. Check the order or entitlement, then return to the browser and verify that the customer can actually use it. The test should explain whether the failure occurred before delivery, during event handling or when presenting the updated state. Repeat from clean initial data so a previous successful purchase cannot hide a broken path.</p><p class="article-source">Sources: <a href="https://molar.it/solutions/checkout-testing">Test a checkout beyond the payment</a></p></section><section><h2 id="a-repeated-event-must-not-repeat-the-business-action">A repeated event must not repeat the business action</h2><p>Stripe documents that webhook events can be retried, duplicated and delivered without an ordering guarantee. Include these cases in your handler tests. Deliver the same event again and assert that it does not create another order, grant duplicate credits or trigger the same business action twice. Signature verification establishes the sender; it does not itself make processing idempotent.</p><p>For out-of-order delivery, test the handler’s actual recovery path and inspect the resulting business state. Keep signature verification in the tested path, using the raw body and the correct test endpoint secret. Avoid treating an HTTP success response as proof that all downstream work finished.</p><p class="article-source">Sources: <a href="https://docs.stripe.com/webhooks">Stripe: handle webhook events</a> · <a href="https://molar.it/solutions/checkout-testing">Test a checkout beyond the payment</a></p></section><section><h2 id="check-access-as-the-intended-customer">Check access as the intended customer</h2><p>Run the access assertion in the customer’s own browser session. Confirm the protected feature and expected role, rather than checking only that authentication succeeded. An administrator account can have permissions that bypass the defect entirely. A fresh customer account makes the test closer to the experience you want to protect.</p><p>Add a second account as a negative case: purchasing access for one customer must not grant it to another. Revisit the original session after the delayed event, and check whether refreshing or signing in again changes the observed result. Those observations help separate an entitlement problem from a stale browser session without guessing at the cause.</p><p class="article-source">Sources: <a href="https://molar.it/solutions/authentication-testing">Check sign-in and account access</a> · <a href="https://molar.it/solutions/checkout-testing">Test a checkout beyond the payment</a></p></section><section><h2 id="use-controlled-services-and-keep-a-contract-check">Use controlled services and keep a contract check</h2><p>Molar Clones provides controlled state for supported payment behavior. It can make repeatable integration testing easier when a test depends on external services. Keep the setup explicit: configure the test environment for the supported clone endpoints and verify which operations your scenario needs. Do not assume a test clone reproduces every part of a vendor’s service.</p><p>Finish with a small vendor-sandbox contract check for the real APIs and events your application relies on. The controlled test answers whether your application handles a chosen sequence correctly; the sandbox check helps confirm that the sequence still matches the vendor contract. Together, these checks provide a clearer explanation of a failure than a single green checkout screen.</p><p class="article-source">Sources: <a href="https://molar.it/solutions/checkout-testing">Test a checkout beyond the payment</a></p></section><section><h2 id="about-this-guide">About this guide</h2><p>Prepared with AI assistance from the cited material and checked against those sources before publication. Product availability can change during beta.</p><p><a href="https://molar.it/ask">Ask a related testing question</a> or <a href="https://molar.it/blog">browse all guides</a>.</p></section>]]></content:encoded>
    </item>
    <item>
      <title>Test an invoice download beyond the button click</title>
      <link>https://molar.it/blog/verify-downloaded-invoice-browser-test</link>
      <guid isPermaLink="true">https://molar.it/blog/verify-downloaded-invoice-browser-test</guid>
      <description>Capture the download, save it before teardown, and check the invoice fields that matter. Includes a Playwright example and failure cases.</description>
      <pubDate>Sun, 06 Sep 2026 00:00:00 GMT</pubDate>
      <category>Engineering</category>
      <dc:creator>Molar editorial</dc:creator>
      <content:encoded><![CDATA[<section><h2 id="define-which-invoice-the-customer-should-receive">Define which invoice the customer should receive</h2><p>A download button can work while returning the wrong invoice. Start with a disposable account and one known invoice: a specific customer, invoice number, currency, total, and billing period. The test should prove that this account receives this document. A screenshot of the billing page is useful context, but it cannot establish what was written into the file.</p><p>Write the expected fields independently of the export response. For example, seed invoice INV-1042 for the test account with a total of USD 49.00, then compare the downloaded document with that fixture. Copying values from the same response that generated the document would allow both sides of a broken calculation to agree.</p><p>Treat access as a separate assertion. A user who can see a billing link should still need permission to retrieve its invoice. Include a second account in an authorization test and confirm that it cannot obtain the first account’s document. Keep these records synthetic so saved files can be inspected without exposing customer billing data.</p><p class="article-source">Sources: <a href="https://playwright.dev/docs/best-practices">Playwright: write resilient tests</a></p></section><figure class="article-graphic article-graphic--download" data-article-graphic data-article-family="download" aria-label="A download needs a file assertion"><div class="article-graphic__canvas is-download"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M154 126H301"/><path class="" d="M459 126H606"/><path class="is-flow" d="M154 126H301"/><path class="is-flow" d="M459 126H606"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">WAIT</div><h3>Listen for download</h3><p>Start waiting before the click so a fast export cannot be missed.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">event pending</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">ACTION</div><h3>Click Export</h3><p>Trigger the control and capture the download event from the browser.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">event received</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">FILE</div><h3>Save and validate</h3><p>Check the filename, type, and expected contents before the test passes.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">file asserted</span></span></div></div></div><figcaption><strong>A download needs a file assertion</strong><span>Wait for the download, trigger the export, then validate the saved file you expected.</span></figcaption></figure><section><h2 id="capture-and-save-the-browser-download">Capture and save the browser download</h2><p>For a control that produces a browser attachment, start waiting for the download before clicking. This avoids missing a fast response. Await that event in the main test, then save the file into the test’s own output directory. Playwright’s saveAs operation waits for completion; the download event itself only tells you that downloading started.</p><p>The example below checks transport completion, the suggested extension, and the PDF signature. It is a starting check, not a complete invoice assertion. Replace the button label with your app’s accessible name. If the app opens an inline PDF viewer instead of an attachment, this event-based example does not apply unchanged: verify that navigation and the document response separately.</p><p>Save before the browser context closes. Playwright removes that context’s temporary downloads during teardown. Use a per-test output path instead of a shared invoice.pdf in the repository, especially when workers run concurrently. A failed run should leave its own document available for investigation under your normal artifact retention policy.</p><div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>import { test, expect } from &#39;@playwright/test&#39;;
import { readFile } from &#39;node:fs/promises&#39;;

test(&#39;invoice downloads as a PDF&#39;, async ({ page }, testInfo) =&gt; {
  // Configure baseURL and a test account that owns INV-1042.
  await page.goto(&#39;/billing/invoices/INV-1042&#39;);
  const pending = page.waitForEvent(&#39;download&#39;);
  await page.getByRole(&#39;button&#39;, { name: &#39;Download invoice&#39; }).click();
  const download = await pending;
  expect(await download.failure()).toBeNull();
  expect(download.suggestedFilename()).toMatch(/\.pdf$/i);

  const file = testInfo.outputPath(&#39;invoice.pdf&#39;);
  await download.saveAs(file);
  const bytes = await readFile(file);
  expect(bytes.subarray(0, 5).toString()).toBe(&#39;%PDF-&#39;);
  // Next: parse and assert the invoice fields described below.
});</code></pre><p class="copy-status sr-only" role="status"></p></div><p class="article-source">Sources: <a href="https://playwright.dev/docs/downloads">Playwright: wait for and save a download</a> · <a href="https://playwright.dev/docs/api/class-download">Playwright: inspect download completion and filenames</a></p></section><section><h2 id="validate-the-document-not-just-its-filename">Validate the document, not just its filename</h2><p>A file named invoice.pdf could contain an error page, an empty export, or another customer’s invoice. The header check above catches some obvious wrong-file responses; it does not prove that the PDF is well formed or that its contents are correct. Parse the saved document with a PDF parser your project supports, then compare the extracted invoice number, recipient, currency, and total with the seeded expectation.</p><p>Use assertions suited to the format. For a CSV export, parse columns and values rather than searching the raw file for a number that might appear in an unrelated row. For a PDF, normalize only known formatting differences such as whitespace. Do not remove currency symbols, signs, or decimal separators in a way that makes different amounts compare equal.</p><p>Check the suggested filename when its naming pattern is part of the product contract. Otherwise, avoid requiring an exact timestamp or browser-specific spelling. Keep filename checks distinct from document checks so a failure explains whether naming changed, the transfer failed, or the invoice itself is wrong.</p><p class="article-source">Sources: <a href="https://playwright.dev/docs/api/class-download">Playwright: inspect download completion and filenames</a> · <a href="https://playwright.dev/docs/best-practices">Playwright: write resilient tests</a></p></section><section><h2 id="make-the-unhappy-paths-explain-themselves">Make the unhappy paths explain themselves</h2><p>Run separate cases for an expired session, an unauthorized invoice, and an export that fails. Assert the intended error message or response rather than waiting indefinitely for a file that should never arrive. Add a two-invoice case to catch stale selection: choose the second invoice and verify that the first document is not downloaded again.</p><p>On failure, retain the expected invoice identifier, failed assertion, and saved file when available. Keep the browser trace alongside them to show which control was clicked. Do not label a run successful because the browser action completed if file validation never ran. This distinction gives the next person a concrete starting point for debugging.</p><p class="article-source">Sources: <a href="https://playwright.dev/docs/api/class-download">Playwright: inspect download completion and filenames</a> · <a href="https://playwright.dev/docs/best-practices">Playwright: write resilient tests</a></p></section><section><h2 id="about-this-guide">About this guide</h2><p>Prepared with AI assistance from the cited material and checked against those sources before publication. Product availability can change during beta.</p><p><a href="https://molar.it/ask">Ask a related testing question</a> or <a href="https://molar.it/blog">browse all guides</a>.</p></section>]]></content:encoded>
    </item>
    <item>
      <title>AI agents and the future of software testing</title>
      <link>https://molar.it/blog/ai-agents-future-software-testing</link>
      <guid isPermaLink="true">https://molar.it/blog/ai-agents-future-software-testing</guid>
      <description>How coding agents can inspect browser results, review generated tests, and check changes before release.</description>
      <pubDate>Mon, 29 Jun 2026 00:00:00 GMT</pubDate>
      <category>Agents</category>
      <content:encoded><![CDATA[<section>
        <h2 id="more-code-ships-verification-still-waits">More code ships; verification still waits</h2>
        <p>
          Code generation got cheap. Code verification didn’t. A coding agent can emit a thousand-line diff
          in minutes; a human reviewer needs an hour to read it properly, and a test suite that predates the
          agent can only check what someone thought to specify. The result is an asymmetry: throughput of
          change is exploding while confidence per change is flat or falling.
        </p>
        <p>Test authoring can become another bottleneck as code changes faster. Agents can help draft scenarios and investigate unfamiliar pages, but teams still need to review assertions and establish that each test runs reliably.</p>
      </section><figure class="article-graphic article-graphic--agent" data-article-graphic data-article-family="agent" aria-label="Review the generated test before CI"><div class="article-graphic__canvas is-agent"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M154 126H301"/><path class="" d="M459 126H606"/><path class="is-flow" d="M154 126H301"/><path class="is-flow" d="M459 126H606"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">TASK</div><h3>Plain-English browser task</h3><p>Describe the behavior and the success condition.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">intent first</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">PROPOSAL</div><h3>Playwright spec draft</h3><p>The export includes locators, assertions, and a clean starting state.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">diff to inspect</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">REVIEW</div><h3>Run the reviewed spec</h3><p>The committed test decides pass or fail without an inference call.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">human decision</span></span></div></div></div><figcaption><strong>Review the generated test before CI</strong><span>Let an agent propose the task or repair, then keep the executed check repeatable and reviewable.</span></figcaption></figure>

      <section>
        <h2 id="review-generated-tests-before-running-them-in-ci">Review generated tests before running them in CI</h2>
        <p>
          The design choice that separates serious systems from demos is where the model sits. LLMs are
          excellent at the parts of testing that benefit from judgment: reading an unfamiliar app, mapping
          flows, proposing assertions, diagnosing why a selector broke. They are terrible at the parts that
          need repeatability: deciding pass/fail on Tuesday the same way they did on Monday.
        </p>
        <p>
          So put the model at authoring and healing time, never in the runtime path:
        </p>
        <ul>
          <li><strong>Authoring:</strong> an exploration agent crawls your app and emits real Playwright specs from plain-English scenarios — the mechanics we detailed in <a href="https://molar.it/blog/ai-playwright-test-generation">AI Playwright test generation</a>.</li>
          <li><strong>Healing:</strong> when a locator breaks, the model proposes a patch against the new DOM; a human or policy layer approves it — the discipline described in <a href="https://molar.it/blog/self-healing-test-automation">self-healing test automation</a>.</li>
          <li><strong>Runtime:</strong> the emitted spec executes deterministically. Same input, same verdict. No inference call decides whether your checkout test passes.</li>
        </ul>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code># Runtime path contains zero LLM calls:
#   spec (committed) → browser → assertions → pass/fail
# Model touches only:
#   crawl → generate → propose heal → human review → commit</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <div>
          Trust rule: any system where a probabilistic component decides pass/fail at runtime cannot be a
          merge gate. Determinism isn’t a nicety here — it’s what makes a red check mean something.
        </div>
      </section>

      <section>
        <h2 id="coding-agents-should-verify-their-own-work">Coding agents should verify their own work</h2>
        <p>
          The most underrated shift is protocol-shaped. Via MCP, a coding agent doesn’t just write the
          feature — it invokes the QA platform itself, gets structured results back, reads the failure
          evidence, fixes the code, and re-runs. No human relay in between:
        </p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>molar verify "the login form is visible" --url http://localhost:3000 --json</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>Structured results let a coding agent inspect a failed check without a developer manually copying screenshots. <a href="https://molar.it/platform/developer-tools">Molar’s CLI and MCP interfaces</a> make browser checks available from the development workflow. The app and required test access must be configured first.</p>
      </section>

      <section>
        <h2 id="decide-which-behavior-the-test-must-protect">Decide which behavior the test must protect</h2>
        <p>
          Agents are converging on execution. Three things remain yours:
        </p>
        <ul>
          <li><strong>Intent.</strong> What should be true? Which flows matter enough to gate a merge on? An agent can discover flows, but only you know which ones would end your business if they broke.</li>
          <li><strong>Judgment calls.</strong> Is this pixel drift acceptable? Is this edge case a bug or a feature? These are product decisions wearing engineering costumes.</li>
          <li><strong>Review of proposed changes.</strong> Including proposed test changes — a healing patch that quietly weakens an assertion is a vulnerability, not a convenience.</li>
        </ul>
        <p>
          The role shift is real but familiar: less time authoring steps, more time specifying outcomes
          and auditing what the machines proposed. It mirrors what happened to compilers — nobody misses
          hand-writing assembly, and nobody reviews the machine code either.
        </p>
      </section>

      <section>
        <h2 id="the-failure-mode-worth-designing-against">The failure mode worth designing against</h2>
        <p>
          Every automation wave in testing has produced the same pathology: tests that pass while the
          product breaks, because the suite was optimized for green rather than truth. Agent-driven QA can
          amplify this dramatically — an agent that both writes and heals its own tests can, without any
          malice, converge on assertions that match whatever the app currently does.
        </p>
        <p>
          Three structural defenses: keep runtime verdicts deterministic so healing requires a reviewable
          diff; version scenarios as source files so weakening an assertion shows up like any other change;
          and sample agent-modified suites against known-broken builds periodically to confirm they still
          catch what they claim to catch. Mutation-style spot checks are cheap insurance here — break the
          checkout total on purpose once a month and confirm something red appears within one run.
        </p>
      </section>

      <section>
        <h2 id="the-verification-model-to-build">The verification model to build</h2>
        <p>A useful direction is to version plain-English scenarios alongside code, review generated changes, and rerun tests from clean state. Humans still decide what behavior matters and whether an unexpected change is acceptable.</p>
        <p>To evaluate Molar, <a href="https://molar.it/examples/autonomous-browser-check">inspect a recorded browser task</a> and then choose one check for your app. Gxported Playwright tests are in beta; review each one before it runs in CI.</p>]]></content:encoded>
    </item>
    <item>
      <title>AI Playwright test generation: from a task to a reviewed test</title>
      <link>https://molar.it/blog/ai-playwright-test-generation</link>
      <guid isPermaLink="true">https://molar.it/blog/ai-playwright-test-generation</guid>
      <description>How to review generated browser tests: locators, assertions, clean starting state, and repeatability in CI.</description>
      <pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate>
      <category>Agents</category>
      <content:encoded><![CDATA[<section><h2 id="generation-is-the-beginning-of-test-review">Generation is the beginning of test review.</h2><p>An AI-assisted workflow can turn an app URL and a task into a draft Playwright file. A useful pipeline still has distinct decisions: discover the pages, describe the user task, choose locators, write assertions, run from clean state, and review the diff. Treat the output as source code proposed by a tool, not as a passing test.</p><p><a href="https://playwright.dev/docs/codegen">Playwright Codegen</a> can record actions and suggest locators. Molar Cartographer explores authorized applications and captures browser tasks; its generated Playwright export and clean replay remain in beta. Review the exported file, its credentials and fixtures, and the application behavior it claims to protect.</p></section><figure class="article-graphic article-graphic--agent" data-article-graphic data-article-family="agent" aria-label="Review the generated test before CI"><div class="article-graphic__canvas is-agent"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M154 126H301"/><path class="" d="M459 126H606"/><path class="is-flow" d="M154 126H301"/><path class="is-flow" d="M459 126H606"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">TASK</div><h3>Plain-English browser task</h3><p>Describe the behavior and the success condition.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">intent first</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">PROPOSAL</div><h3>Playwright spec draft</h3><p>The export includes locators, assertions, and a clean starting state.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">diff to inspect</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">REVIEW</div><h3>Run the reviewed spec</h3><p>The committed test decides pass or fail without an inference call.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">human decision</span></span></div></div></div><figcaption><strong>Review the generated test before CI</strong><span>Let an agent propose the task or repair, then keep the executed check repeatable and reviewable.</span></figcaption></figure>
<section><h2 id="prefer-locators-that-describe-the-control">Prefer locators that describe the control.</h2><p>Playwright’s <a href="https://playwright.dev/docs/locators">locator guidance</a> recommends user-facing locators such as roles, labels, and text, with explicit test IDs when a stable contract is needed. A generated selector like <code>div:nth-child(3) &gt; span</code> depends on layout. A role locator communicates the control and often survives a CSS refactor.</p><div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>await page.getByLabel('Email').fill(testUser.email);
await page.getByLabel('Password').fill(testUser.password);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();</code></pre><p class="copy-status sr-only" role="status"></p></div><p>Review strictness and uniqueness. If two buttons have the same accessible name, decide whether the product needs clearer semantics or the test needs a more precise scope. Do not accept a locator that passes only because it happens to match the first element today.</p></section>
<section><h2 id="ask-what-the-test-proves-after-every-important-action">Ask what the test proves after every important action.</h2><p>A click is not a business result. After signup, assert the verified account or workspace. After checkout, assert the order and entitlement. After an invitation, assert both the recipient and the granted role. A model can suggest an assertion, but the product owner or engineer must decide whether it demonstrates the behavior worth protecting.</p><p>Watch for weak generated checks: page loaded, URL changed, button disappeared, or text contains a generic “success.” Pair the visible assertion with a downstream record when the workflow crosses a service boundary. A screenshot can support investigation; it cannot replace the assertion.</p></section>
<section><h2 id="run-the-exported-file-from-clean-state">Run the exported file from clean state.</h2><p>Before making generated code part of CI, review its setup and run it twice from equivalent starting conditions. Confirm that it creates or selects the intended test identity, resets service data, waits on meaningful state, and leaves no customer or live-provider side effect. Make the test fail deliberately once so you know the assertion catches the intended regression.</p><div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>npx playwright test e2e/checkout.spec.ts --trace on
npx playwright show-trace test-results/checkout/trace.zip</code></pre><p class="copy-status sr-only" role="status"></p></div><p>Playwright’s <a href="https://playwright.dev/docs/trace-viewer">Trace Viewer</a> can open the saved trace for action, DOM, network, and console inspection. In CI, prefer a configured failure or retry trace policy instead of recording everything by default when that cost does not fit your workflow.</p></section>
<section><h2 id="keep-generated-changes-reviewable">Keep generated changes reviewable.</h2><p>Commit the reviewed spec, fixtures, and any test data contract together. When the app changes, compare the proposed locator or assertion with the failure that prompted it. A repair that changes “account has Pro access” into “checkout page loaded” has hidden the regression. Keep the original intent visible in the test name and assertion.</p><p>Use version control to review generated diffs, and rerun the test after editing it. Model output can vary between runs, so avoid treating a regenerated file as a harmless formatting update. Check imports, async handling, fixture scope, and command syntax the same way you would for handwritten Playwright code.</p></section>
<section><h2 id="keep-payments-and-credentials-in-a-test-environment">Keep payments and credentials in a test environment.</h2><p>Generated checkout tests should use Stripe test mode or a supported clone, never a live payment credential. The <a href="https://molar.it/blog/test-stripe-without-real-cards">Stripe checkout guide</a> explains the payment, callback, and account assertions that belong in one task. Molar’s beta export is a starting point for review; it does not establish that the generated file is ready for your repository or release gate.</p><p><a href="https://molar.it/products/cartographer">Explore Cartographer</a>, <a href="https://molar.it/examples/autonomous-browser-check">watch a recorded browser task</a>, or <a href="https://molar.it/contact">talk to us</a> with the setup and limits of your app in view.</p></section><section><h2 id="when-a-locator-fails-repair-the-cause-you-can-explain">When a locator fails, repair the cause you can explain.</h2><p>Start with the trace and the accessible structure at the failed step. If the button’s accessible name changed because the product copy changed, update the locator and review the product behavior. If the generated selector depended on a wrapper or position, replace it with a role, label, or explicit test ID that expresses the intended contract.</p><div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>const checkout = page.getByRole('region', { name: 'Checkout' });
await checkout.getByRole('button', { name: 'Pay now' }).click();
await expect(page.getByText('Payment confirmed')).toBeVisible();</code></pre><p class="copy-status sr-only" role="status"></p></div><p>Run the repaired test from the same clean state and inspect the diff. A passing rerun is evidence that this scenario works; it does not prove every route, browser, payment method, or generated test is correct.</p></section>]]></content:encoded>
    </item>
    <item>
      <title>API mocking vs stateful service clones</title>
      <link>https://molar.it/blog/api-mocking-vs-service-clones</link>
      <guid isPermaLink="true">https://molar.it/blog/api-mocking-vs-service-clones</guid>
      <description>Compare configured API responses, stateful mocks, and supported service clones for integration testing.</description>
      <pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate>
      <category>Integrations</category>
      <content:encoded><![CDATA[<section><h2 id="choose-the-smallest-substitute-that-proves-the-behavior">Choose the smallest substitute that proves the behavior.</h2><p>An API mock answers a request with a response you define. A stateful mock changes that response as the test moves through a scenario. A service clone implements selected provider behavior and keeps records that later requests can read or change. All three can keep test traffic away from a live customer service; they differ in what your team must model and what the test can observe.</p><p>For a tax label or feature flag, one response may be enough. For checkout, a useful test may need a customer, a payment, a callback, an order, and an entitlement to agree. Start by listing the request and state transitions your application actually depends on. Choose a substitute that can represent those transitions without pretending to cover the provider’s entire API.</p></section><figure class="article-graphic article-graphic--service" data-article-graphic data-article-family="service" aria-label="Test the boundary your app actually crosses"><div class="article-graphic__canvas is-service"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M154 126H301"/><path class="" d="M459 126H606"/><path class="is-flow" d="M154 126H301"/><path class="is-flow" d="M459 126H606"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">APPLICATION</div><h3>Vendor-shaped request</h3><p>Your app calls the same API contract used in the integration.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">base URL changes</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">ISOLATED SERVICE</div><h3>State you can inspect</h3><p>A clone or mock returns the behavior your scenario needs without shared live side effects.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">known response</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">ASSERTION</div><h3>Customer-visible result</h3><p>Check the browser state and the callback or record that caused it.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">contract + screen state</span></span></div></div></div><figcaption><strong>Test the boundary your app actually crosses</strong><span>Keep the vendor API request, isolate its state, and inspect the callback or response your application consumes.</span></figcaption></figure>
<section><h2 id="stateful-mocks-are-real-state-machines">Stateful mocks are real state machines.</h2><p>WireMock’s <a href="https://wiremock.org/docs/stateful-behaviour/">scenario documentation</a> models a sequence with an initial state, request matching, and a transition to a named next state. That is enough for many workflows and keeps the behavior close to the test’s intent. It also means your team owns the mappings, response bodies, state names, and reset discipline.</p><div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>stubFor(post(urlEqualTo("/checkout"))
  .inScenario("checkout")
  .whenScenarioStateIs(Scenario.STARTED)
  .willReturn(okJson("{\"status\":\"processing\"}"))
  .willSetStateTo("completed"));

stubFor(get(urlEqualTo("/checkout/status"))
  .inScenario("checkout")
  .whenScenarioStateIs("completed")
  .willReturn(okJson("{\"status\":\"paid\"}")));</code></pre><p class="copy-status sr-only" role="status"></p></div><p>The Java DSL above is illustrative and assumes WireMock’s static imports. Reset scenarios between tests, and verify that the application is calling the mock endpoint you intended. A stale base URL can make a test pass against a real service or fail because only half the calls were intercepted.</p></section>
<section><h2 id="clones-package-provider-behavior-you-can-reuse">Clones package provider behavior you can reuse.</h2><p>A clone can reduce repeated fixture work when several tests need the same provider-shaped records and transitions. Molar Clones exposes only the operations and states documented for each service. Check the <a href="https://docs.molar.it/docs/clones">clone catalog</a> against your application’s calls before relying on it. A Stripe clone covering a PaymentIntent does not imply coverage for every subscription, Connect, tax, or dispute path.</p><p>The practical tradeoff is control versus compatibility. A configurable mock gives you precise response control and can model an unusual failure quickly. A provider test environment checks more of the provider contract but may be slower or less controllable. A clone gives reusable supported behavior and inspectable records, but still needs compatibility checks against the provider.</p></section>
<section><h2 id="run-the-same-checkout-against-each-option">Run the same checkout against each option.</h2><ol><li>Create a disposable customer and start with no entitlement.</li><li>Submit the payment request from the browser or application API.</li><li>Read the created payment record with a later request.</li><li>Deliver the callback or transition the scenario state.</li><li>Assert that the application grants access once.</li><li>Repeat the callback and confirm idempotency.</li><li>Reset all state and confirm the next run starts clean.</li></ol><p>Record which layer each assertion checks. A 200 response proves that one request succeeded; it does not prove that your order changed. A stateful substitute proves the model you configured or the behavior it implements; it does not prove that the real provider accepts every field or emits identical timing.</p></section>
<section><h2 id="make-failure-and-compatibility-checks-explicit">Make failure and compatibility checks explicit.</h2><p>Choose one declined payment, one delayed callback, one malformed or missing object, and one signature failure that matters to your application. Compare status codes, required fields, error payloads, and callback ordering with the provider’s authorized test environment. Keep the compatibility test small enough to run when you update your integration or substitute.</p><p>Give each parallel run its own namespace, account, or process. Reset WireMock scenarios with its admin API or restart the instance according to your setup. For a clone, use the documented snapshot and restore path. Do not assume that “reset” cleared an external provider sandbox.</p></section>
<section><h2 id="choose-from-the-calls-your-app-actually-makes">Choose from the calls your app actually makes.</h2><p>Read Molar’s <a href="https://docs.molar.it/docs/clones">supported clone catalog</a>, then map each required endpoint to a mock, clone, or provider test path. Keep the substitute close to versioned test code and review changes to mappings as carefully as application code. The <a href="https://molar.it/vs/wiremock">WireMock comparison</a> and <a href="https://molar.it/blog/test-stripe-without-real-cards">Stripe testing guide</a> show the tradeoff on concrete payment examples.</p><p>No substitute removes the need to test the provider contract. It gives you a safer, repeatable place to exercise your application’s own behavior before a smaller compatibility check reaches the provider sandbox.</p></section>]]></content:encoded>
    </item>
    <item>
      <title>Continuous testing in your CI/CD pipeline</title>
      <link>https://molar.it/blog/continuous-testing-ci-cd-pipeline</link>
      <guid isPermaLink="true">https://molar.it/blog/continuous-testing-ci-cd-pipeline</guid>
      <description>Choose which tests to run during development, pull requests, deployment, and production monitoring.</description>
      <pubDate>Mon, 06 Jul 2026 00:00:00 GMT</pubDate>
      <category>Engineering</category>
      <content:encoded><![CDATA[<section>
        <h2 id="set-the-wait-time-for-each-pipeline-stage">Set the wait time for each pipeline stage</h2>
        <p>
          Before deciding what to run where, decide what developers are allowed to wait for. A starting
          budget teams can tune to their CI:
        </p>
        <ul>
          <li><strong>Pre-push / commit:</strong> keep the loop short enough that people run it locally.</li>
          <li><strong>Pull request:</strong> keep required checks within a review-friendly wall-clock budget.</li>
          <li><strong>Deploy:</strong> keep smoke checks short because they block the release itself.</li>
          <li><strong>Scheduled (production):</strong> unbounded runtime is fine — nobody is waiting.</li>
        </ul>
        <p>
          The budgets are the strategy. Anything that can’t fit a stage’s budget moves right — to a later
          stage where waiting is acceptable — instead of slowing the earlier one. This is the core of the
          shift-left discipline we describe in
          <a href="https://molar.it/blog/shift-left-testing-explained">shift-left testing for shipping teams</a>: catch bugs
          early, but only pay for early detection where it’s cheap.
        </p>
      </section><figure class="article-graphic article-graphic--release" data-article-graphic data-article-family="release" aria-label="A release gate narrows the decision"><div class="article-graphic__canvas is-release"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M154 126H301"/><path class="" d="M459 126H606"/><path class="" d="M380 126V67H606"/><path class="is-flow" d="M154 126H301"/><path class="is-flow" d="M459 126H606"/><path class="is-flow" d="M380 126V67H606"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">CHANGE</div><h3>Pull request</h3><p>Start with the paths affected by the change.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">scope the run</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">CHECKS</div><h3>Signup · checkout · access</h3><p>Each scenario reports its own assertion and captured route.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">one blocked</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">DECISION</div><h3>Ready or blocked</h3><p>A reviewer sees why the release can proceed or what needs attention.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">reviewable result</span></span></div></div></div><figcaption><strong>A release gate narrows the decision</strong><span>Run the checks that match the change, attach the failed route, and leave the merge decision reviewable.</span></figcaption></figure>

      <section>
        <h2 id="commit-stage-seconds-not-minutes">Commit stage: seconds, not minutes</h2>
        <p>
          What runs before code even reaches shared branches should be nearly free: lint, typecheck, unit
          tests, maybe a fast subset of integration tests against containers. The goal is not coverage —
          it’s a tight loop that catches the majority of mechanical mistakes.
        </p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code># package.json — the local loop stays under ~90s
{
  "scripts": {
    "check": "npm run lint && npm run typecheck && vitest run --changed HEAD"
  }
}</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>
          Note <code>--changed</code>: running only tests affected by the diff keeps the loop honest as the
          suite grows. If your “fast” stage regularly exceeds its budget, the fix is almost always sharding,
          caching, or moving slow tests right — never asking developers to wait longer.
        </p>
      </section>

      <section>
        <h2 id="pr-stage-critical-path-e2e-behind-required-checks">PR stage: critical-path E2E behind required checks</h2>
        <p>
          The pull request is where behavior verification belongs. Run end-to-end tests over the critical
          paths your change could plausibly affect — signup, login, checkout, the top three workflows by
          traffic — plus targeted regression tests derived from the diff.
        </p>
        <p>
          Two mechanisms make this stage real rather than decorative. First, the checks must be
          required, so a red mark actually blocks merge — we walk through that configuration in
          <a href="https://molar.it/blog/required-status-checks-merge-gate">required status checks as a merge gate</a>.
          Second, flaky failures must be quarantined within hours, not weeks, because a gate that fails
          randomly gets admin-overridden into oblivion. The Playwright plumbing itself is covered step by
          step in <a href="https://molar.it/blog/github-actions-playwright-ci">Playwright in GitHub Actions</a>.
        </p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code># .github/workflows/pr-e2e.yml (excerpt)
jobs:
  critical-path:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npx playwright install --with-deps
      - run: npx playwright test e2e/critical --shard=${{ matrix.shard }}/4
        env:
          STRIPE_BASE_URL: http://localhost:12111   # clone, not sandbox
      - uses: actions/upload-artifact@v4
        if: failure()
        with: { name: traces, path: e2e/test-results }</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <div>
          Track one number per week: median PR check time. If it creeps past your stated budget, move tests —
          don’t add parallel runners and call it fixed. Hardware hides policy debt for about a quarter.
        </div>
      </section>

      <section>
        <h2 id="deploy-stage-smoke-then-promote">Deploy stage: smoke, then promote</h2>
        <p>
          After deploy to an environment, run a smoke suite: one pass through each critical flow against the
          deployed artifact, using cloned third parties so it neither spends money nor sends email. Smoke
          tests assert “the release can serve users,” not “every feature works.” Five minutes, hard limit;
          anything deeper belongs to scheduled production testing.
        </p>
        <p>
          Promote only on green. The value here isn’t catching logic bugs — those were caught at PR time —
          it’s catching deployment bugs: missing env vars, wrong migrations, CDN misconfig, the
          staging-parity gaps described in
          <a href="https://molar.it/blog/staging-environment-alternatives">why staging lies to you</a>.
        </p>
      </section>

      <section>
        <h2 id="production-stage-schedules-not-releases">Production stage: schedules, not releases</h2>
        <p>
          The last stage runs continuously against production with read-only or marked identities: synthetic
          logins, search, checkout up to (but not through) payment, API contract probes. This is the safety
          net for everything upstream can’t see — real data shapes, real CDN behavior, real vendor drift.
          We cover the design in <a href="https://molar.it/blog/synthetic-monitoring-production">synthetic monitoring for
          production</a>; the broader cadence map is in our guide to
          <a href="https://molar.it/blog/regression-testing-strategies">regression testing strategies</a>.
        </p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code># Every 15 minutes, from three regions
- name: login → dashboard loads < 2s
- name: search returns seeded result
- name: api/v1/orders schema matches OpenAPI
# Alert routing: page only on 2 consecutive failures, same region</code></pre><p class="copy-status sr-only" role="status"></p></div>
      </section>

      <section>
        <h2 id="attach-artifacts-so-failures-can-be-diagnosed">Attach artifacts so failures can be diagnosed</h2>
        <p>Attach the available trace, screenshots, requests, and service results to a failed check so the author can start investigating. Capture the details your test needs and make sure teammates can open them. <a href="https://molar.it/products/guard">Molar Guard</a> and <a href="https://molar.it/products/trace">Trace</a> support this workflow where the beta integration is configured; recording and publication depend on that setup.</p>
      </section>

      <section>
        <h2 id="how-pipelines-lose-trust">How pipelines lose trust</h2>
        <ul>
          <li><strong>Budget creep.</strong> Each new feature adds “just one more” PR test until median check time doubles and engineers start merging on red. Re-audit against the budget quarterly and move tests right.</li>
          <li><strong>Flake debt.</strong> Retries hide flaky tests instead of fixing them; the retry count becomes your real reliability metric. Track first-run pass rate per suite and quarantine anything below it.</li>
          <li><strong>Orphaned stages.</strong> Deploy smoke keeps running for an app whose flows changed months ago, asserting a UI that no longer exists. Every scheduled stage needs an owner who edits its scenarios as the product changes.</li>
        </ul>
        <p>
          None of these are tooling problems; all three are policy problems with tooling symptoms. Write
          the policy down — budgets, quarantine rules, stage owners — and revisit it whenever someone says
          “CI is slow again.”
        </p>]]></content:encoded>
    </item>
    <item>
      <title>End-to-end testing: practical habits for reliable checks</title>
      <link>https://molar.it/blog/e2e-testing-best-practices</link>
      <guid isPermaLink="true">https://molar.it/blog/e2e-testing-best-practices</guid>
      <description>Choose important app tasks, stable locators, isolated test data, and useful failure recordings.</description>
      <pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate>
      <category>Engineering</category>
      <content:encoded><![CDATA[<section>
        <h2 id="prioritize-the-critical-path-ruthlessly">Prioritize the critical path ruthlessly</h2>
        <p>
          E2E tests are the most expensive tests you own — slow to run, slower to debug, sensitive to every
          layer of the stack. Spending that budget on a settings toggle while signup-to-checkout goes untested
          is malpractice. Start by mapping your revenue and trust funnel: signup, login, search, cart, checkout,
          refund, password reset. Those flows get coverage first, deepest, and best maintenance.
        </p>
        <p>
          A concrete allocation: identify the flows that generate or
          protect revenue, write one clean test per flow plus variants only where risk justifies it, and hold
          that set to a strict runtime budget appropriate for your pull request gate. Everything beyond the
          critical path belongs in lower-cost layers — component tests, API tests — or in scheduled runs rather
          than PR gating. If you’re building this foundation deliberately, our guide to
          <a href="https://molar.it/blog/regression-testing-strategies">regression testing strategies</a> shows how to layer
          suites by cost without leaving gaps.
        </p>
      </section><figure class="article-graphic article-graphic--data" data-article-graphic data-article-family="data" aria-label="Fresh state keeps parallel runs honest"><div class="article-graphic__canvas is-data"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M154 126H301"/><path class="" d="M459 126H606"/><path class="is-flow" d="M154 126H301"/><path class="is-flow" d="M459 126H606"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">SEED</div><h3>Unique test data</h3><p>Use repeatable records, clocks, and test identities with clear prefixes.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">created for this run</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">WORKER</div><h3>Isolated browser + services</h3><p>One run can change its records without racing another run.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">isolated worker</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">RESET</div><h3>Restore or discard</h3><p>Start the next run from a known baseline instead of deleting shared rows.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">repeat from clean state</span></span></div></div></div><figcaption><strong>Fresh state keeps parallel runs honest</strong><span>Create or restore the state each worker needs; cleanup instructions alone do not make tests independent.</span></figcaption></figure>

      <section>
        <h2 id="choose-locators-that-describe-the-visible-control">Choose locators that describe the visible control</h2>
        <p>
          The single highest-leverage habit in browser automation is selecting elements the way a user perceives
          them: by role and accessible name, not by DOM coordinates. A locator like
          <code>getByRole('button', { name: 'Pay now' })</code> survives redesigns, refactors, and CSS
          rewrites; <code>#checkout-form > div:nth-child(3) > button</code> survives nothing.
        </p>
        <p>Example to adapt: helper functions, accounts, and endpoints depend on your test project. Adapt this example to your project.</p><div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>test('signed-in customer can complete checkout', async ({ page }) => {
  const user = await makeUser();            // factory: unique, disposable
  await login(page, user);
  await page.getByRole('link', { name: 'Cart' }).click();
  await page.getByRole('button', { name: 'Checkout' }).click();
  await expect(page.getByText('Payment confirmed')).toBeVisible();
});</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>
          Role-based selection has a second payoff people underrate: it continuously audits accessibility.
          A test that can’t find a button by accessible name often means screen readers can’t either. When UIs
          change legitimately, ranked locators also give self-healing systems something principled to work with —
          see <a href="https://molar.it/blog/self-healing-test-automation">how that re-ranking works</a>. And when a failure is
          about how something looks rather than behaves, route it to dedicated visual checks as described in our
          <a href="https://molar.it/blog/visual-regression-testing-guide">visual regression guide</a>, instead of bolting pixel
          assertions onto functional flows.
        </p>
      </section>

      <section>
        <h2 id="every-test-stands-alone">Every test stands alone</h2>
        <p>
          Order dependence is the quiet killer. The suite passes when run alphabetically on Monday and fails
          when sharded across four runners on Tuesday, because test 47 assumed test 12 created its account.
          Each fix buys down future flakiness: every test creates its own data through factories; no test reads
          state another test wrote; cleanup removes what setup created. As a forcing function, shuffle execution
          order locally — if the suite breaks under shuffling, it was already broken, just quietly.
        </p>
        <p>
          Independence isn’t free: per-test setup costs seconds, and shared fixtures are tempting. Take them,
          but scope them correctly — a worker-scoped database container is fine; a worker-scoped user
          silently reintroduces coupling. The reward compounds elsewhere: independent tests are safe to retry
          individually, safe to shard arbitrarily, and safe to skip selectively when debugging. They’re also the
          prerequisite for the parallelization math in the next section.
        </p>
      </section>

      <section>
        <h2 id="deterministic-data-beats-cleaned-up-data">Deterministic data beats cleaned-up data</h2>
        <p>
          Most “flaky” data problems are really non-determinism: tests that depend on whatever rows previous
          runs left behind, timestamps computed at runtime, counters asserted against unbounded tables. The fix
          is to make every input explicit. Factories mint fresh records with unique suffixes per test; fixed
          seeds replace random generators; clocks freeze where time matters. Assert against data you created —
          never against global counts like “the orders table has N rows”, which any parallel test can invalidate.
        </p>
        <p>
          Side-effecting third parties deserve special treatment, because their state is both nondeterministic
          and expensive: real cards decline randomly on test numbers, real email arrives late, real SMS gets rate-limited.
          Stateful clones that speak the vendor’s API deterministically solve all three — the full pattern is in
          <a href="https://molar.it/blog/test-stripe-without-real-cards">Stripe testing without real cards</a>, and the broader
          strategy (factories, seeds, cleanup policies, clone lifecycles) is covered in
          <a href="https://molar.it/blog/test-data-management-best-practices">test data management best practices</a>.
        </p>
      </section>

      <section>
        <h2 id="parallelize-deliberately">Parallelize deliberately</h2>
        <p>
          Serial suites die by wall-clock arithmetic. A 40-minute serial suite is not a quality gate, it’s a
          context-switch generator. Sharding divides the work across machines, and the math is simple:
          shards = ceil(total_runtime / target_runtime), so 40 minutes across 4 runners lands near 10 minutes —
          assuming balanced shards. Balance matters more than count: group by measured duration, not file size,
          or one runner inherits the two slowest specs while three finish early.
        </p>
        <p>
          Two caveats keep parallelism honest. First, contention is real — parallel workers hammering one
          database turn logical races into physical ones, which is why the isolation practices above come first;
          give each worker its own schema or use row-level uniqueness aggressively. Second, diminishing returns
          kick in around setup costs and license-constrained resources; going from 4 to 16 shards helps far less
          than going from 0 to 4. The complete CI configuration — caching, matrix sharding, merged reports — is
          walked through in our <a href="https://molar.it/blog/github-actions-playwright-ci">GitHub Actions guide</a>.
        </p>
      </section>

      <section>
        <h2 id="keep-the-details-needed-to-investigate-a-failure">Keep the details needed to investigate a failure</h2>
        <div>
          An E2E failure without artifacts costs a round of archaeology: reproduce, guess, instrument,
          repeat. A trace shortens the investigation — open the timeline, read the network waterfall,
          watch the DOM at the failing step. Configure artifact capture before you need it, because you will
          need it.
        </div>
        <p>
          The minimum viable set: trace (DOM snapshots, network, console, action log), screenshots at failure,
          video on retry, and server logs correlated by request ID. Retain them long enough to debug — a week is
          plenty for PR artifacts — and link them directly in the failure report so nobody hunts through CI logs.
          Deterministic replay goes one step further than capture: being able to re-run the exact scenario against
          recorded third-party responses converts “couldn’t reproduce” into a routine morning task.
        </p>
        <p>Start with a critical task, use stable locators, isolate its data, and retain the details needed to investigate failure. Apply the same review to generated tests. Molar’s generated Playwright export and clean replay remain under validation in beta; inspect the code and prove repeatability before adding it to a release gate.</p>]]></content:encoded>
    </item>
    <item>
      <title>Test email verification, invitations, and notifications</title>
      <link>https://molar.it/blog/email-workflow-testing-otp</link>
      <guid isPermaLink="true">https://molar.it/blog/email-workflow-testing-otp</guid>
      <description>Check captured messages, recipients, links, codes, and expiry using a configured test inbox.</description>
      <pubDate>Sun, 19 Jul 2026 00:00:00 GMT</pubDate>
      <category>Integrations</category>
      <content:encoded><![CDATA[<section>
        <h2 id="test-the-inbox-steps-users-depend-on">Test the inbox steps users depend on</h2>
        <p>
          Consider a normal B2B signup: user submits the form → receives a verification code → confirms → gets a
          team invite → clicks a link → lands in the app. Three of five steps happen outside your UI, over email.
          A test suite that mocks the mailer with “assume sent” verifies nothing: broken templates, wrong merge
          fields, dead links, expired tokens, and missing emails all ship to production looking green.
        </p>
        <p>
          Teams avoid asserting on email because the traditional options are bad. Real SMTP through a provider is
          slow (seconds to minutes per message), costs money at CI volume, risks sending actual mail to actual
          people, and gives you no clean programmatic inbox to assert against. Shared Gmail test inboxes accumulate
          state across runs and trip spam filters. What you need instead is a mail system built for machines —
          which is exactly what an <a href="https://molar.it/blog/testing-third-party-apis-safely">email clone</a> provides:
          SMTP/API compatible, instantly delivered, fully inspectable.
        </p>
      </section><figure class="article-graphic article-graphic--otp" data-article-graphic data-article-family="otp" aria-label="An OTP is a lifecycle, not one input"><div class="article-graphic__canvas is-otp"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M95 126H245"/><path class="" d="M265 126H415"/><path class="" d="M435 126H585"/><path class="is-flow" d="M95 126H245"/><path class="is-flow" d="M265 126H415"/><path class="is-flow" d="M435 126H585"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">01 · ISSUE</div><h3>Verification email sent</h3><p>Record the message and its issued-at time.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">captured</span></span></div><div class="article-graphic__card "><div class="article-graphic__eyebrow">02 · VALID CASE</div><h3>Code accepted</h3><p>Independent attempt: submit the fresh code before its validity window closes.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">account verified</span></span></div><div class="article-graphic__card "><div class="article-graphic__eyebrow">03 · EXPIRED CASE</div><h3>Old code rejected</h3><p>Independent attempt: submit the original code after its validity window.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">expiry asserted</span></span></div><div class="article-graphic__card "><div class="article-graphic__eyebrow">04 · RESEND CASE</div><h3>Fresh code only</h3><p>Independent attempt: request again and keep the original code invalid.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">retry asserted</span></span></div></div></div><figcaption><strong>An OTP is a lifecycle, not one input</strong><span>After one issuance, exercise valid, expired, and resend cases independently from a clean start.</span></figcaption></figure>

      <section>
        <h2 id="assert-delivery-links-codes-and-expiry">Assert delivery, links, codes, and expiry</h2>
        <p>
          For every transactional email your app sends, three properties are worth verifying in E2E tests:
        </p>
        <ul>
          <li><strong>Arrival.</strong> The right email reaches the right address within your SLA window — one assertion catches the entire broken-template / crashed-worker class of bugs.</li>
          <li><strong>Links.</strong> Every actionable URL resolves: extract it from the message, request or navigate it, assert the destination state (“reset confirmed”, not a 404). This catches token-expiry misconfigurations and base-URL drift between environments.</li>
          <li><strong>Codes.</strong> For OTP flows, extract the code and drive the UI with it — proving the full loop works, not just that something was rendered.</li>
        </ul>
        <p>Example to adapt: helper functions, accounts, and endpoints depend on your test project. Adapt this example to your project.</p><div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>test('password reset completes via emailed link', async ({ page }) => {
  await requestReset(page, 'ci-run-a7f3+user@mail.clone');
  const mail = await clone.waitForMessage({
    to: 'ci-run-a7f3+user@mail.clone',
    subjectContains: 'Reset your password',
    timeoutMs: 5_000,
  });
  const link = mail.links.find(l => l.href.includes('/reset/'));
  await page.goto(link.href);
  await page.fill('#new-password', 'correct-horse-battery');
  await expect(page.getByText('Password updated')).toBeVisible();
});</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>
          Notice what’s not asserted here: pixel-perfect rendering of the template. That belongs in a
          separate, slower template-snapshot job, not in the critical path of each run.
        </p>
        <p>
          Apply the same three-part lens to the other transactional types. Invites: assert the email reaches the
          invitee, the accept link lands them in the right workspace with the right role, and a stale or already-
          accepted link shows a sensible state instead of an error. Notifications (receipts, shipping, alerts):
          assert arrival and that dynamic fields rendered with real values — “Your order #1042 shipped” not “Your
          order #<code>undefined</code> shipped.” Merge-field bugs are embarrassing precisely because they only appear in the
          email body, which is exactly where untested suites never look.
        </p>
      </section>

      <section>
        <h2 id="unique-per-run-addresses">Unique-per-run addresses</h2>
        <p>
          The single biggest source of email-test flakiness is address reuse. If two runs read from
          <code>test@example.com</code>, run B sees run A’s messages, filters get confused, and parallel workers
          corrupt each other’s assertions. Give each run its own namespace instead — plus-addressing or a
          wildcard catch-all domain both work:
        </p>
        <ul>
          <li><code>run-{CI_JOB_ID}+signup@clone.test</code> — sortable, greppable, self-documenting.</li>
          <li>Anything <code>@clone.test</code> is accepted by the mail clone, so you never provision addresses.</li>
        </ul>
        <p>
          Unique addresses make assertions trivially race-free: “wait for exactly one message to this exact
          address” has no cross-talk by construction. They also make cleanup honest — purge by prefix after the
          run, and nothing leaks between jobs. This is standard
          <a href="https://molar.it/blog/test-data-management-best-practices">test data hygiene</a> applied to the mailbox.
        </p>
        <div>
          Never point automated tests at domains you don’t control. A typo’d recipient on a live provider is how
          test traffic becomes accidental email to strangers — with your product’s name on it. Clones keep every
          message inside the run sandbox; nothing leaves the machine.
        </div>
      </section>

      <section>
        <h2 id="code-extraction-helpers">Code extraction helpers</h2>
        <p>
          OTP emails vary in format — “Your code is 123456”, spaced digits, embedded buttons. Centralize parsing
          in one helper so template changes break one place, not forty tests:
        </p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>// helpers/mail.ts
export function extractOtp(mail) {
  const m = mail.text.match(/\b(\d[\s-]?){5}(\d)\b/g)?.[0];
  if (!m) throw new Error(`No OTP found in: ${mail.subject}`);
  return m.replace(/\D/g, '');
}

export async function latestCodeTo(clone, addr) {
  const mail = await clone.waitForMessage({ to: addr, timeoutMs: 5_000 });
  return extractOtp(mail);
}</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>
          Beyond codes, expose two more primitives from the same helper: <code>linksFor(addr)</code> returning
          parsed anchor hrefs (so link assertions stay one line), and <code>assertNoMessage(addr, subject)</code>
          for the negative cases teams forget — password reset on an account that doesn’t exist must not send
          anything, or you’ve built a user-enumeration oracle. Negative email assertions are impossible with real
          inboxes (absence is indistinguishable from slow delivery) and trivial with a deterministic clone.
        </p>
        <p>
          Two details worth stealing: fail loudly with the raw body in the error (debugging “why didn’t my regex
          match” without the payload is misery), and prefer waiting on subject + recipient rather than sleep —
          polling a deterministic clone returns in milliseconds anyway.
        </p>
      </section>

      <section>
        <h2 id="use-a-test-inbox-instead-of-smtp">Use a test inbox instead of SMTP</h2>
        <p>
          The clone approach removes the entire risk surface of real-mail testing: no sends to third parties, no
          provider rate limits in CI, no deliverability lottery, no cost scaling with run count. Delivery is local
          and synchronous-fast, which means an OTP step adds milliseconds, not minutes — keeping suites under the
          latency budget where developers actually run them. It composes with the rest of your stack too: pair it
          with the SMS side described in <a href="https://molar.it/blog/test-twilio-sms-otp-flows">Twilio OTP testing</a>, and run
          the whole thing on a schedule against staging as part of your
          <a href="https://molar.it/blog/regression-testing-strategies">regression strategy</a>.
        </p>
        <p>The <a href="https://docs.molar.it/docs/clones/vendors/sendgrid">Molar email clone reference</a> describes supported message capture and inspection. For an invitation test, check the recipient and link, then assert that accepting it grants the intended access. The <a href="https://molar.it/examples/team-invitation">recorded invitation example</a> shows one configured flow.</p>]]></content:encoded>
    </item>
    <item>
      <title>Flaky tests: causes and fixes</title>
      <link>https://molar.it/blog/flaky-tests-causes-and-fixes</link>
      <guid isPermaLink="true">https://molar.it/blog/flaky-tests-causes-and-fixes</guid>
      <description>Investigate timing races, shared data, changing environments, and unstable selectors before adding retries.</description>
      <pubDate>Sat, 08 Aug 2026 00:00:00 GMT</pubDate>
      <category>Engineering</category>
      <content:encoded><![CDATA[<section>
        <h2 id="treat-intermittent-failures-as-bugs-to-investigate">Treat intermittent failures as bugs to investigate</h2>
        <p>
          When the same commit passes on one run and fails on the next, teams learn the worst possible habit:
          re-run until green. Once that habit forms, the suite stops being a gate and becomes weather — people
          check it, nobody trusts it. The economic damage is quiet but compounding: engineers buffer their
          estimates for “CI roulette”, real regressions hide among false ones, and eventually someone proposes
          deleting the suite entirely.
        </p>
        <p>
          The useful reframe is that flakiness is not random. In our experience, most intermittent failures
          fall into one of five groups: <strong>timing races</strong>, <strong>shared state</strong>,
          <strong>environment drift</strong>, <strong>third-party failure</strong>, and
          <strong>selector brittleness</strong>. Each group has a durable fix. Find the group
          first; reaching for <code>retry</code> before diagnosis just moves the problem from your failure
          report into your blind spot.
        </p>
      </section><figure class="article-graphic article-graphic--failure" data-article-graphic data-article-family="failure" aria-label="Move from a red check to a cause"><div class="article-graphic__canvas is-failure"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M96 126H205"/><path class="" d="M225 126H334"/><path class="" d="M354 126H463"/><path class="" d="M483 126H592"/><path class="is-flow" d="M96 126H205"/><path class="is-flow" d="M225 126H334"/><path class="is-flow" d="M354 126H463"/><path class="is-flow" d="M483 126H592"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">FAILED ASSERTION</div><h3>Expected state missing</h3><p>Keep the original assertion visible; do not replace it with “page loaded.”</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">start here</span></span></div><div class="article-graphic__card "><div class="article-graphic__eyebrow">REPRODUCE</div><h3>Same account, same input</h3><p>Repeat from a clean state and separate product failure from setup failure.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">control variables</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">INSPECT</div><h3>Browser · request · console</h3><p>Align the browser, request, and console records around the first divergence.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">find the first break</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">NEXT STEP</div><h3>Run one new experiment</h3><p>Change one variable, then decide whether the fix belongs in code or the test.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">reviewed diagnosis</span></span></div></div></div><figcaption><strong>Move from a red check to a cause</strong><span>A useful failed run preserves the failing assertion, the relevant request, and the next experiment.</span></figcaption></figure>

      <section>
        <h2 id="timing-races-sleeps-are-guesses">Timing races: sleeps are guesses</h2>
        <p>
          The classic flake: the test works on your fast laptop, fails in CI where eight containers share four
          cores. The app takes 400ms to render instead of 200ms, and a fixed sleep that was tuned to “usually
          work” now loses the race. Any test that waits a duration is really waiting for luck.
        </p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>// Fragile: sleeps guess at timing
await page.waitForTimeout(3000);
await page.click('#submit');

// Durable: act, then wait on observable state
await page.getByRole('button', { name: 'Pay now' }).click();
await expect(page.getByText('Payment confirmed')).toBeVisible();</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>
          Web-first assertions flip the model: instead of sleeping then checking once, they poll observable
          state until it’s true or a timeout expires. Two companion rules close the gap entirely. First, treat
          unhandled promise rejections and unexpected network errors as test failures rather than console noise —
          they are tomorrow’s flakes announcing themselves today. Second, freeze sources of variance you don’t
          need: animations off, fake clocks for anything time-based, deterministic IDs for anything paginated.
        </p>
      </section>

      <section>
        <h2 id="shared-state-two-tests-one-row">Shared state: two tests, one row</h2>
        <p>
          The second bucket shows up when tests collide through something they both touch: the same seeded user,
          the same database table, the same localStorage profile. Symptoms include assertions on counts (“cart
          should have 3 items” seeing 7), duplicate-key errors that appear only under parallelism, and tests
          that pass alone but fail in the full run. Parallel execution doesn’t create these bugs — it reveals
          them.
        </p>
        <p>
          Durable fixes, in order of preference: give every test its own data (factories that mint unique users
          and orders per invocation, then clean up); scope expensive shared fixtures per worker rather than
          globally; and where isolation must be absolute, roll back via transactions or run each worker against
          its own schema or container. If a test needs a specific global to exist, that dependency belongs in
          its setup — explicit, owned, and visible — never in the residue of whichever test ran last.
        </p>
      </section>

      <section>
        <h2 id="environment-drift-it-passed-on-my-machine">Environment drift: it passed on my machine</h2>
        <p>
          Some flakes aren’t in your code at all. The staging server was mid-deploy. A feature flag flipped.
          CI runs in a region where the page renders in another locale, or under a timezone that makes your
          “end of month” invoice test compute a different month. These failures look mysterious precisely
          because the cause lives outside the test.
        </p>
        <p>
          Countermeasures are organizational more than technical. Pin everything your tests implicitly depend
          on: timezone, locale, feature-flag state, seed data version. Prefer short-lived preview environments
          created per pull request over a long-lived shared staging box that accumulates entropy — see our guide
          to <a href="https://molar.it/blog/staging-environment-alternatives">staging environment alternatives</a>. And add a
          pre-flight health check so a half-deployed app produces “environment unhealthy”, not forty misleading
          test failures.
        </p>
      </section>

      <section>
        <h2 id="third-party-failures-someone-else-s-uptime">Third-party failures: someone else’s uptime</h2>
        <p>
          Checkout tests that hit a live payment sandbox inherit its outage schedule. Email tests wait on real
          SMTP relay latency. Rate limits turn a burst of parallel tests into intermittent 429s. None of this is
          your product breaking, yet your build goes red and your team burns an afternoon proving it.
        </p>
        <p>
          The durable fix is to stop sharing fate with third parties during routine runs. Stateful substitutes
          that speak the vendor’s API — returning deterministic IDs, replaying webhooks, delivering email to an
          inspectable inbox — remove the network, the latency, and the outages while exercising your real code
          paths. We compare the options in <a href="https://molar.it/blog/api-mocking-vs-service-clones">API mocking versus service clones</a>,
          and walk a payments example in <a href="https://molar.it/blog/test-stripe-without-real-cards">testing Stripe without real cards</a>.
          Keep exactly one scheduled smoke against the real sandbox as a drift alarm, and you get hermetic daily
          runs without blinding yourself to contract changes.
        </p>
      </section>

      <section>
        <h2 id="replace-selectors-tied-to-page-structure">Replace selectors tied to page structure</h2>
        <p>
          The fifth bucket degrades gradually rather than intermittently, but it manufactures flakes all the
          same: a selector like <code>div:nth-child(3) > span.btn-primary</code> survives until a designer
          wraps one node in a new div, and suddenly the locator matches nothing — or worse, matches a different
          button. Ambiguous text selectors produce the intermittent flavor: they pass while unique, then silently
          bind to a newly added element.
        </p>
        <p>
          The fix ladder is well established: prefer role plus accessible name (<code>getByRole('button', { name: 'Checkout' })</code>),
          then label associations, then purpose-built <code>data-testid</code>, then unique text, and treat
          structural CSS as a code smell. When the UI legitimately changes, a disciplined
          <a href="https://molar.it/blog/self-healing-test-automation">self-healing layer</a> can re-rank locators and propose
          the update for review instead of failing the build — provided intent was captured at authoring time.
        </p>
      </section>

      <section>
        <h2 id="set-a-retry-limit-and-assign-an-owner">Set a retry limit and assign an owner</h2>
        <p>
          Retries deserve nuance. Used deliberately — one retry, both outcomes reported — they absorb genuine
          environmental noise and generate a flake-rate metric you can manage down. Used silently, they convert
          your suite into a lottery where the prize is ignorance. The difference is whether a retry that flips
          red to green triggers investigation or celebration.
        </p>
        <div>
          A retry that passes is a bug report, not a pass. If your green build contains three retried tests,
          you shipped with three undiagnosed defects wearing a green badge.
        </div>
        <p>Set a small retry limit and report when a run needs it. Give repeated flakes an owner and a repair deadline; keep them visible if they temporarily stop blocking merges. A retry should not erase the first failure. See the <a href="https://molar.it/integrations/github-actions">GitHub Actions guide</a> for running a configured Molar scenario in CI.</p>]]></content:encoded>
    </item>
    <item>
      <title>Run Playwright tests in GitHub Actions</title>
      <link>https://molar.it/blog/github-actions-playwright-ci</link>
      <guid isPermaLink="true">https://molar.it/blog/github-actions-playwright-ci</guid>
      <description>Configure test jobs, sharding, failure recordings, and required checks in GitHub Actions.</description>
      <pubDate>Thu, 30 Jul 2026 00:00:00 GMT</pubDate>
      <category>Engineering</category>
      <content:encoded><![CDATA[<section>
        <h2 id="the-baseline-workflow">The baseline workflow</h2>
        <p>
          Start with one workflow that runs on pull requests and on pushes to main. The version below already
          includes the three decisions most teams retrofit later: matrix sharding, blob-report merging, and
          artifact upload scoped to failures. We’ll unpack each in the following sections.
        </p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>name: e2e
on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - run: npm ci

      - name: Install Playwright browsers
        run: npx playwright install --with-deps chromium

      - name: Run tests (shard)
        run: npx playwright test --shard=${{ matrix.shard }}/4 --reporter=blob

      - name: Upload report
        uses: actions/upload-artifact@v4
        if: ${{ always() }}
        with:
          name: blob-report-${{ matrix.shard }}
          path: blob-report/
          retention-days: 7

  merge-reports:
    if: ${{ always() }}
    needs: [test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci
      - uses: actions/download-artifact@v4
        with:
          path: all-reports
          pattern: blob-report-*
          merge-multiple: true
      - run: npx playwright merge-reports --reporter=html ./all-reports
      - uses: actions/upload-artifact@v4
        if: ${{ always() }}
        with:
          name: html-report
          path: playwright-report/
          retention-days: 7</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>
          Note <code>fail-fast: false</code>: with it unset, one flaky shard cancels the others mid-run and you
          lose half your evidence. The tradeoff is paying for full execution even after an early failure — worth
          it, because parallel evidence from the other shards often contains the trace that explains the first one.
        </p>
      </section><figure class="article-graphic article-graphic--release" data-article-graphic data-article-family="release" aria-label="A release gate narrows the decision"><div class="article-graphic__canvas is-release"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M154 126H301"/><path class="" d="M459 126H606"/><path class="" d="M380 126V67H606"/><path class="is-flow" d="M154 126H301"/><path class="is-flow" d="M459 126H606"/><path class="is-flow" d="M380 126V67H606"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">CHANGE</div><h3>GitHub Actions run</h3><p>Start with the paths affected by the change.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">scope the run</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">CHECKS</div><h3>Signup · checkout · access</h3><p>Each scenario reports its own assertion and captured route.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">one blocked</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">DECISION</div><h3>Ready or blocked</h3><p>A reviewer sees why the release can proceed or what needs attention.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">reviewable result</span></span></div></div></div><figcaption><strong>A release gate narrows the decision</strong><span>Run the checks that match the change, attach the failed route, and leave the merge decision reviewable.</span></figcaption></figure>

      <section>
        <h2 id="cache-what-s-slow">Cache what’s slow</h2>
        <p>
          Two things dominate cold-start time: dependency installation and browser binaries. The
          <code>cache: npm</code> input above handles dependencies by hashing the lockfile. Browsers need explicit
          handling — browser binaries and system dependencies can make cold starts expensive:
        </p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>- uses: actions/cache@v4
  with:
    path: ~/.cache/ms-playwright
    key: pw-${{ runner.os }}-${{ hashFiles('package-lock.json') }}</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>
          Keying on the lockfile works because Playwright versions are pinned there; bump the version, the key
          changes, the cache rebuilds once, and every subsequent run restores instantly. Keep
          <code>npx playwright install --with-deps chromium</code> in the workflow regardless — cache hits make
          it near-instant, misses make it correct. Only install browsers you actually use; installing all three
          engines triples download time for zero benefit.
        </p>
      </section>

      <section>
        <h2 id="split-independent-tests-across-ci-jobs">Split independent tests across CI jobs</h2>
        <p>
          Sharding splits the suite across matrix jobs. Pick the target wall-clock time first, then derive shard
          count: a serial suite split across four shards can still be limited by balance and setup overhead.
          Playwright balances shards by test count; if durations vary wildly, sort specs into size buckets or
          split known-slow files so no single shard dominates. Watch for two scaling ceilings: shared backend
          contention (parallel shards hammering one database turn into a different kind of red), and per-shard
          fixed costs for checkout, install, and browser boot can dominate once shards are small.
        </p>
        <p>
          If sharding exposes order dependence — tests failing only under some shard arrangements — fix the
          dependence, not the arrangement. That class of bug is covered in the isolation section of our
          <a href="https://molar.it/blog/e2e-testing-best-practices">E2E practices guide</a>.
        </p>
      </section>

      <section>
        <h2 id="traces-and-artifacts-on-failure">Traces and artifacts on failure</h2>
        <p>
          Configure capture in <code>playwright.config.ts</code> so every CI failure arrives debuggable:
        </p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>export default defineConfig({
  retries: process.env.CI ? 1 : 0,
  use: {
    trace: 'retain-on-failure',
    video: 'retain-on-failure',
    screenshot: 'only-on-failure',
  },
});</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>A retry can reveal an intermittent failure, but a passing retry still needs to be reported. Open a captured trace with <code>npx playwright show-trace trace.zip</code>, or retain it as a CI artifact. Set retention to match your debugging needs. The <a href="https://molar.it/vs/playwright-trace">Molar Trace comparison</a> explains how to inspect the captured data available from each type of run.</p>
      </section>

      <section>
        <h2 id="make-the-check-required-carefully">Make the check required — carefully</h2>
        <p>
          A green-required gate starts with naming. Branch protection references check names exactly, and a
          matrix job produces one check per shard named like <code>e2e / test (1)</code>, <code>(2)</code>,
          and so on. Require either each shard explicitly or add a final aggregate job whose success means “all
          shards passed” — the latter survives future shard-count changes without editing protection rules.
          The exact configuration steps are in our
          <a href="https://molar.it/blog/required-status-checks-merge-gate">required status checks guide</a>, and the
          <a href="https://molar.it/integrations/github-actions">GitHub Actions integration page</a> documents the configured Guard path for
          reporting verdicts as native check runs.
        </p>
        <p>
          As more code is generated, machine-verified gates
          carry proportionally more of the safety burden — a gate that can be trained-around is a liability at
          exactly the moment usage patterns are drifting that way.
        </p>
        <div>
          Concurrency saves real money on busy repos. Add <code>concurrency: { group: e2e-${{ github.ref }},
          cancel-in-progress: true }</code> so new pushes cancel superseded runs instead of queueing behind them.
        </div>
      </section>

      <section>
        <h2 id="keep-the-gate-honest-as-the-suite-grows">Keep the gate honest as the suite grows</h2>
        <p>
          CI quality decays through retries, not failures. Cap retries at one in CI, review which tests consume
          them weekly, and quarantine repeat offenders out of the blocking set with an owner and expiry — the
          full policy lives in our <a href="https://molar.it/blog/flaky-tests-causes-and-fixes">flaky-tests guide</a>. Run the
          complete suite nightly rather than forcing every edge case onto PRs; a fast required smoke on every PR
          plus a thorough scheduled run catches more regressions per minute of engineer attention than a
          monolithic blocking suite ever will. This tiered shape is the core of
          <a href="https://molar.it/blog/continuous-testing-ci-cd-pipeline">continuous testing in the CI/CD pipeline</a>.
        </p>
        <p>
          The end state to aim for: every pull request gets sub-10-minute E2E feedback it can trust, every
          failure arrives with enough evidence to diagnose without a local reproduction, and the merge gate
          stays green for exactly one reason — the app actually works.
        </p><p>Reference: <a href="https://playwright.dev/docs/test-sharding">Playwright’s CI sharding and report-merging guide</a>.</p>]]></content:encoded>
    </item>
    <item>
      <title>Playwright vs Cypress: choosing for your app</title>
      <link>https://molar.it/blog/playwright-vs-cypress-comparison</link>
      <guid isPermaLink="true">https://molar.it/blog/playwright-vs-cypress-comparison</guid>
      <description>Evaluate browser navigation, CI execution, failure inspection, and test maintenance on a representative task.</description>
      <pubDate>Mon, 22 Jun 2026 00:00:00 GMT</pubDate>
      <category>Engineering</category>
      <content:encoded><![CDATA[<section><h2 id="compare-the-test-your-team-has-to-maintain">Compare the test your team has to maintain.</h2><p>Playwright and Cypress both drive browser tests, make assertions against web pages, and fit into CI. The useful decision is not a feature checklist; it is how each handles your navigation model, application state, test data, and failure investigation.</p><p>Choose one representative task, such as signing in, opening a new tab, changing a plan, and checking the account afterward. Include the awkward part of your app: a popup, external identity provider, iframe, file download, or callback. Run the same task from clean data in both tools, then compare the test code, failure report, and work required to rerun it.</p></section><figure class="article-graphic article-graphic--decision" data-article-graphic data-article-family="decision" aria-label="Compare setup against one browser task"><div class="article-graphic__canvas is-decision"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M154 126H301"/><path class="" d="M459 126H606"/><path class="is-flow" d="M154 126H301"/><path class="is-flow" d="M459 126H606"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">TASK</div><h3>Run one concrete task</h3><p>Use the same account, input, and success condition for each option.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">shared test question</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">SETUP</div><h3>Compare the boundary</h3><p>Look at authoring, service state, environments, and maintenance—not just the first green run.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">tradeoffs visible</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">DECISION</div><h3>Keep the result you can repeat</h3><p>Choose the tool that leaves your team with a result it can inspect and run again.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">review with context</span></span></div></div></div><figcaption><strong>Compare setup against one browser task</strong><span>Comparison guides become useful when one test task exposes setup, maintenance, and review tradeoffs.</span></figcaption></figure>
<section><h2 id="model-tabs-and-origin-changes-deliberately">Model tabs and origin changes deliberately.</h2><p>Playwright exposes browser contexts and pages directly. A popup can be awaited at the same time as the click that opens it:</p><div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>const popupPromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'Open account' }).click();
const popup = await popupPromise;
await popup.getByRole('heading', { name: 'Account' }).waitFor();</code></pre><p class="copy-status sr-only" role="status"></p></div><p>Cypress keeps commands in its command queue and uses <code>cy.origin()</code> when a test must run commands on another origin:</p><div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>cy.origin('https://login.example.test', () =&gt; {
  cy.get('input[name=email]').type(Cypress.env('TEST_EMAIL'));
  cy.get('button[type=submit]').click();
});</code></pre><p class="copy-status sr-only" role="status"></p></div><p>Read the current <a href="https://playwright.dev/docs/pages">Playwright pages guide</a> and Cypress’s <a href="https://docs.cypress.io/api/commands/origin">cy.origin documentation</a> against your actual identity flow. Cross-origin support has constraints in both systems; a demo login is not proof that your provider and callback sequence will work unchanged.</p></section>
<section><h2 id="compare-network-control-with-a-real-assertion">Compare network control with a real assertion.</h2><p>Playwright can observe or route requests through its browser context. Cypress uses <code>cy.intercept()</code> to spy on or stub requests made by the application:</p><div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>cy.intercept('POST', '/api/orders').as('createOrder');
cy.get('[data-testid=buy]').click();
cy.wait('@createOrder').its('response.statusCode').should('eq', 201);
cy.contains('Order confirmed').should('be.visible');</code></pre><p class="copy-status sr-only" role="status"></p></div><p>Cypress’s <a href="https://docs.cypress.io/api/commands/intercept">intercept documentation</a> explains that intercepted routes are cleared before each test and that browser-cached requests may not reach the interception layer. In either framework, a stubbed 201 is only a client-side check. Add a service or database assertion when the behavior depends on a downstream record.</p></section>
<section><h2 id="measure-ci-with-isolated-data">Measure CI with isolated data.</h2><p>Playwright supports <a href="https://playwright.dev/docs/test-sharding">test sharding</a> across CI jobs. Cypress documents parallelization through its recorded run workflow. Compare queue time, execution time, artifact retention, and failure diagnosis on the machines and plan limits you will actually use.</p><p>Separate accounts and records before increasing concurrency. Two workers that reuse one user can overwrite each other’s session or consume the same verification code. A shorter suite with contaminated data is harder to trust than a slightly longer suite that starts clean.</p></section>
<section><h2 id="break-one-assertion-and-inspect-the-report">Break one assertion and inspect the report.</h2><p>Change one expected heading or response deliberately. Can you identify the failing action, see the page state, and locate the relevant request? Can a teammate open the same report without reproducing locally?</p><p>Playwright’s <a href="https://playwright.dev/docs/trace-viewer">Trace Viewer</a> can open a saved trace with DOM snapshots, network activity, and console details. Cypress provides local command inspection and, when configured, Cloud recording tools. Check what your own setup captures and retains. A video or screenshot shows what was rendered; it does not prove that the assertion was correct.</p></section>
<section><h2 id="keep-the-framework-that-solves-your-concrete-constraint">Keep the framework that solves your concrete constraint.</h2><p>Use the framework your team can review and maintain. If you evaluate Molar alongside it, test one difficult task rather than rewriting a working suite. Molar’s browser agents and supported service clones are additional paths; generated Playwright export and clean replay remain in beta and require review from clean state.</p><p>Read <a href="https://molar.it/vs/playwright">Molar and Playwright</a>, <a href="https://molar.it/vs/cypress">Molar and Cypress</a>, or <a href="https://molar.it/contact">talk to us</a> before choosing a migration plan.</p></section><section><h2 id="make-the-ci-choice-from-the-failure-you-need-to-explain">Make the CI choice from the failure you need to explain.</h2><p>Run the same representative task against a preview build before deciding that a runner is faster or easier. Give each run a clean account and a fixed base URL. In Playwright, configure projects for the browsers you support and enable sharding only after the unsharded test is reliable; its <a href="https://playwright.dev/docs/test-sharding">sharding guide</a> explains how workers divide a suite. In Cypress, keep the command log and network aliases close to the assertion so a failed command points to the request or page state that needs attention.</p><p>Compare the maintenance work after an intentional failure: expired session, blocked popup, slow response, and a missing permission. The better fit is the one that leaves your team with a clear next action. Molar can run a browser task and return evidence in beta, but it does not remove the need to own test data, provider configuration, or the assertions in your repository.</p></section>]]></content:encoded>
    </item>
    <item>
      <title>Choose which regression tests to run before release</title>
      <link>https://molar.it/blog/regression-testing-strategies</link>
      <guid isPermaLink="true">https://molar.it/blog/regression-testing-strategies</guid>
      <description>Map changes to affected tasks, keep essential checks, and learn from regressions your tests missed.</description>
      <pubDate>Sun, 12 Jul 2026 00:00:00 GMT</pubDate>
      <category>Engineering</category>
      <content:encoded><![CDATA[<section>
        <h2 id="why-full-suite-gating-stops-scaling">Why full-suite gating stops scaling</h2>
        <p>
          Regression suites fail in a predictable way as teams grow. At five engineers, “run everything on every
          push” works — a 10-minute suite is invisible. At fifty engineers with a 90-minute E2E suite, the math
          stops working: ten concurrent PRs contend for runners, feedback latency kills flow state, and engineers
          start batching changes to “make CI worth it,” which multiplies both merge conflicts and risk.
        </p>
        <p>
          The standard responses are both wrong. Cutting the suite (“we’ll rely on code review”) trades known
          costs for unknown ones — production incidents that a 30-second check would have caught. And brute-forcing
          with more machines hits an economic wall: E2E suites grow superlinearly in runtime because flows share
          setup and interact. The exit is not more hardware or fewer tests. It’s running the right tests,
          at the right time, with full knowledge of what changed.
        </p>
      </section><figure class="article-graphic article-graphic--release" data-article-graphic data-article-family="release" aria-label="A release gate narrows the decision"><div class="article-graphic__canvas is-release"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M154 126H301"/><path class="" d="M459 126H606"/><path class="" d="M380 126V67H606"/><path class="is-flow" d="M154 126H301"/><path class="is-flow" d="M459 126H606"/><path class="is-flow" d="M380 126V67H606"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">CHANGE</div><h3>Pull request</h3><p>Start with the paths affected by the change.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">scope the run</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">CHECKS</div><h3>Signup · checkout · access</h3><p>Each scenario reports its own assertion and captured route.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">one blocked</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">DECISION</div><h3>Ready or blocked</h3><p>A reviewer sees why the release can proceed or what needs attention.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">reviewable result</span></span></div></div></div><figcaption><strong>A release gate narrows the decision</strong><span>Run the checks that match the change, attach the failed route, and leave the merge decision reviewable.</span></figcaption></figure>

      <section>
        <h2 id="risk-based-selection-starts-from-the-diff">Risk-based selection starts from the diff</h2>
        <p>
          A pull request is a hypothesis about what it affects. Selection should encode exactly that hypothesis:
          map files to modules to user flows, then run the tests that exercise the touched surface plus everything
          downstream of it.
        </p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code># Illustrative selection policy — adapt to your CI tooling
rules:
  - touch: "src/checkout/**"
    run_flows: [cart, checkout, order-confirmation, refunds]
  - touch: ["src/auth/**"]
    run_flows: [signup, login, password-reset, session-security]
  - touch: "package-lock.json"
    run_flows: all            # dependency bumps get everything
always_run: [smoke]           # never skipped</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>
          The payoff is concrete: a PR that edits invoice rendering runs only the affected flows, while a dependency
          bump still gets the world. Keep the mapping in the repo, reviewed like
          code, because it is code — it encodes your beliefs about coupling, and stale mappings rot into
          silent gaps. This is also where AI-authored suites change the economics: when
          <a href="https://molar.it/blog/ai-playwright-test-generation">test generation is cheap</a>, covering every affected flow
          stops being a headcount problem.
        </p>
      </section>

      <section>
        <h2 id="map-code-changes-to-the-tasks-they-can-affect">Map code changes to the tasks they can affect</h2>
        <p>
          File-path rules are the floor, not the ceiling. A change to a shared component or an API response shape
          touches flows its path never mentions. The stronger version builds an actual dependency graph: which
          routes render this component, which flows traverse those routes, which API endpoints feed them.
        </p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>Example mapping to review:
PricingTable.tsx changes
→ Pricing and upgrade pages use this component
→ Run plan-selection and checkout tests
→ Include the normal smoke checks</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>
          Two failure modes to design against. Over-selection (everything depends on everything) usually means a
          shared utility needs decoupling — treat it as architecture signal, not a tool bug. Under-selection is the
          dangerous one, and you catch it with data: when a nightly run finds a regression your selection missed,
          trace back which edge was absent from the graph and add it. Escape analysis feeds selection accuracy;
          see the measurement section below.
        </p>
      </section>

      <section>
        <h2 id="the-nightly-full-suite-is-insurance-not-gating">The nightly full suite is insurance, not gating</h2>
        <p>
          Even good selection has blind spots: interactions between flows, environment drift, vendor behavior
          changes, cache poisoning that only manifests after hours of activity. The nightly full run exists to
          catch exactly those. Treat it as insurance — nobody waits on it to merge — but take its failures
          seriously: anything red at night that was green on merge is either a real cross-flow regression or a
          flaky test, and triage decides which within the morning.
        </p>
        <ul>
          <li><strong>Schedule it off-peak</strong> against production-like data, using clones for third-party side effects so the run is safe to repeat (<a href="https://molar.it/blog/staging-environment-alternatives">no dedicated staging needed</a>).</li>
          <li><strong>Capture evidence per flow</strong> — traces, network logs, DOM snapshots — so morning triage is reading, not re-running.</li>
          <li><strong>Track suite duration weekly.</strong> A nightly that grows steadily is telling you something about test debt before the PR gate feels it.</li>
        </ul>
        <p>
          Production schedules extend the same idea outward: Guard-style recurring checks against live environments
          catch what no pre-merge suite can — CDN issues, cert expiry, vendor drift. That’s the complement to
          <a href="https://molar.it/blog/synthetic-monitoring-production">synthetic monitoring</a>, not a replacement for the nightly.
        </p>
      </section>

      <section>
        <h2 id="quarantine-flaky-checks-with-an-owner">Quarantine flaky checks with an owner</h2>
        <p>
          Flaky tests are where regression programs die. Not because flakes are unfixable — most have diagnosable
          causes (<a href="https://molar.it/blog/flaky-tests-causes-and-fixes">root causes and fixes here</a>) — but because teams
          lack a shared policy, so every engineer improvises: mute it, delete it, retry-until-green, ignore red.
          Write the policy down instead:
        </p>
        <ul>
          <li><strong>Quarantine means tracked.</strong> A quarantined test moves out of the gate, gets an owner and a ticket, and shows up on a dashboard. It does not vanish.</li>
          <li><strong>Time-boxed hard.</strong> Fourteen days default. Quarantine without expiry is how suites rot while dashboards stay green.</li>
          <li><strong>Snooze ≠ quarantine.</strong> Snoozing a known-flaky test during a feature branch is fine; quarantining a failing test to ship a release requires the owner’s name next to it.</li>
          <li><strong>Budget the rate.</strong> If a substantial share of the suite is quarantined at steady state, stop merging features and fix reliability — the gate’s credibility is the product.</li>
        </ul>
        <div>
          The graveyard pattern is the most common way regression suites die: tests get quarantined, the quarantine
          list grows, nobody remembers why, and eventually the suite “passes” by testing nothing. Expiry dates and
          named owners are the entire defense.
        </div>
      </section>

      <section>
        <h2 id="review-regressions-your-tests-missed">Review regressions your tests missed</h2>
        <p>
          The only number that tells you whether any of this works is <strong>escape rate</strong>: regressions
          that reached users divided by total regressions found, over time. Everything else — coverage percent,
          test count, suite runtime — is an input metric that can look great while customers find bugs first.
        </p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>escape_rate = bugs_found_in_production_with_regression_origin
            / (same_numerator + regressions_caught_by_suite)

# Track monthly, segment by area, and review every escape
# Every escape → write the missing test + trace why selection missed it</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>Track time to useful feedback and the time between a production failure and a response. Review escaped regressions to learn whether a test was missing, an assertion was weak, or a selection rule skipped the right check. Adjust the rule or test based on that finding.</p>
        <p>
          Finally, make the gate itself non-negotiable: required status checks turn “please run the tests” into
          physics. The mechanics are in our guide to
          <a href="https://molar.it/blog/required-status-checks-merge-gate">required checks as a merge gate</a>.
        </p>]]></content:encoded>
    </item>
    <item>
      <title>Require passing checks before merging</title>
      <link>https://molar.it/blog/required-status-checks-merge-gate</link>
      <guid isPermaLink="true">https://molar.it/blog/required-status-checks-merge-gate</guid>
      <description>Choose the GitHub checks that must pass, configure the requirement, and define how to handle an unreliable check.</description>
      <pubDate>Tue, 28 Jul 2026 00:00:00 GMT</pubDate>
      <category>Engineering</category>
      <content:encoded><![CDATA[<section>
        <h2 id="make-the-intended-check-a-requirement-for-merging">Make the intended check a requirement for merging</h2>
        <p>
          The failure mode is always the same story. CI runs on every pull request and posts its little green
          checkmark. But nothing enforces it: someone merges while the run is still in progress, someone else
          clicks through a red X because “it’s probably flaky”, and main drifts into a state no commit ever
          tested. The pipeline exists; the gate doesn’t. Advisory checks measure quality culture — required
          checks enforce it.
        </p>
        <p>
          The distinction lives in branch protection, not in your workflow file. GitHub Actions can only report
          results; whether those results block anything is decided by rules attached to the branch.
          That separation confuses people once and then costs them years of false confidence, so let’s make the
          enforcement layer explicit. If you haven’t built the underlying test job yet, start with our
          <a href="https://molar.it/blog/github-actions-playwright-ci">Playwright on GitHub Actions guide</a>, then come back
          and make it binding.
        </p>
      </section><figure class="article-graphic article-graphic--release" data-article-graphic data-article-family="release" aria-label="A release gate narrows the decision"><div class="article-graphic__canvas is-release"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M154 126H301"/><path class="" d="M459 126H606"/><path class="" d="M380 126V67H606"/><path class="is-flow" d="M154 126H301"/><path class="is-flow" d="M459 126H606"/><path class="is-flow" d="M380 126V67H606"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">CHANGE</div><h3>Pull request</h3><p>Start with the paths affected by the change.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">scope the run</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">CHECKS</div><h3>Signup · checkout · access</h3><p>Each scenario reports its own assertion and captured route.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">one blocked</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">DECISION</div><h3>Ready or blocked</h3><p>A reviewer sees why the release can proceed or what needs attention.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">required check</span></span></div></div></div><figcaption><strong>A release gate narrows the decision</strong><span>Run the checks that match the change, attach the failed route, and leave the merge decision reviewable.</span></figcaption></figure>

      <section>
        <h2 id="configure-the-required-check-in-github">Configure the required check in GitHub</h2>
        <p>
          In repository settings, under Branches (or Rules → Rulesets on newer plans), create a rule targeting
          <code>main</code>. Three switches matter most: require a pull request before merging, require status
          checks to pass, and select which checks are required from the list of checks that have recently run.
          Via CLI, the same configuration is one API call:
        </p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>gh api repos/{owner}/{repo}/branches/main/protection -X PUT \
  --input - <<'JSON'
{
  "required_status_checks": {
    "strict": true,
    "contexts": ["e2e / verdict", "lint"]
  },
  "enforce_admins": true,
  "required_pull_request_reviews": {
    "required_approving_review_count": 1
  },
  "restrictions": null,
  "allow_force_pushes": false,
  "allow_deletions": false
}
JSON</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>
          Two settings deserve explanation. <code>"strict": true</code> requires the branch to be up to date
          with main before merging — it closes the “tested against stale base” hole where two compatible-looking
          PRs merge independently and only their combination is broken. <code>"enforce_admins": true</code>
          applies the rules to administrators too; leave it off and you’ve built a gate with a permanent,
          temptation-shaped hole in it. Note the contexts list: required checks must be registered
          by exact name, which is where most gates quietly fail — more on that below.
        </p>
      </section>

      <section>
        <h2 id="advisory-versus-required-precisely">Advisory versus required, precisely</h2>
        <p>
          An advisory check runs and reports. Anyone with write access can merge past a failing advisory check;
          depending on settings they may even merge before it finishes. Nothing about this is visible in the UI
          as dangerous — the red X looks identical whether or not it has teeth. A required check, by contrast,
          changes what the merge button does: the PR cannot merge until every required context reaches success.
          Not “completed” — succeeded. Skipped jobs count as neither, which is why a required job
          guarded by <code>if:</code> conditions can leave a gate permanently stuck rather than safely passing.
        </p>
        <p>
          Choose per check, not globally. Require what verifies correctness of the diff: build, lint, unit
          tests, critical-path E2E. Keep long-tail suites advisory or scheduled — a 40-minute exhaustive run
          as a hard requirement converts every typo fix into a coffee break and teaches the team to hate the
          gate. Tiering by cost is exactly the strategy behind
          <a href="https://molar.it/blog/continuous-testing-ci-cd-pipeline">continuous testing across the pipeline</a>: fast
          blocking feedback on PRs, deep coverage on schedule.
        </p>
      </section>

      <section>
        <h2 id="the-naming-trap">The naming trap</h2>
        <p>
          Required contexts match check-run names byte-for-byte, and several common patterns change names
          invisibly. Matrix jobs append their matrix values: a job <code>test</code> sharded four ways produces
          <code>e2e / test (1)</code> through <code>(4)</code> — requiring plain <code>e2e / test</code> protects
          nothing. Renaming a workflow file changes names. Display-name edits change names. Each rename leaves
          the old required context pointing at a check that will never run again, which means the gate either
          wedges every PR or — if someone “fixes” it by removing the requirement — stops existing.
        </p>
        <p>
          The robust pattern is an aggregator: a final job that runs after all shards and succeeds only if every
          shard succeeded. Require the aggregator’s single stable name, and future shard-count changes never
          touch branch protection again. External systems integrate the same way — Guard, for instance, posts
          its pass/fail/flaky verdict as a native check run precisely so it can occupy a slot in the required
          list like any first-class job (see the <a href="https://molar.it/integrations/github-actions">GitHub Actions integration</a>).
        </p>
      </section>

      <section>
        <h2 id="investigate-checks-that-pass-only-after-a-retry">Investigate checks that pass only after a retry</h2>
        <div>
          A required check that fails randomly often enough doesn’t reduce risk — it trains bypass.
          Engineers learn that red usually means “roll the dice again”, and within a quarter the team is
          merging through admin override reflexively. Flake budget is gate security.
        </div>
        <p>
          Before making a suite required, earn it: cap retries at one, track which tests consume them, and move
          repeat offenders to a quarantine lane that still runs and reports but doesn’t block, with a named owner
          and expiry date. Review flake rate weekly like an SLO; set a target your team can defend from observed history. Diagnosis comes
          before policy — timing races, shared state, third-party outages, and environment drift each need their
          own fix, laid out in <a href="https://molar.it/blog/flaky-tests-causes-and-fixes">our flaky-tests guide</a>. A merge
          queue pairs well here: with strict mode plus a queue, each PR is validated against the true tip of
          main, serializing the small window where independent branches would otherwise conflict untested.
        </p>
      </section>

      <section>
        <h2 id="define-how-to-unblock-a-broken-gate">Define how to unblock a broken gate</h2>
        <p>
          Hard gates meet reality at the worst time — production is down, the fix is three lines, and CI takes
          twelve minutes you don’t have. Decide the protocol now, calmly, instead of improvising during an
          incident. The standard toolkit: an admin-override path that works but logs loudly (audit entries are
          reviewable later); a small, named bypass list for on-call rotations rather than “everyone”; and a
          written rule that any bypass triggers an immediate follow-up — revert forward, land the green fix
          behind it, and post the incident note. Some teams invert the pressure entirely: allow bypass only via
          a dedicated hotfix branch whose protection is slightly softer, keeping main’s gate absolute.
        </p>
        <p>Review how often checks are bypassed and why. Repeated overrides may indicate slow tests, unreliable environments, or a policy that no longer fits the team. Retain the captured steps and failure details so an engineer can investigate the result. For rollout guidance, see <a href="https://molar.it/blog/regression-testing-strategies">regression testing strategies</a>.</p>]]></content:encoded>
    </item>
    <item>
      <title>Review automated test repairs without weakening assertions</title>
      <link>https://molar.it/blog/self-healing-test-automation</link>
      <guid isPermaLink="true">https://molar.it/blog/self-healing-test-automation</guid>
      <description>How to evaluate a proposed locator change, preserve the intended behavior, and rerun the original assertion.</description>
      <pubDate>Wed, 05 Aug 2026 00:00:00 GMT</pubDate>
      <category>Agents</category>
      <content:encoded><![CDATA[<section>
        <h2 id="what-self-healing-is-and-isn-t">What self-healing is — and isn’t</h2>
        <p>
          Strip away the marketing and self-healing is one operation: when an element a test targets can no
          longer be found, the system re-identifies it from context and updates the locator. That’s it. Done
          well, it compresses hours of mechanical repair after every redesign into a reviewable diff. Done
          badly, it quietly re-points assertions at whatever element looks closest — including brand-new,
          half-built UI — and your suite reports green while the product regresses underneath it.
        </p>
        <p>
          The scope boundary matters as much as the mechanism. Self-healing addresses locator drift:
          the element still exists, still serves the same user purpose, but its DOM coordinates changed.
          It does not address changed behavior, changed copy semantics, or broken flows. A tool that claims
          to “heal” failing assertions by rewriting them is not maintaining your tests; it is deleting your
          safety net one rewrite at a time.
        </p>
      </section><figure class="article-graphic article-graphic--agent" data-article-graphic data-article-family="agent" aria-label="Review the generated test before CI"><div class="article-graphic__canvas is-agent"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M154 126H301"/><path class="" d="M459 126H606"/><path class="is-flow" d="M154 126H301"/><path class="is-flow" d="M459 126H606"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">FAILURE</div><h3>Locator no longer matches</h3><p>The runtime check stops with its original assertion intact.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">runtime stays red</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">PROPOSAL</div><h3>Candidate locator patch</h3><p>The agent suggests a narrow change against the current page.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">diff to inspect</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">REVIEW</div><h3>Approve, edit, or reject</h3><p>A person or policy decides whether the assertion still means the same thing.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">human decision</span></span></div></div></div><figcaption><strong>Review the generated test before CI</strong><span>Let an agent propose the task or repair, then keep the executed check repeatable and reviewable.</span></figcaption></figure>

      <section>
        <h2 id="locator-ranking-is-most-of-the-battle">Locator ranking is most of the battle</h2>
        <p>
          The amount of healing a suite needs is largely decided at authoring time. A recorder that grabs the
          first matching CSS path creates maximal future damage; a generator that ranks candidates creates
          almost none. Every serious implementation converges on roughly the same ladder:
        </p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code># Locator ranking, best to worst
1. data-testid             (purpose-built, stable across redesigns)
2. role + accessible name  (getByRole('button', { name: 'Checkout' }))
3. label association       (getByLabel('Email') for form fields)
4. unique text content     (human-meaningful, survives restyling)
5. structural CSS / XPath  (last resort — breaks on any DOM change)</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>
          Roles and accessible names survive styling overhauls because they describe what the element
          is, not where it sits in the tree. If your current stack makes this ranking awkward, our
          <a href="https://molar.it/blog/playwright-vs-cypress-comparison">Playwright vs Cypress comparison</a> covers how their
          selector engines differ. Healing then becomes narrow: re-run this same ranking when the primary
          candidate fails, constrained by what the step is supposed to accomplish.
        </p>
      </section>

      <section>
        <h2 id="capture-intent-at-authoring-time">Capture intent at authoring time</h2>
        <p>
          Here is the part most tools skip. To re-identify an element safely, you must know what it was
          for — not just what it looked like. “Click the third button” contains no recoverable intent;
          “click ‘Pay now’ to submit the order” contains plenty. When authoring (or generating) a test, record
          alongside each step: the semantic action, the target’s accessible identity, the assertion expected
          afterward, and a screenshot or accessibility snapshot at that point.
        </p>
        <p>
          This metadata is cheap to store and priceless later. With it, healing is a verification problem:
          find candidate elements, keep the ones whose role, name, and position satisfy the recorded intent,
          propose the strongest one. Without it, healing degenerates into fuzzy text matching against pixels,
          which is exactly how suites get silently re-pointed at the wrong controls. AI-generated tests have an
          advantage here — <a href="https://molar.it/blog/ai-playwright-test-generation">generation pipelines that map flows first</a>
          naturally produce steps with semantic labels attached — but hand-written suites benefit equally from
          disciplined naming and comments. The same authoring-time discipline underpins everything in our
          <a href="https://molar.it/blog/e2e-testing-best-practices">E2E practices guide</a>.
        </p>
      </section>

      <section>
        <h2 id="the-heal-loop-propose-review-approve">The heal loop: propose, review, approve</h2>
        <p>
          A production-grade heal loop has five stages: detect the breakage, re-rank candidates against stored
          intent, verify the proposal (the new element must satisfy every recorded constraint), emit a diff with
          evidence, and wait for human approval before anything lands. The output should look like code review,
          because it is code review:
        </p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>- // Checkout: submit order
- await page.click('div.checkout > form > button:nth-child(4)');
+ await page.getByRole('button', { name: 'Pay now' }).click();
  # Evidence: redesign commit moved checkout into a modal.
  # Candidate matched: role=button, name="Pay now", visible, enabled.
  # Post-step assertion held: confirmation banner appeared.</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>Review a locator change through the same process as other test code, then rerun the original assertion. Confirm that the target still serves the same purpose. Molar’s <a href="https://molar.it/products/mender">Mender</a> remains in preview; do not assume automatic repair is enabled for a beta project.</p>
      </section>

      <section>
        <h2 id="audit-trails-make-healing-defensible">Audit trails make healing defensible</h2>
        <p>
          The moment software starts editing your tests, “trust me” stops being acceptable. Every heal should be
          logged as a queryable record: which test, which step, old locator, new locator, match confidence, the
          run evidence that justified it, who approved it, and when. That record turns disputes (“did this test
          ever cover the old flow?”) into database lookups instead of archaeology, and it gives you the aggregate
          numbers that tell you whether healing is working: proposals per week, acceptance rate, mean time-to-heal,
          and — critically — heals later reverted, which are your false-positive signal.
        </p>
        <p>
          Auditability also changes team behavior. Engineers accept machine-proposed diffs far more readily when
          rejection is one click and fully reversible; they resist anything that feels like it edits behind their
          back. Deterministic replay closes the loop — being able to replay the exact run that motivated a heal,
          with full DOM and network state, makes the evidence checkable rather than anecdotal.
        </p>
      </section>

      <section>
        <h2 id="preserve-the-original-assertion-when-changing-a-locator">Preserve the original assertion when changing a locator</h2>
        <div>
          Silent auto-patching converts a regression detector into a compliance mechanism. If the “Buy” button
          becomes “Subscribe” and the heal silently follows it, your payment-flow test stays green while your
          revenue model changed. A green badge produced by an unreviewed rewrite is worth less than no test
          at all.
        </div>
        <p>
          Draw the line explicitly in policy, not just in code. Locators are mechanics — safe to re-rank with
          review. Assertions, step order, and coverage boundaries are semantics — never touched by automation.
          And diagnose before healing: if the same test fails intermittently rather than after a specific UI
          commit, you’re looking at flakiness, not drift, and healing will just paper over a race condition.
          Our <a href="https://molar.it/blog/flaky-tests-causes-and-fixes">flaky-tests field guide</a> covers that diagnosis path,
          and the CI wiring that surfaces both failure classes is in the
          <a href="https://molar.it/blog/github-actions-playwright-ci">GitHub Actions guide</a>.
        </p>]]></content:encoded>
    </item>
    <item>
      <title>Plan tests before a change is ready to ship</title>
      <link>https://molar.it/blog/shift-left-testing-explained</link>
      <guid isPermaLink="true">https://molar.it/blog/shift-left-testing-explained</guid>
      <description>Review acceptance criteria early and bring useful browser checks into the pull-request workflow.</description>
      <pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate>
      <category>Engineering</category>
      <content:encoded><![CDATA[<section>
        <h2 id="the-cost-curve-stated-honestly">The cost curve, stated honestly</h2>
        <p>
          The classic diagrams show cost rising an order of magnitude per phase — design → code → test →
          production. Exact multiples vary by study and by team; what never varies is the direction. The
          mechanism is concrete:
        </p>
        <ul>
          <li><strong>Found in review:</strong> one comment thread. Fix touches one file.</li>
          <li><strong>Found in CI:</strong> a red check plus a trace. Fix touches a few files; re-review is cheap.</li>
          <li><strong>Found after release:</strong> incident channel, customer reports, hotfix under pressure, backport, postmortem — and regression tests written after the context has already evaporated.</li>
        </ul>
        <p>
          Note what the curve does not say: that everything should run earlier regardless of cost.
          A production synthetic probe is cheap and irreplaceable for certain bug classes; a full browser
          suite on every local save is expensive and useless. Shift-left means placing each check at the
          earliest point where it’s reliable and affordable.
        </p>
      </section><figure class="article-graphic article-graphic--release" data-article-graphic data-article-family="release" aria-label="A release gate narrows the decision"><div class="article-graphic__canvas is-release"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M154 126H301"/><path class="" d="M459 126H606"/><path class="" d="M380 126V67H606"/><path class="is-flow" d="M154 126H301"/><path class="is-flow" d="M459 126H606"/><path class="is-flow" d="M380 126V67H606"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">CHANGE</div><h3>Pull request</h3><p>Start with the paths affected by the change.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">scope the run</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">CHECKS</div><h3>Signup · checkout · access</h3><p>Each scenario reports its own assertion and captured route.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">one blocked</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">DECISION</div><h3>Ready or blocked</h3><p>A reviewer sees why the release can proceed or what needs attention.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">reviewable result</span></span></div></div></div><figcaption><strong>A release gate narrows the decision</strong><span>Run the checks that match the change, attach the failed route, and leave the merge decision reviewable.</span></figcaption></figure>

      <section>
        <h2 id="the-leftmost-shift-ambiguity-caught-in-design">The leftmost shift: ambiguity, caught in design</h2>
        <p>
          The most expensive bugs are specification bugs wearing implementation costumes. “What happens to
          a half-completed subscription when the card expires?” is free to answer in a design doc and
          ruinous to discover from a support ticket. So the cheapest test artifact is a written scenario
          list produced before code exists:
        </p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code># Example acceptance criteria for checkout
Scenario: Expired saved card
  When I pay with an expired saved card
  Then I see the retry form
  And no successful payment is recorded

Scenario: Duplicate submit
  When I double-click "Pay"
  Then exactly one charge exists for this order</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>Plain-English scenarios let product, engineering, and support review expected behavior before implementation. Keep them specific enough to turn into assertions later. The examples above describe behavior; they are not an executable Molar configuration.</p>
      </section>

      <section>
        <h2 id="the-pr-shift-verification-inside-the-review-window">The PR shift: verification inside the review window</h2>
        <p>
          The second shift moves behavioral verification from “after merge, eventually” to “inside the PR,
          automatically.” Concretely: every pull request runs the critical-path scenarios against an
          ephemeral environment with safe third-party substitutes, posts results with trace evidence as
          comments, and blocks merge via required checks when core flows break.
        </p>
        <p>
          The mechanics are covered elsewhere in depth — making checks mandatory in
          <a href="https://molar.it/blog/required-status-checks-merge-gate">required status checks as a merge gate</a>, and
          wiring the runners in <a href="https://molar.it/blog/github-actions-playwright-ci">Playwright in GitHub
          Actions</a>. The shift-left framing adds one rule: evidence belongs in the PR itself. A
          reviewer who can watch a failing replay — DOM state, network calls, console errors, the cloned
          third-party responses — without leaving the thread reviews in minutes instead of hours. That’s
          the difference between shift-left speeding up reviews and slowing them.
        </p>
      </section>

      <section>
        <h2 id="guardrails-not-gatekeeping">Guardrails, not gatekeeping</h2>
        <p>
          Shift-left fails socially when it becomes ceremony. The working distinction:
        </p>
        <ul>
          <li><strong>Budgets.</strong> Required checks must finish inside the PR feedback budget your team sets or they will be bypassed — the stage-by-stage policy is in <a href="https://molar.it/blog/continuous-testing-ci-cd-pipeline">continuous testing in your CI/CD pipeline</a>.</li>
          <li><strong>Scope.</strong> Gate on critical paths only; long-tail coverage belongs to scheduled runs, not every developer’s every push.</li>
          <li><strong>Flake policy.</strong> Quarantine flaky checks same-day. One random failure costs more goodwill than twenty true catches earn back — root causes are catalogued in our piece on <a href="https://molar.it/blog/flaky-tests-causes-and-fixes">flaky tests</a>.</li>
          <li><strong>Escape hatch with receipts.</strong> Admin-override stays possible but logged and reviewed weekly. Trust with accountability beats either extreme.</li>
        </ul>
        <div>
          If required checks fail routinely for non-code reasons, developers learn to override them
          reflexively — and then the one check that catches the real outage gets overridden too. Flake
          hygiene isn’t polish for a shift-left program; it’s the load-bearing wall.
        </div>
      </section>

      <section>
        <h2 id="a-minimum-viable-program">A minimum viable program</h2>
        <ul>
          <li><strong>This week:</strong> write scenario lists for your top three revenue-critical flows; get product sign-off on the edge cases.</li>
          <li><strong>This sprint:</strong> make those scenarios executable and run them on every PR against ephemeral environments with cloned third parties — no shared staging bottleneck, no real charges.</li>
          <li><strong>This quarter:</strong> promote the suites to required checks, add trace-evidence comments on failure, and schedule long-tail regression coverage so nothing silently rots — see our <a href="https://molar.it/blog/regression-testing-strategies">regression testing strategies</a>.</li>
        </ul>
        <p>
          Measure the program by where bugs surface, not by coverage percentages. If after a quarter your
          post-release incident count hasn’t moved, the checks are running at the wrong depth or guarding
          the wrong flows — shift-left is a direction, not a destination, and the metric that matters is
          the phase in which defects die.
        </p>
        <p>Molar is building toward reusable browser checks across development and releases. Start with the <a href="https://molar.it/products/cartographer">Cartographer beta workflow</a>; generated Playwright export and clean replay are still being validated. Configure and verify each release integration before relying on it.</p>]]></content:encoded>
    </item>
    <item>
      <title>Make test environments easier to trust</title>
      <link>https://molar.it/blog/staging-environment-alternatives</link>
      <guid isPermaLink="true">https://molar.it/blog/staging-environment-alternatives</guid>
      <description>Reduce configuration drift, isolate data, and choose appropriate test services for preview and staging environments.</description>
      <pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate>
      <category>Integrations</category>
      <content:encoded><![CDATA[<section>
        <h2 id="where-drift-comes-from">Where drift comes from</h2>
        <p>
          Staging drifts from production through a hundred small, individually reasonable decisions:
        </p>
        <ul>
          <li><strong>Manual hotfixes.</strong> Someone fixes prod by hand during an incident and never backports the config change.</li>
          <li><strong>Scale-shaped differences.</strong> One small instance vs. an autoscaling fleet; queue limits, connection pools, and timeout behavior all change with them.</li>
          <li><strong>Cheaper substitutes.</strong> Real CDN replaced by direct serving, real WAF disabled, feature flags defaulted differently.</li>
          <li><strong>Infrequent deploys.</strong> Prod gets daily releases; staging gets them weekly, so even the code diverges.</li>
        </ul>
        <p>
          Each gap is invisible until exactly the wrong moment — and the gaps compound. A config difference
          that’s harmless alone (feature flags defaulted off) becomes the reason nobody notices a data-shape
          difference for weeks, because the code path that reads it never runs in either environment the
          same way twice. The industry’s honest answer is not “try harder to maintain staging” but to shrink
          what staging is asked to prove.
        </p>
      </section><figure class="article-graphic article-graphic--data" data-article-graphic data-article-family="data" aria-label="Fresh state keeps parallel runs honest"><div class="article-graphic__canvas is-data"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M154 126H301"/><path class="" d="M459 126H606"/><path class="is-flow" d="M154 126H301"/><path class="is-flow" d="M459 126H606"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">SEED</div><h3>Known environment state</h3><p>Use repeatable records, clocks, and test identities with clear prefixes.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">created for this run</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">WORKER</div><h3>Isolated browser + services</h3><p>One run can change its records without racing another run.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">isolated worker</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">RESET</div><h3>Restore or discard</h3><p>Start the next run from a known baseline instead of deleting shared rows.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">repeat from clean state</span></span></div></div></div><figcaption><strong>Fresh state keeps parallel runs honest</strong><span>Create or restore the state each worker needs; cleanup instructions alone do not make tests independent.</span></figcaption></figure>

      <section>
        <h2 id="data-divergence-is-worse-than-config-divergence">Data divergence is worse than config divergence</h2>
        <p>
          Config you can diff. Data you can’t. Staging databases hold either a stale restore (yesterday’s
          shape, last month’s cardinality), synthetic seed rows (uniform, clean, unrealistically joined),
          or months of accumulated test junk (duplicate emails, half-deleted records, colliding slugs).
          None of these behave like production data:
        </p>
        <ul>
          <li>Queries that plan fine on 10k rows fall over on 10M — N+1s hide until cardinality betrays them.</li>
          <li>Unique-constraint races never fire against seed data because nothing collides.</li>
          <li>Migration lock times are unmeasurable on a table that takes milliseconds to scan.</li>
        </ul>
        <p>
          The discipline that helps — masking, subsetting, seeding deterministically — lives in our guide to
          <a href="https://molar.it/blog/test-data-management-best-practices">test data management</a>. But it treats symptoms;
          the deeper fix is needing less shared environment at all.
        </p>
      </section>

      <section>
        <h2 id="sandbox-behavior-hides-vendor-failures">Sandbox behavior hides vendor failures</h2>
        <p>
          Most “staging” setups fake out vendors in the least honest way possible: sandbox accounts shared
          across every environment, stub servers that only know happy paths, or — worst — live keys with a
          test card. So the flows that fail loudest in production (payments, OTP, email deliverability,
          storage lifecycle rules) are precisely the ones staging validates least. We documented the
          payments version of this in <a href="https://molar.it/blog/test-stripe-without-real-cards">testing Stripe without
          real cards</a>, and the general pattern — mocks vs. stateful clones — in
          <a href="https://molar.it/blog/api-mocking-vs-service-clones">API mocking vs. service clones</a>.
        </p>
        <div>
          The most dangerous staging bug is config divergence: staging points at the vendor sandbox while
          production points at live, and no test ever exercises the production wiring itself. When you do run
          against real infrastructure, make it explicit, allow-listed, and read-only — never accidental.
        </div>
      </section>

      <section>
        <h2 id="the-alternative-ephemeral-apps-cloned-dependencies">The alternative: ephemeral apps + cloned dependencies</h2>
        <p>
          Modern stacks invert the model. Instead of maintaining one long-lived staging replica, you spin up
          an ephemeral full stack per PR — preview URL included — and point its third-party dependencies at
          stateful clones:
        </p>
        <p>Example to adapt: helper functions, accounts, and endpoints depend on your test project. Adapt this example to your project.</p><div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code># ephemerous-but-deterministic: env for a PR preview
DATABASE_URL=postgres://ci:${TOKEN}@db-pr-4821.internal/app
STRIPE_BASE_URL=http://stripe-clone.internal:12111   # stateful, signed webhooks
EMAIL_BASE_URL=http://mailhole.internal:9025         # capture inbox, no delivery
S3_ENDPOINT=http://s3-clone.internal:9000            # real API, throwaway bucket</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>An environment per pull request can isolate data and make cleanup simpler, provided its dependencies and reset behavior are also configured. Check that parallel runs do not share mutable state. <a href="https://molar.it/products/clones">Molar Clones</a> offers supported service substitutes, while <a href="https://molar.it/products/guard">Guard</a> runs your scenarios as release checks.</p>
      </section>

      <section>
        <h2 id="when-you-must-test-against-production-mark-everything">When you must test against production, mark everything</h2>
        <p>
          Some classes of bug only reproduce against production: CDN cache behavior, geo-routing, real
          vendor rate limits, data-volume effects. For those, run scheduled tests against production using
          strictly marked identities rather than pretending staging would have caught them:
        </p>
        <ul>
          <li>Dedicated accounts like <code>qa-probe-7@yourdomain.com</code>, flagged in the users table.</li>
          <li>Request headers (<code>X-Molar-Probe: true</code>) your app logs for easy exclusion from analytics.</li>
          <li>Read-only assertions wherever possible; side effects limited to records the check itself created.</li>
          <li>Alerting on two consecutive failures to avoid paging on one-off blips — the cadence details are
              in <a href="https://molar.it/blog/synthetic-monitoring-production">synthetic monitoring for production</a>.</li>
        </ul>
        <p>
          The marked-identity discipline matters even when everything works, because the worst outcome of a
          probe is not a false alarm but a true event nobody can distinguish from customer traffic:
          support sees a strange signup, deletes it, and your probe starts failing mysteriously. Flagged
          identities turn that archaeology into a filter query. Combined with the pipeline placement rules
          in <a href="https://molar.it/blog/continuous-testing-ci-cd-pipeline">continuous testing in CI/CD</a>, this replaces
          “staging said it was fine” with evidence from the environment that actually matters.
        </p>
      </section>

      <section>
        <h2 id="a-pragmatic-migration-path">A pragmatic migration path</h2>
        <ul>
          <li><strong>Week 1:</strong> audit staging’s actual divergence — config diff, data age, vendor wiring. Write down what each check in staging can truly catch.</li>
          <li><strong>Week 2:</strong> move critical-path E2E off shared staging onto per-PR ephemeral stacks with cloned dependencies.</li>
          <li><strong>Week 3:</strong> add marked-identity production probes for the bugs only production reveals.</li>
          <li><strong>Ongoing:</strong> demote staging to what it’s genuinely good for — stakeholder demos and long-lived integration experiments.</li>
        </ul>
        <p>
          Teams rarely delete staging after this. They just stop asking it to predict production — which is
          the only promise it was never able to keep.
        </p>]]></content:encoded>
    </item>
    <item>
      <title>Startup QA without a dedicated QA team</title>
      <link>https://molar.it/blog/startup-qa-without-qa-team</link>
      <guid isPermaLink="true">https://molar.it/blog/startup-qa-without-qa-team</guid>
      <description>Start with important customer tasks, build repeatable tests, and add release checks your team can maintain.</description>
      <pubDate>Sat, 20 Jun 2026 00:00:00 GMT</pubDate>
      <category>Engineering</category>
      <content:encoded><![CDATA[<section><h2 id="start-with-the-checks-people-repeat-before-release">Start with the checks people repeat before release.</h2><p>A small team does not need a hundred end-to-end tests to get useful protection. Write down the actions someone performs before shipping: create an account, verify email, complete the main task, pay, invite a teammate, or change a plan. Mark which tasks affect revenue, trust, or access, and which ones change often. That list is your first test map.</p><p>Measure the current manual cost and the usual failure point. If checkout takes ten minutes to verify and a missing callback can leave a paid account locked out, it deserves an automated check before a low-risk settings page. The goal is a short list that catches consequential regressions while the author still remembers the change.</p></section><figure class="article-graphic article-graphic--decision" data-article-graphic data-article-family="decision" aria-label="Compare setup against one browser task"><div class="article-graphic__canvas is-decision"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M154 126H301"/><path class="" d="M459 126H606"/><path class="is-flow" d="M154 126H301"/><path class="is-flow" d="M459 126H606"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">TASK</div><h3>Protect the flow that pays</h3><p>Use the same account, input, and success condition for each option.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">shared test question</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">SETUP</div><h3>Compare the boundary</h3><p>Look at authoring, service state, environments, and maintenance—not just the first green run.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">tradeoffs visible</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">DECISION</div><h3>Keep the result you can repeat</h3><p>Choose the tool that leaves your team with a result it can inspect and run again.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">review with context</span></span></div></div></div><figcaption><strong>Compare setup against one browser task</strong><span>Comparison guides become useful when one test task exposes setup, maintenance, and review tradeoffs.</span></figcaption></figure>
<section><h2 id="write-an-outcome-another-person-can-verify">Write an outcome another person can verify.</h2><p>“Checkout works” is too vague to review. “A new account pays for Pro, sees the order number, and can open the Pro workspace exactly once” gives the test a subject, action, and result. Include the downstream state when the customer task crosses a service boundary.</p><div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>test('paid account can open Pro workspace', async ({ page }) =&gt; {
  await signup(page, testUser);
  await verifyEmail(page, testUser);
  await page.getByRole('link', { name: 'Plans' }).click();
  await page.getByRole('button', { name: 'Choose Pro' }).click();
  await completeTestPayment(page);
  await expect(page.getByText(/Order number/)).toBeVisible();
  await expect(page.getByRole('link', { name: 'Pro workspace' })).toBeVisible();
});</code></pre><p class="copy-status sr-only" role="status"></p></div><p>The helper names above belong to the project that owns the app. Keep the assertion tied to a customer-visible outcome and add a service or database check where the browser cannot see the complete handoff.</p></section>
<section><h2 id="make-data-and-service-state-disposable">Make data and service state disposable.</h2><p>Use a unique test identity, a reachable preview or local URL, and a clean starting state. A test that needs one engineer to delete yesterday’s order before it passes is not ready for CI. For payments, email, SMS, authentication, or storage, use a provider test environment or a supported clone. Molar’s <a href="https://docs.molar.it/docs/clones">clone catalog</a> lists the implemented operations; it does not promise complete vendor parity.</p><p>When a test creates state, name it so a failure can be found quickly. Keep credentials in the CI secret store. If a service supports snapshots or reset, verify the reset before rerunning. Parallel runs should not share a user, inbox, payment record, or object path unless the test is specifically checking shared access.</p></section>
<section><h2 id="promote-one-stable-check-into-ci">Promote one stable check into CI.</h2><p>Run the same scenario locally, in a preview environment, and in CI before making it required. Molar’s documented CLI path can start a browser verification such as:</p><div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>molar verify "the login form is visible" \
  --url http://localhost:3000 \
  --json</code></pre><p class="copy-status sr-only" role="status"></p></div><p>Read the status and captured screenshot, fix the app when it fails, and rerun from the same setup. Once a check is stable, connect it to the repository policy your team uses. Do not hide a flaky check behind retries and call it reliable; record the flake, assign an owner, and decide whether it should block while it is repaired.</p></section>
<section><h2 id="keep-failures-actionable">Keep failures actionable.</h2><p>A small team cannot afford a red build that only says “timeout.” Retain the failed step, URL or target, assertion, screenshot, and relevant request or service identifier. The author should be able to answer whether the app broke, the test account was missing, or the dependent service was unavailable.</p><p>When a failure needs a decision, state it in the review: fix the app, update the expected behavior, or improve the test setup. Never weaken an assertion just to make the check green. Keep scheduled checks separate from merge checks when they need longer setup or production access.</p></section>
<section><h2 id="expand-from-escaped-bugs-and-repeated-work">Expand from escaped bugs and repeated work.</h2><p>After the first check is useful, add the next branch based on a real gap: a declined payment, expired code, missing receipt, revoked invitation, or callback delivered twice. Keep existing unit, API, Playwright, or Cypress tests that already serve the team. Browser agents and service clones can fill gaps when a maintained scripted test is expensive to author, but generated export and clean replay remain under beta validation.</p><p>See the <a href="https://molar.it/blog/required-status-checks-merge-gate">required-check guide</a>, <a href="https://molar.it/blog/test-data-management-best-practices">test data guide</a>, or <a href="https://molar.it/solutions/startups">Molar for small teams</a>. What you can test depends on your app, test accounts, environment and the supported integrations.</p></section>]]></content:encoded>
    </item>
    <item>
      <title>Check that important tasks still work in production</title>
      <link>https://molar.it/blog/synthetic-monitoring-production</link>
      <guid isPermaLink="true">https://molar.it/blog/synthetic-monitoring-production</guid>
      <description>Choose browser checks, test accounts, schedules, and alerts for the application your customers use.</description>
      <pubDate>Sat, 25 Jul 2026 00:00:00 GMT</pubDate>
      <category>Engineering</category>
      <content:encoded><![CDATA[<section>
        <h2 id="pings-measure-liveness-not-correctness">Pings measure liveness, not correctness</h2>
        <p>
          Every outage postmortem has a version of the same line: “monitoring was green.” Uptime pings answer one
          narrow question — did the server return an HTTP success code within N milliseconds? They don’t exercise
          the code path your revenue depends on. A broken signup form behind a bad JS bundle, an expired OAuth client
          secret, a payment provider whose schema changed silently, a feature flag that renders an empty dashboard —
          all invisible to a ping, all catastrophic for users.
        </p>
        <p>
          The industry’s answer is synthetic monitoring: scripted checks that behave like real users,
          running continuously against production. Done right, this is just
          <a href="https://molar.it/blog/e2e-testing-best-practices">end-to-end testing</a> executed on a schedule against live
          infrastructure. Done badly, it’s a pile of flaky checks that page you at 3am because a CDN hiccuped.
          The difference is entirely in the design decisions below.
        </p>
      </section><figure class="article-graphic article-graphic--failure" data-article-graphic data-article-family="failure" aria-label="Move from a red check to a cause"><div class="article-graphic__canvas is-failure"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M96 126H205"/><path class="" d="M225 126H334"/><path class="" d="M354 126H463"/><path class="" d="M483 126H592"/><path class="is-flow" d="M96 126H205"/><path class="is-flow" d="M225 126H334"/><path class="is-flow" d="M354 126H463"/><path class="is-flow" d="M483 126H592"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">SCHEDULED RUN</div><h3>Important task regressed</h3><p>The check records the task and environment where it changed.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">start here</span></span></div><div class="article-graphic__card "><div class="article-graphic__eyebrow">REPRODUCE</div><h3>Same account, same input</h3><p>Repeat from a clean state and separate product failure from setup failure.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">control variables</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">INSPECT</div><h3>Browser · request · console</h3><p>Align the browser, request, and console records around the first divergence.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">find the first break</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">NEXT STEP</div><h3>Route to the owner</h3><p>Give the team a trace and an actionable notification.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">reviewed diagnosis</span></span></div></div></div><figcaption><strong>Move from a red check to a cause</strong><span>A useful failed run preserves the failing assertion, the relevant request, and the next experiment.</span></figcaption></figure>

      <section>
        <h2 id="what-a-good-synthetic-check-looks-like">What a good synthetic check looks like</h2>
        <p>
          A flow-level check walks one critical user path end to end, asserts on outcomes (not pixels), and cleans
          up after itself. Model each check on a named business flow rather than a URL list:
        </p>
        <p>Example to adapt: helper functions, accounts, and endpoints depend on your test project. Adapt this example to your project.</p><div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code># synthetic-checks/signup.yaml
flow: signup-and-first-login
schedule: "*/5 * * * *"        # every 5 minutes
regions: [us-east, eu-west]
steps:
  - open: /signup
  - fill_email: "{{ synthetic.identity }}@mail.example.com"
  - fill_password: "{{ synthetic.password }}"
  - click: Create account
  - expect_text: "Check your inbox"
  - extract_code_from_email: inbox={{ synthetic.inbox }}   # via mail clone
  - submit_code
  - expect_url: /dashboard
assert_timeout_s: 30</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>
          Three properties matter more than coverage breadth. First, every check has an owner — unowned alerts get
          ignored. Second, assertions are semantic (“dashboard heading renders”) not visual-only, which keeps them
          out of <a href="https://molar.it/blog/flaky-tests-causes-and-fixes">flake territory</a>. Third, the check is hermetic:
          its email, SMS, and payment interactions hit clones, not live vendors, so runs cost nothing side-effect-wise.
        </p>
      </section>

      <section>
        <h2 id="frequency-and-regions-match-cadence-to-blast-radius">Frequency and regions: match cadence to blast radius</h2>
        <p>
          Run everything every minute and you’ll spend real money generating load and drown yourself in noise.
          Run things hourly and your mean time to detection is an hour. Calibrate by blast radius:
        </p>
        <ul>
          <li><strong>Revenue path (checkout, paywall):</strong> frequently, from multiple regions.</li>
          <li><strong>Auth path (login, SSO):</strong> on a shorter cadence. Auth breakage blocks everything else.</li>
          <li><strong>Core UX (search, profile, upload):</strong> on a longer cadence when the path is lower risk.</li>
          <li><strong>Long-tail pages:</strong> daily crawl-style sweeps instead of dedicated checks.</li>
        </ul>
        <p>Choose monitoring locations based on where your customers are and where failures could differ. Check the region, scheduling, alerting, and recording support of the service you use. With Molar Guard, you choose the schedule and environment for each scenario; it is not a promise of every region or monitoring configuration.</p>
        <div>
          Don’t point production synthetics at load-test-sized intervals. A checkout flow running once a minute
          creates ~43k sessions a month. If your checks create orders, make sure downstream systems (finance,
          fulfillment, support tooling) know which ones are synthetic — see the next section.
        </div>
      </section>

      <section>
        <h2 id="marked-synthetic-identities-always">Marked synthetic identities, always</h2>
        <p>
          Every synthetic run should act through clearly labeled identities: emails like
          <code>+synth-</code>-tagged addresses, accounts flagged in your database, order notes marking automation.
          This pays off repeatedly:
        </p>
        <ul>
          <li>Analytics stay clean — dashboards exclude synthetic traffic instead of showing phantom conversion dips.</li>
          <li>Support can recognize synthetic orders instead of investigating “customers” who never respond.</li>
          <li>You can audit check behavior after the fact: every artifact ties back to a known identity.</li>
        </ul>
        <p>
          Unmarked synthetic data is how you end up emailing a fake customer a real invoice. If your checks need
          OTP codes or magic links, route them through a controlled mail system rather than a shared inbox — the
          same mechanics described in our guide to <a href="https://molar.it/blog/email-workflow-testing-otp">testing email workflows</a>.
        </p>
      </section>

      <section>
        <h2 id="group-repeated-failures-into-one-actionable-alert">Group repeated failures into one actionable alert</h2>
        <p>
          A single broken deployment fails every scheduled run until someone fixes it. At a short cadence across multiple regions, one incident can generate a large number of failures. Naive paging on each failure trains everyone
          to ignore the channel within a week. Dedup before notifying:
        </p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code># Grouping rule (pseudo-config)
group_by: flow_id
window: 15m
notify_when: first_failure_in_group
escalate_when: failures >= 3 consecutive   # ~15 min sustained
resolve_when: 2 consecutive passes
suppress_during_deploy_window: true          # known-change quieting</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>
          The key ideas: notify on group-open, not every failure; require sustained failure before paging
          humans (transient network blips self-heal); auto-resolve on consecutive passes; and suppress alerts during
          an active deploy window when a change is already being rolled back. Route sustained failures into the same
          channel your engineers already watch — for most teams that means opening a GitHub issue or firing the
          on-call webhook, not yet another dashboard nobody has open. This pairs naturally with
          <a href="https://molar.it/blog/github-actions-playwright-ci">PR-gated E2E in CI</a>: if a change was going to break the
          flow, the gate should catch it before production ever sees it.
        </p>
      </section>

      <section>
        <h2 id="compare-failures-with-recent-changes">Compare failures with recent changes</h2>
        <p>
          An alert that says “signup flow failed” starts an investigation. An alert that says “signup flow started
          failing at 14:32 UTC, first failing run followed deploy <code>a71f0e3</code> by 90 seconds; trace shows
          the submit button no longer enables” starts a fix. Correlating check failures with recent deploys and
          attaching full execution evidence — DOM snapshot, network log, console output — turns production incidents
          into ten-minute fixes.
        </p>
        <p>
          This deploy-correlation habit also feeds your broader
          <a href="https://molar.it/blog/regression-testing-strategies">regression strategy</a>: flows that keep breaking in
          production are telling you your pre-merge suite has a gap at exactly that spot. Treat every synthetic
          escape as a missing test case, write it, and add it to the PR gate.
        </p>]]></content:encoded>
    </item>
    <item>
      <title>Manage test data across local runs and CI</title>
      <link>https://molar.it/blog/test-data-management-best-practices</link>
      <guid isPermaLink="true">https://molar.it/blog/test-data-management-best-practices</guid>
      <description>Use separate accounts, predictable starting data, and verified cleanup to prevent tests from interfering.</description>
      <pubDate>Thu, 18 Jun 2026 00:00:00 GMT</pubDate>
      <category>Integrations</category>
      <content:encoded><![CDATA[<section>
        <h2 id="shared-state-is-where-flakiness-is-born">Shared state is where flakiness is born</h2>
        <p>
          The canonical failure: two tests both operate on <code>test@example.com</code>. Run alone, each
          passes. Run together, one deletes the user the other is mid-session with. The suite becomes
          order-dependent — it passes today because of alphabetical file naming, fails Tuesday when someone
          renames a spec. Retries don’t fix this; they convert a deterministic bug into an intermittent one,
          which is strictly worse to debug.
        </p>
        <p>
          The diagnostic is simple: if you can’t run any subset of your suite in parallel against the same
          environment, your tests share state. The fix is structural, not procedural — no amount of
          “please clean up after yourselves” survives contact with real parallelism. Everything below exists
          to make every test hermetic by construction.
        </p>
      </section><figure class="article-graphic article-graphic--data" data-article-graphic data-article-family="data" aria-label="Fresh state keeps parallel runs honest"><div class="article-graphic__canvas is-data"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M154 126H301"/><path class="" d="M459 126H606"/><path class="is-flow" d="M154 126H301"/><path class="is-flow" d="M459 126H606"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">SEED</div><h3>Unique test data</h3><p>Use repeatable records, clocks, and test identities with clear prefixes.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">created for this run</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">WORKER</div><h3>Isolated browser + services</h3><p>One run can change its records without racing another run.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">isolated worker</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">RESET</div><h3>Restore or discard</h3><p>Start the next run from a known baseline instead of deleting shared rows.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">repeat from clean state</span></span></div></div></div><figcaption><strong>Fresh state keeps parallel runs honest</strong><span>Create or restore the state each worker needs; cleanup instructions alone do not make tests independent.</span></figcaption></figure>

      <section>
        <h2 id="rule-1-unique-per-run-data-beats-cleanup">Rule 1: unique-per-run data beats cleanup</h2>
        <p>
          The cheapest isolation is collision-proof identifiers. Every user, org, project, and invoice a
          test creates gets a unique suffix generated at runtime:
        </p>
        <p>Example to adapt: helper functions, accounts, and endpoints depend on your test project. Adapt this example to your project.</p><div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>import { nanoid } from 'nanoid';

export const newUser = (role: 'member' | 'admin' = 'member') => ({
  email: `qa-${runId()}-${nanoid(8)}@example.test`, // .test is reserved, never routable
  plan: 'free',
  role,
});</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>
          Note the tradeoff honestly: unique emails make local debugging harder (“which row was mine?”).
          Mitigate with structure — a stable <code>qa-</code> prefix, the CI run id, then randomness — so a
          DBA can find everything from one run with a single <code>LIKE 'qa-4812-%'</code>. Never share
          accounts between tests, even sequentially; “it passed locally” is usually shared-state luck.
        </p>
      </section>

      <section>
        <h2 id="rule-2-deterministic-seeds-and-factories">Rule 2: deterministic seeds and factories</h2>
        <p>
          Randomness inside a test should be seeded or it’s a future flake. If your factory uses Faker,
          seed it per run (<code>Faker.seed(42)</code>) so failures reproduce. Freeze anything time-shaped:
          inject a clock rather than reading wall time, so “invoice due next month” means the same thing in
          August and February.
        </p>
        <p>
          Prefer factories over committed fixtures. A checked-in <code>users.json</code> drifts silently
          from your schema until the night someone adds a <code>NOT NULL</code> column. Factories derive
          data from current code, so schema migrations break loudly at build time instead of softly at 2am.
          Keep seed scripts versioned alongside migrations — schema and baseline state move as one unit.
          For deeper background on why order-dependent suites rot, see our
          <a href="https://molar.it/blog/flaky-tests-causes-and-fixes">flaky-tests causes and fixes</a>.
        </p>
      </section>

      <section>
        <h2 id="rule-3-synthetic-identities-that-announce-themselves">Rule 3: synthetic identities that announce themselves</h2>
        <p>
          Test identities should be unmistakable at a glance and harmless in reality. Email plus-addressing
          gives you both properties: <code>qa+signup-4812@yourdomain.test</code> routes into one monitored
          inbox yet stays unique per flow and per run. Adopt a written policy for the rest:
        </p>
        <ul>
          <li><strong>Names:</strong> obviously fake (“QA BOT”, “Test Runner”), never realistic personas.</li>
          <li><strong>Phones:</strong> reserved/fictional ranges only, backed by an SMS clone — never a real SIM.</li>
          <li><strong>Emails/SMS delivery:</strong> disposable inboxes and cloned providers, not employee mailboxes. We show the OTP pattern for <a href="https://molar.it/blog/email-workflow-testing-otp">email verification flows</a> and <a href="https://molar.it/blog/test-twilio-sms-otp-flows">Twilio-style SMS codes</a>.</li>
          <li><strong>Never</strong> touch records belonging to real customers, domains, or employees — not even read-only “just to check.”</li>
        </ul>
        <p>
          Marked identities pay off downstream: support can spot them instantly, analytics filters exclude
          them by prefix, and incident triage never confuses a test storm with an outage.
        </p>
      </section>

      <section>
        <h2 id="rule-4-pii-minimization-and-masking">Rule 4: PII minimization and masking</h2>
        <p>
          Don’t copy production data into staging wholesale — it’s the fastest way to turn a test
          environment into an unmonitored data breach. Prefer generating synthetic data shaped like
          production (same cardinalities, same edge cases) over masking real people. When masking is
          unavoidable, do it deterministically: the same input always maps to the same surrogate, so joins,
          foreign keys, and dedup logic keep working.
        </p>
        <p>
          Extend hygiene to artifacts: traces, videos, and console logs capture whatever the page rendered.
          Redact at capture time for anything user-shaped, and treat screenshot storage like production
          storage, because it effectively is one.
        </p>
        <div>
          A frozen snapshot of production data is still processing personal data. GDPR/CCPA erasure requests
          don’t reach your staging bucket or old CI caches — which makes unmasked copies a compliance
          liability, not just a security one.
        </div>
      </section>

      <section>
        <h2 id="rule-5-cleanup-vs-snapshot-restore">Rule 5: cleanup vs snapshot/restore</h2>
        <p>
          Cleanup scripts are the polite fiction of test data management: under parallelism they race.
          Two runs deleting “their” rows by prefix will eventually collide on a foreign key, a unique
          index, or a cache. Deletion-based isolation scales poorly precisely when your suite gets fast
          enough for it to matter.
        </p>
        <p>
          The structural alternative is snapshot/restore: give each worker an ephemeral database, seed it
          once from a golden image (schema + baseline data), and let the run trash it freely. Restore is
          O(seconds), cleanup becomes irrelevant, and parallelism is bounded only by hardware. Costs to
          weigh: image size, image drift (rebuild the golden snapshot whenever migrations land). The
          pragmatic hybrid most teams converge on is an ephemeral database per worker, booted with
          migrations plus a small deterministic seed. Third-party side effects get the same treatment via
          service clones instead of shared vendor sandboxes — we compare the approaches in
          <a href="https://molar.it/blog/api-mocking-vs-service-clones">API mocking vs service clones</a>, and the wider
          environment question in <a href="https://molar.it/blog/staging-environment-alternatives">staging alternatives</a>.
        </p>
      </section>

      <section>
        <h2 id="the-six-rules-recapped">The six rules, recapped</h2>
        <ul>
          <li><strong>Unique-per-run identifiers</strong> for everything a test creates — structured, searchable prefixes.</li>
          <li><strong>Determinism everywhere:</strong> seeded generators, injected clocks, factories over frozen fixtures.</li>
          <li><strong>Synthetic marked identities:</strong> plus-addressed email, fictional phones, clone-backed OTP.</li>
          <li><strong>No raw production PII:</strong> synthesize where possible; mask deterministically when not.</li>
          <li><strong>Snapshot/restore over cleanup scripts</strong> once the suite runs in parallel.</li>
          <li><strong>Treat artifacts as data:</strong> redact traces and screenshots like the production systems they are.</li>
        </ul>
        <p>
          Many apparent flakes are shared-state failures: two tests sharing a row can look like timing bad luck.
        </p>]]></content:encoded>
    </item>
    <item>
      <title>Test Stripe checkout without charging a real card</title>
      <link>https://molar.it/blog/test-stripe-without-real-cards</link>
      <guid isPermaLink="true">https://molar.it/blog/test-stripe-without-real-cards</guid>
      <description>Check payment records, callbacks, and account access with an authorized test environment or supported clone.</description>
      <pubDate>Sat, 27 Jun 2026 00:00:00 GMT</pubDate>
      <category>Integrations</category>
      <content:encoded><![CDATA[<section><h2 id="use-test-mode-to-check-the-whole-payment-handoff">Use test mode to check the whole payment handoff.</h2><p>Stripe’s test environment lets you exercise payment flows without moving real money. Start with a test secret key and Stripe’s documented test payment methods; never paste a live secret key into a test job. A test card proves only the payment method path you selected. Your application still has to create the right customer or checkout session, handle the payment status, process the webhook, and grant access once.</p><p>Before writing the browser step, write down the state that should exist before and after it. For example: a new account has no Pro entitlement; checkout creates one payment; the callback marks the order paid; the account can open the Pro feature. That list makes it possible to tell a payment failure from an account-update failure.</p><p>Stripe’s <a href="https://docs.stripe.com/testing">testing guide</a> covers test values and the test environment. Molar’s <a href="https://docs.molar.it/docs/clones/vendors/stripe">Stripe Clone reference</a> covers the supported local paths when you need a resettable service instead.</p></section><figure class="article-graphic article-graphic--payment" data-article-graphic data-article-family="payment" aria-label="A payment callback changes access"><div class="article-graphic__canvas is-payment"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M154 126H301"/><path class="" d="M459 126H606"/><path class="is-flow" d="M154 126H301"/><path class="is-flow" d="M459 126H606"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card is-browser"><div class="article-graphic__eyebrow">BROWSER</div><h3>Payment confirmed</h3><p>The customer sees the checkout result.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">screen state</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">CALLBACK</div><h3>Payment succeeded</h3><p>The app receives and verifies the provider event.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">delivery can lag</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">APPLICATION</div><h3>Access granted</h3><p>Account permissions change only after the callback is applied.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">assert here</span></span></div></div></div><figcaption><strong>A payment callback changes access</strong><span>The browser can show success before the application has processed the provider callback.</span></figcaption></figure>
<section><h2 id="choose-stripe-test-mode-or-a-supported-clone">Choose Stripe test mode or a supported clone.</h2><p>Use Stripe test mode when you need compatibility with the provider’s own request and webhook contract. Use a supported Molar Clone when repeated local or CI runs need records you can inspect, controlled starting state, or supported failure cases without depending on a shared provider sandbox. Check the documented endpoints before choosing: a clone that covers PaymentIntents does not automatically cover every Billing, Connect, tax, dispute, or refund workflow.</p><p>In either setup, keep the application’s provider base URL and credentials explicit. Fail the test setup if the expected test variable is missing; silently falling back to a live account is a configuration bug, not a useful test result.</p><div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>const paymentIntent = await stripe.paymentIntents.create({
  amount: 4900,
  currency: 'usd',
  payment_method_types: ['card'],
  payment_method: 'pm_card_visa',
  confirm: true,
});

expect(paymentIntent.status).toBe('succeeded');</code></pre><p class="copy-status sr-only" role="status"></p></div><p>This Node example uses Stripe’s test payment method value. Adapt the client setup and assertion to the SDK version your project uses, and keep the secret in the test environment.</p></section>
<section><h2 id="run-one-clean-checkout-from-payment-to-access">Run one clean checkout from payment to access.</h2><ol><li><strong>Seed a disposable account.</strong> Create a customer with no paid entitlement and record its identifier for the test.</li><li><strong>Open checkout.</strong> Use the browser to select the plan and submit the test payment. Assert the amount, currency, and confirmation visible to the user.</li><li><strong>Capture the payment ID.</strong> Keep the PaymentIntent, Checkout Session, or order identifier your application uses to connect the browser action to the callback.</li><li><strong>Process the callback.</strong> Deliver the supported event through your test setup and verify signature validation. Do not bypass the webhook handler just to make the browser test green.</li><li><strong>Check the account.</strong> Assert that the intended feature is available once and that a second callback does not create a second entitlement.</li><li><strong>Reset and repeat.</strong> Restore the clone snapshot or recreate the provider test data, then run again from the same starting state.</li></ol><p>The browser confirmation and the payment record answer different questions. Keep both assertions so a page that says “paid” cannot hide a missing callback or an order that was never persisted.</p></section>
<section><h2 id="exercise-the-failures-that-change-customer-treatment">Exercise the failures that change customer treatment.</h2><p>Use documented Stripe test values or clone controls for a declined payment, a payment that needs additional action, and a delayed or repeated callback where your setup supports them. The expected product behavior should be explicit: a decline leaves the account unpaid and offers a retry; additional action leaves checkout pending until the customer completes it; a repeated event is ignored after the first successful update.</p><p>Subscriptions add time to the problem. Stripe <a href="https://docs.stripe.com/billing/testing">Billing testing</a> describes test clocks for advancing subscription state and observing resulting events. A clock-backed test still needs to check your webhook consumer, invoice handling, and account entitlement. It does not prove that every production billing path is covered.</p><div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>const event = stripe.webhooks.constructEvent(
  rawBody,
  signatureHeader,
  process.env.STRIPE_WEBHOOK_SECRET,
);

await handleStripeEvent(event);</code></pre><p class="copy-status sr-only" role="status"></p></div><p>Use the raw request body for signature verification in the framework configuration that receives your webhook. A parsed-and-re-serialized body can invalidate the signature before your handler runs.</p></section>
<section><h2 id="keep-the-failed-run-useful">Keep the failed run useful.</h2><p>When checkout fails, retain the browser step, payment identifier, callback status, and final account assertion together. A missing screenshot means capture is incomplete, not that checkout failed. A successful PaymentIntent with no entitlement calls for checking callback delivery and application state. A declined test payment calls for checking the expected failure handling. This separation saves a team from rerunning the same flow without changing the diagnosis.</p><p>Start Molar with one defined checkout and the integrations its current workflow supports. <a href="https://molar.it/products/clones">Explore Clones</a> for resettable service state, <a href="https://molar.it/products/guard">review Guard</a> for configured release checks, or <a href="https://molar.it/contact">talk to us</a>. Exporting runs as Playwright tests and blocking merges on them are in beta, so review the generated tests before you rely on them.</p></section>]]></content:encoded>
    </item>
    <item>
      <title>Test SMS verification with captured messages</title>
      <link>https://molar.it/blog/test-twilio-sms-otp-flows</link>
      <guid isPermaLink="true">https://molar.it/blog/test-twilio-sms-otp-flows</guid>
      <description>Configure a test SMS service, inspect the code, and check account verification, expiry, and retry behavior.</description>
      <pubDate>Wed, 22 Jul 2026 00:00:00 GMT</pubDate>
      <category>Integrations</category>
      <content:encoded><![CDATA[<section>
        <h2 id="why-real-sms-breaks-ci">Why real SMS breaks CI</h2>
        <p>
          Pointing your test suite at a live messaging provider fails in four predictable ways:
        </p>
        <ul>
          <li><strong>Cost.</strong> Every verification sends a billable message. A suite that runs 200 times a day across PRs turns your test budget into an invoice line item.</li>
          <li><strong>Latency.</strong> Real delivery takes seconds to minutes. A 30-second OTP wait inside a 90-second Playwright timeout is a coin flip.</li>
          <li><strong>Carrier filtering.</strong> Carriers aggressively filter A2P traffic to short-code ranges and test-looking numbers. Messages silently vanish; your test can’t distinguish “code not sent” from “code lost.”</li>
          <li><strong>Nondeterminism.</strong> Codes arrive out of order, retries collide, shared test numbers accumulate state from other runs. The result is exactly the kind of intermittent red described in our guide to <a href="https://molar.it/blog/flaky-tests-causes-and-fixes">flaky tests</a>.</li>
        </ul>
        <p>
          Some teams buy dedicated test phone numbers or negotiate sandbox access. Both still route through carrier
          networks, so latency and loss remain — you’ve paid for a faster coin flip, not determinism. Number pools
          shared across CI jobs introduce a subtler failure: two pipelines verifying against the same number
          concurrently, each consuming the other’s codes. The fix is removing the network from the loop entirely,
          which is what service clones are for.
        </p>
      </section><figure class="article-graphic article-graphic--otp" data-article-graphic data-article-family="otp" aria-label="An OTP is a lifecycle, not one input"><div class="article-graphic__canvas is-otp"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M95 126H245"/><path class="" d="M265 126H415"/><path class="" d="M435 126H585"/><path class="is-flow" d="M95 126H245"/><path class="is-flow" d="M265 126H415"/><path class="is-flow" d="M435 126H585"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">01 · ISSUE</div><h3>SMS code created</h3><p>Record the message and its issued-at time.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">captured</span></span></div><div class="article-graphic__card "><div class="article-graphic__eyebrow">02 · VALID CASE</div><h3>Code accepted</h3><p>Independent attempt: submit the fresh code before its validity window closes.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">account verified</span></span></div><div class="article-graphic__card "><div class="article-graphic__eyebrow">03 · EXPIRED CASE</div><h3>Old code rejected</h3><p>Independent attempt: submit the original code after its validity window.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">expiry asserted</span></span></div><div class="article-graphic__card "><div class="article-graphic__eyebrow">04 · RESEND CASE</div><h3>Fresh code only</h3><p>Independent attempt: request again and keep the original code invalid.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">retry asserted</span></span></div></div></div><figcaption><strong>An OTP is a lifecycle, not one input</strong><span>After one issuance, exercise valid, expired, and resend cases independently from a clean start.</span></figcaption></figure>

      <section>
        <h2 id="inspect-the-messages-captured-by-your-test-service">Inspect the messages captured by your test service</h2>
        <p>
          A Twilio clone implements the provider’s API contract — sending messages, looking them up and verification
          codes — but keeps every message in a local, inspectable store. Your application doesn’t change: it
          still POSTs to a base URL, still reads back status callbacks. Only the base URL points at the clone.
        </p>
        <p>Example to adapt: helper functions, accounts, and endpoints depend on your test project. Adapt this example to your project.</p><div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code># App code is unchanged except TWILIO_BASE_URL
curl -X POST "$CLONE/2010-04-01/Accounts/ACxxx/Messages.json" \
  -d To=+15550100042 -d From=+15550001111 \
  -d Body="Your code is 491722"

# The clone stores it deterministically — fetch it like the real API
curl "$CLONE/2010-04-01/Accounts/ACxxx/Messages.json?To=%2B15550100042"
# → {"messages":[{"body":"Your code is 491722","status":"delivered", ...}]}</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>
          Determinism is the point: for a given seed, the clone issues the same codes, in the same order, with the
          same timestamps. Your assertions stop guessing and start knowing. And because nothing leaves the machine,
          there’s no cost per run and no PII touching a third party — the same safety argument we make for
          <a href="https://molar.it/blog/testing-third-party-apis-safely">third-party APIs in general</a>.
        </p>
      </section>

      <section>
        <h2 id="test-what-happens-when-a-code-expires">Test what happens when a code expires</h2>
        <p>
          OTP expiry is where hand-rolled tests give up. Nobody wants to wait five minutes for a code to lapse, so
          expiry logic ships untested until it breaks. A clone with a virtual clock solves this cleanly: advance
          simulated time, then act.
        </p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>// Advance the clone's clock past expiry, then try the stale code
await clock.advance({ seconds: 301 });
await page.fill('#otp-input', codeFromClone);   // issued at t=0
await page.click('text=Verify');
await expect(page.getByText('Code expired')).toBeVisible();

// Request again → new code, old code rejected even if typed correctly
const fresh = await clone.getLatestCode('+15550100042');
expect(fresh).not.toBe(codeFromClone);</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>
          This covers the whole family of time bugs: resend throttles, retry lockouts after N attempts,
          “code valid for X minutes” copy drifting from server truth, and replay attempts. These are precisely
          the behaviors attackers probe, which makes them worth automating rather than spot-checking by hand.
        </p>
      </section>

      <section>
        <h2 id="check-delivery-callbacks-and-signatures">Check delivery callbacks and signatures</h2>
        <p>
          Most integrations don’t poll — they consume webhooks: delivery receipts, inbound replies (STOP/HELP),
          verification status events. If your clone only fakes the REST API, you’ve tested half the surface. Look
          for three things in any substitute:
        </p>
        <ul>
          <li><strong>Lifecycle parity.</strong> queued → sent → delivered transitions fire in order, with realistic timing you control.</li>
          <li><strong>Signature correctness.</strong> Callbacks carry valid request signatures so your verification middleware is exercised, not bypassed.</li>
          <li><strong>Inbound simulation.</strong> You can inject an inbound “STOP” reply and assert your suppression logic fires.</li>
        </ul>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>// Simulate an inbound STOP and verify opt-out handling
await clone.injectInboundSms({ from: '+15550100042', body: 'STOP' });
await expect.poll(() => clone.suppressionList())
  .toContain('+15550100042');</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>
          Webhook parity also changes what you can test about ordering. Real providers retry undelivered
          callbacks and can deliver events out of order under load; a clone lets you replay those conditions on
          demand instead of waiting for a bad day in production. Assert that a duplicate
          <code>verification.completed</code> event is absorbed idempotently, that a status callback for a message
          your app never sent is rejected, and that signature failures return 403 rather than 500 — small checks
          that each map to a real outage class.
        </p>
      </section>

      <section>
        <h2 id="connect-the-clone-to-your-test-runner">Connect the clone to your test runner</h2>
        <p>
          In practice the pattern is small: point your app’s provider credentials at the clone before boot, run
          your flows, pull codes out of the clone when the UI asks for them.
        </p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>// Playwright fixture
export async function otpFor(phone) {
  const msg = await clone.latestMessageTo(phone);
  return msg.body.match(/\b(\d{6})\b/)[1];
}
test('signup verifies by phone', async ({ page }) => {
  await signup(page, '+15550100042');
  await page.fill('#otp', await otpFor('+15550100042'));
  await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
});</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>The <a href="https://docs.molar.it/docs/clones/vendors/twilio">Molar Twilio clone reference</a> lists supported SMS paths and limits. Configure test numbers and endpoints, inspect the captured message, and verify the account state after the code is submitted. For adjacent steps, see <a href="https://molar.it/blog/email-workflow-testing-otp">email testing</a> and <a href="https://molar.it/blog/test-stripe-without-real-cards">payment testing</a>.</p>
        <div>
          Keep one smoke check against the live provider behind a manual trigger. Clones verify your logic;
          a weekly manual pass confirms the vendor hasn’t changed their contract underneath you.
        </div>]]></content:encoded>
    </item>
    <item>
      <title>Test the third-party services behind your app</title>
      <link>https://molar.it/blog/testing-third-party-apis-safely</link>
      <guid isPermaLink="true">https://molar.it/blog/testing-third-party-apis-safely</guid>
      <description>Choose test endpoints, isolate accounts and data, and inspect the callbacks that complete a customer task.</description>
      <pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate>
      <category>Integrations</category>
      <content:encoded><![CDATA[<section><h2 id="make-every-outbound-test-request-intentional">Make every outbound test request intentional.</h2><p>Payments, identity, email, SMS, file storage, analytics, and shipping can all sit behind one browser action. Before writing the test, draw the request path: which service receives the call, which account or bucket it uses, which callback returns, and which application record changes. Put the endpoint and credential for each dependency in test configuration that fails loudly when it is missing.</p><p>A test should never quietly fall back to a live account because a clone or provider sandbox is unavailable. Use separate environment variables, test credentials, and hostnames for test traffic. Add a startup check that prints safe endpoint names and refuses live-looking credentials in the test job.</p></section><figure class="article-graphic article-graphic--service" data-article-graphic data-article-family="service" aria-label="Test the boundary your app actually crosses"><div class="article-graphic__canvas is-service"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M154 126H301"/><path class="" d="M459 126H606"/><path class="is-flow" d="M154 126H301"/><path class="is-flow" d="M459 126H606"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">APPLICATION</div><h3>Vendor-shaped request</h3><p>Your app calls the same API contract used in the integration.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">base URL changes</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">ISOLATED SERVICE</div><h3>State you can inspect</h3><p>A clone or mock returns the behavior your scenario needs without shared live side effects.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">known response</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">ASSERTION</div><h3>Customer-visible result</h3><p>Check the browser state and the callback or record that caused it.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">contract + screen state</span></span></div></div></div><figcaption><strong>Test the boundary your app actually crosses</strong><span>Keep the vendor API request, isolate its state, and inspect the callback or response your application consumes.</span></figcaption></figure>
<section><h2 id="choose-a-provider-sandbox-mock-or-clone">Choose a provider sandbox, mock, or clone.</h2><p>A provider test environment is useful for checking compatibility with the provider’s own contract. A mock lets you return a selected response, inject a failure, or model a short sequence. A stateful clone can keep provider-shaped records that your application creates and later reads. Choose based on the behavior under test, then read the limits of that option.</p><p>Stripe’s <a href="https://docs.stripe.com/testing">testing documentation</a> explains its test values and sandbox behavior. WireMock’s <a href="https://wiremock.org/docs/stateful-behaviour/">stateful behavior guide</a> shows how scenarios can move from one response state to another. Molar’s <a href="https://docs.molar.it/docs/clones">clone catalog</a> lists the supported service paths and is the source to check before relying on a Molar-specific integration.</p></section>
<section><h2 id="give-each-run-separate-identities-and-records">Give each run separate identities and records.</h2><p>Create a disposable user, inbox, phone number, storage prefix, or provider customer for each isolated run where the service supports it. Parallel runs should not race for one verification code or delete each other’s object. Prefix records with a run identifier so a failure can be traced without exposing secrets in logs.</p><p>If the setup has snapshots or reset, capture a known state before the test and restore it afterward. Verify a representative record after reset. Cleanup is not the same as isolation: deleting the final order does not prove that a webhook queue, inbox, or provider-side object returned to its starting condition.</p></section>
<section><h2 id="test-the-callback-that-completes-the-work">Test the callback that completes the work.</h2><p>A successful outbound request often starts asynchronous work. Your app may be waiting for a signed payment event, delivery receipt, email, or file-processing status. Assert the incoming payload, signature validation, duplicate handling, and application state after the callback.</p><div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>cy.intercept('POST', '/api/orders').as('createOrder');
cy.get('[data-testid=buy]').click();
cy.wait('@createOrder').its('response.statusCode').should('eq', 201);

// Follow with an application-level assertion:
cy.contains('Order confirmed').should('be.visible');</code></pre><p class="copy-status sr-only" role="status"></p></div><p>This Cypress example observes the application request; it does not prove that a third-party callback arrived. Add the provider or clone step your workflow supports, and keep the callback test separate when the browser cannot observe it directly.</p></section>
<section><h2 id="exercise-delay-duplication-and-rejection">Exercise delay, duplication, and rejection.</h2><p>Choose failure cases that change customer treatment: a timeout, a 429, a 5xx, an invalid signature, a duplicate event, an out-of-order event, or a missing object. Assert the intended pending, retry, rejection, or recovery state. Do not claim that a generic mock tests provider behavior unless its payloads, headers, and signing rules match the contract you depend on.</p><p>Keep a small compatibility check against the provider’s authorized test environment. Run it when you update API versions or integration code. A clone can make your application logic repeatable; it cannot establish that an undocumented provider edge case behaves the same way.</p></section>
<section><h2 id="inspect-a-complete-customer-task-before-expanding">Inspect a complete customer task before expanding.</h2><p>For an invitation, check that the admin creates the invitation, the intended inbox receives the link, the recipient accepts it, and the new account receives the right membership. Keep browser steps and downstream records linked to the same run.</p><p>The <a href="https://molar.it/examples/team-invitation">Molar invitation example</a> is a controlled scripted test with email and identity clones. For your app, start with one integration and make its endpoint, account, callback, reset, and failure behavior explicit. Then <a href="https://molar.it/contact">talk to us</a> if the supported path matches the task you need to protect.</p></section><section><h2 id="keep-a-contract-check-beside-the-repeatable-integration-test">Keep a contract check beside the repeatable integration test.</h2><p>When you change an SDK, provider API version, or webhook schema, run a small compatibility check against the provider’s authorized test environment. Verify the request method, required headers, response fields, signing format, and error codes your application actually consumes. Store the expected contract in code or fixtures so a review can see what changed.</p><p>Then run the larger scenario against a mock or clone from a known state. If the two checks disagree, first ask whether the substitute is stale or whether the application relied on an undocumented provider behavior. The split keeps repeatable local feedback fast while preserving one direct signal that the integration still matches its external contract.</p></section>]]></content:encoded>
    </item>
    <item>
      <title>Review visual changes without losing functional checks</title>
      <link>https://molar.it/blog/visual-regression-testing-guide</link>
      <guid isPermaLink="true">https://molar.it/blog/visual-regression-testing-guide</guid>
      <description>Set useful screenshot scopes and baselines, control rendering variation, and review the behavior behind the page.</description>
      <pubDate>Tue, 16 Jun 2026 00:00:00 GMT</pubDate>
      <category>Engineering</category>
      <content:encoded><![CDATA[<section>
        <h2 id="pair-visual-checks-with-functional-assertions">Pair visual checks with functional assertions</h2>
        <p>
          Start with functional coverage: does signup work, is the total correct, did the row get created.
          Semantic assertions are cheap to maintain and directly encode product intent. Visual checks earn
          their place only after that layer exists — they catch the regressions semantics can’t see:
          broken layouts, overlapping text, missing icons, collapsed sidebars.
        </p>
        <div>
          A passing screenshot asserts nothing about behavior. Pixel-perfect pages can still compute the
          wrong total, drop the webhook, or save an empty form. Visual checks complement oracles; they are
          never a replacement for them.
        </div>
        <p>When reviewing a new screenshot baseline, also check the behavior the screen represents. A checkout can look correct while calculating the wrong amount. Generated tests need the same combination of reviewed functional and visual assertions as handwritten tests.</p>
      </section><figure class="article-graphic article-graphic--release" data-article-graphic data-article-family="release" aria-label="A release gate narrows the decision"><div class="article-graphic__canvas is-release"><svg class="article-graphic__route " viewBox="0 0 760 250" preserveAspectRatio="none" aria-hidden="true"><g><path class="" d="M154 126H301"/><path class="" d="M459 126H606"/><path class="" d="M380 126V67H606"/><path class="is-flow" d="M154 126H301"/><path class="is-flow" d="M459 126H606"/><path class="is-flow" d="M380 126V67H606"/></g></svg><div class="article-graphic__nodes"><div class="article-graphic__card "><div class="article-graphic__eyebrow">CHANGE</div><h3>Pull request</h3><p>Start with the paths affected by the change.</p><span class="article-graphic__state"><span class="article-graphic__tag is-quiet">scope the run</span></span></div><div class="article-graphic__card is-event"><div class="article-graphic__eyebrow">CHECKS</div><h3>Signup · checkout · access</h3><p>Each scenario reports its own assertion and captured route.</p><span class="article-graphic__state"><span class="article-graphic__tag is-coral">one blocked</span></span></div><div class="article-graphic__card is-state"><div class="article-graphic__eyebrow">DECISION</div><h3>Ready or blocked</h3><p>A reviewer sees why the release can proceed or what needs attention.</p><span class="article-graphic__state"><span class="article-graphic__tag is-green">reviewable result</span></span></div></div></div><figcaption><strong>A release gate narrows the decision</strong><span>Run the checks that match the change, attach the failed route, and leave the merge decision reviewable.</span></figcaption></figure>

      <section>
        <h2 id="scope-full-page-vs-component-snapshots">Scope: full-page vs component snapshots</h2>
        <p>
          Full-page snapshots maximize recall and minimize signal-to-noise: one stray “Recommended for you”
          banner fails every shot it appears in, and baseline storage balloons into tens of megabytes per
          screen. Component-scoped (element) snapshots invert the tradeoff — tight blast radius, small
          diffs, but they only see what you aimed at.
        </p>
        <p>
          The pragmatic split most teams land on: element-level shots for design-system components (buttons,
          cards, forms, empty states), where changes should be intentional and reviewable; full-page shots
          reserved for a handful of money screens — landing, pricing, checkout — reviewed on a slower cadence.
          Name snapshots by surface and state (<code>checkout/error-state.png</code>), not by test id, so a
          failing diff tells you where to look before you open anything.
        </p>
      </section>

      <section>
        <h2 id="anti-flaking-fonts-animations-viewport-locks">Anti-flaking: fonts, animations, viewport locks</h2>
        <p>Control common sources of visual variation: late-loading fonts, animations, blinking carets, clocks, and changing remote images. Set a consistent viewport for each comparison. Choose additional viewports based on the layouts your customers use, rather than treating one desktop size as complete coverage.</p>
        <div class="code-window article-code"><div class="article-code-bar"><span>Code example</span><button type="button" class="copy-button" data-copy><svg class="icon " width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg><span>Copy</span></button></div><pre><code>// playwright.config.ts
use: { viewport: { width: 1440, height: 900 } };

// dashboard.spec.ts
await page.evaluate(() => document.fonts.ready);
await page.clock.install({ time: new Date('2026-06-16T10:00:00Z') }); // freeze time

await expect(page).toHaveScreenshot('dashboard.png', {
  animations: 'disabled',
  caret: 'hide',
  mask: [page.getByTestId('live-ticker')], // dynamic regions opt out
  maxDiffPixelRatio: 0.01,
});</code></pre><p class="copy-status sr-only" role="status"></p></div>
        <p>
          Stub the network for anything non-deterministic rather than masking blindly — a masked region is a
          blind spot forever, while a stubbed response is still rendered and reviewed. More general
          de-flaking technique lives in our <a href="https://molar.it/blog/flaky-tests-causes-and-fixes">flaky-tests root
          causes guide</a>.
        </p>
      </section>

      <section>
        <h2 id="thresholds-that-respect-antialiasing">Thresholds that respect antialiasing</h2>
        <p>
          A zero-tolerance threshold will eventually fail because two CI runners rasterized a font two
          subpixels apart. Cross-machine antialiasing variance is a fact of life; budget for it. Set thresholds empirically per surface: full-page shots need tolerance for known rendering variance, while
          component shots can run tighter. Start from a recorded baseline and adjust only after reviewing real diffs.
        </p>
        <p>
          Two thresholds beat one: a hard limit that fails the build, and a softer watch level that flags
          drift for review without blocking. Watch for distribution shifts too — many small diffs across a
          page usually mean a layout shift or font substitution, which no pixel ratio correctly prices. And
          if your team ships across browsers, expect WebKit to be the pickiest renderer; budget its
          thresholds separately rather than averaging.
        </p>
      </section>

      <section>
        <h2 id="review-baseline-changes-alongside-the-code">Review baseline changes alongside the code</h2>
        <p>
          Baseline images are code. Treat their changes like code: updating a baseline happens through a
          dedicated command (<code>--update-snapshots</code> locally), lands as a PR, and requires a human
          who can articulate why the pixels changed — “marketing header redesign,” not “it looked
          different.” If nobody can explain a diff, that’s exactly when you want to stop the line.
        </p>
        <p>Show expected, actual, and difference images together during review. Store or retain baselines where the team can associate them with the relevant code change. Require an explanation before accepting a changed baseline, and keep the functional assertions that verify what the page does.</p>
      </section>

      <section>
        <h2 id="a-pragmatic-workflow">A pragmatic workflow</h2>
        <ul>
          <li><strong>Step 1.</strong> Functional/semantic suite green first. No screenshots until intent is asserted.</li>
          <li><strong>Step 2.</strong> Lock determinism: viewport 1440×900, fonts awaited, animations disabled, clocks frozen, network stubbed.</li>
          <li><strong>Step 3.</strong> Element shots for design-system components, tight thresholds.</li>
          <li><strong>Step 4.</strong> Full-page shots for money screens, looser thresholds, PR-gated.</li>
          <li><strong>Step 5.</strong> Review every baseline change like a code change; never auto-update.</li>
          <li><strong>Step 6.</strong> Fold visual checks into your broader cadence per our <a href="https://molar.it/blog/regression-testing-strategies">regression testing strategies</a>.</li>
        </ul>
        <p>
          Built this way, visual regression adds the layer it’s uniquely good at — catching what assertions
          miss — without taxing every merge with noise.
        </p>]]></content:encoded>
    </item>
  </channel>
</rss>
