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. + +![BrowserStack Logo](https://d98b8t1nnulk5.cloudfront.net/production/images/layout/logo-header.png?1469004780) + +## 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" + } +}