<?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: Mushtaq Mohd Rejowan</title>
    <description>The latest articles on DEV Community by Mushtaq Mohd Rejowan (@mmrejowan).</description>
    <link>https://dev.to/mmrejowan</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%2F4097855%2F5e5e164b-16d4-4309-9a50-d963f100c2b9.jpg</url>
      <title>DEV Community: Mushtaq Mohd Rejowan</title>
      <link>https://dev.to/mmrejowan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mmrejowan"/>
    <language>en</language>
    <item>
      <title>Building a Product-Centric Playwright Automation Framework with Functional Programming</title>
      <dc:creator>Mushtaq Mohd Rejowan</dc:creator>
      <pubDate>Thu, 27 Aug 2026 17:32:27 +0000</pubDate>
      <link>https://dev.to/mmrejowan/building-a-product-centric-playwright-automation-framework-with-functional-programming-3906</link>
      <guid>https://dev.to/mmrejowan/building-a-product-centric-playwright-automation-framework-with-functional-programming-3906</guid>
      <description>&lt;p&gt;Modern UI automation can easily become difficult to maintain.&lt;/p&gt;

&lt;p&gt;A test suite may start with a few simple scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;login → click → fill → submit → verify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few years later, it can turn into hundreds or thousands of tests spread across page objects, utilities, fixtures, helpers, data files, and duplicated workflows.&lt;/p&gt;

&lt;p&gt;At that point, adding a new feature doesn't necessarily mean writing a new test.&lt;/p&gt;

&lt;p&gt;It means figuring out &lt;strong&gt;where the existing logic belongs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This was one of the problems I wanted to solve while building the &lt;strong&gt;Redemption Framework&lt;/strong&gt; — a modular Playwright automation framework designed around the product itself rather than around technical layers.&lt;/p&gt;

&lt;p&gt;The result is a framework built around three major ideas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Product-centric feature slicing&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Functional programming principles&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A strict 5-file architecture contract&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Problem With Traditional Automation Structures
&lt;/h2&gt;

&lt;p&gt;A common automation project eventually evolves into something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pages/
utils/
tests/
fixtures/
helpers/
data/
services/
common/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initially, this looks clean.&lt;/p&gt;

&lt;p&gt;But imagine you're working on a feature called &lt;strong&gt;Nomination&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Its selectors might be in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pages/NominationPage.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its test data might be in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data/nomination.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its reusable workflow might be in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;utils/nominationHelper.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its fixture might be somewhere else.&lt;/p&gt;

&lt;p&gt;And the actual test could live under:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tests/rewards/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The feature is logically one thing, but physically distributed across the repository.&lt;/p&gt;

&lt;p&gt;As the application grows, this creates a cognitive problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The code structure stops resembling the product structure.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I wanted the framework to make the opposite choice.&lt;/p&gt;




&lt;h1&gt;
  
  
  Designing Around the Product
&lt;/h1&gt;

&lt;p&gt;The framework uses &lt;strong&gt;feature slicing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of organizing code primarily by technical responsibility, functionality is grouped according to the product.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;modules/
├── auth/
├── dashboard/
├── rewards/
├── redemption/
├── site-admin/
├── wish-anniversary/
└── mobile/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside a feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;modules/
└── rewards/
    └── nomination/
        ├── locators.js
        ├── actions.js
        ├── workflow.js
        ├── fixture.js
        └── data.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything required to understand and maintain the feature is close together.&lt;/p&gt;

&lt;p&gt;This gives the repository a useful property:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The architecture mirrors the application's domain.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If I need to work on nomination, I don't need to navigate through five unrelated top-level directories.&lt;/p&gt;

&lt;p&gt;I go to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;modules/rewards/nomination/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the feature is there.&lt;/p&gt;




&lt;h1&gt;
  
  
  The 5-File Architecture Contract
&lt;/h1&gt;

