diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..34bb7be --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,89 @@ +# 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: | + pip install -r requirements.txt + playwright install chromium + + - name: Run sample on BrowserStack + run: browserstack-sdk behave features/sample.feature + + - 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') + } diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39ea696 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.venv/ +__pycache__/ +*.pyc +log/ +local.log diff --git a/README.md b/README.md index 36bd3fb..dbb9c8b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,77 @@ -# 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 + ``` +* Set `BROWSERSTACK_USERNAME` and `BROWSERSTACK_ACCESS_KEY` as environment variables, or replace `userName` and `accessKey` directly in `browserstack.yml` with your [BrowserStack Username and Access Key](https://www.browserstack.com/accounts/settings). Env vars take precedence. + +### Running your tests +There are two sample scenarios in `features/`: + +* **`features/sample.feature`** — drives `https://www.bstackdemo.com` (a public site) and adds a product to the cart. +* **`features/local.feature`** — drives `http://bs-local.com:45454/` through the BrowserStack Local tunnel; verifies the page title contains "BrowserStack Local". + +`browserstack.yml` enables `browserstackLocal: true`, so the SDK starts and stops the BrowserStack Local tunnel for you on every run — no manual binary lifecycle. + +#### Sample test (public site) +Runs in parallel across the 3 Playwright browser engines (chromium / firefox / webkit) declared in `browserstack.yml`: + +```sh +browserstack-sdk behave features/sample.feature +``` + +#### Local test (private / localhost host) +Start a local HTTP server first — `features/local-html/` contains a tiny page titled "BrowserStack Local Test Page": + +```sh +python3 -m http.server 45454 --directory features/local-html +``` + +Then in a separate terminal: + +```sh +browserstack-sdk behave features/local.feature +``` + +`bs-local.com` is a hostname BrowserStack Local resolves to your machine inside the remote browser — for your own app, point your scenarios at `http://bs-local.com:/` instead of a public URL. + +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. +- **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 CLI is `browserstack-sdk behave …`** — wraps `behave` and injects the SDK at runtime. + +### Repo layout +``` +. +├── browserstack.yml # SDK config: credentials, 3 parallel platforms, Local toggle, reporting +├── requirements.txt +└── features/ + ├── sample.feature # bstackdemo add-to-cart scenario + ├── local.feature # BrowserStack Local tunnel scenario + ├── local-html/ + │ └── index.html # static page served on :45454 for local.feature + ├── 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! diff --git a/browserstack.yml b/browserstack.yml new file mode 100644 index 0000000..cf9deaa --- /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: behave-playwright-sdk-build-1 +# `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} +# 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: behave + +source: behave-playwright-browserstack:sample-main:v1.0 + +# ======================================= +# 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) +# The three entries below map 1:1 to the Playwright engine families (chromium / firefox / +# webkit). Customer code calls `chromium.launch()` and the SDK transparently routes the +# launch to the 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 + +# ======================= +# 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: true # # 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) diff --git a/features/environment.py b/features/environment.py new file mode 100644 index 0000000..c3effc3 --- /dev/null +++ b/features/environment.py @@ -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() diff --git a/features/local-html/index.html b/features/local-html/index.html new file mode 100644 index 0000000..61c07cb --- /dev/null +++ b/features/local-html/index.html @@ -0,0 +1,16 @@ + + + + + BrowserStack Local Test Page + + +

BrowserStack Local Test Page

+

+ Served on http://localhost:45454/ for the + features/local.feature scenario. The BrowserStack Local + tunnel resolves bs-local.com:45454 on the remote browser + back to this page. +

+ + diff --git a/features/local.feature b/features/local.feature new file mode 100644 index 0000000..40565a9 --- /dev/null +++ b/features/local.feature @@ -0,0 +1,5 @@ +Feature: Verify BrowserStack Local + + Scenario: Navigate to a page served on localhost through the BrowserStack Local tunnel + Given I visit local app website + Then the page title should contain "BrowserStack Local" diff --git a/features/sample.feature b/features/sample.feature new file mode 100644 index 0000000..49a2f95 --- /dev/null +++ b/features/sample.feature @@ -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 diff --git a/features/steps/local_steps.py b/features/steps/local_steps.py new file mode 100644 index 0000000..61d8d0c --- /dev/null +++ b/features/steps/local_steps.py @@ -0,0 +1,14 @@ +from behave import given, then + + +@given("I visit local app website") +def visit_local_app(context): + context.page.goto("http://bs-local.com:45454/") + + +@then('the page title should contain "{expected}"') +def verify_title_contains(context, expected): + title = context.page.title() + assert expected in title, ( + f"expected title to contain {expected!r}, got {title!r}" + ) diff --git a/features/steps/sample_steps.py b/features/steps/sample_steps.py new file mode 100644 index 0000000..aaad462 --- /dev/null +++ b/features/steps/sample_steps.py @@ -0,0 +1,27 @@ +from behave import given, when, then + + +@given("I visit bstackdemo website") +def visit_bstackdemo(context): + context.page.goto("https://www.bstackdemo.com/") + assert context.page.title() == "StackDemo" + + +@when("I add a product to the cart") +def add_product(context): + product_locator = context.page.locator('xpath=//*[@id="1"]/p') + context.product_on_page_text = product_locator.text_content() + context.page.locator('xpath=//*[@id="1"]/div[4]').click() + + +@then("I should see same product in cart section") +def verify_cart(context): + cart = context.page.locator('xpath=//*[@class="float-cart__content"]') + cart.wait_for(state="visible") + cart_product_locator = context.page.locator( + 'xpath=//*[@id="__next"]/div/div/div[2]/div[2]/div[2]/div/div[3]/p[1]' + ) + product_on_cart_text = cart_product_locator.text_content() + assert product_on_cart_text == context.product_on_page_text, ( + f"expected {context.product_on_page_text!r} in cart, got {product_on_cart_text!r}" + ) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5b2db15 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +browserstack-sdk>=1.46.0 +behave>=1.2.7 +playwright==1.49.0