<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: TestDino</title>
    <description>The latest articles on DEV Community by TestDino (@testdino01).</description>
    <link>https://dev.to/testdino01</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3387297%2F3dcba2ef-c4e5-4d5f-9673-02bfbd96a9a5.jpeg</url>
      <title>DEV Community: TestDino</title>
      <link>https://dev.to/testdino01</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/testdino01"/>
    <language>en</language>
    <item>
      <title>What to Check When Your Playwright Test Passes Locally But Fails in CI</title>
      <dc:creator>TestDino</dc:creator>
      <pubDate>Thu, 25 Jun 2026 10:46:05 +0000</pubDate>
      <link>https://dev.to/testdino01/what-to-check-when-your-playwright-test-passes-locally-but-fails-in-ci-1hj6</link>
      <guid>https://dev.to/testdino01/what-to-check-when-your-playwright-test-passes-locally-but-fails-in-ci-1hj6</guid>
      <description>&lt;p&gt;You pushed the test. Passed locally 10 times clean. CI runs it. Fails.&lt;/p&gt;

&lt;p&gt;You open the report. Screenshot is blank. Stack trace points to a selector you know exists.&lt;/p&gt;

&lt;p&gt;You add a &lt;code&gt;waitForTimeout(2000)&lt;/code&gt;. Push again. Passes.&lt;br&gt;
Next week, same test. Same blank screenshot. Different error message.&lt;/p&gt;

&lt;p&gt;You are not fixing the test. You are guessing until it stops failing temporarily.&lt;/p&gt;

&lt;p&gt;Here is what is actually happening and how to get out of it.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Does a Playwright Test Pass Locally But Fail in CI?
&lt;/h2&gt;

&lt;p&gt;Your test is probably not broken. The CI environment is the variable nobody talks about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On your machine:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One test runs. Full CPU. Full network. Nothing competing.&lt;/li&gt;
&lt;li&gt;Page loads in under a second. Images render. API responds in under 200ms.&lt;/li&gt;
&lt;li&gt;Screenshot captures a fully loaded state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;In CI with parallel tests running:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;CPU is split across all parallel processes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Network bandwidth is shared across all shards&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;API calls queue behind dozens of others hitting the same endpoint&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Page starts loading, resources get starved halfway through, rendering stops&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your selector checks for an element that never finished rendering&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Screenshot captures a blank or half-loaded state&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your selector was never wrong. The environment killed the render before the element existed.&lt;/p&gt;

&lt;p&gt;But the report says &lt;code&gt;element not found&lt;/code&gt;. So you go fix the selector.&lt;/p&gt;

&lt;p&gt;That is the loop.&lt;/p&gt;


&lt;h2&gt;
  
  
  What Does a Blank Playwright Screenshot in CI Actually Mean?
&lt;/h2&gt;

&lt;p&gt;Not "test failed." One of these four specific things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Page timed out before full render&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Network got thin mid-load. Playwright captured the loading state. The element you checked for was never painted onto the screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You see:&lt;/strong&gt; blank screenshot&lt;br&gt;
&lt;strong&gt;What happened:&lt;/strong&gt; resource contention during page load, not a selector problem&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Image format changed and the CDN did not catch up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your main branch now serves WebP or AVIF optimized images. Staging CDN does not support that format yet. Image request returns 404 silently or hangs. Placeholder stays. Your selector looks for the real rendered image. Fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You see:&lt;/strong&gt; &lt;code&gt;element not found: img.product-image&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;What happened:&lt;/strong&gt; image format change on main branch broke the CDN request in staging&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. API timeout cascade&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Parallel tests hammer the same endpoint. Your call joins the queue. Times out. Page never finishes rendering. Screenshot captures whatever loaded before timeout fired.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You see:&lt;/strong&gt; blank or partial screenshot&lt;br&gt;
&lt;strong&gt;What happened:&lt;/strong&gt; API queue filled up under parallel load, your request died in it&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Resource starvation from parallel execution&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;OS deprioritized your page load because 99 other processes were competing. Load that takes under a second locally takes 3 to 4 seconds in parallel CI. Playwright timeout fires before the page finishes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You see:&lt;/strong&gt; blank screenshot&lt;br&gt;
&lt;strong&gt;What happened:&lt;/strong&gt; physics, not your test code&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Why Can't You Tell Which One It Is From the Report?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Standard Playwright reporter shows you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Assertion failed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Blank screenshot&lt;/li&gt;
&lt;li&gt;Stack trace pointing at the selector line&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not show you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which network requests failed or timed out&lt;/li&gt;
&lt;li&gt;When your selector ran relative to when the API responded&lt;/li&gt;
&lt;li&gt;How many tests were running at the same time&lt;/li&gt;
&lt;li&gt;Whether the page was mid-render or fully loaded when failure fired&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So you have a failure with no context. You guess.&lt;/p&gt;

&lt;p&gt;Most QAs guess timing and add a wait. But it was a CDN or environment issue. The wait sometimes helps by coincidence. The real problem stays. It comes back next week with a slightly different error and you start the loop again.&lt;/p&gt;


&lt;h2&gt;
  
  
  How Do You Actually Debug a Playwright CI Failure?
&lt;/h2&gt;

&lt;p&gt;Here is the step-by-step process to stop guessing and start knowing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Check the network tab before you touch your test&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before you change a selector or add a wait, get the network data from that CI run.&lt;/p&gt;