&lt;p&gt;The most important design decision in the framework is the &lt;strong&gt;5-File Rule&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every module follows the same structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;locators.js
actions.js
workflow.js
fixture.js
data.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each file has one responsibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;locators.js&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This file contains selector definitions.&lt;/p&gt;

&lt;p&gt;It intentionally contains &lt;strong&gt;no imports&lt;/strong&gt; and no business logic.&lt;/p&gt;

&lt;p&gt;For example:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nominationLocators&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;nominateButton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-testid="nominate-button"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;recipientInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-testid="recipient-input"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;submitButton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-testid="submit-button"]&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;The idea is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Selector knowledge should remain selector knowledge.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the UI changes, I should be able to inspect the locator layer without navigating through business logic.&lt;/p&gt;




&lt;h1&gt;
  
  
  2. &lt;code&gt;actions.js&lt;/code&gt;
&lt;/h1&gt;

&lt;p&gt;Actions represent interactions with the application.&lt;/p&gt;

&lt;p&gt;But there is an important architectural rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every action returns a &lt;code&gt;Result&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of allowing arbitrary exceptions to propagate through every layer, actions communicate success or failure explicitly.&lt;/p&gt;

&lt;p&gt;Conceptually:&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;ok&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&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;or:&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;selectRecipient&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Recipient was not available&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;This provides a predictable contract between the action and workflow layers.&lt;/p&gt;

&lt;p&gt;It also makes failures easier to reason about.&lt;/p&gt;




&lt;h1&gt;
  
  
  3. &lt;code&gt;workflow.js&lt;/code&gt;
&lt;/h1&gt;

&lt;p&gt;Actions are useful individually.&lt;/p&gt;

&lt;p&gt;But real user journeys require multiple actions.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Open nomination
      ↓
Choose award
      ↓
Select recipient
      ↓
Enter message
      ↓
Submit
      ↓
Verify result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of putting this entire sequence into the test, the workflow layer composes these operations.&lt;/p&gt;

&lt;p&gt;The framework uses a &lt;code&gt;pipe()&lt;/code&gt; abstraction to create asynchronous pipelines.&lt;/p&gt;

&lt;p&gt;Conceptually:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nominationWorkflow&lt;/span&gt; &lt;span class="o"&gt;=&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="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;openModal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;chooseType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;addRecipients&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;pickAward&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;submit&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="nx"&gt;params&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the business flow immediately visible.&lt;/p&gt;

&lt;p&gt;A developer reading the workflow can understand &lt;strong&gt;what the user journey is&lt;/strong&gt; without reading every Playwright command.&lt;/p&gt;




&lt;h1&gt;
  
  
  4. &lt;code&gt;fixture.js&lt;/code&gt;
&lt;/h1&gt;

&lt;p&gt;Fixtures provide the environment required by the feature.&lt;/p&gt;

&lt;p&gt;Playwright's &lt;code&gt;test.extend()&lt;/code&gt; is used to expose module-specific setup and authenticated pages.&lt;/p&gt;

&lt;p&gt;For example, instead of every test repeatedly implementing authentication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;login
wait
navigate
configure tenant
prepare environment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the fixture can provide the correct context.&lt;/p&gt;

&lt;p&gt;This keeps tests focused on the behavior being validated.&lt;/p&gt;




&lt;h1&gt;
  
  
  5. &lt;code&gt;data.js&lt;/code&gt;
&lt;/h1&gt;

&lt;p&gt;Test data and environment-specific parameters belong here.&lt;/p&gt;

&lt;p&gt;This can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test credentials&lt;/li&gt;
&lt;li&gt;Scenarios&lt;/li&gt;
&lt;li&gt;Tenant configuration&lt;/li&gt;
&lt;li&gt;Environment parameters&lt;/li&gt;
&lt;li&gt;Expected values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Separating data from workflows prevents the test implementation from becoming a mixture of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;business logic + selectors + credentials + test scenarios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Functional Programming as the Core
&lt;/h1&gt;

