Who Is This Course For?
This course is for QA testers and Python learners who want to master the fastest-growing automation tool in the industry. You will go from writing your first sync_playwright() script all the way to building a professional B148_AFW framework — with config-driven browser selection, Page Object Model, Excel data driving, and an automated Jenkins pipeline that runs your tests on every code push.
- Playwright is a free Microsoft tool — supports Chromium (Chrome/Edge), Firefox, and WebKit (Safari) in one framework
- Auto-waiting built in — it retries every action until the element is ready, unlike Selenium
- Works with Desktop, Web, and Mobile browser testing from one API
- Playwright vs older tools — modern locators, Shadow DOM support, built-in assertions
pip install playwrightthenplaywright install— downloads all three browser binarieswith sync_playwright() as p:— context manager handles Playwright lifecycle safelybrowser = p.chromium.launch(headless=False)— opens visible Chromepage = browser.new_page()— create the page object you'll interact with throughoutpage.goto(url),page.title(),browser.close()— the three-line test pattern
page.go_back(),page.go_forward(),page.reload()— browser control methodspage.set_viewport_size({"width": 1280, "height": 720})— consistent screen size for all tests- Switching browsers:
p.firefox,p.webkit— no code change needed beyond this line
page.get_by_placeholder("Search here...")— matches the greyed hint text inside an inputpage.get_by_label("Username")— finds the input associated with a visible labelpage.get_by_alt_text("company logo")— finds images by their description textpage.get_by_title("Close dialog")— matches the tooltip text on hoverpage.get_by_text("Submit")— finds any element by its visible textpage.get_by_test_id("login-btn")— the most stable: a special attribute added by developers for automationpage.get_by_role("button", name="Sign In")— finds by what the element IS and what it SAYSpage.locator("css-or-xpath")— universal fallback when nothing else fits
- Partial match — searching "Log" returns "Login" and "Logout"; good for dynamic text
- Exact match —
exact=True— only returns a perfect match; good for specific buttons - Case-insensitive — useful when text capitalisation varies between environments
- Role-based locators map to browser accessibility roles — your tests work even if styling changes
- Tag:
button,input,div— match all elements of that type - ID:
#username— the sharpest selector, one element only - Class:
.btn-primary,.btn-primary.active— match by CSS styling class - Attribute:
input[type='password'],a[href='/login'] - Child:
form > button— direct child only; Descendant:form button— any depth - The standard CSS limitation: you cannot select an element by its text content
:text('Submit')— Playwright's own CSS extension for partial text matching:text-is('Submit')— exact text match (case-sensitive):has-text('Welcome')— container that contains the text anywhere inside it- Attribute wildcards:
[class*='btn']contains,[href^='https']starts-with,[src$='.png']ends-with - When CSS outperforms XPath — for well-structured apps, CSS is shorter and faster
- Relative XPath:
//input[@id='username']— start searching from anywhere in the page - Absolute XPath:
/html/body/form/input— full path from root; breaks when page structure changes - Match by text:
//button[text()='Login']— CSS can't do this; XPath can - Combine conditions:
//tr[@class='active' and @data-id='5']— must match both
starts-with(@id, 'user_')— for auto-generated IDs with a predictable prefixcontains(@class, 'btn')— partial class name matchcontains(text(), 'Welcome')— partial text match, works around whitespace- Browser XPath 1.0 limitation —
ends-with()requires 2.0; usecontains()workaround
following-sibling— "the input that comes after the label saying Email"parent— "the div that directly contains this button"ancestor— "the form that wraps this field no matter how deep"preceding-sibling— "the cell to the left of the cell showing John"- I&D XPath — Independent & Dependent: anchor on a stable element, then traverse to the target
- Real-world practice: complex product tables on Flipkart, Amazon, and Myntra
.filter(has_text="Active")— "from all matching rows, keep only those showing Active".filter(has_not_text="Disabled")— exclude rows that contain this text.filter(has=page.get_by_role("checkbox"))— keep only elements that contain a checkbox inside.filter(visible=True)— exclude hidden elements from your results
.and_(other_locator)— must match both locators simultaneously — AND logic.or_(fallback_locator)— try the first; if not found, try the second — OR logic- Scoped locator:
page.locator("table").locator("tr")— find rows only inside that table
.first— the top-most matching element; avoids index errors on single results.last— the bottom-most match; useful for "most recent" records.nth(2)— third element (0-based index); for known position in a list.count()— how many elements matched? Use in assertions to verify result counts.all()— returns a Python list of all matches — iterate to check each one
- An iFrame is a web document embedded inside the main document — a page within a page
page.frame_locator("iframe[name='payment']")— scope directly into the frame- Work inside the frame just like the main page — no switching, no context juggling
page.frame("frame-name")— access a frame object by its name attributepage.frames— list all frames — useful for discovery when names are unknown- Nested frames:
page.frame_locator("outer").frame_locator("inner")— one line for two levels
- Shadow DOM encapsulates web component internals — normal selectors can't see inside
- Playwright automatically pierces Shadow DOM for most locator types — no manual code needed
- This is a major advantage over Selenium which requires manual
shadow_rootextraction
locator.click()— the single most-used action in every testlocator.fill("value")— wipe field and type at once; fastest for simple inputslocator.press_sequentially("value")— type character by character, triggering keydown/keyup events — for autocomplete fieldslocator.clear()— clear without typing; for conditional form interactionslocator.press("Enter")— press a key; Enter submits, Tab moves focus, Escape cancelslocator.check()/locator.uncheck()— checkboxes; Playwright only acts if state needs to changelocator.select_text()— select all text in a field (Ctrl+A equivalent)locator.focus()/locator.blur()— trigger form field validation eventslocator.scroll_into_view_if_needed()— bring off-screen element into viewport firstlocator.hover()— reveal dropdown menus and tooltips without clicking
locator.dblclick()— double-click for inline editing or file selectionlocator.click(button="right")— right-click to open context menuspage.drag_and_drop("source-css", "target-css")— Kanban cards, file upload zoneslocator.bounding_box()— returns{"x", "y", "width", "height"}— for coordinate-based interactions
page.keyboard.press("Control+A")— shortcut keys and combinationspage.keyboard.down("Shift")/page.keyboard.up("Shift")— hold modifier keyspage.mouse.wheel(0, 300)— scroll the page programmaticallypage.evaluate("() => window.scrollTo(0, 500)")— JavaScript execution as ultimate fallback
locator.is_visible()— True if element is in DOM and visible to userslocator.is_hidden()— True if element is absent or set to display:nonelocator.is_enabled()— True if the element can be interacted withlocator.is_disabled()— True if the element has the disabled attributelocator.is_checked()— True for checked checkboxes and selected radio buttons
locator.text_content()— all text including any hidden nodes in the subtreelocator.inner_text()— only the text visible to users — respects CSS visibilitylocator.input_value()— the current value typed into an input or textarealocator.get_attribute("class")— any HTML attribute value for assertionslocator.inner_html()— the HTML inside an element — for structure assertionslocator.all_text_contents()— list of text from every matching element — for table verification
locator.highlight()— visually marks the found element — debugging without consolelocator.aria_snapshot()— accessibility tree — verify how screen readers perceive the elementlocator.screenshot(path="element.png")— screenshot of just this elementpage.screenshot(path="full.png")— full-page screenshot for bug reportspage.screenshot(type="jpeg", quality=80)— optimised size for CI/CD artifact storage
locator.select_option("CA")— select by the hidden value attribute in HTMLlocator.select_option(label="Canada")— select by the text users see on screenlocator.select_option(index=2)— select the third item (0-based) by positionlocator.select_option(["option1", "option2"])— select multiple options at oncelocator.all_text_contents()— read all available option texts as a Python list
- Iterating over all options — check which are enabled, disabled, or selected
- Using
page.evaluate()as a fallback for custom (non-HTML) dropdown widgets - Assignment exercises: display options in reverse order, alphabetically sorted — interview-ready skills
- Three popup types: alert (just OK), confirm (OK/Cancel), prompt (text input + OK/Cancel)
page.once("dialog", lambda d: d.accept())— handle the very next popup onlypage.on("dialog", my_handler)— handle ALL popups for the entire test automaticallydialog.accept()— click OK; works for alert and confirm dialogsdialog.dismiss()— click Cancel on a confirm or promptdialog.message— read the popup message before deciding how to responddialog.type— check whether it's an alert, confirm, or promptdialog.accept("typed text")— fill in the prompt text box and confirm
context.grant_permissions(["geolocation"])— pre-approve before navigating- Notifications, camera, microphone — granted at the browser context level, not mid-test
- Playwright can make direct HTTP requests without a browser — faster than UI for validation
- Broken link verification — scan a page's links via API calls instead of clicking each one (100x faster)
- JSON response handling — assert API data directly from Playwright test code
- Playwright's default 30-second action timeout — it retries automatically before failing
page.set_default_timeout(5000)— tune the global patience for your app's speedlocator.wait_for(state="visible")— wait for:attached,visible,hidden,detachedpage.wait_for_url("**/dashboard")— wait until URL matches a pattern after navigationpage.wait_for_load_state("networkidle")— wait until all background XHR requests settlepage.wait_for_load_state("domcontentloaded")— HTML is parsed; images may still be loadingpage.wait_for_load_state("load")— everything including images and stylesheets donepage.wait_for_selector("css-or-xpath")— legacy-style wait by selector stringpage.wait_for_event("download")— wait for a specific browser event to firepage.wait_for_function("() => window.isLoaded === true")— custom JavaScript conditionpage.set_default_navigation_timeout(10000)— separate timeout just for page navigation
from playwright.sync_api import expect— import once, use everywhereexpect(page).to_have_title("Dashboard")— fails if title doesn't match within 5 secondsexpect(page).to_have_url("https://app.example.com/home")- Regex matching:
expect(page).to_have_title(re.compile("dash", re.IGNORECASE))— partial, case-insensitive - 5-second default — long enough to handle slow renders without hard-coded sleeps
expect(locator).to_be_visible()— fail if element is absent or hiddenexpect(locator).to_be_hidden()— fail if element is showing when it should be hiddenexpect(locator).to_be_enabled()— fail if button is greyed outexpect(locator).to_be_disabled()— fail if field is editable when it shouldn't beexpect(locator).to_be_focused()— fail if cursor is not in this fieldexpect(locator).to_be_checked()— fail if checkbox is uncheckedexpect(locator).to_have_text("Welcome, John")— fail if message doesn't match
- Install Node.js Playwright on the remote machine:
npm install playwright - Start the browser server:
npx playwright run-server --port=8082 --host=0.0.0.0 - Connection URL:
ws://server-ip:8082/— WebSocket protocol - Python connects:
p.chromium.connect("ws://server-ip:8082/")— rest of your code unchanged - Remote machine runs headless — no display required; perfect for CI/CD servers
- Compatibility testing across Windows, macOS, and Linux without owning every machine
- BrowserStack — cloud service with thousands of real browser/OS combinations on demand
- Connect to BrowserStack exactly like your own remote server — identical Python code
- Multi-browser parallel testing via remote grid — all browsers tested simultaneously
@pytest.fixture(autouse=True)— browser opens before each test, closes after, automatically@pytest.mark.smoke,@pytest.mark.regression— tag tests and run subsets on demandconftest.py— shared fixtures for all test files; no import needed- Fixture scopes:
sessionopens browser once for all tests;functionopens fresh for each @pytest.fixture(params=[user1, user2])— run fixture once per data item automaticallyrequest.param— access the current data item inside the parametrized fixturepytest -n 4(pytest-xdist) — run 4 tests simultaneously on 4 CPU corespytest -vs --alluredir=allure-results— run tests and collect Allure data
- Three folders:
generics/(setup),page/(page classes),tests/(test files) base_test.py—BaseTestclass with@pytest.fixture(autouse=True)that reads config and launches browserconfig.properties— one file controls browser choice, URL, timeout, local vs remotepyjavaproperties:p = Properties(); p.load(...)— read config without hardcoding values- Switch from Chrome to Firefox: change one line in config — no code change
- Encapsulation (DUI) — Declaration, Initialization, Utilization in every page class
login_page.py—set_username(),set_password(),click_go_button()— tests call methods, not locatorsutility.py—openpyxl-based Excel reader shared across all tests
- GitHub SCM — version control, team collaboration, pull requests from PyCharm
allure serve allure-results— launch interactive dashboard with step details and screenshots- Jenkins Freestyle Project with Python builder step for running pytest
- Build trigger: push to GitHub → Jenkins detects → tests run automatically
- Scheduled builds:
H * * * *cron syntax — run at specific times daily - Allure post-build action — publish report inside Jenkins UI after every run
- Build stability tracking — see pass/fail trends across 10, 20, 50 consecutive runs
- Full 21-step B148_AFW execution: config read → browser launch → test run → Allure report published
Playwright vs Selenium
Answer confidently: Playwright auto-waits on every action (no sleep timers), supports Chromium/Firefox/Safari from one API, automatically pierces Shadow DOM, and has expect() assertions with built-in retry. These are real daily advantages, not just marketing.
Locator Strategy Questions
Explain the 8 built-in locators and your priority order: get_by_test_id() when available (most stable), get_by_role() for semantic elements, get_by_placeholder() for inputs, CSS for well-structured pages, and XPath with axes for complex dynamic tables.
Test Flakiness Questions
Explain Playwright's five waiting strategies and when each applies: wait_for(state="visible") for appearing elements, wait_for_load_state("networkidle") after Ajax-heavy actions, wait_for_url() after redirects. And explain why expect() beats plain assert for resilience.
Framework Architecture
Describe B148_AFW's design: BaseTest reads config.properties and launches browser automatically, page classes encapsulate all locators (DUI pattern), tests only call methods, utility.py reads Excel data, and Jenkins publishes Allure reports on every push.
Pytest Integration Questions
Explain conftest.py as the automatically-discovered fixture file. Describe scope="session" — one browser for all 200 tests. Explain fixture parametrization with params=[] and request.param. Know how pytest-xdist -n 4 cuts test time by running in parallel.
Remote & CI/CD Questions
Describe the remote server setup: Node.js Playwright server started with npx playwright run-server, Python connects via p.chromium.connect("ws://..."). For Jenkins: Git push triggers build → pytest runs → Allure results collected → report published. Zero human intervention.