&lt;p&gt;First, make sure you are capturing traces. If &lt;code&gt;trace: 'on-first-retry'&lt;/code&gt; is not in your config, you have zero network data on failures.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// playwright.config.ts&lt;/span&gt;
&lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;on-first-retry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;only-on-failure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;retain-on-failure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://testdino.com/blog/playwright-trace-viewer" rel="noopener noreferrer"&gt;Playwright Trace Viewer&lt;/a&gt; &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you have trace data, open Trace Viewer and go to the Network tab. Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any request with a 404 or 5xx status&lt;/li&gt;
&lt;li&gt;Any request that shows no response at all (started, then nothing)&lt;/li&gt;
&lt;li&gt;Image requests specifically if the failure involves a missing image element&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a network request failed before your selector ran: your test is innocent. Stop there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Check the timeline, not just the error message&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The order of events matters more than what the error says.&lt;/p&gt;

&lt;p&gt;You want to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When did the page load finish?&lt;/li&gt;
&lt;li&gt;When did the API call complete?&lt;/li&gt;
&lt;li&gt;When did your selector check run?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your selector ran at 2100ms and the API responded at 2200ms, that is a timing issue. The element existed, your check ran 100ms too early.&lt;/p&gt;

&lt;p&gt;If the API never responded at all, that is an environment issue. Not your test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Classify the failure type before writing any fix&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are three kinds of CI failures and each needs a different response:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real test failure&lt;/strong&gt;&lt;br&gt;
Same error, fails consistently on every run, every environment.&lt;br&gt;
What you do: fix your test code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment failure&lt;/strong&gt;&lt;br&gt;
Passes when running alone, fails under parallel load. Network timeouts, resource starvation.&lt;br&gt;
What you do: fix the environment config or escalate to ops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timing failure&lt;/strong&gt;&lt;br&gt;
Passes after you add a wait, was checking before the element existed.&lt;br&gt;
What you do: replace waitForTimeout with waitForSelector or waitForResponse.&lt;/p&gt;

&lt;p&gt;When you add &lt;code&gt;waitForTimeout&lt;/code&gt; to everything without classifying first, you cover up environment failures instead of surfacing them. The suite fills up with arbitrary waits and inflated timeouts. It gets slower, more fragile, and the real problems stay invisible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Replace static waits with explicit waits&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once you confirm it is a timing issue and not a network issue, fix it properly.&lt;/p&gt;

&lt;p&gt;Replace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// this is a guess&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or for a specific element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForSelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;img.product-image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Static waits hide the real problem. &lt;code&gt;waitForResponse&lt;/code&gt; and &lt;code&gt;waitForSelector&lt;/code&gt; make timing explicit and debuggable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://playwright.dev/docs/api/class-page#page-wait-for-response" rel="noopener noreferrer"&gt;Playwright waitForResponse docs&lt;/a&gt;&lt;br&gt;
&lt;a href="https://playwright.dev/docs/api/class-page#page-wait-for-selector" rel="noopener noreferrer"&gt;Playwright waitForSelector docs&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  How Does the Image Optimization Problem Break CI in 2026?
&lt;/h2&gt;

&lt;p&gt;This one trips up QA teams specifically because it looks exactly like a selector problem and the fix looks obvious but is completely wrong.&lt;/p&gt;

&lt;p&gt;Your main branch started serving WebP and AVIF images for performance optimization. Your staging CDN config has not been updated in months. It does not support AVIF. Image requests either return 404 silently or hang until timeout.&lt;/p&gt;

&lt;p&gt;Here is the actual sequence when your test hits CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0ms    -- Page starts loading
400ms  -- Image request fires (AVIF format from main branch)
2000ms -- Main page HTML content loads fine
3000ms -- Image CDN timeout threshold hit. No response from CDN.
3100ms -- Your selector checks for img.product-image
3200ms -- Selector fails. Image element never rendered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Report says:&lt;/strong&gt; element not found: img.product-image&lt;br&gt;
&lt;strong&gt;Screenshot shows:&lt;/strong&gt; page looks loaded, image area is a blank placeholder&lt;br&gt;
&lt;strong&gt;You think:&lt;/strong&gt; wrong selector, or needs a wait&lt;br&gt;
&lt;strong&gt;What actually happened:&lt;/strong&gt; CDN does not support AVIF in staging, element never existed&lt;/p&gt;

&lt;p&gt;Quick check to confirm this in your CI environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;url &lt;span class="nt"&gt;-I&lt;/span&gt; https://your-staging-cdn.com/images/product.avif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns &lt;code&gt;404&lt;/code&gt;: your blank screenshot is explained. Fix the CDN config. Do not touch your test.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Does Parallel Execution Make This Worse?
&lt;/h2&gt;

&lt;p&gt;Because it is the default now and most reports do not reflect it.&lt;/p&gt;

&lt;p&gt;GitHub Actions, GitLab CI, and CircleCI all shard and parallelize by default. Running 100 tests in parallel on a runner with 2 CPUs means you have already created resource starvation.&lt;/p&gt;

&lt;p&gt;What this creates:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CPU contention:&lt;/strong&gt; Your test alone runs in under a second. With 99 others competing on a 2-CPU runner, it runs in 3 to 4 seconds. Same test. Different resources. Nothing wrong with your code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API queue buildup:&lt;/strong&gt; Your endpoint responds in 150ms under normal load. Under 100 parallel requests, it responds in 1800ms or times out entirely. Same API. Different load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CDN cache miss storm:&lt;/strong&gt; Locally, your browser has the images cached. In parallel CI, 100 tests hit the CDN simultaneously with cold caches. Some requests hit the raw origin. Some time out.&lt;/p&gt;

