From a3cd34777b695ec26a742a12a413f189d5d552d0 Mon Sep 17 00:00:00 2001 From: Jimesh-browserstack Date: Fri, 24 Apr 2026 13:34:05 +0530 Subject: [PATCH 1/4] Initial commit: cucumber-java-playwright BrowserStack sample Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/CODEOWNERS | 3 + .github/workflows/Semgrep.yml | 48 ++++ .github/workflows/cucumber-workflow-run.yml | 82 +++++++ .gitignore | 28 +++ LICENSE | 21 ++ README.md | 212 ++++++++++++++++++ browserstack.yml | 84 +++++++ pom.xml | 168 ++++++++++++++ .../browserstack/RunCucumberLocalTest.java | 15 ++ .../com/browserstack/RunCucumberTest.java | 15 ++ .../stepdefs/e2e/StackDemoSteps.java | 107 +++++++++ .../stepdefs/local/StackLocalSteps.java | 89 ++++++++ .../features/localtest/local.feature | 6 + src/test/resources/features/test/e2e.feature | 7 + src/test/resources/testng.xml | 9 + src/test/resources/testngLocal.xml | 9 + 16 files changed, 903 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/Semgrep.yml create mode 100644 .github/workflows/cucumber-workflow-run.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 browserstack.yml create mode 100644 pom.xml create mode 100644 src/test/java/com/browserstack/RunCucumberLocalTest.java create mode 100644 src/test/java/com/browserstack/RunCucumberTest.java create mode 100644 src/test/java/com/browserstack/stepdefs/e2e/StackDemoSteps.java create mode 100644 src/test/java/com/browserstack/stepdefs/local/StackLocalSteps.java create mode 100644 src/test/resources/features/localtest/local.feature create mode 100644 src/test/resources/features/test/e2e.feature create mode 100644 src/test/resources/testng.xml create mode 100644 src/test/resources/testngLocal.xml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..8da635d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +.github/workflows/*.yml @browserstack/asi-devs + +* @browserstack/automate-public-repos diff --git a/.github/workflows/Semgrep.yml b/.github/workflows/Semgrep.yml new file mode 100644 index 0000000..ab2ae7e --- /dev/null +++ b/.github/workflows/Semgrep.yml @@ -0,0 +1,48 @@ +# Name of this GitHub Actions workflow. +name: Semgrep + +on: + # Scan changed files in PRs (diff-aware scanning): + # The branches below must be a subset of the branches above + pull_request: + branches: ["master", "main"] + push: + branches: ["master", "main"] + schedule: + - cron: '0 6 * * *' + + +permissions: + contents: read + +jobs: + semgrep: + # User definable name of this GitHub Actions job. + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + name: semgrep/ci + # If you are self-hosting, change the following `runs-on` value: + runs-on: ubuntu-latest + + container: + # A Docker image with Semgrep installed. Do not change this. + image: returntocorp/semgrep + + # Skip any PR created by dependabot to avoid permission issues: + if: (github.actor != 'dependabot[bot]') + + steps: + # Fetch project source with GitHub Actions Checkout. + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + # Run the "semgrep ci" command on the command line of the docker image. + - run: semgrep ci --sarif --output=semgrep.sarif + env: + # Add the rules that Semgrep uses by setting the SEMGREP_RULES environment variable. + SEMGREP_RULES: p/default # more at semgrep.dev/explore + + - name: Upload SARIF file for GitHub Advanced Security Dashboard + uses: github/codeql-action/upload-sarif@6c089f53dd51dc3fc7e599c3cb5356453a52ca9e # v2.20.0 + with: + sarif_file: semgrep.sarif + if: always() diff --git a/.github/workflows/cucumber-workflow-run.yml b/.github/workflows/cucumber-workflow-run.yml new file mode 100644 index 0000000..6b13550 --- /dev/null +++ b/.github/workflows/cucumber-workflow-run.yml @@ -0,0 +1,82 @@ +# Runs the Cucumber Java + Playwright sample against BrowserStack SDK on workflow_dispatch. +# Matrix covers Java LTS versions across macOS, Windows and Ubuntu so the sample is +# verified on every OS a customer is likely to develop on. + +name: Cucumber Java Playwright SDK Test workflow on workflow_dispatch + +on: + workflow_dispatch: + inputs: + commit_sha: + description: 'The full commit id to build' + required: true + +jobs: + comment-run: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + max-parallel: 3 + matrix: + java: [ '8', '11', '17' ] + os: [ 'macos-latest', 'windows-latest', 'ubuntu-latest' ] + name: Cucumber Java ${{ matrix.java }} - ${{ matrix.os }} Sample + env: + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.inputs.commit_sha }} + - uses: actions/github-script@98814c53be79b1d30f795b907e553d8679345975 + id: status-check-in-progress + env: + job_name: Cucumber Java ${{ matrix.java }} - ${{ matrix.os }} Sample + commit_sha: ${{ github.event.inputs.commit_sha }} + with: + github-token: ${{ github.token }} + script: | + const result = await github.rest.checks.create({ + owner: context.repo.owner, + repo: context.repo.repo, + name: process.env.job_name, + head_sha: process.env.commit_sha, + status: 'in_progress' + }).catch((err) => ({status: err.status, response: err.response})); + console.log(`The status-check response : ${result.status} Response : ${JSON.stringify(result.response)}`) + if (result.status !== 201) { + console.log('Failed to create check run') + } + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: ${{ matrix.java }} + cache: 'maven' + - name: Run sample-test profile (cross-platform parallel) + run: mvn test -P sample-test + - name: Run sample-local-test profile (BrowserStack Local) + run: mvn test -P sample-local-test + - if: always() + uses: actions/github-script@98814c53be79b1d30f795b907e553d8679345975 + id: status-check-completed + env: + conclusion: ${{ job.status }} + job_name: Cucumber Java ${{ matrix.java }} - ${{ matrix.os }} Sample + commit_sha: ${{ github.event.inputs.commit_sha }} + with: + github-token: ${{ github.token }} + script: | + const result = await github.rest.checks.create({ + owner: context.repo.owner, + repo: context.repo.repo, + name: process.env.job_name, + head_sha: process.env.commit_sha, + status: 'completed', + conclusion: process.env.conclusion + }).catch((err) => ({status: err.status, response: err.response})); + console.log(`The status-check response : ${result.status} Response : ${JSON.stringify(result.response)}`) + if (result.status !== 201) { + console.log('Failed to create check run') + } diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..27cf3aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Maven +target/ +dependency-reduced-pom.xml +.mvn/ + +# Reports +reports/ + +# BrowserStack +browserstack.err +browserstack-reports/ +log/bstack_sdk.log +logs/ + +# IDE / OS +.idea/ +.vscode/ +*.iml +*.swp +*.swo +.DS_Store +Thumbs.db + +# Playwright +test-results/ +playwright-report/ +*.png +*.log diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d9ff1e4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) BrowserStack + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e1a104 --- /dev/null +++ b/README.md @@ -0,0 +1,212 @@ +# cucumber-java-playwright-browserstack + +A ready-to-run sample that runs [Cucumber](https://cucumber.io/docs/installation/java/) scenarios in [Playwright for Java](https://playwright.dev/java/) against real browsers on [BrowserStack](https://www.browserstack.com/automate/playwright). Uses the [BrowserStack Java SDK](https://www.browserstack.com/docs/automate/playwright/java-sdk-quickstart) to fan out across platforms, stream results to the dashboard, and manage the BrowserStack Local tunnel. + +![BrowserStack Logo](https://d98b8t1nnulk5.cloudfront.net/production/images/layout/logo-header.png?1469004780) + +## Prerequisites + +- **JDK 8 or newer** (Temurin 8 / 11 / 17 are all fine). +- **Maven 3.6+**. +- A **BrowserStack account** — grab your [username and access key](https://www.browserstack.com/accounts/settings). +- Works on **macOS, Windows, and Linux** — the sample targets Java 8 so it compiles and runs everywhere. + +No browsers or drivers to install locally. Playwright connects to BrowserStack's cloud over a WebSocket; browser binaries live in the cloud. + +## Quick start + +```bash +# 1. Clone +git clone https://github.com/browserstack/cucumber-java-playwright-browserstack.git +cd cucumber-java-playwright-browserstack + +# 2. Set credentials (pick one method — see "Setting credentials" below) +export BROWSERSTACK_USERNAME= +export BROWSERSTACK_ACCESS_KEY= + +# 3. Run against a public site (bstackdemo.com) on all configured platforms +mvn test -P sample-test +``` + +That's it. Maven downloads Cucumber, TestNG, Playwright for Java, and the BrowserStack SDK on first run, then fans the suite out to the 3 platforms in `browserstack.yml` in parallel. + +**What you'll see:** + +- Console: `Tests run: 3, Failures: 0, Errors: 0` (one test per platform). +- Dashboard: a new build at [automate.browserstack.com](https://automate.browserstack.com) named `browserstack build #N`, with one session per platform. Each session has a **video**, **network logs**, **Playwright logs**, and **console output**. +- Test Observability: intelligent report at [observability.browserstack.com](https://observability.browserstack.com). + +## Setting credentials + +Pick **one** of these three options. Env vars always override `browserstack.yml`. + +| Option | When to use it | How | +|---|---|---| +| **Env vars** (recommended) | Any real workflow; keeps secrets out of the repo | see below | +| **Edit `browserstack.yml`** | Quick local experimentation | Replace `YOUR_USERNAME` / `YOUR_ACCESS_KEY` at the top of the file | +| **Inline on the command** | One-off debug | `BROWSERSTACK_USERNAME=... BROWSERSTACK_ACCESS_KEY=... mvn test -P sample-test` (bash/zsh only) | + +Env var syntax per shell: + +```bash +# macOS / Linux (bash, zsh) +export BROWSERSTACK_USERNAME= +export BROWSERSTACK_ACCESS_KEY= +``` + +```powershell +# Windows — PowerShell +$env:BROWSERSTACK_USERNAME = "" +$env:BROWSERSTACK_ACCESS_KEY = "" +``` + +```cmd +:: Windows — cmd.exe +set BROWSERSTACK_USERNAME= +set BROWSERSTACK_ACCESS_KEY= +``` + +> **CI / GitHub Actions:** the shipped `.github/workflows/cucumber-workflow-run.yml` reads `BROWSERSTACK_USERNAME` and `BROWSERSTACK_ACCESS_KEY` from repository secrets. Add those two secrets in your fork's **Settings → Secrets and variables → Actions** before triggering the workflow. + +## Testing a page on `localhost` (BrowserStack Local) + +If your app lives on `localhost`, a staging host, or behind a firewall, run the `sample-local-test` profile. It hits `http://bs-local.com:45454/` — `bs-local.com` is a hostname BrowserStack Local resolves to `localhost` inside the cloud browser. + +You need **two things**: + +1. **A server running on port 45454 on your machine** that serves a page whose `` contains `BrowserStack Local`. +2. **`browserstackLocal: true`** in `browserstack.yml` (already set in this repo). The SDK launches and tears down the Local tunnel for you — no manual binary download. + +Spin up a throwaway server in ~10 seconds: + +```bash +# macOS / Linux — bash/zsh +mkdir -p /tmp/bstack-local-demo +printf '<!doctype html><html><head><title>BrowserStack Local demook' > /tmp/bstack-local-demo/index.html +python3 -m http.server 45454 --directory /tmp/bstack-local-demo & +mvn test -P sample-local-test +kill %1 # stop the server when done +``` + +```powershell +# Windows — PowerShell +New-Item -ItemType Directory -Force $env:TEMP\bstack-local-demo | Out-Null +'BrowserStack Local demook' | Set-Content $env:TEMP\bstack-local-demo\index.html +Start-Process python -ArgumentList '-m','http.server','45454','--directory',"$env:TEMP\bstack-local-demo" -NoNewWindow +mvn test -P sample-local-test +Get-Process python | Stop-Process # stop the server when done +``` + +Swap the demo URL for your real app whenever you're ready — just change the `Given I am on "…"` line in `src/test/resources/features/localtest/local.feature` and the assertion in `local.feature` / `StackLocalSteps.java` to something meaningful for your app. + +## Project layout + +``` +cucumber-java-playwright-browserstack/ +├── pom.xml Maven deps + profiles (sample-test, sample-local-test) +├── browserstack.yml BrowserStack SDK config: credentials, platforms, Local, reporting +├── src/test/ +│ ├── java/com/browserstack/ +│ │ ├── RunCucumberTest.java TestNG runner for sample-test +│ │ ├── RunCucumberLocalTest.java TestNG runner for sample-local-test +│ │ └── stepdefs/ +│ │ ├── e2e/StackDemoSteps.java Steps for bstackdemo.com add-to-cart +│ │ └── local/StackLocalSteps.java Steps for BrowserStack Local connectivity check +│ └── resources/ +│ ├── features/test/e2e.feature @e2e — bstackdemo.com add-to-cart +│ ├── features/localtest/local.feature @local — title contains "BrowserStack Local" +│ ├── testng.xml Suite XML for sample-test +│ └── testngLocal.xml Suite XML for sample-local-test +└── .github/workflows/ CI workflow (matrix over Java × OS) + Semgrep +``` + +## Customizing the run + +### Platforms + +`browserstack.yml` declares the OS / browser matrix. The shipped config covers the three Playwright engines: + +```yaml +platforms: + - os: OS X + osVersion: Ventura + browserName: chrome # chromium + browserVersion: latest + - os: Windows + osVersion: 10 + browserName: playwright-firefox + browserVersion: latest + - os: OS X + osVersion: Monterey + browserName: playwright-webkit + browserVersion: latest +``` + +Add, remove, or change entries freely — the full supported list is at [BrowserStack: browsers & platforms for Playwright](https://www.browserstack.com/list-of-browsers-and-platforms/playwright). The SDK spins up one session per `platforms:` entry × `parallelsPerPlatform:`. + +### Parallelism + +Tune `parallelsPerPlatform` in `browserstack.yml` for your plan's parallel capacity — use the [Parallel Test Calculator](https://www.browserstack.com/automate/parallel-calculator?ref=github) to pick a number. + +### Reporting + +`projectName`, `buildName`, and `buildIdentifier` in `browserstack.yml` control how runs group in the dashboard. `testObservability: true` is on by default. + +### Debugging capabilities + +Flip these in `browserstack.yml` when you need to reproduce a flaky failure: + +| Key | What it does | +|---|---| +| `debug: true` | Step-by-step screenshots for each Playwright action | +| `networkLogs: true` | HAR capture for every request | +| `consoleLogs: verbose` | Full browser console stream (levels: `disable`, `errors`, `warnings`, `info`, `verbose`) | + +## Adopting this in your own project + +To port this pattern into an existing Cucumber-JVM + Playwright project: + +1. **Add the SDK to `pom.xml`** and wire the `-javaagent:` into Surefire: + ```xml + + com.browserstack + browserstack-java-sdk + LATEST + compile + + ``` + ```xml + + maven-surefire-plugin + + -javaagent:${com.browserstack:browserstack-java-sdk:jar} + + + ``` + Use this project's `pom.xml` as the full template — it includes `cucumber-testng`, the surefire `` block, and Maven profiles. + +2. **Copy `browserstack.yml`** into your project root. Set `userName`, `accessKey`, `projectName`, and your own `platforms:`. Set `framework: cucumber-testng`. + +3. **Copy the `@Before` Playwright connection block** from `StackDemoSteps.java` into your step definitions. The SDK rewires the WebSocket URL per thread so the same block targets a different platform on each parallel run. + +4. **Run** — the javaagent is already wired, so plain `mvn test` is enough: + ```bash + mvn test + ``` + +## Troubleshooting + +| Symptom | Fix | +|---|---| +| `401 Unauthorized` from `cdp.browserstack.com` | `BROWSERSTACK_USERNAME` / `BROWSERSTACK_ACCESS_KEY` not set, or placeholders still in `browserstack.yml`. Re-check credentials. | +| Tests hang at `browserType.connect(...)` | Network/firewall blocking WebSocket to `*.browserstack.com`. Confirm outbound `443` is open. | +| `sample-local-test` fails with `ERR_CONNECTION_REFUSED` | Nothing listening on `localhost:45454`. Start the demo server from the "Testing a page on localhost" section. | +| `sample-local-test` port conflict | Something else is using `45454`. Change it in `local.feature` and in the `python -m http.server` command; both must match. | +| `Could not find -javaagent:${com.browserstack:...:jar}` | The `maven-dependency-plugin` `properties` goal is missing from `pom.xml`. Keep it as shipped. | +| Dashboard missing the build | Credentials are valid but belong to a different account — double-check which account generated the key in use. | + +## Notes + +- Results: [BrowserStack Automate dashboard](https://www.browserstack.com/automate) · [Test Observability](https://observability.browserstack.com). +- The sample targets Java 8 (`maven.compiler.source: 1.8`). If your project is on a newer JDK, bump `source` / `target` — nothing else in the sample depends on a specific Java version. +- Playwright for Java connects to BrowserStack over a WebSocket; there's no local browser binary to install or update. diff --git a/browserstack.yml b/browserstack.yml new file mode 100644 index 0000000..3d289d4 --- /dev/null +++ b/browserstack.yml @@ -0,0 +1,84 @@ +# ============================= +# 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: browserstack build +# `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}' +# Set `framework` of your test suite. Example, `testng`, `cucumber`, `cucumber-testng` +# This property is needed to send test context to BrowserStack (test name, status) +framework: cucumber-testng + +source: cucumber-java-playwright:sample-master:v1.0 + +# ======================================= +# Platforms (Browsers / Devices to test) +# ======================================= +# Platforms object contains all the browser / device combinations you want to test on. +# Playwright supports `chrome` (chromium), `playwright-firefox`, and `playwright-webkit`. +# Entire list available here -> (https://www.browserstack.com/list-of-browsers-and-platforms/playwright) +platforms: + - os: OS X + osVersion: Ventura + browserName: chrome + browserVersion: latest + - os: Windows + osVersion: 10 + browserName: playwright-firefox + browserVersion: latest + - os: OS X + osVersion: Monterey + browserName: playwright-webkit + 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: true # (Default false) + +# Options to be passed to BrowserStack local in-case of advanced configurations +# browserStackLocalOptions: + # 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 + +# =================== +# 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 Observability is an intelligent test reporting & debugging product. It collects data using the SDK. Read more about what data is collected at https://www.browserstack.com/docs/test-observability/references/terms-and-conditions +# Visit observability.browserstack.com to see your test reports and insights. To disable test observability, specify `testObservability: false` in the key below. +testObservability: true diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2d64c31 --- /dev/null +++ b/pom.xml @@ -0,0 +1,168 @@ + + + 4.0.0 + + com.browserstack + cucumber-java-playwright-browserstack + 1.0-SNAPSHOT + jar + + cucumber-java-playwright-browserstack + https://www.browserstack.com + + + UTF-8 + 1.8 + 1.8 + 7.4.1 + 7.4.0 + 1.55.0 + 1.0.6 + 2.0 + 20210307 + 3.0.0-M5 + + + + + io.cucumber + cucumber-java + ${cucumber.version} + test + + + io.cucumber + cucumber-testng + ${cucumber.version} + test + + + org.testng + testng + ${testng.version} + test + + + com.microsoft.playwright + playwright + ${playwright.version} + compile + + + org.json + json + ${json.version} + test + + + org.yaml + snakeyaml + ${snakeyaml.version} + test + + + com.browserstack + browserstack-local-java + ${browserstack-local-java.version} + + + com.browserstack + browserstack-java-sdk + LATEST + compile + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 8 + 8 + + + + org.apache.maven.plugins + maven-dependency-plugin + + + getClasspathFilenames + + properties + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + src/test/resources/testng.xml + + + -javaagent:${com.browserstack:browserstack-java-sdk:jar} + + + true + + + + + + + + + sample-test + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + src/test/resources/testng.xml + + + -javaagent:${com.browserstack:browserstack-java-sdk:jar} + + + true + + + + + + + + sample-local-test + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + src/test/resources/testngLocal.xml + + + -javaagent:${com.browserstack:browserstack-java-sdk:jar} + + + true + + + + + + + + diff --git a/src/test/java/com/browserstack/RunCucumberLocalTest.java b/src/test/java/com/browserstack/RunCucumberLocalTest.java new file mode 100644 index 0000000..ce96d95 --- /dev/null +++ b/src/test/java/com/browserstack/RunCucumberLocalTest.java @@ -0,0 +1,15 @@ +package com.browserstack; + +import io.cucumber.testng.AbstractTestNGCucumberTests; +import io.cucumber.testng.CucumberOptions; + +@CucumberOptions( + glue = "com.browserstack.stepdefs.local", + features = "src/test/resources/features/localtest", + plugin = { + "pretty", + "html:reports/cucumber/cucumber-pretty.html", + "json:reports/cucumber/cucumber.json" + } +) +public class RunCucumberLocalTest extends AbstractTestNGCucumberTests {} diff --git a/src/test/java/com/browserstack/RunCucumberTest.java b/src/test/java/com/browserstack/RunCucumberTest.java new file mode 100644 index 0000000..f62e28e --- /dev/null +++ b/src/test/java/com/browserstack/RunCucumberTest.java @@ -0,0 +1,15 @@ +package com.browserstack; + +import io.cucumber.testng.AbstractTestNGCucumberTests; +import io.cucumber.testng.CucumberOptions; + +@CucumberOptions( + glue = "com.browserstack.stepdefs.e2e", + features = "src/test/resources/features/test", + plugin = { + "pretty", + "html:reports/cucumber/cucumber-pretty.html", + "json:reports/cucumber/cucumber.json" + } +) +public class RunCucumberTest extends AbstractTestNGCucumberTests {} diff --git a/src/test/java/com/browserstack/stepdefs/e2e/StackDemoSteps.java b/src/test/java/com/browserstack/stepdefs/e2e/StackDemoSteps.java new file mode 100644 index 0000000..da95d1d --- /dev/null +++ b/src/test/java/com/browserstack/stepdefs/e2e/StackDemoSteps.java @@ -0,0 +1,107 @@ +package com.browserstack.stepdefs.e2e; + +import com.microsoft.playwright.Browser; +import com.microsoft.playwright.BrowserContext; +import com.microsoft.playwright.BrowserType; +import com.microsoft.playwright.Locator; +import com.microsoft.playwright.Page; +import com.microsoft.playwright.Playwright; +import com.microsoft.playwright.options.WaitForSelectorState; +import io.cucumber.java.After; +import io.cucumber.java.Before; +import io.cucumber.java.Scenario; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import org.json.JSONObject; +import org.testng.Assert; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; + +public class StackDemoSteps { + private Playwright playwright; + private Browser browser; + private BrowserContext context; + private Page page; + private String selectedProductName; + + private static final String FIRST_PRODUCT_NAME = "//*[@id=\"1\"]/p"; + private static final String FIRST_PRODUCT_ADD_TO_CART = "//*[@id=\"1\"]/div[4]"; + private static final String CART_PANE = ".float-cart__content"; + private static final String PRODUCT_IN_CART = "//*[@id=\"__next\"]/div/div/div[2]/div[2]/div[2]/div/div[3]/p[1]"; + + @Before + public void setUp(Scenario scenario) { + playwright = Playwright.create(); + BrowserType browserType = playwright.chromium(); + + Map config = loadBrowserStackYaml(); + String userName = envOrYaml("BROWSERSTACK_USERNAME", config, "userName"); + String accessKey = envOrYaml("BROWSERSTACK_ACCESS_KEY", config, "accessKey"); + + HashMap caps = new HashMap<>(); + caps.put("browserstack.user", userName); + caps.put("browserstack.key", accessKey); + caps.put("browserstack.source", "cucumber-java-playwright:sample-master:v1.0"); + caps.put("browser", "chrome"); + caps.put("sessionName", scenario.getName()); + + String encoded = URLEncoder.encode(new JSONObject(caps).toString(), StandardCharsets.UTF_8); + String wsEndpoint = "wss://cdp.browserstack.com/playwright?caps=" + encoded; + + browser = browserType.connect(wsEndpoint); + context = browser.newContext(new Browser.NewContextOptions().setViewportSize(1920, 1080)); + page = context.newPage(); + } + + @Given("I am on {string}") + public void i_am_on(String url) { + page.navigate(url); + } + + @When("I add the first product to the cart") + public void i_add_the_first_product_to_the_cart() { + selectedProductName = page.locator(FIRST_PRODUCT_NAME).textContent(); + page.locator(FIRST_PRODUCT_ADD_TO_CART).click(); + page.locator(CART_PANE).waitFor( + new Locator.WaitForOptions() + .setState(WaitForSelectorState.VISIBLE) + .setTimeout(30000)); + } + + @Then("the product in the cart should match the product I added") + public void the_product_in_the_cart_should_match_the_product_i_added() { + String inCart = page.locator(PRODUCT_IN_CART).textContent(); + Assert.assertEquals(inCart, selectedProductName); + } + + @After + public void tearDown() { + if (context != null) context.close(); + if (browser != null) browser.close(); + if (playwright != null) playwright.close(); + } + + private Map loadBrowserStackYaml() { + File file = new File(System.getProperty("user.dir") + "/browserstack.yml"); + try (InputStream in = Files.newInputStream(file.toPath())) { + return new Yaml().load(in); + } catch (Exception e) { + throw new RuntimeException("Could not read browserstack.yml: " + e.getMessage(), e); + } + } + + private String envOrYaml(String envKey, Map yaml, String yamlKey) { + String fromEnv = System.getenv(envKey); + if (fromEnv != null && !fromEnv.isEmpty()) return fromEnv; + Object fromYaml = yaml.get(yamlKey); + return fromYaml == null ? null : fromYaml.toString(); + } +} diff --git a/src/test/java/com/browserstack/stepdefs/local/StackLocalSteps.java b/src/test/java/com/browserstack/stepdefs/local/StackLocalSteps.java new file mode 100644 index 0000000..7f618cb --- /dev/null +++ b/src/test/java/com/browserstack/stepdefs/local/StackLocalSteps.java @@ -0,0 +1,89 @@ +package com.browserstack.stepdefs.local; + +import com.microsoft.playwright.Browser; +import com.microsoft.playwright.BrowserContext; +import com.microsoft.playwright.BrowserType; +import com.microsoft.playwright.Page; +import com.microsoft.playwright.Playwright; +import io.cucumber.java.After; +import io.cucumber.java.Before; +import io.cucumber.java.Scenario; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import org.json.JSONObject; +import org.testng.Assert; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; + +public class StackLocalSteps { + private Playwright playwright; + private Browser browser; + private BrowserContext context; + private Page page; + + @Before + public void setUp(Scenario scenario) { + playwright = Playwright.create(); + BrowserType browserType = playwright.chromium(); + + Map config = loadBrowserStackYaml(); + String userName = envOrYaml("BROWSERSTACK_USERNAME", config, "userName"); + String accessKey = envOrYaml("BROWSERSTACK_ACCESS_KEY", config, "accessKey"); + + HashMap caps = new HashMap<>(); + caps.put("browserstack.user", userName); + caps.put("browserstack.key", accessKey); + caps.put("browserstack.source", "cucumber-java-playwright:sample-master:v1.0"); + caps.put("browser", "chrome"); + caps.put("browserstack.local", "true"); + caps.put("sessionName", scenario.getName()); + + String encoded = URLEncoder.encode(new JSONObject(caps).toString(), StandardCharsets.UTF_8); + String wsEndpoint = "wss://cdp.browserstack.com/playwright?caps=" + encoded; + + browser = browserType.connect(wsEndpoint); + context = browser.newContext(new Browser.NewContextOptions().setViewportSize(1920, 1080)); + page = context.newPage(); + } + + @Given("I am on {string}") + public void i_am_on(String url) { + page.navigate(url); + } + + @Then("the page title should contain {string}") + public void the_page_title_should_contain(String expected) { + Assert.assertTrue(page.title().contains(expected), + "expected title to contain '" + expected + "' but was '" + page.title() + "'"); + } + + @After + public void tearDown() { + if (context != null) context.close(); + if (browser != null) browser.close(); + if (playwright != null) playwright.close(); + } + + private Map loadBrowserStackYaml() { + File file = new File(System.getProperty("user.dir") + "/browserstack.yml"); + try (InputStream in = Files.newInputStream(file.toPath())) { + return new Yaml().load(in); + } catch (Exception e) { + throw new RuntimeException("Could not read browserstack.yml: " + e.getMessage(), e); + } + } + + private String envOrYaml(String envKey, Map yaml, String yamlKey) { + String fromEnv = System.getenv(envKey); + if (fromEnv != null && !fromEnv.isEmpty()) return fromEnv; + Object fromYaml = yaml.get(yamlKey); + return fromYaml == null ? null : fromYaml.toString(); + } +} diff --git a/src/test/resources/features/localtest/local.feature b/src/test/resources/features/localtest/local.feature new file mode 100644 index 0000000..ade6152 --- /dev/null +++ b/src/test/resources/features/localtest/local.feature @@ -0,0 +1,6 @@ +@local +Feature: BrowserStack Local - Reach localhost via tunnel + + Scenario: Reach a page served on localhost through BrowserStack Local + Given I am on "http://bs-local.com:45454/" + Then the page title should contain "BrowserStack Local" diff --git a/src/test/resources/features/test/e2e.feature b/src/test/resources/features/test/e2e.feature new file mode 100644 index 0000000..b8d7e2f --- /dev/null +++ b/src/test/resources/features/test/e2e.feature @@ -0,0 +1,7 @@ +@e2e +Feature: BStackDemo - Add product to cart + + Scenario: Add a product to the cart on bstackdemo.com + Given I am on "https://www.bstackdemo.com" + When I add the first product to the cart + Then the product in the cart should match the product I added diff --git a/src/test/resources/testng.xml b/src/test/resources/testng.xml new file mode 100644 index 0000000..427ecd6 --- /dev/null +++ b/src/test/resources/testng.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/test/resources/testngLocal.xml b/src/test/resources/testngLocal.xml new file mode 100644 index 0000000..9e1ffa3 --- /dev/null +++ b/src/test/resources/testngLocal.xml @@ -0,0 +1,9 @@ + + + + + + + + + From 5ae343579ec863dbb6254db4f99bb5b2687c73bf Mon Sep 17 00:00:00 2001 From: Jimesh-browserstack Date: Fri, 24 Apr 2026 14:04:10 +0530 Subject: [PATCH 2/4] Preserve root CODEOWNERS from main Keeps the root-level CODEOWNERS file so merging to main does not remove it. .github/CODEOWNERS takes precedence for GitHub's ownership routing. Co-Authored-By: Claude Opus 4.7 (1M context) --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..d7a5318 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @browserstack/automate-dev \ No newline at end of file From f71545c6fd2d09009823fdb04b938d3ecaccdf12 Mon Sep 17 00:00:00 2001 From: Jimesh-browserstack Date: Fri, 24 Apr 2026 14:17:11 +0530 Subject: [PATCH 3/4] Add least-privilege permissions to cucumber workflow Addresses CodeQL finding: workflow did not limit GITHUB_TOKEN permissions. - Top-level: contents: read - Job-level: checks: write (required by actions/github-script calls to github.rest.checks.create) Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/cucumber-workflow-run.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/cucumber-workflow-run.yml b/.github/workflows/cucumber-workflow-run.yml index 6b13550..86a08fc 100644 --- a/.github/workflows/cucumber-workflow-run.yml +++ b/.github/workflows/cucumber-workflow-run.yml @@ -11,9 +11,15 @@ on: description: 'The full commit id to build' required: true +permissions: + contents: read + jobs: comment-run: runs-on: ${{ matrix.os }} + permissions: + contents: read + checks: write strategy: fail-fast: false max-parallel: 3 From 40cfb6f58875ddb4287ba1b11f7ec6aabae421c5 Mon Sep 17 00:00:00 2001 From: Jimesh-browserstack Date: Fri, 24 Apr 2026 16:45:09 +0530 Subject: [PATCH 4/4] Drop .github/CODEOWNERS; keep root CODEOWNERS as-is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert the CODEOWNERS move so this PR doesn't introduce a new ownership file on main. Root CODEOWNERS (@browserstack/automate-dev) remains unchanged — matching what main already enforces. --- .github/CODEOWNERS | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 8da635d..0000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,3 +0,0 @@ -.github/workflows/*.yml @browserstack/asi-devs - -* @browserstack/automate-public-repos