<?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: Anubhav Chattopadhyay</title>
    <description>The latest articles on DEV Community by Anubhav Chattopadhyay (@anubhav_chattopadhyay).</description>
    <link>https://dev.to/anubhav_chattopadhyay</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3894996%2F155b3aba-6b59-4f6a-bc62-243acef98a1a.png</url>
      <title>DEV Community: Anubhav Chattopadhyay</title>
      <link>https://dev.to/anubhav_chattopadhyay</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anubhav_chattopadhyay"/>
    <language>en</language>
    <item>
      <title>Playwright BDD Without Cucumber: TypeScript Decorators and DataTables</title>
      <dc:creator>Anubhav Chattopadhyay</dc:creator>
      <pubDate>Fri, 24 Apr 2026 00:05:37 +0000</pubDate>
      <link>https://dev.to/anubhav_chattopadhyay/playwright-bdd-without-cucumber-typescript-decorators-and-datatables-b31</link>
      <guid>https://dev.to/anubhav_chattopadhyay/playwright-bdd-without-cucumber-typescript-decorators-and-datatables-b31</guid>
      <description>&lt;p&gt;If you've worked with Playwright for a while, you've probably had this conversation with a stakeholder or a QA lead:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Can we write our tests in Gherkin? The business wants to be able to read the test cases."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The usual answer is to reach for CucumberJS or &lt;code&gt;playwright-bdd&lt;/code&gt;. Both work, but they come with a cost, you now maintain &lt;code&gt;.feature&lt;/code&gt; files alongside your test code, wire up a step definition runner, and manage a parallel toolchain that sits on top of Playwright. Your Playwright runner is effectively replaced.&lt;/p&gt;

&lt;p&gt;I wanted the readability of BDD without any of that overhead. So I built &lt;strong&gt;playwright-gherkin-steps&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Is
&lt;/h2&gt;

&lt;p&gt;A lightweight pattern, three helper files that adds Gherkin-style &lt;code&gt;Given&lt;/code&gt;, &lt;code&gt;When&lt;/code&gt;, &lt;code&gt;Then&lt;/code&gt;, and &lt;code&gt;And&lt;/code&gt; steps directly to native Playwright &lt;code&gt;test()&lt;/code&gt; blocks. No &lt;code&gt;.feature&lt;/code&gt; files. No separate runner. No extra runtime dependencies beyond Playwright itself.&lt;/p&gt;

