The gist
Imagine you hired a tester who works at computer speed: in 30 seconds they check 50 forms, click every button, fill in the fields, confirm the right message appears, and do it exactly the same way every time. Playwright (Microsoft's browser automation library) + Claude Code is that tester. You watch it work, spot problems, tell it to fix them, and it gets better right in front of you.
Terms in this lesson: API (application programming interface, the way programs talk to each other), headless (running without a visible window), CI/CD (Continuous Integration/Delivery, automatic building and shipping), deploy (putting code live on a server), workflow (a sequence of automated steps).
Key concepts
- Claude Code controls the browser: clicks, typing, scraping, screenshots
- Playwright: headed (a browser with a visible window) vs. headless (a browser with no window, running in the background) modes
- A live example: QA (quality assurance) testing of a multi-page form, iteration by iteration
- Parallel browsers for speed
- A signed-in browser through a Chrome profile
Theory
What Claude Code can do with a browser
Through Playwright, Claude Code can control a browser from code:
- Navigation: open a URL, go back/forward, reload the page
- Interaction: click an element, type text, pick from a dropdown, upload a file
- Scraping: pull out text, tables, images, metadata
- Screenshots: capture the whole page or a specific element
- Waiting: wait for an element to appear, disappear or change
- Request interception: intercept and modify API requests
Playwright gives you full control of the browser from code. There's also a second path that needs no script of your own: the Claude in Chrome extension (more below).
The second path: Claude in Chrome
Claude Code can work with your own browser through the Claude in Chrome extension. You don't need to write a script: you describe the task in words, and Claude opens tabs, clicks, reads the console and takes screenshots.
claude --chromeThe /chrome command in a session shows the connection status and permission settings. What you need: Google Chrome or Microsoft Edge (other Chromium-based browsers are detected too), a current version of the Claude in Chrome extension, a paid Anthropic plan (Pro, Max, Team or Enterprise) and signing in with /login. The integration doesn't work with an API key alone. It isn't supported in WSL.
The fine print:
- Claude uses your browser sessions, so it can open any site where you're already signed in. That's convenient and dangerous at the same time: see the Permissions and security lesson.
- If it hits a sign-in page or a CAPTCHA, Claude stops and asks you to handle it by hand. You shouldn't try to get past a CAPTCHA with automation, and it won't work anyway.
- When you need a repeatable test that runs on a server with no screen, write a Playwright script: it's faster and isn't tied to your browser.
- The Claude in Chrome extension is out of beta; for the current requirements, including the minimum extension version, see the documentation and the What's current page.
Headed vs. headless
Headed mode (the browser is visible on screen):
browser = playwright.chromium.launch(headless=False)- You see every action in real time
- You can step in if something goes wrong
- Ideal for development and debugging
- Slower (it renders the interface)
Headless mode (the browser runs in the background):
browser = playwright.chromium.launch(headless=True)- The browser isn't visible; it works in the background
- Faster (no rendering)
- Ideal for production and CI/CD
- Can run on a server with no screen
The golden rule:
- Development → headed (you watch what happens and debug)
- Deployment → headless (speed, and there's no screen on the server)
Use cases
QA automation: Test every form, every button, every user scenario. Write a test once and run it hundreds of times. Catch a regression (an old bug coming back) before your users do.
Web scraping: Collect data from sites that rely on JavaScript (the programming language of web pages, used for dynamic content). A simple HTTP (HyperText Transfer Protocol) request won't work; you have to wait for the DOM (Document Object Model, the structure of the page) to load. Playwright waits automatically.
Automating actions: Fill in forms, post content, interact with web apps the way a user would. For example: automatically post articles to a blogging platform on a schedule (cron, a scheduler that runs tasks at set times).
Visual testing: Take a screenshot of every page and compare it with a reference image. If the pixels changed, something broke visually.
A live example: QA testing a multi-page form
Picture this: you have a 5-page loan application form. You need to make sure that:
- Each page loads without errors
- Validation works correctly (you can't move on with an empty field)
- The progress bar shows the right percentage
- The final page shows the correct summary
Step 1: Claude Code writes a Playwright script
from playwright.sync_api import sync_playwright
def test_loan_application_form():
with sync_playwright() as p:
# Headed mode for development
browser = p.chromium.launch(headless=False)
page = browser.new_page()
# Page 1: Personal details
page.goto("https://myapp.com/apply")
page.fill("#first-name", "John")
page.fill("#last-name", "Smith")
page.fill("#email", "john@example.com")
page.click("#next-button")
# Check that we moved to page 2
page.wait_for_url("**/apply/step-2")
assert page.locator(".progress-bar").get_attribute("value") == "40"
# Page 2: Finances
page.fill("#monthly-income", "5000")
page.fill("#loan-amount", "20000")
page.click("#next-button")
# ... and so onStep 2: Run it and watch
Chrome opens. You watch it automatically:
- Type text into the fields
- Click the "Next" button
- Move to the next page
Step 3: A problem shows up
You notice: the "Next" button gets clicked twice when things run fast. That causes a double submit.
Test error: Expected URL: /apply/step-2 Actual URL: /apply/step-3 # It skipped a page!
Step 4: Stop and fix it
You tell Claude Code: "The button click fires twice, so page 2 gets skipped. Fix it: add wait_for_load_state after the click, and make sure the next page has loaded before the next action."
Claude Code fixes the script:
page.click("#next-button")
page.wait_for_load_state("networkidle") # Wait for the page to fully load
page.wait_for_url("**/apply/step-2") # Make sure we're on the right URLStep 5: Run it again
Now it works correctly. The test gets through all 5 pages.
The development loop:
Build the test → Run it → See a problem → Fix it → Run it againThis loop repeats 5-10 times until the test passes reliably.
Parallel browsers
One browser tests one scenario. Running in parallel tests several scenarios at once:
from playwright.sync_api import sync_playwright
import concurrent.futures
test_cases = [
{"user": "john_smith", "loan": "20000"},
{"user": "mary_jones", "loan": "40000"},
{"user": "alex_brown", "loan": "10000"},
]
def run_test(test_case):
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
# ... the test for this specific case
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
results = executor.map(run_test, test_cases)Three browsers work in parallel, so testing goes 3x faster.
A signed-in browser
The problem: you want to automate Gmail, or any site that requires signing in. Playwright in headless mode isn't signed in.
The solution: use a Chrome profile with saved sessions. It's better to create a separate profile for this (not your main one): the robot will only get access to what you've opened in that profile:
browser = playwright.chromium.launch_persistent_context(
user_data_dir="/Users/yourname/Library/Application Support/Google/Chrome/Default",
headless=False,
channel="chrome"
)Playwright opens Chrome with the sessions and cookies saved in the profile, so the automation works with a signed-in browser. Recent versions of Chrome may refuse to run automation on the default profile, so a separate profile is more reliable.
Where to use it:
- Checking your own account area or your site's admin panel
- Exporting data from your own sections of websites
- Automating SaaS tools that have no API
Important: use this only for your own accounts. Automating other people's accounts violates the services' terms of service. Many platforms (social networks, LinkedIn and others) prohibit automating actions even in your own account: read the terms of service, or you risk getting banned. Don't give the automation more access than the task needs: a session where you're signed in everywhere is a tempting target for prompt injection (see defending against prompt injection).
Live iteration: how the process works
An example process: checking your own online store.
Iteration 1:
Task: every morning, check that the "Add to cart" button works on 50 product pages
Run it → the browser opens → open a product page → click the button
Problem: "the button click fires twice"
(two items end up in the cart)
Fix: wait until the button is active, then click once
---
Iteration 2:
Run it → clicks work → go through the product pages one by one
Problem: "after 20 pages the server responds with error 429 (too many requests)"
Fix: add pauses between pages (1-3 seconds),
check them in batches of 10 with a break instead of all 50 in a row,
be polite to your own server
---
Iteration 3:
Run it → it works reliably → 50 product pages per run with no errors
Run it on a schedule: once a day, with a report sent by emailEach iteration solves a specific problem found in the previous run.
⚠️ Don't automate other people's services in a way that gets around their protections (CAPTCHAs, rate limits): it violates their terms and can get your account banned. Practice on your own sites and test environments.
What to avoid in browser automation
Aggressiveness: actions that are too fast → the server rate-limits or blocks you. Add pauses, respect robots.txt and the terms of service.
No waiting: click() without wait_for_element() → a click on an element that hasn't loaded yet → an error. Always wait for the element before interacting with it.
Hardcoded selectors: #submit-btn-v2 → the site updates → the selector breaks. Use semantic selectors: button[type="submit"], role=button name="Submit".
No retry: the network is flaky → one request fails → the whole test fails. Add retries for unreliable operations.
Practice
Task: write a QA test for a public form
- Pick a public site with a form (for example: a sign-up form on any service, a contact form)
- Ask Claude Code to install Playwright:
pip install playwright && playwright install chromium - Describe the test: "write a Playwright test for the sign-up form at [URL]. The test should check: all fields can be filled in, validation works (an empty field blocks submission), and a successful sign-up shows a confirmation"
- Run it in headed mode and watch the browser
- Find at least one problem (a real one, or enter invalid data on purpose) and ask Claude to fix it
- After a successful run: switch to headless mode and make sure it still works
- Bonus: add a parallel run of two browsers with different test data
Comparing browser automation tools
| Criterion | Playwright | Puppeteer | Selenium |
|---|---|---|---|
| Languages | Python, JS, Java, C# | JavaScript/TypeScript | Python, JS, Java, C#, Ruby |
| Browsers | Chromium, Firefox, WebKit | Chromium (Firefox beta) | All major ones |
| Speed | Fast | Fast | Slower |
| Automatic waiting | Yes (built in) | Partial | No (you do it by hand) |
| Parallel contexts | Yes (browser contexts) | Yes (incognito) | Through Grid |
| Codegen (recording actions) | Yes | No | IDE plugins |
| Mobile device support | Emulation | Emulation | Real devices |
| Best for | Modern projects, QA | Lightweight Chrome scripts | Legacy projects, Selenium Grid |
Recommendation: for new projects, start with Playwright. It has the best balance of speed, documentation and features.
Common mistakes
1. Not waiting for dynamic content
# ❌ Wrong: the element may not have loaded yet
page.click("#dynamic-button")
# ✅ Right: wait for the element to appear
page.wait_for_selector("#dynamic-button")
page.click("#dynamic-button")2. Not using headed mode when debugging If a test fails, switch to headless=False and WATCH what happens. Most problems become obvious once you can see them.
3. Hardcoding XPath instead of semantic selectors
# ❌ Fragile: breaks with any layout change
page.click("//div[3]/div[2]/button[1]")
# ✅ Sturdy: doesn't depend on position in the DOM
page.click("button:has-text('Submit')")
page.get_by_role("button", name="Submit").click()4. Not handling timeouts The network can be slow. Always set a reasonable timeout and handle it.
5. Running tests without retries for unreliable elements Add expect(locator).to_be_visible(timeout=10000) before interacting with elements that load asynchronously.
Tools and resources
- Claude in Chrome: an extension that lets Claude Code work with your browser:
claude --chrome, documentation - Playwright:
pip install playwright, a browser automation library, playwright.dev - Playwright Python docs: playwright.dev/python
- Puppeteer (an alternative for Chrome): pptr.dev
- Selenium (a legacy alternative): selenium.dev
- playwright install: installs the browsers: chromium, firefox, webkit
- Playwright Inspector:
PWDEBUG=1 python test.py, a visual debugger - Playwright Codegen:
playwright codegen https://example.com, records your actions and generates code - Playwright Trace Viewer: replays a test run step by step
Key takeaways
Headed mode for development: you see what's happening and can catch the bug. Headless for production: fast, and it runs on a server with no screen.
The "build → test → watch → fix → test again" loop is normal. 5-10 iterations to get a stable test is a good result.
A signed-in browser through a Chrome profile = automating anything you have access to, without wrestling with OAuth and 2FA.
Related lessons
- Computer Use: controlling native apps (for when Playwright won't do and you need to automate desktop apps)
- Websites and web apps: Playwright is ideal for testing the sites you built in that lesson
What's next
→ Permissions and security: how to give an agent access safely
The mark stays in this browser only and is never sent anywhere. My progress