&lt;p&gt;A test that passes running alone but fails with 99 others is not a flaky test. It is a resource allocation problem.&lt;/p&gt;

&lt;p&gt;Check this in your CI config:&lt;/p&gt;

&lt;p&gt;`# In your GitHub Actions workflow&lt;/p&gt;

&lt;h1&gt;
  
  
  If you have a 2-CPU runner, 4 shards is reasonable. 100 is not.
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;run: npx playwright test --shard=1/4`&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More shards than your runner can support is the problem. More waits will not fix that.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Should a CI Failure Report Actually Show You?
&lt;/h2&gt;

&lt;p&gt;Here is what good failure visibility looks like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visual state at failure:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Screenshot at the exact moment of failure&lt;/li&gt;
&lt;li&gt;What was rendered and what was not&lt;/li&gt;
&lt;li&gt;Was it a blank page, a partial load, or a fully loaded page with one missing element&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Network state at failure:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which requests completed with 200&lt;/li&gt;
&lt;li&gt;Which returned 4xx or 5xx&lt;/li&gt;
&lt;li&gt;Which timed out and at exactly what timestamp&lt;/li&gt;
&lt;li&gt;Full sequence so you can see what failed first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Timing:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When did page load complete&lt;/li&gt;
&lt;li&gt;When did each API call complete&lt;/li&gt;
&lt;li&gt;When did your selector check run&lt;/li&gt;
&lt;li&gt;Gap between last network event and selector check&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Environment context:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many tests were running at the same time&lt;/li&gt;
&lt;li&gt;Whether resources were under pressure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you have all four, you go from blind guessing to "image CDN request timed out at 3000ms, selector ran at 3100ms, element never rendered, not a test issue" in under two minutes.&lt;/p&gt;

&lt;p&gt;Without all four, you are doing forensics manually every time.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is the Complete Process to Follow on Every CI Failure?
&lt;/h2&gt;

&lt;p&gt;Follow these in order. Do not skip to your test code first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Open trace. Go to the Network tab.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Found a 404 or 5xx? Image or API infrastructure issue. Your test is not the problem. Check CDN config or escalate to ops.&lt;/li&gt;
&lt;li&gt;Found a request with no response at all? If it is an image request, check your CDN supports current image formats. If it is an API request, you are hitting a rate limit or queue issue under parallel load. Not your test.&lt;/li&gt;
&lt;li&gt;All requests returned 200? Move to step 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Check the timing in the trace timeline.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Selector ran before the API completed? Use &lt;code&gt;waitForResponse&lt;/code&gt; instead of &lt;code&gt;waitForTimeout&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Selector ran after everything completed and element still not found? Now check your selector. This is the one case where your test code is actually the issue.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Check if the failure is consistent or only under parallel load.&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Fails consistently on every run? Real test bug. Fix the selector or assertion.&lt;/li&gt;
&lt;li&gt;Only fails under parallel load? Resource starvation. Check your shard count against runner CPU count. Reduce shards or increase runner size.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Network first. Timing second. Selector third. That order is everything.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Does Getting the Right Visibility Change Everything?
&lt;/h2&gt;

&lt;p&gt;The QA teams that stopped spending hours on every CI failure did not find better selectors or learn to write better waits. They got the right information in front of them when a failure happened.&lt;/p&gt;

&lt;p&gt;Once you can see network requests, timing, and visual state together in one report:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You stop adjusting selectors that were never wrong&lt;/li&gt;
&lt;li&gt;You stop adding waits for problems that were never timing issues&lt;/li&gt;
&lt;li&gt;You surface CDN and infra problems to the right people instead of absorbing them as test flakiness&lt;/li&gt;
&lt;li&gt;You spend 2 minutes on a failure instead of 45&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your tests do not need to get better first. Your visibility does.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Blank screenshot in CI = environment problem until the network tab proves otherwise&lt;/li&gt;
&lt;li&gt;Turn on trace: 'on-first-retry' in playwright.config.ts or you are debugging blind&lt;/li&gt;
&lt;li&gt;Check network requests before touching your selector&lt;/li&gt;
&lt;li&gt;Check timing before adding a wait&lt;/li&gt;
&lt;li&gt;WebP or AVIF image optimization on main branch causes "element not found" that looks like a selector issue but is a CDN config problem&lt;/li&gt;
&lt;li&gt;100 shards on a 2-CPU runner is resource starvation, not flaky tests&lt;/li&gt;
&lt;li&gt;The fix order is: network first, timing second, test code last&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your test is probably fine. Your visibility is the gap.&lt;/p&gt;




&lt;p&gt;You do your job right. You catch things. You dig in when everyone else moves on.&lt;/p&gt;

&lt;p&gt;The problem is that most reports stop at "it failed" and leave you doing the detective work alone.&lt;/p&gt;

