<?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: FlakeLab</title>
    <description>The latest articles on DEV Community by FlakeLab (@flakelab_dev).</description>
    <link>https://dev.to/flakelab_dev</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%2F4199264%2F23ee840e-4836-4bcd-8977-aab18648c3fe.png</url>
      <title>DEV Community: FlakeLab</title>
      <link>https://dev.to/flakelab_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/flakelab_dev"/>
    <language>en</language>
    <item>
      <title>360 green runs, and one test that would pass with the server down</title>
      <dc:creator>FlakeLab</dc:creator>
      <pubDate>Tue, 27 Oct 2026 04:36:30 +0000</pubDate>
      <link>https://dev.to/flakelab_dev/360-green-runs-and-one-test-that-would-pass-with-the-server-down-3hco</link>
      <guid>https://dev.to/flakelab_dev/360-green-runs-and-one-test-that-would-pass-with-the-server-down-3hco</guid>
      <description>&lt;p&gt;I gave an AI a Playwright + TypeScript repo and ten test scenarios. It wrote 13 tests. I ran every test in the repo 20 times with retries off: &lt;strong&gt;360 runs, 360 passed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then I read the tests. One of them would still pass if the server were down.&lt;/p&gt;

&lt;p&gt;This article covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the project structure the AI worked in, and why each piece is there&lt;/li&gt;
&lt;li&gt;how I measured the tests instead of trusting the green&lt;/li&gt;
&lt;li&gt;where the AI got it right, and where it went wrong&lt;/li&gt;
&lt;li&gt;a four-question checklist for reviewing AI-generated tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything is in a public repo: &lt;a href="https://github.com/flakelab-dev/playwright-structure" rel="noopener noreferrer"&gt;flakelab-dev/playwright-structure&lt;/a&gt;. The AI's raw output is committed before any fix, so you can check every claim here.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stable is not the same as correct.&lt;/strong&gt; 360/360 proved the tests don't flake. It didn't prove they check the right thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The AI wrote good code and followed the structure.&lt;/strong&gt; Its mistakes were about &lt;em&gt;what to check&lt;/em&gt;, not &lt;em&gt;how to write it&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It loosened an assertion to fit a bug,&lt;/strong&gt; and it copied a gap from my own tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Without a spec, the AI turns current behavior into expected behavior.&lt;/strong&gt; So do we, when we're in a hurry.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Seeded bugs told me more than 360 green runs.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The structure
&lt;/h2&gt;

&lt;p&gt;Two public demo apps: &lt;a href="https://www.saucedemo.com" rel="noopener noreferrer"&gt;SauceDemo&lt;/a&gt; for UI and &lt;a href="https://restful-booker.herokuapp.com/apidoc/index.html" rel="noopener noreferrer"&gt;Restful Booker&lt;/a&gt; for API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config/environments.ts   URLs per environment, picked with TEST_ENV
src/
  pages/                 Page objects: locators and actions, no business assertions
  api/                   Typed API client + Zod schemas
  fixtures/              Wires pages, clients and auth together
  data/                  Test data builders
tests/
  ui/                    SauceDemo tests
  api/                   Restful Booker tests
playwright.config.ts     Two projects, "ui" and "api"; retries off
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four decisions matter most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Fixtures do the setup. Tests only declare what they need.&lt;/strong&gt; A test that asks for &lt;code&gt;loggedInPage&lt;/code&gt; gets a browser that is already logged in:&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;adding a product updates the cart badge&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;loggedInPage&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;loggedInPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToCart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sauce Labs Backpack&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;loggedInPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cartBadge&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&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;&lt;strong&gt;2. The API client checks every response.&lt;/strong&gt; Methods like &lt;code&gt;createBooking()&lt;/code&gt; validate the body with a Zod schema, and a failed call reports the URL, status and body:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;jsonOrThrow&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="nx"&gt;APIResponse&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;unknown&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&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;ok&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&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="nx"&gt;response&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="s2"&gt; returned &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="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="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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="k"&gt;return&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Methods ending in &lt;code&gt;Raw&lt;/code&gt; return the response untouched, for tests about status codes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Test data is unique per run.&lt;/strong&gt; &lt;code&gt;buildBooking()&lt;/code&gt; adds a random suffix to the last name, so parallel tests can't collide on a shared public API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Retries are off.&lt;/strong&gt; Retries hide flakiness, and measuring flakiness is the point. The CI workflow can repeat the whole suite from the Actions tab.&lt;/p&gt;

