Skip to content

Commit adf1607

Browse files
Merge pull request #1 from browserstack/feat/add-sample-code
feat: add Robot Framework sample using playwright-browserstack PyPI package
2 parents 0fc221c + b11f2a2 commit adf1607

7 files changed

Lines changed: 378 additions & 1 deletion

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
output/
2+
*.png
3+
__pycache__/
4+
*.pyc
5+
.env

README.md

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,46 @@
11
# playwright-robot-android-browserstack
2-
Sample project demonstrating Robot Framework with Playwright Library running on real Android devices via BrowserStack Automate.
2+
3+
Sample project demonstrating Robot Framework with the [playwright-browserstack](https://pypi.org/project/playwright-browserstack/) library running on real Android devices via BrowserStack Automate.
4+
5+
## Prerequisites
6+
7+
- Python >= 3.9
8+
- A [BrowserStack](https://www.browserstack.com/) account (set `BROWSERSTACK_USERNAME` and `BROWSERSTACK_ACCESS_KEY`)
9+
10+
## Setup
11+
12+
```bash
13+
pip install -r requirements.txt
14+
```
15+
16+
## Run
17+
18+
```bash
19+
export BROWSERSTACK_USERNAME=<your_username>
20+
export BROWSERSTACK_ACCESS_KEY=<your_access_key>
21+
22+
robot tests/sample_test.robot
23+
```
24+
25+
## Project Structure
26+
27+
```
28+
libraries/
29+
PlaywrightAndroidLibrary.py # Robot Framework keyword library (Playwright Android)
30+
resources/
31+
android_keywords.robot # High-level BrowserStack keywords
32+
variables.robot # Credentials + device capabilities
33+
tests/
34+
sample_test.robot # Sample test suite (The Internet demo site)
35+
output/ # Robot output artifacts (gitignored)
36+
```
37+
38+
## What the sample tests cover
39+
40+
| Test | Action |
41+
|------|--------|
42+
| Verify Page Title | Navigate to The Internet, assert page title |
43+
| Verify Checkbox Interactions | Check/uncheck checkboxes |
44+
| Verify Dropdown Selection | Select Option 1 from a dropdown |
45+
| Verify Go Back Navigation | Browser back navigation |
46+
| Take Screenshot And Mark Passed | Screenshot + mark BrowserStack session passed |
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
"""
2+
Robot Framework keyword library for Playwright Android (BrowserStack).
3+
4+
Usage in .robot files:
5+
Library ../libraries/PlaywrightAndroidLibrary.py
6+
"""
7+
8+
import json
9+
import urllib.parse
10+
from playwright.sync_api import sync_playwright, expect
11+
12+
13+
class PlaywrightAndroidLibrary:
14+
"""Playwright Android keywords for Robot Framework."""
15+
16+
ROBOT_LIBRARY_SCOPE = "SUITE"
17+
18+
def __init__(self):
19+
self._pw = None
20+
self._device = None
21+
self._context = None
22+
self._page = None
23+
24+
# ── Connection ────────────────────────────────────────────────────────────
25+
26+
def connect_to_browserstack(self, username, access_key, caps: dict, timeout=120_000):
27+
"""Connect to a BrowserStack Android device.
28+
29+
Example:
30+
| ${caps}= | Create Dictionary | deviceName=Samsung Galaxy S23 | osVersion=13.0 |
31+
| Connect To BrowserStack | myuser | mykey | ${caps} |
32+
"""
33+
caps = dict(caps)
34+
caps["browserstack.username"] = username
35+
caps["browserstack.accessKey"] = access_key
36+
ws_endpoint = (
37+
"wss://cdp.browserstack.com/playwright?caps="
38+
+ urllib.parse.quote(json.dumps(caps))
39+
)
40+
self._pw = sync_playwright().start()
41+
self._device = self._pw.android.connect(ws_endpoint, timeout=int(timeout))
42+
43+
def get_device_info(self):
44+
"""Return (serial, model) of the connected device."""
45+
self._require_device()
46+
return self._device.serial, self._device.model
47+
48+
# ── Browser ───────────────────────────────────────────────────────────────
49+
50+
def launch_browser(self, pkg=None, has_touch=False):
51+
"""Launch Chrome on the device and create a new page.
52+
53+
Example:
54+
| Launch Browser |
55+
| Launch Browser | has_touch=True |
56+
"""
57+
self._require_device()
58+
try:
59+
self._device.shell("am force-stop com.android.chrome")
60+
except Exception:
61+
pass
62+
self._context = self._device.launch_browser(
63+
pkg=pkg, has_touch=has_touch == "True" or has_touch is True
64+
)
65+
self._page = self._context.new_page()
66+
67+
def close_browser(self):
68+
"""Close the browser context."""
69+
if self._page:
70+
self._page.close()
71+
self._page = None
72+
if self._context:
73+
self._context.close()
74+
self._context = None
75+
76+
# ── Navigation ────────────────────────────────────────────────────────────
77+
78+
def navigate_to(self, url, timeout=30_000):
79+
"""Navigate to a URL.
80+
81+
Example:
82+
| Navigate To | https://example.com |
83+
"""
84+
self._require_page()
85+
self._page.goto(url, timeout=int(timeout))
86+
87+
def go_back(self):
88+
"""Navigate back in browser history."""
89+
self._require_page()
90+
self._page.go_back()
91+
92+
def wait_for_timeout(self, milliseconds):
93+
"""Wait for the given number of milliseconds."""
94+
self._require_page()
95+
self._page.wait_for_timeout(int(milliseconds))
96+
97+
# ── Assertions ────────────────────────────────────────────────────────────
98+
99+
def page_title_should_be(self, expected_title):
100+
"""Assert the current page title equals the expected value."""
101+
self._require_page()
102+
expect(self._page).to_have_title(expected_title)
103+
104+
def page_title_should_contain(self, text):
105+
"""Assert the current page title contains the given text."""
106+
self._require_page()
107+
actual = self._page.title()
108+
assert text in actual, f"Title {actual!r} does not contain {text!r}"
109+
110+
def heading_should_contain(self, name, text):
111+
"""Assert a heading with the given name contains text."""
112+
self._require_page()
113+
heading = self._page.get_by_role("heading", name=name)
114+
content = heading.text_content()
115+
assert text in content, f"Heading text {content!r} does not contain {text!r}"
116+
117+
# ── Interactions ──────────────────────────────────────────────────────────
118+
119+
def click_link(self, name):
120+
"""Click a link by its visible text.
121+
122+
Example:
123+
| Click Link | Checkboxes |
124+
"""
125+
self._require_page()
126+
self._page.get_by_role("link", name=name).click()
127+
128+
def select_option_by_label(self, selector, label):
129+
"""Select a dropdown option by its visible label.
130+
131+
Example:
132+
| Select Option By Label | #dropdown | Option 1 |
133+
"""
134+
self._require_page()
135+
self._page.locator(selector).select_option(label=label)
136+
137+
# ── Checkboxes ────────────────────────────────────────────────────────────
138+
139+
def checkbox_should_be_unchecked(self, index=0):
140+
"""Assert the Nth checkbox (0-based) is unchecked."""
141+
self._require_page()
142+
checkboxes = self._page.get_by_role("checkbox")
143+
cb = checkboxes.nth(int(index))
144+
assert not cb.is_checked(), f"Checkbox {index} should be unchecked"
145+
146+
def checkbox_should_be_checked(self, index=0):
147+
"""Assert the Nth checkbox (0-based) is checked."""
148+
self._require_page()
149+
checkboxes = self._page.get_by_role("checkbox")
150+
cb = checkboxes.nth(int(index))
151+
assert cb.is_checked(), f"Checkbox {index} should be checked"
152+
153+
def check_checkbox(self, index=0):
154+
"""Check the Nth checkbox (0-based)."""
155+
self._require_page()
156+
self._page.get_by_role("checkbox").nth(int(index)).check()
157+
158+
def uncheck_checkbox(self, index=0):
159+
"""Uncheck the Nth checkbox (0-based)."""
160+
self._require_page()
161+
self._page.get_by_role("checkbox").nth(int(index)).uncheck()
162+
163+
# ── Screenshot ────────────────────────────────────────────────────────────
164+
165+
def take_page_screenshot(self, path="screenshot.png"):
166+
"""Save a screenshot of the current page.
167+
168+
Example:
169+
| Take Page Screenshot | output/my_test.png |
170+
"""
171+
self._require_page()
172+
self._page.screenshot(path=path)
173+
174+
def take_device_screenshot(self, path="device_screenshot.png"):
175+
"""Save a full-device screenshot.
176+
177+
Example:
178+
| Take Device Screenshot | output/device.png |
179+
"""
180+
self._require_device()
181+
self._device.screenshot(path=path)
182+
183+
# ── BrowserStack session status ───────────────────────────────────────────
184+
185+
def set_session_name(self, name):
186+
"""Set the BrowserStack session name."""
187+
self._require_page()
188+
self._page.evaluate(
189+
"_ => {}",
190+
f"browserstack_executor: {json.dumps({'action': 'setSessionName', 'arguments': {'name': name}})}",
191+
)
192+
193+
def mark_session_as_passed(self, reason="Test passed"):
194+
"""Mark the BrowserStack session as passed."""
195+
self._set_session_status("passed", reason)
196+
197+
def mark_session_as_failed(self, reason="Test failed"):
198+
"""Mark the BrowserStack session as failed."""
199+
self._set_session_status("failed", reason)
200+
201+
def _set_session_status(self, status, reason):
202+
self._require_page()
203+
self._page.evaluate(
204+
"_ => {}",
205+
f"browserstack_executor: {json.dumps({'action': 'setSessionStatus', 'arguments': {'status': status, 'reason': reason}})}",
206+
)
207+
208+
# ── Teardown ──────────────────────────────────────────────────────────────
209+
210+
def disconnect_device(self):
211+
"""Close the browser, disconnect the device, and stop Playwright."""
212+
self.close_browser()
213+
if self._device:
214+
self._device.close()
215+
self._device = None
216+
if self._pw:
217+
self._pw.stop()
218+
self._pw = None
219+
220+
# ── Internal ──────────────────────────────────────────────────────────────
221+
222+
def _require_device(self):
223+
if self._device is None:
224+
raise RuntimeError("No device connected. Call 'Connect To BrowserStack' first.")
225+
226+
def _require_page(self):
227+
if self._page is None:
228+
raise RuntimeError("No page open. Call 'Launch Browser' first.")

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
playwright-browserstack>=1.56.1
2+
robotframework>=6.0

resources/android_keywords.robot

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
*** Settings ***
2+
Library ../libraries/PlaywrightAndroidLibrary.py
3+
Resource variables.robot
4+
5+
6+
*** Keywords ***
7+
Connect To BrowserStack Device
8+
[Documentation] Open a BrowserStack Android session using suite-level caps.
9+
Connect To BrowserStack ${BS_USERNAME} ${BS_ACCESS_KEY} ${BS_CAPS}
10+
${serial} ${model}= Get Device Info
11+
Log Connected: serial=${serial} model=${model}
12+
13+
Open Chrome On Device
14+
[Documentation] Launch Chrome and open a blank page.
15+
Launch Browser
16+
17+
Open Chrome With Touch On Device
18+
[Documentation] Launch Chrome with touch enabled (required for touchscreen.tap).
19+
Launch Browser has_touch=True
20+
21+
Teardown Session
22+
[Arguments] ${status}=${TEST STATUS} ${msg}=${TEST MESSAGE}
23+
[Documentation] Mark session status, screenshot, then disconnect.
24+
Run Keyword If '${status}' == 'PASS'
25+
... Mark Session As Passed ${msg}
26+
... ELSE Mark Session As Failed ${msg}
27+
Take Page Screenshot output/${TEST NAME}.png
28+
Disconnect Device

resources/variables.robot

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
*** Variables ***
2+
# BrowserStack credentials — override via CLI: -v BS_USERNAME:myuser
3+
${BS_USERNAME} %{BROWSERSTACK_USERNAME}
4+
${BS_ACCESS_KEY} %{BROWSERSTACK_ACCESS_KEY}
5+
6+
# Device capabilities
7+
&{BS_CAPS}
8+
... deviceName=Samsung Galaxy S23
9+
... osVersion=13.0
10+
... browserName=chrome
11+
... realMobile=true
12+
... name=playwright-robot-android-sample
13+
... build=playwright-robot-android
14+
15+
# URLs
16+
${BASE_URL} https://the-internet.herokuapp.com/

tests/sample_test.robot

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
*** Settings ***
2+
Documentation Sample Playwright Android test on BrowserStack using Robot Framework.
3+
... Mirrors browserstack_sample_test.py — tests The Internet demo site.
4+
Library ../libraries/PlaywrightAndroidLibrary.py
5+
Resource ../resources/android_keywords.robot
6+
Suite Setup Connect To BrowserStack Device
7+
Suite Teardown Disconnect Device
8+
Test Teardown Run Keyword If Test Failed Mark Session As Failed ${TEST NAME} failed
9+
10+
11+
*** Test Cases ***
12+
13+
Verify Page Title
14+
[Documentation] Navigate to The Internet and assert the page title.
15+
Open Chrome On Device
16+
Set Session Name playwright-robot-android-sample
17+
Navigate To ${BASE_URL}
18+
Wait For Timeout 3000
19+
Page Title Should Be The Internet
20+
21+
Verify Checkbox Interactions
22+
[Documentation] Check and uncheck checkboxes on the Checkboxes page.
23+
Navigate To ${BASE_URL}
24+
Click Link Checkboxes
25+
26+
# Checkbox 1 starts unchecked — check it
27+
Checkbox Should Be Unchecked 0
28+
Check Checkbox 0
29+
Checkbox Should Be Checked 0
30+
31+
# Checkbox 2 starts checked — uncheck it
32+
Checkbox Should Be Checked 1
33+
Uncheck Checkbox 1
34+
Checkbox Should Be Unchecked 1
35+
36+
Verify Dropdown Selection
37+
[Documentation] Select Option 1 from the dropdown.
38+
Navigate To ${BASE_URL}
39+
Click Link Dropdown
40+
Select Option By Label \#dropdown Option 1
41+
42+
Verify Go Back Navigation
43+
[Documentation] Go back from Dropdown page and verify the heading.
44+
Navigate To ${BASE_URL}
45+
Click Link Dropdown
46+
Go Back
47+
Heading Should Contain Available Examples Available Examples
48+
49+
Take Screenshot And Mark Passed
50+
[Documentation] Take a screenshot and mark the session as passed.
51+
Navigate To ${BASE_URL}
52+
Take Page Screenshot output/final_screenshot.png
53+
Mark Session As Passed All tests completed successfully
54+
Close Browser

0 commit comments

Comments
 (0)