<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>playwright-page-object | Blog</title><description>Typed, decorator-driven Page Object Model for Playwright. Reusable, lazy locator chains in plain TypeScript classes.</description><link>https://sergeyshmakov.github.io/playwright-page-object/blog/</link><language>en</language><atom:link href="https://sergeyshmakov.github.io/playwright-page-object/blog/rss.xml" rel="self" type="application/rss+xml" /><item><title>How to Build Typed Playwright Page Objects with Decorators</title><link>https://sergeyshmakov.github.io/playwright-page-object/blog/typed-playwright-page-objects/</link><guid isPermaLink="true">https://sergeyshmakov.github.io/playwright-page-object/blog/typed-playwright-page-objects/</guid><pubDate>Sun, 07 Jun 2026 00:00:00 GMT</pubDate><dc:creator>Sergei Shmakov</dc:creator><content:encoded>&lt;p&gt;&lt;em&gt;Last updated: 2026-06-07 · by Sergei Shmakov&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;You can build typed, reusable Playwright page objects out of plain TypeScript classes, with no base class to extend and no manual locator wiring. Decorate a class with &lt;code dir=&quot;auto&quot;&gt;@RootSelector(&quot;CheckoutPage&quot;)&lt;/code&gt;, add &lt;code dir=&quot;auto&quot;&gt;accessor&lt;/code&gt; properties for the controls, and each one resolves to a lazy &lt;code dir=&quot;auto&quot;&gt;Locator&lt;/code&gt; chain that rebuilds on access. That last part kills a whole class of flaky tests, and you keep full type safety. Here’s the pain it removes and the refactor that gets you there.&lt;/p&gt;
&lt;p&gt;If your Playwright suite has more than ~20 tests, the page object layer is usually where the rot starts. Selectors get copy-pasted across files. A base &lt;code dir=&quot;auto&quot;&gt;BasePage&lt;/code&gt; class grows a method every sprint until three subclasses fight over &lt;code dir=&quot;auto&quot;&gt;this&lt;/code&gt;. Then one redesign moves a &lt;code dir=&quot;auto&quot;&gt;data-testid&lt;/code&gt; and 40 tests go red at once. The fix most teams reach for (a deeper inheritance tree) is the thing making it worse.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-do-playwright-page-objects-get-repetitive&quot;&gt;Why do Playwright page objects get repetitive?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Because every action re-types the page’s scope plus the control’s selector, and nothing shares that chain. Write &lt;code dir=&quot;auto&quot;&gt;page.getByTestId(&quot;CheckoutPage&quot;).getByTestId(&quot;PromoCodeInput&quot;)&lt;/code&gt; in one test and you’ll write it again in the next five. The page object pattern is supposed to fix this, but the common base-class version trades duplication for an inheritance hierarchy that’s just as hard to change.&lt;/p&gt;
&lt;p&gt;Here’s a normal raw-Playwright checkout spec. Watch the &lt;code dir=&quot;auto&quot;&gt;CheckoutPage&lt;/code&gt; scope repeat on every line:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { test, expect } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;@playwright/test&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test.&lt;/span&gt;&lt;span&gt;beforeEach&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; ({ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page.&lt;/span&gt;&lt;span&gt;goto&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;apply promo, then remove an item&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; ({ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;CheckoutPage&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;PromoCodeInput&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;SAVE20&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;CheckoutPage&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;button&quot;&lt;/span&gt;&lt;span&gt;, { name: &lt;/span&gt;&lt;span&gt;&quot;Apply&quot;&lt;/span&gt;&lt;span&gt; }).&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;CheckoutPage&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;CartItem_1&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Remove&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// CartItem_ is a prefix shared by every row, so raw getByTestId needs a regex.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;CheckoutPage&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;^&lt;/span&gt;&lt;span&gt;&lt;span&gt;CartItem_&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)).&lt;/span&gt;&lt;span&gt;toHaveCount&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Four lines, the same &lt;code dir=&quot;auto&quot;&gt;CheckoutPage&lt;/code&gt; scope on every one. A second test repeats all of it. When &lt;code dir=&quot;auto&quot;&gt;CheckoutPage&lt;/code&gt; becomes &lt;code dir=&quot;auto&quot;&gt;CheckoutShell&lt;/code&gt;, you edit every occurrence by hand.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-makes-reused-playwright-locators-flaky&quot;&gt;What makes reused Playwright locators flaky?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Caching a &lt;code dir=&quot;auto&quot;&gt;Locator&lt;/code&gt; reference across &lt;code dir=&quot;auto&quot;&gt;await&lt;/code&gt;s is the quiet culprit. A Playwright &lt;code dir=&quot;auto&quot;&gt;Locator&lt;/code&gt; is a lazy query, not a handle, so it’s normally safe. But the moment you wrap it in a page object field that’s assigned once in a constructor, you can capture a stale scope after the component re-renders. Rebuilding the chain on every access avoids it.&lt;/p&gt;
&lt;p&gt;This is the difference between assigning a locator once and resolving it lazily:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CheckoutPage&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Assigned once at construction. If the page re-renders or you re-navigate,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// this field still points at the chain built from the old `page` state.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;readonly&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;promoInput&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.page.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;CheckoutPage&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;PromoCodeInput&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;constructor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;readonly&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;) {}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Playwright’s own &lt;a href=&quot;https://playwright.dev/docs/locators&quot;&gt;locator guide&lt;/a&gt; recommends building locators at use time for exactly this reason. A decorator-driven &lt;code dir=&quot;auto&quot;&gt;accessor&lt;/code&gt; does that for you automatically, which is the next section.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-do-you-write-a-typed-playwright-page-object-without-a-base-class&quot;&gt;How do you write a typed Playwright page object without a base class?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Decorate a plain class. &lt;code dir=&quot;auto&quot;&gt;@RootSelector(&quot;CheckoutPage&quot;)&lt;/code&gt; scopes the class to &lt;code dir=&quot;auto&quot;&gt;page.locator(&quot;body&quot;).getByTestId(&quot;CheckoutPage&quot;)&lt;/code&gt;, and each &lt;code dir=&quot;auto&quot;&gt;@Selector&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;@SelectorByRole&lt;/code&gt; &lt;code dir=&quot;auto&quot;&gt;accessor&lt;/code&gt; resolves relative to that scope, lazily, on every read. No base class, no &lt;code dir=&quot;auto&quot;&gt;super()&lt;/code&gt;, no manual chaining. The class is a plain TypeScript class you instantiate with &lt;code dir=&quot;auto&quot;&gt;new&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here’s the same checkout flow as a page object:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; { Locator, Page } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;@playwright/test&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { RootSelector, Selector, SelectorByRole } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;playwright-page-object&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;RootSelector&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;CheckoutPage&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CheckoutPage&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;constructor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;readonly&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;) {}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Selector&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;PromoCodeInput&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;accessor&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PromoCodeInput&lt;/span&gt;&lt;span&gt;!:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Locator&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;SelectorByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;button&quot;&lt;/span&gt;&lt;span&gt;, { name: &lt;/span&gt;&lt;span&gt;&quot;Apply&quot;&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;accessor&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ApplyPromoButton&lt;/span&gt;&lt;span&gt;!:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Locator&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;applyPromoCode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;code&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.PromoCodeInput.&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;(code);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.ApplyPromoButton.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The test reads like the domain, not the DOM:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;apply promo, then remove an item&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; ({ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;checkout&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CheckoutPage&lt;/span&gt;&lt;span&gt;(page);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; checkout.&lt;/span&gt;&lt;span&gt;applyPromoCode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;SAVE20&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;PromoCodeInput&lt;/code&gt; is a real &lt;code dir=&quot;auto&quot;&gt;Locator&lt;/code&gt;, so the full Playwright API is available with no wrapper to learn. The &lt;code dir=&quot;auto&quot;&gt;accessor&lt;/code&gt; keyword is what makes this typed and lazy: the decorator intercepts the getter and returns a freshly built chain each time. When &lt;code dir=&quot;auto&quot;&gt;CheckoutPage&lt;/code&gt; becomes &lt;code dir=&quot;auto&quot;&gt;CheckoutShell&lt;/code&gt;, you change one string in one place.&lt;/p&gt;
&lt;p&gt;The decorator picks the output shape from how you declare the property. Type it as &lt;code dir=&quot;auto&quot;&gt;Locator&lt;/code&gt; for a raw locator. Initialize it with &lt;code dir=&quot;auto&quot;&gt;new PageObject()&lt;/code&gt; when you want wait helpers and &lt;code dir=&quot;auto&quot;&gt;.expect()&lt;/code&gt;. Pass a control class as the second argument when you already have one. All three styles coexist in the same class, covered in &lt;a href=&quot;https://sergeyshmakov.github.io/playwright-page-object/getting-started/choosing-a-style/&quot;&gt;Choosing a Style&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;do-i-have-to-rewrite-my-whole-test-suite-to-adopt-this&quot;&gt;Do I have to rewrite my whole test suite to adopt this?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;No. The page object is a plain class, so you introduce it in one spec file and leave the rest of the suite untouched. There’s no global config, no custom test runner, and no base class every page must inherit. Migrate the noisiest page first, measure whether it pays back, then move on.&lt;/p&gt;
&lt;p&gt;A practical three-step adoption path, taken from the &lt;a href=&quot;https://sergeyshmakov.github.io/playwright-page-object/guides/incremental-adoption/&quot;&gt;incremental adoption guide&lt;/a&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Duplicate locator chains across tests&lt;/strong&gt; — wrap them in a plain class with &lt;code dir=&quot;auto&quot;&gt;@RootSelector&lt;/code&gt; and raw &lt;code dir=&quot;auto&quot;&gt;Locator&lt;/code&gt; accessors. Stop here for most pages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The same methods called on the same accessor&lt;/strong&gt; (&lt;code dir=&quot;auto&quot;&gt;fill&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;click&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;expect&lt;/code&gt;) — extract a &lt;a href=&quot;https://sergeyshmakov.github.io/playwright-page-object/guides/custom-controls/&quot;&gt;custom control&lt;/a&gt; so the calls live in one place.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The same wait or assertion code repeating&lt;/strong&gt; — switch that accessor to the built-in &lt;a href=&quot;https://sergeyshmakov.github.io/playwright-page-object/guides/built-in-pom/&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;PageObject&lt;/code&gt;&lt;/a&gt; for &lt;code dir=&quot;auto&quot;&gt;waitVisible()&lt;/code&gt;, soft assertions, and list filters.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Setup is one dev dependency and a TypeScript target of &lt;code dir=&quot;auto&quot;&gt;ES2015&lt;/code&gt; or higher. No &lt;code dir=&quot;auto&quot;&gt;experimentalDecorators&lt;/code&gt; flag, because the library uses standard &lt;a href=&quot;https://github.com/tc39/proposal-decorators&quot;&gt;ECMAScript decorators&lt;/a&gt; and the &lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html&quot;&gt;TypeScript 5.0 &lt;code dir=&quot;auto&quot;&gt;accessor&lt;/code&gt; keyword&lt;/a&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-D&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;playwright-page-object&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;when-should-you-not-use-this-pattern&quot;&gt;When should you not use this pattern?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Skip it when the abstraction costs more than the duplication it removes. A one-off script or a five-test smoke suite reads fine with a couple of &lt;code dir=&quot;auto&quot;&gt;page.getByTestId(...)&lt;/code&gt; calls inline. Three similar &lt;code dir=&quot;auto&quot;&gt;.fill()&lt;/code&gt; calls are not duplication. Five matching wait blocks across files are.&lt;/p&gt;
&lt;p&gt;A short honest list of where this falls down:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tiny or throwaway suites.&lt;/strong&gt; Inline locators are clearer than a class you read once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pages with no repeated structure.&lt;/strong&gt; If nothing recurs, there’s nothing to factor out.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A team already happy with a different POM convention.&lt;/strong&gt; Consistency across a suite beats a marginally nicer pattern in one corner of it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The library is a locator-composition layer, not a testing framework. It adds typed page objects to the Playwright runner you already use. It does not replace your runner, your config, or your assertions.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-does-it-work-with-playwright-fixtures&quot;&gt;How does it work with Playwright fixtures?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Wire instantiation once with &lt;code dir=&quot;auto&quot;&gt;createFixtures&lt;/code&gt;, then every test receives ready-built page objects as fixture arguments. No &lt;code dir=&quot;auto&quot;&gt;new CheckoutPage(page)&lt;/code&gt; in each test, and the types flow through.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { test &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; base } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;@playwright/test&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { createFixtures } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;playwright-page-object&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; base.&lt;/span&gt;&lt;span&gt;extend&lt;/span&gt;&lt;span&gt;&amp;#x3C;{ &lt;/span&gt;&lt;span&gt;checkoutPage&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CheckoutPage&lt;/span&gt;&lt;span&gt; }&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;createFixtures&lt;/span&gt;&lt;span&gt;({ checkoutPage: CheckoutPage }),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;apply promo code&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; ({ &lt;/span&gt;&lt;span&gt;checkoutPage&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; checkoutPage.&lt;/span&gt;&lt;span&gt;applyPromoCode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;SAVE20&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;See the &lt;a href=&quot;https://sergeyshmakov.github.io/playwright-page-object/guides/fixtures/&quot;&gt;Fixtures guide&lt;/a&gt; for factory functions and shared configuration. One more thing worth mentioning: the package ships an &lt;a href=&quot;https://sergeyshmakov.github.io/playwright-page-object/ai-tooling/agent-skills/&quot;&gt;Agent Skill and a Context7 entry&lt;/a&gt; so AI coding assistants generate correct page objects against the current API instead of guessing at the decorator names.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;try-it&quot;&gt;Try it&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The &lt;a href=&quot;https://sergeyshmakov.github.io/playwright-page-object/getting-started/quick-start/&quot;&gt;Quick Start&lt;/a&gt; is a complete working example in under five minutes. If it saves you an afternoon of selector wrangling, the easiest way to say thanks is a star on &lt;a href=&quot;https://github.com/sergeyshmakov/playwright-page-object&quot;&gt;GitHub&lt;/a&gt; and &lt;code dir=&quot;auto&quot;&gt;npm i -D playwright-page-object&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;is-playwright-page-object-a-framework-or-a-library&quot;&gt;Is playwright-page-object a framework or a library?&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;A library, and a small one. It adds decorators and a couple of optional base classes to the standard &lt;code dir=&quot;auto&quot;&gt;@playwright/test&lt;/code&gt; runner. There’s no bespoke test command, no config file it owns, and no runtime it boots. Your existing Playwright setup keeps working; you opt into the page object layer file by file.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;does-it-work-with-the-standard-playwrighttest-runner&quot;&gt;Does it work with the standard @playwright/test runner?&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Yes. Page objects are plain TypeScript classes. Instantiate one with &lt;code dir=&quot;auto&quot;&gt;new CheckoutPage(page)&lt;/code&gt; inside a test, or wire it through &lt;code dir=&quot;auto&quot;&gt;createFixtures&lt;/code&gt; so it arrives as a fixture argument. Nothing about the runner, reporters, or &lt;code dir=&quot;auto&quot;&gt;playwright.config.ts&lt;/code&gt; changes.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;what-is-the-typescript-accessor-keyword&quot;&gt;What is the TypeScript &lt;code dir=&quot;auto&quot;&gt;accessor&lt;/code&gt; keyword?&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;accessor&lt;/code&gt; is a TypeScript 5.0 (and ECMAScript Stage-3) feature that declares an auto-implemented getter/setter pair backed by a private field. Decorators can wrap that getter, which is how this library returns a freshly resolved &lt;code dir=&quot;auto&quot;&gt;Locator&lt;/code&gt; on every read. The &lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html&quot;&gt;TypeScript 5.0 notes&lt;/a&gt; cover the syntax.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;do-i-need-experimentaldecorators-in-my-tsconfig&quot;&gt;Do I need &lt;code dir=&quot;auto&quot;&gt;experimentalDecorators&lt;/code&gt; in my tsconfig?&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;No. The library uses standard ECMAScript decorators, supported natively in TypeScript 5.0+, so the legacy &lt;code dir=&quot;auto&quot;&gt;experimentalDecorators&lt;/code&gt; flag stays off. Target &lt;code dir=&quot;auto&quot;&gt;ES2015&lt;/code&gt; or higher (&lt;code dir=&quot;auto&quot;&gt;ES2022&lt;/code&gt; is a good default) and you’re set. See &lt;a href=&quot;https://sergeyshmakov.github.io/playwright-page-object/getting-started/installation/&quot;&gt;Installation&lt;/a&gt; for the full requirements.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;can-i-use-playwright-page-objects-without-inheritance&quot;&gt;Can I use Playwright page objects without inheritance?&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Yes, and that’s the default. A plain class with &lt;code dir=&quot;auto&quot;&gt;@RootSelector&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;accessor&lt;/code&gt; properties needs no base class. If you drop &lt;code dir=&quot;auto&quot;&gt;@RootSelector&lt;/code&gt;, selectors chain from &lt;code dir=&quot;auto&quot;&gt;page.locator(&quot;body&quot;)&lt;/code&gt; instead. The built-in &lt;code dir=&quot;auto&quot;&gt;PageObject&lt;/code&gt; base exists only for the cases that want its wait helpers, and it’s opt-in.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;does-this-reduce-flaky-playwright-tests&quot;&gt;Does this reduce flaky Playwright tests?&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;It removes one specific cause: stale cached locator references. Because accessors rebuild the chain on every access, a page re-render between two &lt;code dir=&quot;auto&quot;&gt;await&lt;/code&gt;s can’t leave you holding an outdated handle. It won’t fix flakiness from application races or network timing, which is what Playwright’s &lt;a href=&quot;https://playwright.dev/docs/actionability&quot;&gt;auto-waiting&lt;/a&gt; is for.&lt;/p&gt;</content:encoded><category>Playwright</category><category>TypeScript</category><category>Page Object Model</category><category>Test Automation</category><category>E2E</category></item></channel></rss>