&lt;p&gt;Your tests look like this:&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;new user can register successfully&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signupPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SignupPage&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;registerSteps&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;When&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Then&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createGherkin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nc"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User navigates to AutomationExercise&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="nc"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User submits the signup form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`
    | name     | email                    |
    | John Doe | testuser@mailinator.com  |
  `&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="nc"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User should be on the account info page&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;And your page object looks like this:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SignupPage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&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;page&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Given User navigates to AutomationExercise&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="nf"&gt;navigate&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="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;https://automationexercise.com/login&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="nd"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;When User submits the signup form&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="nf"&gt;submitSignupForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DataTable&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="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;email&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;table&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="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;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[data-qa='signup-name']&lt;/span&gt;&lt;span class="dl"&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="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;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[data-qa='signup-email']&lt;/span&gt;&lt;span class="dl"&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="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;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[data-qa='signup-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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Then User should be on the account info 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="nf"&gt;assertOnAccountInfoPage&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;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;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/.*signup/&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;That's it. Native Playwright &lt;code&gt;test()&lt;/code&gt; blocks, page object pattern you already know, and BDD-style readability on top.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem With Existing BDD Solutions
&lt;/h2&gt;

&lt;p&gt;The most common BDD solutions for Playwright are CucumberJS and &lt;code&gt;playwright-bdd&lt;/code&gt;. Both require you to maintain separate &lt;code&gt;.feature&lt;/code&gt; files with your Gherkin scenarios and write matching step definitions in a separate file, keeping the two in sync as your tests evolve. In most projects I've seen, those &lt;code&gt;.feature&lt;/code&gt; files end up being written by developers anyway, so you're paying the framework cost without the intended benefit of business-readable scenarios owned by non-developers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;playwright-bdd&lt;/code&gt;&lt;/strong&gt; keeps the Playwright runner, which is good. But it introduces its own overhead of having to convert your &lt;code&gt;.feature&lt;/code&gt; files into &lt;code&gt;spec.ts&lt;/code&gt; files as a build step before Playwright can run them. More moving parts, more to maintain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CucumberJS&lt;/strong&gt; replaces the Playwright runner entirely with its own — and the Playwright runner is one of the most powerful things about Playwright. Think about what you give up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Playwright's HTML report&lt;/strong&gt; - rich, interactive, with traces, screenshots, and video attached to every failure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trace Viewer&lt;/strong&gt; — the ability to replay every action, network request, and DOM snapshot from a failed test run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;test.step()&lt;/code&gt;&lt;/strong&gt; — native step grouping that shows up in the report with timings per step&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixtures&lt;/strong&gt; — Playwright's dependency injection system for sharing state cleanly across tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI Mode&lt;/strong&gt; — the interactive watch mode for debugging tests live in a browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you replace the Playwright runner with CucumberJS's runner, all of that goes away or has to be laboriously re-wired. You're essentially using Playwright as a browser automation library and throwing away the test framework built around it.&lt;/p&gt;

&lt;p&gt;This project takes the opposite approach: &lt;strong&gt;keep the Playwright runner, add the readability&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The implementation is three files.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;@step&lt;/code&gt; Decorator —&lt;code&gt;helpers/StepRegistry.ts&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;@step&lt;/code&gt; decorator registers a page object method under a description string at class definition 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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;propertyKey&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="nx"&gt;descriptor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PropertyDescriptor&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="nx"&gt;stepRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;instance&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;descriptor&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;return&lt;/span&gt; &lt;span class="nx"&gt;descriptor&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;registerSteps(instance)&lt;/code&gt; then binds the live page object instance to each registered method, so &lt;code&gt;this.page&lt;/code&gt; works correctly when the step runs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;runStep(description)&lt;/code&gt; looks up and executes the step — and wraps it in Playwright's native &lt;code&gt;test.step()&lt;/code&gt; so it appears in the HTML report.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. DataTable - &lt;code&gt;helpers/DataTable.ts&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Honestly, while working with native Playwright, the one thing I do miss from Cucumber is the DataTable. A clean, highly readable data structure that clearly depicts the actual data being passed into a test step. I've always had a soft spot for the pipe-delimited table format offered by Cucumber.&lt;/p&gt;

&lt;p&gt;That need was actually a big part of why I built this. And importantly, &lt;strong&gt;you don't need to use &lt;code&gt;Given&lt;/code&gt;/&lt;code&gt;When&lt;/code&gt;/&lt;code&gt;Then&lt;/code&gt; at all to benefit from it&lt;/strong&gt;. The &lt;code&gt;DataTable&lt;/code&gt; class is completely standalone, if you just want clean, structured test data in your native Playwright tests without adopting any BDD vocabulary, you can use it on its own.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;DataTable&lt;/code&gt; class parses a pipe-delimited string into an array of row objects keyed by the header row:&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;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DataTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
  | name     | email                |
  | John Doe | john@mailinator.com  |
  | Jane Doe | jane@mailinator.com  |
`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;table&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="c1"&gt;// { name: 'John Doe', email: 'john@mailinator.com' }&lt;/span&gt;
&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hashes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// array of all rows&lt;/span&gt;
&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ['John Doe', 'Jane Doe']&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Empty cells are preserved as empty strings, they don't shift subsequent columns.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Gherkin Context - &lt;code&gt;helpers/Gherkin.ts&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;createGherkin()&lt;/code&gt; returns &lt;code&gt;Given&lt;/code&gt;, &lt;code&gt;When&lt;/code&gt;, &lt;code&gt;Then&lt;/code&gt;, and &lt;code&gt;And&lt;/code&gt; as async functions. Each accepts an optional second argument — either an inline template literal string or a pre-built &lt;code&gt;DataTable&lt;/code&gt; instance — and resolves it transparently before calling &lt;code&gt;runStep&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;// Option 1 — inline template literal&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nc"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User submits the signup form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`
  | name     | email              |
  | John Doe | john@example.com   |
`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Option 2 - pre-defined DataTable variable&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DataTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
  | name     | email              |
  | John Doe | john@example.com   |
`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nc"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User submits the signup form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your page object method always receives a &lt;code&gt;DataTable&lt;/code&gt; instance regardless of which style you used.&lt;/p&gt;




&lt;h2&gt;
  
  
  Playwright HTML Report Integration
&lt;/h2&gt;