&lt;p&gt;One more thing about structure: it is also what you give the AI. The AI doesn't read your mind, it reads your repo. Keep that in mind for later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The prompt was written and committed before generating.&lt;/strong&gt; (&lt;a href="https://github.com/flakelab-dev/playwright-structure/blob/main/experiments/01-ai-generated-tests/prompt.md" rel="noopener noreferrer"&gt;prompt.md&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fresh session:&lt;/strong&gt; Claude Opus 5.5 in Claude Code, with only the repo and the prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The output was committed exactly as produced,&lt;/strong&gt; before running or reviewing it. (&lt;a href="https://github.com/flakelab-dev/playwright-structure/commit/76fa7d1" rel="noopener noreferrer"&gt;raw output&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every test ran 20 times, retries off.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I reviewed every test by hand&lt;/strong&gt; against the code and the API docs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I ran the UI tests against users with deliberate bugs.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The prompt listed five UI scenarios (sorting, cart, checkout, form errors, logout) and five API scenarios (PUT, PATCH, filter, invalid payload, wrong credentials). It ended with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Rules: no hard waits, no retries, each test independent.
Do not run the tests; only write them.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why "do not run"? If the AI runs its tests, it fixes them until they pass, and I lose the first draft. I wanted to measure that draft.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 1: 360 out of 360
&lt;/h2&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;--repeat-each&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 13 generated tests plus the 5 already in the repo, 20 times each: 360 runs, 360 passed.&lt;/p&gt;

&lt;p&gt;That proves the tests are stable. It doesn't prove they are useful. A test that never fails might simply be unable to fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 2: seeding bugs
&lt;/h2&gt;

&lt;p&gt;SauceDemo has demo users with deliberate bugs. The &lt;code&gt;loggedInPage&lt;/code&gt; fixture reads the user from an environment variable, so I ran the same 7 UI tests as each user, 3 times each:&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="nv"&gt;UI_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;problem_user npx playwright &lt;span class="nb"&gt;test &lt;/span&gt;tests/ui/ai- &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ui &lt;span class="nt"&gt;--repeat-each&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;User&lt;/th&gt;
&lt;th&gt;Passed&lt;/th&gt;
&lt;th&gt;What failed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;standard_user&lt;/td&gt;
&lt;td&gt;21/21&lt;/td&gt;
&lt;td&gt;Nothing (control)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;problem_user&lt;/td&gt;
&lt;td&gt;9/21&lt;/td&gt;
&lt;td&gt;Sorting doesn't sort; the last name field ignores input, so checkout breaks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;error_user&lt;/td&gt;
&lt;td&gt;12/21&lt;/td&gt;
&lt;td&gt;Sorting doesn't sort; checkout can't finish; a missing last name isn't reported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;visual_user&lt;/td&gt;
&lt;td&gt;18/21&lt;/td&gt;
&lt;td&gt;The prices shown after sorting are not the prices shown before&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;performance_glitch_user&lt;/td&gt;
&lt;td&gt;17/21&lt;/td&gt;
&lt;td&gt;Not a bug in the app's logic: login is slow (see below)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I checked every failure: each one came from the seeded bug, not from a mistake in the test. The tests that stayed green did so because the bug wasn't on their path (logout works for every user).&lt;/p&gt;

&lt;p&gt;This is the result I trust more than 360/360. &lt;strong&gt;The generated UI tests catch real bugs.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A side lesson on flakiness
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;performance_glitch_user&lt;/code&gt; takes over 5 seconds to log in. Look at what that did:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Passed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sorting&lt;/td&gt;
&lt;td&gt;0/3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logout&lt;/td&gt;
&lt;td&gt;2/3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cart and checkout (5)&lt;/td&gt;
&lt;td&gt;15/15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same slowness, three different outcomes. The difference is the first step after login:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sorting and logout start with an &lt;strong&gt;assertion&lt;/strong&gt;: &lt;code&gt;expect(loggedInPage.title).toHaveText('Products')&lt;/code&gt;. Assertions give up after 5 seconds by default.&lt;/li&gt;
&lt;li&gt;Cart and checkout start with an &lt;strong&gt;action&lt;/strong&gt;: &lt;code&gt;addToCart()&lt;/code&gt;. Actions have no timeout of their own by default, so they keep waiting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Logout passed 2 times out of 3. That's what a flaky test looks like the first time you meet it. With retries on, it would have been green and the slow login would have gone unnoticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the AI got it right
&lt;/h2&gt;

