<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: kadir</title>
    <description>The latest articles on DEV Community by kadir (@aktibaba).</description>
    <link>https://dev.to/aktibaba</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F874003%2F9fca7a23-ae78-4520-b137-995b26b38252.jpeg</url>
      <title>DEV Community: kadir</title>
      <link>https://dev.to/aktibaba</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aktibaba"/>
    <language>en</language>
    <item>
      <title>Capstone: A 100-Test Suite, End to End (Playwright + TypeScript, Ch.26)</title>
      <dc:creator>kadir</dc:creator>
      <pubDate>Mon, 08 Jun 2026 23:19:38 +0000</pubDate>
      <link>https://dev.to/aktibaba/capstone-a-100-test-suite-end-to-end-playwright-typescript-ch26-5b9n</link>
      <guid>https://dev.to/aktibaba/capstone-a-100-test-suite-end-to-end-playwright-typescript-ch26-5b9n</guid>
      <description>&lt;p&gt;This is where everything comes together. If you started this series never having&lt;br&gt;
written an automated test, look at what you can now read and build: a layered Playwright&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript framework — fixtures, Page Objects, API + UI + integration tests, CI,
reporting — running against a real dockerized app. The capstone makes it whole.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;── Run summary ───────────────────────────────
  result:   passed
  tests:    100  (✓ 100  ✘ 0  ⤿ flaky 0  – skipped 0)
  projects: setup 1  api 66  ui 33
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;Code for this chapter is tagged &lt;code&gt;ch-26&lt;/code&gt; in the repo:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;https://github.com/aktibaba/playwright-qa-course&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  End-to-end journeys
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;end-to-end (E2E)&lt;/strong&gt; test exercises the whole product the way a real user would,&lt;br&gt;
start to finish — and a &lt;strong&gt;regression&lt;/strong&gt; suite is the set of tests you run to make sure&lt;br&gt;
nothing that used to work has broken. The capstone's headline tests are exactly that,&lt;br&gt;
and each one creates a &lt;strong&gt;fresh user&lt;/strong&gt; so it's completely isolated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a new user signs up, publishes an article, and sees it on their profile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;signUpPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;articleEditorPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;articlePage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;uniqueId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;author&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/-/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;signUpPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signUp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@test.io`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Password123!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Capstone article &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;articleEditorPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publishArticle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;capstone&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;articlePage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expectTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/#/profile/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;heading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sign up → write an article → view it → see it on the profile, all through the UI,&lt;br&gt;
reusing every Page Object and fixture we built. Notice how a journey this rich is just a&lt;br&gt;
dozen readable lines — that's the payoff of the architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 100 tests cover
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API&lt;/strong&gt; (66 tests): articles CRUD, comments, favorites, follows, profiles, tags,
pagination, the personalised feed, auth &amp;amp; sessions, validation, and authorization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI&lt;/strong&gt; (33 tests): locators &amp;amp; assertions, login/signup/logout, the article editor,
comments, settings, profile &amp;amp; feed, tag filtering, network mocking, visual regression,
accessibility, and the end-to-end journeys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-cutting:&lt;/strong&gt; seed-via-API/verify-in-UI, &lt;code&gt;storageState&lt;/code&gt; login, sharded CI, a
custom reporter, and unique-data isolation throughout.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bugs the suite found
&lt;/h2&gt;

&lt;p&gt;Run honestly and at scale, the suite did what good tests do — it found &lt;strong&gt;seven real bugs&lt;br&gt;
in the application&lt;/strong&gt;, all fixed in &lt;code&gt;sut/&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;createArticle&lt;/code&gt; crashed when &lt;code&gt;tagList&lt;/code&gt; was omitted.&lt;/li&gt;
&lt;li&gt;A null-author race (an un-awaited &lt;code&gt;setAuthor&lt;/code&gt;) crashed &lt;code&gt;GET /articles&lt;/code&gt; under load.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;slug&lt;/code&gt; wasn't unique → duplicate slugs → favorites colliding on a primary key.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;offset&lt;/code&gt; pagination broke the RealWorld contract (&lt;code&gt;offset * limit&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;WCAG-AA &lt;strong&gt;colour-contrast&lt;/strong&gt; failures across the UI.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;updateUser&lt;/code&gt; returned a 500 on every profile update (an &lt;code&gt;||&lt;/code&gt; that's always true) and
risked overwriting passwords.&lt;/li&gt;
&lt;li&gt;An invalid token returned &lt;strong&gt;500&lt;/strong&gt; instead of &lt;strong&gt;401&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the real return on a framework: not just "do the tests pass," but a suite&lt;br&gt;
trustworthy enough that when it goes red, you &lt;em&gt;believe&lt;/em&gt; it — and it catches things the&lt;br&gt;
UI alone never would.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to take it next
&lt;/h2&gt;

&lt;p&gt;You don't need any of these, but each is an afternoon, not a rewrite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;More browsers/devices&lt;/strong&gt; — add WebKit and Firefox projects, and a mobile viewport.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual coverage in CI&lt;/strong&gt; — generate Linux baselines in the Playwright Docker image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data &amp;amp; trends&lt;/strong&gt; — ship &lt;code&gt;json&lt;/code&gt;/&lt;code&gt;blob&lt;/code&gt; results to a dashboard; track flaky rate over
time (Chapter 25).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contract testing&lt;/strong&gt; — check the API against the published RealWorld OpenAPI spec.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance budgets&lt;/strong&gt; — fail a test when a key request gets too slow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They're afternoons because the architecture from Part 2 was built to extend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank you
&lt;/h2&gt;

&lt;p&gt;That's the course — from "why a framework?" to a production-grade, 100-test, API + UI&lt;br&gt;
suite that runs in CI and even improved the app it tests. Clone the&lt;br&gt;
&lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;repo&lt;/a&gt;, check out any &lt;code&gt;ch-NN&lt;/code&gt; tag, and&lt;br&gt;
make it your own.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If this series helped you, star the repo and tell me what you built with it. Happy&lt;br&gt;
testing. 🎭&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>playwright</category>
      <category>typescript</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Reporting: Custom Reporters &amp; Result Visibility (Playwright + TypeScript, Ch.25)</title>
      <dc:creator>kadir</dc:creator>
      <pubDate>Mon, 08 Jun 2026 22:54:44 +0000</pubDate>
      <link>https://dev.to/aktibaba/reporting-custom-reporters-result-visibility-playwright-typescript-ch25-2c4f</link>
      <guid>https://dev.to/aktibaba/reporting-custom-reporters-result-visibility-playwright-typescript-ch25-2c4f</guid>
      <description>&lt;p&gt;Chapter 6 made individual &lt;em&gt;failures&lt;/em&gt; legible (traces, the HTML report). This chapter is&lt;br&gt;
about &lt;strong&gt;results as a whole&lt;/strong&gt; — the signal a team reads every day: what passed, what's&lt;br&gt;
flaky, what's slow — and getting it in front of people without anyone opening a report.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code for this chapter is tagged &lt;code&gt;ch-25&lt;/code&gt; in the repo:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;https://github.com/aktibaba/playwright-qa-course&lt;/a&gt;&lt;/strong&gt; — see&lt;br&gt;
&lt;code&gt;reporters/summary-reporter.ts&lt;/code&gt; and the &lt;code&gt;reporter&lt;/code&gt; array in &lt;code&gt;playwright.config.ts&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  The built-ins, recapped
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;reporter&lt;/strong&gt; turns the stream of test events into output. We already stack three&lt;br&gt;
(Chapter 20): &lt;code&gt;list&lt;/code&gt; (terminal), &lt;code&gt;html&lt;/code&gt; (rich, browsable), and &lt;code&gt;junit&lt;/code&gt; (XML for CI).&lt;br&gt;
Others ship in the box: &lt;code&gt;dot&lt;/code&gt; (very compact for huge suites), &lt;code&gt;json&lt;/code&gt;&lt;br&gt;
(machine-readable), &lt;code&gt;github&lt;/code&gt; (annotations right on a pull request), and &lt;code&gt;blob&lt;/code&gt;&lt;br&gt;
(mergeable across shards). You can run any combination at once.&lt;/p&gt;
&lt;h2&gt;
  
  
  Write a custom reporter
&lt;/h2&gt;

&lt;p&gt;When the built-ins don't say &lt;em&gt;exactly&lt;/em&gt; what you want, you write your own by&lt;br&gt;
implementing Playwright's &lt;strong&gt;&lt;code&gt;Reporter&lt;/code&gt; interface&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An &lt;em&gt;interface&lt;/em&gt; here is a set of methods Playwright will call at certain moments. You&lt;br&gt;
only implement the ones you care about. The key &lt;strong&gt;lifecycle hooks&lt;/strong&gt;:&lt;br&gt;
&lt;code&gt;onBegin&lt;/code&gt; (the run starts), &lt;code&gt;onTestEnd&lt;/code&gt; (one test finished — called once per test),&lt;br&gt;
and &lt;code&gt;onEnd&lt;/code&gt; (the whole run finished).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's a reporter that collects every result and prints an end-of-run summary —&lt;br&gt;
totals, a flaky count, and the slowest tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// reporters/summary-reporter.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Reporter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TestResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;FullResult&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@playwright/test/reporter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SummaryReporter&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;Reporter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TestResult&lt;/span&gt; &lt;span class="p"&gt;}[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="nf"&gt;onTestEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TestResult&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;        &lt;span class="c1"&gt;// remember each finished test&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;onEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FullResult&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                   &lt;span class="c1"&gt;// at the very end, summarise&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TestResult&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// "flaky" = passed, but only after a retry&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flaky&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;passed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retry&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`\n  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; — ✓ &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;passed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;  ✘ &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;  ⤿ flaky &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;flaky&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// …plus slowest tests and a per-project breakdown&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register it alongside the built-ins (the string is just the path to the file):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// playwright.config.ts&lt;/span&gt;
&lt;span class="nx"&gt;reporter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;open&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;never&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;junit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;outputFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test-results/junit.xml&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./reporters/summary-reporter.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every run ends with a glanceable summary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;── Run summary ───────────────────────────────
  result:   passed
  tests:    100  (✓ 100  ✘ 0  ⤿ flaky 0  – skipped 0)
  projects: setup 1  api 66  ui 33
  slowest:
    741ms  home page has no serious accessibility violations
    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;flaky&lt;/strong&gt; number is the one to watch over time — a test that passes only on a retry&lt;br&gt;
