Playwright Codegen for Python: Record pytest Tests and Pick Locators

Codegen is Playwright’s test recorder. You click through your application in a real browser, and Playwright writes the Python code for every action as you go, including good locators. In this lesson you will record pytest tests, read the generated code line by line, add assertions from the recorder toolbar, and turn a raw recording into a clean, reliable test.

Advertisement

πŸ“– What You’ll Learn: starting Codegen with the python-pytest target, saving recordings to a file, what the generated code means, the recorder toolbar (record, pick locator, assertions), emulating devices, languages and logins while recording, recording from inside a test with page.pause(), and cleaning up recorded code.


πŸŽ₯ Start Recording

Activate your virtual environment and run:

⌨️ Terminal

playwright codegen --target python-pytest https://www.saucedemo.com/

Two windows open:

  1. A browser showing the URL. Everything you do here is recorded.
  2. The Playwright Inspector, which shows the generated code live.

--target python-pytest tells Codegen to write a pytest test function that uses the page fixture. Other Python targets are python (a sync script with sync_playwright, the default) and python-async (an asyncio script).

To save the recording straight into your project, add -o (or --output):

⌨️ Terminal

playwright codegen --target python-pytest -o tests/test_recorded_login.py https://www.saucedemo.com/

The file is written when you close the browser window.


πŸ“ What Codegen Writes

We recorded this flow on Sauce Demo: click the Username field, type standard_user, click the Password field, type secret_sauce, click Login, click Add to cart for the backpack, then open the cart. Codegen produced exactly this:

πŸ“„ tests/test_recorded_login.py

import re
from playwright.sync_api import Page, expect


def test_example(page: Page) -> None:
    page.goto("https://www.saucedemo.com/")
    page.locator("[data-test=\"username\"]").click()
    page.locator("[data-test=\"username\"]").fill("standard_user")
    page.locator("[data-test=\"password\"]").click()
    page.locator("[data-test=\"password\"]").fill("secret_sauce")
    page.locator("[data-test=\"login-button\"]").click()
    page.locator("[data-test=\"add-to-cart-sauce-labs-backpack\"]").click()
    page.locator("[data-test=\"shopping-cart-link\"]").click()

Reading it line by line:

  • import re is always added, so you can use regular expressions in assertions. Page and expect are imported for the test.
  • def test_example(page: Page) -> None: is a normal pytest test. The -> None type hint just says the function returns nothing. Rename the function to describe the behaviour.
  • page.goto(...) is the URL you started with.
  • Each click and each typed value became one line. Codegen records the click into a field and the fill separately; the click is harmless but unnecessary, because fill() focuses the field itself.
  • This site gives its elements data-test attributes, so Codegen used them: [data-test="username"] is a CSS attribute selector. The \" sequences are just double quotes escaped inside a Python string.

⚠️ Watch out: The recording has no assertions. It proves the clicks did not crash, not that the app works. A test without an expect() is not a real test yet.


🎯 How Codegen Chooses Locators

Codegen looks at each element and picks the most stable, human-readable locator it can, roughly in this order: test-id attributes, then role plus accessible name, then text, placeholder or label, and CSS only as a fallback. On the SQA Tools practice page, which has no test-id attributes, we recorded typing a username, choosing Female, ticking Python, choosing India and clicking Submit:

import re
from playwright.sync_api import Page, expect


def test_example(page: Page) -> None:
    page.goto("https://sqatools.in/automation-practice-page/")
    page.get_by_role("textbox", name="Enter username").click()
    page.get_by_role("textbox", name="Enter username").fill("qa_learner")
    page.get_by_role("radio", name="Female").check()
    page.get_by_role("checkbox", name="Python").check()
    page.locator("#country").select_option("india")
    page.get_by_role("button", name="Submit").click()
  • get_by_role("textbox", name="Enter username") finds the input by its role and accessible name, which here comes from its placeholder text.
  • Clicking a radio button or checkbox was recorded as check(), which is more precise than click(): it only ticks, never unticks.
  • The dropdown has the id country, so Codegen used the CSS id selector #country, and it selected the option by its value (india), not its visible label (India).

If your app uses a different test-id attribute name, tell Codegen with --test-id-attribute:

⌨️ Terminal

playwright codegen --target python-pytest --test-id-attribute data-qa https://example.com

🧰 The Recorder Toolbar

The toolbar at the top of the browser window (and in the Inspector) has these tools:

ToolWhat it does
RecordStart or pause recording actions
Pick locatorHover over any element to see the locator Codegen would use; click to copy it into the Inspector
Assert visibilityClick an element to add expect(...).to_be_visible()
Assert textClick an element to add expect(...).to_contain_text(...), or to_have_text(...) when you keep the whole text
Assert valueClick an input to add expect(...).to_have_value(...)
Assert snapshotClick an element to add an accessibility-tree assertion (to_match_aria_snapshot)

Even if you write your tests by hand, Pick locator is extremely useful: it shows you the best locator for any element in seconds.


🧹 From Recording to a Real Test

Raw recordings are a starting point. Clean them up in four steps:

  1. Rename test_example to describe the behaviour.
  2. Delete the unnecessary click() before each fill().
  3. Add assertions after each important step (the recorder’s assert tools help).
  4. Prefer user-facing locators where they read better, and move hard-coded data into variables.

Here is the Sauce Demo recording after cleanup (tests/test_cart.py):

πŸ“„ tests/test_cart.py

import re

from playwright.sync_api import Page, expect


