<?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: Shaban Umar</title>
    <description>The latest articles on DEV Community by Shaban Umar (@shaban_umar_eb956a5f684ce).</description>
    <link>https://dev.to/shaban_umar_eb956a5f684ce</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%2F4136899%2F8bed58cf-2726-40f4-8d43-8a61abf01acb.jpg</url>
      <title>DEV Community: Shaban Umar</title>
      <link>https://dev.to/shaban_umar_eb956a5f684ce</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shaban_umar_eb956a5f684ce"/>
    <language>en</language>
    <item>
      <title>I showed AI the buggy code. Its tests started protecting the bug.</title>
      <dc:creator>Shaban Umar</dc:creator>
      <pubDate>Thu, 08 Oct 2026 09:27:37 +0000</pubDate>
      <link>https://dev.to/shaban_umar_eb956a5f684ce/i-showed-ai-the-buggy-code-its-tests-started-protecting-the-bug-48a3</link>
      <guid>https://dev.to/shaban_umar_eb956a5f684ce/i-showed-ai-the-buggy-code-its-tests-started-protecting-the-bug-48a3</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/kaggle-2026-09-23"&gt;Kaggle Benchmarking Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I've been a QA engineer since 2023. The most dangerous test suite I've ever seen was a green one: every check passed, the build shipped, and the bug went out with it.&lt;/p&gt;

&lt;p&gt;So when people say LLMs "write good unit tests", my question is: &lt;strong&gt;good at what?&lt;/strong&gt; Passing is easy. A suite that asserts nothing passes. The job of a test is to &lt;strong&gt;fail when the code is wrong&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For the Kaggle Community Benchmarks challenge I built &lt;strong&gt;Green Tests, Shipped Bugs&lt;/strong&gt;, a benchmark that measures exactly that, and it turned up the most useful thing I've learned about AI-written tests:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;When a model can see the code, it often tests what the code &lt;em&gt;does&lt;/em&gt; instead of what it &lt;em&gt;should do&lt;/em&gt;, and a buggy function gets a test suite that protects the bug.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One sentence in the prompt mostly fixes it. And the best models don't need the sentence at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;I wrote 12 small functions of the kind that sit in every codebase: pagination, URL slugs, money rounding, interval merging, first-occurrence binary search, business-day counting, IPv4 validation, duration parsing, string truncation, percent change, chunking and leap years. Each one has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a precise &lt;strong&gt;docstring&lt;/strong&gt;, which is the contract;&lt;/li&gt;
&lt;li&gt;one &lt;strong&gt;correct&lt;/strong&gt; implementation;&lt;/li&gt;
&lt;li&gt;one &lt;strong&gt;planted bug&lt;/strong&gt;, a realistic mistake someone could ship: &lt;code&gt;page &amp;lt; 0&lt;/code&gt; instead of &lt;code&gt;page &amp;lt; 1&lt;/code&gt;, &lt;code&gt;float&lt;/code&gt; rounding on money, a binary search that finds &lt;em&gt;an&lt;/em&gt; occurrence instead of the &lt;em&gt;first&lt;/em&gt;;&lt;/li&gt;
&lt;li&gt;2–3 extra &lt;strong&gt;mutants&lt;/strong&gt;, meaning more small, plausible bugs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model writes a pytest suite. A sandbox runs it against every version. This is &lt;strong&gt;mutation testing&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Invalid:&lt;/strong&gt; the suite fails on the &lt;strong&gt;correct&lt;/strong&gt; code. It cries wolf, so it scores zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caught:&lt;/strong&gt; a valid suite fails on a buggy version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bug-locking test:&lt;/strong&gt; a test that fails on the correct code but &lt;strong&gt;passes on the buggy code&lt;/strong&gt;. It asserts the bug as if it were the spec.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every mutant is checked against a hand-written reference suite, so each one can be caught by a test that simply follows the docstring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three conditions
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;condition&lt;/th&gt;
&lt;th&gt;the model sees&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spec_only&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;signature + docstring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;code_shown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the full implementation, &lt;strong&gt;which contains the planted bug&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;code_warned&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the same buggy code, plus: &lt;em&gt;"this hasn't been reviewed and may contain bugs; test the docstring, not the code"&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;code_shown&lt;/code&gt; is how people actually use these tools: paste a function, type "write tests for this". Nothing in the prompt says there's a bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Models Tested
&lt;/h2&gt;