&lt;p&gt;Because every &lt;code&gt;Given&lt;/code&gt;/&lt;code&gt;When&lt;/code&gt;/&lt;code&gt;Then&lt;/code&gt; call goes through &lt;code&gt;runStep&lt;/code&gt;, which wraps execution in &lt;code&gt;test.step()&lt;/code&gt;, your HTML report automatically breaks down each test by step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="err"&gt;✓&lt;/span&gt; &lt;span class="err"&gt;new&lt;/span&gt; &lt;span class="err"&gt;user&lt;/span&gt; &lt;span class="err"&gt;c&lt;/span&gt;&lt;span class="nf"&gt;an &lt;/span&gt;register successfully

    &lt;span class="err"&gt;✓&lt;/span&gt; &lt;span class="nf"&gt;Given &lt;/span&gt;User navigates to AutomationExercise          45ms
    &lt;span class="err"&gt;✓&lt;/span&gt; &lt;span class="nf"&gt;When &lt;/span&gt;User submits the signup form                   312ms
    &lt;span class="err"&gt;✓&lt;/span&gt; &lt;span class="nf"&gt;Then &lt;/span&gt;User should be on the account info page         28ms
    &lt;span class="err"&gt;✓&lt;/span&gt; &lt;span class="nf"&gt;When &lt;/span&gt;User fills account personal information        189ms
    &lt;span class="err"&gt;✓&lt;/span&gt; &lt;span class="nf"&gt;When &lt;/span&gt;User fills address information                 276ms
    &lt;span class="err"&gt;✓&lt;/span&gt; &lt;span class="nf"&gt;When &lt;/span&gt;User clicks Create Account                     534ms
    &lt;span class="err"&gt;✓&lt;/span&gt; &lt;span class="nf"&gt;Then &lt;/span&gt;Account created page should be shown            61ms
    &lt;span class="err"&gt;✓&lt;/span&gt; &lt;span class="nf"&gt;When &lt;/span&gt;User continues after account creation          198ms
    &lt;span class="err"&gt;✓&lt;/span&gt; &lt;span class="nf"&gt;Then &lt;/span&gt;User should be logged in as                     44ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No extra configuration. The step labels in the report exactly match what you wrote in the test.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Styles: Pick What Fits
&lt;/h2&gt;

&lt;p&gt;The project supports two styles depending on how much BDD vocabulary you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Style 1 - Full BDD with Given/When/Then:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tests read like Gherkin scenarios. Best for teams that want maximum readability or stakeholder visibility.&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="k"&gt;await&lt;/span&gt; &lt;span class="nc"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User navigates to AutomationExercise&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="nc"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User submits the signup form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`
  | name     | email                   |
  | John Doe | testuser@mailinator.com |
`&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="nc"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User should be on the account info page&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;&lt;strong&gt;Style 2 - DataTable only, no BDD layer:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Call page object methods directly, but pass structured &lt;code&gt;DataTable&lt;/code&gt; objects instead of raw strings. Best for teams that want clean data management without adopting the full BDD vocabulary.&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;signupData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DataTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
  | name     | email                   |
  | John Doe | testuser@mailinator.com |
`&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;submitSignupForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signupData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both styles live in the same project. You can mix and match per test file.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;The project is on GitHub with full setup instructions, both usage styles documented with working examples against &lt;a href="https://automationexercise.com" rel="noopener noreferrer"&gt;automationexercise.com&lt;/a&gt;, and a &lt;code&gt;tsconfig.json&lt;/code&gt; that works out of the box.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://github.com/anubhav-chattopadhyay/playwright-gherkin-steps" rel="noopener noreferrer"&gt;github.com/anubhav-chattopadhyay/playwright-gherkin-steps&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Copy the three files in &lt;code&gt;helpers/&lt;/code&gt; into your project and you're done. No additional dependencies, no configuration beyond enabling &lt;code&gt;experimentalDecorators&lt;/code&gt; in your &lt;code&gt;tsconfig.json&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;A few things I'm considering adding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step hooks&lt;/strong&gt; - &lt;code&gt;Before&lt;/code&gt; and &lt;code&gt;After&lt;/code&gt; equivalents that run around each step&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step catalog&lt;/strong&gt; - a utility that lists all registered steps across page objects, useful for larger projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variable substitution&lt;/strong&gt; in step names — so you can write &lt;code&gt;@step("When User logs in as {role}")&lt;/code&gt; and match it dynamically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of those sound useful to you, open an issue or drop a comment below. And if you end up using this in your project, I'd love to hear how it fits.&lt;/p&gt;




</description>
      <category>playwright</category>
      <category>bdd</category>
      <category>typescript</category>
      <category>testautomation</category>
    </item>
  </channel>
</rss>
