<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>BDR Methodology | Blog</title><description/><link>https://bdr-methodology.dev/</link><language>en</language><item><title>Why Your Test Suite Lies to You at Scale</title><link>https://bdr-methodology.dev/blog/why_your_test_suite_lies_to_you_at_scale/</link><guid isPermaLink="true">https://bdr-methodology.dev/blog/why_your_test_suite_lies_to_you_at_scale/</guid><pubDate>Sat, 16 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://bdr-methodology.dev/images/mediumdata.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;div&gt;&lt;h1 id=&quot;why-your-test-suite-lies-to-you-at-scale-&quot;&gt;Why Your Test Suite Lies to You at Scale &lt;span&gt; PRO IMPLEMENTATION &lt;/span&gt;&lt;/h1&gt;&lt;/div&gt;
&lt;p&gt;&lt;em&gt;New to Playwright reliability? Start with the fundamentals: &lt;strong&gt;&lt;a href=&quot;https://bdr-methodology.dev/blog/flaky_tests_youcant_fix_with_better_selectors/&quot;&gt;Flaky Tests You Can’t Fix With Better Selectors&lt;/a&gt;&lt;/strong&gt; — the same concepts with more explanation and simpler examples.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Green tests and broken production is a specific failure mode that gets more common as test suites grow. The locators are right, the assertions are correct, the mocks return the expected data — and none of it reflects what the system actually does under load, with real network conditions, against a real database.&lt;/p&gt;
&lt;p&gt;This article covers three architectural problems that cause this: API non-idempotency, mock drift, and data accumulation. Each is invisible at small scale. Each becomes expensive at large scale.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Code examples are intentionally simplified — focus on the architectural pattern.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-failure-mode-nobody-talks-about&quot;&gt;The Failure Mode Nobody Talks About&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Most flakiness guides focus on selectors and timing. That’s the visible layer. The invisible layer is data and integration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A POST request succeeds on the server, the response is lost in transit, Playwright retries, the server creates a second record. Your test now has two orders instead of one, and the assertion that checks order count fails — not because the feature is broken, but because the network hiccuped.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Your mock returns &lt;code dir=&quot;auto&quot;&gt;{ order_id: &quot;123&quot; }&lt;/code&gt;. The backend deployed last Tuesday and now returns &lt;code dir=&quot;auto&quot;&gt;{ orderId: &quot;123&quot; }&lt;/code&gt;. Tests are green. The field your frontend reads is undefined. Production is broken.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tests create 100 users per minute. Nobody cleans up reliably. Two weeks later, unique constraint violations start appearing in unrelated tests. The database that was supposed to be isolated is shared state in disguise.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These aren’t test bugs. They’re architectural gaps. And they require architectural solutions.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;idempotency-making-post-requests-safe-to-retry&quot;&gt;Idempotency: Making POST Requests Safe to Retry&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The standard mental model of HTTP: a request either succeeds or fails. The reality: a request can succeed on the server and fail to deliver the response. The client sees a timeout and retries. The server sees a new request.&lt;/p&gt;
&lt;p&gt;For GET requests this is harmless. For POST requests that create or modify state, it creates duplicates.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The solution: idempotency keys&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;An idempotency key is a client-generated identifier that the server uses to detect duplicate requests. If the server has processed a request with this key before, it returns the cached result instead of processing again.&lt;/p&gt;
&lt;p&gt;The key design question is how to generate the key. A static key per test fails when a test makes multiple POST requests — the server treats the second request as a duplicate of the first. A random UUID per request defeats the purpose — retries get new keys and bypass the deduplication.&lt;/p&gt;
&lt;p&gt;The correct approach: derive the key deterministically from the request context.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;api/infrastructure/idempotency.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { createHash } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;crypto&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;generateIdempotencyKey&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;method&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;payload&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;method&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(data)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;createHash&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;sha256&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;update&lt;/span&gt;&lt;span&gt;(payload)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;digest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;hex&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;slice&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;16&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;api/clients/BaseApiClient.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;abstract&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BaseApiClient&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;protected&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;generateIdempotencyKey&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;data);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;(url, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;data,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;headers: { &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;X-Idempotency-Key&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: key },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Two calls to &lt;code dir=&quot;auto&quot;&gt;createUser&lt;/code&gt; with identical data get identical keys — the server deduplicates. Two calls with different data (create user, then create order) get different keys — both process correctly.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important nuance:&lt;/strong&gt; if your test legitimately needs two identical records (same method, URL, and body), they’ll get the same key — and the server will return the cached result for the second call. This is correct behaviour for retries, but it means this approach assumes each unique operation has unique data. If you genuinely need two identical resources, add a distinguishing field (like a &lt;code dir=&quot;auto&quot;&gt;requestId&lt;/code&gt; or timestamp) to the body.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;The backend requirement:&lt;/strong&gt; this only works if the server implements idempotency key handling. Most payment APIs (Stripe, PayPal) support this natively. If your payment provider doesn’t — that’s their problem to solve, not yours. Use WireMock to mock them, or find their sandbox/test mode. If it’s your own internal backend that’s missing support — that’s a tech-debt conversation with your backend team. The pattern is well-documented and the database cost is minimal: store key + response hash, expire after 24 hours.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The network failure scenario:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Client → POST /orders (key: abc123) → Server processes, creates order&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Server → Response lost in transit&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Client → Timeout, retry POST /orders (key: abc123) → Server returns cached response&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Result: One order, correct state&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Without idempotency keys, the retry creates a second order. Your test’s assertion that checks order count fails, and you spend an hour investigating a “bug” that is actually a network reliability issue.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;mock-architecture-three-levels-three-use-cases&quot;&gt;Mock Architecture: Three Levels, Three Use Cases&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The mistake teams make is treating mocking as a single tool. &lt;code dir=&quot;auto&quot;&gt;page.route&lt;/code&gt; for everything. Then wondering why server-side failures aren’t caught.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Level 1: Native mocks (&lt;code dir=&quot;auto&quot;&gt;page.route&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;page.route&lt;/code&gt; intercepts requests made from inside the browser context. It’s the right tool for testing UI behavior in isolation.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Testing error state UI&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;route&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;**/api/orders&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;route&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;route&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fulfill&lt;/span&gt;&lt;span&gt;({ status: &lt;/span&gt;&lt;span&gt;503&lt;/span&gt;&lt;span&gt;, body: &lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;({ error: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Service unavailable&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; }) });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;goto&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/orders&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;alert&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toContainText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Service unavailable&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The architectural boundary: &lt;code dir=&quot;auto&quot;&gt;page.route&lt;/code&gt; cannot intercept requests made via Playwright’s &lt;code dir=&quot;auto&quot;&gt;request&lt;/code&gt; fixture, or any server-to-server calls your backend makes. Those requests originate outside the browser context.&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;How the request is made&lt;/th&gt;&lt;th&gt;Intercepted by &lt;code dir=&quot;auto&quot;&gt;page.route&lt;/code&gt;?&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;page.goto()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;page.click()&lt;/code&gt; — browser navigation&lt;/td&gt;&lt;td&gt;✅ Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;page.evaluate(() =&gt; fetch(&apos;/api/...&apos;))&lt;/code&gt; — fetch inside browser&lt;/td&gt;&lt;td&gt;✅ Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;page.request.get(&apos;/api/...&apos;)&lt;/code&gt; — browser request context&lt;/td&gt;&lt;td&gt;✅ Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;request.get(&apos;/api/...&apos;)&lt;/code&gt; — standalone &lt;code dir=&quot;auto&quot;&gt;request&lt;/code&gt; fixture (Node.js)&lt;/td&gt;&lt;td&gt;❌ No&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Backend server-to-server calls (Stripe, etc.)&lt;/td&gt;&lt;td&gt;❌ No&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The distinction is browser context vs Node.js context — not UI vs API.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code dir=&quot;auto&quot;&gt;route.fulfill()&lt;/code&gt; instead of &lt;code dir=&quot;auto&quot;&gt;route.abort()&lt;/code&gt;?&lt;/strong&gt; &lt;code dir=&quot;auto&quot;&gt;abort()&lt;/code&gt; causes the request to fail with a network error. Well-written apps handle this gracefully, but many enter an infinite retry loop waiting for a response that never comes. &lt;code dir=&quot;auto&quot;&gt;fulfill()&lt;/code&gt; returns a proper HTTP response — even a synthetic one — so the app moves on cleanly. Use &lt;code dir=&quot;auto&quot;&gt;abort()&lt;/code&gt; only when you specifically want to test network error handling.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Level 2: Infrastructure mocks (WireMock)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Server-to-server integrations — payment processors, SMS gateways, shipping APIs — need to be mocked at the network level, not the browser level.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;docker-compose.yml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;wiremock&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;image&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;wiremock/wiremock:3.3.1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;ports&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;8080:8080&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;volumes&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;./wiremock/mappings:/home/wiremock/mappings&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;command&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;--global-response-templating&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;--verbose&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;wiremock/mappings/stripe.json&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;request&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;method&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;urlPattern&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/v1/payment_intents&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;response&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;status&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;200&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;jsonBody&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;pi_{{randomValue length=24 type=&apos;ALPHANUMERIC&apos;}}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;status&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;succeeded&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;amount&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;{{request.body.amount}}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;transformers&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;response-template&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Response templating lets WireMock echo back request values, making mocks feel more realistic without hardcoding specific values. Point your backend’s external API base URLs to &lt;code dir=&quot;auto&quot;&gt;localhost:8080&lt;/code&gt; via environment variables, and the backend never makes real external calls in tests.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One prerequisite:&lt;/strong&gt; your backend needs to use configurable base URLs for external services — not hardcoded production endpoints. In well-structured backends this is already the case. If it’s not, that’s a refactor worth doing regardless of testing — hardcoded external URLs are a deployment problem too.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Level 3: Contract testing (Pact)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;WireMock solves availability. It doesn’t solve drift. Your WireMock mapping can become outdated the moment the real API changes. This is the Lying Mock problem — and it requires a different solution.&lt;/p&gt;
&lt;p&gt;Consumer-Driven Contract Testing (CDC) creates a formal, verifiable link between your test expectations and the provider’s actual implementation.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;tests/contracts/orders.pact.spec.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { PactV3, MatchersV3 } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@pact-foundation/pact&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const { &lt;/span&gt;&lt;span&gt;like&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;integer&lt;/span&gt;&lt;span&gt; } = &lt;/span&gt;&lt;span&gt;MatchersV3;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;provider&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PactV3&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;consumer: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;test-suite&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;provider: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;order-service&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;dir: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;./pacts&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;logLevel: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Order Service contract&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;returns order details&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; provider&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;given&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;order ord_123 exists&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;uponReceiving&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;GET /orders/ord_123&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;withRequest&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;method: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;GET&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;path: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/orders/ord_123&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;headers: { Authorization: &lt;/span&gt;&lt;span&gt;like&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Bearer token&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;) },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;willRespondWith&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;status: &lt;/span&gt;&lt;span&gt;200&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;body: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;order_id: &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;ord_123&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;), &lt;/span&gt;&lt;span&gt;// field name is part of the contract&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;status: &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;CONFIRMED&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;total: &lt;/span&gt;&lt;span&gt;integer&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;4999&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;executeTest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;mockServer&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;order&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;fetchOrder&lt;/span&gt;&lt;span&gt;(mockServer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;ord_123&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(order&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;CONFIRMED&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This test runs against a local mock server and generates a &lt;code dir=&quot;auto&quot;&gt;./pacts/test-suite-order-service.json&lt;/code&gt; contract file. The backend team publishes this contract to a Pact Broker and runs verification against their actual code:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# On the provider side, in their CI pipeline&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pact-provider-verifier&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--provider-base-url&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;http://localhost:8080&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--pact-broker-url&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;https://your-pact-broker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--provider&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;order-service&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--publish-verification-results&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If the backend renames &lt;code dir=&quot;auto&quot;&gt;order_id&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;orderId&lt;/code&gt;, verification fails in their pipeline before the change merges. The contract breaks at the source, not in production.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Pact Broker&lt;/strong&gt; is optional but valuable — it stores contract versions, tracks which consumer-provider pairs are compatible, and enables the &lt;code dir=&quot;auto&quot;&gt;can-i-deploy&lt;/code&gt; check that blocks deployments when contracts are broken. For smaller teams, storing contract files in a shared repository works as a simpler alternative.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Where to start with contracts:&lt;/strong&gt; don’t try to contract-test everything. Start with the API calls that have caused the most incidents, or the ones that change most frequently. One contract on your critical payment or order flow is immediately valuable. Expand from there.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The organizational reality:&lt;/strong&gt; contract testing requires the backend team to run verification in their pipeline. This is a commitment from both sides, not just a technical decision. For small teams or teams without strong cross-team coordination, a simpler starting point is storing contract JSON files in the backend repo and running verification manually — no Pact Broker required. Also worth being explicit: contracts verify response structure and field names. They don’t catch business logic bugs, side effects, or behaviour changes that preserve the schema.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;data-hygiene-the-infrastructure-approach&quot;&gt;Data Hygiene: The Infrastructure Approach&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;afterEach(() =&gt; api.deleteUser(userId))&lt;/code&gt; is the standard cleanup pattern. It has two failure modes that make it unreliable at scale:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;If the test crashes before &lt;code dir=&quot;auto&quot;&gt;userId&lt;/code&gt; is set, the cleanup never runs&lt;/li&gt;
&lt;li&gt;If the test runner itself crashes or is killed, &lt;code dir=&quot;auto&quot;&gt;afterAll&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;afterEach&lt;/code&gt; hooks don’t execute&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The result: orphaned test data accumulates. Unique constraints start failing on unrelated tests. Query performance degrades. The “isolated” test database becomes shared state.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Approach 1: TTL at the database level&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Add &lt;code dir=&quot;auto&quot;&gt;expires_at&lt;/code&gt; to all test-created entities and set it to a short window:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// In your base API client or fixture&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;protected async &lt;/span&gt;&lt;span&gt;createTestEntity&lt;/span&gt;&lt;span&gt;(url: string, data: unknown) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;(url, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;data,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;expires_at: &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Date&lt;/span&gt;&lt;span&gt;(Date&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;24&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;60&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;60&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1000&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toISOString&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;is_test: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The database handles cleanup automatically. In PostgreSQL with &lt;code dir=&quot;auto&quot;&gt;pg_cron&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- Install pg_cron extension once&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- Note: pg_cron may not be available on all managed PostgreSQL services (e.g. some cloud providers).&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- If unavailable, use a server-level cron job or a background worker instead.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; EXTENSION &lt;/span&gt;&lt;span&gt;IF&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NOT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;EXISTS&lt;/span&gt;&lt;span&gt; pg_cron;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- Schedule cleanup every hour&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cron&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;schedule&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;cleanup-test-entities&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;0 * * * *&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, $$&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;DELETE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; users&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; expires_at &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NOW&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; is_test &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; true;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;DELETE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; orders&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; expires_at &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NOW&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; is_test &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; true;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;DELETE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; payment_intents&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; expires_at &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NOW&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; is_test &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; true;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$$);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;In MongoDB, a TTL index handles this natively:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;users&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;createIndex&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ expires_at: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ expireAfterSeconds: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;// documents deleted at expires_at time&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Approach 2: Cleanup queue with global teardown&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For cases where TTL isn’t practical — databases that don’t support it, or entities that need ordered cleanup (delete orders before users, not after):&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;cleanup/queue.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; CleanupItem {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;priority&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;// higher priority = deleted first&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CleanupQueue&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; items&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CleanupItem&lt;/span&gt;&lt;span&gt;[] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;item&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;CleanupItem&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;items&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(item);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;flush&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;APIRequestContext&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sorted&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;items&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sort&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;priority&lt;/span&gt;&lt;span&gt; - &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;priority&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;item&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;of&lt;/span&gt;&lt;span&gt; sorted) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;delete&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;item&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;item&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// Log but don&apos;t throw — cleanup failures shouldn&apos;t fail the suite&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Cleanup failed for &lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;item&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;item&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;items&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;cleanupQueue&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CleanupQueue&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;global-teardown.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { cleanupQueue } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;./cleanup/queue&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;globalTeardown&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; cleanupQueue&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;flush&lt;/span&gt;&lt;span&gt;(globalApiClient);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The cleanup queue survives individual test failures. Only a full runner crash (SIGKILL, power loss) prevents it from executing — and in that case, the TTL approach serves as a second line of defense. This is why TTL should be your default: it operates at the database level, independently of your test process, and survives any kind of crash. The cleanup queue is a complement for ordered cleanup, not a replacement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Approach 3: Table partitioning for high-volume environments&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When tests run continuously and create thousands of entities per hour, even scheduled deletes can become expensive. Deleting a million rows from a PostgreSQL table is a slow, lock-intensive operation.&lt;/p&gt;
&lt;p&gt;Partitioning by date makes cleanup instantaneous — you drop a partition rather than deleting rows:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- Create partitioned table&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TABLE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;orders_test&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;id UUID &lt;/span&gt;&lt;span&gt;PRIMARY KEY&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;created_at &lt;/span&gt;&lt;span&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NOT NULL&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;expires_at &lt;/span&gt;&lt;span&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- other fields&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;PARTITION&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BY&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;RANGE&lt;/span&gt;&lt;span&gt; (created_at);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- Create monthly partitions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TABLE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;orders_test_2024_12&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;PARTITION&lt;/span&gt;&lt;span&gt; OF orders_test&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;FOR&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;2024-12-01&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;TO&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;2025-01-01&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TABLE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;orders_test_2025_01&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;PARTITION&lt;/span&gt;&lt;span&gt; OF orders_test&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;FOR&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;2025-01-01&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;TO&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;2025-02-01&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Dropping last month’s partition:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- Instantaneous, no table lock on the live partition&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;DROP&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TABLE&lt;/span&gt;&lt;span&gt; orders_test_2024_12;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This is worth the setup complexity when your test suite creates more than ~10K entities per day. Below that threshold, the TTL approach is simpler and sufficient.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Limitations worth knowing:&lt;/strong&gt; partitioning is PostgreSQL-native and well-supported, but MySQL’s implementation has more restrictions, and some ORMs handle partitioned tables poorly. More importantly, partitioning complicates migrations — adding a column to a partitioned table requires updating all existing partitions. And you need to create future partitions in advance — either manually or via a scheduled job. Don’t reach for this pattern unless you’re genuinely hitting performance problems with TTL-based cleanup.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-decision-framework&quot;&gt;The Decision Framework&lt;/h2&gt;&lt;/div&gt;





































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Situation&lt;/th&gt;&lt;th&gt;Right tool&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Testing UI error states in isolation&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;page.route&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Backend calls external payment/SMS API&lt;/td&gt;&lt;td&gt;WireMock&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Backend API changes cause test failures&lt;/td&gt;&lt;td&gt;Contract tests (Pact)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Test creates &amp;#x3C; 1K entities/day&lt;/td&gt;&lt;td&gt;TTL + pg_cron&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cleanup order matters&lt;/td&gt;&lt;td&gt;Cleanup queue&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Test creates &gt; 10K entities/day&lt;/td&gt;&lt;td&gt;Table partitioning&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;POST request creates duplicates on retry&lt;/td&gt;&lt;td&gt;Idempotency keys&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;what-this-solves&quot;&gt;What This Solves&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The patterns here don’t make individual tests faster or more readable. They make the test suite trustworthy at scale — which is a different problem.&lt;/p&gt;
&lt;p&gt;A suite that’s trustworthy means: when tests are green, you can deploy with confidence. When tests fail, the failure points to a real problem, not a network hiccup or a stale mock. When a test fails in CI, you can reproduce it locally with the same data.&lt;/p&gt;
&lt;p&gt;That’s the gap between a test suite that’s a liability and one that’s an asset.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Reference implementation: &lt;a href=&quot;https://github.com/dmitryAQA/playwright-bdr-template&quot;&gt;Playwright BDR Template&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title>Flaky Tests You Can&apos;t Fix With Better Selectors</title><link>https://bdr-methodology.dev/blog/flaky_tests_youcant_fix_with_better_selectors/</link><guid isPermaLink="true">https://bdr-methodology.dev/blog/flaky_tests_youcant_fix_with_better_selectors/</guid><pubDate>Fri, 15 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://bdr-methodology.dev/images/devtodata.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;div&gt;&lt;h1 id=&quot;flaky-tests-you-cant-fix-with-better-selectors-&quot;&gt;Flaky Tests You Can’t Fix With Better Selectors &lt;span&gt; CONCEPT &lt;/span&gt;&lt;/h1&gt;&lt;/div&gt;
&lt;p&gt;You’ve fixed your &lt;a href=&quot;https://bdr-methodology.dev/blog/why_your_playwright_tests_fail_in_ci_base/#rule-3-use-the-right-locators--and-know-why&quot;&gt;locators&lt;/a&gt;. You’ve switched to &lt;a href=&quot;https://bdr-methodology.dev/blog/why_your_playwright_tests_fail_in_ci_base/#rule-4-stop-using-isvisible-in-assertions&quot;&gt;web-first assertions&lt;/a&gt;. Your tests still fail intermittently — but now the failures look different. Duplicate records in the database. Tests that pass alone but fail in parallel. Mocks that say everything is fine while production is broken.&lt;/p&gt;
&lt;p&gt;This is the next layer of flakiness. It lives in your API calls, your test doubles, and your database. Better selectors won’t help here.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Code examples are simplified for clarity — focus on the idea, not the boilerplate.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;tldr&quot;&gt;TL;DR&lt;/h2&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Use idempotency keys on POST requests — one network glitch shouldn’t create two orders&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;page.route&lt;/code&gt; mocks the browser, not your server — know the difference&lt;/li&gt;
&lt;li&gt;Use WireMock for server-to-server integrations you can’t control&lt;/li&gt;
&lt;li&gt;Contract tests catch API drift before it reaches production&lt;/li&gt;
&lt;li&gt;Never rely on &lt;code dir=&quot;auto&quot;&gt;afterEach&lt;/code&gt; for database cleanup — use TTL or a cleanup queue instead&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-problem-flakiness-that-looks-like-application-bugs&quot;&gt;The Problem: Flakiness That Looks Like Application Bugs&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;When a selector fails, the error is obvious. When a test creates a duplicate order because a network request was retried, the error looks like a business logic bug. You spend an hour investigating something that has nothing to do with your application code.&lt;/p&gt;
&lt;p&gt;Three categories cause this:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;API flakiness&lt;/strong&gt; — a request succeeds on the server but the response never arrives. Playwright retries. Now you have two orders.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lying mocks&lt;/strong&gt; — your mocks say the API returns &lt;code dir=&quot;auto&quot;&gt;{ order_id: &quot;123&quot; }&lt;/code&gt;. The backend deployed last week and now returns &lt;code dir=&quot;auto&quot;&gt;{ orderId: &quot;123&quot; }&lt;/code&gt;. Tests are green. Production is broken.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Data pollution&lt;/strong&gt; — tests create users, orders, and transactions but don’t clean up reliably. After a week, the test database is a graveyard that slows down queries and causes unique constraint violations.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-1-idempotency-keys--one-request-one-result&quot;&gt;Rule #1: Idempotency Keys — One Request, One Result&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Networks are unreliable. A POST request can reach the server, create a record, and then the response gets lost in transit. Playwright sees a timeout and retries. The server sees a new request and creates another record.&lt;/p&gt;
&lt;p&gt;The fix is an idempotency key — a unique header that tells the server “if you’ve seen this request before, return the same result instead of processing it again.”&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;api/infrastructure/idempotency.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { createHash } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;crypto&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;generateIdempotencyKey&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;method&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;payload&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;method&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(data)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;createHash&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;sha256&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;update&lt;/span&gt;&lt;span&gt;(payload)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;digest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;hex&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;slice&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;16&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;api/clients/BaseApiClient.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;abstract&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BaseApiClient&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;protected&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;generateIdempotencyKey&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;data);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;(url, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;data,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;headers: { &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;X-Idempotency-Key&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: key },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The key is generated from the request method, URL, and body — so two identical requests get the same key, but two different requests (create user, then create order) get different keys. One network glitch no longer creates two records.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important nuance:&lt;/strong&gt; if your test legitimately creates two identical orders (same body, same URL), they’ll get the same key — and the server will return the first result for both. This is intentional behaviour for retries, but it means this approach assumes each unique operation has unique data. If you need two genuinely identical records, add a unique field (like &lt;code dir=&quot;auto&quot;&gt;requestId&lt;/code&gt;) to the body.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This only works if your backend handles the &lt;code dir=&quot;auto&quot;&gt;X-Idempotency-Key&lt;/code&gt; header. Check with your backend team — many order APIs support this out of the box. If your payment provider doesn’t support it — that’s their problem to solve, not yours. Look for their sandbox or test mode, or use WireMock to mock them entirely. If it’s your own backend that’s missing support — that’s a tech-debt conversation with your backend team, not something to work around in tests.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-2-know-what-your-mocks-actually-cover&quot;&gt;Rule #2: Know What Your Mocks Actually Cover&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;page.route&lt;/code&gt; is Playwright’s built-in way to intercept requests. It’s great for testing UI behavior in isolation — how does the page look when the API returns an error?&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// ✅ Good use of page.route — testing UI error state&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;route&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;**/api/orders&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;route&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;route&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fulfill&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;status: &lt;/span&gt;&lt;span&gt;500&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;body: &lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;({ error: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Internal Server Error&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; }),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;goto&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/orders&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Something went wrong&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeVisible&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;The catch:&lt;/strong&gt; &lt;code dir=&quot;auto&quot;&gt;page.route&lt;/code&gt; only intercepts requests made from inside the browser. If your test makes API calls directly through Playwright’s &lt;code dir=&quot;auto&quot;&gt;request&lt;/code&gt; fixture — server-side, without a browser — &lt;code dir=&quot;auto&quot;&gt;page.route&lt;/code&gt; won’t see them.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// This request bypasses page.route entirely&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/api/orders&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { data: &lt;/span&gt;&lt;span&gt;orderData&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// But this goes through the browser context and IS intercepted by page.route&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;evaluate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; =&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/api/orders&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { method: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code dir=&quot;auto&quot;&gt;route.fulfill()&lt;/code&gt; instead of &lt;code dir=&quot;auto&quot;&gt;route.abort()&lt;/code&gt;?&lt;/strong&gt; &lt;code dir=&quot;auto&quot;&gt;abort()&lt;/code&gt; causes the request to fail with a network error. Some apps handle this gracefully, but others enter an infinite retry loop waiting for a response that never comes. &lt;code dir=&quot;auto&quot;&gt;fulfill()&lt;/code&gt; returns a proper HTTP response (even a fake one) so the app moves on cleanly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For direct API calls in tests, you need mocks at a different level — either a wrapper around &lt;code dir=&quot;auto&quot;&gt;request&lt;/code&gt;, or an infrastructure mock like WireMock.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-3-wiremock-for-integrations-you-dont-control&quot;&gt;Rule #3: WireMock for Integrations You Don’t Control&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Your backend calls Stripe for payments. It calls Twilio for SMS. It calls a shipping provider to get rates. In tests, you don’t want any of that to actually happen.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;page.route&lt;/code&gt; can’t help here — these are server-to-server calls that never touch the browser. The solution is WireMock: a mock server that runs alongside your test environment and intercepts HTTP calls at the network level.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;docker-compose.yml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;wiremock&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;image&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;wiremock/wiremock:3.3.1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;ports&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;8080:8080&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;volumes&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;./wiremock/mappings:/home/wiremock/mappings&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;wiremock/mappings/stripe-payment.json&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;request&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;method&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;url&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/v1/payment_intents&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;response&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;status&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;200&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;jsonBody&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;pi_test_123&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;status&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;succeeded&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Now your backend hits &lt;code dir=&quot;auto&quot;&gt;localhost:8080&lt;/code&gt; in tests instead of the real Stripe API. Tests are fast, isolated, and don’t depend on external uptime.&lt;/p&gt;
&lt;p&gt;Point your backend’s base URLs to WireMock via environment variables in your test environment:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;STRIPE_BASE_URL&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;http://localhost:8080&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;TWILIO_BASE_URL&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;http://localhost:8080&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One thing to be aware of:&lt;/strong&gt; this requires your backend to use configurable base URLs for external services. In most well-structured backends this is already the case. If it’s not — that’s a conversation with the backend team, not a reason to skip WireMock.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-4-contract-tests--stop-trusting-your-mocks&quot;&gt;Rule #4: Contract Tests — Stop Trusting Your Mocks&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here’s the problem with all mocks: they can lie. Your WireMock returns &lt;code dir=&quot;auto&quot;&gt;{ payment_id: &quot;pay_123&quot; }&lt;/code&gt;. The backend team renames the field to &lt;code dir=&quot;auto&quot;&gt;paymentId&lt;/code&gt;. Your tests stay green. Production breaks.&lt;/p&gt;
&lt;p&gt;This is called a Lying Mock — a test double that no longer matches reality.&lt;/p&gt;
&lt;p&gt;Contract testing fixes this. Instead of just mocking the response, you write a contract: “I expect this request to return this response.” The backend then verifies that contract against its actual code.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;tests/contracts/payment.pact.spec.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { PactV3, MatchersV3 } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@pact-foundation/pact&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;provider&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PactV3&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;consumer: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;frontend-tests&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;provider: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;payment-service&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;dir: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;./pacts&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Payment API contract&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;returns payment confirmation&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; provider&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;given&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;a valid payment intent exists&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;uponReceiving&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;POST /v1/payment_intents&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;withRequest&lt;/span&gt;&lt;span&gt;({ method: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, path: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/v1/payment_intents&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;willRespondWith&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;status: &lt;/span&gt;&lt;span&gt;200&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;body: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;id: MatchersV3&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;pi_test_123&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;status: MatchersV3&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;succeeded&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;executeTest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;mockServer&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;createPayment&lt;/span&gt;&lt;span&gt;(mockServer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;succeeded&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;After this test runs, it generates a JSON contract file in &lt;code dir=&quot;auto&quot;&gt;./pacts&lt;/code&gt;. The backend team runs that contract against their actual API. If they rename &lt;code dir=&quot;auto&quot;&gt;id&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;paymentId&lt;/code&gt; — the contract verification fails in their pipeline, before the change is merged.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Where to start:&lt;/strong&gt; You don’t need to contract-test everything. Start with the APIs that change most often or have caused the most incidents. One contract on your payment flow is worth more than ten contracts on stable read-only endpoints.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One important caveat:&lt;/strong&gt; contract testing requires the backend team to actually run the verification in their pipeline. This is an organizational commitment, not just a technical one. For small teams, storing contract JSON files in the backend repo and running verification manually is a simpler starting point than a full Pact Broker setup. Also note that contracts verify response structure — they don’t catch business logic bugs or side effects.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-5-stop-relying-on-aftereach-for-cleanup&quot;&gt;Rule #5: Stop Relying on &lt;code dir=&quot;auto&quot;&gt;afterEach&lt;/code&gt; for Cleanup&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The classic approach to test data cleanup:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// ❌ Unreliable&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;afterEach&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; api&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;deleteUser&lt;/span&gt;&lt;span&gt;(userId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This fails silently when a test crashes before setting &lt;code dir=&quot;auto&quot;&gt;userId&lt;/code&gt;. It doesn’t run when the test runner itself crashes. After a CI failure mid-run, your database has orphaned records that affect the next run.&lt;/p&gt;
&lt;p&gt;Three approaches that actually work:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TTL — let the database clean up automatically&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Add an &lt;code dir=&quot;auto&quot;&gt;expires_at&lt;/code&gt; field to your test entities and set it when creating them:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// When creating test data&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; api&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;email: &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;test_&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;Date&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;@example.com&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;expires_at: &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Date&lt;/span&gt;&lt;span&gt;(Date&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;24&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;60&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;60&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1000&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toISOString&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;In PostgreSQL, a scheduled job handles cleanup:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- Runs every hour via pg_cron&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cron&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;schedule&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;cleanup-test-data&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;0 * * * *&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, $$&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;DELETE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; users &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; expires_at &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NOW&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; is_test &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; true;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;DELETE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; orders &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; expires_at &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NOW&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; is_test &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; true;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$$);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;In MongoDB and Redis, TTL indexes handle this natively — no cron job needed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cleanup queue — collect IDs, delete in bulk&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Track everything your tests create, then clean it all up in a global teardown:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// In your base API client&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;protected async &lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;(url: string, data&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; unknown) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;(url&lt;/span&gt;&lt;span&gt;, { &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (body&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;cleanupQueue&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;({ url, id: body&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; response;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// global-teardown.ts&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;globalTeardown&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;item&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;of&lt;/span&gt;&lt;span&gt; cleanupQueue) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; api&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;delete&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;item&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;item&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Even if individual tests fail, the global teardown runs and cleans up the queue.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Which approach to use:&lt;/strong&gt; TTL is the more reliable default — it works even if the test runner is killed with SIGKILL, because cleanup happens at the database level independently of your test process. Use TTL as your first line of defence. The cleanup queue is a good complement when you need guaranteed cleanup order or when your database doesn’t support scheduled jobs — but it won’t run if the process is hard-killed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;putting-it-together-the-data-reliability-cheat-sheet&quot;&gt;Putting It Together: The Data Reliability Cheat Sheet&lt;/h2&gt;&lt;/div&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Symptom&lt;/th&gt;&lt;th&gt;Root cause&lt;/th&gt;&lt;th&gt;Fix&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Duplicate records after CI failure&lt;/td&gt;&lt;td&gt;No idempotency on POST requests&lt;/td&gt;&lt;td&gt;Add &lt;code dir=&quot;auto&quot;&gt;X-Idempotency-Key&lt;/code&gt; header&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Tests green, production broken&lt;/td&gt;&lt;td&gt;Mocks don’t match real API&lt;/td&gt;&lt;td&gt;Add contract tests for critical endpoints&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;page.route&lt;/code&gt; mock not working&lt;/td&gt;&lt;td&gt;Request bypasses browser&lt;/td&gt;&lt;td&gt;Use WireMock or request wrapper&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Database full of test garbage&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;afterEach&lt;/code&gt; cleanup unreliable&lt;/td&gt;&lt;td&gt;TTL field + pg_cron or cleanup queue&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;External API causing flakiness&lt;/td&gt;&lt;td&gt;Real network calls in tests&lt;/td&gt;&lt;td&gt;WireMock for server-to-server calls&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-next&quot;&gt;What’s Next?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;You now have three layers covered: test infrastructure, object lifecycle, and data reliability. The next layer is observability — how do you measure test health, identify patterns in flakiness, and prove to your manager that stability work has business value?&lt;/p&gt;
&lt;p&gt;Want to go deeper on any of these topics? Check out the advanced version: &lt;strong&gt;&lt;a href=&quot;https://bdr-methodology.dev/blog/why_your_test_suite_lies_to_you_at_scale/&quot;&gt;Why Your Test Suite Lies to You at Scale&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;All patterns in this article are implemented in the &lt;a href=&quot;https://github.com/dmitryAQA/playwright-bdr-template&quot;&gt;Playwright BDR Template&lt;/a&gt; on GitHub.&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title>Playwright Fixtures as a Dependency Injection Container: The Architecture That Scales</title><link>https://bdr-methodology.dev/blog/playwright_fixtures_as_a_dependency_injection_container/</link><guid isPermaLink="true">https://bdr-methodology.dev/blog/playwright_fixtures_as_a_dependency_injection_container/</guid><pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://bdr-methodology.dev/images/mediumfixture.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;div&gt;&lt;h1 id=&quot;playwright-fixtures-as-a-dependency-injection-container-the-architecture-that-scales-&quot;&gt;Playwright Fixtures as a Dependency Injection Container: The Architecture That Scales &lt;span&gt; PRO IMPLEMENTATION &lt;/span&gt;&lt;/h1&gt;&lt;/div&gt;
&lt;p&gt;&lt;em&gt;New to Playwright architecture? Start with the fundamentals: &lt;strong&gt;&lt;a href=&quot;https://bdr-methodology.dev/blog/your_playwright_tests_will_need_refactoring/&quot;&gt;Your Playwright Tests Will Need Refactoring. Here’s How to Make It Painless&lt;/a&gt;&lt;/strong&gt; — the same concepts with more explanation.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Most Playwright codebases start the same way: Page Objects instantiated with &lt;code dir=&quot;auto&quot;&gt;new&lt;/code&gt; inside tests, fixtures as an afterthought, test data seeded with &lt;code dir=&quot;auto&quot;&gt;workerIndex&lt;/code&gt;. This works at 50 tests. At 500, the maintenance cost becomes visible. At 1000, it becomes the primary engineering problem.&lt;/p&gt;
&lt;p&gt;This article is about the architectural decisions that prevent that progression — specifically, treating Playwright’s fixture system as a proper DI container, not just a convenience wrapper around &lt;code dir=&quot;auto&quot;&gt;beforeEach&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Code examples are intentionally simplified — focus on the architectural pattern.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;three-layer-architecture-pom-flow-and-tests&quot;&gt;Three-Layer Architecture: POM, Flow, and Tests&lt;/h2&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;p&gt;##TL;DR&lt;/p&gt;
&lt;p&gt;Fixtures are a DI container with lifecycle management — not just a beforeEach wrapper. Getters enforce statelessness architecturally, not stylistically. Seed from testId + RUN_ID + repeatEachIndex — workerIndex breaks across shards. Domain-split fixtures with namespacing eliminate silent collisions. Builder pattern when factory overrides get unwieldy. test.step() for business-intent reporting.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Before diving into fixtures, it’s worth establishing the architectural model this article assumes. Most Playwright codebases that scale well use three layers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Page Object (POM)&lt;/strong&gt; — responsible for interacting with elements on a specific page: locators, clicks, form fills. Knows nothing about business logic or test scenarios.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://bdr-methodology.dev/blog/3-layers-architecture-pro/&quot;&gt;Flow&lt;/a&gt;&lt;/strong&gt; — describes complete business scenarios: “checkout”, “user registration”, “password reset”. Orchestrates Page Objects in the right sequence. The test calls &lt;code dir=&quot;auto&quot;&gt;checkoutFlow.submitOrder()&lt;/code&gt; and Flow handles which pages to visit, in what order, and what data to fill.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test&lt;/strong&gt; — declares intent. Reads like a specification: given this user, when this action, then this result.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This separation matters because changes are isolated: UI changes only touch Page Objects, process changes only touch Flows, tests remain stable. Fixtures are what make this architecture work — they manage the lifecycle of all three layers.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;why-new-inside-tests-is-a-scaling-problem&quot;&gt;Why &lt;code dir=&quot;auto&quot;&gt;new&lt;/code&gt; Inside Tests Is a Scaling Problem&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The naive approach looks like this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;checkout flow&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;cartPage&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartPage&lt;/span&gt;&lt;span&gt;(page);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;checkoutPage&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CheckoutPage&lt;/span&gt;&lt;span&gt;(page);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;checkoutFlow&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CheckoutFlow&lt;/span&gt;&lt;span&gt;(cartPage&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;checkoutPage);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; checkoutFlow&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;submitOrder&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;At first glance this is fine — explicit, readable, no magic. The problem surfaces when &lt;code dir=&quot;auto&quot;&gt;CartPage&lt;/code&gt; needs a new dependency. Now every test that constructs &lt;code dir=&quot;auto&quot;&gt;CartPage&lt;/code&gt; needs updating. In a 500-test suite, that’s a multi-day refactor with non-trivial regression risk.&lt;/p&gt;
&lt;p&gt;The deeper issue: this pattern makes the test responsible for dependency resolution. That’s not the test’s job.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;fixtures-as-a-di-container&quot;&gt;Fixtures as a DI Container&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Playwright’s fixture system is, architecturally, a dependency injection container with lifecycle management. The key insight is that fixtures compose:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;fixtures.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;base&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;extend&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;cartPage&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartPage&lt;/span&gt;&lt;span&gt;(page))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;checkoutPage&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CheckoutPage&lt;/span&gt;&lt;span&gt;(page))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Playwright resolves dependencies automatically&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;checkoutFlow&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;cartPage&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;checkoutPage&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;flow&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CheckoutFlow&lt;/span&gt;&lt;span&gt;(cartPage&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;checkoutPage)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(flow)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;flow&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;cleanup&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;// teardown guaranteed regardless of test outcome&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Playwright builds the dependency graph, resolves it in the correct order, and handles teardown. If five tests depend on &lt;code dir=&quot;auto&quot;&gt;cartPage&lt;/code&gt;, Playwright creates one instance per test — not five, not one shared instance. The isolation is automatic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The caching behavior matters:&lt;/strong&gt; when multiple fixtures in the same test depend on the same fixture (e.g., both &lt;code dir=&quot;auto&quot;&gt;checkoutFlow&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;analyticsFlow&lt;/code&gt; depend on &lt;code dir=&quot;auto&quot;&gt;cartPage&lt;/code&gt;), Playwright creates exactly one &lt;code dir=&quot;auto&quot;&gt;cartPage&lt;/code&gt; instance for that test. This isn’t just an optimization — it means the two flows share state correctly, as they would in a real user session.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-lifecycle-argument-for-fixtures&quot;&gt;The Lifecycle Argument for Fixtures&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here’s the argument that matters for long-lived codebases: use fixtures even when the object seems stateless today.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;LINK_TO_3_LAYER_ARCHITECTURE_ARTICLE&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;CheckoutFlow&lt;/code&gt;&lt;/a&gt; might be a pure orchestrator right now — no state, no side effects, no external connections. But requirements change:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Next sprint: Flow needs to track an order ID for verification&lt;/li&gt;
&lt;li&gt;Month after: Flow opens a WebSocket for real-time updates&lt;/li&gt;
&lt;li&gt;Quarter later: Flow acquires a distributed lock that must be released&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each of these changes requires teardown. If &lt;code dir=&quot;auto&quot;&gt;CheckoutFlow&lt;/code&gt; is created with &lt;code dir=&quot;auto&quot;&gt;new&lt;/code&gt; in 300 tests, adding teardown means touching 300 files. If it’s in a fixture, you add one &lt;code dir=&quot;auto&quot;&gt;after use&lt;/code&gt; block:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;checkoutFlow: &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;cartPage&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;checkoutPage&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;flow&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CheckoutFlow&lt;/span&gt;&lt;span&gt;(cartPage&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;checkoutPage);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(flow);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; flow&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;releaseLock&lt;/span&gt;&lt;span&gt;(); &lt;/span&gt;&lt;span&gt;// added once, applies everywhere&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; flow&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;closeConnection&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The fixture system gives you lifecycle management for free. The upfront investment is real — a few hours to set up the pattern properly. The cost of retrofitting it later: proportional to the number of tests.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-pragmatic-rule-when-fixtures-are-overkill&quot;&gt;The Pragmatic Rule: When Fixtures Are Overkill&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Everything above is an argument for fixtures. Here’s the counterargument, because a good architecture isn’t about dogma.&lt;/p&gt;
&lt;p&gt;Fixtures make sense when an object needs one or more of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lifecycle management&lt;/strong&gt; — setup before the test, teardown after&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shared dependencies&lt;/strong&gt; — the object depends on &lt;code dir=&quot;auto&quot;&gt;page&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;request&lt;/code&gt;, or another fixture&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Potential for state&lt;/strong&gt; — today stateless, but realistically might not be tomorrow&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When none of these apply, a fixture is unnecessary indirection. A pure utility function — one that takes inputs and returns outputs with no side effects and no browser context — doesn’t belong in a fixture system. It belongs in a module:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Just a function — no fixture needed&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;formatOrderId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;ORD-&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toUpperCase&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Factory function — pure, no browser context&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;overrides&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Partial&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; { role: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;customer&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; discount: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;overrides };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Fixture — depends on page, has implicit lifecycle&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cartPage: &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartPage&lt;/span&gt;&lt;span&gt;(page));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The decision rule: if an object touches the browser context or has any chance of needing teardown as the codebase evolves — fixture. If it’s a pure function or a data factory with no external dependencies — just export it and call it directly.&lt;/p&gt;
&lt;p&gt;The cost of using fixtures when you don’t strictly need it: near zero. The cost of not using them when you should have: proportional to the number of tests you need to update.&lt;/p&gt;
&lt;p&gt;Putting everything into fixtures because “it might need lifecycle someday” is the same mistake as premature optimization. It adds indirection without value and makes the codebase harder to read. The goal is judgment, not consistency for its own sake.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;lazy-pom-why-getters-beat-constructor-assignments&quot;&gt;Lazy POM: Why Getters Beat Constructor Assignments&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The standard Page Object pattern assigns locators in the constructor:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Technically safe, architecturally suboptimal&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartPage&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;readonly&lt;/span&gt;&lt;span&gt; submitButton&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Locator&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;constructor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;submitButton&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;locator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;button#submit&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Playwright locators are lazy — they don’t query the DOM at construction time, they query it at the moment of interaction. So a locator assigned in the constructor won’t go stale: even if the DOM re-renders between construction and use, Playwright finds the element fresh when you call &lt;code dir=&quot;auto&quot;&gt;.click()&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;.isVisible()&lt;/code&gt;. This is technically fine.&lt;/p&gt;
&lt;p&gt;The problem is what this pattern enables: the temptation to compute actual state in the constructor.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// This is a race condition bomb&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;constructor&lt;/span&gt;&lt;span&gt;(page: Page) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;initialItemCount&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;locator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;.item&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;count&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;})();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The IIFE fires and is forgotten. The test accesses &lt;code dir=&quot;auto&quot;&gt;initialItemCount&lt;/code&gt; before the promise resolves. In a fast local environment this usually works. Under CI load with multiple workers competing for resources, it fails intermittently and is nearly impossible to reproduce.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The architectural fix: getters enforce statelessness&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartPage&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;constructor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; {}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Evaluated fresh on every access — no state, no race conditions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;submitButton&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Place order&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;items&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;locator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;.cart-item&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// For computed state, return a promise explicitly&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;getItemCount&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;items&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;count&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Getters make it structurally impossible to cache state at construction time. The Page Object is forced to be stateless — it can only describe how to find elements and interact with them, not what their current state is. Reading state is always an explicit async operation.&lt;/p&gt;
&lt;p&gt;This is the State Trap pattern in reverse: instead of accidentally capturing a DOM snapshot at construction time, you’re architecturally prevented from doing so.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;deterministic-test-data-at-scale&quot;&gt;Deterministic Test Data at Scale&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;workerIndex&lt;/code&gt; as a faker seed is the most common data isolation mistake. The reasoning seems sound: each worker gets a unique number, so data is unique. The failure mode is subtle.&lt;/p&gt;
&lt;p&gt;On 10 parallel CI shards, each shard has its own “Worker 0”, “Worker 1”, etc. The &lt;code dir=&quot;auto&quot;&gt;workerIndex&lt;/code&gt; namespace is shard-local. If the same test runs on Shard 1 Worker 0 and Shard 2 Worker 0 — during a retry, or due to shard misconfiguration — both generate identical data for the same &lt;code dir=&quot;auto&quot;&gt;testId&lt;/code&gt;. In a shared database, this means collisions — and the kind of intermittent failures that look like application bugs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The correct seed: combine test identity with CI build ID&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;utils/faker.utils.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { TestInfo } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@playwright/test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { faker } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@faker-js/faker&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;hashCode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; str&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;split&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;reduce&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;acc&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;char&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; (Math&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;imul&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;31&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; acc) &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; char&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;charCodeAt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)) &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Note: not cryptographically secure, but collision probability is negligible&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// for the number of tests in any realistic suite — fine for faker seeding&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;seedFaker&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;testInfo&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;TestInfo&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt;&lt;span&gt; faker {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;RUN_ID&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;process&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;RUN_ID&lt;/span&gt;&lt;span&gt; ?? &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Three components:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// testId: hash of file path + test name — unique per test, stable across runs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// RUN_ID: CI build ID — different builds get different data&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// repeatEachIndex: handles retries — same test run gets same data on retry&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;seed&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;hashCode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;testInfo&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;testId&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;RUN_ID&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;testInfo&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;repeatEachIndex&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;faker&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;seed&lt;/span&gt;&lt;span&gt;(seed);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; faker;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;fixtures.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;base&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;extend&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;faker&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{}, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;testInfo&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;seedFaker&lt;/span&gt;&lt;span&gt;(testInfo))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;repeatEachIndex&lt;/code&gt; component is worth explaining: when a test retries, it runs on potentially a different worker. Without &lt;code dir=&quot;auto&quot;&gt;repeatEachIndex&lt;/code&gt; in the seed, a retry would generate different data than the original run. If the failure was data-dependent, you can’t reproduce it. With &lt;code dir=&quot;auto&quot;&gt;repeatEachIndex&lt;/code&gt;, retries are deterministic — same seed, same data, reproducible failure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The debugging payoff:&lt;/strong&gt; when a test fails in CI, take the &lt;code dir=&quot;auto&quot;&gt;RUN_ID&lt;/code&gt; from the pipeline logs and run the test locally with &lt;code dir=&quot;auto&quot;&gt;RUN_ID=&amp;#x3C;value&gt; npx playwright test &amp;#x3C;test-name&gt;&lt;/code&gt;. You get the exact data that was generated in CI. This transforms “I can’t reproduce this” into a reproducible failure in under a minute.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;factory-pattern-separating-structure-from-noise&quot;&gt;Factory Pattern: Separating Structure From Noise&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Random data everywhere obscures test intent. If a field doesn’t affect the outcome, it shouldn’t be visible in the test.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;user.factory.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; User {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;role&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;customer&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;vip&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;admin&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;discount&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;overrides&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Partial&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&lt;span&gt;&gt;, &lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt;&lt;span&gt; faker &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; faker&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;id: f&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;uuid&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;email: f&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;internet&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name: f&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;person&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fullName&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;role: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;customer&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;discount: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;overrides&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The factory provides structure and defaults. Overrides express what the test actually cares about:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Only the meaningful fields are visible&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;VIP discount applied at checkout&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;checkoutFlow&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;faker&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ role: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;vip&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, discount: &lt;/span&gt;&lt;span&gt;0.15&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;faker);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;order&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;checkoutFlow&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;asUser&lt;/span&gt;&lt;span&gt;(user)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;checkout&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(order&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;total&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(order&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;subtotal&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0.85&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;For business scenarios that repeat across multiple tests, extract named datasets rather than duplicating overrides:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;data/datasets/users.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;VIP_USER&lt;/span&gt;&lt;span&gt; = {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;role: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;vip&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;discount: &lt;/span&gt;&lt;span&gt;0.15&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;} as &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; satisfies &lt;/span&gt;&lt;span&gt;Partial&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;ADMIN_USER&lt;/span&gt;&lt;span&gt; = {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;role: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;admin&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;discount: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;} as &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; satisfies &lt;/span&gt;&lt;span&gt;Partial&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// In tests — intent is immediately clear&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;VIP_USER&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;faker);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;satisfies&lt;/code&gt; operator here is doing real work: it validates that the dataset fields match the &lt;code dir=&quot;auto&quot;&gt;User&lt;/code&gt; type without widening the type. If someone adds a required field to &lt;code dir=&quot;auto&quot;&gt;User&lt;/code&gt; and forgets to update the dataset, TypeScript catches it at compile time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When to consider the Builder pattern instead&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The factory + overrides approach works well when objects are simple and combinations are limited. When complexity grows — a user with a role, subscription tier, notification preferences, and order history — the override object becomes unwieldy:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Hard to read at a glance&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;role: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;vip&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;subscription: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;premium&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;notifications: { email: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, sms: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;orderCount: &lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;faker&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;A Builder makes the same intent readable:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Builder — reads like a specification&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserBuilder&lt;/span&gt;&lt;span&gt;(faker)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;asVip&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;withPremiumSubscription&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;withNotifications&lt;/span&gt;&lt;span&gt;({ email: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, sms: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;withOrderHistory&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;user.builder.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserBuilder&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; overrides&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Partial&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;constructor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt;&lt;span&gt; faker&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;asVip&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;overrides&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;role&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;vip&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;overrides&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;discount&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0.15&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;withPremiumSubscription&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;overrides&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;subscription&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;premium&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;withOrderHistory&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;count&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;overrides&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;orderCount&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; count;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;overrides&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The Builder delegates to the factory at the end — so you keep one source of truth for defaults, and the Builder just provides a fluent API for complex combinations. Use it when you have more than 3–4 meaningful combinations that appear repeatedly across tests. For simpler cases, the factory with overrides is less code and just as clear.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;scaling-fixtures-mergetests-and-namespacing&quot;&gt;Scaling Fixtures: &lt;code dir=&quot;auto&quot;&gt;mergeTests&lt;/code&gt; and Namespacing&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A single &lt;code dir=&quot;auto&quot;&gt;fixtures.ts&lt;/code&gt; file works until it doesn’t. The inflection point is usually around 15–20 fixtures, when multiple engineers are editing the same file simultaneously and merge conflicts become routine.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Domain-driven fixture splitting:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;auth.fixtures.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { test &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; base } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@playwright/test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { LoginPage, AdminPage } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;../pages&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; AuthFixtures &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; { loginPage&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LoginPage&lt;/span&gt;&lt;span&gt;; adminPage&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AdminPage&lt;/span&gt;&lt;span&gt; };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;authTest&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;base&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;extend&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;AuthFixtures&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;loginPage&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LoginPage&lt;/span&gt;&lt;span&gt;(page))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;adminPage&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AdminPage&lt;/span&gt;&lt;span&gt;(page))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// cart.fixtures.ts&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; CartFixtures &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; { cartPage&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartPage&lt;/span&gt;&lt;span&gt;; checkoutPage&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CheckoutPage&lt;/span&gt;&lt;span&gt; };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;cartTest&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;base&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;extend&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;CartFixtures&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// fixtures.ts — composition point&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { mergeTests } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@playwright/test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { authTest } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;./auth.fixtures&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { cartTest } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;./cart.fixtures&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;mergeTests&lt;/span&gt;&lt;span&gt;(authTest&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;cartTest);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; { expect } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@playwright/test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Tests import from &lt;code dir=&quot;auto&quot;&gt;fixtures.ts&lt;/code&gt; and see nothing change. The split is organizational, not behavioral.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The silent collision problem:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;mergeTests&lt;/code&gt; doesn’t check for fixture name conflicts. If &lt;code dir=&quot;auto&quot;&gt;auth.fixtures.ts&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;billing.fixtures.ts&lt;/code&gt; both export a &lt;code dir=&quot;auto&quot;&gt;user&lt;/code&gt; fixture, the last one registered wins — silently. Tests that worked before &lt;code dir=&quot;auto&quot;&gt;mergeTests&lt;/code&gt; may start using a different &lt;code dir=&quot;auto&quot;&gt;user&lt;/code&gt; object without any error.&lt;/p&gt;
&lt;p&gt;Namespacing eliminates this class of bug:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; AuthFixtures &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;auth&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;admin&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Admin&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;guest&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Guest&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;authTest&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;base&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;extend&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;AuthFixtures&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;auth&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;admin: &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Admin&lt;/span&gt;&lt;span&gt;(page)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;user: &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;(page)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;guest: &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Guest&lt;/span&gt;&lt;span&gt;(page)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Collision is now structurally impossible&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// auth.user vs billing.user — different namespaces, different objects&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;admin manages billing&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;auth&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;billing&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; auth&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;admin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; billing&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;subscribe&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The namespace also makes test code self-documenting: &lt;code dir=&quot;auto&quot;&gt;auth.admin&lt;/code&gt; vs &lt;code dir=&quot;auto&quot;&gt;billing.user&lt;/code&gt; is unambiguous in a way that two separate &lt;code dir=&quot;auto&quot;&gt;admin&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;user&lt;/code&gt; fixtures are not.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;business-steps-teststep-and-bdr&quot;&gt;Business Steps: test.step and BDR&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The quality of step descriptions in your reports determines how useful they are for debugging. The native Playwright tool is &lt;code dir=&quot;auto&quot;&gt;test.step()&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Technical log — breaks when implementation changes&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;async &lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; test&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Click the login button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Login&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Business intent — survives refactoring&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;async &lt;/span&gt;&lt;span&gt;loginAs&lt;/span&gt;&lt;span&gt;(user: User) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; test&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Authenticate as &quot;&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;username&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;loginPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt;(user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;username&lt;/span&gt;&lt;span&gt;, user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;password&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The second version remains valid even if the login mechanism changes from a form to SSO. The report reads like a scenario, not a sequence of DOM operations.&lt;/p&gt;
&lt;p&gt;In &lt;a href=&quot;https://bdr-methodology.dev/blog/beyond-cucumber-pro/&quot;&gt;BDR methodology&lt;/a&gt;, this pattern is formalized with a &lt;code dir=&quot;auto&quot;&gt;@Step&lt;/code&gt; decorator that wraps methods automatically — eliminating the manual &lt;code dir=&quot;auto&quot;&gt;test.step()&lt;/code&gt; wrapping. If you’re building at scale and want cleaner syntax, it’s worth exploring.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;eslint-architectural-enforcement&quot;&gt;ESLint: Architectural Enforcement&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The fixture architecture only works if objects aren’t created with &lt;code dir=&quot;auto&quot;&gt;new&lt;/code&gt; inside tests. Document the rule in code:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;.eslintrc.js&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;module&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;exports&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;overrides: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;// Scoped to test files only — won&apos;t flag Pagination or other non-POM classes&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;files: [&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;tests/**/*.ts&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;**/*.spec.ts&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;rules: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;no-restricted-syntax&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;selector: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;NewExpression[callee.name=/.*Page$/]&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;message: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Instantiate Page Objects via fixtures, not new. See fixtures.ts.&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;selector: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;NewExpression[callee.name=/.*Flow$/]&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;message: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Instantiate Flow objects via fixtures, not new. See fixtures.ts.&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;When a genuine exception exists — a factory function that creates a Page Object for testing purposes, for instance — the escape hatch is &lt;code dir=&quot;auto&quot;&gt;// eslint-disable-next-line&lt;/code&gt; with a mandatory comment:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// eslint-disable-next-line no-restricted-syntax&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Factory function — not a test file, constructing for unit testing POM behavior&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LoginPage&lt;/span&gt;&lt;span&gt;(mockPage);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The comment makes the exception visible and reviewable. Blanket disables without explanation are a red flag in code review.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-architecture-in-summary&quot;&gt;The Architecture in Summary&lt;/h2&gt;&lt;/div&gt;





















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Decision&lt;/th&gt;&lt;th&gt;Wrong&lt;/th&gt;&lt;th&gt;Right&lt;/th&gt;&lt;th&gt;Why&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Object creation&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;new PageObject()&lt;/code&gt; in tests&lt;/td&gt;&lt;td&gt;Fixtures&lt;/td&gt;&lt;td&gt;Single update point when constructor changes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Locator definition&lt;/td&gt;&lt;td&gt;Constructor assignments&lt;/td&gt;&lt;td&gt;Getters&lt;/td&gt;&lt;td&gt;Prevents state capture, enforces statelessness&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Faker seed&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;workerIndex&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;testId&lt;/code&gt; + &lt;code dir=&quot;auto&quot;&gt;RUN_ID&lt;/code&gt; + &lt;code dir=&quot;auto&quot;&gt;repeatEachIndex&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Stable across shards and retries&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fixture organization&lt;/td&gt;&lt;td&gt;One monolithic file&lt;/td&gt;&lt;td&gt;Domain files + &lt;code dir=&quot;auto&quot;&gt;mergeTests&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Parallel editing, clear ownership&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fixture naming&lt;/td&gt;&lt;td&gt;Flat namespace&lt;/td&gt;&lt;td&gt;Domain namespacing&lt;/td&gt;&lt;td&gt;Eliminates silent collisions&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Architecture enforcement&lt;/td&gt;&lt;td&gt;Code review comments&lt;/td&gt;&lt;td&gt;ESLint rules scoped to &lt;code dir=&quot;auto&quot;&gt;tests/**&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Automated, consistent, zero overhead&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Step reporting&lt;/td&gt;&lt;td&gt;Technical descriptions&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;test.step()&lt;/code&gt; with business intent&lt;/td&gt;&lt;td&gt;Report reads like a scenario, not a DOM log&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;what-this-architecture-actually-solves&quot;&gt;What This Architecture Actually Solves&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;None of this is complex to implement. The fixture DI pattern is an afternoon. Seeded faker is 20 lines. Namespacing is a refactor you can do incrementally.&lt;/p&gt;
&lt;p&gt;What it solves is the compounding cost of the alternative. Every &lt;code dir=&quot;auto&quot;&gt;new PageObject()&lt;/code&gt; in a test is a future refactoring touchpoint. Every &lt;code dir=&quot;auto&quot;&gt;workerIndex&lt;/code&gt; seed is a potential data collision waiting for sufficient parallelism to trigger. Every flat fixture namespace is a silent collision waiting for the second engineer to add a fixture with the same name.&lt;/p&gt;
&lt;p&gt;The architecture described here doesn’t make tests faster or more readable in the short term. It makes the codebase cheaper to maintain as it grows — which is the only metric that matters at scale.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Reference implementation: &lt;a href=&quot;https://github.com/dmitryAQA/playwright-bdr-template&quot;&gt;Playwright BDR Template&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title>Your Playwright Tests Will Need Refactoring. Here&apos;s How to Make It Painless</title><link>https://bdr-methodology.dev/blog/your_playwright_tests_will_need_refactoring/</link><guid isPermaLink="true">https://bdr-methodology.dev/blog/your_playwright_tests_will_need_refactoring/</guid><pubDate>Wed, 13 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://bdr-methodology.dev/images/devtofixtures.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;div&gt;&lt;h1 id=&quot;your-playwright-tests-will-need-refactoring-heres-how-to-make-it-painless-&quot;&gt;Your Playwright Tests Will Need Refactoring. Here’s How to Make It Painless &lt;span&gt; CONCEPT &lt;/span&gt;&lt;/h1&gt;&lt;/div&gt;
&lt;p&gt;You write 50 tests. Everything works. Six months later the team grows, tests become 300, and someone changes a constructor — and you spend two days updating imports across the entire project. Sound familiar?&lt;/p&gt;
&lt;p&gt;This isn’t a discipline problem. It’s an architecture problem. And it’s fixable before it happens.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Code examples are simplified for clarity — focus on the idea, not the boilerplate.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;tldr&quot;&gt;TL;DR&lt;/h2&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Never instantiate Page Objects with &lt;code dir=&quot;auto&quot;&gt;new&lt;/code&gt; inside tests — use fixtures&lt;/li&gt;
&lt;li&gt;Use getters instead of constructor assignments in Page Objects&lt;/li&gt;
&lt;li&gt;Seed your test data with a combination of &lt;code dir=&quot;auto&quot;&gt;testId&lt;/code&gt; + &lt;code dir=&quot;auto&quot;&gt;RUN_ID&lt;/code&gt; + &lt;code dir=&quot;auto&quot;&gt;repeatEachIndex&lt;/code&gt; for reproducibility&lt;/li&gt;
&lt;li&gt;Split fixtures by domain when the file gets large — use &lt;code dir=&quot;auto&quot;&gt;mergeTests&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Use Namespacing to avoid silent fixture name collisions&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;what-is-a-flow-quick-explainer&quot;&gt;What Is a Flow? (Quick Explainer)&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Before we dive in — this article uses the term &lt;strong&gt;Flow&lt;/strong&gt;, which might be unfamiliar.&lt;/p&gt;
&lt;p&gt;In a well-structured Playwright project, tests are built in three layers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Page Object (POM)&lt;/strong&gt; — knows how to interact with elements on a specific page: find a button, fill a field, click a link&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://bdr-methodology.dev/blog/3-layers-architecture-base/&quot;&gt;Flow&lt;/a&gt;&lt;/strong&gt; — knows how to complete a business scenario: “checkout”, “register a user”, “reset a password”. It orchestrates Page Objects in the right sequence so tests don’t have to&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test&lt;/strong&gt; — just calls the Flow and checks the result&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So when you see &lt;code dir=&quot;auto&quot;&gt;checkoutFlow.submitOrder()&lt;/code&gt; in a test, that one line is hiding a sequence of page navigations, form fills, and button clicks — all managed by the Flow. The test doesn’t need to know the details.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-problem-architecture-that-fights-you-at-scale&quot;&gt;The Problem: Architecture That Fights You at Scale&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;At 50 tests, messy architecture is invisible. At 300 tests, it becomes expensive. Two separate problems compound each other:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Data isolation breaks in parallel runs.&lt;/strong&gt; Two workers create a user named “Ivan”, one test reads the other’s data, both fail. You spend an hour debugging something that has nothing to do with your application. This is a data seeding problem — solved in Rule #3.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Refactoring takes days instead of hours.&lt;/strong&gt; Someone changes a constructor signature. Now you have 150 files to update. With modern tools this is still risky — you might miss one. This is a dependency management problem — solved in Rule #1.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tests are impossible to read.&lt;/strong&gt; Ten lines of setup before the actual test logic. New team members can’t tell what’s being tested and what’s just noise. This too is a dependency management problem — when setup lives in fixtures, tests read like specifications.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-1-stop-using-new-inside-tests&quot;&gt;Rule #1: Stop Using &lt;code dir=&quot;auto&quot;&gt;new&lt;/code&gt; Inside Tests&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This is the most common pattern that makes refactoring painful:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Every test manages its own dependencies&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;checkout&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;cartPage&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartPage&lt;/span&gt;&lt;span&gt;(page);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;checkoutPage&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CheckoutPage&lt;/span&gt;&lt;span&gt;(page);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;checkoutFlow&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CheckoutFlow&lt;/span&gt;&lt;span&gt;(cartPage&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;checkoutPage);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; checkoutFlow&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;submitOrder&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If &lt;code dir=&quot;auto&quot;&gt;CartPage&lt;/code&gt; needs a new dependency tomorrow — a logger, a config object, an API client — you update every single test that creates it. That’s your two days of refactoring.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The fix: fixtures as a DI container&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// fixtures.ts — one place to manage all object creation&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;base&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;extend&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;cartPage&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartPage&lt;/span&gt;&lt;span&gt;(page))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;checkoutFlow&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;cartPage&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;checkoutPage&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CheckoutFlow&lt;/span&gt;&lt;span&gt;(cartPage&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;checkoutPage))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// The test reads like a specification&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;checkout&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;checkoutFlow&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; checkoutFlow&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;submitOrder&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;When &lt;code dir=&quot;auto&quot;&gt;CartPage&lt;/code&gt; constructor changes — you update &lt;code dir=&quot;auto&quot;&gt;fixtures.ts&lt;/code&gt;. One file. Done.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why fixtures even when &lt;a href=&quot;https://bdr-methodology.dev/blog/3-layers-architecture-base/&quot;&gt;Flow&lt;/a&gt; seems stateless today:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Your &lt;code dir=&quot;auto&quot;&gt;CheckoutFlow&lt;/code&gt; might be pure today — no state, no side effects. But requirements change. Tomorrow it needs to track an order ID. Next month it opens a WebSocket connection that needs to be closed after the test.&lt;/p&gt;
&lt;p&gt;If Flow is created via &lt;code dir=&quot;auto&quot;&gt;new&lt;/code&gt; in every test, adding teardown means updating hundreds of files. If it’s in a fixture, you add &lt;code dir=&quot;auto&quot;&gt;after use&lt;/code&gt; cleanup in one place:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;checkoutFlow: &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;cartPage&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;checkoutPage&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;flow&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CheckoutFlow&lt;/span&gt;&lt;span&gt;(cartPage&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;checkoutPage);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(flow);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; flow&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;cleanup&lt;/span&gt;&lt;span&gt;(); &lt;/span&gt;&lt;span&gt;// added in one place, applies everywhere&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The upfront investment is real — a few hours to set up fixtures properly. The cost of refactoring later: days, proportional to how many tests you have.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on pragmatism:&lt;/strong&gt; Fixtures are for managing state and lifecycle. If you have a stateless utility function — like &lt;code dir=&quot;auto&quot;&gt;formatDate&lt;/code&gt; or a math helper — don’t wrap it in a fixture. A simple ES6 import is faster and less complex. Use fixtures for things that hold a page context or require setup/teardown. Everything else is just a function.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-2-use-getters-in-page-objects-not-constructor-assignments&quot;&gt;Rule #2: Use Getters in Page Objects, Not Constructor Assignments&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This is subtle but important. Most tutorials show this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Locator computed once at construction time&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartPage&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; submitButton&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Locator&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;constructor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;submitButton&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;locator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;button#submit&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This looks fine. Playwright locators are lazy — they don’t query the DOM at construction time, they query it when you interact with them. So assigning a locator in the constructor is technically safe.&lt;/p&gt;
&lt;p&gt;The real danger is what this pattern enables — the temptation to capture actual state in the constructor:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Never do this&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;constructor&lt;/span&gt;&lt;span&gt;(page: Page) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;itemCount&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;locator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;.items&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;count&lt;/span&gt;&lt;span&gt;(); &lt;/span&gt;&lt;span&gt;// race condition bomb&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;})();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This creates an unmanaged race condition. Your test might read &lt;code dir=&quot;auto&quot;&gt;itemCount&lt;/code&gt; before the async function inside the constructor has resolved. This causes random CI failures that are nearly impossible to reproduce locally.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The fix: lazy getters&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Getters are the architectural solution — not because they prevent stale locators (Playwright handles that), but because they make it structurally impossible to capture state at construction time. A getter can’t be &lt;code dir=&quot;auto&quot;&gt;async&lt;/code&gt;, so you physically can’t write &lt;code dir=&quot;auto&quot;&gt;this.itemCount = await something&lt;/code&gt; inside one.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Fresh locator on every access, stateless by design&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartPage&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;constructor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; {}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;submitButton&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Place order&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Named cartItems, not itemCount — this returns a locator, not a number&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cartItems&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;locator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;.cart-item&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// For actual count — explicit async method, not a getter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;getItemCount&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;cartItems&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;count&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The Page Object stays stateless. Reading state is always an explicit async operation, never something that happens silently at construction time.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-3-isolate-test-data-for-parallel-runs&quot;&gt;Rule #3: Isolate Test Data for Parallel Runs&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;When you run 1000 tests in parallel across multiple CI shards, data collisions are inevitable — unless you design against them.&lt;/p&gt;
&lt;p&gt;The common mistake is using &lt;code dir=&quot;auto&quot;&gt;workerIndex&lt;/code&gt; as a seed for test data. It seems logical: each worker gets a unique number, so data should be unique. The problem is that &lt;code dir=&quot;auto&quot;&gt;workerIndex&lt;/code&gt; resets per shard. On 10 parallel CI agents, each has its own “Worker 0”. Collisions are guaranteed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The fix: combine test identity with CI build ID — not worker index&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;utils/faker.utils.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { TestInfo } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@playwright/test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { faker } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@faker-js/faker&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;hashCode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; str&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;split&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;reduce&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;acc&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;char&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; (Math&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;imul&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;31&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; acc) &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; char&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;charCodeAt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)) &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;seedFaker&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;testInfo&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;TestInfo&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;RUN_ID&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;process&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;RUN_ID&lt;/span&gt;&lt;span&gt; || &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;seed&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;hashCode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;testInfo&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;testId&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;RUN_ID&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;testInfo&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;repeatEachIndex&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;faker&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;seed&lt;/span&gt;&lt;span&gt;(seed);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; faker;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// fixtures.ts&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;base&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;extend&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;faker&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{}, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;testInfo&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;seedFaker&lt;/span&gt;&lt;span&gt;(testInfo))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Three components in the seed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;testId&lt;/code&gt; — unique hash of the test file path and test name&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;RUN_ID&lt;/code&gt; — the CI build ID (e.g. &lt;code dir=&quot;auto&quot;&gt;GITHUB_RUN_ID&lt;/code&gt;), so different builds get different data&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;repeatEachIndex&lt;/code&gt; — handles retries correctly&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code dir=&quot;auto&quot;&gt;RUN_ID&lt;/code&gt; is an environment variable provided by your CI system — for example, &lt;code dir=&quot;auto&quot;&gt;GITHUB_RUN_ID&lt;/code&gt; in GitHub Actions. If it’s missing, the code falls back to &lt;code dir=&quot;auto&quot;&gt;&apos;local&apos;&lt;/code&gt;, so everything works on your machine without any extra setup.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;The payoff:&lt;/strong&gt; when a test fails in CI, grab the &lt;code dir=&quot;auto&quot;&gt;RUN_ID&lt;/code&gt; from the pipeline logs, run the test locally with the same ID, and you get the exact same names, emails, and UUIDs that were generated in CI. Reproducible failures instead of “I can’t reproduce this locally.”&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-4-structure-test-data-with-factories-and-overrides&quot;&gt;Rule #4: Structure Test Data With Factories and Overrides&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Random data everywhere creates noise. If a field doesn’t affect the test outcome, it shouldn’t be visible in the test.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// user.factory.ts — sensible defaults&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;overrides&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Partial&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&lt;span&gt;&gt;, &lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; faker&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;id: f&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;uuid&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;email: f&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;internet&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name: f&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;person&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fullName&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;role: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;customer&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;overrides&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// In the test — only what matters&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;VIP discount applies at checkout&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;checkoutFlow&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;faker&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ role: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;vip&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, discount: &lt;/span&gt;&lt;span&gt;0.15&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;faker);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; checkoutFlow&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;asUser&lt;/span&gt;&lt;span&gt;(user)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;applyPromo&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The test declares intent, not implementation. When you read it, you know exactly what’s being tested: VIP role and discount. Everything else — name, email, UUID — is noise that the factory handles.&lt;/p&gt;
&lt;p&gt;For data that represents specific business cases and appears repeatedly, extract it as a named dataset:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;data/datasets/users.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;VIP_USER&lt;/span&gt;&lt;span&gt; = { role: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;vip&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, discount: &lt;/span&gt;&lt;span&gt;0.15&lt;/span&gt;&lt;span&gt; } as const&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// In tests&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;VIP_USER&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;faker);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Use the &lt;code dir=&quot;auto&quot;&gt;satisfies&lt;/code&gt; operator (TypeScript 4.9+) instead of &lt;code dir=&quot;auto&quot;&gt;as const&lt;/code&gt; for datasets. It ensures your data matches the &lt;code dir=&quot;auto&quot;&gt;User&lt;/code&gt; type without losing the specific literal values — catching type errors before you even run the test:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;VIP_USER&lt;/span&gt;&lt;span&gt; = {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;role: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;vip&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;discount: &lt;/span&gt;&lt;span&gt;0.15&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;} satisfies &lt;/span&gt;&lt;span&gt;Partial&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If someone adds a required field to &lt;code dir=&quot;auto&quot;&gt;User&lt;/code&gt; and forgets to update the dataset, TypeScript will tell you at compile time, not at runtime.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-5-scale-fixtures-with-mergetests-and-namespacing&quot;&gt;Rule #5: Scale Fixtures With &lt;code dir=&quot;auto&quot;&gt;mergeTests&lt;/code&gt; and Namespacing&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;One &lt;code dir=&quot;auto&quot;&gt;fixtures.ts&lt;/code&gt; file is fine at the start. At 20+ fixtures it becomes a 400-line file that multiple people edit simultaneously.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Split by domain:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;auth.fixtures.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { test &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; base } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@playwright/test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { LoginPage } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;../pages/LoginPage&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;authTest&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;base&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;extend&lt;/span&gt;&lt;span&gt;&amp;#x3C;{ &lt;/span&gt;&lt;span&gt;loginPage&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LoginPage&lt;/span&gt;&lt;span&gt; }&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;loginPage&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LoginPage&lt;/span&gt;&lt;span&gt;(page))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// cart.fixtures.ts&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { test &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; base } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@playwright/test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { CartPage } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;../pages/CartPage&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;cartTest&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;base&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;extend&lt;/span&gt;&lt;span&gt;&amp;#x3C;{ &lt;/span&gt;&lt;span&gt;cartPage&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartPage&lt;/span&gt;&lt;span&gt; }&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;cartPage&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartPage&lt;/span&gt;&lt;span&gt;(page))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// fixtures.ts — merge everything&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { mergeTests } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@playwright/test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { authTest } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;./auth.fixtures&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { cartTest } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;./cart.fixtures&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;mergeTests&lt;/span&gt;&lt;span&gt;(authTest&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;cartTest);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Tests don’t change at all — they still import from &lt;code dir=&quot;auto&quot;&gt;fixtures.ts&lt;/code&gt;. The split is purely organizational.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Watch out for name collisions:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If &lt;code dir=&quot;auto&quot;&gt;auth.fixtures.ts&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;cart.fixtures.ts&lt;/code&gt; both define a fixture called &lt;code dir=&quot;auto&quot;&gt;user&lt;/code&gt;, Playwright won’t warn you. The last one wins silently. This creates subtle bugs that are very hard to track down.&lt;/p&gt;
&lt;p&gt;The fix is namespacing — group fixtures by domain:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// No collision possible&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { test &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; base } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@playwright/test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { Admin } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;../pages/Admin&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { User } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;../pages/User&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;base&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;extend&lt;/span&gt;&lt;span&gt;&amp;#x3C;{ &lt;/span&gt;&lt;span&gt;auth&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;admin&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Admin&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; } }&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;auth&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;admin: &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Admin&lt;/span&gt;&lt;span&gt;(page)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;user: &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;(page)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// In tests&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;admin can manage users&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;auth&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; auth&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;admin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; auth&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;register&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-6-write-business-steps-not-technical-logs&quot;&gt;Rule #6: Write Business Steps, Not Technical Logs&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you use Allure or any step-based reporter, the quality of your step descriptions determines how useful the report is.&lt;/p&gt;
&lt;p&gt;The native Playwright way is &lt;code dir=&quot;auto&quot;&gt;test.step()&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Technical log — describes implementation&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;async &lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; test&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Click the login button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Login&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Business intent — describes what happened&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;async &lt;/span&gt;&lt;span&gt;loginAs&lt;/span&gt;&lt;span&gt;(user: User) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; test&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Authenticate as &quot;&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;username&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;loginPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt;(user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;username&lt;/span&gt;&lt;span&gt;, user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;password&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The first version breaks when you rename the button. The second version remains valid even if the entire login mechanism changes from a form to SSO. The report reads like a scenario, not a DOM manipulation log.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In &lt;a href=&quot;https://bdr-methodology.dev/blog/beyond-cucumber-base/&quot;&gt;BDR methodology&lt;/a&gt; we use a &lt;code dir=&quot;auto&quot;&gt;@Step&lt;/code&gt; decorator instead of wrapping every method manually — same result, cleaner syntax. If you’re interested in that approach, check it out.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;eslint-enforce-the-architecture-automatically&quot;&gt;ESLint: Enforce the Architecture Automatically&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The best rule is one that doesn’t require a code review comment:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;.eslintrc.js&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;module&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;exports&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;overrides: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;// Only applies inside test files — won&apos;t flag Page Object factories or helpers&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;files: [&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;tests/**/*.ts&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;**/*.spec.ts&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;rules: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;no-restricted-syntax&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;selector: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;NewExpression[callee.name=/.*Page$/]&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;message: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Use fixtures instead of new for Page Objects. See fixtures.ts.&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;selector: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;NewExpression[callee.name=/.*Flow$/]&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;message: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Use fixtures instead of new for Flow objects. See fixtures.ts.&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Scoping to &lt;code dir=&quot;auto&quot;&gt;tests/**&lt;/code&gt; prevents false positives — &lt;code dir=&quot;auto&quot;&gt;new Pagination()&lt;/code&gt; in your app code won’t trigger this. Only &lt;code dir=&quot;auto&quot;&gt;new LoginPage()&lt;/code&gt; inside test files will.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;architecture-cheat-sheet&quot;&gt;Architecture Cheat Sheet&lt;/h2&gt;&lt;/div&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Symptom&lt;/th&gt;&lt;th&gt;Root cause&lt;/th&gt;&lt;th&gt;Fix&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Refactoring takes days&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;new PageObject()&lt;/code&gt; in every test&lt;/td&gt;&lt;td&gt;Move to fixtures&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Parallel tests corrupt each other’s data&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;workerIndex&lt;/code&gt; as seed&lt;/td&gt;&lt;td&gt;Seed with &lt;code dir=&quot;auto&quot;&gt;testId&lt;/code&gt; + &lt;code dir=&quot;auto&quot;&gt;RUN_ID&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Can’t reproduce CI failures locally&lt;/td&gt;&lt;td&gt;Non-deterministic test data&lt;/td&gt;&lt;td&gt;Seeded faker fixture&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;fixtures.ts is 400 lines&lt;/td&gt;&lt;td&gt;No domain separation&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;mergeTests&lt;/code&gt; + domain files&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fixture collision, wrong object used&lt;/td&gt;&lt;td&gt;Flat fixture namespace&lt;/td&gt;&lt;td&gt;Namespace by domain&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Report is unreadable&lt;/td&gt;&lt;td&gt;Technical step descriptions&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;test.step()&lt;/code&gt; with business intent (or &lt;code dir=&quot;auto&quot;&gt;@Step&lt;/code&gt; in &lt;a href=&quot;https://bdr-methodology.dev/blog/beyond-cucumber-base/&quot;&gt;BDR&lt;/a&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-next&quot;&gt;What’s Next?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This architecture handles the object lifecycle and data isolation. The next layer is async reliability — &lt;code dir=&quot;auto&quot;&gt;expect.poll&lt;/code&gt;, idempotency keys for parallel API calls, and cleaning up test data without relying on &lt;code dir=&quot;auto&quot;&gt;afterEach&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Want to go deeper? Check out the advanced version: &lt;strong&gt;&lt;a href=&quot;https://bdr-methodology.dev/blog/playwright_fixtures_as_a_dependency_injection_container/&quot;&gt;Playwright Architecture at Scale: What Senior Engineers Do Differently&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;All patterns in this article are implemented in the &lt;a href=&quot;https://github.com/dmitryAQA/playwright-bdr-template&quot;&gt;Playwright BDR Template&lt;/a&gt; on GitHub.&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title>Playwright CI: What Senior Engineers Do Differently</title><link>https://bdr-methodology.dev/blog/what_senior_engineers_do_differently_pro/</link><guid isPermaLink="true">https://bdr-methodology.dev/blog/what_senior_engineers_do_differently_pro/</guid><pubDate>Tue, 12 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://bdr-methodology.dev/images/medium_ci.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;div&gt;&lt;h1 id=&quot;playwright-ci-what-senior-engineers-do-differently-&quot;&gt;Playwright CI: What Senior Engineers Do Differently &lt;span&gt; PRO IMPLEMENTATION &lt;/span&gt;&lt;/h1&gt;&lt;/div&gt;
&lt;p&gt;&lt;em&gt;New to Playwright architecture? Start with the fundamentals first: &lt;strong&gt;&lt;a href=&quot;https://bdr-methodology.dev/blog/why_your_playwright_tests_fail_in_ci_base/&quot;&gt;Why Your Playwright Tests Fail in CI (And Never Locally)&lt;/a&gt;&lt;/strong&gt; — the same concepts with more explanation and simpler examples.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Most teams reach a point where their test suite becomes a liability. Green locally, red in CI. Passes on retry, fails on the next run. The usual response is to increase timeouts, add &lt;code dir=&quot;auto&quot;&gt;waitForTimeout&lt;/code&gt;, and move on. The problem compounds quietly until someone spends a full day debugging a test that was never actually broken.&lt;/p&gt;
&lt;p&gt;This guide is about the architectural decisions that prevent that from happening. Not “use better selectors” — you already know that. The decisions that determine whether your test infrastructure scales or slowly collapses under its own weight.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Code examples are intentionally simplified — focus on the architectural pattern, not the implementation details.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;mental-model-shift-leaving-legacy-baggage-behind&quot;&gt;Mental Model Shift: Leaving Legacy Baggage Behind&lt;/h2&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;p&gt;##TL;DR&lt;/p&gt;
&lt;p&gt;Dependency Projects over globalSetup — fail fast when the environment is down, not after 800 tests. API auth in 50ms, not UI auth in 5 seconds. getByRole queries the accessibility tree — role survives refactoring, &lt;code dir=&quot;auto&quot;&gt;{ name }&lt;/code&gt; doesn’t survive translation. Web-first assertions poll until ready — isVisible() is a snapshot. expect.poll for state that changes outside the UI — webhooks, background jobs, queues. Trace Viewer’s Action/Before/After snapshots show you why a click failed, not just that it did.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Before getting into architecture, a quick audit. Senior engineers migrating from Selenium or Puppeteer often bring habits that fight Playwright instead of leveraging it. These aren’t stylistic preferences — they’re architectural differences that affect reliability at scale.&lt;/p&gt;
&lt;p&gt;If any of these look familiar in your codebase, fix them before layering on anything else:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;page.$()&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;page.$$()&lt;/code&gt; → &lt;code dir=&quot;auto&quot;&gt;getByRole()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;getByLabel()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;getByTestId()&lt;/code&gt;
Playwright locators are lazy and auto-retried on assertions. &lt;code dir=&quot;auto&quot;&gt;$()&lt;/code&gt; executes immediately against the current DOM state and cannot be polled.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;waitForSelector()&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;waitForTimeout()&lt;/code&gt; → Remove them
Playwright auto-waits for actionability before every interaction. Explicit waits are almost always either redundant or masking a real problem.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;waitForNavigation()&lt;/code&gt; → &lt;code dir=&quot;auto&quot;&gt;await expect(page).toHaveURL(&apos;/dashboard&apos;)&lt;/code&gt;
&lt;code dir=&quot;auto&quot;&gt;waitForNavigation()&lt;/code&gt; is prone to race conditions — it can resolve before the page is actually ready. &lt;code dir=&quot;auto&quot;&gt;toHaveURL&lt;/code&gt; polls until the URL matches, which is what you actually want.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;isVisible()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;isEnabled()&lt;/code&gt; in assertions → &lt;code dir=&quot;auto&quot;&gt;expect(loc).toBeVisible()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;expect(loc).toBeEnabled()&lt;/code&gt;
Snapshot methods return the state at one millisecond. Web-first assertions retry until the condition is true or the timeout expires.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;console.log(&apos;HERE&apos;)&lt;/code&gt; → Trace Viewer
Logs tell you that something happened. Traces show you the DOM, network, and console at the exact moment it happened — in CI, after the fact.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your team is mid-migration, this is worth a dedicated refactor sprint. The patterns below assume you’re past this baseline.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-problem-with-how-most-teams-structure-test-infrastructure&quot;&gt;The Problem With How Most Teams Structure Test Infrastructure&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The typical Playwright setup looks like this: a &lt;code dir=&quot;auto&quot;&gt;globalSetup&lt;/code&gt; file that handles authentication, maybe some shared fixtures, and a flat list of test files. This works at 50 tests. At 500, the cracks appear.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;globalSetup&lt;/code&gt; runs once, outside Playwright’s normal execution context. When it fails, you get dry Node.js logs. No trace, no network timeline, no DOM snapshots. You’re debugging blind.&lt;/p&gt;
&lt;p&gt;More critically: there’s no built-in way to say “don’t run 800 tests if the environment is down.” You get 800 failures that all say the same thing and tell you nothing useful.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-architecture-dependency-projects-as-a-dependency-graph&quot;&gt;The Architecture: Dependency Projects as a Dependency Graph&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The senior approach treats test infrastructure as a directed acyclic graph. Each node has prerequisites. If a prerequisite fails, dependent nodes don’t run.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;playwright.config.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;defineConfig&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;projects: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;auth-setup&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;testMatch:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;auth&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;ts&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;healthcheck&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;testMatch:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;health&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;ts&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;dependencies: [&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;auth-setup&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;chromium&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;use: { &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;devices[&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Desktop Chrome&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;] },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;dependencies: [&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;healthcheck&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;firefox&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;use: { &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;devices[&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Desktop Firefox&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;] },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;dependencies: [&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;healthcheck&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The order in the array doesn’t matter — Playwright builds the graph automatically. What matters is the &lt;code dir=&quot;auto&quot;&gt;dependencies&lt;/code&gt; field.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What this buys you:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When the staging environment goes down at 2am, your CI doesn’t burn 40 minutes running tests that will all fail for the same reason. The healthcheck fails, Playwright stops, you get one clear failure instead of eight hundred.&lt;/p&gt;
&lt;p&gt;When auth breaks after a backend deploy, you know immediately — not after waiting for the full suite to time out.&lt;/p&gt;
&lt;p&gt;And crucially: every node in this graph is a real Playwright test. That means full Trace Viewer support. When auth setup fails in CI, you open the trace and see exactly which API call returned 401, what the response body said, and what the DOM looked like if there was a redirect. Compare that to parsing a stack trace from &lt;code dir=&quot;auto&quot;&gt;globalSetup&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;authentication-the-50ms-vs-4-second-decision&quot;&gt;Authentication: The 50ms vs 4 Second Decision&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Every test that needs authentication has to pay the auth cost. The question is how much.&lt;/p&gt;
&lt;p&gt;UI login on a realistic app with SSR, asset loading, and form rendering: 2–5 seconds. API login: 50–100ms. At 500 tests, that’s 2500 seconds vs 50 seconds of auth overhead — before you’ve even started testing anything.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;auth.setup.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;authenticate&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/api/auth/login&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;data: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;email: &lt;/span&gt;&lt;span&gt;process&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;TEST_USER_EMAIL&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;password: &lt;/span&gt;&lt;span&gt;process&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;TEST_USER_PASSWORD&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(response&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;200&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Cookies are automatically captured from the request context&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;storageState&lt;/span&gt;&lt;span&gt;({ path: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;.auth/user.json&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;playwright.config.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;use: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;storageState: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;.auth/user.json&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The non-obvious part: you should have exactly one test that tests the login UI. Every other test that requires authentication just consumes the saved state. You’re not testing login 500 times — you’re testing it once and reusing the result.&lt;/p&gt;
&lt;p&gt;This also means your login test is isolated. If the login flow changes, one test fails, clearly, with a good error message. Not 400 tests failing with “element not found” somewhere in the middle of an unrelated scenario.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;locator-strategy-understanding-the-model-not-memorizing-the-rules&quot;&gt;Locator Strategy: Understanding the Model, Not Memorizing the Rules&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The common framing — “use &lt;code dir=&quot;auto&quot;&gt;getByRole&lt;/code&gt; for actions, &lt;code dir=&quot;auto&quot;&gt;getByTestId&lt;/code&gt; for stable anchors” — is a simplification that leads engineers to make wrong choices in edge cases. The more useful mental model is understanding what each locator actually queries and what that means for test reliability.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What &lt;code dir=&quot;auto&quot;&gt;getByRole&lt;/code&gt; actually does&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;getByRole&lt;/code&gt; queries the accessibility tree, not the DOM. The accessibility tree is a parallel representation of the page that browsers expose to screen readers and assistive technology. It’s built from semantic HTML — &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;button&gt;&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;input&gt;&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;h1&gt;&lt;/code&gt; — plus ARIA attributes.&lt;/p&gt;
&lt;p&gt;This distinction matters: CSS classes, DOM structure, and visual styling don’t affect the accessibility tree. A &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;div class=&quot;btn-primary&quot;&gt;&lt;/code&gt; has no role. A &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;button&gt;&lt;/code&gt; always has role &lt;code dir=&quot;auto&quot;&gt;button&lt;/code&gt; regardless of how it’s styled.&lt;/p&gt;
&lt;p&gt;One important nuance: &lt;code dir=&quot;auto&quot;&gt;getByRole&lt;/code&gt; usually takes a &lt;code dir=&quot;auto&quot;&gt;{ name: &apos;...&apos; }&lt;/code&gt; parameter to identify which element you mean. That name is resolved from the element’s text content, &lt;code dir=&quot;auto&quot;&gt;aria-label&lt;/code&gt;, or &lt;code dir=&quot;auto&quot;&gt;aria-labelledby&lt;/code&gt;. The role itself survives refactoring — but the name is tied to visible text, which means it breaks in multilingual apps when the locale changes. This is why &lt;code dir=&quot;auto&quot;&gt;getByTestId&lt;/code&gt; or a fixed &lt;code dir=&quot;auto&quot;&gt;aria-label&lt;/code&gt; are better choices when text is dynamic.&lt;/p&gt;
&lt;p&gt;When &lt;code dir=&quot;auto&quot;&gt;getByRole&lt;/code&gt; fails to find an element, it usually means one of two things: the element genuinely doesn’t exist yet (timing issue), or the element has no semantic role (accessibility issue). The second case is a real bug in your application — your test is catching it.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// This finds the button by its role and accessible name&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Works regardless of CSS class, DOM nesting, or visual styling&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Place order&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// If this fails because there&apos;s no button role —&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// that&apos;s an accessibility bug worth fixing, not a test bug&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The accessible name in &lt;code dir=&quot;auto&quot;&gt;{ name: &apos;...&apos; }&lt;/code&gt; can come from: the element’s text content, an &lt;code dir=&quot;auto&quot;&gt;aria-label&lt;/code&gt; attribute, or an &lt;code dir=&quot;auto&quot;&gt;aria-labelledby&lt;/code&gt; reference. Playwright checks all three automatically.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code dir=&quot;auto&quot;&gt;getByLabel&lt;/code&gt; is semantically stronger than &lt;code dir=&quot;auto&quot;&gt;getByTestId&lt;/code&gt; for forms&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;getByLabel&lt;/code&gt; finds form inputs by their associated label. The label is a contract: it tells users (and screen readers) what the field is for. If that contract changes, your test should know.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// If the label changes from &apos;Email address&apos; to &apos;Work email&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// this test fails — correctly, because the UX changed&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByLabel&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Email address&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;user@example.com&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;getByTestId&lt;/code&gt; on the same field would pass silently. You might want that stability, or you might want the test to catch the label change. The choice depends on whether the label is a UX requirement or an implementation detail.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When &lt;code dir=&quot;auto&quot;&gt;getByTestId&lt;/code&gt; is the right choice — and why&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;getByTestId&lt;/code&gt; bypasses the accessibility tree entirely. It finds elements by a &lt;code dir=&quot;auto&quot;&gt;data-testid&lt;/code&gt; attribute you add to the DOM. This makes it stable in specific situations where semantic locators genuinely don’t work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Complex component libraries&lt;/strong&gt; (Ant Design, MUI) — these generate DOM structures where a single Select or Combobox contains multiple elements with the same role: a hidden native input, a trigger button, a text field. &lt;code dir=&quot;auto&quot;&gt;getByRole(&apos;combobox&apos;)&lt;/code&gt; picks the first in DOM order — deterministic, but often wrong. And it can change between library versions as internal structure shifts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-language applications&lt;/strong&gt; — &lt;code dir=&quot;auto&quot;&gt;getByRole(&apos;button&apos;, { name: &apos;Submit&apos; })&lt;/code&gt; breaks when the locale changes to French. &lt;code dir=&quot;auto&quot;&gt;getByTestId(&apos;submit-button&apos;)&lt;/code&gt; doesn’t care about the label language&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A/B tests and personalization&lt;/strong&gt; — button text varies per user variant; &lt;code dir=&quot;auto&quot;&gt;getByTestId&lt;/code&gt; gives you a stable anchor&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Icon-only buttons&lt;/strong&gt; — SVG icons without &lt;code dir=&quot;auto&quot;&gt;aria-label&lt;/code&gt; have no accessible name; &lt;code dir=&quot;auto&quot;&gt;getByTestId&lt;/code&gt; is the fallback&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The tradeoff is real: &lt;code dir=&quot;auto&quot;&gt;getByTestId&lt;/code&gt; passes even if the element is visually broken, hidden by styles, or completely inaccessible to screen readers. You’re opting out of semantic validation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The decision algorithm&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;1. Does the element have a reliable semantic role?&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;→ Yes: use getByRole&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;→ No: continue&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;2. Is it a form field with a label?&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;→ Yes: use getByLabel&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;→ No: continue&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;3. Can you ask the developer to add aria-label?&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;→ Yes: add it, then use getByRole(..., { name: &apos;aria-label value&apos; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;→ No: continue&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;4. Use getByTestId — consciously, not by default&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;The correction to the “actions vs assertions” mental model&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The framing “use &lt;code dir=&quot;auto&quot;&gt;getByTestId&lt;/code&gt; for clicks, &lt;code dir=&quot;auto&quot;&gt;getByRole&lt;/code&gt; for assertions” is wrong in both directions. The question is not what you’re doing with the element — it’s how stable the element’s semantics are.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Both clicks — different locators because semantics differ&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Place order&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;(); &lt;/span&gt;&lt;span&gt;// stable role + name&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;lang-switcher&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;(); &lt;/span&gt;&lt;span&gt;// dynamic text, no stable role&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Both assertions — different locators for the same reason&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;heading&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toHaveText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Order confirmed&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;// content IS the requirement&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;order-status&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeVisible&lt;/span&gt;&lt;span&gt;(); &lt;/span&gt;&lt;span&gt;// existence matters, not label&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Use &lt;code dir=&quot;auto&quot;&gt;getByRole&lt;/code&gt; whenever the element has reliable semantics — for both clicks and assertions. Use &lt;code dir=&quot;auto&quot;&gt;getByTestId&lt;/code&gt; when semantics are unreliable — for both clicks and assertions.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;web-first-assertions-why-the-implementation-matters&quot;&gt;Web-First Assertions: Why the Implementation Matters&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The difference between &lt;code dir=&quot;auto&quot;&gt;isVisible()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;expect(locator).toBeVisible()&lt;/code&gt; isn’t just syntax. It’s the difference between a point-in-time snapshot and a polling loop.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;isVisible()&lt;/code&gt; makes one DOM query and returns immediately. If the element isn’t there at that exact millisecond, you get &lt;code dir=&quot;auto&quot;&gt;false&lt;/code&gt;. If your app is 10ms slower than usual in CI, the test fails.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;expect(locator).toBeVisible()&lt;/code&gt; polls the DOM every ~100ms until the condition is true or the timeout expires. It’s designed for asynchronous UIs.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Snapshot — fails if element isn&apos;t ready at this exact moment&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;visible&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;dialog&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isVisible&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(visible)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Polling — waits for the element to appear&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;dialog&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeVisible&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The more interesting case is &lt;code dir=&quot;auto&quot;&gt;expect.poll&lt;/code&gt; for non-UI state — and the contrast with &lt;code dir=&quot;auto&quot;&gt;waitForTimeout&lt;/code&gt; is worth making explicit.&lt;/p&gt;
&lt;p&gt;The tempting pattern:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Guessing — works until it doesn&apos;t&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Place order&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;waitForTimeout&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;5000&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;order&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;api&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getOrder&lt;/span&gt;&lt;span&gt;(id);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(order&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;PAID&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This works in development where the backend is fast and the machine is unloaded. In CI under parallel execution, the backend takes 5001ms on a slow run. The test fails — not because the feature is broken, but because you guessed wrong about timing.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;waitForTimeout&lt;/code&gt; is deterministic in the wrong direction: it fails on the system being slower than expected, but also wastes time when the system is faster. At 1000 tests, those wasted seconds add up to real CI cost.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The boundary that matters:&lt;/strong&gt; web-first assertions (&lt;code dir=&quot;auto&quot;&gt;toBeVisible&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;toHaveURL&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;toHaveText&lt;/code&gt;) cover 95% of cases — they have built-in retry and should always be your first choice. &lt;code dir=&quot;auto&quot;&gt;expect.poll&lt;/code&gt; is for the remaining 5%: state that changes outside the UI with no visible indicator. A background job updating order status in the DB. A payment webhook from Stripe arriving and updating payment state. A message processed from Kafka by another service. The common pattern: you triggered something, the UI has nothing useful to show, and you can only verify the result via a direct API call.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Background job updated order status — only verifiable via API&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; expect&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;poll&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;/api/orders/&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;orderId&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;order&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; order&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;message: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Order should reach CONFIRMED status&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;timeout: &lt;/span&gt;&lt;span&gt;30_000&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;CONFIRMED&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This is the correct tool for Eventual Consistency scenarios — distributed systems where the UI updates before the database has committed, or where background jobs need to complete before the state is queryable.&lt;/p&gt;
&lt;p&gt;A common mistake: manually setting &lt;code dir=&quot;auto&quot;&gt;intervals: [1000, 2000, 5000]&lt;/code&gt; on every &lt;code dir=&quot;auto&quot;&gt;poll&lt;/code&gt;. Playwright’s default intervals are reasonable. If you need custom timing, set a global timeout via &lt;code dir=&quot;auto&quot;&gt;test.setTimeout(60_000)&lt;/code&gt; for slow scenarios rather than tuning every individual poll.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;expecttopass-when-you-need-to-retry-an-entire-interaction&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;expect.toPass&lt;/code&gt;: When You Need to Retry an Entire Interaction&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;expect.poll&lt;/code&gt; retries a single assertion. Sometimes you need to retry a whole sequence of actions — click a button, wait for a state change, verify the result. That’s &lt;code dir=&quot;auto&quot;&gt;expect.toPass&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Sync&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;sync-status&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toHaveText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Complete&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;})&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toPass&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;intervals: [&lt;/span&gt;&lt;span&gt;1_000&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;2_000&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;5_000&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;timeout: &lt;/span&gt;&lt;span&gt;15_000&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Here the intervals make sense — you’re controlling how often to repeat a user-visible action, not an internal polling check.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The decision boundary between &lt;code dir=&quot;auto&quot;&gt;poll&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;toPass&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Use &lt;code dir=&quot;auto&quot;&gt;expect.poll&lt;/code&gt; when you’re checking state without side effects — reading an API endpoint, querying a value. The polling itself is invisible to the system.&lt;/p&gt;
&lt;p&gt;Use &lt;code dir=&quot;auto&quot;&gt;expect.toPass&lt;/code&gt; when the check requires triggering an action — clicking a refresh button, submitting a form, calling an endpoint that changes state. Here you want explicit control over retry frequency because each attempt has a visible effect.&lt;/p&gt;
&lt;p&gt;Mixing them up creates subtle problems: using &lt;code dir=&quot;auto&quot;&gt;expect.toPass&lt;/code&gt; for a pure state check works but fires unnecessary user actions. Using &lt;code dir=&quot;auto&quot;&gt;expect.poll&lt;/code&gt; when you need to click something doesn’t work at all — &lt;code dir=&quot;auto&quot;&gt;poll&lt;/code&gt; only retries the assertion, not the preceding action.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;hydration-the-silent-test-killer-in-ssr-applications&quot;&gt;Hydration: The Silent Test Killer in SSR Applications&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If your application uses Next.js, Nuxt, or any other SSR framework, you’ve likely hit this: Playwright clicks a button, no error is thrown, but the application doesn’t respond. The test eventually times out waiting for a state change that never came.&lt;/p&gt;
&lt;p&gt;The cause is hydration. The server sends fully-rendered HTML — the page looks complete, the button is in the DOM, Playwright’s actionability checks pass. But the JavaScript bundle hasn’t executed yet. There are no event listeners. The click lands on a dead element.&lt;/p&gt;
&lt;p&gt;The solution is to wait for a signal that hydration is complete before starting meaningful interactions:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Many frameworks add a class or attribute when hydration completes&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;waitForSelector&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;[data-hydrated=&quot;true&quot;]&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { state: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;attached&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Or wait for a loading indicator to disappear&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;locator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;#app-loading&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeHidden&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Or wait for a specific element that only appears post-hydration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;navigation&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeVisible&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The right signal depends on your application. Work with your frontend team to add a reliable hydration marker if one doesn’t exist. It’s a small investment that eliminates an entire category of intermittent failures.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A note on &lt;code dir=&quot;auto&quot;&gt;force: true&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When a click does nothing, &lt;code dir=&quot;auto&quot;&gt;force: true&lt;/code&gt; is tempting. Understand what you’re actually disabling. Playwright’s actionability checks verify four things before every interaction:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Visible&lt;/strong&gt; — element is not hidden by CSS or outside the viewport&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stable&lt;/strong&gt; — element is not moving (animations, transitions in progress)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enabled&lt;/strong&gt; — element is not in a disabled or read-only state&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Receiving events&lt;/strong&gt; — element is not covered by another element&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bypassing these means your test no longer reflects what a real user can do. The test passes; the user is still stuck.&lt;/p&gt;
&lt;p&gt;There is one legitimate exception: hidden file inputs (&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;input type=&quot;file&quot;&gt;&lt;/code&gt;). The native element is hard to style, so developers often intentionally hide it and show a custom button instead. In such cases, Playwright cannot interact with the hidden element without force: true. When you genuinely need force: true, document it:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// force: true required — file input is visually hidden by design&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;locator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;input[type=&quot;file&quot;]&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setInputFiles&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;document.pdf&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { force: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;For everything else: find what’s blocking the element and wait for it to clear. &lt;code dir=&quot;auto&quot;&gt;force: true&lt;/code&gt; without a comment is a code smell that should fail review.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;network-hygiene-whats-actually-slowing-your-tests&quot;&gt;Network Hygiene: What’s Actually Slowing Your Tests&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Third-party scripts are a common source of CI flakiness that’s easy to overlook. Analytics, support chat, session recording tools — these make network requests that can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Trigger &lt;code dir=&quot;auto&quot;&gt;networkidle&lt;/code&gt; waits to never settle (if a script sends requests every 400ms)&lt;/li&gt;
&lt;li&gt;Add latency to page loads&lt;/li&gt;
&lt;li&gt;Occasionally fail with 5xx errors that your application handles gracefully but that affect timing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The fix is straightforward:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// In a base fixture, applied to all tests&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;route&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;google-analytics&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;com&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;segment&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;com&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;intercom&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;io&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;fullstory&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;com&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;route&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// fulfill with 200 rather than abort — prevents apps from retrying indefinitely&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;route&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fulfill&lt;/span&gt;&lt;span&gt;({ status: &lt;/span&gt;&lt;span&gt;200&lt;/span&gt;&lt;span&gt;, body: &lt;/span&gt;&lt;span&gt;&apos;&apos;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;One subtlety: don’t block web fonts unless you’ve confirmed your app handles them gracefully. Missing fonts cause layout shifts, which fail Playwright’s stability checks and can make elements appear to move right before you try to interact with them.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;trace-viewer-making-ci-failures-debuggable&quot;&gt;Trace Viewer: Making CI Failures Debuggable&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The difference between a test suite that’s maintainable and one that isn’t often comes down to how debuggable failures are. A screenshot tells you what the page looked like. A trace tells you everything that happened.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;playwright.config.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;use: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;trace: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;retain-on-failure&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;screenshot: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;only-on-failure&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;video: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;retain-on-failure&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;// optional but useful for complex interactions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Navigating a trace effectively:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Metadata tab&lt;/strong&gt; — check this first when a test fails in CI but passes locally. It shows the browser version, viewport size, and launch parameters. “Element not found” failures that only happen in CI are often caused by a different viewport — the element exists but is off-screen or hidden by a responsive breakpoint.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Snapshots: Action / Before / After&lt;/strong&gt; — this is where most debugging happens. Each action in the trace has three states:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Before&lt;/strong&gt;: DOM state before Playwright started the action&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Action&lt;/strong&gt;: The moment of interaction — you’ll see a red dot showing exactly where Playwright clicked&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;After&lt;/strong&gt;: DOM state after the action completed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When a click does nothing, open the Action snapshot. If you see the red dot landing on a loading skeleton or an overlay div instead of your button, that’s your answer. The button was there, but something was on top of it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Network tab&lt;/strong&gt; — click any request to see headers, payload, and response body. When a test fails because a state change didn’t happen, check whether the API call was made, what it returned, and how long it took. A 200 response with an error in the body is a common cause of tests that fail without obvious reason.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Interactive DOM&lt;/strong&gt; — snapshots aren’t screenshots. They’re live DOM captures you can inspect with DevTools. Open any snapshot, right-click an element, and you have full access to computed styles, attributes, and the element tree — at the exact moment in time when the action occurred. This is the feature that makes Trace Viewer genuinely different from video recording.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;eslint-enforcing-architecture-automatically&quot;&gt;ESLint: Enforcing Architecture Automatically&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The best architectural rules are the ones that don’t require human enforcement. Configure these once and they apply to every PR forever:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// .eslintrc.js (ESLint v8)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;module&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;exports&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;extends: [&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;plugin:playwright/recommended&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;rules: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// Hard failures — these break things&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;playwright/no-wait-for-timeout&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;playwright/no-focused-test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;playwright/no-page-pause&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;playwright/missing-playwright-await&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// Warnings — architectural debt worth addressing&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;playwright/prefer-web-first-assertions&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;playwright/no-force-option&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;playwright/no-skipped-test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// Prevent bypassing seeded faker (if you use deterministic test data)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;no-restricted-imports&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;paths: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@faker-js/faker&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;message: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Use the seeded faker fixture from test context for reproducible test data.&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;For ESLint v9+:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;eslint.config.mjs&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; playwright &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;eslint-plugin-playwright&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;files: [&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;tests/**&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;...playwright&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;configs&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;flat/recommended&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;rules: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;...playwright&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;configs&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;flat/recommended&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;rules&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;playwright/no-wait-for-timeout&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;playwright/no-focused-test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;playwright/no-page-pause&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;playwright/missing-playwright-await&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;playwright/prefer-web-first-assertions&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;playwright/no-force-option&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;error&lt;/code&gt; vs &lt;code dir=&quot;auto&quot;&gt;warn&lt;/code&gt; distinction matters. &lt;code dir=&quot;auto&quot;&gt;error&lt;/code&gt; means the CI pipeline fails. &lt;code dir=&quot;auto&quot;&gt;warn&lt;/code&gt; means the developer sees it in their IDE and in the PR, but it doesn’t block a merge. Use &lt;code dir=&quot;auto&quot;&gt;error&lt;/code&gt; for things that will definitely cause test failures or leave debug artifacts in CI. Use &lt;code dir=&quot;auto&quot;&gt;warn&lt;/code&gt; for patterns that indicate technical debt but may have legitimate exceptions.&lt;/p&gt;
&lt;p&gt;On that note: rules exist to be broken consciously. If you’re working with a heavy component library — Ant Design, MUI with deeply nested generated selectors — sometimes &lt;code dir=&quot;auto&quot;&gt;// eslint-disable-next-line&lt;/code&gt; is the honest answer. The difference between a senior and a junior here isn’t that the senior never disables rules. It’s that they write a comment explaining why, and they don’t do it as a reflex.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-flakiness-diagnostic-framework&quot;&gt;The Flakiness Diagnostic Framework&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;When a test fails intermittently, the question isn’t “why did it fail this time?” It’s “what class of problem is this?”&lt;/p&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Symptom&lt;/th&gt;&lt;th&gt;Root cause&lt;/th&gt;&lt;th&gt;Solution&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Click lands, nothing happens&lt;/td&gt;&lt;td&gt;Hydration — JS not loaded yet&lt;/td&gt;&lt;td&gt;Wait for hydration signal&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Passes locally, fails in CI consistently&lt;/td&gt;&lt;td&gt;Resource contention / network latency&lt;/td&gt;&lt;td&gt;Block third-party scripts, check &lt;code dir=&quot;auto&quot;&gt;workers&lt;/code&gt; config&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fails on 1 in 10 runs, no pattern&lt;/td&gt;&lt;td&gt;Race condition in assertion&lt;/td&gt;&lt;td&gt;Replace snapshot assertion with Web-first assertion&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;All tests fail simultaneously&lt;/td&gt;&lt;td&gt;Environment down / auth broken&lt;/td&gt;&lt;td&gt;Add healthcheck dependency project&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fails after deploy, selector not found&lt;/td&gt;&lt;td&gt;Fragile locator&lt;/td&gt;&lt;td&gt;Replace CSS with &lt;code dir=&quot;auto&quot;&gt;getByTestId&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;getByRole&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Timeout waiting for state change&lt;/td&gt;&lt;td&gt;Eventual consistency&lt;/td&gt;&lt;td&gt;Replace &lt;code dir=&quot;auto&quot;&gt;waitForTimeout&lt;/code&gt; with &lt;code dir=&quot;auto&quot;&gt;expect.poll&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The last row is where most teams go wrong. When a test times out waiting for a database state change, the instinct is to increase the timeout. The correct fix is to stop guessing how long the operation takes and start asking the system when it’s done.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;worker-configuration-the-resource-math&quot;&gt;Worker Configuration: The Resource Math&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;fullyParallel: true&lt;/code&gt; is one line. The consequences of getting the worker count wrong are dozens of intermittent failures that look like application bugs.&lt;/p&gt;
&lt;p&gt;The math: each Playwright worker runs a browser instance. A Chromium instance needs roughly 200–300MB of RAM under load. On a CI agent with 4GB RAM, running 20 workers means 4–6GB just for browsers — before Node.js, your application server, and the OS.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;playwright.config.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;defineConfig&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;fullyParallel: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;workers: process&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;CI&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;50%&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;undefined&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;50%&lt;/code&gt; of available cores leaves headroom for everything else. The tests run slightly slower than theoretical maximum, but they run reliably. The alternative — running at 100% and getting OOM kills that look like test failures — is worse in every way.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;what-this-architecture-actually-buys-you&quot;&gt;What This Architecture Actually Buys You&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;None of these patterns are difficult to implement. The dependency graph takes an afternoon. API auth is 20 lines. ESLint config is copy-paste.&lt;/p&gt;
&lt;p&gt;The compounding value is that they change the economics of flakiness. Without them, every intermittent failure requires investigation — is this a real bug or noise? With them, most failures are deterministic and self-explanatory.&lt;/p&gt;
&lt;p&gt;A healthcheck that fails clearly is better than 800 timeouts that might be anything. A trace that shows “button covered by loading overlay” is better than 40 minutes of local reproduction attempts. An ESLint error that prevents &lt;code dir=&quot;auto&quot;&gt;waitForTimeout&lt;/code&gt; from being committed is better than a code review comment that gets ignored.&lt;/p&gt;
&lt;p&gt;The goal isn’t zero flakiness — distributed systems are inherently non-deterministic. The goal is failures that tell you something useful.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;The patterns in this article are implemented in the &lt;a href=&quot;https://github.com/dmitryAQA/playwright-bdr-template&quot;&gt;Playwright BDR Template&lt;/a&gt; — a reference implementation you can clone and run.&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title>Why Your Playwright Tests Fail in CI (And Never Locally)</title><link>https://bdr-methodology.dev/blog/why_your_playwright_tests_fail_in_ci_base/</link><guid isPermaLink="true">https://bdr-methodology.dev/blog/why_your_playwright_tests_fail_in_ci_base/</guid><pubDate>Mon, 11 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://bdr-methodology.dev/images/devto_ci.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;div&gt;&lt;h1 id=&quot;why-your-playwright-tests-fail-in-ci-and-never-locally-&quot;&gt;Why Your Playwright Tests Fail in CI (And Never Locally) &lt;span&gt; CONCEPT &lt;/span&gt;&lt;/h1&gt;&lt;/div&gt;
&lt;p&gt;You run your tests locally — everything is green. You push to CI — three tests fail. You run CI again — different three tests fail. Sound familiar?&lt;/p&gt;
&lt;p&gt;This isn’t bad luck. It’s a set of fixable architectural mistakes. In this guide I’ll walk you through the six rules that eliminated flakiness in our test suite. No magic, no “just increase the timeout” advice.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;All code examples are simplified for clarity — focus on the idea, not the boilerplate.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;tldr&quot;&gt;TL;DR&lt;/h2&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Use Dependency Projects instead of &lt;code dir=&quot;auto&quot;&gt;globalSetup&lt;/code&gt; — if the environment is down, stop immediately instead of running 1000 failing tests&lt;/li&gt;
&lt;li&gt;Locator priority: &lt;code dir=&quot;auto&quot;&gt;getByRole&lt;/code&gt; &gt; &lt;code dir=&quot;auto&quot;&gt;getByLabel&lt;/code&gt; &gt; &lt;code dir=&quot;auto&quot;&gt;getByTestId&lt;/code&gt;. CSS selectors — last resort only&lt;/li&gt;
&lt;li&gt;Never use &lt;code dir=&quot;auto&quot;&gt;isVisible()&lt;/code&gt; in assertions — it’s a snapshot. Use Web-first assertions that wait&lt;/li&gt;
&lt;li&gt;Block analytics and tracking scripts with &lt;code dir=&quot;auto&quot;&gt;page.route&lt;/code&gt; — they cause &lt;code dir=&quot;auto&quot;&gt;networkidle&lt;/code&gt; to hang&lt;/li&gt;
&lt;li&gt;Trace Viewer is your debugging tool. Screenshots show you what, traces show you why&lt;/li&gt;
&lt;li&gt;Always authenticate via API, not UI — 50ms vs 5 seconds, per test&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;why-ci-breaks-tests-that-pass-locally&quot;&gt;Why CI breaks tests that pass locally&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Your local machine is fast. CI is not. Less CPU, higher latency between services, multiple parallel processes all competing for resources. Asynchronous problems exist locally too — a powerful machine and fast network just hide them. When conditions get slightly worse, timings fall apart.&lt;/p&gt;
&lt;p&gt;This is why “works on my machine” is such a common story in test automation.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-1-stop-running-tests-in-a-vacuum&quot;&gt;Rule #1: Stop Running Tests in a Vacuum&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;When your staging environment goes down at night, do you want to run 1000 tests just to get 1000 failures? Of course not. But that’s exactly what happens without a proper dependency chain.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The solution: Dependency Projects&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Instead of one big &lt;code dir=&quot;auto&quot;&gt;globalSetup&lt;/code&gt; file, build a dependency graph in your Playwright config:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;playwright.config.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;defineConfig&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;projects: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// Step 1: Authenticate and save session&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;auth-setup&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;testMatch:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;auth&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;ts&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// Step 2: Check if the environment is actually alive&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;healthcheck&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;testMatch:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;health&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;ts&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;dependencies: [&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;auth-setup&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// Step 3: Only run real tests if steps 1 and 2 passed&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;chromium&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;use: { &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;devices[&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Desktop Chrome&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;] },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;dependencies: [&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;healthcheck&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If auth fails or the environment is down — Playwright stops immediately. No wasted CI minutes, no flood of useless alerts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why not &lt;code dir=&quot;auto&quot;&gt;globalSetup&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;globalSetup&lt;/code&gt; gives you dry logs when something fails. Dependency Projects give you full Trace Viewer support — you can see exactly what happened during setup: network requests, screenshots, console errors. And you can run just one project in isolation: &lt;code dir=&quot;auto&quot;&gt;npx playwright test --project=auth-setup&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-2-authenticate-via-api-not-ui&quot;&gt;Rule #2: Authenticate via API, Not UI&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;UI login is slow. A full page load with all assets and rendering takes 2–5 seconds. An API login call takes 50–100ms. At CI scale, this difference adds up fast.&lt;/p&gt;
&lt;p&gt;More importantly: you shouldn’t be testing your login form 500 times. Test it once, in a dedicated test. For everything else, just reuse the session.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;auth.setup.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;authenticate&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Direct API call — no browser rendering needed&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/api/login&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;data: { username: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;user@example.com&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, password: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;secret&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Save cookies and storage state for all other tests&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;storageState&lt;/span&gt;&lt;span&gt;({ path: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;.auth/user.json&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Then in your config:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;use: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;storageState: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;.auth/user.json&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Every test now starts already authenticated. Zero UI login overhead.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-3-use-the-right-locators--and-know-why&quot;&gt;Rule #3: Use the Right Locators — and Know Why&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A locator isn’t just a way to find an element. It’s a statement about what your test actually cares about. The wrong locator makes tests brittle. The right locator makes failures meaningful.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code dir=&quot;auto&quot;&gt;getByRole&lt;/code&gt; is the default choice&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;getByRole&lt;/code&gt; finds elements by their semantic role in the accessibility tree — &lt;code dir=&quot;auto&quot;&gt;button&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;heading&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;link&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;dialog&lt;/code&gt;. This matters because role is tied to behavior, not implementation. A CSS class can be renamed, a DOM structure can be refactored — but if the element is still a button, &lt;code dir=&quot;auto&quot;&gt;getByRole&lt;/code&gt; still finds it.&lt;/p&gt;
&lt;p&gt;One important nuance: &lt;code dir=&quot;auto&quot;&gt;getByRole&lt;/code&gt; often takes a &lt;code dir=&quot;auto&quot;&gt;{ name: &apos;...&apos; }&lt;/code&gt; parameter to narrow down which element you mean. That name comes from the button’s text or &lt;code dir=&quot;auto&quot;&gt;aria-label&lt;/code&gt;. If you rely on visible text and the app is multilingual — that name changes per locale, and your locator breaks. The role survives translation. The name doesn’t.&lt;/p&gt;
&lt;p&gt;There’s a bonus: if &lt;code dir=&quot;auto&quot;&gt;getByRole&lt;/code&gt; can’t find your element, it often means the element has no semantic role — which is an accessibility bug. Your test is catching a real problem.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Finds the button regardless of CSS class or DOM structure&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Place order&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code dir=&quot;auto&quot;&gt;getByLabel&lt;/code&gt; for form fields&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;getByLabel&lt;/code&gt; finds inputs by their associated label text. The label is a contract between the UI and the user — if it changes, that’s a UX change worth knowing about. This locator also catches cases where a field exists but has no label — another real bug.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByLabel&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Email address&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;user@example.com&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;When &lt;code dir=&quot;auto&quot;&gt;getByTestId&lt;/code&gt; is the right answer&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;getByTestId&lt;/code&gt; is stable but semantically blind — it finds the element regardless of its role, text, or visual state. That’s a feature in specific situations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ant Design, Material UI, or other component libraries&lt;/strong&gt; — these generate DOM structures where a single Select or Combobox contains multiple elements with the same role: a hidden native input, a trigger button, a text field. getByRole(‘combobox’) picks the first one in DOM order, which is often not the one you need to interact with — and it can change between library versions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-language apps&lt;/strong&gt; — button text changes per locale; &lt;code dir=&quot;auto&quot;&gt;getByTestId&lt;/code&gt; doesn’t care&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A/B tests or personalization&lt;/strong&gt; — the label varies per user variant&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Icon buttons without text&lt;/strong&gt; — SVG icons with no &lt;code dir=&quot;auto&quot;&gt;aria-label&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Stable regardless of language or variant&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;checkout-button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The tradeoff: &lt;code dir=&quot;auto&quot;&gt;getByTestId&lt;/code&gt; passes even if the button is visually broken, hidden by styles, or inaccessible to screen readers. You’re trading semantic coverage for stability. That’s a conscious choice, not a default.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The decision algorithm&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Try &lt;code dir=&quot;auto&quot;&gt;getByRole&lt;/code&gt; first — if the element has a semantic role, this is always better&lt;/li&gt;
&lt;li&gt;If text is dynamic (translations, A/B) or the element has no stable role — ask your developer to add an &lt;code dir=&quot;auto&quot;&gt;aria-label&lt;/code&gt;. Then use &lt;code dir=&quot;auto&quot;&gt;getByRole(..., { name: &apos;aria-label value&apos; })&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;If that’s not possible — use &lt;code dir=&quot;auto&quot;&gt;getByTestId&lt;/code&gt; without guilt&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Both of these use getByRole — role is stable&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Place order&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;heading&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toHaveText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Order confirmed&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Both of these use getByTestId — text is dynamic&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;checkout-button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;order-status&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toHaveText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Confirmed&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-4-stop-using-isvisible-in-assertions&quot;&gt;Rule #4: Stop Using &lt;code dir=&quot;auto&quot;&gt;isVisible()&lt;/code&gt; in Assertions&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This is one of the most common sources of flakiness. Here’s why:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// This checks visibility at this exact millisecond&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;isVisible&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isVisible&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(isVisible)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeTruthy&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If the page is still loading at that millisecond — the test fails. Not because something is broken, but because you asked too early.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Web-first assertions wait for you:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// This polls the DOM until the condition is true (or timeout)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeVisible&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The difference: &lt;code dir=&quot;auto&quot;&gt;expect(locator).toBeVisible()&lt;/code&gt; keeps checking every ~100ms until the element appears or the timeout is reached. It’s a built-in retry loop.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quick reference:&lt;/strong&gt;&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Instead of this&lt;/th&gt;&lt;th&gt;Use this&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;await loc.isVisible()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;await expect(loc).toBeVisible()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;await loc.textContent() === &apos;...&apos;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;await expect(loc).toHaveText(&apos;...&apos;)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;await loc.count()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;await expect(loc).toHaveCount(3)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;await loc.isChecked()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;await expect(loc).toBeChecked()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;await loc.isEnabled()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;await expect(loc).toBeEnabled()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One exception:&lt;/strong&gt; &lt;code dir=&quot;auto&quot;&gt;isVisible()&lt;/code&gt; is fine inside conditional logic — for example, to decide whether to close a cookie banner before continuing. Just don’t use it as a final assertion.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-5-waitfortimeout-is-not-a-solution--heres-what-to-use-instead&quot;&gt;Rule #5: &lt;code dir=&quot;auto&quot;&gt;waitForTimeout&lt;/code&gt; is not a solution — here’s what to use instead&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you feel the urge to add &lt;code dir=&quot;auto&quot;&gt;waitForTimeout&lt;/code&gt; — stop. In 95% of cases there’s a better tool. The question is which one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use web-first assertions (&lt;code dir=&quot;auto&quot;&gt;toBeVisible&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;toHaveText&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;toHaveURL&lt;/code&gt;, etc.) when:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An element appears or disappears after a click&lt;/li&gt;
&lt;li&gt;The URL changes after navigation&lt;/li&gt;
&lt;li&gt;Text updates after data loads&lt;/li&gt;
&lt;li&gt;A form shows a validation error&lt;/li&gt;
&lt;li&gt;Anything that is visible in the UI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This covers the vast majority of cases. Web-first assertions have built-in retry — you don’t need anything else.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Built-in retry — no polling needed&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Order confirmed&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeVisible&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toHaveURL&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/dashboard&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Use &lt;code dir=&quot;auto&quot;&gt;expect.poll&lt;/code&gt; when:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A background job updated order status in the DB, and the UI only shows a spinner&lt;/li&gt;
&lt;li&gt;A payment webhook arrived from Stripe or PayPal and updated the payment status&lt;/li&gt;
&lt;li&gt;A message was processed from a queue (Kafka, RabbitMQ) by another service&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The common pattern: &lt;strong&gt;you clicked something, the UI shows nothing useful (or just a spinner), but something should have happened behind the scenes.&lt;/strong&gt; You can only verify it via a direct API call.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Background job updated order status — not visible in UI&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; expect&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;poll&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;/api/orders/&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;orderId&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;order&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; order&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;message: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Waiting for order status to become PAID&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;timeout: &lt;/span&gt;&lt;span&gt;30_000&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;PAID&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Use &lt;code dir=&quot;auto&quot;&gt;expect.toPass&lt;/code&gt; when:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You need to click a button repeatedly until the UI shows the expected result&lt;/li&gt;
&lt;li&gt;An action needs to be repeated until a condition is met&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Click Refresh until status appears in UI&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Refresh&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Status: Ready&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeVisible&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;})&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toPass&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;intervals: [&lt;/span&gt;&lt;span&gt;1_000&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;2_000&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;5_000&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;timeout: &lt;/span&gt;&lt;span&gt;15_000&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; If you find yourself writing &lt;code dir=&quot;auto&quot;&gt;expect.poll&lt;/code&gt; more than once or twice per test file — stop and reconsider. Either the UI is missing proper loading indicators, or the architecture needs rethinking. &lt;code dir=&quot;auto&quot;&gt;expect.poll&lt;/code&gt; is a last resort, not a default tool.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-6-block-analytics-and-tracking-scripts&quot;&gt;Rule #6: Block Analytics and Tracking Scripts&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Your app loads Google Analytics, a support chat widget, maybe a heatmap tool. These services are slow, sometimes unreliable, and completely irrelevant to what you’re testing. They also interfere with &lt;code dir=&quot;auto&quot;&gt;networkidle&lt;/code&gt; waits.&lt;/p&gt;
&lt;p&gt;Block them:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// In your fixture or beforeEach&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;route&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;google-analytics&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;com&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;intercom&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;io&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;hotjar&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;com&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;route&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Use fulfill instead of abort so the app doesn&apos;t hang waiting for a response&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;route&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fulfill&lt;/span&gt;&lt;span&gt;({ status: &lt;/span&gt;&lt;span&gt;200&lt;/span&gt;&lt;span&gt;, body: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;ok&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Watch out for fonts:&lt;/strong&gt; Blocking external fonts can cause layout shifts, which may trigger Playwright’s stability checks and slow things down. Either allow fonts through or make sure your app handles missing fonts gracefully.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-7-use-trace-viewer-not-screenshots&quot;&gt;Rule #7: Use Trace Viewer, Not Screenshots&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;When a test fails in CI, a screenshot shows you what the page looked like. Trace Viewer shows you &lt;em&gt;why&lt;/em&gt; it failed.&lt;/p&gt;
&lt;p&gt;A screenshot: a frozen image of a page that looks fine.&lt;/p&gt;
&lt;p&gt;Trace Viewer: every action, every network request, every console error, the DOM state before and after each step — all in a timeline you can scrub through.&lt;/p&gt;
&lt;p&gt;Enable it in your config:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;playwright.config.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;use: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Only save traces when tests fail — keeps your artifacts small&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;trace: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;retain-on-failure&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;screenshot: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;only-on-failure&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;What to look for in Trace Viewer:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Actionability tab&lt;/strong&gt;: If a click didn’t work, this tells you exactly which element was blocking it (a loading skeleton, an overlay, a tooltip)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network tab&lt;/strong&gt;: See which API calls were slow or failed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Console tab&lt;/strong&gt;: See JavaScript errors that don’t show up in your test output&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Snapshots&lt;/strong&gt;: The actual DOM state at each step — you can open DevTools on a past moment in time&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When a test fails because a button was “covered by another element” — Trace Viewer shows you the exact element, with a red dot on the snapshot. No guessing required.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;hydration-why-clicks-sometimes-do-nothing&quot;&gt;Hydration: Why Clicks Sometimes Do Nothing&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you work with React, Next.js, Vue, or Nuxt — you’ve probably seen this: Playwright clicks a button, no error is thrown, but nothing happens.&lt;/p&gt;
&lt;p&gt;This is hydration. The server sends HTML that looks like a working page, but the JavaScript hasn’t loaded yet. The button exists in the DOM but has no event listeners. Playwright clicks it, the click lands, and nothing responds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Wait for a signal that the app is ready before interacting:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Wait for a loading indicator to disappear&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;locator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;#global-loader&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeHidden&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Or wait for a class that your app adds when hydration is complete&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;waitForSelector&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;.app-ready&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { state: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;attached&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;About &lt;code dir=&quot;auto&quot;&gt;force: true&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You might be tempted to use &lt;code dir=&quot;auto&quot;&gt;force: true&lt;/code&gt; to bypass Playwright’s checks. Before you do, understand what you’re skipping. Playwright’s actionability checks verify that an element is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Visible&lt;/strong&gt; — not hidden by CSS or outside the viewport&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stable&lt;/strong&gt; — not moving (animations, transitions)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enabled&lt;/strong&gt; — not disabled or read-only&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Receiving events&lt;/strong&gt; — not covered by another element like a modal or overlay&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you add &lt;code dir=&quot;auto&quot;&gt;force: true&lt;/code&gt;, all four checks are disabled. You’re no longer testing what a real user experiences — you’re manipulating the DOM directly. The test passes, the user is still stuck.&lt;/p&gt;
&lt;p&gt;There is one legitimate exception: hidden file inputs (&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;input type=&quot;file&quot;&gt;&lt;/code&gt;). Browsers render this element as a native, hard-to-style button. Developers often intentionally hide it (make it invisible) and draw a custom button on top, consistent with the rest of the design. In such cases, Playwright cannot interact with the hidden element without &lt;code dir=&quot;auto&quot;&gt;force: true&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// force: true required — file input is visually hidden by design,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// replaced by a styled button that triggers it&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;locator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;input[type=&quot;file&quot;]&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setInputFiles&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;file.pdf&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { force: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;For everything else — find the root cause. If an element is covered, wait for the overlay to disappear. If it’s disabled, wait for the enabled state. &lt;code dir=&quot;auto&quot;&gt;force: true&lt;/code&gt; without a comment is a red flag in code review.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;eslint-let-the-robot-enforce-the-rules&quot;&gt;ESLint: Let the Robot Enforce the Rules&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Don’t explain these rules in every code review. Automate it:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;.eslintrc.js&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;module&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;exports&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;extends: [&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;plugin:playwright/recommended&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;rules: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;playwright/no-wait-for-timeout&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;// No sleeps&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;playwright/no-focused-test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;// No test.only in commits&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;playwright/no-page-pause&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;// No page.pause() in commits&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;playwright/prefer-web-first-assertions&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;// Nudge toward better assertions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;playwright/no-force-option&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;// Flag force: true usage&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;error&lt;/code&gt; for things that definitely break your tests or CI. &lt;code dir=&quot;auto&quot;&gt;warn&lt;/code&gt; for architectural debt that’s worth addressing but not blocking.&lt;/p&gt;
&lt;p&gt;One more thing: rules exist to be broken consciously. If you’re working with a component library that generates dynamic selectors you can’t control, &lt;code dir=&quot;auto&quot;&gt;// eslint-disable-next-line&lt;/code&gt; is sometimes the honest answer. The key word is &lt;em&gt;consciously&lt;/em&gt; — disable the rule, write a comment explaining why, and move on. What you want to avoid is blanket disables that hide real problems.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;migration-cheat-sheet-old-playwright-vs-current&quot;&gt;Migration Cheat Sheet: Old Playwright vs Current&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you’re coming from Selenium or older Playwright patterns, here’s the direct translation:&lt;/p&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;What you used to do&lt;/th&gt;&lt;th&gt;What to do now&lt;/th&gt;&lt;th&gt;Why&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;page.$()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;page.$$()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;getByRole()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;getByLabel()&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;getByTestId()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Lazy evaluation + automatic retry on assertions&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;waitForSelector()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Not needed — built into actions&lt;/td&gt;&lt;td&gt;Playwright waits for actionability before every click/fill&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;waitForTimeout(3000)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;expect(loc).toBeVisible()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Polls until ready instead of guessing&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;waitForNavigation()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;await expect(page).toHaveURL(&apos;/dashboard&apos;)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;toHaveURL&lt;/code&gt; has built-in polling, no race condition&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;isVisible()&lt;/code&gt; in assertions&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;expect(loc).toBeVisible()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;One is a snapshot, the other waits&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;console.log(&apos;HERE&apos;)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Trace Viewer&lt;/td&gt;&lt;td&gt;Full timeline with network, DOM, console — in CI&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;flakiness-cheat-sheet&quot;&gt;Flakiness Cheat Sheet&lt;/h2&gt;&lt;/div&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Symptom&lt;/th&gt;&lt;th&gt;Likely cause&lt;/th&gt;&lt;th&gt;Fix&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Click lands, nothing happens&lt;/td&gt;&lt;td&gt;Hydration&lt;/td&gt;&lt;td&gt;Wait for app-ready signal&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Timeout in CI, passes locally&lt;/td&gt;&lt;td&gt;Slow network / analytics&lt;/td&gt;&lt;td&gt;Block third-party scripts&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Selector not found after deploy&lt;/td&gt;&lt;td&gt;Fragile CSS / text changed&lt;/td&gt;&lt;td&gt;Use &lt;code dir=&quot;auto&quot;&gt;data-testid&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;getByRole&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Random failures, no pattern&lt;/td&gt;&lt;td&gt;Race condition in assertions&lt;/td&gt;&lt;td&gt;Switch to Web-first assertions&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;All tests fail at once&lt;/td&gt;&lt;td&gt;Environment down&lt;/td&gt;&lt;td&gt;Add healthcheck dependency&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-next&quot;&gt;What’s Next?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;These six rules cover the most common sources of flakiness. Once you have them in place, the next level is async handling at scale — &lt;code dir=&quot;auto&quot;&gt;expect.poll&lt;/code&gt;, idempotency keys, contract testing, and data hygiene.&lt;/p&gt;
&lt;p&gt;Want to go deeper into the architecture? Check out the advanced version of this guide: &lt;strong&gt;&lt;a href=&quot;https://bdr-methodology.dev/blog/what_senior_engineers_do_differently_pro/&quot;&gt;Playwright CI: What Senior Engineers Do Differently&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;All patterns in this article are implemented in the &lt;a href=&quot;https://github.com/dmitryAQA/playwright-bdr-template&quot;&gt;Playwright BDR Template&lt;/a&gt; on GitHub — clone it and see how everything fits together.&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title>Why flat test architectures fail: Moving beyond POM to a 3-layer BDR approach</title><link>https://bdr-methodology.dev/blog/3-layers-architecture-pro/</link><guid isPermaLink="true">https://bdr-methodology.dev/blog/3-layers-architecture-pro/</guid><pubDate>Sun, 03 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://bdr-methodology.dev/images/Why-flat-test-architectures-fail.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;div&gt;&lt;h1 id=&quot;why-flat-test-architectures-fail-moving-beyond-pom-to-a-3-layer-bdr-approach-&quot;&gt;Why flat test architectures fail: Moving beyond POM to a 3-layer BDR approach &lt;span&gt; PRO IMPLEMENTATION &lt;/span&gt;&lt;/h1&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;This is a technical deep dive into BDR’s layered architecture. For an introduction to why BDR exists and how the &lt;code dir=&quot;auto&quot;&gt;@Step&lt;/code&gt; decorator works internally, see &lt;a href=&quot;https://bdr-methodology.dev/blog/beyond-cucumber-pro&quot;&gt;Beyond Cucumber: A Type-Safe 4-Layer BDD Architecture with Playwright&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; BDR (Behavior-Driven Living Requirements) is my own architectural approach to organizing Playwright tests — a Cucumber-free alternative to BDD that I designed and documented at &lt;a href=&quot;https://bdr-methodology.dev/concepts/manifesto&quot;&gt;bdr-methodology.dev&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-problem-with-flat-test-architecture&quot;&gt;The problem with flat test architecture&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Most Playwright projects start with two layers: Page Objects and tests. It works fine at twenty tests. At two hundred, it collapses.&lt;/p&gt;
&lt;p&gt;Here’s a typical flat architecture failure:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// The test knows too much&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;User can complete purchase&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Setup — copy-pasted from 40 other tests&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;goto&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/login&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByLabel&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Email&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;user@example.com&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByLabel&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Password&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;password123&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Log In&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// The actual test&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;add-to-cart&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;checkout-submit&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByLabel&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Card Number&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;4242424242424242&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Pay&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Order confirmed&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeVisible&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;When this test fails, your report shows:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;✗ Test: User can complete purchase&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- goto&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- fill&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- fill&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- click&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- click&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- click&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- fill&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- click&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Which click failed? What was the state? What was being tested — login, cart, or payment? Nobody knows without reading the entire test.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;why-three-layers-not-two&quot;&gt;Why three layers, not two&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The standard advice is “add a Flow layer”. But most teams add it for the wrong reason — DRY. They think “I keep copy-pasting the cart setup, let me extract it into a Flow.”&lt;/p&gt;
&lt;p&gt;DRY is a nice side effect. It’s not the point.&lt;/p&gt;
&lt;p&gt;The real reason for three layers is &lt;strong&gt;separation of abstraction levels&lt;/strong&gt;. Each layer speaks a different language:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;POM&lt;/strong&gt; speaks the language of markup: “click this button”, “fill this field”, “find this element”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flow&lt;/strong&gt; speaks the language of business: “add product to cart”, “place order”, “process payment” — these are self-contained business entities, not just reusable helpers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spec&lt;/strong&gt; speaks the language of scenarios: assembles business entities like Lego to express intent&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s what that looks like in practice with an e-commerce app:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Three separate business entities — each its own Flow&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartFlow&lt;/span&gt;&lt;span&gt;      { &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;addProduct&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;product&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;Product&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;} }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CheckoutFlow&lt;/span&gt;&lt;span&gt;  { &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;placeOrder&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;address&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;Address&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;} }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PaymentFlow&lt;/span&gt;&lt;span&gt;   { &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pay&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;card&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;Card&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;} }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Spec assembles them for different scenarios&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Full purchase flow&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;cart&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;checkout&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;payment&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; cart&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addProduct&lt;/span&gt;&lt;span&gt;(laptop);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; checkout&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;placeOrder&lt;/span&gt;&lt;span&gt;(address);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; payment&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pay&lt;/span&gt;&lt;span&gt;(card);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Cart total updates correctly&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;cart&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; cart&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addProduct&lt;/span&gt;&lt;span&gt;(laptop);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; cart&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addProduct&lt;/span&gt;&lt;span&gt;(mouse);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; cart&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;verifyTotal&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1225&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Same building blocks, different scenarios. &lt;code dir=&quot;auto&quot;&gt;CartFlow&lt;/code&gt; exists not because you’ll reuse it (though you will), but because “managing the cart” is a real business concept with its own rules and boundaries.&lt;/p&gt;
&lt;p&gt;This distinction matters because it changes &lt;em&gt;how&lt;/em&gt; you design Flows. A DRY-driven Flow is shaped by what’s convenient to reuse. A business-entity Flow is shaped by what the business actually does. The second one is stable. The first one drifts.&lt;/p&gt;
&lt;p&gt;Here’s the precise responsibility of each layer:&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;layer-1-technical-page-objects&quot;&gt;Layer 1: Technical (Page Objects)&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Job:&lt;/strong&gt; Encapsulate raw Playwright interactions. Know about selectors. Know nothing else.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;pom/CartPage.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartPage&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;constructor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; {}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Exposes WHAT can be done, not HOW the business uses it&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;checkoutButton&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Locator&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;checkout-submit&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;clickCheckout&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;checkoutButton&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;What it must NOT do:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// WRONG: POM containing business logic&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;async &lt;/span&gt;&lt;span&gt;proceedToCheckoutAndVerify&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;checkoutButton&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// This is business logic — it doesn&apos;t belong here&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toHaveURL&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/payment&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Why? Because the URL &lt;code dir=&quot;auto&quot;&gt;/payment&lt;/code&gt; is a business rule, not a UI detail. If the business decides to show a modal instead of navigating — your POM shouldn’t need to change.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h3 id=&quot;layer-2-action-flows&quot;&gt;Layer 2: Action (Flows)&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Job:&lt;/strong&gt; Orchestrate business processes using Page Objects. Know about business rules. Know nothing about selectors.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;flows/CheckoutFlow.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CheckoutFlow&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Dependency Injection: receives ready Page Object instances&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;constructor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cartPage&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartPage&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;paymentPage&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PaymentPage&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;completePurchase&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;orderData&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;OrderData&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; test&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;WHEN: User proceeds to checkout&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;cartPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;clickCheckout&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;// Business rule: payment form must appear&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;paymentPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeVisible&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; test&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;WHEN: User fills payment details&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;// Data comes from outside — no hardcoded values in Flows&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;paymentPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fillDetails&lt;/span&gt;&lt;span&gt;(orderData&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;card&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;paymentPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;submit&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;What it must NOT do:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// WRONG: Flow reaching into selectors&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;async &lt;/span&gt;&lt;span&gt;completePurchase&lt;/span&gt;&lt;span&gt;(orderData: OrderData) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// This bypasses the POM entirely — now Flow is coupled to selectors&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;checkout-submit&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Why does this matter? If &lt;code dir=&quot;auto&quot;&gt;checkout-submit&lt;/code&gt; becomes &lt;code dir=&quot;auto&quot;&gt;checkout-btn&lt;/code&gt;, you now have to find and fix this in every Flow that touches it — instead of fixing it once in &lt;code dir=&quot;auto&quot;&gt;CartPage&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h3 id=&quot;layer-3-specification-tests&quot;&gt;Layer 3: Specification (Tests)&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Job:&lt;/strong&gt; Express business intent. Read like a user story. Know nothing about implementation.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;tests/checkout.spec.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;User can complete a purchase&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;checkoutFlow&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BDR&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Given&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;the user has items in their cart&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; checkoutFlow&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addProductToCart&lt;/span&gt;&lt;span&gt;(testProduct);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BDR&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;When&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;the user completes the purchase&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; checkoutFlow&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;completePurchase&lt;/span&gt;&lt;span&gt;(testOrderData);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BDR&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Then&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;the order is confirmed&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; checkoutFlow&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;verifyOrderConfirmation&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;A non-engineer can read this and understand exactly what’s being tested. That’s the goal.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What it must NOT do:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// WRONG: Test reaching into POM directly&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;User can complete a purchase&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Test now knows about selectors — living documentation is broken&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;checkout-submit&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-boundary-violation-cascade&quot;&gt;The boundary violation cascade&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here’s what actually happens when teams blur the boundaries:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Month 1:&lt;/strong&gt; “It’s just one selector in the Flow, it’s fine.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Month 2:&lt;/strong&gt; The selector changes. You fix it in the POM — but the Flow breaks too. Two places to fix instead of one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Month 3:&lt;/strong&gt; A new developer adds business logic to the POM because “that’s where the page stuff is”. Now the POM has assertions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Month 6:&lt;/strong&gt; Every layer knows about every other layer. Changing anything breaks everything. Nobody knows where to look when a test fails.&lt;/p&gt;
&lt;p&gt;The three-layer rule isn’t aesthetic. It’s the thing that keeps your test suite maintainable at scale.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;what-the-report-looks-like-with-proper-layering&quot;&gt;What the report looks like with proper layering&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;With this architecture, your Allure report becomes a business document:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;✓ User can complete a purchase&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;✓ GIVEN: The user has items in their cart&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;📊 Cart Contents: [Laptop Pro x1, $1200]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;✓ WHEN: User proceeds to checkout&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;✓ WHEN: User fills payment details&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;📊 Payment Data: [Card: **** 4242, Amount: $1200]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;✓ THEN: Order is confirmed&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;📊 Order Summary: [ID: #12345, Status: confirmed]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;p&gt;When a test fails:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;✗ User can complete a purchase&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;✓ GIVEN: The user has items in their cart&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;✗ WHEN: User proceeds to checkout&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;📊 Cart State before click: [button status: disabled, reason: stock_unavailable]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;❌ Expected payment form to be visible&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;p&gt;Thirty seconds from opening the report to understanding the failure. No code diving required.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;fixtures-the-dependency-injection-container&quot;&gt;Fixtures: the dependency injection container&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The glue that makes all this work without boilerplate is Playwright’s fixture system:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;fixtures/index.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { test &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; base } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@playwright/test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { CartPage } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;../pom/CartPage&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { PaymentPage } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;../pom/PaymentPage&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { CheckoutFlow } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;../flows/CheckoutFlow&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; Fixtures &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;cartPage&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartPage&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;paymentPage&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PaymentPage&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;checkoutFlow&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CheckoutFlow&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;base&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;extend&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Fixtures&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;cartPage&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartPage&lt;/span&gt;&lt;span&gt;(page))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;paymentPage&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PaymentPage&lt;/span&gt;&lt;span&gt;(page))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Flow receives its Page Objects automatically via DI&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;checkoutFlow&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;cartPage&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;paymentPage&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CheckoutFlow&lt;/span&gt;&lt;span&gt;(cartPage&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;paymentPage))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Your test declares what it needs — Playwright provides it. Fresh instance per test, no shared state, no manual wiring.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;anti-patterns-and-how-to-spot-them&quot;&gt;Anti-patterns and how to spot them&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Anti-pattern 1: The God Test&lt;/strong&gt;
The test does everything: setup, interaction, assertion, cleanup — all with raw Playwright calls. Sign: test file is 100+ lines.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Anti-pattern 2: The Smart POM&lt;/strong&gt;
Page Object contains assertions, navigation logic, or business rules. Sign: &lt;code dir=&quot;auto&quot;&gt;expect()&lt;/code&gt; calls inside a POM method.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Anti-pattern 3: The Leaky Flow&lt;/strong&gt;
Flow accesses &lt;code dir=&quot;auto&quot;&gt;page&lt;/code&gt; directly or imports locators. Sign: &lt;code dir=&quot;auto&quot;&gt;this.page.getBy...&lt;/code&gt; inside a Flow class.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Anti-pattern 4: The Copy-Paste Chain&lt;/strong&gt;
Same setup code (login, navigate, seed data) repeated across test files. Sign: changing one thing requires a grep-and-replace.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-rule-in-one-sentence&quot;&gt;The rule in one sentence&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Each layer talks only to the layer directly below it. Spec → Flow → POM. Never skip a level. Never reach up.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Follow this and your test suite stays maintainable. Violate it and you’ll be rewriting everything in six months.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;try-it&quot;&gt;Try it&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This architecture is implemented in the BDR Playwright template — ready to clone and use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/dmitryAQA/bdr-methodology&quot;&gt;BDR Methodology&lt;/a&gt;&lt;/strong&gt; — full architecture docs and guides&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/dmitryAQA/playwright-bdr-template&quot;&gt;Playwright BDR Template&lt;/a&gt;&lt;/strong&gt; — working implementation&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;I’m open to QA Automation roles — remote, contract, or full-time.&lt;/em&gt;
&lt;em&gt;&lt;a href=&quot;mailto:dmitryAQA@outlook.com&quot;&gt;dmitryAQA@outlook.com&lt;/a&gt; | &lt;a href=&quot;https://t.me/DmitryMeAQA&quot;&gt;@DmitryMeAQA&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title>Nobody reads your test reports. Here&apos;s how I re-engineered them with a 3-layer architecture</title><link>https://bdr-methodology.dev/blog/3-layers-architecture-base/</link><guid isPermaLink="true">https://bdr-methodology.dev/blog/3-layers-architecture-base/</guid><pubDate>Sat, 02 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://bdr-methodology.dev/images/Nobody-reads-your-test-reports.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;div&gt;&lt;h1 id=&quot;nobody-reads-your-test-reports-heres-how-i-re-engineered-them-with-a-3-layer-architecture-&quot;&gt;Nobody reads your test reports. Here’s how I re-engineered them with a 3-layer architecture. &lt;span&gt; CONCEPT &lt;/span&gt;&lt;/h1&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; BDR (Behavior-Driven Living Requirements) is my own architectural approach to organizing Playwright tests — a Cucumber-free alternative to BDD that I designed and documented at &lt;a href=&quot;https://bdr-methodology.dev/concepts/manifesto&quot;&gt;bdr-methodology.dev&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;Monday morning. Coffee. You open GitLab — and CI is red. Classic.&lt;/p&gt;
&lt;p&gt;You open the report. There’s a wall of text, five screens long. Somewhere in there: &lt;code dir=&quot;auto&quot;&gt;TimeoutError&lt;/code&gt; on a click. The selector looks fine — &lt;code dir=&quot;auto&quot;&gt;data-testid=&quot;checkout-submit&quot;&lt;/code&gt;. But why did it fail? Was the database down? Did the frontend not render the button? Did some API return an unexpected response?&lt;/p&gt;
&lt;p&gt;To find out, you have to dive into the test code and debug it line by line. Mentally reconstruct what the app state was. Read through fifty lines of setup just to understand what was being tested.&lt;/p&gt;
&lt;p&gt;This is the real cost of unreadable test reports. Not the failure itself — but the hour you spend just figuring out &lt;em&gt;what&lt;/em&gt; failed and &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-classic-pom-looks-clean-reports-terribly&quot;&gt;The classic POM: looks clean, reports terribly&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Most teams start here. You write a clean Page Object:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { Page } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@playwright/test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartPage&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;constructor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;readonly&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; {}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;clickCheckout&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;checkout-submit&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The code looks great. Clean, atomic, no logic in the wrong place.&lt;/p&gt;
&lt;p&gt;But the report? It looks like this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;✓ Test: User can complete purchase&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- clickCheckout&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- fillDetails&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- submit&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;How do you understand the context from that in five seconds? You can’t. The developer opens the test code, reads through it, swears, mentally reconstructs what was happening. Time gone.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://bdr-methodology.dev/images/allure-non-context.png&quot; alt=&quot;Example of a bad report — raw method names, no context&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;just-use-teststep-everywhere--dont-do-this&quot;&gt;“Just use test.step everywhere” — don’t do this&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Someone will suggest: “Just wrap everything in &lt;code dir=&quot;auto&quot;&gt;test.step&lt;/code&gt;, what’s the problem?”&lt;/p&gt;
&lt;p&gt;Don’t. It works for three tests. At a hundred, it kills the project.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Copy-paste will destroy you.&lt;/strong&gt; The login → cart → checkout chain ends up in most test files. Login logic changes? Congratulations, you’re editing fifty files by hand.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Maintenance becomes a nightmare.&lt;/strong&gt; Checkout now requires a “agree to terms” checkbox? Go insert &lt;code dir=&quot;auto&quot;&gt;await page.click(...)&lt;/code&gt; in a hundred places.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tests lose their meaning.&lt;/strong&gt; A ten-line test balloons to fifty lines of &lt;code dir=&quot;auto&quot;&gt;await test.step(...)&lt;/code&gt; noise. The actual business intent disappears behind the boilerplate.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-fix-a-flow-layer-between-pom-and-tests&quot;&gt;The fix: a Flow layer between POM and tests&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The solution is a layer between “dumb” pages and tests. But here’s the key insight most teams miss: &lt;strong&gt;a Flow is not just a reusable helper. It’s a business entity.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Think of an e-commerce app. You have three distinct business actions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Adding a product to the cart&lt;/strong&gt; — a self-contained business event&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Placing an order&lt;/strong&gt; — another self-contained business event&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Processing payment&lt;/strong&gt; — yet another&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each of these deserves its own Flow class. Not because of DRY (though that’s a nice side effect), but because each one represents a real business concept with its own rules and responsibilities.&lt;/p&gt;
&lt;p&gt;Then your Spec just assembles them like Lego:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Scenario 1: full happy path&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; cart&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addProduct&lt;/span&gt;&lt;span&gt;(laptop);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; checkout&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;placeOrder&lt;/span&gt;&lt;span&gt;(address);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; payment&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pay&lt;/span&gt;&lt;span&gt;(card);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Scenario 2: just verify cart behaviour&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; cart&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addProduct&lt;/span&gt;&lt;span&gt;(laptop);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; cart&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;verifyTotal&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1200&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Same building blocks, different scenarios. The Spec doesn’t care how “add product” works internally — it just uses the business entity.&lt;/p&gt;
&lt;p&gt;This distinction has a real consequence. If the business process for checkout changes from one screen to three, your test remains the same:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; checkoutFlow&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;completePurchase&lt;/span&gt;&lt;span&gt;(orderData);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;You change the implementation inside the Flow, but the test — the business intent — stays untouched. That’s the difference between a brittle script and a resilient test framework.&lt;/p&gt;
&lt;p&gt;A Flow is a conductor — it knows nothing about selectors or clicks. It only knows about the business process.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CheckoutFlow&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;constructor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cartPage&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CartPage&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;paymentPage&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PaymentPage&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;completePurchase&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;orderData&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;OrderData&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; test&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;WHEN: User proceeds to checkout&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;cartPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;clickCheckout&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;paymentPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeVisible&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; test&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;WHEN: User fills payment details&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;paymentPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fillDetails&lt;/span&gt;&lt;span&gt;(orderData&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;card&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;paymentPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;submit&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Now the report looks like this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;✓ Test: User can complete purchase&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;✓ WHEN: User proceeds to checkout&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;✓ WHEN: User fills payment details&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;✓ THEN: Order confirmation is displayed&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://bdr-methodology.dev/images/allure-passed-attachable.png&quot; alt=&quot;Clean report with business-level step names&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Test failed? The developer opens the report. Thirty seconds — and they know exactly which business step broke. No code diving required.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;why-three-layers--and-what-breaks-if-you-skip-one&quot;&gt;Why three layers — and what breaks if you skip one&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This is the part most teams skip. They add a Flow layer but let the boundaries blur. A month later, everything is tangled again.&lt;/p&gt;
&lt;p&gt;Here’s why each layer exists and what happens when you violate it:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;POM knows about selectors. Nothing else.&lt;/strong&gt;
If your POM starts containing business logic — “click checkout AND verify the payment page appeared” — you’ve coupled UI structure to business rules. Change the UI, and your business logic breaks with it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flow knows about business processes. Nothing about selectors.&lt;/strong&gt;
If your Flow starts calling &lt;code dir=&quot;auto&quot;&gt;page.getByTestId(...)&lt;/code&gt; directly, you’ve lost the separation that makes refactoring safe. Now a selector change requires touching both the POM and the Flow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spec knows about intent. Nothing about implementation.&lt;/strong&gt;
Your test should read like a user story. If it’s full of &lt;code dir=&quot;auto&quot;&gt;.fill()&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;.click()&lt;/code&gt; calls, a non-engineer can’t read it — and you’ve lost the “living documentation” value entirely.&lt;/p&gt;
&lt;p&gt;The rule: &lt;strong&gt;each layer talks only to the layer directly below it.&lt;/strong&gt; Spec → Flow → POM. Never skip a level.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;what-the-report-becomes&quot;&gt;What the report becomes&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;With this architecture, your Allure report stops being a log of browser actions and becomes a record of business events.&lt;/p&gt;
&lt;p&gt;When a test fails, the report answers three questions immediately:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What&lt;/strong&gt; was being tested (the test name)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Where&lt;/strong&gt; it broke (the step name)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What the state was&lt;/strong&gt; (attached tables with data)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;That’s the difference between a report that developers ignore and one they actually use.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;try-it&quot;&gt;Try it&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This architecture is the foundation of BDR — Behavior-Driven Living Requirements.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/dmitryAQA/bdr-methodology&quot;&gt;BDR Methodology&lt;/a&gt;&lt;/strong&gt; — full architecture docs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/dmitryAQA/playwright-bdr-template&quot;&gt;Playwright BDR Template&lt;/a&gt;&lt;/strong&gt; — working implementation to clone&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;I’m open to QA Automation roles — remote, contract, or full-time.&lt;/em&gt;
&lt;em&gt;&lt;a href=&quot;mailto:dmitryAQA@outlook.com&quot;&gt;dmitryAQA@outlook.com&lt;/a&gt; | &lt;a href=&quot;https://t.me/DmitryMeAQA&quot;&gt;@DmitryMeAQA&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title>Beyond Cucumber: A Type-Safe 4-Layer BDD Architecture with Playwright</title><link>https://bdr-methodology.dev/blog/beyond-cucumber-pro/</link><guid isPermaLink="true">https://bdr-methodology.dev/blog/beyond-cucumber-pro/</guid><pubDate>Tue, 28 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://bdr-methodology.dev/images/beyond-cucumber-pro.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;div&gt;&lt;h1 id=&quot;beyond-cucumber-a-type-safe-4-layer-bdd-architecture-with-playwright-&quot;&gt;Beyond Cucumber: A Type-Safe 4-Layer BDD Architecture with Playwright &lt;span&gt; PRO IMPLEMENTATION &lt;/span&gt;&lt;/h1&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;If you want the story behind why BDR exists — I wrote about it &lt;a href=&quot;https://bdr-methodology.dev/blog/beyond-cucumber-base&quot;&gt;this Article&lt;/a&gt;. This article is the technical deep dive: architecture, real code, and implementation details.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; BDR (Behavior-Driven Living Requirements) is my own architectural approach to organizing Playwright tests — a Cucumber-free alternative to BDD that I designed and documented at &lt;a href=&quot;https://bdr-methodology.dev/concepts/manifesto&quot;&gt;bdr-methodology.dev&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-problem-with-cucumber-in-one-sentence&quot;&gt;The problem with Cucumber in one sentence&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;You write your scenario in a &lt;code dir=&quot;auto&quot;&gt;.feature&lt;/code&gt; file, then wire it to TypeScript in a step definition file, and your IDE has no idea they’re connected. Rename a method — nothing breaks at compile time. Run your tests — everything breaks at runtime.&lt;/p&gt;
&lt;p&gt;BDR solves this by keeping Given/When/Then &lt;strong&gt;directly in TypeScript&lt;/strong&gt;. Same BDD philosophy, zero translation layer.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-4-layer-architecture&quot;&gt;The 4-Layer Architecture&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;BDR enforces strict separation of concerns across 4 layers. Each layer has one job:&lt;/p&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Layer&lt;/th&gt;&lt;th&gt;Responsibility&lt;/th&gt;&lt;th&gt;Example&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Specification&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Business intent. Reads like a user story.&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;test(&apos;User can log in&apos;)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Scenario&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Given/When/Then steps&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;BDR.When(&apos;User enters credentials&apos;, ...)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Action (Flow)&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Reusable business logic&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;loginFlow.submitCredentials(user)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Technical (POM)&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Raw selectors and Playwright interactions&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;page.getByLabel(&apos;Username&apos;).fill(value)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The rule: &lt;strong&gt;no layer reaches down more than one level.&lt;/strong&gt; Your Specification layer never touches selectors. Your POM layer never knows about business logic.&lt;/p&gt;
&lt;p&gt;This means if you switch from Playwright to Selenium tomorrow — only the Technical layer changes. Business scenarios stay untouched.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-bdr-step-builder&quot;&gt;The BDR Step Builder&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Instead of Gherkin strings wired to step definitions, BDR gives you a fluent API:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;bdr/bdr.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;createStep&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;prefix&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;return async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;[]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pop&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;if &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt; !== &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;throw &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;BDR.&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;prefix&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;: Last argument must be a function&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;stepName&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;prefix&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toUpperCase&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;formatTitle&lt;/span&gt;&lt;span&gt;(name&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;args)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;executionFn&lt;/span&gt;&lt;span&gt; = async &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; =&gt; &lt;/span&gt;&lt;span&gt;(body&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt; &gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt; ? &lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;args)&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;return &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;(stepName&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;executionFn)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;BDR&lt;/span&gt;&lt;span&gt; = {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Given: &lt;/span&gt;&lt;span&gt;createStep&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Given&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;When: &lt;/span&gt;&lt;span&gt;createStep&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;When&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Then: &lt;/span&gt;&lt;span&gt;createStep&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Then&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;And: &lt;/span&gt;&lt;span&gt;createStep&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;And&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Usage in a test:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;User can log in with valid credentials&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;loginPage&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BDR&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Given&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;the user is on the login page&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; loginPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;goto&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BDR&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;When&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;the user enters valid credentials&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; loginPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;testuser&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;password123&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BDR&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Then&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;the user is redirected to the dashboard&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toHaveURL&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/dashboard&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Your IDE fully understands this. &lt;code dir=&quot;auto&quot;&gt;loginPage.login&lt;/code&gt; is a real TypeScript method — rename it and the IDE updates every reference instantly.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;smart-title-interpolation-with-formattitle&quot;&gt;Smart title interpolation with formatTitle&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Step titles support argument interpolation — so your reports are always meaningful:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;bdr/utils.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;formatTitle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;template&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;[]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;let &lt;/span&gt;&lt;span&gt;argIndex&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; template&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;{(&lt;/span&gt;&lt;span&gt;\d&lt;/span&gt;&lt;span&gt;+|&lt;/span&gt;&lt;span&gt;[\w.]&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;)}&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;match&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (key &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&apos;&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; argIndex &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; args&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;(args[argIndex&lt;/span&gt;&lt;span&gt;++&lt;/span&gt;&lt;span&gt;]) &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; match;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;parts&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;split&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;index&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;parseInt&lt;/span&gt;&lt;span&gt;(parts[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;isNaN&lt;/span&gt;&lt;span&gt;(index) &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt; index &lt;/span&gt;&lt;span&gt;&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt; index &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; args&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;let &lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;args[index];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;let &lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;; i &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; parts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt;; i&lt;/span&gt;&lt;span&gt;++&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (value &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt;&lt;span&gt; value &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;value &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; value[parts[i]];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; match;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; value &lt;/span&gt;&lt;span&gt;!==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;undefined&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;(value) &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; match;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; match;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This supports three interpolation modes:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Index-based&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;formatTitle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Login as {0}&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, [&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;admin&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;]);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// → &quot;Login as admin&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Sequential&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;formatTitle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Filter by {} and {}&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, [&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Electronics&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;price&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;]);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// → &quot;Filter by Electronics and price&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Nested property access&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;formatTitle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Welcome {0.user.name}&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, [{ user: { name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;John&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; } }]);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// → &quot;Welcome John&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Your Allure report shows &lt;code dir=&quot;auto&quot;&gt;WHEN: Filter by Electronics and price&lt;/code&gt; — not a generic string, but a meaningful description of what actually happened.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-step-decorator-for-flow-classes&quot;&gt;The @Step Decorator for Flow classes&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;For reusable business flows, BDR provides a &lt;code dir=&quot;auto&quot;&gt;@Step&lt;/code&gt; decorator that wraps class methods automatically:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;bdr/decorators.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;options&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;StepOptions&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;[]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;wrapMethodInStep&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;originalMethod&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;Function&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;return async function &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;methodArgs&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;[]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;stepName&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;formatTitle&lt;/span&gt;&lt;span&gt;(title&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;methodArgs)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;return &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;(stepName&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; async &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; =&gt; &lt;/span&gt;&lt;span&gt;originalMethod&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;apply&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;methodArgs))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// Supports both Legacy and Stage 3 decorators&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt;&lt;span&gt; args[&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;kind&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; args[&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;]) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;wrapMethodInStep&lt;/span&gt;&lt;span&gt;(args[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]); &lt;/span&gt;&lt;span&gt;// Stage 3&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt;&lt;span&gt; args[&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;descriptor&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;args[&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;descriptor&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;wrapMethodInStep&lt;/span&gt;&lt;span&gt;(descriptor&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; descriptor; &lt;/span&gt;&lt;span&gt;// Legacy&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Usage in a Flow class:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;flows/ProductFlow.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ProductFlow&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;constructor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;products&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Product&lt;/span&gt;&lt;span&gt;[]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;@Step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;GIVEN: I have a product catalog with {0} items&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;logProducts&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;count&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;attachTable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Source Product Catalog&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;products&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;@Step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;WHEN: I filter products by category &quot;{0}&quot;&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;filterByCategory&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;category&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;filtered&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;products&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;filter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; &lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;category&lt;/span&gt;&lt;span&gt; === &lt;/span&gt;&lt;span&gt;category);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;attachTable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Filtered Products: &lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;category&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;, filtered);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; filtered;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;@Step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;THEN: The total price should be calculated&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;calculateTotalPrice&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;total&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;products&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;reduce&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sum&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; &lt;/span&gt;&lt;span&gt;sum&lt;/span&gt;&lt;span&gt; + &lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;price&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;attachTable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Price Summary&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Total Items&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;products&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Total Price&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;total&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toFixed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; total;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Every public method is automatically wrapped in a named &lt;code dir=&quot;auto&quot;&gt;test.step&lt;/code&gt;. The report shows exactly which business action was running when something failed.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;fixtures--the-glue-of-the-architecture&quot;&gt;Fixtures — the glue of the architecture&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Fixtures inject Page Objects and Flows into tests automatically. No manual instantiation, no shared state between tests:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;fixtures/index.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { test &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; base } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@playwright/test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { LoginPage } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;../pom/LoginPage&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { ProductsPage } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;../pom/ProductsPage&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; MyFixtures &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;loginPage&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LoginPage&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;productsPage&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ProductsPage&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;base&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;extend&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;MyFixtures&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;loginPage&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LoginPage&lt;/span&gt;&lt;span&gt;(page))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;productsPage&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ProductsPage&lt;/span&gt;&lt;span&gt;(page))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; { expect } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@playwright/test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Each test gets a fresh instance. No state leaking between tests. And because it’s TypeScript — if you remove a fixture, every test that depends on it fails at compile time, not at runtime.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rich-diagnostics-with-attachtable&quot;&gt;Rich diagnostics with attachTable&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This is where BDR goes beyond standard Playwright reporting. &lt;code dir=&quot;auto&quot;&gt;attachTable&lt;/code&gt; generates a styled HTML table and attaches it directly to the Allure report step:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;bdr/tables.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;attachTable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;[]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;data &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; data&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;generateHtmlTable&lt;/span&gt;&lt;span&gt;(data);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; test&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;info&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;attach&lt;/span&gt;&lt;span&gt;(name&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;body: Buffer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt;(html)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;contentType: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;text/html&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;generateHtmlTable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;[]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;headers&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;Object&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;keys&lt;/span&gt;&lt;span&gt;(data[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;ths&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;headers&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&amp;#x3C;th&gt;&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&amp;#x3C;/th&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;trs&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;row&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;tds&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;headers&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;val&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;row[h];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&amp;#x3C;td&gt;&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;val&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;undefined&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;val&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;val&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&amp;#x3C;/td&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&amp;#x3C;tr&gt;&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;tds&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&amp;#x3C;/tr&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;html&gt;&amp;#x3C;head&gt;&amp;#x3C;style&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;table { border-collapse: collapse; width: 100%; box-shadow: 0 2px 15px rgba(0,0,0,0.1); }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;th { background-color: #2c3e50; color: #fff; padding: 12px 15px; text-transform: uppercase; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;td { padding: 12px 15px; border-bottom: 1px solid #ddd; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;tr:nth-child(even) { background-color: #f8f9fa; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;tr:hover { background-color: #f1f4f6; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/style&gt;&amp;#x3C;/head&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;body&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;table&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;thead&gt;&amp;#x3C;tr&gt;&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;ths&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&amp;#x3C;/tr&gt;&amp;#x3C;/thead&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;tbody&gt;&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;trs&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&amp;#x3C;/tbody&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/table&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/body&gt;&amp;#x3C;/html&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Here’s what this looks like in the report:&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://bdr-methodology.dev/images/allure-passed-attachable.png&quot; alt=&quot;Allure report — test step with attachTable showing a styled HTML table inside the step&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;attachcomparetable--expected-vs-actual&quot;&gt;attachCompareTable — Expected vs Actual&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This is the diagnostic killer feature. When a test fails on a data mismatch, &lt;code dir=&quot;auto&quot;&gt;attachCompareTable&lt;/code&gt; shows you exactly which fields don’t match:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;attachCompareTable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;expected&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;actual&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;allKeys&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;Array&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Set&lt;/span&gt;&lt;span&gt;([&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;Object&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;keys&lt;/span&gt;&lt;span&gt;(expected)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;Object&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;keys&lt;/span&gt;&lt;span&gt;(actual)]));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;comparisonData&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;allKeys&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;exp&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;expected[key]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;act&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;actual[key]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;isMatch&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(exp)&lt;/span&gt;&lt;span&gt; === &lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(act)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;return {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Field: &lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Expected: &lt;/span&gt;&lt;span&gt;exp&lt;/span&gt;&lt;span&gt; === &lt;/span&gt;&lt;span&gt;undefined&lt;/span&gt;&lt;span&gt; ? &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;&amp;#x3C;undefined&gt;&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(exp)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Actual: &lt;/span&gt;&lt;span&gt;act&lt;/span&gt;&lt;span&gt; === &lt;/span&gt;&lt;span&gt;undefined&lt;/span&gt;&lt;span&gt; ? &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;&amp;#x3C;undefined&gt;&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(act)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Result: &lt;/span&gt;&lt;span&gt;isMatch&lt;/span&gt;&lt;span&gt; ? &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;✅ MATCH&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;❌ MISMATCH&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;attachTable&lt;/span&gt;&lt;span&gt;(name&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; comparisonData);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Instead of:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;AssertionError: expected { role: &apos;admin&apos; } to equal { role: &apos;user&apos; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;You get a table in the report:&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Field&lt;/th&gt;&lt;th&gt;Expected&lt;/th&gt;&lt;th&gt;Actual&lt;/th&gt;&lt;th&gt;Result&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;id&lt;/td&gt;&lt;td&gt;”123&quot;&lt;/td&gt;&lt;td&gt;&quot;123”&lt;/td&gt;&lt;td&gt;MATCH&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;email&lt;/td&gt;&lt;td&gt;”&lt;a href=&quot;mailto:john@example.com&quot;&gt;john@example.com&lt;/a&gt;&quot;&lt;/td&gt;&lt;td&gt;&quot;&lt;a href=&quot;mailto:john@example.com&quot;&gt;john@example.com&lt;/a&gt;”&lt;/td&gt;&lt;td&gt;MATCH&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;role&lt;/td&gt;&lt;td&gt;”user&quot;&lt;/td&gt;&lt;td&gt;&quot;admin”&lt;/td&gt;&lt;td&gt;MISMATCH&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://bdr-methodology.dev/images/allure-failed-attachable.png&quot; alt=&quot;Allure report — attachCompareTable showing Expected vs Actual with MATCH/MISMATCH status per field&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;a-complete-hybrid-scenario-api-setup--ui-verification&quot;&gt;A complete hybrid scenario: API setup + UI verification&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here’s a real-world scenario that uses all the layers together:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;User created via API can log in through UI&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;loginPage&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;newUser&lt;/span&gt;&lt;span&gt; = {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;email: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;john.doe@example.com&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;password: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;SecurePass123&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;role: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;customer&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BDR&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Given&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;a user exists in the system&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;attachTable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;New User Payload&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, [newUser]);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/users&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { data: &lt;/span&gt;&lt;span&gt;newUser&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(response&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;201&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;created&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;attachTable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Created User Response&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, [created]);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BDR&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;When&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;the user logs in through the UI&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; loginPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;goto&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; loginPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt;(newUser&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt;, newUser&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;password&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BDR&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Then&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;the user sees their dashboard&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toHaveURL&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/dashboard&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;p&gt;When this test fails, your report shows: the exact payload sent to the API, the response received, and a screenshot at the moment of failure. No reproduction needed.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;cucumber-vs-bdr--the-technical-comparison&quot;&gt;Cucumber vs BDR — the technical comparison&lt;/h2&gt;&lt;/div&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Cucumber + Gherkin&lt;/th&gt;&lt;th&gt;BDR&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Where scenarios live&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Separate &lt;code dir=&quot;auto&quot;&gt;.feature&lt;/code&gt; files&lt;/td&gt;&lt;td&gt;Directly in TypeScript&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;IDE support&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Steps are strings — no autocomplete&lt;/td&gt;&lt;td&gt;Full TypeScript — autocomplete, go-to-definition&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Compile-time safety&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;None — errors at runtime&lt;/td&gt;&lt;td&gt;Full — broken references caught immediately&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Renaming a method&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Hunt across &lt;code dir=&quot;auto&quot;&gt;.feature&lt;/code&gt; files manually&lt;/td&gt;&lt;td&gt;IDE updates every reference instantly&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Report richness&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Basic pass/fail + step names&lt;/td&gt;&lt;td&gt;Steps + styled HTML tables + screenshots + API logs&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Decorator support&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;N/A&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;@Step&lt;/code&gt; with title interpolation and nested property access&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Maintenance cost&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Two places to update&lt;/td&gt;&lt;td&gt;One place&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;try-it&quot;&gt;Try it&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/dmitryAQA/bdr-methodology&quot;&gt;BDR Methodology&lt;/a&gt;&lt;/strong&gt; — full architecture docs, guides, and manifesto&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/dmitryAQA/playwright-bdr-template&quot;&gt;Playwright BDR Template&lt;/a&gt;&lt;/strong&gt; — working implementation, clone and run&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;I’m open to QA Automation roles — remote, contract, or full-time. If you’re building a team and care about test architecture, reach out.&lt;/em&gt;
&lt;a href=&quot;mailto:_dmitryAQA@outlook.com&quot;&gt;_dmitryAQA@outlook.com&lt;/a&gt; | &lt;a href=&quot;https://t.me/DmitryMeAQA&quot;&gt;@DmitryMeAQA&lt;/a&gt;_&lt;/p&gt;</content:encoded></item><item><title>Your test failed. But why? — How I built BDR to actually answer that question</title><link>https://bdr-methodology.dev/blog/beyond-cucumber-base/</link><guid isPermaLink="true">https://bdr-methodology.dev/blog/beyond-cucumber-base/</guid><pubDate>Mon, 27 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://bdr-methodology.dev/images/Your-test-failed.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;div&gt;&lt;h1 id=&quot;your-test-failed-but-why--how-i-built-bdr-to-actually-answer-that-question-&quot;&gt;Your test failed. But why? — How I built BDR to actually answer that question &lt;span&gt; CONCEPT &lt;/span&gt;&lt;/h1&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; BDR (Behavior-Driven Living Requirements) is my own architectural approach to organizing Playwright tests — a Cucumber-free alternative to BDD that I designed and documented at &lt;a href=&quot;https://bdr-methodology.dev/concepts/manifesto&quot;&gt;bdr-methodology.dev&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;A developer once left a comment on one of my articles about test automation. He described something painfully familiar:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“You can see the button is disabled, so the click doesn’t work. But now the question is — why? And where is the developer supposed to find the answer? You try to reproduce it manually… and suddenly it works fine. So what happened? Nobody knows. You need logs. You need video. You need something.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;He was right. And that comment stuck with me.&lt;/p&gt;
&lt;p&gt;Because that’s not a rare edge case. That’s Tuesday in QA.&lt;/p&gt;
&lt;p&gt;Test fails in CI. You open the report. You see: &lt;code dir=&quot;auto&quot;&gt;Error: element not clickable&lt;/code&gt;. That’s it. No context. No screenshot at the right moment. No API logs. No idea what the app state was. You spend an hour trying to reproduce it locally — and it doesn’t reproduce. The ticket gets closed as “flaky”. The bug stays in production.&lt;/p&gt;
&lt;p&gt;This is the real problem with most test automation: &lt;strong&gt;tests tell you that something broke, but not why.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Of course, you can enable Playwright Trace Viewer, videos, and screenshots. It’s the standard advice.
But here’s the reality:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Trace Viewer is a firehose of data. If you have 300 tests running in parallel, opening a 50MB trace file for every single flaky test is a full-time job. It shows you what happened, but it doesn’t tell you why the business logic failed.&lt;/li&gt;
&lt;li&gt;Videos are useless for high-speed flaky bugs. You spend minutes watching a 30-second video at 0.5x speed, trying to catch that one flicker of an error message.&lt;/li&gt;
&lt;li&gt;The core problem remains: These tools tell you how it failed, but they don’t explain what the application state was from a business perspective.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My goal with BDR wasn’t just to see the crash — it was to make the crash self-explanatory.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;i-looked-at-bdd-then-i-looked-at-cucumber-then-i-had-a-problem&quot;&gt;I looked at BDD. Then I looked at Cucumber. Then I had a problem.&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;BDD made sense to me. Given/When/Then is a great way to write tests that humans can actually read. Business-readable scenarios. Living documentation. Tests that explain intent, not just implementation.&lt;/p&gt;
&lt;p&gt;The promise of BDD is powerful:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Business sees exactly what the product does — in plain language&lt;/li&gt;
&lt;li&gt;Engineers write tests that serve as living requirements&lt;/li&gt;
&lt;li&gt;When a test fails, it’s a signal that a business requirement is broken&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So I looked at Cucumber. And I saw the idea was right — but the implementation was painful.&lt;/p&gt;
&lt;p&gt;Here’s what you actually get with Cucumber in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;.feature&lt;/code&gt; files that live separately from your code&lt;/li&gt;
&lt;li&gt;Step definitions that need to be wired up manually&lt;/li&gt;
&lt;li&gt;A developer renames a button → you spend an afternoon hunting which &lt;code dir=&quot;auto&quot;&gt;.feature&lt;/code&gt; file broke&lt;/li&gt;
&lt;li&gt;A test fails → you read the Gherkin, then find the step definition, then find the actual code, then maybe understand what happened&lt;/li&gt;
&lt;li&gt;Every new scenario requires writing in two places: the &lt;code dir=&quot;auto&quot;&gt;.feature&lt;/code&gt; file AND the TypeScript&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You’re not writing tests anymore. You’re maintaining a translation layer between English and code. That’s the &lt;strong&gt;Gherkin tax&lt;/strong&gt; — and it compounds as your suite grows.&lt;/p&gt;
&lt;p&gt;And here’s the painful irony: &lt;strong&gt;business still doesn’t read those &lt;code dir=&quot;auto&quot;&gt;.feature&lt;/code&gt; files.&lt;/strong&gt; They’re buried in a repository nobody outside engineering opens. You paid the Gherkin tax and got nothing for it.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;cucumber-vs-bdr--side-by-side&quot;&gt;Cucumber vs BDR — side by side&lt;/h2&gt;&lt;/div&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Cucumber + Gherkin&lt;/th&gt;&lt;th&gt;BDR&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Where scenarios live&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Separate &lt;code dir=&quot;auto&quot;&gt;.feature&lt;/code&gt; files&lt;/td&gt;&lt;td&gt;Directly in code&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;IDE support&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Limited — steps are strings&lt;/td&gt;&lt;td&gt;Full — TypeScript, autocomplete, refactoring&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Renaming a method&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Hunt across &lt;code dir=&quot;auto&quot;&gt;.feature&lt;/code&gt; files&lt;/td&gt;&lt;td&gt;IDE updates everything instantly&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Error caught&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;At runtime&lt;/td&gt;&lt;td&gt;At compile time&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Report richness&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Basic pass/fail + steps&lt;/td&gt;&lt;td&gt;Steps + tables + screenshots + API logs&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Business reads it?&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Rarely (it’s in a repo)&lt;/td&gt;&lt;td&gt;Yes — via Allure report, no repo access needed&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Maintenance cost&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;High — two places to update&lt;/td&gt;&lt;td&gt;Low — one place&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;what-if-givenwhenthen-lived-directly-in-code&quot;&gt;What if Given/When/Then lived directly in code?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;That’s the question that led me to build &lt;strong&gt;BDR — Behavior-Driven Living Requirements&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;BDR is not a framework. It’s a methodology. The core idea is simple:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep everything that’s good about BDD. Remove the part that slows you down.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Given/When/Then structure — kept&lt;/li&gt;
&lt;li&gt;Business-readable scenarios — kept&lt;/li&gt;
&lt;li&gt;Living documentation — kept, and made richer&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;.feature&lt;/code&gt; files — gone&lt;/li&gt;
&lt;li&gt;Step definition wiring — gone&lt;/li&gt;
&lt;li&gt;Gherkin maintenance — gone&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The result: &lt;strong&gt;a happy engineer makes a transparent product for the business.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;the-4-layer-architecture&quot;&gt;The 4-Layer Architecture&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;BDR separates concerns into 4 layers. Each layer has one job and doesn’t bleed into others:&lt;/p&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Layer&lt;/th&gt;&lt;th&gt;What it does&lt;/th&gt;&lt;th&gt;Example&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Specification&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Business intent. Reads like a user story.&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;test(&apos;User can log in with valid credentials&apos;)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Scenario&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Given/When/Then steps&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;test.step(&apos;When user enters credentials&apos;)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Action&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Business logic. Reusable flows.&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;loginPage.login(username, password)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Technical&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Raw selectors and Playwright interactions&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;page.getByLabel(&apos;Username&apos;).fill(value)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;This separation means: if you switch from Playwright to Selenium tomorrow, only the Technical layer changes. Your business scenarios stay untouched.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;what-it-looks-like-in-practice&quot;&gt;What it looks like in practice&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;technical-layer--page-objects-with-robust-locators&quot;&gt;Technical Layer — Page Objects with robust locators&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;pages/LoginPage.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { Page, Locator } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@playwright/test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LoginPage&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;constructor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; {}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;usernameInput&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Locator&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByLabel&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Username&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;passwordInput&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Locator&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByLabel&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Password&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;loginButton&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Locator&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Log In&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;goto&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;goto&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/login&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;username&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;password&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;usernameInput&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;(username);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;passwordInput&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;(password);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;loginButton&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;No magic strings. No CSS selectors that break on every UI change. Full IDE support.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;how-fixtures-wire-everything-together&quot;&gt;How fixtures wire everything together&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;This is the glue of the whole architecture. Fixtures inject Page Objects into your tests automatically — no manual instantiation, no boilerplate:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;baseFixtures.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { test &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; base } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@playwright/test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { LoginPage } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;./pages/LoginPage&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { ProductsPage } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;./pages/ProductsPage&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; MyFixtures &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;loginPage&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LoginPage&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;productsPage&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ProductsPage&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;base&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;extend&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;MyFixtures&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;loginPage&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LoginPage&lt;/span&gt;&lt;span&gt;(page))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;productsPage&lt;/span&gt;&lt;span&gt;: async &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;await &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ProductsPage&lt;/span&gt;&lt;span&gt;(page))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; { expect } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@playwright/test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Now every test gets a fresh, properly initialized Page Object — just by declaring it as an argument.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;specification-layer--givenwhenthen-in-code&quot;&gt;Specification Layer — Given/When/Then in code&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;tests/ui/login.spec.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { test, expect } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;../baseFixtures&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;User can log in with valid credentials&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;loginPage&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; test&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Given the user is on the login page&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; loginPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;goto&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; test&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;When the user enters valid credentials&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; loginPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;testuser&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;password123&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; test&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Then the user should be redirected to the dashboard&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toHaveURL&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/dashboard&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This reads exactly like a BDD scenario. But it’s real TypeScript. Your IDE catches errors at compile time, not when CI runs at 2am.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;rich-reporting-with-attachtable&quot;&gt;Rich reporting with attachTable&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This is where BDR goes beyond what Gherkin can do. Every step can carry structured data — tables, payloads, state snapshots — directly in the report.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;tests/ui/products.spec.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { test, expect } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;../baseFixtures&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { attachTable } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@bdr/core&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Product search filters correctly&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;productsPage&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; test&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Given products are available&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;attachTable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Available Products&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;ID&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Name&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Category&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Price&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;101&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Laptop Pro&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Electronics&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;1200&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;102&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Mouse X&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Electronics&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;25&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; productsPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;goto&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; test&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;When the user filters by &quot;Electronics&quot;&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; productsPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;filterByCategory&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Electronics&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; test&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Then only Electronics products are displayed&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;displayed&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;productsPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getDisplayedProductNames&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(displayed)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toEqual&lt;/span&gt;&lt;span&gt;([&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Laptop Pro&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Mouse X&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;]);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;attachTable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Filtered Results&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Name&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Category&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Laptop Pro&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Electronics&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Mouse X&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Electronics&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Here’s what this looks like in the Allure report:&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://bdr-methodology.dev/images/allure-passed-attachable.png&quot; alt=&quot;Allure report showing a passed test.&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Business opens this report and sees exactly what happened — without touching the codebase. That’s living documentation.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;diagnostics-before-and-after&quot;&gt;Diagnostics: before and after&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Remember the developer’s comment from the beginning? Here’s what debugging looks like with and without BDR.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Without BDR:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Error: Timeout 30000ms exceeded&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That’s it. Good luck.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;With BDR:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The report shows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The scenario stopped at step: &lt;code dir=&quot;auto&quot;&gt;&quot;When: user submits the login form&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Attached table: &lt;strong&gt;Form state before click&lt;/strong&gt; — username filled, password filled, button status: &lt;code dir=&quot;auto&quot;&gt;disabled&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Attached: &lt;strong&gt;API request log&lt;/strong&gt; — &lt;code dir=&quot;auto&quot;&gt;POST /auth&lt;/code&gt; returned &lt;code dir=&quot;auto&quot;&gt;403 Forbidden&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Screenshot: captured automatically at the moment of failure&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://bdr-methodology.dev/images/allure-failed-attachable.png&quot; alt=&quot;Allure report showing a failed test with a detailed comparison table.&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Now you know exactly what happened. No reproduction needed. The report IS the reproduction.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;api-testing-with-full-payload-visibility&quot;&gt;API testing with full payload visibility&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;tests/api/users.spec.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { test, expect } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@playwright/test&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { attachTable } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;@bdr/core&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Create a new user via API&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;newUser&lt;/span&gt;&lt;span&gt; = {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;firstName: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;John&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;lastName: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Doe&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;email: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;john.doe@example.com&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;role: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;customer&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; test&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;When a POST request is sent to /users&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;attachTable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Request Payload&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, Object&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;entries&lt;/span&gt;&lt;span&gt;(newUser));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/users&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { data: &lt;/span&gt;&lt;span&gt;newUser&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(response&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;201&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; test&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;step&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Then the user is created successfully&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;verify&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;/users?email=&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;newUser&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;users&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;verify&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;created&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;users&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;find&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; &lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt; === &lt;/span&gt;&lt;span&gt;newUser&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(created)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toMatchObject&lt;/span&gt;&lt;span&gt;({ email: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;john.doe@example.com&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;attachTable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Response&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Object&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;entries&lt;/span&gt;&lt;span&gt;(created)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;filter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;k&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;includes&lt;/span&gt;&lt;span&gt;(k)),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;p&gt;Every request payload, every response — attached to the report. When something breaks in CI, you open the report and see exactly what was sent and what came back.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;what-bdr-actually-gives-you&quot;&gt;What BDR actually gives you&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;For engineers:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full IDE support — autocomplete, compile-time errors, instant refactoring&lt;/li&gt;
&lt;li&gt;One place to update when things change&lt;/li&gt;
&lt;li&gt;Reports that answer “why?” without manual reproduction&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;For business:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Allure reports readable without engineering knowledge&lt;/li&gt;
&lt;li&gt;Living documentation that’s always current — if the test runs, the doc is up to date&lt;/li&gt;
&lt;li&gt;Clear signal when a business requirement is broken&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; a happy engineer makes a transparent product for the business.&lt;/p&gt;
&lt;hr&gt;
&lt;div&gt;&lt;h2 id=&quot;try-it&quot;&gt;Try it&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/dmitryAQA/bdr-methodology&quot;&gt;BDR Methodology&lt;/a&gt;&lt;/strong&gt; — the full philosophy, 4-layer architecture, and guides&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/dmitryAQA/playwright-bdr-template&quot;&gt;Playwright BDR Template&lt;/a&gt;&lt;/strong&gt; — working implementation you can clone today&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;I’m open to QA Automation roles — remote, contract, or full-time. If you’re building a team and care about test architecture, I’d love to talk.&lt;/em&gt;
&lt;a href=&quot;mailto:_dmitryAQA@outlook.com&quot;&gt;_dmitryAQA@outlook.com&lt;/a&gt; | &lt;a href=&quot;https://t.me/DmitryMeAQA&quot;&gt;@DmitryMeAQA&lt;/a&gt;_&lt;/p&gt;</content:encoded></item></channel></rss>