def test_user_can_add_backpack_to_cart(page: Page) -> None:
    # Log in
    page.goto("https://www.saucedemo.com/")
    page.locator("[data-test='username']").fill("standard_user")
    page.locator("[data-test='password']").fill("secret_sauce")
    page.locator("[data-test='login-button']").click()
    expect(page).to_have_url(re.compile(r".*/inventory\.html"))

    # Add the backpack to the cart
    page.locator("[data-test='add-to-cart-sauce-labs-backpack']").click()
    expect(page.locator("[data-test='shopping-cart-badge']")).to_have_text("1")

    # Open the cart and check its contents
    page.locator("[data-test='shopping-cart-link']").click()
    expect(page).to_have_url(re.compile(r".*/cart\.html"))
    expect(page.locator("[data-test='inventory-item-name']")).to_have_text("Sauce Labs Backpack")

What changed:

  • Single quotes inside the selectors ([data-test='username']) avoid the \" escapes and are easier to read. Both forms are valid CSS.
  • Three checkpoints now prove the flow works: the inventory page opened, the cart badge shows 1, and the cart contains the backpack.
  • Comments split the test into readable stages.

And the practice-page recording, cleaned up (tests/test_practice_form.py):

πŸ“„ tests/test_practice_form.py

from playwright.sync_api import Page, expect


def test_fill_practice_form(page: Page) -> None:
    page.goto("https://sqatools.in/automation-practice-page/")

    username = page.get_by_role("textbox", name="Enter username")
    username.fill("qa_learner")
    expect(username).to_have_value("qa_learner")

    female = page.get_by_role("radio", name="Female")
    female.check()
    expect(female).to_be_checked()

    python = page.get_by_role("checkbox", name="Python")
    python.check()
    expect(python).to_be_checked()

    country = page.locator("#country")
    country.select_option("india")
    expect(country).to_have_value("india")
  • Storing a locator in a variable (username = ...) avoids repeating it for the action and the assertion.
  • to_have_value, to_be_checked and friends verify that each action really took effect.

Both tests pass:

πŸ–₯️ Output

tests/test_cart.py::test_user_can_add_backpack_to_cart[chromium] PASSED
tests/test_practice_form.py::test_fill_practice_form[chromium] PASSED
tests/test_recorded_login.py::test_example[chromium] PASSED

βš™οΈ Recording with Emulation and Saved Logins

Codegen accepts the same kind of options as a browser context, so you can record exactly the situation you want to test:

⌨️ Terminal

# Record on an emulated phone
playwright codegen --target python-pytest --device "iPhone 13" https://www.saucedemo.com/

# Record with a fixed window size, language and time zone
playwright codegen --viewport-size "1280,720" --lang "en-GB" --timezone "Europe/London" https://www.saucedemo.com/

# Record in dark mode
playwright codegen --color-scheme dark https://www.saucedemo.com/

# Log in once and save the session, then record later flows already logged in
playwright codegen --save-storage auth.json https://www.saucedemo.com/
playwright codegen --load-storage auth.json https://www.saucedemo.com/inventory.html
  • --save-storage auth.json saves cookies and local storage when you close the browser; --load-storage auth.json starts the next session with them, so you skip the login steps.
  • Other useful options: -b firefox or -b webkit to record in another engine, --ignore-https-errors for test servers with self-signed certificates, and --save-har to save all network traffic.

⚠️ Watch out: auth.json contains session cookies, which work like a password. Add it to .gitignore and never commit it.


⏸️ Record from Inside a Test: page.pause()

Sometimes you want to record the middle of a flow, after your test has already logged in or prepared data. Add page.pause() where you want to start, and run the test headed:

def test_record_after_login(page: Page) -> None:
    page.goto("https://www.saucedemo.com/")
    page.locator("[data-test='username']").fill("standard_user")
    page.locator("[data-test='password']").fill("secret_sauce")
    page.locator("[data-test='login-button']").click()

    page.pause()   # opens the Playwright Inspector here

⌨️ Terminal

pytest tests/test_record_after_login.py --headed

The test stops at page.pause() and opens the Inspector. Click Record to record new steps from that point, then copy the generated lines into your test and remove page.pause().


❓ Frequently Asked Questions

πŸ€” Should I build my whole suite with Codegen?

No. Use it to learn the API, discover locators and draft tests quickly. Real suites need assertions, reusable page objects and test data, which you will add yourself in later modules.

πŸ€” Why does Codegen generate page.locator(“#id”) instead of get_by_role?

It chooses the most reliable locator it can find. When an element has no accessible name that is unique on the page, an id or test-id selector is safer. You can always change it.

πŸ€” Can Codegen record file uploads, new tabs and dialogs?

Yes. It records uploads as set_input_files(), new tabs inside a with page.expect_popup() as page1_info: block, downloads inside with page.expect_download(), and dialogs as page.once("dialog", lambda dialog: dialog.dismiss()). Note that the dialog handler dismisses the dialog (like clicking Cancel); change it to dialog.accept() if your test needs OK. You will learn all of these in Module 4.

πŸ€” What is the difference between python and python-pytest targets?

python writes a standalone script that launches and closes the browser itself with sync_playwright(). python-pytest writes a test function that receives the page fixture, ready to drop into your tests folder.


🏁 Summary

  • playwright codegen --target python-pytest -o tests/test_x.py URL records a pytest test.
  • Codegen prefers test ids, then roles and names; Pick locator shows the locator for any element.
  • Recordings have no assertions: rename, remove extra clicks, add expect() checks and tidy locators.
  • Use emulation flags, --save-storage/--load-storage and page.pause() to record exactly the flow you need.

That completes Module 1. Test yourself with the Module 1 mock interview, then continue to Module 2, Python essentials for Playwright.

🎬 Watch the Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on the SQA Tools Automation Practice Page

Leave a Comment