<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[BDR methodology]]></title><description><![CDATA[BDR methodology]]></description><link>https://bdr-methodology.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/69f087e210a70b33358ff6ee/cef45d5e-90b3-4de8-8536-502c5a1e406f.jpg</url><title>BDR methodology</title><link>https://bdr-methodology.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 14 Oct 2026 07:03:27 GMT</lastBuildDate><atom:link href="https://bdr-methodology.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Beyond Cucumber: A Type-Safe 4-Layer BDD Architecture with Playwright]]></title><description><![CDATA[If you want the story behind why BDR exists — I wrote about it this Article. This article is the technical deep dive: architecture, real code, and implementation details.

Note: BDR (Behavior-Driven L]]></description><link>https://bdr-methodology.hashnode.dev/beyond-cucumber-a-type-safe-4-layer-bdd-architecture-with-playwright</link><guid isPermaLink="true">https://bdr-methodology.hashnode.dev/beyond-cucumber-a-type-safe-4-layer-bdd-architecture-with-playwright</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[playwright]]></category><category><![CDATA[QA automation]]></category><category><![CDATA[qa testing]]></category><category><![CDATA[QA]]></category><category><![CDATA[Playwright Automation ]]></category><category><![CDATA[bdd]]></category><category><![CDATA[bdd cucumber framework]]></category><category><![CDATA[bdr]]></category><dc:creator><![CDATA[Dmitry]]></dc:creator><pubDate>Tue, 28 Apr 2026 10:29:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69f087e210a70b33358ff6ee/b41c3467-33b1-44ab-a196-b9c9c7b36220.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you want the story behind why BDR exists — I wrote about it <a href="../beyond-cucumber-base">this Article</a>. This article is the technical deep dive: architecture, real code, and implementation details.</p>
<blockquote>
<p><strong>Note:</strong> BDR (Behavior-Driven Living Requirements) is my own architectural approach to organizing Playwright tests — a Cucumber-free alternative to BDD that I designed and documented at <a href="../../concepts/manifesto">bdr-methodology.dev</a>.</p>
</blockquote>
<hr />
<h2>The problem with Cucumber in one sentence</h2>
<p>You write your scenario in a <code>.feature</code> file, then wire it to TypeScript in a step definition file, and your IDE has no idea they're connected. Rename a method — nothing breaks at compile time. Run your tests — everything breaks at runtime.</p>
<p>BDR solves this by keeping Given/When/Then <strong>directly in TypeScript</strong>. Same BDD philosophy, zero translation layer.</p>
<hr />
<h2>The 4-Layer Architecture</h2>
<p>BDR enforces strict separation of concerns across 4 layers. Each layer has one job:</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Responsibility</th>
<th>Example</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Specification</strong></td>
<td>Business intent. Reads like a user story.</td>
<td><code>test('User can log in')</code></td>
</tr>
<tr>
<td><strong>Scenario</strong></td>
<td>Given/When/Then steps</td>
<td><code>BDR.When('User enters credentials', ...)</code></td>
</tr>
<tr>
<td><strong>Action (Flow)</strong></td>
<td>Reusable business logic</td>
<td><code>loginFlow.submitCredentials(user)</code></td>
</tr>
<tr>
<td><strong>Technical (POM)</strong></td>
<td>Raw selectors and Playwright interactions</td>
<td><code>page.getByLabel('Username').fill(value)</code></td>
</tr>
</tbody></table>
<p>The rule: <strong>no layer reaches down more than one level.</strong> Your Specification layer never touches selectors. Your POM layer never knows about business logic.</p>
<p>This means if you switch from Playwright to Selenium tomorrow — only the Technical layer changes. Business scenarios stay untouched.</p>
<hr />
<h2>The BDR Step Builder</h2>
<p>Instead of Gherkin strings wired to step definitions, BDR gives you a fluent API:</p>
<pre><code class="language-typescript">// bdr/bdr.ts
const createStep = (prefix: string) =&gt; {
  return async (name: string, ...args: any[]): Promise&lt;any&gt; =&gt; {
    const body = args.pop();

    if (typeof body !== 'function') {
      throw new Error(`BDR.${prefix}: Last argument must be a function`);
    }

    const stepName = `\({prefix.toUpperCase()}: \){formatTitle(name, args)}`;
    const executionFn = async () =&gt; (body.length &gt; 0 ? body(...args) : body());

    return test.step(stepName, executionFn);
  };
};