&lt;p&gt;The framework doesn't use functional programming simply because it sounds interesting.&lt;/p&gt;

&lt;p&gt;The goal is to make automation behavior &lt;strong&gt;composable and predictable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Three concepts are particularly important:&lt;/p&gt;

&lt;h3&gt;
  
  
  Result
&lt;/h3&gt;

&lt;p&gt;Actions return:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ok()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fail()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pipe
&lt;/h3&gt;

&lt;p&gt;Workflows compose operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;action → action → action → action
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Currying / Factories
&lt;/h3&gt;

&lt;p&gt;Playwright's &lt;code&gt;page&lt;/code&gt; is bound once.&lt;/p&gt;

&lt;p&gt;For example:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;A&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createNominationActions&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then individual actions can be invoked with their parameters:&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addRecipient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user@vc.com&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;The underlying idea is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Bind the environment once, then work with small composable functions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This makes individual pieces easier to reuse and reason about.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why the Result Pattern Matters in Automation
&lt;/h1&gt;

&lt;p&gt;Traditional Playwright code often relies heavily on exceptions:&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="k"&gt;try&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;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&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;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// handle failure&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When this pattern is repeated across hundreds of workflows, error handling can become inconsistent.&lt;/p&gt;

&lt;p&gt;The Result abstraction provides a common contract.&lt;/p&gt;

&lt;p&gt;A workflow can evaluate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Did the previous step succeed?
        ↓
Yes → continue
No  → propagate structured failure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And at the test boundary, the result can be unwrapped:&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="nf"&gt;unwrap&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;workflow&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;params&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the lower layers explicit while allowing the test to fail naturally when a workflow cannot complete.&lt;/p&gt;




&lt;h1&gt;
  
  
  A Framework Is More Than Test Scripts
&lt;/h1&gt;

&lt;p&gt;One of the lessons from building a larger automation framework is that &lt;strong&gt;test execution is only one part of the problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A mature automation system also needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Observability&lt;/li&gt;
&lt;li&gt;Reporting&lt;/li&gt;
&lt;li&gt;Debugging&lt;/li&gt;
&lt;li&gt;Environment management&lt;/li&gt;
&lt;li&gt;Data preparation&lt;/li&gt;
&lt;li&gt;Architecture enforcement&lt;/li&gt;
&lt;li&gt;Maintainability&lt;/li&gt;
&lt;li&gt;CI integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The framework therefore includes several supporting systems.&lt;/p&gt;




&lt;h1&gt;
  
  
  Observability
&lt;/h1&gt;

&lt;p&gt;The framework uses &lt;strong&gt;Allure reporting&lt;/strong&gt; to provide detailed test reports.&lt;/p&gt;

&lt;p&gt;Playwright traces are also enabled for failed CI tests.&lt;/p&gt;

&lt;p&gt;This creates a useful debugging path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Test failure
     ↓
Allure report
     ↓
Playwright trace
     ↓
DOM / network / screenshots
     ↓
Root cause
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a large automation suite, this is extremely valuable.&lt;/p&gt;

&lt;p&gt;A failed test should ideally answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What failed, where did it fail, and what was the application doing at that moment?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Historical Test Intelligence
&lt;/h1&gt;

&lt;p&gt;The framework also includes a custom dashboard built using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;React
   +
Node.js
   +
SQLite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The purpose isn't just to display pass/fail results.&lt;/p&gt;

&lt;p&gt;Historical test data can help identify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flaky tests&lt;/li&gt;
&lt;li&gt;Recurring failures&lt;/li&gt;
&lt;li&gt;Regression patterns&lt;/li&gt;
&lt;li&gt;Module stability&lt;/li&gt;
&lt;li&gt;Historical trends&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This shifts automation from simply being a &lt;strong&gt;test execution mechanism&lt;/strong&gt; toward being a &lt;strong&gt;quality intelligence system&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  Real Backend and Frontend Validation
&lt;/h1&gt;