is a bug waiting to turn the pipeline red (Chapter 19).&lt;/p&gt;

&lt;h2&gt;
  
  
  Put results where people look
&lt;/h2&gt;

&lt;p&gt;A report nobody opens isn't reporting. Two cheap, high-value channels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The CI run page.&lt;/strong&gt; GitHub Actions exposes a special file via the
&lt;code&gt;GITHUB_STEP_SUMMARY&lt;/code&gt; environment variable — append Markdown to it and it renders
right on the run's summary page. Our reporter writes a pass/fail table there when that
variable is present, so results show up without downloading anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR annotations.&lt;/strong&gt; The built-in &lt;code&gt;github&lt;/code&gt; reporter marks failing lines directly in the
pull-request diff. Add it to the &lt;code&gt;reporter&lt;/code&gt; array on CI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For &lt;strong&gt;history and trends&lt;/strong&gt; — flaky rate over time, durations, ownership — reach for a&lt;br&gt;
dedicated tool: &lt;strong&gt;Allure&lt;/strong&gt; (&lt;code&gt;allure-playwright&lt;/code&gt;) or shipping the &lt;code&gt;json&lt;/code&gt;/&lt;code&gt;blob&lt;/code&gt; output to&lt;br&gt;
a dashboard. Use those when "how is the suite trending?" becomes a recurring question;&lt;br&gt;
the custom reporter covers the single-run story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The principle
&lt;/h2&gt;

&lt;p&gt;Reporting is a &lt;strong&gt;transformation of raw results into a decision&lt;/strong&gt;: collect → summarise →&lt;br&gt;
deliver to where your audience already looks. Playwright gives you the events; a few&lt;br&gt;
lines of reporter turn them into the one line your team actually reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next up
&lt;/h2&gt;

&lt;p&gt;Everything's in place — and visible. &lt;strong&gt;Chapter 26 — Capstone:&lt;/strong&gt; one comprehensive&lt;br&gt;
end-to-end regression that exercises the whole product (sign up → author → comment →&lt;br&gt;
favorite → follow) and ties every technique from the course together. Tag: &lt;code&gt;ch-26&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Following along? Star the &lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;repo&lt;/a&gt;&lt;br&gt;
and tell me what your end-of-run summary would highlight.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>playwright</category>
      <category>typescript</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Framework Maturation &amp; Docs (Playwright + TypeScript, Ch.24)</title>
      <dc:creator>kadir</dc:creator>
      <pubDate>Mon, 08 Jun 2026 22:53:26 +0000</pubDate>
      <link>https://dev.to/aktibaba/framework-maturation-docs-playwright-typescript-ch24-oj</link>
      <guid>https://dev.to/aktibaba/framework-maturation-docs-playwright-typescript-ch24-oj</guid>
      <description>&lt;p&gt;A framework isn't finished when it &lt;em&gt;works&lt;/em&gt; — it's finished when &lt;strong&gt;someone else can&lt;br&gt;
extend it&lt;/strong&gt; without reading every file. "Maturation" is that last mile: filling&lt;br&gt;
coverage gaps and writing the docs that make the project approachable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code for this chapter is tagged &lt;code&gt;ch-24&lt;/code&gt; in the repo:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;https://github.com/aktibaba/playwright-qa-course&lt;/a&gt;&lt;/strong&gt; — see the new&lt;br&gt;
&lt;code&gt;profile-and-feed&lt;/code&gt; / &lt;code&gt;article-filters&lt;/code&gt; specs and &lt;code&gt;docs/reference/framework.md&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Rounding out coverage
&lt;/h2&gt;

&lt;p&gt;We fill the obvious gaps with flows that reuse everything we've built:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Profile&lt;/strong&gt; — an author's article shows on their profile page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global feed &amp;amp; tag filter&lt;/strong&gt; — toggling the feed and clicking a tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editing&lt;/strong&gt; — open the editor for an existing article and save a change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API list filters&lt;/strong&gt; — by author, the empty case, and the personalised feed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A couple of these reinforced a lesson worth burning in: &lt;strong&gt;keep assertions deterministic&lt;br&gt;
under parallelism.&lt;/strong&gt; The danger is asserting on a &lt;em&gt;specific item's position&lt;/em&gt; in a list&lt;br&gt;
that other tests are changing at the same time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ fragile: the unfiltered feed shows only a few newest articles, and other&lt;/span&gt;
&lt;span class="c1"&gt;//    tests are adding articles right now — yours may not be on the page.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;heading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ robust option A: assert the behaviour, not a specific item&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;globalFeed&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/active/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// the toggle worked&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.preview-link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// some article rendered&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ robust option B: isolate, so only YOUR data matches&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;author&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;registerUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// a brand-new user&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createArticleAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/#/profile/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// their profile has exactly one article&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new &lt;code&gt;createArticleAs(token, …)&lt;/code&gt; helper creates an article authored by a &lt;em&gt;specific&lt;/em&gt;&lt;br&gt;
user — the building block that makes an isolated profile or author-filter test possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docs that make it extendable
&lt;/h2&gt;

&lt;p&gt;Finally, a &lt;strong&gt;&lt;a href="https://dev.to/reference/framework"&gt;framework reference&lt;/a&gt;&lt;/strong&gt; — one page that maps the&lt;br&gt;
layers, states the rules (the dependency direction, the determinism rules), and answers&lt;br&gt;
"how do I add a test / Page Object / fixture / data factory?". With the repo's &lt;code&gt;README&lt;/code&gt;,&lt;br&gt;
a newcomer can be productive in minutes instead of digging through files.&lt;/p&gt;

&lt;p&gt;Good docs capture the decisions the code can't explain on its own: &lt;em&gt;why&lt;/em&gt; tests import&lt;br&gt;
only from &lt;code&gt;@fixtures&lt;/code&gt;, &lt;em&gt;why&lt;/em&gt; no test resets the database, &lt;em&gt;why&lt;/em&gt; you never assert on a&lt;br&gt;
list position. That's what keeps a maturing framework from drifting back into chaos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next up
&lt;/h2&gt;

&lt;p&gt;Everything is in place — and the results are visible. &lt;strong&gt;Chapter 25 — Reporting:&lt;/strong&gt; a&lt;br&gt;
dedicated look at turning raw results into something a team acts on, including a custom&lt;br&gt;
Playwright reporter. Then &lt;strong&gt;Chapter 26&lt;/strong&gt; is the grand &lt;strong&gt;Capstone&lt;/strong&gt; — a full end-to-end&lt;br&gt;
regression tying every technique together. Tag: &lt;code&gt;ch-25&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Following along? Star the &lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;repo&lt;/a&gt;&lt;br&gt;
and tell me what you'd put in your framework's onboarding doc.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>playwright</category>
      <category>typescript</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Stability &amp; Maintainability at Scale (Playwright + TypeScript, Ch.23)</title>
      <dc:creator>kadir</dc:creator>
      <pubDate>Mon, 08 Jun 2026 22:53:11 +0000</pubDate>
      <link>https://dev.to/aktibaba/stability-maintainability-at-scale-playwright-typescript-ch23-5fih</link>
      <guid>https://dev.to/aktibaba/stability-maintainability-at-scale-playwright-typescript-ch23-5fih</guid>
      <description>&lt;p&gt;As a suite grows, two qualities decide whether it stays an asset or becomes a burden:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stable&lt;/strong&gt; — it fails only for &lt;em&gt;real&lt;/em&gt; reasons (not random flakiness).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintainable&lt;/strong&gt; — you can add the next test without copy-pasting setup everywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This chapter is about the habits that keep both true, shown by adding comment and&lt;br&gt;
settings flows.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code for this chapter is tagged &lt;code&gt;ch-23&lt;/code&gt; in the repo:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;https://github.com/aktibaba/playwright-qa-course&lt;/a&gt;&lt;/strong&gt; — see &lt;code&gt;src/utils/unique.ts&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;src/pages/SettingsPage.ts&lt;/code&gt;, and the new &lt;code&gt;comment-ui&lt;/code&gt; / &lt;code&gt;settings-ui&lt;/code&gt; specs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Centralize the tricky bits
&lt;/h2&gt;

&lt;p&gt;Remember the flaky slug bug? It came from generating "unique" data that wasn't actually&lt;br&gt;
unique across parallel workers. The lesson isn't "be more careful" — it's &lt;em&gt;put the hard&lt;br&gt;
thing in one place so nobody can get it wrong again&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/utils/unique.ts&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;uniqueId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// timestamp + counter + randomness → unique even across parallel workers&lt;/span&gt;
  &lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e9&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the article factory and the user factory both call &lt;code&gt;uniqueId()&lt;/code&gt; — one proven&lt;br&gt;
