Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Runs the behave + Playwright sample against BrowserStack SDK on workflow_dispatch.
# Mirrors the pattern from browserstack/cucumber-java-playwright-browserstack:
# triggered manually with a commit SHA, posts check statuses back to that SHA so
# results show up on PRs without binding to push/pull_request triggers.

name: Behave Playwright SDK Test workflow on workflow_dispatch

on:
workflow_dispatch:
inputs:
commit_sha:
description: 'The full commit id to build'
required: true

permissions:
contents: read

jobs:
sample-run:
runs-on: ubuntu-latest
permissions:
contents: read
checks: write
name: Behave Playwright Sample
env:
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}

steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha }}

- uses: actions/github-script@98814c53be79b1d30f795b907e553d8679345975
id: status-check-in-progress
env:
job_name: Behave Playwright 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 Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: pip

- name: Install dependencies
run: make install

- name: Run parallel sample on BrowserStack
run: make parallel

- name: Run local sample on BrowserStack
run: make local

- if: always()
uses: actions/github-script@98814c53be79b1d30f795b907e553d8679345975
id: status-check-completed
env:
conclusion: ${{ job.status }}
job_name: Behave Playwright 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')
}
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.venv/
__pycache__/
*.pyc
browserstack.yml.bak
log/
local.log
40 changes: 40 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Makefile for behave + Playwright + BrowserStack SDK sample.
#
# The SDK only reads ./browserstack.yml, so the `single` and `local` targets
# swap in an alternate config from config/ for the duration of the run and
# restore the parallel default afterwards (even on failure).

PARALLEL_CMD := browserstack-sdk behave features/sample.feature
LOCAL_CMD := browserstack-sdk behave features/local.feature

.PHONY: parallel single local install

# Default: parallel run uses the committed browserstack.yml (3 platforms).
parallel:
$(PARALLEL_CMD)

# Single-platform run: swap in config/browserstack.single.yml.
single:
@cp browserstack.yml browserstack.yml.bak; \
cp config/browserstack.single.yml browserstack.yml; \
$(PARALLEL_CMD); status=$$?; \
mv browserstack.yml.bak browserstack.yml; \
exit $$status

# Local-tunnel run: serve a small HTML page on :45454 and point a localhost
# scenario at it. The SDK starts/stops the BrowserStack Local tunnel because
# the alternate config sets `browserstackLocal: true`.
local:
@cp browserstack.yml browserstack.yml.bak; \
cp config/browserstack.local.yml browserstack.yml; \
python3 -m http.server 45454 --directory features/local-html >/tmp/bspl-local-server.log 2>&1 & \
server_pid=$$!; \
sleep 1; \
$(LOCAL_CMD); status=$$?; \
kill $$server_pid 2>/dev/null; \
mv browserstack.yml.bak browserstack.yml; \
exit $$status

install:
pip install -r requirements.txt
playwright install chromium
59 changes: 57 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,57 @@
# behave-playwright-browserstack
Sample repo for customers
behave-playwright-browserstack (BrowserStack SDK + Playwright)
===============================================================