&lt;p&gt;Another important architectural decision is avoiding assumptions about the application under test.&lt;/p&gt;

&lt;p&gt;For new tests, the framework can be connected to the actual backend and frontend repositories.&lt;/p&gt;

&lt;p&gt;This allows automation development to validate things such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API endpoints
        ↓
Backend source

UI selectors
        ↓
Frontend source
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rather than inventing an endpoint or guessing a locator, the automation layer can be validated against the actual implementation.&lt;/p&gt;

&lt;p&gt;This is particularly useful in a large product where APIs and UI structures evolve continuously.&lt;/p&gt;




&lt;h1&gt;
  
  
  Database-Backed Test Preparation
&lt;/h1&gt;

&lt;p&gt;Some tests require realistic data.&lt;/p&gt;

&lt;p&gt;For UAT-backed scenarios, the framework can connect to remote MySQL databases through an SSH tunnel.&lt;/p&gt;

&lt;p&gt;This is useful for workflows where test preparation depends on actual tenant data rather than static fixtures.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Test
 ↓
Prepare tenant data
 ↓
Database
 ↓
Application
 ↓
Playwright workflow
 ↓
Validation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows certain tests to operate closer to real production-like conditions.&lt;/p&gt;




&lt;h1&gt;
  
  
  From Product Hierarchy to Automation Hierarchy
&lt;/h1&gt;

&lt;p&gt;The resulting architecture looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Product
│
├── Authentication
│
├── Dashboard
│
├── Rewards
│   ├── Nomination
│   ├── Appreciation
│   └── Feed
│
├── Redemption
│   ├── Gift Cards
│   ├── Merchandise
│   ├── Vouchers
│   └── Wallet
│
├── Site Administration
│
├── Anniversary / Wishes
│
└── Mobile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And each feature follows the same internal contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Feature
│
├── locators.js
├── actions.js
├── workflow.js
├── fixture.js
└── data.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This consistency becomes increasingly valuable as the number of modules grows.&lt;/p&gt;




&lt;h1&gt;
  
  
  Architecture Guardrails
&lt;/h1&gt;

&lt;p&gt;A convention is only useful if it can be enforced.&lt;/p&gt;

&lt;p&gt;That's why the framework also uses architectural guardrails and specialized tooling to validate things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Module structure&lt;/li&gt;
&lt;li&gt;Locator rules&lt;/li&gt;
&lt;li&gt;Action contracts&lt;/li&gt;
&lt;li&gt;Cross-repository assumptions&lt;/li&gt;
&lt;li&gt;Coverage gaps&lt;/li&gt;
&lt;li&gt;Scaffolding conventions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repository also contains task-specific AI skills for activities such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scaffolding
Architecture validation
Locator auditing
Coverage analysis
Cross-repository validation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal is not to make developers memorize every architectural rule.&lt;/p&gt;

&lt;p&gt;The goal is to make the architecture &lt;strong&gt;executable&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  What a Test Should Look Like
&lt;/h1&gt;

&lt;p&gt;Ideally, the test itself becomes relatively small.&lt;/p&gt;

&lt;p&gt;Instead of exposing every implementation detail:&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="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;click&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;fill&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;waitForSelector&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;click&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="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the test can express the business behavior:&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="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user can nominate a colleague&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;nomination&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="nf"&gt;unwrap&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;nomination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user@vc.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;award&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Great Work&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Excellent work!&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;The implementation details stay inside the module.&lt;/p&gt;

&lt;p&gt;The test describes &lt;strong&gt;what is being validated&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That distinction becomes increasingly important as test suites grow.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Architecture in One Diagram
&lt;/h1&gt;

&lt;p&gt;The overall design can be thought of as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    PRODUCT
                       │
                       ▼
              ┌─────────────────┐
              │ Feature Module  │
              └────────┬────────┘
                       │
          ┌────────────┼────────────┐
          ▼            ▼            ▼
      Locators      Actions       Data
                       │
                       ▼
                   Workflow
                       │
                       ▼
                    Fixture
                       │
                       ▼
                   Playwright
                       │
                       ▼
                 Application
                       │
          ┌────────────┼────────────┐
          ▼            ▼            ▼
       Allure       Trace       Dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer has a defined responsibility.&lt;/p&gt;