&lt;p&gt;Before the mistakes, credit where it's due. Some of these I didn't ask for.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It followed the structure.&lt;/strong&gt; It used the fixtures, page objects and client. It added new page objects and client methods using the existing &lt;code&gt;Raw&lt;/code&gt; naming, and a data builder like the one already there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It used unique data.&lt;/strong&gt; The filter test searches for a first name like &lt;code&gt;Flake-3f9a1c2e&lt;/code&gt;, so other bookings on the shared public API can't match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It guarded the sorting test.&lt;/strong&gt; If the products already loaded in ascending order, the test would pass even with a broken sort button. The AI checked for that first:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Guard: if the default order were already ascending, the test would prove nothing.&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;pricesBefore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeGreaterThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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;pricesBefore&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&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;It declared its assumptions.&lt;/strong&gt; Its report listed four, like "Restful Booker returns 500 for an incomplete booking payload". All four are true today. None of them is documented.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where the AI went wrong
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. It loosened an assertion to fit a bug
&lt;/h3&gt;

&lt;p&gt;Scenario: "Creating a booking with an invalid payload is rejected."&lt;/p&gt;

&lt;p&gt;Restful Booker answers a booking with missing fields with &lt;strong&gt;500&lt;/strong&gt;. A validation error should be 400; a 500 means the server failed. The AI knew this, and wrote:&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;// Restful Booker answers 500 here rather than 400; the test checks rejection, not the code.&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;ok&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s2"&gt;`expected rejection, got &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="s2"&gt;`&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;false&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;toBeGreaterThanOrEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&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 with 400, with 500, and with 503 Service Unavailable. &lt;strong&gt;If the service were down, this test would still pass.&lt;/strong&gt; That's the test in the title.&lt;/p&gt;

&lt;p&gt;The AI didn't hide the bug, since it left a comment. But it wrote the assertion so the bug fits, and the test can no longer fail for the reason that matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. An error check that accepts any error
&lt;/h3&gt;

&lt;p&gt;Scenario: "Authenticating with wrong credentials 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;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;bookingClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiAdmin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&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-password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;rejects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toThrow&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;toThrow()&lt;/code&gt; with no message accepts any error: a network failure, a timeout, a typo in the URL.&lt;/p&gt;

&lt;p&gt;And here's why it passed. Restful Booker answers wrong credentials with &lt;strong&gt;200&lt;/strong&gt; and &lt;code&gt;{"reason":"Bad credentials"}&lt;/code&gt;. The client tried to read a token from that body, the schema check failed, and that error made the test pass. It passed because of a parsing error, not because the API rejected the login.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. It copied my gap
&lt;/h3&gt;

&lt;p&gt;The PUT, PATCH and filter tests create bookings and never delete them. That's about 60 abandoned bookings per 20-run session, on a public service that other people use.&lt;/p&gt;

&lt;p&gt;The test I wrote by hand had the same gap. The AI followed the pattern it was given, including the bad part.&lt;/p&gt;

&lt;p&gt;That's the lesson I keep from this experiment: &lt;strong&gt;AI multiplies your repo's conventions, good and bad.&lt;/strong&gt; Fix the base before you generate on top of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Minor: duplication
&lt;/h3&gt;

&lt;p&gt;The same locators (page title, item names, cart badge) are defined in three page objects, and four checkout tests repeat the same setup. They aren't bugs, so I left them for a later refactor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger problem: there was no spec
&lt;/h2&gt;

&lt;p&gt;SauceDemo has no documentation. Every expected value in the UI tests (the error messages, the empty cart after checkout, the text after logout) came from what the app does today.&lt;/p&gt;

&lt;p&gt;When I reviewed them, I got stuck on one question: if the app has a bug today, should the test adapt to it, or fail? And based on what?&lt;/p&gt;

&lt;p&gt;Without a spec, these tests are &lt;strong&gt;characterization tests&lt;/strong&gt;: they detect &lt;em&gt;change&lt;/em&gt;, not &lt;em&gt;correctness&lt;/em&gt;. That's still useful (the seeded bugs prove it), but it's worth naming. I marked all 7 UI tests "⚠️ Oracle": the expected behavior isn't backed by a specification.&lt;/p&gt;