export const BDR = {
  Given: createStep('Given'),
  When: createStep('When'),
  Then: createStep('Then'),
  And: createStep('And'),
};
</code></pre>
<p>Usage in a test:</p>
<pre><code class="language-typescript">test('User can log in with valid credentials', async ({ loginPage, page }) =&gt; {
  await BDR.Given('the user is on the login page', async () =&gt; {
    await loginPage.goto();
  });

  await BDR.When('the user enters valid credentials', async () =&gt; {
    await loginPage.login('testuser', 'password123');
  });

  await BDR.Then('the user is redirected to the dashboard', async () =&gt; {
    await expect(page).toHaveURL('/dashboard');
  });
});
</code></pre>
<p>Your IDE fully understands this. <code>loginPage.login</code> is a real TypeScript method — rename it and the IDE updates every reference instantly.</p>
<hr />
<h2>Smart title interpolation with formatTitle</h2>
<p>Step titles support argument interpolation — so your reports are always meaningful:</p>
<pre><code class="language-typescript">// bdr/utils.ts
export function formatTitle(template: string, args: any[]): string {
  let argIndex = 0;
  return template.replace(/{(\d+|[\w.]*)}/g, (match, key) =&gt; {
    if (key === '') {
      return argIndex &lt; args.length ? String(args[argIndex++]) : match;
    }
    const parts = key.split('.');
    const index = parseInt(parts[0], 10);
    if (!isNaN(index) &amp;&amp; index &gt;= 0 &amp;&amp; index &lt; args.length) {
      let value = args[index];
      for (let i = 1; i &lt; parts.length; i++) {
        if (value &amp;&amp; typeof value === 'object') {
          value = value[parts[i]];
        } else return match;
      }
      return value !== undefined ? String(value) : match;
    }
    return match;
  });
}
</code></pre>
<p>This supports three interpolation modes:</p>
<pre><code class="language-typescript">// Index-based
formatTitle('Login as {0}', ['admin']);
// → "Login as admin"

// Sequential
formatTitle('Filter by {} and {}', ['Electronics', 'price']);
// → "Filter by Electronics and price"

// Nested property access
formatTitle('Welcome {0.user.name}', [{ user: { name: 'John' } }]);
// → "Welcome John"
</code></pre>
<p>Your Allure report shows <code>WHEN: Filter by Electronics and price</code> — not a generic string, but a meaningful description of what actually happened.</p>
<hr />
<h2>The @Step Decorator for Flow classes</h2>
<p>For reusable business flows, BDR provides a <code>@Step</code> decorator that wraps class methods automatically:</p>
<pre><code class="language-typescript">// bdr/decorators.ts
export function Step(title: string, options: StepOptions = {}) {
  return function (...args: any[]) {
    const wrapMethodInStep = (originalMethod: Function) =&gt; {
      return async function (this: any, ...methodArgs: any[]) {
        const stepName = formatTitle(title, methodArgs);
        return test.step(stepName, async () =&gt; originalMethod.apply(this, methodArgs));
      };
    };

    // Supports both Legacy and Stage 3 decorators
    if (typeof args[1] === 'object' &amp;&amp; 'kind' in args[1]) {
      return wrapMethodInStep(args[0]); // Stage 3
    }
    if (typeof args[1] === 'string') {
      const descriptor = args[2];
      descriptor.value = wrapMethodInStep(descriptor.value);
      return descriptor; // Legacy
    }
  };
}
</code></pre>
<p>Usage in a Flow class:</p>
<pre><code class="language-typescript">// flows/ProductFlow.ts
export class ProductFlow {
  constructor(private products: Product[]) {}

  @Step('GIVEN: I have a product catalog with {0} items')
  async logProducts(count: number) {
    await attachTable('Source Product Catalog', this.products);
  }

  @Step('WHEN: I filter products by category "{0}"')
  async filterByCategory(category: string) {
    const filtered = this.products.filter((p) =&gt; p.category === category);
    await attachTable(`Filtered Products: ${category}`, filtered);
    return filtered;
  }