&lt;p&gt;You follow the process, you surface what broke, you fix the right thing. We are here to make sure that process takes two minutes instead of two hours. The hard work is yours.&lt;strong&gt;The visibility is on us.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>ci</category>
      <category>qa</category>
    </item>
    <item>
      <title>TestDino Public API: One Call for Errors, Coverage, Specs, and Traces. Here Is What That Enables.</title>
      <dc:creator>TestDino</dc:creator>
      <pubDate>Mon, 01 Jun 2026 09:18:37 +0000</pubDate>
      <link>https://dev.to/testdino01/testdino-public-api-one-call-for-errors-coverage-specs-and-traces-here-is-what-that-enables-4opb</link>
      <guid>https://dev.to/testdino01/testdino-public-api-one-call-for-errors-coverage-specs-and-traces-here-is-what-that-enables-4opb</guid>
      <description>&lt;p&gt;If you have ever tried to get Playwright test data out of a reporting tool programmatically, you know the problem. You call the list endpoint, get statuses, then make a separate call per run for errors, another for coverage, another for spec breakdown. By the time you have enough data to post something useful to a PR or a CI pipeline export, you have chained four requests and written more glue code than the feature itself.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.testdino.com/api-reference/overview" rel="noopener noreferrer"&gt;TestDino Public API&lt;/a&gt; handles this differently. One parameter changes what you get back from a single run detail call. That difference is what makes building a Playwright reporting API integration actually worth the time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; TestDino is Playwright-only. Cypress, Vitest, Jest: not supported.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The &lt;code&gt;?include=&lt;/code&gt; Parameter&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Default call to a run detail endpoint returns run metadata: status, duration, branch, environment, test counts. Lean and fast.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;?include=errors,coverage,specs,artifacts&lt;/code&gt; and the same request returns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;errors:&lt;/code&gt; failures grouped by error message with stack traces&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;coverage:&lt;/code&gt; Istanbul code coverage metrics for that run&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;specs:&lt;/code&gt; spec files with flattened test cases and individual status per test&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;artifacts:&lt;/code&gt; authenticated download URLs for Playwright traces, screenshots, videos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;One call. No chaining.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /{projectId}/test-runs/{runId} include=errors,coverage,specs,artifacts
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;?include=&lt;/code&gt; parameter only works on two endpoints. &lt;code&gt;GET /{projectId}/test-runs/{runId}&lt;/code&gt; accepts &lt;code&gt;errors&lt;/code&gt;, &lt;code&gt;coverage&lt;/code&gt;,&lt;code&gt;specs&lt;/code&gt;,&lt;code&gt;artifacts&lt;/code&gt;, &lt;code&gt;all&lt;/code&gt;. &lt;code&gt;GET /{projectId}/test-cases/{caseId}&lt;/code&gt; accepts &lt;code&gt;history&lt;/code&gt;, &lt;code&gt;all&lt;/code&gt;. List endpoints, analytics, and dashboard endpoints do not support it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Endpoint List
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Base URL:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://api.testdino.com/api/public/v1/{projectId}/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auth:&lt;/strong&gt; Bearer token with &lt;code&gt;tdp_&lt;/code&gt; prefix, &lt;code&gt;public-api&lt;/code&gt; scope. &lt;a href="https://docs.testdino.com/guides/generate-api-keys" rel="noopener noreferrer"&gt;Generate from Project Settings&lt;/a&gt;. Token is scoped to one project. Project A token returns 403 on project B.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limits:&lt;/strong&gt; 100 req/min per token, 200/min per IP. Every response includes &lt;code&gt;RateLimit-Remaining&lt;/code&gt; and &lt;code&gt;RateLimit-Reset&lt;/code&gt;. PDF generation is 1/min per token.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET  /token-info                 # verify PAT, read project and org metadata
GET  /test-runs                  # list runs, filter by branch/env/developer/tag/date
GET  /test-runs/{runId}          # run detail + ?include=errors,coverage,specs,artifacts
GET  /test-cases/{caseId}        # test case detail + ?include=history
GET  /specs                      # spec file health: pass rate, flakiness score, duration
GET  /test-case-explorer         # aggregated metrics across runs
GET  /analytics/summary          # summary metrics and trend data
GET  /dashboard                  # project health overview
GET  /manual-tests               # manual suites, cases, steps
POST /reports/pdf                # generate a PDF report
GET  /filters                    # environments, branches, developers, tags in a project
GET  /usage                      # subscription usage and allocation
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full auth and error handling: d&lt;a href="https://docs.testdino.com/api-reference/conventions" rel="noopener noreferrer"&gt;ocs.testdino.com/api-reference/conventions&lt;/a&gt;&lt;br&gt;
Five-minute quickstart: &lt;a href="https://docs.testdino.com/api-reference/quickstart" rel="noopener noreferrer"&gt;docs.testdino.com/api-reference/quickstart&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  What You Can Actually Build
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. GitHub PR Annotation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pain before:&lt;/strong&gt; Reviewers merging without knowing if tests passed. The "can you check" comment in every PR thread.&lt;/p&gt;

&lt;p&gt;After CI finishes, a workflow step calls the latest run for that branch, pulls pass rate and top failed test names with errors via ?&lt;code&gt;include=errors&lt;/code&gt;, posts a PR comment. Reviewer sees test status without leaving GitHub.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /test-runs?branch=feature/my-branch&amp;amp;limit=1
GET /test-runs/{runId}?include=errors
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;2. Nightly Digest Email&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pain before:&lt;/strong&gt; Engineering leadership has no visibility into test health trends unless someone manually checks and reports.&lt;/p&gt;