recipe, no chance to reintroduce the collision. (This is the &lt;strong&gt;DRY&lt;/strong&gt; principle — &lt;em&gt;Don't&lt;br&gt;
Repeat Yourself&lt;/em&gt; — applied to the riskiest line.) Maintainability means &lt;em&gt;the correct way&lt;br&gt;
is the only way.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Wait for the right signal, not a guess
&lt;/h2&gt;

&lt;p&gt;The settings screen loads the current user &lt;strong&gt;asynchronously&lt;/strong&gt;, then fills the form.&lt;br&gt;
Editing a field &lt;em&gt;before&lt;/em&gt; that load finishes would submit blank values over the real&lt;br&gt;
ones. The stable fix is &lt;strong&gt;never&lt;/strong&gt; a fixed &lt;code&gt;waitForTimeout(2000)&lt;/code&gt; (which is either too&lt;br&gt;
short and flaky, or too long and slow) — it's waiting for the actual &lt;em&gt;readiness signal&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/pages/SettingsPage.ts&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/#/settings&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updateButton&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toHaveValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// wait until the form is filled in&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;not.toHaveValue("")&lt;/code&gt; waits until the username field is no longer empty — a concrete&lt;br&gt;
sign the form finished loading. Putting that wait &lt;em&gt;inside the Page Object&lt;/em&gt; means every&lt;br&gt;
settings test gets the stability for free — the test just calls &lt;code&gt;goto()&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  New flows, same machinery
&lt;/h2&gt;

&lt;p&gt;Adding comments and settings needed &lt;strong&gt;no new infrastructure&lt;/strong&gt; — they reuse the fixtures&lt;br&gt;
and Page Objects we already have. A comment test reads as plain behaviour:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;storageState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.auth/playwright.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;post a comment and see it appear&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;makeArticle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;articlePage&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;makeArticle&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;           &lt;span class="c1"&gt;// seed via API (fast)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;articlePage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Nice article &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;articlePage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// act in the UI&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;articlePage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// verify in the UI&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The settings test goes a step further on isolation: it &lt;strong&gt;registers a fresh user&lt;/strong&gt;&lt;br&gt;
through the API and logs in as them, so changing a profile never collides with other&lt;br&gt;
tests using the shared seed user. New surface, but the same &lt;code&gt;registerUser&lt;/code&gt;, &lt;code&gt;loginPage&lt;/code&gt;,&lt;br&gt;
and &lt;code&gt;settingsPage&lt;/code&gt; building blocks. &lt;em&gt;That's&lt;/em&gt; what "scales" means — the cost of the next&lt;br&gt;
flow is small because the pieces already exist.&lt;/p&gt;
&lt;h2&gt;
  
  
  …and another real bug
&lt;/h2&gt;

&lt;p&gt;Writing the settings flow, the UI test failed — and so did a direct API check. The app's&lt;br&gt;
update endpoint &lt;strong&gt;returned a 500 error on every profile update&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// the original, buggy condition&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;   &lt;span class="c1"&gt;// this is ALWAYS true!&lt;/span&gt;
  &lt;span class="nx"&gt;loggedUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;bcryptHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// hashing `undefined` → crash&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;a !== x || a !== y&lt;/code&gt; can never be false (a value can't equal two different things at&lt;br&gt;
once), so &lt;strong&gt;every&lt;/strong&gt; update tried to hash an absent password and crashed — and on a real&lt;br&gt;
save it would have &lt;em&gt;overwritten the user's password&lt;/em&gt;. One character — &lt;code&gt;||&lt;/code&gt; → &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; —&lt;br&gt;
fixed it. The suite didn't just &lt;em&gt;check&lt;/em&gt; the settings screen; it proved the whole feature&lt;br&gt;
was broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next up
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Chapter 24 — Framework maturation &amp;amp; docs:&lt;/strong&gt; we tidy the project, document how to run&lt;br&gt;
and extend it, and round out coverage so a newcomer can be productive in minutes. Tag:&lt;br&gt;
&lt;code&gt;ch-24&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Following along? Star the &lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;repo&lt;/a&gt;&lt;br&gt;
and tell me the one helper that removed the most flakiness from your suite.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>playwright</category>
      <category>typescript</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Advanced: Network Mocking, Visual &amp; Accessibility (Playwright + TypeScript, Ch.22)</title>
      <dc:creator>kadir</dc:creator>
      <pubDate>Mon, 08 Jun 2026 22:11:35 +0000</pubDate>
      <link>https://dev.to/aktibaba/advanced-network-mocking-visual-accessibility-playwright-typescript-ch22-5eb2</link>
      <guid>https://dev.to/aktibaba/advanced-network-mocking-visual-accessibility-playwright-typescript-ch22-5eb2</guid>
      <description>&lt;p&gt;Welcome to &lt;strong&gt;Part 6&lt;/strong&gt;. The framework is solid; now we add three powerful kinds of test&lt;br&gt;
that go beyond "click and check the text."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code for this chapter is tagged &lt;code&gt;ch-22&lt;/code&gt; in the repo:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;https://github.com/aktibaba/playwright-qa-course&lt;/a&gt;&lt;/strong&gt; — see &lt;code&gt;src/tests/ui/&lt;/code&gt;:&lt;br&gt;
&lt;code&gt;network-mock.spec.ts&lt;/code&gt;, &lt;code&gt;visual.spec.ts&lt;/code&gt;, &lt;code&gt;a11y.spec.ts&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Network mocking — test the UI in isolation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mocking&lt;/strong&gt; the network means &lt;em&gt;intercepting&lt;/em&gt; the app's requests and replying with a&lt;br&gt;
fake response &lt;strong&gt;you&lt;/strong&gt; write. &lt;code&gt;page.route(pattern, handler)&lt;/code&gt; catches matching requests;&lt;br&gt;
&lt;code&gt;route.fulfill(...)&lt;/code&gt; answers them. That makes states which are awkward to set up for&lt;br&gt;
real — an empty list, a server error, weird data — trivial and 100% predictable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shows the empty state when the feed is empty&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Intercept the articles request and pretend the list is empty.&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;**/api/articles?*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fulfill&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="na"&gt;articlesCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Articles not available.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;survives an API error without crashing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Pretend the server returned a 500 error.&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;**/api/articles?*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fulfill&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;boom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sign up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// app still works&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The &lt;code&gt;**/api/articles?*&lt;/code&gt; is a glob pattern — &lt;code&gt;**&lt;/code&gt; matches anything, so this catches the&lt;br&gt;
articles request whatever its full URL.) These tests need no database and no login — the&lt;br&gt;
test owns the data. Use mocking for &lt;strong&gt;UI behaviour on hard-to-produce responses&lt;/strong&gt;; keep&lt;br&gt;
the real-backend integration tests from Part 4 for the contract itself. You want both.&lt;/p&gt;
&lt;h2&gt;
  
  
  Visual regression — catch the unintended
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;visual regression&lt;/strong&gt; test takes a screenshot and compares it pixel-by-pixel against a&lt;br&gt;
saved reference image (the &lt;strong&gt;baseline&lt;/strong&gt;). It catches things no text assertion would — a&lt;br&gt;
broken layout, a wrong colour, a button clipped off the edge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;login page matches its baseline&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/#/login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fonts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// wait for web fonts to load&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveScreenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;login.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxDiffPixelRatio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first run &lt;em&gt;creates&lt;/em&gt; the baseline; later runs compare against it. Two things keep&lt;br&gt;
visual tests trustworthy instead of flaky:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Let the page settle first.&lt;/strong&gt; Waiting on &lt;code&gt;document.fonts.ready&lt;/code&gt; avoids the most
common cause of false differences — a screenshot taken while a web font is still
swapping in. &lt;code&gt;maxDiffPixelRatio: 0.02&lt;/code&gt; tolerates tiny (2%) rendering differences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Baselines are platform-specific.&lt;/strong&gt; Text renders slightly differently on macOS vs
Linux, so a macOS baseline won't match a Linux CI machine. We &lt;code&gt;test.skip&lt;/code&gt; visual tests
on CI and generate Linux baselines separately. &lt;strong&gt;Never&lt;/strong&gt; diff a baseline made on one OS
against another.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Accessibility — and real bugs we fixed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Accessibility (a11y)&lt;/strong&gt; is whether people using assistive tech — screen readers,&lt;br&gt;
keyboard-only navigation — can use the app. The &lt;strong&gt;WCAG&lt;/strong&gt; standard defines rules (e.g.&lt;br&gt;
text must have enough colour contrast). The &lt;code&gt;@axe-core/playwright&lt;/code&gt; library scans a page&lt;br&gt;
and reports violations; we fail the test on serious ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AxeBuilder&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withTags&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wcag2a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wcag2aa&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;        &lt;span class="c1"&gt;// which rule sets to check&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.pagination&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="c1"&gt;// skip a third-party widget (see below)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;serious&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impact&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;serious&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impact&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;critical&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;serious&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;              &lt;span class="c1"&gt;// expect NO serious violations&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first run &lt;strong&gt;failed&lt;/strong&gt; — and the violations were real:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Colour contrast.&lt;/strong&gt; The navbar links (contrast 2.1:1), the banner subtitle, muted
dates, and the green feed toggle (3.0:1) all fell short of WCAG AA's required 4.5:1.
We &lt;strong&gt;fixed the app&lt;/strong&gt; (&lt;code&gt;sut/&lt;/code&gt;): darkened the brand green and the muted greys to pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orphaned list items&lt;/strong&gt; came from the third-party &lt;code&gt;react-paginate&lt;/code&gt; widget rendering its
&lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt; with &lt;code&gt;role="navigation"&lt;/code&gt;. We can't fix that from our app code, so we
&lt;code&gt;.exclude(".pagination")&lt;/code&gt; with a comment and would report it upstream.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the realistic a11y workflow: &lt;strong&gt;scan, fix what's yours, triage what isn't.&lt;/strong&gt; And&lt;br&gt;
fixing contrast is a genuine improvement for real users, not just a green test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next up
&lt;/h2&gt;