&lt;p&gt;The API has docs, so the review was clearer. PUT, PATCH and filter match the docs. The wrong-credentials response (200 with a &lt;code&gt;reason&lt;/code&gt;) isn't described in the docs at all, and the test treats it as correct.&lt;/p&gt;

&lt;p&gt;Final count for the 13 tests:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;Tests&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;✅ Good as is&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⚠️ Oracle: expected value not backed by a spec&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⚠️ Weak: wouldn't catch the failure&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;❌ Wrong&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The AI did what we all do in a hurry: it looked at current behavior and wrote it down as expected behavior. At least it listed its assumptions. &lt;strong&gt;Read that list: it's where the oracle problems are.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The fixes
&lt;/h2&gt;

&lt;p&gt;Each fix is a separate commit on top of the AI's output, so you can compare before and after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Invalid payload&lt;/strong&gt; (&lt;a href="https://github.com/flakelab-dev/playwright-structure/commit/bed0c5d" rel="noopener noreferrer"&gt;bed0c5d&lt;/a&gt;): assert the exact status and record the bug.&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;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;annotations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;known bug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Returns 500 instead of 400 for missing fields&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="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;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the test fails if the service is down (503), and it fails if the bug gets fixed (400), which is exactly when you want to update it. The annotation shows up in the HTML report, so the bug stays visible.&lt;/p&gt;

&lt;p&gt;I considered &lt;code&gt;test.fail()&lt;/code&gt; and rejected it: it treats &lt;em&gt;any&lt;/em&gt; failure as expected, including an outage. Same problem as before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong credentials&lt;/strong&gt; (&lt;a href="https://github.com/flakelab-dev/playwright-structure/commit/c272114" rel="noopener noreferrer"&gt;c272114&lt;/a&gt;): the client now throws &lt;code&gt;Authentication failed: &amp;lt;reason&amp;gt;&lt;/code&gt; when the API rejects the login, and the test asserts that exact message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cleanup&lt;/strong&gt; (&lt;a href="https://github.com/flakelab-dev/playwright-structure/commit/5b5ce0b" rel="noopener noreferrer"&gt;5b5ce0b&lt;/a&gt;): the &lt;code&gt;bookingClient&lt;/code&gt; fixture deletes every booking the test created, after the test ends. To check it, a temporary test created a booking; after the run, a GET for that booking returned 404.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist for AI-generated tests
&lt;/h2&gt;

&lt;p&gt;Ask these four questions about every test:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Oracle:&lt;/strong&gt; where does each expected value come from: a spec, or what the app does today?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sensitivity:&lt;/strong&gt; if the feature broke, would this test fail? Better than guessing: break it and see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolation:&lt;/strong&gt; does it depend on data it doesn't control, or leave data behind?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure:&lt;/strong&gt; does it use the fixtures, page objects and client, or bypass them?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this experiment, questions 1 and 2 found every real problem. Question 4 is the one the AI already handles well.&lt;/p&gt;

&lt;p&gt;One tip from my own review: read the support code before judging a test. I flagged a missing status check in the API tests, and it was already there, in the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/flakelab-dev/playwright-structure
&lt;span class="nb"&gt;cd &lt;/span&gt;playwright-structure
npm ci
npx playwright &lt;span class="nb"&gt;install &lt;/span&gt;chromium
npm &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/flakelab-dev/playwright-structure/blob/main/experiments/01-ai-generated-tests/review.md" rel="noopener noreferrer"&gt;Full review sheet&lt;/a&gt; with every verdict and note&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/flakelab-dev/playwright-structure/blob/main/experiments/01-ai-generated-tests/prompt.md" rel="noopener noreferrer"&gt;The prompt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/flakelab-dev/playwright-structure/commit/76fa7d1" rel="noopener noreferrer"&gt;The AI's raw output&lt;/a&gt;, before any fix&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run &lt;code&gt;npm run test:repeat&lt;/code&gt; to measure it yourself, or set &lt;code&gt;UI_USER&lt;/code&gt; to one of the broken users and watch the tests catch the bugs.&lt;/p&gt;




&lt;p&gt;How do you review AI-generated tests today? I'd like to hear what you check first.&lt;/p&gt;

&lt;p&gt;I publish one measured experiment like this at a time. If you want the next one by email: &lt;a href="https://flakelab.dev/subscribe/" rel="noopener noreferrer"&gt;flakelab.dev/subscribe&lt;/a&gt;&lt;/p&gt;

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