<?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: Yongjae Lee</title>
    <description>The latest articles on DEV Community by Yongjae Lee (@voidmatcha).</description>
    <link>https://dev.to/voidmatcha</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%2F4012519%2F862e2023-0e7d-43ae-b63a-7473e3e3cd06.jpg</url>
      <title>DEV Community: Yongjae Lee</title>
      <link>https://dev.to/voidmatcha</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/voidmatcha"/>
    <language>en</language>
    <item>
      <title>E2E tests can pass while testing nothing</title>
      <dc:creator>Yongjae Lee</dc:creator>
      <pubDate>Fri, 03 Jul 2026 00:01:12 +0000</pubDate>
      <link>https://dev.to/voidmatcha/your-e2e-tests-pass-are-you-sure-they-can-fail-2fik</link>
      <guid>https://dev.to/voidmatcha/your-e2e-tests-pass-are-you-sure-they-can-fail-2fik</guid>
      <description>&lt;p&gt;Code-in-the-browser, &lt;a href="https://github.com/coder/code-server" rel="noopener noreferrer"&gt;code-server&lt;/a&gt;, had an E2E suite that looked healthy. CI was green. The PR had been reviewed. But one test file still contained this:&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="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;only&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one line quietly disabled the rest of the tests in the file. Eight tests did not run for seven months, while the check still passed. The fix eventually merged in &lt;a href="https://github.com/coder/code-server/pull/7845" rel="noopener noreferrer"&gt;coder/code-server#7845&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is not a story about code-server being careless. It is almost the opposite: even mature projects, reviewed PRs, and AI-reviewed changes can end up with E2E tests that are green but do not prove the thing they appear to prove.&lt;/p&gt;

&lt;p&gt;I mean "testing nothing" in a narrow sense. The test may execute, and CI may pass, but the assertion does not observe the user-visible condition it claims to protect.&lt;/p&gt;

&lt;p&gt;I started collecting these cases while fixing Playwright and Cypress tests across open-source repositories. The same shapes kept appearing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same shape in other projects
&lt;/h2&gt;