&lt;p&gt;A scheduled script paginates through the previous day's runs, aggregates pass rate and failure counts per branch, grabs the five most flaky spec files via &lt;code&gt;GET /specs&lt;/code&gt;, formats a plain text email, sends it. Director reads it before standup. Automated. Nobody had to remember.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;3. Client-Facing Status Page&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pain before:&lt;/strong&gt; Clients emailing "what is the current test status" weekly.&lt;/p&gt;

&lt;p&gt;A script pulls the latest run and writes pass rate, timestamp, and branch to a Notion database or simple webpage. Clients check it themselves. No TestDino account required on their end. Those messages stop.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;4. Flakiness Heat Map&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pain before:&lt;/strong&gt; Knowing a suite is getting unstable but not knowing which files are pulling the numbers down.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GET /specs&lt;/code&gt; returns per-file flakiness scores across runs. &lt;code&gt;GET /test-cases/{caseId}?include=history&amp;amp;historyStatus=flaky&lt;/code&gt; gives per-test flaky history. Combine them into a weekly view that surfaces exactly which files and tests are degrading. Teams that build this catch the problem before it becomes a standup conversation.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;5. MCP: Querying Test Data from an AI Tool&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.testdino.com/mcp/overview" rel="noopener noreferrer"&gt;TestDino MCP server&lt;/a&gt; is a separate access path from the REST API. Two versions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A. Local MCP&lt;/strong&gt; Install via npx, connect Cursor, Claude Code, or Claude Desktop. The IDE tool queries live run data directly during your session.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx @testdino/mcp&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.testdino.com/mcp/local" rel="noopener noreferrer"&gt;Local MCP setup&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;B. Remote MCP&lt;/strong&gt; Hosted at mcp.testdino.com, connects ChatGPT, Claude on the web, and other remote-MCP-capable tools. No local install.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.testdino.com/mcp/remote" rel="noopener noreferrer"&gt;Remote MCP setup&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pain before:&lt;/strong&gt; Copying stack traces into an AI prompt and hoping it reasons correctly from pasted context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The practical shift with MCP:&lt;/strong&gt; You ask "what error pattern appeared most in staging this week" and the assistant queries actual run data to answer. Not a guess. Not training data. Your data.&lt;/p&gt;

&lt;p&gt;Based on the &lt;a href="https://docs.testdino.com/testdino-alternatives" rel="noopener noreferrer"&gt;public alternatives comparison&lt;/a&gt;, no other Playwright reporting tool has a remote MCP server.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Access and Availability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The API and MCP are included on every plan. There is no separate API tier, no add-on, and no unlock. You can start on the free plan and make your first API call today without a credit card.&lt;/p&gt;

&lt;p&gt;Most alternatives in this space gate their API behind paid plans or charge significantly more for equivalent access. Full breakdown on the &lt;a href="https://docs.testdino.com/testdino-alternatives" rel="noopener noreferrer"&gt;alternatives comparison page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://testdino.com/" rel="noopener noreferrer"&gt;Get started&lt;/a&gt; | &lt;a href="https://testdino.com/pricing" rel="noopener noreferrer"&gt;Pricing&lt;/a&gt;&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>api</category>
      <category>qa</category>
    </item>
    <item>
      <title>Mistake 14/14: Not using the tools Playwright already gives you</title>
      <dc:creator>TestDino</dc:creator>
      <pubDate>Tue, 21 Apr 2026 13:30:37 +0000</pubDate>
      <link>https://dev.to/testdino01/mistake-1414-not-using-the-tools-playwright-already-gives-you-131a</link>
      <guid>https://dev.to/testdino01/mistake-1414-not-using-the-tools-playwright-already-gives-you-131a</guid>
      <description>&lt;p&gt;This is the final mistake in the series.&lt;/p&gt;

&lt;p&gt;A lot of teams still debug failed tests the hard way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read the error&lt;/li&gt;
&lt;li&gt;add a console log&lt;/li&gt;
&lt;li&gt;re-run the suite&lt;/li&gt;
&lt;li&gt;repeat&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That works, but it is slow.&lt;/p&gt;

&lt;p&gt;Playwright already ships with tools that make debugging much easier:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trace Viewer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Replay a failed run step by step and inspect DOM state, actions, network calls, and timing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx playwright show-trace trace.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;UI Mode&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run tests in an interactive mode and watch them execute with time-travel debugging.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx playwright &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--ui&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Codegen&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Generate locators and actions by recording browser interactions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx playwright codegen https://your-app.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Last failed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Re-run only the tests that failed, not the whole suite.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx playwright &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--last-failed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Use scripts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can also add these to &lt;code&gt;package.json&lt;/code&gt; so the team actually uses them regularly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx playwright test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:ui"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx playwright test --ui"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:failed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx playwright test --last-failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:trace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx playwright show-trace trace.zip"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"codegen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx playwright codegen"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The biggest lesson from this series is simple: Most debugging pain is not caused by missing tools. It is caused by not using the tools you already have.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>automation</category>
      <category>ai</category>
    </item>
    <item>
      <title>Playwright Mistake 13/14: You have no test cleanup strategy</title>
      <dc:creator>TestDino</dc:creator>
      <pubDate>Fri, 17 Apr 2026 13:09:08 +0000</pubDate>
      <link>https://dev.to/testdino01/playwright-mistake-1314-you-have-no-test-cleanup-strategy-6fa</link>
      <guid>https://dev.to/testdino01/playwright-mistake-1314-you-have-no-test-cleanup-strategy-6fa</guid>
      <description>&lt;p&gt;Your tests create users, orders, and comments. But they never delete them.&lt;/p&gt;