&lt;p&gt;9 models through Kaggle's model proxy, with identical prompts: GPT-6 Astra, Claude Opus 5, Claude Sonnet 5, Claude Haiku 4.5, Gemini 3.1 Pro, Gemini 3.5 Flash-Lite, DeepSeek-R1, Qwen3-Coder 480B and GLM-5.&lt;/p&gt;

&lt;p&gt;I picked them to spread across three axes: &lt;strong&gt;frontier vs. small&lt;/strong&gt; (Opus 5 and GPT-6 Astra next to Haiku 4.5 and Flash-Lite), &lt;strong&gt;general vs. code-specialised&lt;/strong&gt; (Qwen3-Coder), and &lt;strong&gt;closed vs. open-weight&lt;/strong&gt; (DeepSeek-R1, Qwen3-Coder, GLM-5). That spread turned out to matter: the habit of copying the bug into the tests doesn't simply follow model size.&lt;/p&gt;

&lt;p&gt;12 functions × 3 conditions = 36 suites per model, capped at 40 test cases and 10,000 output tokens each. (I planned a tenth, gpt-oss-120b, but the proxy answered "model is currently experiencing heavy load" on three separate days.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Findings
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;p&gt;Planted bug caught, by condition, and bug-locking tests summed over all 36 suites, sorted by mutation score (the leaderboard metric):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;model&lt;/th&gt;
&lt;th&gt;spec only&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;code shown&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;code + warning&lt;/th&gt;
&lt;th&gt;bug-locking tests&lt;/th&gt;
&lt;th&gt;mutation score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GPT-6 Astra&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.1 Pro&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;92%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.97&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 5&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;92%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.97&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.5 Flash-Lite&lt;/td&gt;
&lt;td&gt;58%&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;td&gt;83%&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0.72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 5&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;33%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;53&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.69&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek-R1&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;td&gt;42%&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;td&gt;0.63&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Haiku 4.5&lt;/td&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;td&gt;42%&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;0.56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM-5&lt;/td&gt;
&lt;td&gt;58%&lt;/td&gt;
&lt;td&gt;42%&lt;/td&gt;
&lt;td&gt;58%&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;0.51&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-Coder 480B&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;42%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;67&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.39&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Across all models:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;spec only&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;code shown&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;code + warning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;planted bug caught&lt;/td&gt;
&lt;td&gt;71%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;68%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;77%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bug-locking tests (total)&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;158&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;suites that lock the planted bug in&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  What I learned
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Seeing the code multiplied the bug-locking tests
&lt;/h4&gt;

&lt;p&gt;The headline isn't the catch rate, which only dips (71% → 68%). It's what the tests &lt;em&gt;assert&lt;/em&gt;. With only the docstring, the models wrote 13 bug-locking tests between them. Given the same docstring &lt;strong&gt;plus the buggy code&lt;/strong&gt;: &lt;strong&gt;158&lt;/strong&gt;. In 14 suites the planted bug itself was locked in: the suite passes on the buggy code and fails on the fix.&lt;/p&gt;

&lt;p&gt;The model reads the implementation, works out what it returns, and writes that down as the expected value, faithfully including the bug.&lt;/p&gt;

&lt;p&gt;It isn't universal, and the split is the interesting part. &lt;strong&gt;The three strongest models (GPT-6 Astra, Claude Opus 5 and Gemini 3.1 Pro) wrote zero bug-locking tests in any condition.&lt;/strong&gt; Shown the buggy code, they tested the docstring anyway. Every one of the 206 bug-locking tests came from the other six. Flash-Lite and Haiku even caught &lt;em&gt;more&lt;/em&gt; bugs with the code in front of them. So this is not "AI can't write tests"; it's a habit some models have, and you won't know which kind you're using unless you check.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Some models do it on purpose
&lt;/h4&gt;

&lt;p&gt;The clearest case is Claude Sonnet 5, a strong model that caught 75% of planted bugs from the docstring alone and &lt;strong&gt;33%&lt;/strong&gt; once it saw the code, with 53 bug-locking tests, all of them in &lt;code&gt;code_shown&lt;/code&gt;. The pagination function's planted bug lets &lt;code&gt;page=0&lt;/code&gt; through instead of rejecting it. Sonnet's suite contained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_page_zero_is_not_rejected_and_yields_empty_list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test's &lt;em&gt;name&lt;/em&gt; describes the bug. The test &lt;em&gt;requires&lt;/em&gt; it. The same pattern showed up across functions: &lt;code&gt;truncate&lt;/code&gt; tests expecting a result one character longer than &lt;code&gt;max_len&lt;/code&gt;, and &lt;code&gt;slugify&lt;/code&gt; tests expecting &lt;code&gt;"naïve résumé"&lt;/code&gt; → &lt;code&gt;"na-ve-r-sum-"&lt;/code&gt;, which is exactly the planted bug's output.&lt;/p&gt;

&lt;p&gt;That's a real technique, &lt;strong&gt;characterization testing&lt;/strong&gt;: pin down what legacy code does today before you refactor it. But "write tests for this function" doesn't say which one you want. Some models assume &lt;em&gt;pin the current behaviour&lt;/em&gt;, and a pinned bug is a bug nobody will ever fix, because "the tests say it's correct".&lt;/p&gt;

&lt;h4&gt;
  
  
  3. One sentence fixes it (for most models)
&lt;/h4&gt;

&lt;p&gt;Add &lt;em&gt;"this may contain bugs; test the docstring, not the code"&lt;/em&gt; and the catch rate goes to 77%, &lt;strong&gt;higher than spec-only&lt;/strong&gt;, with bug-locking tests down from 158 to 35. Sonnet 5 went from 33% to &lt;strong&gt;100%&lt;/strong&gt; with that one sentence. The model can do the right thing. It needs to be told which job it's doing.&lt;/p&gt;

&lt;p&gt;The exception: Qwen3-Coder wrote 28 bug-locking tests &lt;em&gt;even when warned&lt;/em&gt;. It mostly ignored the instruction.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. The coverage is there; the expected values are wrong
&lt;/h4&gt;

&lt;p&gt;If I throw away each suite's &lt;em&gt;wrong&lt;/em&gt; tests (the ones that fail on correct code) and score what's left, the average kill rate goes from &lt;strong&gt;72% to 94%&lt;/strong&gt;. The models do exercise the right cases. What breaks the suite is the expected values: arithmetic slips (&lt;code&gt;percent_change(-100, 100)&lt;/code&gt;, weekend counts in &lt;code&gt;business_days&lt;/code&gt;, Haiku claiming 1600 and 2000 aren't leap years) and values copied from the buggy code. Even Opus 5's one miss was of this kind: in the &lt;em&gt;warned&lt;/em&gt; condition it expected &lt;code&gt;business_days&lt;/code&gt; to count backwards when the end date comes first, where the docstring says to return 0. The two failure types feel different in practice: an arithmetic slip makes a suite fail on correct code and gets noticed; a copied bug makes a suite &lt;em&gt;green&lt;/em&gt; and doesn't.&lt;/p&gt;

&lt;h4&gt;
  
  
  5. More tests ≠ better tests
&lt;/h4&gt;

&lt;p&gt;Qwen3-Coder wrote the most tests per suite (45 on average, over the 40-case cap) and had the lowest score. GPT-6 Astra wrote 37 and was perfect. Without a cap, some frontier models happily generate several hundred parametrized cases for a 10-line function; an earlier run of mine hit the 10,000-token output limit, which is why the cap exists.&lt;/p&gt;

&lt;h4&gt;
  
  
  6. The models found bugs in &lt;em&gt;my&lt;/em&gt; code
&lt;/h4&gt;

&lt;p&gt;Honest bonus. My first run had bugs in three of my "correct" reference implementations: the money rounder crashed on 32-digit amounts, the duration parser accepted Arabic-Indic digits, and the IPv4 check crashed on a 5,000-digit octet despite a docstring promising it never raises. Several models' tests failed my "correct" code, &lt;strong&gt;and they were right&lt;/strong&gt;. I fixed the references, tightened three docstrings and re-ran everything.&lt;/p&gt;

&lt;p&gt;Then the final run found a fourth. GPT-6 Astra wrote &lt;code&gt;test_rounding_is_independent_of_callers_decimal_context&lt;/code&gt;: it turns on strict &lt;code&gt;decimal&lt;/code&gt; settings globally and checks that &lt;code&gt;round_money&lt;/code&gt; still returns &lt;code&gt;"12345.13"&lt;/code&gt;. My version inherited the caller's settings and crashed. A money function shouldn't depend on global state, so I fixed it and re-graded that suite (no other suite was affected). The reference implementation in a benchmark needs testing too, and here the models did it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to do with this
&lt;/h3&gt;

&lt;p&gt;If you ask an assistant for tests:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Give it the contract, not just the code.&lt;/strong&gt; Docstring, ticket, spec.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Say which job you want.&lt;/strong&gt; "Find bugs against this spec" and "pin current behaviour" are opposite instructions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tell it the code may be wrong.&lt;/strong&gt; It's one sentence, and in this benchmark it cut bug-locking tests from 158 to 35.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the tests against a known-good version when you can.&lt;/strong&gt; A test that fails on correct code is a bug report about the test, or, sometimes, about your code.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What I'd measure next
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Harder mutants.&lt;/strong&gt; The best models caught almost everything; off-by-one bugs in less familiar code (date maths across time zones, Unicode normalisation) would separate them further.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The repair loop.&lt;/strong&gt; Show the model its own failing tests on the correct code and ask it to fix them. Does it fix the test, or "fix" the code to match the test?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Property-based tests.&lt;/strong&gt; Do models that reach for Hypothesis lock in fewer bugs?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My Benchmark
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Benchmark (leaderboard):&lt;/strong&gt; &lt;a href="https://www.kaggle.com/benchmarks/shabanumar/green-tests-shipped-bugs" rel="noopener noreferrer"&gt;https://www.kaggle.com/benchmarks/shabanumar/green-tests-shipped-bugs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task:&lt;/strong&gt; &lt;a href="https://www.kaggle.com/benchmarks/tasks/shabanumar/green-tests-shipped-bugs" rel="noopener noreferrer"&gt;https://www.kaggle.com/benchmarks/tasks/shabanumar/green-tests-shipped-bugs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notebook&lt;/strong&gt; (problems, harness, all per-condition tables): &lt;a href="https://www.kaggle.com/code/shabanumar/green-tests-shipped-bugs" rel="noopener noreferrer"&gt;https://www.kaggle.com/code/shabanumar/green-tests-shipped-bugs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The leaderboard score is the mutation score from a separate, fresh run of the task for each model, so it won't match the tables above exactly (those come from the per-condition analysis runs in the notebook). The leaderboard also has a tenth model, Gemini 3.7 Flash (0.88), which I only ran there.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;leaderboard&lt;/th&gt;
&lt;th&gt;score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.1 Pro&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-6 Astra&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 5&lt;/td&gt;
&lt;td&gt;0.94&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.7 Flash&lt;/td&gt;
&lt;td&gt;0.88&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 5&lt;/td&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.5 Flash-Lite&lt;/td&gt;
&lt;td&gt;0.72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek-R1&lt;/td&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM-5&lt;/td&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Haiku 4.5&lt;/td&gt;
&lt;td&gt;0.61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-Coder 480B&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With 36 suites per model, a single run moves some models by 10–15 points (GLM-5 went from 0.51 to 0.67, Qwen3-Coder from 0.39 to 0.54), while the top three and the bottom of the ranking stayed put. If you build on this, run each model more than once. Adding a problem takes a docstring, a correct function and a few buggy variants. I'd love to see harder mutants, and more cases where the reference implementation is the one that's wrong.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>kagglechallenge</category>
      <category>ai</category>
      <category>testing</category>
    </item>
    <item>
      <title>Why Your Playwright Suite Gets Flaky After 50 Tests (And How to Fix It)</title>
      <dc:creator>Shaban Umar</dc:creator>
      <pubDate>Tue, 22 Sep 2026 05:51:12 +0000</pubDate>
      <link>https://dev.to/shaban_umar_eb956a5f684ce/why-your-playwright-suite-gets-flaky-after-50-tests-and-how-to-fix-it-1b44</link>
      <guid>https://dev.to/shaban_umar_eb956a5f684ce/why-your-playwright-suite-gets-flaky-after-50-tests-and-how-to-fix-it-1b44</guid>
      <description>&lt;p&gt;Every Playwright suite starts clean. Twenty tests, all green, runs in ninety seconds. Then it grows.&lt;/p&gt;

&lt;p&gt;By test fifty, someone has added a &lt;code&gt;waitForTimeout&lt;/code&gt; to "fix" a race. By test a hundred, the suite fails maybe one run in four — never the same test twice. People start re-running the pipeline until it goes green. At that point the suite has stopped being a safety net and become a tax.&lt;/p&gt;

&lt;p&gt;I've watched this happen on enough projects to notice it's almost always the same five causes. None of them are exotic. Here they are, with the fix for each.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. &lt;code&gt;waitForTimeout&lt;/code&gt; is not a wait, it's a bet
&lt;/h2&gt;

&lt;p&gt;This is the big one. It shows up the first time a test fails intermittently and someone reaches for the fastest fix:&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;click&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="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;// ← the bet&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;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;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bet is that two seconds is always enough. On your laptop it is. On a loaded CI runner at 3am it sometimes isn't. And on the days it &lt;em&gt;is&lt;/em&gt; enough, you're still paying two seconds — multiply that across a hundred tests and you've added three minutes to every run for nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; wait for the thing you actually need.&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;click&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="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;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;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// auto-waits, up to the timeout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Playwright's assertions already retry until they pass or time out. You almost never need an explicit wait. When you genuinely do — a click that fires an XHR whose result you need before continuing — wait for that specific thing:&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="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="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;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&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/order&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;click&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&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;Enforce it with lint, not discipline.&lt;/strong&gt; Discipline fails under deadline pressure. Add &lt;code&gt;eslint-plugin-playwright&lt;/code&gt; and turn on &lt;code&gt;no-wait-for-timeout&lt;/code&gt; as an error. Now the pipeline rejects it and nobody has to police it in review.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Tests that share data collide in parallel
&lt;/h2&gt;

&lt;p&gt;This one is sneaky because it only appears once you turn on parallelism.&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;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;registers a new user&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="nf"&gt;registerUser&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test@example.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;Password123!&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;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;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;Works perfectly alone. Run it with two workers, or run it twice without resetting the database, and the second one hits "email already registered". The failure looks like a registration bug. It isn't. It's your test data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; generate unique data per test, never hardcode it.&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="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="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="s2"&gt;`qa.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt;@example.com`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Str0ng!Passw0rd&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A small factory module is worth writing. And — this sounds excessive until it saves you a day — write unit tests for the factory itself. A factory that silently produces duplicates causes failures that look exactly like application bugs, and you will spend hours in the wrong codebase before you suspect it. Asserting "2000 generated emails are all unique" takes thirty milliseconds and removes that entire class of confusion.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Every test logs in again
&lt;/h2&gt;

&lt;p&gt;Not flake exactly, but it compounds everything else. If each test starts by filling a login form, you're paying three to five seconds per test and giving yourself a hundred extra chances to hit a timing failure on the one flow that runs most often.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; log in once, save the session, reuse it.&lt;/p&gt;

&lt;p&gt;Make a setup project:&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;// tests/auth.setup.ts&lt;/span&gt;
&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authenticate&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="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;Username&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="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TEST_USERNAME&lt;/span&gt;&lt;span class="o"&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;Password&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="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TEST_PASSWORD&lt;/span&gt;&lt;span class="o"&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;Login&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;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;link&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;Logout&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="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;context&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;storageState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playwright/.auth/user.json&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it up as a dependency:&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;projects&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;setup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;testMatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/.*&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;setup&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;ts/&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;chromium&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;devices&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 Chrome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;storageState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playwright/.auth/user.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;dependencies&lt;/span&gt;&lt;span class="p"&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;setup&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a 200-test suite this routinely cuts several minutes off every run. It's usually the single biggest speed win available.&lt;/p&gt;

&lt;p&gt;One detail that matters: make the setup assert loudly that login actually succeeded. If it fails silently, every downstream test fails in a confusing way and you'll debug the wrong thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Locators tied to markup instead of meaning
&lt;/h2&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;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.btn-primary.submit-form &amp;gt; span:nth-child(2)&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;This passes today and breaks the next time a developer wraps something in a div. Worse, it breaks &lt;em&gt;silently&lt;/em&gt; in the sense that the failure tells you nothing about what actually changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; locate by what the user perceives.&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;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 order&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Priority order that holds up well:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;getByRole&lt;/code&gt; — survives redesigns, matches what users and screen readers see&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getByLabel&lt;/code&gt; — ties a field to its accessible label&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getByTestId&lt;/code&gt; — when the markup gives you nothing semantic&lt;/li&gt;
&lt;li&gt;CSS/XPath — last resort&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's a side benefit worth naming: if &lt;code&gt;getByRole&lt;/code&gt; and &lt;code&gt;getByLabel&lt;/code&gt; can't find your elements, that's usually a real accessibility gap in the app. Your test suite starts doubling as an a11y smoke check for free.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Visual tests that fail on everything
&lt;/h2&gt;

&lt;p&gt;Teams add visual regression, get failures on every unrelated change, and switch it off within a month. Three causes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Animations.&lt;/strong&gt; A screenshot taken mid-transition never matches. Disable them globally:&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;expect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;toHaveScreenshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;animations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disabled&lt;/span&gt;&lt;span class="dl"&gt;'&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.02&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;Font rendering across machines.&lt;/strong&gt; macOS and Linux anti-alias differently, so baselines generated locally will never match CI. Either generate baselines inside Docker so everyone compares against identical rendering, or keep a small pixel tolerance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Genuinely dynamic content.&lt;/strong&gt; Timestamps, avatars, ad slots. Mask them:&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="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;dashboard.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;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;time&lt;/span&gt;&lt;span class="dl"&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;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;.ad-slot&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And prefer component-level snapshots over full-page ones. A full-page baseline fails when anything anywhere changes. A snapshot of just the checkout form fails when the checkout form changes — which is the signal you actually wanted.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern underneath all five
&lt;/h2&gt;

&lt;p&gt;Every one of these is the same mistake in a different costume: &lt;strong&gt;encoding an assumption about timing or state instead of waiting for the real thing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A fixed delay assumes timing. A hardcoded email assumes no one else is running. A CSS-path locator assumes the DOM shape. A full-page snapshot assumes nothing else on the page ever changes.&lt;/p&gt;

&lt;p&gt;Replace each assumption with the actual condition and the flake goes away. Not "mostly goes away" — goes away. Flaky suites aren't an inherent property of browser testing, they're a property of suites built on assumptions.&lt;/p&gt;




&lt;h2&gt;
  
  
  A practical order to fix this in
&lt;/h2&gt;

&lt;p&gt;If you've inherited a flaky suite, don't rewrite it. Go in this order — cheapest and highest-impact first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add the lint rule&lt;/strong&gt; and let it find every &lt;code&gt;waitForTimeout&lt;/code&gt; for you&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add auth caching&lt;/strong&gt; — biggest speed win, touches one file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix the top 3 flakiest tests&lt;/strong&gt; — your CI history knows which ones they are&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace brittle locators&lt;/strong&gt; as you touch each file, not in one big sweep&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add data generation&lt;/strong&gt; when you next hit a collision&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You'll typically get most of the benefit from the first two.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I packaged these patterns — plus API testing, CI sharding and Docker setup — into a Playwright starter kit, since I kept rebuilding the same thing on every project. But everything in this article works on its own; none of it requires buying anything.&lt;/em&gt;&lt;/p&gt;

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