This repo shows how to run [behave](https://behave.readthedocs.io/) tests on BrowserStack using the [BrowserStack Python SDK](https://pypi.org/project/browserstack-sdk/) and [Playwright Python](https://playwright.dev/python/). The SDK handles capability injection, BrowserStack routing for Playwright launches, parallelization, and BrowserStack Local for you — you describe platforms once in `browserstack.yml` and run the test command unchanged.

## Setup
* Clone this repo
* Install dependencies (creates the BrowserStack SDK CLI on `PATH` and downloads Playwright Chromium)
```sh
pip install -r requirements.txt
playwright install chromium
```
Or simply: `make install`
* Set `BROWSERSTACK_USERNAME` and `BROWSERSTACK_ACCESS_KEY` as environment variables, or replace `userName` and `accessKey` directly in `browserstack.yml` (and the variants under `config/`) with your [BrowserStack Username and Access Key](https://www.browserstack.com/accounts/settings). Env vars take precedence.

### Running your tests
* Run tests in parallel across the 3 Playwright browser engines (chromium / firefox / webkit): `make parallel`
* Run a single-platform test: `make single`
* Run with the BrowserStack Local tunnel against a local HTTP server: `make local`

Understand how many parallel sessions you need by using our [Parallel Test Calculator](https://www.browserstack.com/automate/parallel-calculator?ref=github).

Alternatively the variables can be set in the environment using env or your CI framework (like GitHub Actions or Jenkins). See `.github/workflows/build.yml` for a GitHub Actions example — it runs on `workflow_dispatch` (manual trigger) with a commit SHA input and posts a status check back to that commit.

### How the SDK changes things
- **One `browserstack.yml`** declares platforms; the SDK picks them up automatically — no per-task config switching inside `environment.py`.
- **The SDK runs platforms in parallel for you** — no hand-rolled parallel runner; the SDK forks one behave run per `(platform × parallelsPerPlatform)` cell.
- **The SDK monkeypatches Playwright's browser launches** — the test code calls `chromium.launch()` and the SDK transparently routes the launch to the per-platform browser configured in `browserstack.yml` (chromium, firefox, or webkit). No `chromium.connect(wss_url)` plumbing is needed in customer code.
- **The SDK starts and stops BrowserStack Local** when `browserstackLocal: true` — no manual tunnel lifecycle management.
- **The CLI is `browserstack-sdk behave …`** — the `make` targets shell out to that.

### Repo layout
```
.
├── browserstack.yml # 3-platform parallel default
├── config/
│ ├── browserstack.single.yml # 1 platform
│ └── browserstack.local.yml # 1 platform + browserstackLocal: true
├── requirements.txt
├── Makefile
└── features/
├── sample.feature # bstackdemo add-to-cart scenario
├── local.feature # localhost scenario for the Local tunnel
├── local-html/index.html # static page served on :45454 by `make local`
├── environment.py # behave hooks: launch browser, hand to context.page
└── steps/
├── sample_steps.py
└── local_steps.py
```

### Further Reading
- [behave](https://behave.readthedocs.io/)
- [Playwright Python](https://playwright.dev/python/)
- [BrowserStack documentation for Playwright](https://www.browserstack.com/docs/automate/playwright)
- [BrowserStack Python SDK on PyPI](https://pypi.org/project/browserstack-sdk/)

Happy Testing!
61 changes: 61 additions & 0 deletions browserstack.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# =============================
# Set BrowserStack Credentials
# =============================
# Add your BrowserStack userName and accessKey here, or set the
# BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY environment
# variables. Env vars take precedence over the values in this file.
userName: YOUR_USERNAME
accessKey: YOUR_ACCESS_KEY

# ======================
# BrowserStack Reporting
# ======================
projectName: BrowserStack Samples
buildName: behave-playwright-sdk-build-1
# `buildIdentifier` is a unique id appended to buildName for every run.
# ${BUILD_NUMBER} (default) is an incremental counter; ${DATE_TIME} is a timestamp.
buildIdentifier: '#${BUILD_NUMBER}'

# `framework` lets the SDK send test context (test name, status) to BrowserStack.
framework: behave

# =======================================
# Platforms (Browsers / Devices to test)
# =======================================
# Each entry below is one cross-browser cell. The SDK runs `parallelsPerPlatform`
# parallel sessions per entry, so the three entries below x 1 = 3 parallel sessions.
# These three browsers map 1:1 to the three Playwright engine families (chromium /
# firefox / webkit). The customer code in features/environment.py calls
# `chromium.launch()` — the SDK transparently routes the launch to the correct
# per-platform browser at runtime, so no per-platform branching is needed.
platforms:
- os: Windows
osVersion: 11
browserName: chrome
browserVersion: latest
- os: Windows
osVersion: 11
browserName: playwright-firefox
browserVersion: latest
- os: OS X
osVersion: Sonoma
browserName: playwright-webkit
browserVersion: latest

parallelsPerPlatform: 1

# ===================================
# BrowserStack Local (private hosts)
# ===================================
# Default false in this config. The local-mode example sets this true.
browserstackLocal: false

# ===========
# Diagnostics
# ===========
debug: true
networkLogs: false
consoleLogs: errors

# Identifier so BrowserStack can tag the sample source — please leave as-is.
source: behave-playwright-sdk:sample-master:v1.0
28 changes: 28 additions & 0 deletions config/browserstack.local.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
userName: YOUR_USERNAME
accessKey: YOUR_ACCESS_KEY

projectName: BrowserStack Samples
buildName: behave-playwright-sdk-build-1
buildIdentifier: '#${BUILD_NUMBER}'

framework: behave

# Single-platform run for the local-tunnel example.
platforms:
- os: Windows
osVersion: 11
browserName: chrome
browserVersion: latest

parallelsPerPlatform: 1

# BrowserStack Local on: the SDK starts/stops the tunnel for you, and the
# `make local` target serves a small HTML page on http://localhost:45454/
# which the test scenario hits via the tunnel.
browserstackLocal: true

debug: true
networkLogs: false
consoleLogs: errors

source: behave-playwright-sdk:sample-master:v1.0
25 changes: 25 additions & 0 deletions config/browserstack.single.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
userName: YOUR_USERNAME
accessKey: YOUR_ACCESS_KEY

projectName: BrowserStack Samples
buildName: behave-playwright-sdk-build-1
buildIdentifier: '#${BUILD_NUMBER}'

framework: behave

# Single-platform run: one chromium session, no parallelism.
platforms:
- os: Windows
osVersion: 11
browserName: chrome
browserVersion: latest

parallelsPerPlatform: 1

browserstackLocal: false

debug: true
networkLogs: false
consoleLogs: errors

source: behave-playwright-sdk:sample-master:v1.0
18 changes: 18 additions & 0 deletions features/environment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from playwright.sync_api import sync_playwright


def before_scenario(context, scenario):
# Customer code calls `chromium.launch()` directly. The BrowserStack SDK
# monkeypatches Playwright at runtime so this launch is routed to the
# browser configured in the platform entry the SDK is currently driving
# — works unchanged for chromium, firefox, and webkit platforms.
context.pw = sync_playwright().start()
context.browser = context.pw.chromium.launch()
context.page = context.browser.new_page()


def after_scenario(context, scenario):
try:
context.browser.close()
finally:
context.pw.stop()
11 changes: 11 additions & 0 deletions features/local-html/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>BrowserStack Local Sample</title>
</head>
<body>
<h1>BrowserStack Local Sample</h1>
<p>If you can see this page from a BrowserStack session, the Local tunnel is working.</p>
</body>
</html>
5 changes: 5 additions & 0 deletions features/local.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Feature: Local sample

Scenario: Can reach a localhost page through the BrowserStack Local tunnel
Given I visit the local sample page
Then I should see the local sample heading
6 changes: 6 additions & 0 deletions features/sample.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Feature: Browserstack test

Scenario: Can add the product in cart
Given I visit bstackdemo website
When I add a product to the cart
Then I should see same product in cart section
18 changes: 18 additions & 0 deletions features/steps/local_steps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from behave import given, then


LOCAL_URL = "http://localhost:45454/"
EXPECTED_HEADING = "BrowserStack Local Sample"


@given("I visit the local sample page")
def visit_local(context):
context.page.goto(LOCAL_URL)


@then("I should see the local sample heading")
def verify_local_heading(context):
heading = context.page.locator("h1").text_content()
assert heading == EXPECTED_HEADING, (
f"expected {EXPECTED_HEADING!r}, got {heading!r}"
)
Loading
Loading