diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9de86f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__ +.pytest_cache/ +local.log +env diff --git a/CODEOWNERS b/CODEOWNERS index 444c87b..c9eea17 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @browserstack/afd-dev +* @browserstack/automate-public-repos diff --git a/README.md b/README.md index 26bc2f2..f98d1ee 100644 --- a/README.md +++ b/README.md @@ -10,29 +10,14 @@ Playwright with PyTest Test runner Integration with BrowserStack. * Clone the repo * Install dependencies `pip install -r requirements.txt` -* To run your automated tests using BrowserStack, you must provide a valid username and access key. This can be done by setting the BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY environment variables. +* To run your automated tests using BrowserStack, you must provide a valid username and access key. This can be done adding your userName and accesKey in the `browserstack.yml` file. +* You can also set the credentials in BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY environment variables. ## Run sample tests -* To run parallel tests, run `paver run sample-test remote` +* To run tests, run `browserstack-sdk pytest -s tests/sample-test.py` +* To run local tests, run `browserstack-sdk pytest -s tests/sample-local-test.py`. -## Run tests on locally hosted websites -* To run a local test, (if you have not set the BROWSERSTACK_ACCESS_KEY environment variable) first go to resources/local.json then edit key on line 3 -* Run `paver run sample-local-test remote` - - -## Run sample tests locally -* To run tests locally you may have to install the browser dependencies. -* For example, if you want to run the test on Firefox browser, you have to run the below command - -``` -playwright install firefox - -``` - -* After installing the browsers, run `paver run sample-test on-prem` - - - Understand how many parallel sessions you need by using our [Parallel Test Calculator](https://www.browserstack.com/automate/parallel-calculator?ref=github) +## Understand how many parallel sessions you need by using our [Parallel Test Calculator](https://www.browserstack.com/automate/parallel-calculator?ref=github) ## Notes -* You can view your test results on the [BrowserStack Automate dashboard](https://www.browserstack.com/automate) \ No newline at end of file +* You can view your test results on the [BrowserStack Automate dashboard](https://www.browserstack.com/automate) diff --git a/browserstack.yml b/browserstack.yml new file mode 100644 index 0000000..a7afbd9 --- /dev/null +++ b/browserstack.yml @@ -0,0 +1,77 @@ +# ============================= +# 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}' # 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: pytest +useW3C: false +# ======================================= +# 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) +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: true # (Default false) +# 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 + +source: pytest-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) diff --git a/conftest.py b/conftest.py deleted file mode 100644 index bda6698..0000000 --- a/conftest.py +++ /dev/null @@ -1,85 +0,0 @@ -import threading -import urllib -import uuid -#import threading -from multiprocessing import Process, Lock -import pytest -from browserstack.local import Local -import os, json -from jsonmerge import merge -from dotenv import load_dotenv -import time - -from playwright.sync_api import Playwright - -lock = Lock() -threaded_count = 0 - -load_dotenv() - -CONFIG_FILE = os.environ['CONFIG_FILE'] if 'CONFIG_FILE' in os.environ else 'resources/parallel.json' -TASK_ID = int(os.environ['TASK_ID']) if 'TASK_ID' in os.environ else 0 - -with open(CONFIG_FILE) as data_file: - CONFIG = json.load(data_file) - - - - -BROWSERSTACK_USERNAME = os.environ['BROWSERSTACK_USERNAME'] if 'BROWSERSTACK_USERNAME' in os.environ else CONFIG["user"] -BROWSERSTACK_ACCESS_KEY = os.environ['BROWSERSTACK_ACCESS_KEY'] if 'BROWSERSTACK_ACCESS_KEY' in os.environ else CONFIG[ - "key"] - - - -if os.environ['REMOTE'] == "true": - @pytest.fixture(scope='session') - def session_capabilities(playwright: Playwright): - global timenow - global lock - test_name = os.environ.get('PYTEST_CURRENT_TEST').split(' ')[0].split('::')[1] - capabilities = merge(CONFIG['environments'][TASK_ID], CONFIG["capabilities"]) - print(str(capabilities)) - capabilities['browserstack.username'] = BROWSERSTACK_USERNAME - capabilities['browserstack.accessKey'] = BROWSERSTACK_ACCESS_KEY - capabilities['source'] = 'pytest:sample-main:v1.0' - capabilities['sessionName'] = test_name - print(CONFIG['base_url']) - if "local" in capabilities and capabilities['local']: - capabilities['browserstack.local'] = "true" - print("capabilities => " + json.dumps(capabilities)) - stringifiedCaps = urllib.parse.quote(json.dumps(capabilities)) - caps = 'wss://cdp.browserstack.com/playwright?caps=' + stringifiedCaps - browser = playwright.chromium.connect(str(caps)) - context = browser.new_context() - page = context.new_page() - yield page - context.close() - browser.close() -else: - @pytest.fixture(scope='session') - def session_capabilities(playwright: Playwright): - capabilities = CONFIG['environments'][TASK_ID] - if "browser" in capabilities and capabilities['browser'] == 'chrome': - browser = playwright.chromium.launch(channel='chrome', headless=False) - if "browser" in capabilities and capabilities['browser'] == 'edge': - browser = playwright.chromium.launch(channel='msedge', headless=False) - elif "browser" in capabilities and capabilities['browser'] == 'firefox': - browser = playwright.firefox.launch(headless=False) - else: - browser = playwright.webkit.launch(headless=False) - context = browser.new_context() - page = context.new_page() - yield page - context.close() - browser.close() - - -@pytest.fixture(scope='session') -def base_url(): - print(CONFIG['base_url']) - return CONFIG['base_url'] - - -#def pytest_sessionfinish(session, exitstatus): -# stop_local() diff --git a/pavement.py b/pavement.py deleted file mode 100644 index 75ff23f..0000000 --- a/pavement.py +++ /dev/null @@ -1,113 +0,0 @@ -from paver.easy import * -from paver.setuputils import setup -#from multiprocess import Process -import threading, os -import platform -import json -from browserstack.local import Local -from multiprocessing import Lock - -bs_local = None -local_flag = False -lock=Lock() - - -setup( - name = "pytest-browserstack", - version = "0.1.0", - author = "BrowserStack", - author_email = "support@browserstack.com", - description = ("PyTest Playwright Integration with BrowserStack"), - license = "MIT", - keywords = "example Playwright browserstack", - url = "https://github.com/browserstack/pytest-browserstack", - packages=['tests'] -) - -def run_py_test(config, run_type, task_id=0): - print("Thread "+threading.current_thread().name+"; Thread count =>"+str(threading.active_count())) - try: - if config == 'local' and run_type == 'remote': - if platform.system() == "Windows": - sh('cmd /C "set CONFIG_FILE=resources/%s.json && set TASK_ID=%s && set REMOTE=true && pytest -s src/tests/sample-local-test.py --base-url http://bs-local.com:45454"' % (config, task_id)) - else: - sh('CONFIG_FILE=resources/%s.json TASK_ID=%s REMOTE=true pytest -v -s src/tests/sample-local-test.py --base-url http://bs-local.com:45454' % (config, task_id)) - elif run_type == 'remote': - if platform.system() == "Windows": - sh('cmd /C "set CONFIG_FILE=resources/%s.json && set TASK_ID=%s && set REMOTE=true && pytest -s src/tests/sample-test.py --base-url https://bstackdemo.com"' % (config, task_id)) - else: - sh('CONFIG_FILE=resources/%s.json TASK_ID=%s REMOTE=true pytest -s src/tests/sample-test.py --base-url https://bstackdemo.com' % (config, task_id)) - else: - if platform.system() == "Windows": - sh('cmd /C "set CONFIG_FILE=resources/%s.json && set TASK_ID=%s && set REMOTE=false && pytest -s src/tests/sample-test.py --base-url https://bstackdemo.com"' % (config, task_id)) - else: - print("No lccal no parallel") - sh('CONFIG_FILE=resources/%s.json TASK_ID=%s REMOTE=false pytest -s src/tests/sample-test.py --base-url https://bstackdemo.com' % (config, task_id)) - if local_flag: - lock.acquire() - try: - print("Thread count after yield: " + str(threading.active_count())) - if threading.active_count() <= 2: - # Stop Local - stop_local() - finally: - lock.release() - print("Finally") - except Exception as err: - lock.acquire() - try: - print("Thread count after yield: " + str(threading.active_count())) - if threading.active_count() <= 2: - # Stop Local - stop_local() - finally: - lock.release() - print("Finally") - -@task -@consume_nargs(2) -def run(args): - """Run single, local and parallel test using different config.""" - jobs = [] - print(*args) - testrun='parallel' - if args[0] == 'sample-local-test': - testrun='local' - print("Test Run =>", testrun) - config_file = 'resources/%s.json' % (testrun) - with open(config_file) as data_file: - CONFIG = json.load(data_file) - environments = CONFIG['environments'] - global local_flag - if "local" in CONFIG["capabilities"] and CONFIG["capabilities"]["local"]: - access_key = os.environ['BROWSERSTACK_ACCESS_KEY'] if 'BROWSERSTACK_ACCESS_KEY' in os.environ else \ - CONFIG["key"] - print("local is set to true") - local_flag=True - start_local(access_key) - else: - print("local is set to false") - local_flag=False - for i in range(len(environments)): - #p = Process(target=run_py_test, args=(args[0], args[1], i)) - p = threading.Thread(target=run_py_test, args=(testrun, args[1], i), name=str(i)) - jobs.append(p) - p.start() - - - -def start_local(access_key): - print("Starting Local") - """Code to start browserstack local before start of test.""" - global bs_local - bs_local = Local() - bs_local_args = {"key": access_key} - bs_local.start(**bs_local_args) - - -def stop_local(): - print("Stopping Local") - """Code to stop browserstack local after end of test.""" - global bs_local - if bs_local is not None: - bs_local.stop() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 6cb2328..ccbbc7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,12 @@ browserstack-local jsonmerge multiprocess -paver playwright pytest-playwright psutil -pytest +pytest==7.4.4 pytest-variables pytest-xdist pytest-base-url -python-dotenv \ No newline at end of file +python-dotenv +browserstack-sdk diff --git a/resources/local.json b/resources/local.json deleted file mode 100644 index 415b6ec..0000000 --- a/resources/local.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "user": "BROWSERSTACK_USERNAME", - "key": "BROWSERSTACK_ACCESS_KEY", - "base_url":"http://bs-local.com:45454", - "capabilities": { - "projectName": "Pytest Browserstack", - "buildName": "browserstack-build-local", - "local": true, - "debug": "true", - "networkLogs": "true" - - }, - "environments": [ - { - "os": "Windows", - "os_version": "11", - "name": "Test on Chrome latest on Windows 11", - "browser": "chrome", - "browser_version": "latest" - }, - { - "os": "OS X", - "os_version": "Ventura", - "name": "Test on Webkit latest on Ventura OS", - "browser": "playwright-webkit", - "browser_version": "latest" - }, - { - "os": "Windows", - "os_version": "11", - "name": "Test on latest Firefox browser on Windows 11", - "browser": "playwright-firefox" - } - ] -} - diff --git a/resources/parallel.json b/resources/parallel.json deleted file mode 100644 index 52536a3..0000000 --- a/resources/parallel.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "user": "BROWSERSTACK_USERNAME", - "key": "BROWSERSTACK_ACCESS_KEY", - "base_url": "https://bstackdemo.com/", - "capabilities": { - "projectName": "Pytest Browserstack", - "buildName": "browserstack-build-parallel", - "local": false, - "debug": "true", - "networkLogs": "true" - }, - "environments": [ - { - "os": "Windows", - "os_version": "11", - "name": "Test on Chrome latest on Windows 11", - "browser": "chrome", - "browser_version": "latest" - }, - { - "os": "OS X", - "os_version": "Ventura", - "name": "Test on Webkit latest on Ventura OS", - "browser": "playwright-webkit", - "browser_version": "latest" - }, - { - "os": "Windows", - "os_version": "11", - "name": "Test on latest Firefox browser", - "browser": "playwright-firefox" - } - ] -} - diff --git a/src/tests/sample-local-test.py b/src/tests/sample-local-test.py deleted file mode 100644 index 8b4971f..0000000 --- a/src/tests/sample-local-test.py +++ /dev/null @@ -1,38 +0,0 @@ -import re - -import pytest -from playwright.sync_api import expect - - -def test_sample(session_capabilities,base_url) -> None: - - try: - # Load the PAge returned by the fixture - page=session_capabilities - print(page) - #Navigate to the base url - page.goto(base_url, timeout=0) - - #Verify if BrowserStackLocal running - print(page.title()) - #expect(page).to_have_title(re.compile("BrowserStack Lical")) - - assert page.title() == "BrowserStack Local" - mark_test_status("passed", "The title contains " + page.title() + ", BrowserStack local is Up & running", page) - - except Exception as err: - #Extract error message from Exception - error=str(err).split("Call log:")[0].replace("\n"," but ").replace(":","=>").replace("'","") - mark_test_status("failed", error, page) - raise ValueError(error) - -def mark_test_status(status, reason, page): - page.evaluate("_ => {}", "browserstack_executor: {\"action\": \"setSessionStatus\", \"arguments\": {\"status\":\""+ status + "\", \"reason\": \"" + reason + "\"}}"); - -def log_contextual_info(desc,loglevel,page): - page.evaluate("_ => {}", - "browserstack_executor: {\"action\": \"annotate\", \"arguments\": {\"data\":\"" + desc + "\", \"level\": \"" + loglevel + "\"}}"); -@pytest.mark.xfail() -def checkTitle(title): - print("Title =>"+title) - assert title() == "BrowserStack Lical" \ No newline at end of file diff --git a/tests/sample-local-test.py b/tests/sample-local-test.py new file mode 100644 index 0000000..7ab2514 --- /dev/null +++ b/tests/sample-local-test.py @@ -0,0 +1,22 @@ +import re + +import pytest +from playwright.sync_api import expect + +def test_bstack_local_sample(page) -> None: + try: + #Navigate to the base url + page.goto("http://bs-local.com:45454", timeout=0) + + #Verify if BrowserStackLocal running + print(page.title()) + assert page.title() == "BrowserStack Local" + mark_test_status("passed", "BrowserStack local is Up & running", page) + except Exception as err: + #Extract error message from Exception + error=str(err).split("Call log:")[0].replace("\n"," but ").replace(":","=>").replace("'","") + mark_test_status("failed", error, page) + raise ValueError(error) + +def mark_test_status(status, reason, page): + page.evaluate("_ => {}", "browserstack_executor: {\"action\": \"setSessionStatus\", \"arguments\": {\"status\":\""+ status + "\", \"reason\": \"" + reason + "\"}}") diff --git a/src/tests/sample-test.py b/tests/sample-test.py similarity index 75% rename from src/tests/sample-test.py rename to tests/sample-test.py index 60337e0..59410ea 100644 --- a/src/tests/sample-test.py +++ b/tests/sample-test.py @@ -2,13 +2,10 @@ from playwright.sync_api import expect -def test_sample(session_capabilities, base_url) -> None: +def test_bstack_sample(page) -> None: try: - # Load the PAge returned by the fixture - page = session_capabilities - print(page) # Navigate to the base url - page.goto(base_url, timeout=0) + page.goto("https://bstackdemo.com/", timeout=0) # Add the first item to cart page.locator("[id=\"\\31 \"]").get_by_text("Add to cart").click() @@ -24,9 +21,6 @@ def test_sample(session_capabilities, base_url) -> None: # Verify if there is only one item in the shopping cart expect(page.locator(".bag__quantity")).to_have_text("1") - # Log information to console - log_contextual_info("The cart has one item", "info", page) - # Get the handle for cart item cart_item = page.locator(".shelf-item__details") @@ -34,8 +28,6 @@ def test_sample(session_capabilities, base_url) -> None: #expect(cart_item.locator(".title")).to_have_text(phone) print("Cart item => "+cart_item.locator(".title").all_inner_texts()[0]) assert cart_item.locator(".title").all_inner_texts()[0]==phone[0] - - # Update the test result mark_test_status("passed", "The cart has " + str(cart_item.locator(".title").all_inner_texts()[0]), page) except Exception as err: # Extract error message from Exception @@ -47,8 +39,3 @@ def test_sample(session_capabilities, base_url) -> None: def mark_test_status(status, reason, page): page.evaluate("_ => {}", "browserstack_executor: {\"action\": \"setSessionStatus\", \"arguments\": {\"status\":\"" + status + "\", \"reason\": \"" + reason + "\"}}"); - - -def log_contextual_info(desc, loglevel, page): - page.evaluate("_ => {}", - "browserstack_executor: {\"action\": \"annotate\", \"arguments\": {\"data\":\"" + desc + "\", \"level\": \"" + loglevel + "\"}}");