&lt;p&gt;A few runs later, your staging database is full of ghost users, orphan orders, and search results polluted with test data.&lt;/p&gt;

&lt;p&gt;Or worse, your tests begin failing because they rely on a “clean” state that is no longer clean. Yesterday’s leftovers become today’s flaky test.&lt;/p&gt;

&lt;p&gt;Test automation shouldn’t leave a trace. The fix is simple: create data before the test, then clean it up after. Even if the test fails.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extend&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;testProduct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Product&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;testProduct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test Product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;29.99&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/products/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;edit product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;testProduct&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/admin/products/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;testProduct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test Product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s the kind of cleanup strategy that keeps test suites reliable and staging data sane.&lt;/p&gt;

&lt;p&gt;Does your test suite clean up after itself, or does it leave a mess behind?&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>automation</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Mistake 12/14: You're not catching visual regressions</title>
      <dc:creator>TestDino</dc:creator>
      <pubDate>Thu, 16 Apr 2026 13:22:33 +0000</pubDate>
      <link>https://dev.to/testdino01/mistake-1214-youre-not-catching-visual-regressions-3c64</link>
      <guid>https://dev.to/testdino01/mistake-1214-youre-not-catching-visual-regressions-3c64</guid>
      <description>&lt;p&gt;A simple CSS change broke the checkout button on mobile. Every functional test passed.&lt;/p&gt;

&lt;p&gt;The scary part? Nobody knew. The button was technically in the DOM. It had the correct text. But to an actual user, it was completely invisible, overlapped by a rogue div on mobile screens only. The kind of bug that gets caught in production when a customer tries to check out and can't.&lt;/p&gt;

&lt;p&gt;Functional tests are great for verifying behavior. They are not designed to check appearance. They can't tell you if your app actually looks right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BEFORE : Functional test passes. Button is covered on mobile.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ BEFORE — Functional test passes, but the button is covered on mobile.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Checkout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// Passes because the button is in the DOM, even if users can't actually click it.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;AFTER : Visual regression catches layout issues across devices.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ AFTER — Visual regression catches layout issues across devices.&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart page looks correct&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveScreenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;timestamp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// changes every run&lt;/span&gt;
      &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;    &lt;span class="c1"&gt;// unique per session&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;maxDiffPixelRatio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 🧪 Test across viewports in playwright.config.ts&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desktop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;720&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mobile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;375&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;667&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Playwright's built-in visual comparisons (&lt;code&gt;toHaveScreenshot&lt;/code&gt;) catch these layout breaks instantly. The quick pro-tip that saves every team time: always mask dynamic elements like timestamps or order IDs to avoid flaky false positives.&lt;/p&gt;

&lt;p&gt;The distinction most teams miss is this: functional tests verify that something works. Visual tests verify that users can actually see and interact with it. Both questions matter. Most suites only answer one of them.&lt;/p&gt;

&lt;p&gt;When you add visual coverage at the viewport level, you stop shipping layout bugs that your test suite is structurally incapable of catching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have visual tests ever caught a bug your functional tests completely missed? Drop it in the comments.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>automation</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Mistake 11/14: Your E2E tests are checking email format</title>
      <dc:creator>TestDino</dc:creator>
      <pubDate>Wed, 15 Apr 2026 13:46:00 +0000</pubDate>
      <link>https://dev.to/testdino01/mistake-1114-your-e2e-tests-are-checking-email-format-11mk</link>
      <guid>https://dev.to/testdino01/mistake-1114-your-e2e-tests-are-checking-email-format-11mk</guid>
      <description>&lt;p&gt;if you are running 400 E2E tests and half of them validate form inputs like email format, you are burning browser time on logic that never needed a browser.&lt;/p&gt;

&lt;p&gt;The inconsistency isn't obvious at first. These tests pass. They feel useful. But every one of them navigates to a login page, authenticates, clicks through menus, waits for redirects. All to verify that a regex rejects "not-an-email". That is 30 seconds of browser overhead for a check that has nothing to do with the browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BEFORE - E2E for email validation. 30 seconds of overhead.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Every spec file repeats this flow&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejects invalid email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                          &lt;span class="c1"&gt;// navigate&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not-an-email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="c1"&gt;// fill&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sign in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// 30 sec for this&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;AFTER - Component test. Same assertion. 200ms&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/experimental-ct-react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LoginForm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../src/components/LoginForm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejects invalid email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;mount&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LoginForm&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// no navigation. no login.&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not-an-email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sign in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The distinction most teams miss is this: E2E tests exist to cover what only E2E can cover. Multi-page flows, payment processing, cross-service coordination. That is the territory.&lt;/p&gt;

&lt;p&gt;Form validation, input constraints, UI state changes. Those belong at the component level.&lt;/p&gt;

&lt;p&gt;When you separate them, everything gets faster and each layer actually earns its place. ⚡&lt;/p&gt;

&lt;p&gt;What percentage of your E2E tests are actually testing component behavior? Drop it in the comments.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>automation</category>
      <category>software</category>
    </item>
    <item>
      <title>Playwright Mistake 10/14: Flaky Tests Are Not Random</title>
      <dc:creator>TestDino</dc:creator>
      <pubDate>Tue, 14 Apr 2026 13:50:10 +0000</pubDate>
      <link>https://dev.to/testdino01/playwright-mistake-1014-flaky-tests-are-not-random-3jp2</link>
      <guid>https://dev.to/testdino01/playwright-mistake-1014-flaky-tests-are-not-random-3jp2</guid>
      <description>&lt;p&gt;You re-run. It passes. You move on.&lt;/p&gt;

