├── .gitignore ├── ReadMe.md ├── conftest.py ├── pytest.ini ├── requirements.txt ├── test_2021 ├── test_context.py ├── test_evaluate.py ├── test_events.py ├── test_route.py ├── test_selectors.py └── test_waiting.py └── test_2022 ├── test_expect.py ├── test_http.py └── test_locator.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .pytest_cache -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # Code Samples of Playwright Python cool stuff 2 | 3 | By Oleksii Ostapov 4 | 5 | ## Useful links 6 | 7 | - [Playwright website](https://playwright.dev/python/) 8 | - [QA Mania Telegram Channel](https://t.me/qamania) <-- SUBSCRIBE 🥳 9 | - [QA Mania blog](https://qamania.org/) 10 | - [Playwright coding hints collection](https://qamania.org/hint/playwright/) 11 | - [My Playwright + Python course](https://www.udemy.com/course/test-automation-with-playwright-and-python/?referralCode=0C1DD39F2C8A28802F95) 12 | 13 | ## Precondition 14 | 15 | In order to run these tests locally you need 16 | 17 | 1. Install [Sample project](https://github.com/Ypurek/TestMe-TCM) which will be tested by tests. It is easy-to-run Test 18 | Case Management system created with Django with main purpose to be tested by autotests. There is installation guide 19 | 2. Install test dependencies using command `pip install -r requirements.txt` 20 | 3. Install Playwright web browser drivers using command `playwright install`. Check [guide](https://playwright.dev/python/docs/intro#installation) 21 | 22 | ## Overview 23 | In order to simplify code [pytest](https://pytest.org/) is used. It is possible to run same code without, as it provided in official documentation. 24 | [conftest.py](conftest.py) contains main fixture with Playwright itself and page fixture, with already authenticated user 25 | 26 | ## Tests v 2.0 (prepared for Youtube guide at QA Mania) 27 | ### Work with elements with .locator() 28 | **Documentation**: https://playwright.dev/python/docs/locators 29 | **Test**: [test_context.py](test_2022/test_locator.py) 30 | **Description**: .locator() does not raise Exception if element not found, but object, and object can be evaluated. 31 | multiple objects can be found and handled. 32 | locator provides more ways to find element with has-text and has arguments 33 | 34 | ### expect - new way of assertion 35 | **Documentation**: https://playwright.dev/python/docs/test-assertions 36 | **Test**: [test_expect.py](test_2022/test_expect.py) 37 | **Description**: as an alternative to python **assert** method expect can be used. It gets locator object and allows 38 | to check it has or has not attributes, values, css, Checks it is visible or not. Optional stuff, but qute nice 39 | 40 | ### Send HTTP requests with Playwright TODO 41 | **Documentation**: https://playwright.dev/python/docs/api-testing 42 | **Test**: [test_http.py](test_2022/test_http.py) 43 | **Description**: Cool way to test call web API directly from Playwright. No need to run separate http client in your code 44 | 45 | ### Record / replay HAR TODO 46 | **Documentation**: 47 | **Test**: 48 | **Description**: 49 | 50 | ### Fallback routes TODO 51 | **Documentation**: 52 | **Test**: 53 | **Description**: 54 | 55 | ## Tests v 1.0 (prepared for QA Day 2021) 56 | ### Test scenario with multiple roles 57 | **Documentation**: https://playwright.dev/python/docs/multi-pages 58 | **Test**: [test_context.py](test_2021/test_context.py) 59 | **Description**: In this test 2 separate contexts created for different users. They act in the same test at the same time! 60 | 61 | ### Test with injecting JS 62 | **Documentation**: https://playwright.dev/python/docs/api/class-page#page-evaluate 63 | **Test**: [test_evaluate.py](test_2021/test_evaluate.py) 64 | **Description**: In this test JS directly in browser executed and value received back into test. Any king of complicated JS logic can be triggered directly 65 | 66 | ### Test events 67 | **Documentation**: https://playwright.dev/python/docs/events 68 | **Test**: [test_events.py](test_2021/test_events.py) 69 | **Description**: In these tests different scenarios of events handling are shown. My favorite way - create context manager, because in this case you can control upcoming events better 70 | 71 | ### Test routes 72 | **Documentation**: https://playwright.dev/python/docs/network 73 | **Test**: [test_route.py](test_2021/test_route.py) 74 | **Description**: In these tests different scenarios of handling browser HTTP requests are shown. You can abort, modify, mock or just log any browser request 75 | 76 | ### Test selectors 77 | **Documentation**: https://playwright.dev/python/docs/selectors 78 | **Test**: [test_selectors.py](test_2021/test_selectors.py) 79 | **Description**: In this test the most awesome Playwright feature is displayed - Selectors. You have a lot of ways to locate you elements 80 | 81 | ### Test waitings 82 | **Documentation**: https://playwright.dev/python/docs/api/class-page#page-wait-for-selector 83 | **Test**: [test_waiting.py](test_2021/test_waiting.py) 84 | **Description**: In these tests different scenarios shown on how you can wait for element, page load or any kind of event emitted by web browser -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from playwright.sync_api import sync_playwright 3 | 4 | SLO_MO = None 5 | HEADLESS = False 6 | 7 | 8 | @fixture(scope='module') 9 | def chromium(): 10 | """ 11 | Main fixture. Creates Chrome browser and yields it into tests 12 | Shows how to work with docs 13 | """ 14 | with sync_playwright() as p: 15 | chromium = p.chromium.launch(headless=HEADLESS, slow_mo=SLO_MO) 16 | yield chromium 17 | chromium.close() 18 | 19 | 20 | @fixture(scope='module') 21 | def page(chromium): 22 | # base url provided for context, so goto method can user only endpoint 23 | context = chromium.new_context(permissions=["geolocation"], 24 | geolocation={"latitude": 48.8, "longitude": 2.3}, 25 | base_url='http://127.0.0.1:8000') 26 | page = context.new_page() 27 | yield page 28 | page.close() 29 | context.close() 30 | 31 | 32 | @fixture(scope='module') 33 | def alice(page): 34 | page.goto('') 35 | page.fill('#id_username', 'alice') 36 | page.fill('id=id_password', 'Qamania123') 37 | page.click('text="Login"') 38 | return page 39 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | playwright 2 | pytest 3 | requests -------------------------------------------------------------------------------- /test_2021/test_context.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | import random as r 3 | 4 | 5 | @fixture(scope='module') 6 | def bob(chromium): 7 | context = chromium.new_context(permissions=["geolocation"], 8 | geolocation={"latitude": 10, "longitude": 10}) 9 | page = context.new_page() 10 | page.goto('http://127.0.0.1:8000') 11 | page.fill('#id_username', 'bob') 12 | page.fill('id=id_password', 'Qamania123') 13 | page.click('text="Login"') 14 | return page 15 | 16 | 17 | # read more https://playwright.dev/python/docs/multi-pages 18 | def test_roles(alice, bob): 19 | total = bob.text_content('.total span') 20 | 21 | alice.click('a[href="/test/new"]') 22 | alice.fill('#id_name', f'test name {r.random()}') 23 | alice.fill('#id_description', 'new test!') 24 | alice.click('.btn input') 25 | 26 | # make sure response received before checking text content 27 | with bob.expect_response(lambda response: response.status == 200): 28 | bob.click('.refresh input') 29 | new_total = bob.text_content('.total span') 30 | 31 | assert int(total) + 1 == int(new_total) 32 | -------------------------------------------------------------------------------- /test_2021/test_evaluate.py: -------------------------------------------------------------------------------- 1 | # read more https://playwright.dev/python/docs/api/class-page#page-evaluate 2 | def test_js(alice): 3 | result = alice.evaluate(''' 4 | var aaa = function(x, y){ 5 | return x + y; 6 | } 7 | aaa(2, 3); 8 | ''') 9 | assert result == 5 10 | -------------------------------------------------------------------------------- /test_2021/test_events.py: -------------------------------------------------------------------------------- 1 | from playwright.sync_api import Dialog, ConsoleMessage, Page, Response 2 | from contextlib import contextmanager 3 | import json 4 | import time 5 | 6 | 7 | def dialog_handler(dialog: Dialog): 8 | # just to show it works 9 | time.sleep(2) 10 | print('\n\n====== close dialog ====== \n\n') 11 | dialog.accept() 12 | 13 | 14 | def console_handler(message: ConsoleMessage): 15 | print(message.text) 16 | 17 | 18 | @contextmanager 19 | def catch_response(page: Page): 20 | def response_handler(response: Response): 21 | print(json.dumps(response.json(), indent=4)) 22 | 23 | page.on('response', response_handler) 24 | yield 25 | page.remove_listener('response', response_handler) 26 | 27 | 28 | # read more https://playwright.dev/python/docs/events 29 | def test_events(alice): 30 | alice.on('dialog', dialog_handler) 31 | alice.on('console', console_handler) 32 | alice.click('[href="/demo/"]') 33 | alice.click('.newPage') 34 | # alice.click('[href="/"]') 35 | 36 | 37 | # Example of event handling within context manager 38 | def test_events_once(alice): 39 | alice.click('[href="/"]') 40 | alice.once('response', lambda response: print('hello world')) 41 | alice.click('.refresh input') 42 | alice.wait_for_timeout(1000) 43 | 44 | 45 | # Example of event handling within context manager 46 | def test_events_with_cm(alice): 47 | alice.click('[href="/"]') 48 | with catch_response(alice): 49 | alice.click('.refresh input') 50 | alice.wait_for_timeout(1000) 51 | -------------------------------------------------------------------------------- /test_2021/test_route.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from playwright.sync_api import Route, Request 4 | 5 | 6 | # read more https://playwright.dev/python/docs/network 7 | def test_review(alice): 8 | # use list as reference object to transfer data from handler to test 9 | request_data = list() 10 | 11 | def handler(route: Route, request: Request): 12 | request_data.append(request.post_data_json) 13 | route.continue_() 14 | 15 | # can be done in context manager 16 | alice.goto('/tests/') 17 | 18 | alice.route('**/status', handler) 19 | alice.click('tbody tr >> nth=0 >> .passBtn') 20 | alice.unroute('**/status', handler) 21 | 22 | assert request_data[0] == {'status': 'PASS'} 23 | 24 | 25 | def test_fulfill(alice): 26 | def handler(route: Route, request: Request): 27 | route.fulfill(status=200, body=json.dumps({"runId": 1000})) 28 | 29 | # can be done in context manager 30 | alice.goto('/tests/') 31 | 32 | alice.click('tbody tr >> nth=0 >> .passBtn') 33 | alice.route('**/status', handler) 34 | alice.click('tbody tr >> nth=0 >> .failBtn') 35 | alice.unroute('**/status', handler) 36 | alice.reload() 37 | 38 | assert alice.text_content('.testRow_1 .ttStatus') == 'PASS' 39 | 40 | 41 | def test_modify(alice): 42 | def handler(route: Route, request: Request): 43 | route.continue_(post_data=json.dumps({'status': 'FAIL'})) 44 | 45 | # can be done in context manager 46 | alice.goto('/tests/') 47 | 48 | alice.route('**/status', handler) 49 | alice.click('tbody tr >> nth=0 >> .passBtn') 50 | alice.unroute('**/status', handler) 51 | alice.reload() 52 | 53 | assert alice.text_content('.testRow_1 .ttStatus') == 'FAIL' 54 | 55 | 56 | def test_abort(alice): 57 | def handler(route: Route, request: Request): 58 | route.abort() 59 | 60 | # can be done in context manager 61 | alice.goto('/tests/') 62 | alice.click('.testRow_1 .passBtn') 63 | 64 | alice.route('**/status', handler) 65 | alice.click('tbody tr >> nth=0 >> .failBtn') 66 | alice.unroute('**/status', handler) 67 | alice.reload() 68 | 69 | assert alice.text_content('.testRow_1 .ttStatus') == 'PASS' 70 | -------------------------------------------------------------------------------- /test_2021/test_selectors.py: -------------------------------------------------------------------------------- 1 | # read more https://playwright.dev/python/docs/selectors 2 | def test_selectors(alice): 3 | # CSS selector + visible pseudo-class 4 | alice.click('a[href="/demo/"]:visible') 5 | # XPath selector 6 | alice.click('//a[@href="/tests/"]') 7 | # Chain of selectors + intermediate match 8 | test_case = alice.query_selector('xpath=//table >> *css=tr >> text="successfull login check"') 9 | for i in range(4): 10 | test_case.query_selector('.passBtn').click() 11 | test_case.query_selector('.failBtn').click() 12 | 13 | text = "empty" 14 | for i in range(4): 15 | # css selector + has pseudo-class 16 | alice.click(f'tr:has-text("{text}") .passBtn') 17 | alice.click(f'tr:has-text("{text}") .failBtn') 18 | 19 | for i in range(4): 20 | # Chain of selectors + nth selector (numeration from 0) 21 | alice.click('tr >> nth=1 >> .passBtn') 22 | alice.click('tr >> nth=1 >> .failBtn') 23 | -------------------------------------------------------------------------------- /test_2021/test_waiting.py: -------------------------------------------------------------------------------- 1 | from playwright.sync_api import Response 2 | 3 | 4 | # read more https://playwright.dev/python/docs/api/class-page#page-wait-for-selector 5 | def test_wait_state(alice): 6 | alice.click('a[href="/demo/"]') 7 | alice.click('.waitAjax') 8 | # waits for all ajax requests done 9 | alice.wait_for_load_state('networkidle') 10 | assert len(alice.query_selector_all('.ajaxResponses p')) == 5 11 | 12 | 13 | def test_wait_selector(alice): 14 | alice.click('a[href="/demo/"]') 15 | alice.click('.waitAjax') 16 | # waits for selector exists 17 | alice.wait_for_selector('.ajaxResponses p >> nth=4') 18 | assert len(alice.query_selector_all('.ajaxResponses p')) == 5 19 | 20 | 21 | def test_expect_navigation(alice): 22 | alice.click('a[href="/demo/"]') 23 | # stop test execution until navigation is not done 24 | with alice.expect_navigation(url="**/waitPage**"): 25 | alice.click('.waitPage') 26 | assert alice.is_visible('text="Status: ok after 5"') 27 | 28 | 29 | def test_expect_response(alice): 30 | def response_predicate(response: Response): 31 | return response.status == 200 and response.request.method == 'GET' 32 | 33 | alice.click('a[href="/"]') 34 | # stop test execution until http response received with status code 200 35 | with alice.expect_response(response_predicate): 36 | alice.click('input[type=button]') 37 | 38 | 39 | def test_expect_page(alice): 40 | alice.click('a[href="/demo/"]') 41 | # stop test execution until 2nd tab is openned 42 | with alice.context.expect_page() as page: 43 | alice.click('.newPage', modifiers=['Control']) 44 | assert page.value.url == 'http://127.0.0.1:8000/demo/' 45 | page.value.close() 46 | -------------------------------------------------------------------------------- /test_2022/test_expect.py: -------------------------------------------------------------------------------- 1 | from playwright.sync_api import expect 2 | from pytest import fixture 3 | import re 4 | 5 | 6 | @fixture 7 | def alice_tc(alice): 8 | alice.goto('/tests') 9 | alice.locator('.passBtn.pass_1').click() 10 | return alice 11 | 12 | 13 | def test_old_fashion_way(): 14 | x = 2 + 2 15 | assert x == 4 16 | 17 | 18 | def test_objects(alice_tc): 19 | # page 20 | expect(alice_tc).to_have_title('Test Cases') 21 | # locator 22 | locator = alice_tc.locator('.fileUploadBtn') 23 | expect(locator).not_to_have_text('hello') 24 | # API response 25 | response = alice_tc.request.get('/getstat/') 26 | expect(response).to_be_ok() 27 | 28 | 29 | def test_expect_basic(alice_tc): 30 | status = alice_tc.locator('.testStatus_1') 31 | 32 | # assert 'PASS' in status.text_content() 33 | expect(status).to_have_text('PASS') 34 | 35 | # check out regex! 36 | expect(status).to_have_text(re.compile('[PAS]{4}')) 37 | 38 | # assert status.is_visible() 39 | expect(status).to_be_visible() 40 | 41 | # assert not status.is_hidden() 42 | expect(status).not_to_be_hidden() 43 | 44 | # assert '123456' not in status.get_attribute('class') 45 | expect(status).not_to_have_class('123456') 46 | 47 | # assert 'testStatus_1 PASS' == status.get_attribute('class') 48 | expect(status).to_have_class('testStatus_1 PASS') 49 | 50 | 51 | def test_timeout(alice_tc): 52 | status = alice_tc.locator('.testStatus_1') 53 | expect(status).to_have_text('PASS', timeout=10_000) 54 | 55 | 56 | def test_expect_details(alice_tc): 57 | pass_btn = alice_tc.locator('.passBtn.pass_1') 58 | # name and value are mandatory 59 | 60 | # assert pass_btn.get_attribute('onclick') == "setStatus(1, 'PASS')" 61 | expect(pass_btn).to_have_attribute(name='onclick', value="setStatus(1, 'PASS')") 62 | 63 | # no way to get css from locator ¯\_(ツ)_/¯ 64 | expect(pass_btn).to_have_css(name='margin', value='0px 5px 0px 0px') 65 | 66 | 67 | def test_expect_count(alice_tc): 68 | menu_items = alice_tc.locator('.menuInner li') 69 | 70 | # assert menu_items.count() == 5 71 | expect(menu_items).to_have_count(5) 72 | 73 | 74 | def test_input(alice): 75 | text = 'hello world' 76 | alice.goto('/test/new') 77 | test_name = alice.locator('#id_name') 78 | test_name.type(text) 79 | 80 | # assert test_name.input_value() == text 81 | # check input value without overhead :) 82 | expect(test_name).to_have_value(text) 83 | -------------------------------------------------------------------------------- /test_2022/test_http.py: -------------------------------------------------------------------------------- 1 | from playwright.sync_api import expect 2 | from pytest import fixture 3 | 4 | 5 | # import requests 6 | 7 | 8 | # def test_http_old_fashion_way(): 9 | # session = requests.session() 10 | # session.post('/login', {'username': 'user', 'password': 'password'}) 11 | # response = session.get('/requested data') 12 | # data = response.json() 13 | # assert data['key'] == 'value' 14 | 15 | 16 | def test_basic(alice): 17 | # no login in advance 18 | # no full url, base_url provided in the fixture 19 | response = alice.request.get('/getstat') 20 | expect(response).to_be_ok() 21 | 22 | 23 | def test_full_url(alice): 24 | response = alice.request.get('https://github.com/Ypurek/') 25 | expect(response).to_be_ok() 26 | 27 | 28 | def test_new_test_case(alice, http_post_condition): 29 | # get csrf token 30 | alice.goto('/test/new') 31 | csrf_token = alice.locator('form input[type=hidden]').get_attribute('value') 32 | test_name = 'shiny new test of http' 33 | 34 | alice.request.post('/test/new', form={'csrfmiddlewaretoken': csrf_token, 35 | 'name': test_name, 36 | 'description': 'nice'}) 37 | 38 | alice.goto('/tests') 39 | new_test = alice.locator('tbody tr', has_text=test_name) 40 | expect(new_test).to_be_visible() 41 | 42 | 43 | @fixture 44 | def http_post_condition(alice): 45 | yield 46 | alice.goto('/tests') 47 | alice.locator('tbody tr:has-text("shiny new test of http") >> .deleteBtn').click() 48 | -------------------------------------------------------------------------------- /test_2022/test_locator.py: -------------------------------------------------------------------------------- 1 | from playwright.sync_api import expect 2 | from pytest import fixture 3 | 4 | 5 | @fixture 6 | def test_case(alice): 7 | alice.goto('/tests') 8 | return alice 9 | 10 | 11 | def test_locator_does_not_exist(test_case): 12 | not_existing_selector = '.not_existing_selector' 13 | 14 | # old fashion way 15 | assert test_case.query_selector(not_existing_selector) is None 16 | # brand-new with pytest assert 17 | assert test_case.locator(not_existing_selector).is_hidden() 18 | # brand-new with playwright expect 19 | expect(test_case.locator(not_existing_selector)).not_to_be_visible() 20 | 21 | 22 | def test_locator_has_text(test_case): 23 | # find row locator by has_text, then find button locator 24 | row = test_case.locator('tbody tr', has_text='Successfull registration') 25 | row.locator('.passBtn').click() 26 | 27 | # same way with selectors chain and css pseudo class :has-text 28 | test_case.locator('tbody tr:has-text("Successfull registration") >> .passBtn').click() 29 | 30 | 31 | def test_locator_has_locator(test_case): 32 | test_case.locator('tbody tr', has=test_case.locator('.delete_1')).locator('.passBtn').click() 33 | test_case.locator('tbody tr:has(.delete_1) .passBtn').click() 34 | 35 | 36 | def test_locator_different_options(test_case): 37 | # if locator can be changed, you may put few, separated with coma. 1st found will be used 38 | test_case.locator('tbody tr:has-text("Successfull registration") >> .passBtn, pass_1, .aaa').click() 39 | 40 | 41 | def test_locator_multiple_findings_handle(test_case): 42 | # click 1st button 43 | test_case.locator('.passBtn').first.click() 44 | # click 2nd button 45 | test_case.locator('.passBtn').nth(1).click() 46 | 47 | # first property work even if 1 locator found 48 | test_case.locator('.pass_1').first.click() 49 | 50 | 51 | def test_locator_filter(test_case): 52 | # if locator still finds many elements, there is a way to filter them 53 | test_case.locator('tbody tr').filter(has=test_case.locator('.pass_1')).locator('.passBtn').click() 54 | 55 | 56 | def test_locator_wait(alice): 57 | alice.goto('/demo') 58 | alice.locator('.waitAjaxRequests').fill('3') 59 | alice.locator('.waitAjax').click() 60 | last_record = alice.locator('p >> nth=2') 61 | # wait until element is visible 62 | # also we can wait it disappear 63 | # last_record.wait_for(state='hidden') 64 | last_record.wait_for() 65 | assert last_record.is_visible() 66 | 67 | 68 | def test_check_all_text(test_case): 69 | expected_summary = ['Successfull registration check', 70 | 'fail password registration check', 71 | 'fail empty form registration check', 72 | 'successfull login check', 73 | 'Check Test Cases list', 74 | ] 75 | actual = test_case.locator('tbody tr td:nth-child(2)').all_text_contents() 76 | assert all(summary in actual for summary in expected_summary) 77 | --------------------------------------------------------------------------------