&lt;p&gt;Carbon had an assertion like this (&lt;a href="https://github.com/carbon-design-system/carbon/pull/22564" rel="noopener noreferrer"&gt;carbon#22564&lt;/a&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="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;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;.cds--progress-step--complete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeTruthy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;page.locator(...)&lt;/code&gt; is not a DOM lookup result. It is a Locator object. The object is truthy whether the element exists or not, so the assertion can pass even when the UI is wrong. The intended check is closer to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- expect(page.locator('.cds--progress-step--complete')).toBeTruthy();
&lt;/span&gt;&lt;span class="gi"&gt;+ await expect(page.locator('.cds--progress-step--complete')).toBeVisible();
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ghost had a Playwright web-first assertion without &lt;code&gt;await&lt;/code&gt; (&lt;a href="https://github.com/TryGhost/Ghost/pull/28712" rel="noopener noreferrer"&gt;Ghost#28712&lt;/a&gt;). The line looked like an assertion, but it did not wait for the condition it was supposed to verify.&lt;/p&gt;

&lt;p&gt;Strapi had this pattern (&lt;a href="https://github.com/strapi/strapi/pull/26630" rel="noopener noreferrer"&gt;strapi#26630&lt;/a&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;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;likeButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isDisabled&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBeTruthy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Playwright, methods such as &lt;code&gt;isDisabled()&lt;/code&gt;, &lt;code&gt;isVisible()&lt;/code&gt;, and &lt;code&gt;isHidden()&lt;/code&gt; return &lt;code&gt;Promise&amp;lt;boolean&amp;gt;&lt;/code&gt;. The Promise object itself is truthy, so the assertion can pass regardless of the actual button state. SvelteKit had the same family of issue in &lt;a href="https://github.com/sveltejs/kit/pull/16068" rel="noopener noreferrer"&gt;kit#16068&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;These were not synthetic examples: the broader pass has produced 12 merged upstream fixes so far.&lt;/p&gt;

&lt;p&gt;The details differ, but the underlying failure is the same: "there is a test" and "the test can fail for the intended regression" are different claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checks that catch the low-hanging fruit
&lt;/h2&gt;

&lt;p&gt;These are not a complete review. They are just fast checks that have found real problems in real Playwright/Cypress suites.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Focused tests accidentally committed&lt;/span&gt;
rg &lt;span class="s2"&gt;"(it|test|describe)&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;only&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; e2e tests cypress playwright

&lt;span class="c"&gt;# 2. Truthiness checks on Locator-like objects or handles&lt;/span&gt;
rg &lt;span class="s2"&gt;"expect&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;[^)]*&lt;/span&gt;&lt;span class="se"&gt;\)\.&lt;/span&gt;&lt;span class="s2"&gt;(toBeTruthy|toBeDefined|not&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;toBeNull)&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; e2e tests cypress playwright

&lt;span class="c"&gt;# 3. Playwright web-first assertions without await&lt;/span&gt;
rg &lt;span class="s2"&gt;"^[[:space:]]*expect&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;.+&lt;/span&gt;&lt;span class="se"&gt;\)\.&lt;/span&gt;&lt;span class="s2"&gt;to(BeVisible|BeHidden|BeEnabled|HaveText|HaveURL|HaveTitle|HaveCount)&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; e2e tests playwright

&lt;span class="c"&gt;# 4. State reads whose result is discarded&lt;/span&gt;
rg &lt;span class="s2"&gt;"^[[:space:]]*(await )?[A-Za-z_.]+&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;(isVisible|isEnabled|isDisabled|isHidden)&lt;/span&gt;&lt;span class="se"&gt;\(\)&lt;/span&gt;&lt;span class="s2"&gt;[[:space:]]*;?[[:space:]]*$"&lt;/span&gt; e2e tests cypress playwright
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first two are usually low-noise. The third and fourth need context before you automate fixes. But as a first pass, they are useful because they look for tests that can stay green while asserting little.&lt;/p&gt;

&lt;p&gt;Some of this belongs in lint rules. I split out the lintable subset into &lt;a href="https://www.npmjs.com/package/eslint-plugin-playwright-silent-pass" rel="noopener noreferrer"&gt;&lt;code&gt;eslint-plugin-playwright-silent-pass&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://www.npmjs.com/package/eslint-plugin-cypress-silent-pass" rel="noopener noreferrer"&gt;&lt;code&gt;eslint-plugin-cypress-silent-pass&lt;/code&gt;&lt;/a&gt;. Grep is good for a quick audit; ESLint is better for preventing repeats.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-D&lt;/span&gt; eslint-plugin-playwright-silent-pass   &lt;span class="c"&gt;# or eslint-plugin-cypress-silent-pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What general reviewers tend to miss
&lt;/h2&gt;

&lt;p&gt;At first I assumed general AI PR reviewers would catch most of this. To check that, I re-reviewed 100 already-AI-reviewed open-source E2E PRs across 77 repositories. A human-confirmed benchmark found 110 material E2E-test issues in those PRs (&lt;a href="https://github.com/voidmatcha/e2e-skills/blob/main/docs/ai-reviewer-benchmark.md" rel="noopener noreferrer"&gt;results&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The comparison was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;e2e-reviewer&lt;/code&gt;: 78 / 110 material E2E issues found, 0 false positives&lt;/li&gt;
&lt;li&gt;General AI PR reviewers: 45 / 110 found, 10 false positives&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I do not think this means general reviewers are bad. They are trying to review architecture, types, security, performance, style, and product logic at the same time. This problem is narrower. You have to keep asking: can this assertion fail? Does this locator observe the DOM state, or just construct an object? Did an async assertion get dropped on the floor?&lt;/p&gt;

&lt;p&gt;About half the cases are not obvious from a grep alone. For example:&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="nf"&gt;expect&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="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That might be an intentional resilience check, or it might be hiding a real auth regression. You need test context to decide.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg34108rts5dmpsp0rerz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg34108rts5dmpsp0rerz.png" alt="e2e-skills overview" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/voidmatcha/e2e-skills" rel="noopener noreferrer"&gt;e2e-skills&lt;/a&gt; is a small set of agent skills for Playwright and Cypress E2E work. The goal is not to ask "does a test exist?" but "can this test actually fail when the product regresses?"&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;e2e-reviewer&lt;/code&gt; statically reads Playwright/Cypress specs and Page Objects, then checks 24 weak-diagnostic anti-patterns: focused tests, locator truthiness, missing &lt;code&gt;await&lt;/code&gt;, discarded state reads, loose status assertions, and related patterns that keep CI green while proving little.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;playwright-test-generator&lt;/code&gt; is for creating new Playwright coverage. It first looks for coverage gaps, then explores the live app in a browser, drafts tests for real user flows, pauses at an approval gate, and finally reviews the result with the same weak-assertion rules. It is closer to "find the flow worth proving and write fail-capable assertions" than "dump a test file into the repo."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;playwright-debugger&lt;/code&gt; reads Playwright reports, traces, screenshots, and CI output to classify failures: timing, selector drift, auth/session setup, environment trouble, or a real product regression.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cypress-debugger&lt;/code&gt; does the same kind of failure classification for Cypress mochawesome/JUnit output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The scope is deliberately narrow: Playwright and Cypress only. I would rather catch common fail-open patterns in those two frameworks well than claim shallow support for every browser automation stack.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/voidmatcha/e2e-skills" rel="noopener noreferrer"&gt;github.com/voidmatcha/e2e-skills&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Even without the tool, the habit is useful: next time your E2E suite is green, ask which test would actually fail if the feature broke tonight.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>cypress</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