&lt;p&gt;Until your whole team is trained to ignore failures. That's when real bugs slip through.&lt;/p&gt;

&lt;p&gt;Here are the 3 root causes behind 90% of flaky Playwright tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Race Condition&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// bad&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.welcome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// good&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Welcome back&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// auto-retries until element exists&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Shared Data&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// bad&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users/test-user-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// 5 workers fight over this&lt;/span&gt;

&lt;span class="c1"&gt;// good&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createUniqueUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// each test gets its own&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Network Timing&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// bad&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/search?q=laptop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.result&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// results not loaded yet&lt;/span&gt;

&lt;span class="c1"&gt;// good&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/search?q=laptop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/api/search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Which of these three has burned you the most on your current project?&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>automation</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Mistake 9/14: Stop Relying on Staging Data for Edge Case Testing</title>
      <dc:creator>TestDino</dc:creator>
      <pubDate>Mon, 13 Apr 2026 13:04:11 +0000</pubDate>
      <link>https://dev.to/testdino01/mistake-914-stop-relying-on-staging-data-for-edge-case-testing-5117</link>
      <guid>https://dev.to/testdino01/mistake-914-stop-relying-on-staging-data-for-edge-case-testing-5117</guid>
      <description>&lt;p&gt;You finish the happy path tests. Coverage looks solid. Then a user reports the error screen is broken. You never caught it because your suite never actually triggered a server error.&lt;/p&gt;

&lt;p&gt;Most teams skip edge cases entirely , or spend hours crafting corrupted data in staging just to trigger one scenario. Then the data gets cleaned up, and the test breaks again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the exact pattern that makes test suites unreliable as they grow.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Playwright's &lt;code&gt;page.route()&lt;/code&gt; removes the backend dependency completely. You return whatever response you want : empty arrays, server errors, malformed JSON, slow responses; directly from the test. Your staging environment stays untouched.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Clean Pattern
&lt;/h2&gt;

&lt;p&gt;Instead of relying on staging state, intercept the network call and return exactly what you need to test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Test 1: Empty state&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shows empty state when no users found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fulfill&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;([]),&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No users found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Then handle the failure and timeout cases the same way:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Test 2: Server error&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shows error message on server failure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fulfill&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Internal Server Error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Something went wrong&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Retry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Test 3: Slow response&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shows loading spinner and timeout message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fulfill&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading-spinner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Request timed out&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero backend involvement:&lt;/strong&gt; No staging data setup or cleanup needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fully deterministic:&lt;/strong&gt; The test controls exactly what the API returns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three edge cases covered:&lt;/strong&gt; Empty state, server error, and timeout , all in one go&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Empty states, error screens, and timeout messages are exactly what users see when things go wrong. Those paths deserve real test coverage, not a "we'll check it manually" comment in the PR.&lt;/p&gt;

&lt;p&gt;Do you enforce API mocking for edge cases on your team, or is it still a free-for-all?&lt;/p&gt;

&lt;p&gt;Drop it in the comments.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>automation</category>
      <category>softwaretesting</category>
    </item>
    <item>
      <title>Playwright Mistake 8/14: You Only Check Status Codes on API Responses</title>
      <dc:creator>TestDino</dc:creator>
      <pubDate>Sat, 11 Apr 2026 12:36:05 +0000</pubDate>
      <link>https://dev.to/testdino01/playwright-mistake-814-you-only-check-status-codes-on-api-responses-5c55</link>
      <guid>https://dev.to/testdino01/playwright-mistake-814-you-only-check-status-codes-on-api-responses-5c55</guid>
      <description>&lt;p&gt;The docs said the field was a string. Production returned null. Your test passed because you only asserted status === 200.&lt;/p&gt;

&lt;p&gt;That's the problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happened&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The frontend called .toUpperCase() on null. Page crashed. Your test had no idea.&lt;/p&gt;

&lt;p&gt;Checking the status code and calling it tested is like confirming the door is closed without checking if it's locked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;These two error messages tell the whole story&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Expected string, received null at 'name'" → 2-minute fix
"Cannot read properties of undefined"      → 2-hour debug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One catches the issue at test time. The other catches it in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix: validate the response shape&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BEFORE&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get user profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users/123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Backend returns { name: null }. Test passes. Frontend breaks.&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AFTER&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;member&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get user profile - validate contract&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users/123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// name null?       Caught here.&lt;/span&gt;
  &lt;span class="c1"&gt;// field removed?   Caught here.&lt;/span&gt;
  &lt;span class="c1"&gt;// type changed?    Caught here.&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters at scale&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One method:&lt;/strong&gt; safeParse covers every field in one call&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visible failure:&lt;/strong&gt; tells you exactly which field broke and why&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No prod surprises:&lt;/strong&gt; field renames, null returns, type changes all fail at test time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you have 200+ API tests only checking status codes, you have 200+ tests that can pass while the app is broken.&lt;/p&gt;

