<?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: Muhammad Usman</title>
    <description>The latest articles on DEV Community by Muhammad Usman (@usmangq).</description>
    <link>https://dev.to/usmangq</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%2F304991%2Ffa227260-ad3c-40c2-bd0e-ee4c7911da28.jpeg</url>
      <title>DEV Community: Muhammad Usman</title>
      <link>https://dev.to/usmangq</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/usmangq"/>
    <language>en</language>
    <item>
      <title>AI-Driven Playwright E2E Tests From One Reference Doc</title>
      <dc:creator>Muhammad Usman</dc:creator>
      <pubDate>Mon, 20 Jul 2026 11:11:17 +0000</pubDate>
      <link>https://dev.to/usmangq/ai-driven-playwright-e2e-tests-from-one-reference-doc-5e7m</link>
      <guid>https://dev.to/usmangq/ai-driven-playwright-e2e-tests-from-one-reference-doc-5e7m</guid>
      <description>&lt;h2&gt;
  
  
  Writing E2E Tests With One Doc Instead of New Tooling
&lt;/h2&gt;

&lt;p&gt;We had a growing monorepo with two frontend apps and zero end-to-end coverage. The blocker was never Playwright itself. It was that every new feature meant babysitting an AI agent through the tests, click by click, and the output still needed rewriting by hand.&lt;/p&gt;

&lt;p&gt;Here is what actually fixed it, and the part I got wrong on the first try.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a dedicated package, not a shared test util
&lt;/h2&gt;

&lt;p&gt;The first instinct was to drop Playwright into the existing shared testing package. That does not work, for boring but real reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The unit-test runner and Playwright's runner cannot cohabit in one package.&lt;/li&gt;
&lt;li&gt;E2E runs must not be build-cached, while unit tests are cacheable. One package can't be both.&lt;/li&gt;
&lt;li&gt;Browser binaries and report output have no business leaking into a shared utility package.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the e2e suite is its own package, built from scratch. That separation is what let the rest of the decisions stay clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  The structure that made tests cheap to write
&lt;/h2&gt;

&lt;p&gt;Three rules, enforced by the setup rather than by review comments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Selectors live only in Page Object Models.&lt;/strong&gt; Spec files never contain a raw locator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page objects are injected as fixtures.&lt;/strong&gt; No manual instantiation in a spec, ever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth runs once.&lt;/strong&gt; A setup project logs in, saves the session state to disk, and every test loads it. No login flow inside a test.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is that a spec reads like the user story and nothing else:&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;user can open the create 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;createItemPage&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;createItemPage&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="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;createItemPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heading&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;Already authenticated, page object already wired in, no selector in sight.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I got wrong first
&lt;/h2&gt;

&lt;p&gt;With the structure in place, I expected the AI-assisted authoring to be the easy win. It was the opposite. Pointing Codegen and the agent at a page produced tests that looked plausible and were quietly wrong. The agent guessed expected values it had no way to know, and every generated spec came back to my desk for a manual rewrite. It was slower than just writing them myself.&lt;/p&gt;

&lt;p&gt;The missing piece was context. The agent knew how to click a button. It did not know what the feature was supposed to &lt;em&gt;do&lt;/em&gt; — the state machine, the validation limits, which behaviors were correct and which were known bugs.&lt;/p&gt;

&lt;p&gt;So I wrote a skill for one feature that spelled all of that out: the flows, the rules, the routes, the confirmed bugs. The next tests it wrote were good. Not "good for AI" good. Better than what I was writing by hand, because it now had the right direction and never lost the thread halfway through a complex flow.&lt;/p&gt;

&lt;p&gt;Then I generalized it.&lt;/p&gt;

&lt;h2&gt;
  
  
  One doc per feature, not a skill per feature
&lt;/h2&gt;

&lt;p&gt;A skill per feature does not scale. The insight was that the &lt;em&gt;skill&lt;/em&gt; was generic; only the &lt;em&gt;facts&lt;/em&gt; were feature-specific. So I split them.&lt;/p&gt;

&lt;p&gt;The authoring engine became one reusable skill. It reads a per-feature reference doc, discovers the routes, reuses or extends the page objects, drives the real app, captures the actual outcomes as assertions, then runs and self-heals the spec. On top of that sits a thin layer: the official Playwright browser-driving skill for the low-level interaction, and one dedicated skill for the single most complex domain that earned extra shortcuts.&lt;/p&gt;

&lt;p&gt;What a new feature needs now is one document: entities, state machine, validation rules, and a numbered list of known bugs. That is it. The reference doc is the contract. If it does not exist, the agent is told to stop and ask for it instead of inventing rules from the UI, which is exactly the failure mode that cost me the first week.&lt;/p&gt;

&lt;p&gt;Before: walk the agent through every flow, then rewrite the output.&lt;br&gt;
After: write one ground-truth doc, point the generic skill at it, review real tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reusable idea
&lt;/h2&gt;

&lt;p&gt;The lesson had nothing to do with Playwright. Agents do not fail e2e work because they can't drive a browser. They fail because they guess at behavior no one wrote down. Encode the business truth in a document the agent is forced to read, keep the authoring engine generic, and adding coverage for the next feature costs one doc instead of a new pile of tooling.&lt;/p&gt;

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