&lt;h1&gt;
  
  
  What I Like About This Approach
&lt;/h1&gt;

&lt;p&gt;The biggest advantage isn't that there are exactly five files.&lt;/p&gt;

&lt;p&gt;The important part is the &lt;strong&gt;contract&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When developers join the project, they don't need to reverse-engineer every module.&lt;/p&gt;

&lt;p&gt;They know:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Selectors → locators.js

Interactions → actions.js

Business flows → workflow.js

Environment/setup → fixture.js

Test data → data.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That predictability reduces the mental overhead of working on the framework.&lt;/p&gt;




&lt;h1&gt;
  
  
  Trade-offs
&lt;/h1&gt;

&lt;p&gt;This architecture isn't universally better.&lt;/p&gt;

&lt;p&gt;Strict architecture introduces constraints.&lt;/p&gt;

&lt;p&gt;Sometimes a five-file contract can feel excessive for a tiny feature.&lt;/p&gt;

&lt;p&gt;Functional programming can also introduce a learning curve for developers who are more comfortable with traditional object-oriented Page Object Models.&lt;/p&gt;

&lt;p&gt;And not every application needs database-backed preparation, custom dashboards, or cross-repository validation.&lt;/p&gt;

&lt;p&gt;The important lesson is that architecture should solve a real problem.&lt;/p&gt;

&lt;p&gt;In this case, the problems were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Growing automation complexity&lt;/li&gt;
&lt;li&gt;Feature ownership becoming unclear&lt;/li&gt;
&lt;li&gt;Repeated workflows&lt;/li&gt;
&lt;li&gt;Inconsistent error handling&lt;/li&gt;
&lt;li&gt;Difficult debugging&lt;/li&gt;
&lt;li&gt;UI/backend changes causing automation drift&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture was designed around those problems.&lt;/p&gt;




&lt;h1&gt;
  
  
  Lessons Learned
&lt;/h1&gt;

&lt;p&gt;After working with this style of framework, a few principles stand out.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Organize around the product
&lt;/h3&gt;

&lt;p&gt;Automation code should be easy to navigate from a product perspective.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Make responsibilities explicit
&lt;/h3&gt;

&lt;p&gt;A file should have a reason to exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Treat automation as software engineering
&lt;/h3&gt;

&lt;p&gt;Large test suites deserve the same architectural discipline as application code.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Make failures observable
&lt;/h3&gt;

&lt;p&gt;A failed test without useful diagnostics is expensive.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Prefer composition over duplication
&lt;/h3&gt;

&lt;p&gt;Reusable actions and workflows can dramatically reduce repeated automation logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Enforce conventions
&lt;/h3&gt;

&lt;p&gt;Documentation alone doesn't guarantee architecture.&lt;/p&gt;

&lt;p&gt;Guardrails make conventions sustainable.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Keep tests expressive
&lt;/h3&gt;

&lt;p&gt;The test should communicate the behavior being validated, not every implementation detail required to perform it.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;A Playwright framework doesn't need to be complicated to be effective.&lt;/p&gt;

&lt;p&gt;But when an automation suite grows into a significant engineering system, structure becomes critical.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Redemption Framework&lt;/strong&gt; is an attempt to solve that problem through a combination of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Product-centric architecture
          +
Functional programming
          +
Composable workflows
          +
Strict module contracts
          +
Observability
          +
Architecture guardrails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important idea isn't the &lt;code&gt;5-File Rule&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's the principle behind it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Automation architecture should make the product easier to understand, test, and maintain—not make the automation framework itself the center of the universe.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the direction I'm continuing to explore with Playwright, functional programming, and modern quality engineering.&lt;/p&gt;

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