  @Step('THEN: The total price should be calculated')
  async calculateTotalPrice() {
    const total = this.products.reduce((sum, p) =&gt; sum + p.price, 0);
    await attachTable('Price Summary', [
      { 'Total Items': this.products.length, 'Total Price': `$${total.toFixed(2)}` },
    ]);
    return total;
  }
}
</code></pre>
<p>Every public method is automatically wrapped in a named <code>test.step</code>. The report shows exactly which business action was running when something failed.</p>
<hr />
<h2>Fixtures — the glue of the architecture</h2>
<p>Fixtures inject Page Objects and Flows into tests automatically. No manual instantiation, no shared state between tests:</p>
<pre><code class="language-typescript">// fixtures/index.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pom/LoginPage';
import { ProductsPage } from '../pom/ProductsPage';

type MyFixtures = {
  loginPage: LoginPage;
  productsPage: ProductsPage;
};

export const test = base.extend&lt;MyFixtures&gt;({
  loginPage: async ({ page }, use) =&gt; {
    await use(new LoginPage(page));
  },
  productsPage: async ({ page }, use) =&gt; {
    await use(new ProductsPage(page));
  },
});

export { expect } from '@playwright/test';
</code></pre>
<p>Each test gets a fresh instance. No state leaking between tests. And because it's TypeScript — if you remove a fixture, every test that depends on it fails at compile time, not at runtime.</p>
<hr />
<h2>Rich diagnostics with attachTable</h2>
<p>This is where BDR goes beyond standard Playwright reporting. <code>attachTable</code> generates a styled HTML table and attaches it directly to the Allure report step:</p>
<pre><code class="language-typescript">// bdr/tables.ts
export async function attachTable(name: string, data: any[]) {
  if (!data || data.length === 0) return;
  const html = generateHtmlTable(data);
  await test.info().attach(name, {
    body: Buffer.from(html),
    contentType: 'text/html',
  });
}