&lt;p&gt;We've widened &lt;em&gt;what&lt;/em&gt; we can check. &lt;strong&gt;Chapter 23 — Stability &amp;amp; maintainability at&lt;br&gt;
scale:&lt;/strong&gt; the small utilities and habits that keep a large suite trustworthy as it grows.&lt;br&gt;
Tag: &lt;code&gt;ch-23&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Following along? Star the &lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;repo&lt;/a&gt;&lt;br&gt;
and tell me which of the three — mocking, visual, or a11y — your suite is missing.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>playwright</category>
      <category>typescript</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>CI/CD with GitHub Actions &amp; Sharding (Playwright + TypeScript, Ch.21)</title>
      <dc:creator>kadir</dc:creator>
      <pubDate>Mon, 08 Jun 2026 17:55:43 +0000</pubDate>
      <link>https://dev.to/aktibaba/cicd-with-github-actions-sharding-playwright-typescript-ch21-3f7m</link>
      <guid>https://dev.to/aktibaba/cicd-with-github-actions-sharding-playwright-typescript-ch21-3f7m</guid>
      <description>&lt;p&gt;A suite that only runs on your laptop protects only your laptop. We want it to run&lt;br&gt;
&lt;strong&gt;automatically, on every change, for everyone&lt;/strong&gt; — that's the job of CI.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;CI/CD&lt;/strong&gt; = Continuous Integration / Continuous Delivery. The CI part: a service runs&lt;br&gt;
your tests every time code is pushed, so problems are caught immediately.&lt;br&gt;
&lt;strong&gt;GitHub Actions&lt;/strong&gt; is GitHub's built-in CI. You describe what to do in a &lt;strong&gt;workflow&lt;/strong&gt;&lt;br&gt;
— a YAML file in &lt;code&gt;.github/workflows/&lt;/code&gt;. A workflow has &lt;strong&gt;jobs&lt;/strong&gt;, each running on a&lt;br&gt;
fresh virtual machine called a &lt;strong&gt;runner&lt;/strong&gt;, and each job is a list of &lt;strong&gt;steps&lt;/strong&gt;.&lt;br&gt;
(YAML is just an indentation-based config format.)&lt;/p&gt;

&lt;p&gt;Code for this chapter is tagged &lt;code&gt;ch-21&lt;/code&gt; in the repo:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;https://github.com/aktibaba/playwright-qa-course&lt;/a&gt;&lt;/strong&gt; — see&lt;br&gt;
&lt;code&gt;.github/workflows/ci.yml&lt;/code&gt; and the new &lt;code&gt;comments&lt;/code&gt; / &lt;code&gt;favorites&lt;/code&gt; / &lt;code&gt;follow&lt;/code&gt; specs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Stand up the app, then test it
&lt;/h2&gt;

&lt;p&gt;The exact command we use locally brings Inkwell up on the CI runner — healthchecks and&lt;br&gt;
all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Start Inkwell (system under test)&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker compose -f sut/docker-compose.yml up -d --build --wait&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;                                  &lt;span class="c1"&gt;# install deps from the lockfile&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright install --with-deps chromium&lt;/span&gt;   &lt;span class="c1"&gt;# download the browser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--wait&lt;/code&gt; matters: the step blocks until every service is healthy, so the tests never&lt;br&gt;
start before the app is ready.&lt;/p&gt;
&lt;h2&gt;
  
  
  Shard across machines
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sharding&lt;/strong&gt; splits the test list into N groups that run on N machines &lt;em&gt;at the same&lt;br&gt;
time&lt;/em&gt;, so the total wall-clock time drops roughly N×. We use a &lt;strong&gt;matrix&lt;/strong&gt; (a way to run&lt;br&gt;
the same job several times with different values) and the &lt;strong&gt;blob&lt;/strong&gt; reporter (made to be&lt;br&gt;
merged later):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;fail-fast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;shard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;                              &lt;span class="c1"&gt;# run this job twice: shard 1 and shard 2&lt;/span&gt;
&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests (sharded)&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright test --shard=${{ matrix.shard }}/2 --reporter=blob&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;           &lt;span class="c1"&gt;# save this shard's results&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ !cancelled() }}&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;blob-report-${{ matrix.shard }}&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;blob-report/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each shard is its &lt;strong&gt;own job&lt;/strong&gt; on its &lt;strong&gt;own&lt;/strong&gt; runner with its &lt;strong&gt;own&lt;/strong&gt; dockerized app —&lt;br&gt;
complete isolation, no two shards sharing a database.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One nuance: &lt;strong&gt;project dependencies run in every shard that needs them.&lt;/strong&gt; Our &lt;code&gt;ui&lt;/code&gt;&lt;br&gt;
project depends on &lt;code&gt;setup&lt;/code&gt; (the saved auth session), so &lt;code&gt;setup&lt;/code&gt; runs once per shard —&lt;br&gt;
a good reason to keep dependency projects small.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Merge the shards into one report
&lt;/h2&gt;

&lt;p&gt;A second job downloads every shard's blob report and merges them into a single HTML&lt;br&gt;
report you can browse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;report&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ !cancelled() }}&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;                                &lt;span class="c1"&gt;# wait for all the shard jobs&lt;/span&gt;
  &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;20&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;npm&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/download-artifact@v4&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;all-blob-reports&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;blob-report-*&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;merge-multiple&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright merge-reports --reporter=html ./all-blob-reports&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;playwright-html-report&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;playwright-report/&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Download that artifact from the run and you get one unified report — with traces on any&lt;br&gt;
failure — exactly as if it had all run on one machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  More coverage — and real bugs in the app
&lt;/h2&gt;

&lt;p&gt;This chapter also adds &lt;strong&gt;comments&lt;/strong&gt;, &lt;strong&gt;favorites&lt;/strong&gt;, and &lt;strong&gt;follows&lt;/strong&gt; test suites, each&lt;br&gt;
using fresh per-test data so counts are deterministic.&lt;/p&gt;