&lt;p&gt;Tag someone who's still only asserting status codes.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>ci</category>
      <category>automation</category>
    </item>
    <item>
      <title>Mistake 7/14: You are still using beforeEach for everything</title>
      <dc:creator>TestDino</dc:creator>
      <pubDate>Fri, 10 Apr 2026 13:36:25 +0000</pubDate>
      <link>https://dev.to/testdino01/mistake-714-you-are-still-using-beforeeach-for-everything-4eik</link>
      <guid>https://dev.to/testdino01/mistake-714-you-are-still-using-beforeeach-for-everything-4eik</guid>
      <description>&lt;p&gt;If you are still copy-pasting &lt;code&gt;beforeEach&lt;/code&gt;blocks across fifty spec files to initialize your Page Objects, you are building a maintenance trap.&lt;/p&gt;

&lt;p&gt;The inconsistency isn't laziness. It is what happens when your setup has no enforced contract across the codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BEFORE - Brittle boilerplate&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Every spec file repeats this logic&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;settingsPage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;settingsPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SettingsPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;AFTER - Scalable dependency injection&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// fixtures.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;settingsPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SettingsPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Playwright built &lt;strong&gt;Fixtures&lt;/strong&gt; to eliminate the "which pattern did we agree on?" conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;-Encapsulation:&lt;/strong&gt; Setup and teardown live together in the fixture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;-Lazy Loading:&lt;/strong&gt; If a test doesn't ask for the fixture, it never runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;-Consistency:&lt;/strong&gt; One source of truth for every Page Object in your framework.&lt;/p&gt;

&lt;p&gt;Stop managing state manually. Let the runner do the heavy lifting. ⚡&lt;/p&gt;

&lt;p&gt;Do you enforce a fixture strategy on your team, or is it a free-for-all? Drop it in the comments.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>automation</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Mistake 6/14: Stop Putting Assertions in Page Objects</title>
      <dc:creator>TestDino</dc:creator>
      <pubDate>Thu, 09 Apr 2026 13:29:08 +0000</pubDate>
      <link>https://dev.to/testdino01/mistake-614-stop-putting-assertions-in-page-objects-2184</link>
      <guid>https://dev.to/testdino01/mistake-614-stop-putting-assertions-in-page-objects-2184</guid>
      <description>&lt;p&gt;A developer wants to keep their tests "dry," so they put the expect statement inside the Page Object method. It makes sense at first. login() handles everything: fill the form, click the button, confirm you’re on the dashboard. One method. Clean. ✅&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Until you need to test a failed login. ✖️&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now your l&lt;code&gt;ogin()&lt;/code&gt; method asserts success but your negative test is looking for a failure. To fix it, you add &lt;code&gt;loginExpectingFailure()&lt;/code&gt;. Then &lt;code&gt;loginAndCheckError()&lt;/code&gt;. Six login methods later, your POM is a graveyard of duplicated logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the exact trap teams fall into when a suite grows.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Page Objects should be simple. They store locators and perform actions. That is it. Assertions belong in the test file, where the intent of the test is immediately visible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Clean Pattern&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of baking the assertion into the class, keep the class "pure":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoginPage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emailInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;passInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;submitBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, handle the different outcomes in your actual test files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Happy path&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loginPage&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;loginPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user@test.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pass123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Negative path&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loginPage&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;loginPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user@test.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wrong&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loginPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One method:&lt;/strong&gt; A single &lt;code&gt;login()&lt;/code&gt; method with zero assertions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visible Intent:&lt;/strong&gt; Anyone reading the test knows exactly what is being verified.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Bloat:&lt;/strong&gt; No duplicated methods. No boolean flags. No &lt;code&gt;loginVersion4()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do you enforce a "no assertions in POM" rule on your team, or is it a free-for-all? Drop it in the comments.&lt;/p&gt;

&lt;h1&gt;
  
  
  playwright #testing #automation #sdet
&lt;/h1&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>automation</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Mistake 5/14: Your Tests Are Unreadable in the Report</title>
      <dc:creator>TestDino</dc:creator>
      <pubDate>Wed, 08 Apr 2026 13:29:19 +0000</pubDate>
      <link>https://dev.to/testdino01/mistake-514-your-tests-are-unreadable-in-the-3b0m</link>
      <guid>https://dev.to/testdino01/mistake-514-your-tests-are-unreadable-in-the-3b0m</guid>
      <description>&lt;p&gt;A test fails in CI. You open the Playwright HTML report and see 60 lines of code in one giant block. It says "line 47 timed out." You have no idea what the test was actually trying to do. You are left staring at a stack trace guessing which part of the flow broke.&lt;/p&gt;

&lt;p&gt;The damage isn't just time. It's clarity. When test results are a pain to decipher, developers stop reading them. Failures get dismissed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;test.step()&lt;/code&gt; fixes this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BEFORE: One giant block. The report just says "X test failed."&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit contact form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/contact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Jane&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jane@test.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Thank you&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;AFTER: Named steps. The report reads like a story.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit contact form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GIVEN user is on contact page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/contact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WHEN user fills and submits form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Jane&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jane@test.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;THEN success message appears&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Thank you&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule: Wrap your code in named steps using GIVEN, WHEN, and THEN.&lt;/p&gt;

&lt;p&gt;Now your report says "WHEN user fills and submits form failed" instead of "line 47 timed out".&lt;/p&gt;

&lt;p&gt;You know exactly what broke instantly. Debugging goes from minutes to seconds.&lt;/p&gt;

&lt;p&gt;Do you use &lt;code&gt;test.step()&lt;/code&gt; to structure your tests, or is your code one giant block? Drop it in the comments.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>qa</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