function generateHtmlTable(data: any[]): string {
  const headers = Object.keys(data[0]);
  const ths = headers.map((h) =&gt; `&lt;th&gt;${h}&lt;/th&gt;`).join('');
  const trs = data
    .map((row) =&gt; {
      const tds = headers
        .map((h) =&gt; {
          const val = row[h];
          return `&lt;td&gt;${val === undefined || val === null ? '' : val}&lt;/td&gt;`;
        })
        .join('');
      return `&lt;tr&gt;${tds}&lt;/tr&gt;`;
    })
    .join('');

  return `
    &lt;html&gt;&lt;head&gt;&lt;style&gt;
        table { border-collapse: collapse; width: 100%; box-shadow: 0 2px 15px rgba(0,0,0,0.1); }
        th { background-color: #2c3e50; color: #fff; padding: 12px 15px; text-transform: uppercase; }
        td { padding: 12px 15px; border-bottom: 1px solid #ddd; }
        tr:nth-child(even) { background-color: #f8f9fa; }
        tr:hover { background-color: #f1f4f6; }
    &lt;/style&gt;&lt;/head&gt;
    &lt;body&gt;
        &lt;table&gt;
            &lt;thead&gt;&lt;tr&gt;${ths}&lt;/tr&gt;&lt;/thead&gt;
            &lt;tbody&gt;${trs}&lt;/tbody&gt;
        &lt;/table&gt;
    &lt;/body&gt;&lt;/html&gt;`;
}
</code></pre>
<p>Here's what this looks like in the report:</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/69f087e210a70b33358ff6ee/9e4827ed-7af3-48ad-8f2e-115224f4fd7b.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>attachCompareTable — Expected vs Actual</h2>
<p>This is the diagnostic killer feature. When a test fails on a data mismatch, <code>attachCompareTable</code> shows you exactly which fields don't match:</p>
<pre><code class="language-typescript">export async function attachCompareTable(name: string, expected: any, actual: any) {
  const allKeys = Array.from(new Set([...Object.keys(expected), ...Object.keys(actual)]));
  const comparisonData = allKeys.map((key) =&gt; {
    const exp = expected[key];
    const act = actual[key];
    const isMatch = JSON.stringify(exp) === JSON.stringify(act);
    return {
      Field: key,
      Expected: exp === undefined ? '&lt;undefined&gt;' : JSON.stringify(exp),
      Actual: act === undefined ? '&lt;undefined&gt;' : JSON.stringify(act),
      Result: isMatch ? '✅ MATCH' : '❌ MISMATCH',
    };
  });
  await attachTable(name, comparisonData);
}
</code></pre>
<p>Instead of:</p>
<pre><code class="language-plaintext">AssertionError: expected { role: 'admin' } to equal { role: 'user' }
</code></pre>
<p>You get a table in the report:</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Expected</th>
<th>Actual</th>
<th>Result</th>
</tr>
</thead>
<tbody><tr>
<td>id</td>
<td>"123"</td>
<td>"123"</td>
<td>MATCH</td>
</tr>
<tr>
<td>email</td>
<td>"<a href="mailto:john@example.com">john@example.com</a>"</td>
<td>"<a href="mailto:john@example.com">john@example.com</a>"</td>
<td>MATCH</td>
</tr>
<tr>
<td>role</td>
<td>"user"</td>
<td>"admin"</td>
<td>MISMATCH</td>
</tr>
</tbody></table>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/69f087e210a70b33358ff6ee/56a557a5-7a98-4339-852c-9ddc09c4ff6a.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>A complete hybrid scenario: API setup + UI verification</h2>
<p>Here's a real-world scenario that uses all the layers together:</p>
<pre><code class="language-typescript">test('User created via API can log in through UI', async ({ loginPage, page, request }) =&gt; {
  const newUser = {
    email: 'john.doe@example.com',
    password: 'SecurePass123',
    role: 'customer',
  };

  await BDR.Given('a user exists in the system', async () =&gt; {
    await attachTable('New User Payload', [newUser]);
    const response = await request.post('/users', { data: newUser });
    expect(response.status()).toBe(201);
    const created = await response.json();
    await attachTable('Created User Response', [created]);
  });

  await BDR.When('the user logs in through the UI', async () =&gt; {
    await loginPage.goto();
    await loginPage.login(newUser.email, newUser.password);
  });

  await BDR.Then('the user sees their dashboard', async () =&gt; {
    await expect(page).toHaveURL('/dashboard');
  });
});
</code></pre>
<hr />
<p>When this test fails, your report shows: the exact payload sent to the API, the response received, and a screenshot at the moment of failure. No reproduction needed.</p>
<hr />
<h2>Cucumber vs BDR — the technical comparison</h2>
<table>
<thead>
<tr>
<th></th>
<th>Cucumber + Gherkin</th>
<th>BDR</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Where scenarios live</strong></td>
<td>Separate <code>.feature</code> files</td>
<td>Directly in TypeScript</td>
</tr>
<tr>
<td><strong>IDE support</strong></td>
<td>Steps are strings — no autocomplete</td>
<td>Full TypeScript — autocomplete, go-to-definition</td>
</tr>
<tr>
<td><strong>Compile-time safety</strong></td>
<td>None — errors at runtime</td>
<td>Full — broken references caught immediately</td>
</tr>
<tr>
<td><strong>Renaming a method</strong></td>
<td>Hunt across <code>.feature</code> files manually</td>
<td>IDE updates every reference instantly</td>
</tr>
<tr>
<td><strong>Report richness</strong></td>
<td>Basic pass/fail + step names</td>
<td>Steps + styled HTML tables + screenshots + API logs</td>
</tr>
<tr>
<td><strong>Decorator support</strong></td>
<td>N/A</td>
<td><code>@Step</code> with title interpolation and nested property access</td>
</tr>
<tr>
<td><strong>Maintenance cost</strong></td>
<td>Two places to update</td>
<td>One place</td>
</tr>
</tbody></table>
<hr />
<h2>Try it</h2>
<ul>
<li><p><a href="https://github.com/dmitryAQA/bdr-methodology"><strong>BDR Methodology</strong></a> — full architecture docs, guides, and manifesto</p>
</li>
<li><p><a href="https://github.com/dmitryAQA/playwright-bdr-template"><strong>Playwright BDR Template</strong></a> — working implementation, clone and run</p>
</li>
</ul>
<hr />
<p><em>I'm open to QA Automation roles — remote, contract, or full-time. If you're building a team and care about test architecture, reach out.</em></p>
<p>Email: <a href="mailto:dmitryAQA@outlook.com">dmitryAQA@outlook.com</a> | Telegram: <a href="https://t.me/DmitryMeAQA">@DmitryMeAQA</a></p>
]]></content:encoded></item></channel></rss>