&lt;p&gt;Cranking up the parallelism did what good tests do: it &lt;strong&gt;found real bugs in the&lt;br&gt;
application.&lt;/strong&gt; Under heavy concurrent load, requests started failing — and a trace plus&lt;br&gt;
the server logs pinned down four genuine defects in Inkwell:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A null-author race.&lt;/strong&gt; &lt;code&gt;createArticle&lt;/code&gt; set the author with an &lt;em&gt;un-awaited&lt;/em&gt;
&lt;code&gt;setAuthor()&lt;/code&gt;, leaving a brief moment where the new article had no author. A
concurrent &lt;code&gt;GET /articles&lt;/code&gt; hitting that row crashed. Fix: set the author atomically
when creating the row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate slugs.&lt;/strong&gt; &lt;code&gt;slug&lt;/code&gt; wasn't unique and the code used a racy "check then
create", so two concurrent same-title creates made two articles with the same slug —
which then broke favoriting. Fix: a &lt;code&gt;unique&lt;/code&gt; constraint on &lt;code&gt;slug&lt;/code&gt; and an atomic
create.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A crash on a missing &lt;code&gt;tagList&lt;/code&gt;&lt;/strong&gt;, and &lt;strong&gt;4. broken &lt;code&gt;offset&lt;/code&gt; pagination&lt;/strong&gt;
(it multiplied &lt;code&gt;offset * limit&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rather than mask these with retries, &lt;strong&gt;we fixed the app itself&lt;/strong&gt; (it's in &lt;code&gt;sut/&lt;/code&gt;). And&lt;br&gt;
that's the real lesson: &lt;em&gt;running your suite at scale is a load test, and load tests find&lt;br&gt;
concurrency bugs.&lt;/em&gt; With the app corrected, the suite is fully deterministic — UI and API&lt;br&gt;
specs run concurrently with no special ordering needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next up — Part 6: Advanced &amp;amp; Capstone
&lt;/h2&gt;

&lt;p&gt;The framework is real and runs in CI. &lt;strong&gt;Chapter 22 — Advanced techniques:&lt;/strong&gt; network&lt;br&gt;
mocking to test the UI in isolation, visual snapshots, and accessibility scans — new&lt;br&gt;
kinds of checks on top of everything we've built. Tag: &lt;code&gt;ch-22&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Following along? Star the &lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;repo&lt;/a&gt;&lt;br&gt;
and tell me how many shards your CI runs.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>playwright</category>
      <category>typescript</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Reporters &amp; Observability (Playwright + TypeScript, Ch.20)</title>
      <dc:creator>kadir</dc:creator>
      <pubDate>Mon, 08 Jun 2026 17:55:30 +0000</pubDate>
      <link>https://dev.to/aktibaba/reporters-observability-playwright-typescript-ch20-36m2</link>
      <guid>https://dev.to/aktibaba/reporters-observability-playwright-typescript-ch20-36m2</guid>
      <description>&lt;p&gt;A suite is only as useful as what it tells you when it fails. A red ✘ with no context&lt;br&gt;
means a re-run; a red ✘ with a screenshot, a recording, and the environment it ran on&lt;br&gt;
means a &lt;em&gt;fix&lt;/em&gt;. This chapter makes failures &lt;strong&gt;self-explanatory&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A &lt;strong&gt;reporter&lt;/strong&gt; is the part of Playwright that turns raw results into output — terminal&lt;br&gt;
text, an HTML page, an XML file, whatever you need. You can run several at once.&lt;/p&gt;

&lt;p&gt;Code for this chapter is tagged &lt;code&gt;ch-20&lt;/code&gt; in the repo:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;https://github.com/aktibaba/playwright-qa-course&lt;/a&gt;&lt;/strong&gt; — see the &lt;code&gt;reporter&lt;/code&gt; config in&lt;br&gt;
&lt;code&gt;playwright.config.ts&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Stack reporters for different audiences
&lt;/h2&gt;

&lt;p&gt;Reporters aren't either/or — list several, and each serves a different consumer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// playwright.config.ts&lt;/span&gt;
&lt;span class="nx"&gt;reporter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;                                             &lt;span class="c1"&gt;// for you, live in the terminal&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;open&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;never&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;                          &lt;span class="c1"&gt;// a rich, browsable report&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;junit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;outputFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test-results/junit.xml&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;  &lt;span class="c1"&gt;// an XML file CI understands&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;list&lt;/code&gt;&lt;/strong&gt; — readable, streaming output while you work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;html&lt;/code&gt;&lt;/strong&gt; — the investigative tool: every test, its steps, attached
screenshots/traces, and the run's metadata. Open it with &lt;code&gt;npm run test:report&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;junit&lt;/code&gt;&lt;/strong&gt; — XML that CI systems (GitHub Actions, GitLab, Jenkins) parse to annotate
pull requests and track history. We wire this into CI next chapter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;blob&lt;/code&gt;&lt;/strong&gt; — a special format made for &lt;em&gt;merging&lt;/em&gt; results from parallel machines
(shards); we reach for it in Chapter 21.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Failures that explain themselves
&lt;/h2&gt;

&lt;p&gt;We set two options back in Chapter 6, and they pay off in every report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;on-first-retry&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;only-on-failure&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a failure, the HTML report carries the &lt;strong&gt;screenshot&lt;/strong&gt; at the moment things broke and&lt;br&gt;
a full &lt;strong&gt;trace&lt;/strong&gt; (DOM snapshots, network, console, a timeline you can scrub) from the&lt;br&gt;
retry. You reconstruct exactly what happened &lt;strong&gt;without&lt;/strong&gt; reproducing it locally — the&lt;br&gt;
difference between minutes and hours on a CI-only flake.&lt;/p&gt;
&lt;h2&gt;
  
  
  Stamp the run with its environment
&lt;/h2&gt;

&lt;p&gt;Because the config sets &lt;code&gt;metadata&lt;/code&gt; (Chapter 18), every report records &lt;em&gt;what it ran&lt;br&gt;
against&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;webURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;apiURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiURL&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"It failed" is noise; "it failed on &lt;strong&gt;staging&lt;/strong&gt;, against &lt;em&gt;that&lt;/em&gt; URL" is a lead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attach your own context
&lt;/h2&gt;

&lt;p&gt;When a test knows something useful — like the API response that drove an assertion —&lt;br&gt;
you can &lt;strong&gt;attach&lt;/strong&gt; it, and it appears inline in the report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;articles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;articles-response&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;testInfo&lt;/code&gt; is an extra argument Playwright passes to every test with details about the&lt;br&gt;
current run; &lt;code&gt;testInfo.attach(...)&lt;/code&gt; saves a file alongside the result. Now the data&lt;br&gt;
that caused a failure travels &lt;em&gt;with&lt;/em&gt; it.&lt;/p&gt;
&lt;h2&gt;
  
  
  More coverage to observe
&lt;/h2&gt;

&lt;p&gt;A report is richer when there's more to observe, so this chapter also broadens the API&lt;br&gt;
coverage — profiles, tags, and pagination — using a &lt;strong&gt;unique tag per test&lt;/strong&gt; so the&lt;br&gt;
filtered results are deterministic even under parallelism:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;limit caps the page and the filtered count is exact&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;makeArticle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`pg-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                  &lt;span class="c1"&gt;// unique → only THIS test's articles&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;makeArticle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tagList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;makeArticle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tagList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;makeArticle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tagList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;articles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;articlesCount&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// exact filtered total&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// capped by limit&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;A finding while writing these: Inkwell's &lt;strong&gt;&lt;code&gt;offset&lt;/code&gt; pagination was broken&lt;/strong&gt; —&lt;br&gt;
&lt;code&gt;?tag=X&amp;amp;limit=2&amp;amp;offset=2&lt;/code&gt; over 3 matches returned &lt;strong&gt;0&lt;/strong&gt; items instead of 1, because&lt;br&gt;
the API multiplied &lt;code&gt;offset * limit&lt;/code&gt; (treating offset as a page number), breaking the&lt;br&gt;
RealWorld contract. Exactly the sort of bug good coverage surfaces — we fix it in the&lt;br&gt;
app in Chapter 21 and the offset test goes green.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Next up
&lt;/h2&gt;

&lt;p&gt;We have results CI can read. &lt;strong&gt;Chapter 21 — CI/CD with GitHub Actions:&lt;/strong&gt; stand up the&lt;br&gt;
dockerized app in a workflow, run the suite &lt;strong&gt;sharded&lt;/strong&gt; across machines, merge the&lt;br&gt;
reports, and publish the HTML report as a downloadable artifact. Tag: &lt;code&gt;ch-21&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Following along? Star the &lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;repo&lt;/a&gt;&lt;br&gt;
and tell me which reporter you live in.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>playwright</category>
      <category>typescript</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Parallelism &amp; Flake Control (Playwright + TypeScript, Ch.19)</title>
      <dc:creator>kadir</dc:creator>
      <pubDate>Mon, 08 Jun 2026 17:55:12 +0000</pubDate>
      <link>https://dev.to/aktibaba/parallelism-flake-control-playwright-typescript-ch19-b9n</link>
      <guid>https://dev.to/aktibaba/parallelism-flake-control-playwright-typescript-ch19-b9n</guid>
      <description>&lt;p&gt;A fast suite runs tests &lt;strong&gt;in parallel&lt;/strong&gt; (many at once). But parallelism is also where&lt;br&gt;
&lt;strong&gt;flaky&lt;/strong&gt; tests are born.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A &lt;strong&gt;flaky&lt;/strong&gt; test is one that sometimes passes and sometimes fails &lt;em&gt;without the code&lt;br&gt;
changing&lt;/em&gt;. It's the most corrosive thing in a test suite — once people stop trusting&lt;br&gt;
a red result, the suite stops protecting anything.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The good news: we've already met (and fixed) the main causes earlier in this course.&lt;br&gt;
This chapter names the model and turns those fixes into principles.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code for this chapter is tagged &lt;code&gt;ch-19&lt;/code&gt; in the repo:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;https://github.com/aktibaba/playwright-qa-course&lt;/a&gt;&lt;/strong&gt; — see the &lt;code&gt;test:flake&lt;/code&gt; script in&lt;br&gt;
&lt;code&gt;package.json&lt;/code&gt; and the parallelism config in &lt;code&gt;playwright.config.ts&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  How Playwright parallelizes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workers&lt;/strong&gt; are separate processes. Playwright starts several (CPU-based locally; we
pin &lt;code&gt;workers: 4&lt;/code&gt; on CI) and spreads tests across them, so many run at the same time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolation is automatic&lt;/strong&gt; — &lt;em&gt;for the browser&lt;/em&gt;. Each test gets its own browser
context and &lt;code&gt;page&lt;/code&gt; (its own cookies, storage, cache), so tests can't see each other's
browser state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;fullyParallel: true&lt;/code&gt;&lt;/strong&gt; spreads tests &lt;em&gt;within&lt;/em&gt; a file across workers too, for
maximum concurrency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the catch: Playwright isolates the &lt;strong&gt;browser&lt;/strong&gt; for you, but it &lt;strong&gt;can't&lt;/strong&gt; isolate&lt;br&gt;
&lt;strong&gt;shared external state&lt;/strong&gt; — one database, one backend. That's where flake lives.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where flake actually comes from
&lt;/h2&gt;

&lt;p&gt;Every flaky test we hit in this course was one of four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Shared mutable state.&lt;/strong&gt; Parallel API tests each reset the database, wiping it out
from under a test that was mid-read (Chapter 11). &lt;em&gt;Fix:&lt;/em&gt; seed once in &lt;code&gt;globalSetup&lt;/code&gt;;
no test resets. Don't share mutable state — or access it in order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Imprecise locators / assertions.&lt;/strong&gt; &lt;code&gt;getByRole("heading", { name: "inkwell" })&lt;/code&gt;
also matched the "Welcome to Inkwell" article heading, so it passed or failed
depending on how fast the feed loaded (Chapter 3). &lt;em&gt;Fix:&lt;/em&gt; &lt;code&gt;{ exact: true }&lt;/code&gt;.
Ambiguity plus timing equals flake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Races with the app.&lt;/strong&gt; Navigating right after login raced the app's &lt;em&gt;asynchronous&lt;/em&gt;
redirect (Chapter 5). &lt;em&gt;Fix:&lt;/em&gt; wait for a real signal (the login form disappearing),
never assume an async action has finished.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order / collision.&lt;/strong&gt; Two tests creating an article with the same title clashed.
&lt;em&gt;Fix:&lt;/em&gt; unique data per test (&lt;code&gt;Date.now()&lt;/code&gt;) and clean up what you create.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice none of these were "Playwright being unreliable." They were shared state,&lt;br&gt;
timing, and ambiguity — the universal causes of flakiness anywhere.&lt;/p&gt;
&lt;h2&gt;
  
  
  The knobs (and when to reach for them)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;fullyParallel&lt;/code&gt; + &lt;code&gt;workers&lt;/code&gt;&lt;/strong&gt; — turn concurrency up. Leave these on by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;test.describe.configure({ mode: "serial" })&lt;/code&gt;&lt;/strong&gt; — force a group of tests to run
one at a time, in order. A scalpel for tests that &lt;em&gt;must&lt;/em&gt; share state — not a default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project &lt;code&gt;dependencies&lt;/code&gt;&lt;/strong&gt; — order whole phases (our &lt;code&gt;ui&lt;/code&gt; waits for &lt;code&gt;api&lt;/code&gt; + &lt;code&gt;setup&lt;/code&gt;)
so cross-project state can't race.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-test isolation&lt;/strong&gt; — the real cure: unique data + cleanup (the &lt;code&gt;makeArticle&lt;/code&gt;
factory), so tests never contend in the first place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;retries&lt;/code&gt;&lt;/strong&gt; — the &lt;em&gt;last&lt;/em&gt; resort.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Retries &lt;strong&gt;hide&lt;/strong&gt; flake; they don't fix it. They're a safety net for genuinely&lt;br&gt;
non-deterministic &lt;em&gt;infrastructure&lt;/em&gt; (a network blip on a remote run), not a substitute&lt;br&gt;
for fixing a data race in your own tests. We keep retries at &lt;strong&gt;0 locally&lt;/strong&gt; precisely&lt;br&gt;
so flake stays visible and gets fixed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Hunt flake before CI does
&lt;/h2&gt;

&lt;p&gt;A test that fails 1 run in 50 will eventually turn your pipeline red at the worst&lt;br&gt;
moment. Surface it on purpose by running each test many times:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run &lt;span class="nb"&gt;test&lt;/span&gt;:flake        &lt;span class="c"&gt;# playwright test --repeat-each=5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--repeat-each=5&lt;/code&gt; runs every test five times. Combine it with &lt;code&gt;--trace on&lt;/code&gt; and the&lt;br&gt;
trace viewer (Chapter 6) to see exactly what diverged on the run that failed. If a test&lt;br&gt;
survives &lt;code&gt;--repeat-each=20&lt;/code&gt; under load, it's stable; if it doesn't, you've found a real&lt;br&gt;
bug to fix — not a retry to paper over it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next up
&lt;/h2&gt;

&lt;p&gt;We can run fast &lt;em&gt;and&lt;/em&gt; trustworthy. &lt;strong&gt;Chapter 20 — Reporters &amp;amp; observability:&lt;/strong&gt; make&lt;br&gt;
results legible — the HTML report, JUnit for CI, and attaching traces so a failure&lt;br&gt;
tells you what happened without a re-run. Tag: &lt;code&gt;ch-20&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Following along? Star the &lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;repo&lt;/a&gt;&lt;br&gt;
and tell me the last flaky test you chased down — and what caused it.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>playwright</category>
      <category>typescript</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Multi-Environment Configuration (Playwright + TypeScript, Ch.18)</title>
      <dc:creator>kadir</dc:creator>
      <pubDate>Mon, 08 Jun 2026 17:55:00 +0000</pubDate>
      <link>https://dev.to/aktibaba/multi-environment-configuration-playwright-typescript-ch18-3oih</link>
      <guid>https://dev.to/aktibaba/multi-environment-configuration-playwright-typescript-ch18-3oih</guid>
      <description>&lt;p&gt;Welcome to &lt;strong&gt;Part 5 — Scaling, Config &amp;amp; CI&lt;/strong&gt;. In Chapter 17 we built a typed &lt;code&gt;env&lt;/code&gt;&lt;br&gt;
module. Now we wire it into &lt;code&gt;playwright.config.ts&lt;/code&gt; so the &lt;em&gt;whole run&lt;/em&gt; adapts to where&lt;br&gt;
it's pointed — the same suite against local, CI, or staging, each with the right URLs&lt;br&gt;
and the right resilience.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;CI&lt;/strong&gt; ("Continuous Integration") is a service — like GitHub Actions — that runs your&lt;br&gt;
tests automatically every time someone pushes code. It's a different, often slower&lt;br&gt;
machine than your laptop, so it sometimes needs different settings.&lt;/p&gt;

&lt;p&gt;Code for this chapter is tagged &lt;code&gt;ch-18&lt;/code&gt; in the repo:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;https://github.com/aktibaba/playwright-qa-course&lt;/a&gt;&lt;/strong&gt; — see &lt;code&gt;playwright.config.ts&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  The config is a function of &lt;code&gt;env&lt;/code&gt; and CI
&lt;/h2&gt;

&lt;p&gt;Two inputs decide everything: which &lt;strong&gt;environment&lt;/strong&gt; (&lt;code&gt;TEST_ENV&lt;/code&gt;), and whether we're&lt;br&gt;
running on &lt;strong&gt;CI&lt;/strong&gt;. We derive the rest from them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// playwright.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./src/utils/env&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isCI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// true when running on CI&lt;/span&gt;

&lt;span class="c1"&gt;// Remote environments are flakier (real network), so allow a retry; local stays&lt;/span&gt;
&lt;span class="c1"&gt;// at 0 so a flaky test is visible immediately.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;retries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isCI&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;staging&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;forbidOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isCI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isCI&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;local&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;local&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;webURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;apiURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiURL&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A couple of syntax notes for newcomers: &lt;code&gt;!!process.env.CI&lt;/code&gt; turns the value into a plain&lt;br&gt;
&lt;code&gt;true&lt;/code&gt;/&lt;code&gt;false&lt;/code&gt;. &lt;code&gt;a ? b : c&lt;/code&gt; is a &lt;em&gt;ternary&lt;/em&gt; — "if &lt;code&gt;a&lt;/code&gt;, use &lt;code&gt;b&lt;/code&gt;, otherwise &lt;code&gt;c&lt;/code&gt;."&lt;/p&gt;

&lt;p&gt;What each option does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;forbidOnly&lt;/code&gt;&lt;/strong&gt; — fails the build if someone left a &lt;code&gt;test.only&lt;/code&gt; in the code (which
would silently skip every other test). Enforced &lt;strong&gt;only on CI&lt;/strong&gt;, so it never blocks
you while you focus on one test locally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;retries&lt;/code&gt;&lt;/strong&gt; — if a test fails, run it again up to N times before calling it failed.
We allow retries on remote/CI runs (real network blips happen) but keep &lt;strong&gt;0
locally&lt;/strong&gt;, so a flaky test is a signal you investigate, not noise you ignore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;workers&lt;/code&gt;&lt;/strong&gt; — how many tests run in parallel. Pinned to 4 on CI (predictable shared
machines), left to Playwright's CPU-based default locally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;timeout&lt;/code&gt; / &lt;code&gt;expect.timeout&lt;/code&gt;&lt;/strong&gt; — how long a test (and an assertion) may take before
giving up. More headroom for slower remote environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;metadata&lt;/code&gt;&lt;/strong&gt; — extra info stamped into the report, so you can always see &lt;em&gt;which
environment&lt;/em&gt; a run was pointed at.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The per-project &lt;code&gt;baseURL&lt;/code&gt; already came from &lt;code&gt;env&lt;/code&gt; in earlier chapters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiURL&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;setup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webURL&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ui&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Desktop Chrome&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One switch flips the whole run
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;test&lt;/span&gt;                     &lt;span class="c"&gt;# local: localhost, 0 retries, fast timeouts&lt;/span&gt;
&lt;span class="nv"&gt;TEST_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;staging npm &lt;span class="nb"&gt;test&lt;/span&gt;    &lt;span class="c"&gt;# staging URLs, 1 retry, longer timeouts&lt;/span&gt;
&lt;span class="nv"&gt;CI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 npm &lt;span class="nb"&gt;test&lt;/span&gt;                &lt;span class="c"&gt;# CI mode: forbidOnly on, 2 retries, 4 workers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing in a test, Page Object, or fixture changes — they read &lt;code&gt;env&lt;/code&gt;, and &lt;code&gt;env&lt;/code&gt; reads&lt;br&gt;
the environment variables. All the configuration lives in just two files (&lt;code&gt;env.ts&lt;/code&gt; and&lt;br&gt;
the config). That's the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime-selected vs. a project per environment
&lt;/h2&gt;

&lt;p&gt;You'll sometimes see suites that define &lt;strong&gt;one Playwright project per environment&lt;/strong&gt; and&lt;br&gt;
run them together. That's right when a &lt;em&gt;single command&lt;/em&gt; must hit several environments&lt;br&gt;
at once (a smoke check across regions, say). For the common case — "run &lt;em&gt;this&lt;/em&gt; suite&lt;br&gt;
against &lt;em&gt;that&lt;/em&gt; environment" — a &lt;strong&gt;runtime-selected&lt;/strong&gt; config like ours is simpler: no&lt;br&gt;
duplicated projects, and the environment is one obvious input. Reach for&lt;br&gt;
project-per-env only when you truly need to target several at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next up
&lt;/h2&gt;

&lt;p&gt;The config now scales across environments. &lt;strong&gt;Chapter 19 — Parallelism &amp;amp; flake&lt;br&gt;
control:&lt;/strong&gt; how Playwright runs tests in parallel, where flakiness &lt;em&gt;actually&lt;/em&gt; comes&lt;br&gt;
from, and the knobs that keep a big suite both fast and trustworthy. Tag: &lt;code&gt;ch-19&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Following along? Star the &lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;repo&lt;/a&gt;&lt;br&gt;
and tell me how many environments your suite targets.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>playwright</category>
      <category>typescript</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Test Data Factories &amp; Environment Config (Playwright + TypeScript, Ch.17)</title>
      <dc:creator>kadir</dc:creator>
      <pubDate>Mon, 08 Jun 2026 17:15:42 +0000</pubDate>
      <link>https://dev.to/aktibaba/test-data-factories-environment-config-playwright-typescript-ch17-b7o</link>
      <guid>https://dev.to/aktibaba/test-data-factories-environment-config-playwright-typescript-ch17-b7o</guid>
      <description>&lt;p&gt;Two kinds of constant have been creeping into our tests: &lt;strong&gt;inline data objects&lt;/strong&gt; (the&lt;br&gt;
&lt;code&gt;{ title, description, body, tagList }&lt;/code&gt; we keep typing) and &lt;strong&gt;URLs&lt;/strong&gt;. Both deserve a&lt;br&gt;
single home. This chapter gives them one — a data &lt;strong&gt;factory&lt;/strong&gt; and a typed&lt;br&gt;
&lt;strong&gt;environment&lt;/strong&gt; module — and closes Part 4.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code for this chapter is tagged &lt;code&gt;ch-17&lt;/code&gt; in the repo:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;https://github.com/aktibaba/playwright-qa-course&lt;/a&gt;&lt;/strong&gt; — see&lt;br&gt;
&lt;code&gt;src/fixtures-data/article.ts&lt;/code&gt; and &lt;code&gt;src/utils/env.ts&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  A data factory
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;factory&lt;/strong&gt; is a function that builds a valid object with sensible defaults, and lets&lt;br&gt;
the caller override just the parts it cares about. It centralises "what a valid article&lt;br&gt;
looks like" so no test has to remember every field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/fixtures-data/article.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ArticleInput&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tagList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;seq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;articleData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;overrides&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ArticleInput&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}):&lt;/span&gt; &lt;span class="nx"&gt;ArticleInput&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;seq&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Test Article &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Generated by the article factory&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Article body for automated tests.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tagList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;                 &lt;span class="c1"&gt;// required by the API (Chapter 13)&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;overrides&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                &lt;span class="c1"&gt;// anything the caller passed wins&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two TypeScript bits worth naming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Partial&amp;lt;ArticleInput&amp;gt;&lt;/code&gt;&lt;/strong&gt; means "an object that may have &lt;em&gt;some&lt;/em&gt; of the article
fields" — so a caller can override only &lt;code&gt;title&lt;/code&gt;, or nothing at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;...overrides&lt;/code&gt;&lt;/strong&gt; is the &lt;em&gt;spread&lt;/em&gt; operator: it copies the caller's fields on top of
the defaults, so their values win. &lt;code&gt;articleData({ tagList: ["x"] })&lt;/code&gt; keeps every
default but replaces &lt;code&gt;tagList&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our provisioning helper now just defers to the factory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/utils/scenarios.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createArticle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;overrides&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ArticleInput&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;articles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;article&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;articleData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;overrides&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So a test stays focused on intent — &lt;code&gt;makeArticle({ tagList: ["integration"] })&lt;/code&gt; — and&lt;br&gt;
the unique title, the valid defaults, and the "tagList is required" rule all live in&lt;br&gt;
&lt;strong&gt;one&lt;/strong&gt; place. Change the article shape once and every test follows.&lt;/p&gt;

&lt;p&gt;Why put it in &lt;code&gt;src/fixtures-data/&lt;/code&gt; (the &lt;code&gt;@data&lt;/code&gt; alias) and not in a fixture? Because&lt;br&gt;
this is &lt;strong&gt;pure data&lt;/strong&gt; — no &lt;code&gt;page&lt;/code&gt;, no setup/teardown. Factories are plain functions;&lt;br&gt;
&lt;em&gt;fixtures&lt;/em&gt; that use them own the lifecycle. Keeping them separate is the layering&lt;br&gt;
discipline from Chapter 10.&lt;/p&gt;
&lt;h2&gt;
  
  
  A typed environment module
&lt;/h2&gt;

&lt;p&gt;URLs are the other scattered constant. &lt;code&gt;env&lt;/code&gt; is our single source of truth, and now&lt;br&gt;
it's &lt;strong&gt;multi-environment&lt;/strong&gt;. First, a definition:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Environment variables&lt;/strong&gt; (&lt;code&gt;process.env.*&lt;/code&gt;) are values set &lt;em&gt;outside&lt;/em&gt; your code — by&lt;br&gt;
your shell or your CI system — like &lt;code&gt;TEST_ENV=staging&lt;/code&gt;. They let you change&lt;br&gt;
behaviour without editing files.&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/utils/env.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EnvName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;local&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ci&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;staging&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ENVIRONMENTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;EnvName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;webURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;apiURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;webURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;apiURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:3001/api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;webURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;apiURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:3001/api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;staging&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;webURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://inkwell-staging.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;apiURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://inkwell-staging.example.com/api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TEST_ENV&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;EnvName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;local&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// pick a target, default local&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ENVIRONMENTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;ENVIRONMENTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;webURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WEB_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// allow per-URL overrides&lt;/span&gt;
  &lt;span class="na"&gt;apiURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;API_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the &lt;strong&gt;same&lt;/strong&gt; suite runs anywhere, by changing only an environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;test&lt;/span&gt;                                &lt;span class="c"&gt;# local (the default)&lt;/span&gt;
&lt;span class="nv"&gt;TEST_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;staging npm &lt;span class="nb"&gt;test&lt;/span&gt;               &lt;span class="c"&gt;# against the staging deployment&lt;/span&gt;
&lt;span class="nv"&gt;API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://host:4000/api npm &lt;span class="nb"&gt;test&lt;/span&gt;   &lt;span class="c"&gt;# a one-off URL override&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The discipline that keeps this clean: &lt;strong&gt;only &lt;code&gt;env.ts&lt;/code&gt; reads &lt;code&gt;process.env&lt;/code&gt;.&lt;/strong&gt; Tests,&lt;br&gt;
Page Objects, and fixtures import &lt;code&gt;env&lt;/code&gt; — never environment variables directly. All&lt;br&gt;
configuration lives in one auditable place (the Chapter 10 layer rule, applied to&lt;br&gt;
config).&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 4, done
&lt;/h2&gt;

&lt;p&gt;The integration milestone is complete: log in once with &lt;code&gt;storageState&lt;/code&gt;, seed via the&lt;br&gt;
API and verify in the UI, and now clean factories and environment config. The suite is&lt;br&gt;
fast, isolated, and portable across environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next up — Part 5: Scaling, Config &amp;amp; CI
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Chapter 18 — Multi-environment configuration&lt;/strong&gt; takes the &lt;code&gt;env&lt;/code&gt; module we just built&lt;br&gt;
and wires it into Playwright's project system, so a single config targets several&lt;br&gt;
environments with the right base URLs, retries, and metadata. Tag: &lt;code&gt;ch-18&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Following along? Star the &lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;repo&lt;/a&gt;&lt;br&gt;
and tell me what your test-data factories generate most.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>playwright</category>
      <category>typescript</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Seed via API, Verify in UI (Playwright + TypeScript, Ch.16)</title>
      <dc:creator>kadir</dc:creator>
      <pubDate>Mon, 08 Jun 2026 17:04:27 +0000</pubDate>
      <link>https://dev.to/aktibaba/seed-via-api-verify-in-ui-playwright-typescript-ch16-4p4i</link>
      <guid>https://dev.to/aktibaba/seed-via-api-verify-in-ui-playwright-typescript-ch16-4p4i</guid>
      <description>&lt;p&gt;This is the payoff of everything so far. Think about a UI test that checks "does an&lt;br&gt;
article render on its page?" To get there &lt;em&gt;through the UI&lt;/em&gt; you'd log in, open the&lt;br&gt;
editor, fill four fields, and publish — &lt;strong&gt;every single time&lt;/strong&gt;. That's slow, and worse:&lt;br&gt;
if the editor form is flaky or gets redesigned, your &lt;em&gt;viewing&lt;/em&gt; test breaks for a reason&lt;br&gt;
that has nothing to do with viewing.&lt;/p&gt;

&lt;p&gt;The integration pattern fixes it: &lt;strong&gt;set up through the API, verify through the UI.&lt;/strong&gt;&lt;br&gt;
The API is fast and stable; use it to get to the starting state, then let the browser&lt;br&gt;
check only the one thing the test is actually about.&lt;/p&gt;

&lt;p&gt;We already have the pieces — &lt;code&gt;makeArticle&lt;/code&gt; from Chapter 14 creates an article in&lt;br&gt;
milliseconds. Now we just point the browser at it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code for this chapter is tagged &lt;code&gt;ch-16&lt;/code&gt; in the repo:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;https://github.com/aktibaba/playwright-qa-course&lt;/a&gt;&lt;/strong&gt; — see&lt;br&gt;
&lt;code&gt;src/tests/ui/seed-via-api.spec.ts&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Create through the API, check in the browser
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@fixtures&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;an article created through the API renders on its page&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;makeArticle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Set up the data through the API — one fast request.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;makeArticle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Seeded via API &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This article was created through the API and rendered by the UI.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tagList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;integration&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Point the browser straight at it.&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/#/article/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Verify what the UI actually renders.&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;heading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;playwright&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;makeArticle&lt;/code&gt; does an authenticated &lt;code&gt;POST&lt;/code&gt; behind the scenes and returns the created&lt;br&gt;
article (including its server-generated &lt;code&gt;slug&lt;/code&gt;). We navigate straight to its page —&lt;br&gt;
no form journey — and check what renders. And it's cleaned up automatically by the&lt;br&gt;
fixture's teardown (Chapter 14).&lt;/p&gt;

&lt;p&gt;Notice the &lt;strong&gt;division of labour&lt;/strong&gt;: &lt;em&gt;viewing&lt;/em&gt; an article is public, so this test needs&lt;br&gt;
no logged-in browser. Only the &lt;em&gt;creation&lt;/em&gt; is authenticated, and that's hidden inside&lt;br&gt;
&lt;code&gt;makeArticle&lt;/code&gt;. When a test does need to view &lt;em&gt;as a logged-in user&lt;/em&gt; (to see authoring&lt;br&gt;
buttons, say), combine this with the &lt;code&gt;storageState&lt;/code&gt; from Chapter 15: seed via API,&lt;br&gt;
load the saved session, verify in the UI.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why this is faster &lt;em&gt;and&lt;/em&gt; more reliable
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Faster.&lt;/strong&gt; An API &lt;code&gt;POST&lt;/code&gt; is milliseconds; driving the editor form is seconds. Across
a whole suite that's the difference between a 2-minute and a 10-minute run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More reliable.&lt;/strong&gt; The setup no longer goes through the UI, so a flaky editor can't
break a test about viewing. Each test fails for exactly &lt;strong&gt;one&lt;/strong&gt; reason — the thing
it asserts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focused.&lt;/strong&gt; The UI test checks one UI behaviour; the API's own correctness is
covered by the Part 3 tests. No duplication.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  …and the reverse
&lt;/h2&gt;

&lt;p&gt;The pattern runs both ways. When the &lt;em&gt;action&lt;/em&gt; genuinely belongs in the UI (a user&lt;br&gt;
clicks "Publish") but the &lt;em&gt;result&lt;/em&gt; is data, &lt;strong&gt;act in the UI and verify through the&lt;br&gt;
API&lt;/strong&gt; — checking the real source of truth is stronger than reading the page's HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// act in the UI…&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;articleEditorPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publishArticle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// …then verify through the API&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`articles/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rule of thumb: &lt;strong&gt;set up and verify through whichever layer is cheaper and more&lt;br&gt;
authoritative; reserve the UI for the behaviour you specifically need to prove.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Next up
&lt;/h2&gt;

&lt;p&gt;We've been writing test data inline. &lt;strong&gt;Chapter 17 — Test data &amp;amp; environment config&lt;/strong&gt;&lt;br&gt;
closes Part 4: data &lt;strong&gt;factories&lt;/strong&gt; for inputs, and a clean multi-environment config so&lt;br&gt;
the same suite runs against local, staging, or CI. Tag: &lt;code&gt;ch-17&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Following along? Star the &lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;repo&lt;/a&gt;&lt;br&gt;
and tell me how much of your UI setup you've moved to the API.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>playwright</category>
      <category>typescript</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Auth Once with storageState (Playwright + TypeScript, Ch.15)</title>
      <dc:creator>kadir</dc:creator>
      <pubDate>Mon, 08 Jun 2026 16:58:09 +0000</pubDate>
      <link>https://dev.to/aktibaba/auth-once-with-storagestate-playwright-typescript-ch15-4fld</link>
      <guid>https://dev.to/aktibaba/auth-once-with-storagestate-playwright-typescript-ch15-4fld</guid>
      <description>&lt;p&gt;Welcome to &lt;strong&gt;Part 4 — Integration&lt;/strong&gt;: making the API and UI layers work &lt;em&gt;together&lt;/em&gt;.&lt;br&gt;
This is what separates a toy suite from a real one, and we start with the highest-value&lt;br&gt;
example — logging in.&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Logging in through the form on every UI test is &lt;strong&gt;slow&lt;/strong&gt; (load the page, type the&lt;br&gt;
email, type the password, click, wait for the redirect) and repetitive. Do it on a&lt;br&gt;
hundred tests and you've wasted real minutes per run.&lt;/p&gt;
&lt;h2&gt;
  
  
  The idea: save the session
&lt;/h2&gt;

&lt;p&gt;When you log in, the browser stores proof that you're logged in — in &lt;strong&gt;cookies&lt;/strong&gt; and&lt;br&gt;
in &lt;strong&gt;&lt;code&gt;localStorage&lt;/code&gt;&lt;/strong&gt; (a small key/value store the page keeps in the browser). Together&lt;br&gt;
these are your &lt;strong&gt;session&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Playwright can &lt;strong&gt;save that session to a file&lt;/strong&gt; and &lt;strong&gt;load it back&lt;/strong&gt; into other tests.&lt;br&gt;
A test that loads it opens the app &lt;strong&gt;already logged in&lt;/strong&gt; — no form needed. That saved&lt;br&gt;
file is called a &lt;strong&gt;storage state&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code for this chapter is tagged &lt;code&gt;ch-15&lt;/code&gt; in the repo:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;https://github.com/aktibaba/playwright-qa-course&lt;/a&gt;&lt;/strong&gt; — see &lt;code&gt;src/setup/auth.setup.ts&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;playwright.config.ts&lt;/code&gt;, and &lt;code&gt;src/tests/ui/authenticated.spec.ts&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  A setup project that logs in once
&lt;/h2&gt;

&lt;p&gt;Playwright lets you run a &lt;strong&gt;setup project&lt;/strong&gt; — a special test file that runs &lt;em&gt;before&lt;/em&gt;&lt;br&gt;
your other tests and prepares things they need. Ours logs in and saves the session.&lt;/p&gt;

&lt;p&gt;Here's the integration twist: instead of driving the slow login &lt;em&gt;form&lt;/em&gt;, we log in&lt;br&gt;
through the &lt;strong&gt;API&lt;/strong&gt; (one fast request), then write the session into the browser's&lt;br&gt;
&lt;code&gt;localStorage&lt;/code&gt; and save it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/setup/auth.setup.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@utils/env&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SEED_USERS&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../fixtures/data.fixture&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.auth/playwright.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authenticate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SEED_USERS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playwright&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 1. Log in via the API (no clicking) and get the token.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiURL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/users/login`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBeTruthy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Write the exact session shape Inkwell reads on load, into localStorage.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Token &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;isAuth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;loggedUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;loggedUser&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Save cookies + localStorage to a file.&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;context&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;storageState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;authFile&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A couple of new things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;page.evaluate(fn, arg)&lt;/code&gt;&lt;/strong&gt; runs &lt;code&gt;fn&lt;/code&gt; &lt;em&gt;inside the browser page&lt;/em&gt; (where
&lt;code&gt;localStorage&lt;/code&gt; lives) and passes &lt;code&gt;arg&lt;/code&gt; into it. That's how we set the value the app
expects.&lt;/li&gt;
&lt;li&gt;We discovered the exact &lt;code&gt;localStorage&lt;/code&gt; shape (&lt;code&gt;loggedUser&lt;/code&gt;) by &lt;strong&gt;reading Inkwell's
source&lt;/strong&gt; — Inkwell restores the session from &lt;code&gt;localStorage.getItem("loggedUser")&lt;/code&gt; on
load. Knowing one small detail of the app under test is the essence of integration
testing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wire it up with project dependencies
&lt;/h2&gt;

&lt;p&gt;We tell Playwright "run &lt;code&gt;setup&lt;/code&gt; before &lt;code&gt;ui&lt;/code&gt;" using &lt;code&gt;dependencies&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// playwright.config.ts&lt;/span&gt;
&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;testDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./src/tests/api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiURL&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;setup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;testDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./src/setup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;testMatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/auth&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;setup&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;ts/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webURL&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ui&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;testDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./src/tests/ui&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;setup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// setup runs first → the auth file exists&lt;/span&gt;
    &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Desktop Chrome&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Opt a test into the session
&lt;/h2&gt;

&lt;p&gt;You choose &lt;strong&gt;per file&lt;/strong&gt; whether to start logged in. Our anonymous tests (home,&lt;br&gt;
locators, login) stay logged out; only this file loads the saved session, with&lt;br&gt;
&lt;code&gt;test.use&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/tests/ui/authenticated.spec.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;storageState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.auth/playwright.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;   &lt;span class="c1"&gt;// load the saved session&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;starts already logged in&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;New Article&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;navigation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;playwright&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sign up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toBeHidden&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;LoginPage&lt;/code&gt;, no form, no redirect — the test opens the app and the user is already&lt;br&gt;
there. Multiply that saving across a hundred tests.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;.auth/&lt;/code&gt; folder is git-ignored — it holds a live token and is regenerated by the&lt;br&gt;
setup project on every run, so it never goes into version control.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When to use which login
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;storageState (this chapter):&lt;/strong&gt; the default for &lt;em&gt;most&lt;/em&gt; authenticated tests — fast,
shared, set up once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging in through the form (&lt;code&gt;LoginPage&lt;/code&gt;):&lt;/strong&gt; keep it for the few tests whose
subject &lt;em&gt;is&lt;/em&gt; the login flow — you still want to prove the form itself works (the
Chapter 4 test stays exactly as it was).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next up
&lt;/h2&gt;

&lt;p&gt;We used the API to set up &lt;strong&gt;auth&lt;/strong&gt;. Next we generalise that to &lt;strong&gt;all&lt;/strong&gt; test data.&lt;br&gt;
&lt;strong&gt;Chapter 16 — Seed via API, verify in UI:&lt;/strong&gt; create an article through the API in&lt;br&gt;
milliseconds, then check it renders in the browser — the pattern that makes UI suites&lt;br&gt;
fast and reliable. Tag: &lt;code&gt;ch-16&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Following along? Star the &lt;a href="https://github.com/aktibaba/playwright-qa-course" rel="noopener noreferrer"&gt;repo&lt;/a&gt;&lt;br&gt;
and tell me how many seconds storageState shaved off your suite.&lt;/p&gt;
&lt;/blockquote&gt;

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