diff --git a/.github/workflows/sanity-workflow.yml b/.github/workflows/sanity-workflow.yml
new file mode 100644
index 0000000..2fb3c8f
--- /dev/null
+++ b/.github/workflows/sanity-workflow.yml
@@ -0,0 +1,65 @@
+# Sanity workflow that verifies the Cucumber-JS + Playwright BrowserStack SDK sample
+# on a clean-network GitHub runner, mirroring browserstack/node-js-playwright-browserstack.
+# Runs the public bstackdemo `sample` profile, then the BrowserStack Local `local`
+# profile against a python http.server harness on port 45454.
+
+name: Cucumber-JS Playwright SDK sanity workflow on workflow_dispatch
+
+on:
+ workflow_dispatch:
+ inputs:
+ ref:
+ description: 'Branch or commit to build (defaults to the triggering ref)'
+ required: false
+
+permissions:
+ contents: read
+
+jobs:
+ sanity:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ node: ['20']
+ name: Cucumber-JS Playwright Sample (Node ${{ matrix.node }})
+ env:
+ BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
+ BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.inputs.ref || github.ref }}
+
+ - name: Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node }}
+
+ - name: Strip credential placeholders so env vars take effect
+ # The yml ships with literal YOUR_USERNAME / YOUR_ACCESS_KEY placeholders;
+ # the SDK uses those literal values unless the lines are absent.
+ run: sed -i '/^userName:/d; /^accessKey:/d' browserstack.yml
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Run sample test (public bstackdemo)
+ run: npm run sample-test
+
+ - name: Run local test (BrowserStack Local + python http.server harness)
+ run: |
+ set -u
+ mkdir -p "$RUNNER_TEMP/bs-local-harness"
+ cat > "$RUNNER_TEMP/bs-local-harness/index.html" <<'HTML'
+
+
BrowserStack Local Test
+ OK
+ HTML
+ ( cd "$RUNNER_TEMP/bs-local-harness" && python3 -m http.server 45454 ) &
+ HTTP_PID=$!
+ trap 'kill "$HTTP_PID" 2>/dev/null || true' EXIT
+ sleep 2
+ sed -i 's/^browserstackLocal: false.*/browserstackLocal: true/' browserstack.yml
+ npm run sample-local-test
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..748011b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+test-results
+log/
+package-lock.json
+local.log
+.DS_Store
+browserstack.err
diff --git a/README.md b/README.md
index 5830aae..ac112b8 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,85 @@
# cucumber-js-playwright-browserstack
-Creating a sample repo for different Playwright languages and runners
+
+This sample shows how to run [Cucumber-JS](https://github.com/cucumber/cucumber-js) + [Playwright](https://playwright.dev/) tests on BrowserStack using the [BrowserStack Node SDK](https://www.npmjs.com/package/browserstack-node-sdk). The SDK reads `browserstack.yml`, fans your scenarios out across the platforms listed there, starts and stops BrowserStack Local automatically, and reports test status to the BrowserStack dashboard. Your test code stays plain `@playwright/test` + `@cucumber/cucumber` -- no manual `connect()`, no capabilities in code.
+
+
+
+## Prerequisites
+
+* [Node.js](https://nodejs.org/) 18, 20, or 22 LTS, and npm (verified on Node 20)
+* A BrowserStack account -- grab your [Username and Access Key](https://www.browserstack.com/accounts/settings)
+
+> **Note on the Cucumber version:** this sample pins `@cucumber/cucumber` to **v11**, which supports Node 18/20/22. Cucumber **v12** (`latest`) requires **Node 22+** (it uses `fs/promises.glob`) and will fail on Node 18/20 with `promises_1.glob is not a function`. Bump to v12 only if you are on Node 22+.
+
+## Setup
+
+* Clone the repo
+* Install dependencies:
+
+ ```sh
+ npm install
+ ```
+
+* Add your credentials to `browserstack.yml` (replace `YOUR_USERNAME` / `YOUR_ACCESS_KEY`), or remove those two lines and export them as environment variables instead:
+
+ ```sh
+ export BROWSERSTACK_USERNAME=
+ export BROWSERSTACK_ACCESS_KEY=
+ ```
+
+## Run Sample Test
+
+Runs the public [bstackdemo.com](https://bstackdemo.com/) add-to-cart scenario (the `sample` Cucumber profile) across every platform in `browserstack.yml`:
+
+```sh
+npm run sample-test
+```
+
+## Run Local Test (BrowserStack Local)
+
+For apps on `localhost`, a staging host, or behind a firewall, set `browserstackLocal: true` in `browserstack.yml`, then run the `local` profile:
+
+```sh
+npm run sample-local-test
+```
+
+The SDK starts and stops the BrowserStack Local tunnel for you -- no manual binary download or lifecycle management. The scenario points at `http://bs-local.com:45454/`, a hostname BrowserStack Local resolves back to your machine.
+
+## How the SDK changes things
+
+- **One `browserstack.yml`** declares platforms, parallelism, the Local toggle, and reporting; the SDK picks them up automatically.
+- **The SDK runs platforms in parallel for you** -- one Cucumber run per `(platform x parallelsPerPlatform)` cell, no per-platform branching needed.
+- **The SDK rewrites Playwright launches** -- `features/support/hooks.js` calls `chromium.launch()` and the SDK transparently redirects it to the per-platform browser configured in the yml (`chrome` / `playwright-webkit` / `playwright-firefox`). No `chromium.connect(wss_url)` plumbing.
+- **The SDK starts and stops BrowserStack Local** when `browserstackLocal: true`.
+
+## Repo layout
+
+```
+.
+├── browserstack.yml # SDK config: credentials, platforms, Local toggle, reporting
+├── package.json # SDK run scripts + deps
+├── cucumber.js # Cucumber profiles: `sample` (bstackdemo) and `local` (bs-local)
+└── features/
+ ├── sample.feature # bstackdemo add-to-cart scenario
+ ├── local.feature # BrowserStack Local scenario
+ ├── step_definitions/
+ │ ├── sample_steps.js
+ │ └── local_steps.js
+ └── support/
+ └── hooks.js # launches Playwright per scenario; SDK routes the launch
+```
+
+## Notes
+
+* View your test results on the [BrowserStack Automate dashboard](https://www.browserstack.com/automate).
+* To test on a different set of browsers, see our [list of supported browsers and platforms](https://www.browserstack.com/list-of-browsers-and-platforms?product=automate).
+* Understand how many parallel sessions you need with the [Parallel Test Calculator](https://www.browserstack.com/automate/parallel-calculator?ref=github).
+
+## Further Reading
+
+- [Cucumber-JS](https://github.com/cucumber/cucumber-js)
+- [Playwright](https://playwright.dev/)
+- [BrowserStack documentation for Playwright](https://www.browserstack.com/docs/automate/playwright)
+- [BrowserStack Node SDK](https://www.npmjs.com/package/browserstack-node-sdk)
+
+Happy Testing!
diff --git a/browserstack.yml b/browserstack.yml
new file mode 100644
index 0000000..7e3dc03
--- /dev/null
+++ b/browserstack.yml
@@ -0,0 +1,82 @@
+# =============================
+# Set BrowserStack Credentials
+# =============================
+# Add your BrowserStack userName and accessKey here or set BROWSERSTACK_USERNAME and
+# BROWSERSTACK_ACCESS_KEY as env variables
+userName: YOUR_USERNAME
+accessKey: YOUR_ACCESS_KEY
+
+# ======================
+# BrowserStack Reporting
+# ======================
+# The following capabilities are used to set up reporting on BrowserStack:
+# Set 'projectName' to the name of your project. Example, Marketing Website
+projectName: BrowserStack Samples
+# Set `buildName` as the name of the job / testsuite being run
+buildName: cucumber-js-playwright-browserstack
+# `buildIdentifier` is a unique id to differentiate every execution that gets appended to
+# buildName. Choose your buildIdentifier format from the available expressions:
+# ${BUILD_NUMBER} (Default): Generates an incremental counter with every execution
+# ${DATE_TIME}: Generates a Timestamp with every execution. Eg. 05-Nov-19:30
+# Read more about buildIdentifiers here -> https://www.browserstack.com/docs/automate/selenium/organize-tests
+buildIdentifier: '#${BUILD_NUMBER}' # Supports strings along with either/both ${expression}
+
+# =======================================
+# Platforms (Browsers / Devices to test)
+# =======================================
+# Platforms object contains all the browser / device combinations you want to test on.
+# Entire list available here -> (https://www.browserstack.com/list-of-browsers-and-platforms/automate)
+# Your step definitions call `chromium.launch()` unconditionally (see features/support/hooks.js)
+# -- the SDK transparently routes the launch to the per-platform browser configured here.
+platforms:
+ - os: Windows
+ osVersion: 11
+ browserName: chrome
+ browserVersion: latest
+ - os: OS X
+ osVersion: Ventura
+ browserName: playwright-webkit
+ browserVersion: latest
+ - os: Windows
+ osVersion: 11
+ browserName: playwright-firefox
+ browserVersion: latest
+
+# =======================
+# Parallels per Platform
+# =======================
+# The number of parallel threads to be used for each platform set.
+# BrowserStack's SDK runner will select the best strategy based on the configured value
+#
+# Example 1 - If you have configured 3 platforms and set `parallelsPerPlatform` as 2, a total of 6 (2 * 3) parallel threads will be used on BrowserStack
+#
+# Example 2 - If you have configured 1 platform and set `parallelsPerPlatform` as 5, a total of 5 (1 * 5) parallel threads will be used on BrowserStack
+parallelsPerPlatform: 1
+
+# ==========================================
+# BrowserStack Local
+# (For localhost, staging/private websites)
+# ==========================================
+# Set browserstackLocal to true if your website under test is not accessible publicly over the internet
+# Learn more about how BrowserStack Local works here -> https://www.browserstack.com/docs/automate/selenium/local-testing-introduction
+browserstackLocal: false # (Default false). Set to true to run the local sample test.
+# browserStackLocalOptions:
+# Options to be passed to BrowserStack local in-case of advanced configurations
+ # localIdentifier: # (Default: null) Needed if you need to run multiple instances of local.
+ # forceLocal: true # (Default: false) Set to true if you need to resolve all your traffic via BrowserStack Local tunnel.
+ # Entire list of arguments available here -> https://www.browserstack.com/docs/automate/selenium/manage-incoming-connections
+
+framework: playwright
+source: cucumber-js-playwright-browserstack:sample-sdk:v1.0
+
+# ===================
+# Debugging features
+# ===================
+debug: false # # Set to true if you need screenshots for every selenium command ran
+networkLogs: false # Set to true to enable HAR logs capturing
+consoleLogs: errors # Remote browser's console debug levels to be printed (Default: errors)
+# Available options are `disable`, `errors`, `warnings`, `info`, `verbose` (Default: errors)
+
+# Test Reporting and Analytics is an intelligent test reporting & debugging product. It collects data using the SDK.
+# Visit automation.browserstack.com to see your test reports and insights. To disable, set `testReporting: false`.
+testReporting: true
diff --git a/cucumber.js b/cucumber.js
new file mode 100644
index 0000000..0f09d06
--- /dev/null
+++ b/cucumber.js
@@ -0,0 +1,19 @@
+// Cucumber-JS profiles. The BrowserStack SDK (invoked via `browserstack-node-sdk
+// cucumber-js`) reads browserstack.yml, fans each profile out across the platforms
+// declared there, and routes the Playwright `chromium.launch()` in support/hooks.js
+// to BrowserStack.
+//
+// npm run sample-test -> --profile sample (public bstackdemo)
+// npm run sample-local-test -> --profile local (BrowserStack Local)
+const common = ['features/support/hooks.js'];
+
+module.exports = {
+ sample: {
+ paths: ['features/sample.feature'],
+ require: ['features/step_definitions/sample_steps.js', ...common],
+ },
+ local: {
+ paths: ['features/local.feature'],
+ require: ['features/step_definitions/local_steps.js', ...common],
+ },
+};
diff --git a/features/local.feature b/features/local.feature
new file mode 100644
index 0000000..5a74d44
--- /dev/null
+++ b/features/local.feature
@@ -0,0 +1,9 @@
+Feature: BStackLocalSample
+
+ As a developer testing a private host
+ I want BrowserStack Local to tunnel my localhost to BrowserStack
+ So that the cloud browser can reach a page only my machine can serve
+
+ Scenario: Reach a private host via BrowserStack Local
+ Given I open the local sample page on bs-local
+ Then the local sample page title contains "BrowserStack Local"
diff --git a/features/sample.feature b/features/sample.feature
new file mode 100644
index 0000000..c9e7c6c
--- /dev/null
+++ b/features/sample.feature
@@ -0,0 +1,10 @@
+Feature: BStackDemo cart
+
+ As a shopper on bstackdemo.com
+ I want to add an item to my cart
+ So that I can verify the cart shows what I picked
+
+ Scenario: Add the first item to cart
+ Given I open the bstackdemo home page
+ When I add the first product to the cart
+ Then the cart shows 1 item that matches the product I added
diff --git a/features/step_definitions/local_steps.js b/features/step_definitions/local_steps.js
new file mode 100644
index 0000000..1b3c342
--- /dev/null
+++ b/features/step_definitions/local_steps.js
@@ -0,0 +1,13 @@
+const { Given, Then } = require('@cucumber/cucumber');
+const assert = require('assert');
+
+// With `browserstackLocal: true` in browserstack.yml the SDK opens a tunnel, so the
+// cloud browser can reach http://bs-local.com:/ -- a host only your machine serves.
+Given('I open the local sample page on bs-local', async function () {
+ await this.page.goto('http://bs-local.com:45454/');
+});
+
+Then('the local sample page title contains {string}', async function (expected) {
+ const title = await this.page.title();
+ assert.ok(title.includes(expected), `expected title to contain "${expected}" but was "${title}"`);
+});
diff --git a/features/step_definitions/sample_steps.js b/features/step_definitions/sample_steps.js
new file mode 100644
index 0000000..32cbd29
--- /dev/null
+++ b/features/step_definitions/sample_steps.js
@@ -0,0 +1,22 @@
+const { Given, When, Then } = require('@cucumber/cucumber');
+const assert = require('assert');
+
+let productTitle;
+
+Given('I open the bstackdemo home page', async function () {
+ await this.page.goto('https://bstackdemo.com/');
+});
+
+When('I add the first product to the cart', async function () {
+ const firstProduct = this.page.locator('[id="1"]');
+ productTitle = await firstProduct.locator('.shelf-item__title').first().innerText();
+ await firstProduct.getByText('Add to Cart').click();
+});
+
+Then('the cart shows 1 item that matches the product I added', async function () {
+ const quantity = await this.page.locator('.bag__quantity').innerText();
+ assert.strictEqual(quantity, '1');
+
+ const cartTitle = await this.page.locator('.shelf-item__details .title').innerText();
+ assert.strictEqual(cartTitle, productTitle);
+});
diff --git a/features/support/hooks.js b/features/support/hooks.js
new file mode 100644
index 0000000..acbc07d
--- /dev/null
+++ b/features/support/hooks.js
@@ -0,0 +1,28 @@
+const { Before, After, BeforeAll, AfterAll, setDefaultTimeout } = require('@cucumber/cucumber');
+const { chromium } = require('@playwright/test');
+
+// Cucumber steps can take longer than the default 5s on a remote browser.
+setDefaultTimeout(60 * 1000);
+
+let browser;
+
+// Your code calls `chromium.launch()` as usual -- the BrowserStack SDK transparently
+// routes the launch to the per-platform browser configured in browserstack.yml.
+BeforeAll(async () => {
+ browser = await chromium.launch();
+});
+
+AfterAll(async () => {
+ if (browser) await browser.close();
+});
+
+// A fresh context + page per scenario; `this` is the Cucumber World shared with steps.
+Before(async function () {
+ this.context = await browser.newContext();
+ this.page = await this.context.newPage();
+});
+
+After(async function () {
+ if (this.page) await this.page.close();
+ if (this.context) await this.context.close();
+});
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..85299db
--- /dev/null
+++ b/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "cucumber-js-playwright-browserstack",
+ "version": "1.0.0",
+ "description": "Sample showing how to run Cucumber-JS + Playwright tests on BrowserStack using the BrowserStack Node SDK",
+ "main": "index.js",
+ "scripts": {
+ "sample-test": "browserstack-node-sdk cucumber-js --profile sample",
+ "sample-local-test": "browserstack-node-sdk cucumber-js --profile local",
+ "postinstall": "npm update browserstack-node-sdk"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "devDependencies": {
+ "@cucumber/cucumber": "^11.0.0",
+ "@playwright/test": "latest",
+ "browserstack-node-sdk": "latest"
+ }
+}