Locator practice, in order.
Twenty one numbered elements, top to bottom. Start where getBy works, watch it break, then learn XPath from a single attribute up to sibling navigation.
Create your account
Seven elements, one per row. Each describes itself in a different way — use Playwright's built-in locator methods.
By alt text
By placeholder
By label
By title attribute
By test id
By visible text
Your details are never shared with sellers
By role
Trending this week
Three rows, three different reasons getBy cannot reach the element — switch to XPath.
Dell Inspiron 15
₹55,000
Samsung Galaxy M14
₹25,000
Noise ColorFit Pro
₹3,200
Delivery details
Start with a single attribute. Finish with sibling navigation and indexing — XPath only.
By id
By another attribute
Two attributes combined
By exact text
Text with spaces — normalize-space
Partial match — contains
Partial match — starts-with
Parent to child
Order summary
Elements 19 to 21 — relative XPath.
| Product | Price | Quantity | Action |
|---|---|---|---|
| Laptop | 1955000 | 1 | |
| 20Mobile | 25000 | 1 | 21 |
Payment
No numbers, no hints — identify the right strategy yourself.
Payment method
Card details
Card expiry
Gift message
Saved cards
| Saved card | Expiry | Action |
|---|---|---|
| HDFC Visa 4411 | 08 / 2029 | |
| ICICI Amex 7732 | 03 / 2028 |
Finish
Passenger preferences
check(), uncheck(), isChecked() and isDisabled(). One row per situation.
Single checkbox
Pre-checked
Disabled checkbox
Group of checkboxes
Select all
Journey details
Only one option in a group can be selected. That is the whole difference from a checkbox.
Radio group
Pre-selected radio
Disabled radio
Booking options
Elements 01 to 05 are real select tags, so selectOption() works. Elements 06 to 08 are built from div and li tags, where selectOption() will not work at all.
Simple select
Grouped options
Disabled option
Multi select
No value attribute
Custom dropdown, no select tag
Searchable dropdown
- Pune — PNQ
- Mumbai — BOM
- Bengaluru — BLR
- Chennai — MAA
- Hyderabad — HYD
- Delhi — DEL
Multi select without a select tag
Search suggestions
The list does not exist until you start typing. fill() pastes everything at once and never triggers it, so use pressSequentially().
Auto suggest with delay
- Delhi Airport Hotel
- Delhi Marriott
- Delhi Oberoi
- Mumbai Taj
- Mumbai Leela
- Mumbai ITC Grand
- Hyderabad Novotel
- Hyderabad Trident
- Bengaluru Hilton
- Bengaluru Sheraton
- Chennai Park Hyatt
- Pune Marriott
- Pune Sheraton
- Kolkata Oberoi Grand
- Goa Taj Holiday Village
Auto suggest without delay
- Mumbai
- Mumbai Central
- Pune
- Pune Airport
- Delhi
- Delhi Cantt
- New Delhi
- Hyderabad
- Bengaluru
- Bengaluru City
- Chennai
- Kolkata
- Goa
- Ahmedabad
- Surat
Booking history
Reading cells, counting rows, moving between siblings, and handling a table that changes after you sort or filter it.
Read a cell
| Booking ID | Route | Fare | Status |
|---|---|---|---|
| BK1001 | Pune to Delhi | 5400 | Confirmed |
| BK1002 | Mumbai to Goa | 3200 | Pending |
| BK1003 | Delhi to Dubai | 18700 | Confirmed |
| BK1004 | Chennai to Pune | 4100 | Cancelled |
Count rows and columns
| Flight | From | To | Seats | Price |
|---|---|---|---|---|
| AI202 | Mumbai | Delhi | 180 | 4500 |
| 6E401 | Pune | Bengaluru | 150 | 3200 |
| SG301 | Chennai | Hyderabad | 120 | 2800 |
Sortable column
| City | Flights |
|---|---|
| Pune | 18 |
| Delhi | 42 |
| Chennai | 25 |
| Bengaluru | 31 |
Filtered table
| City | Airport |
|---|---|
| Pune | PNQ |
| Mumbai | BOM |
| Delhi | DEL |
| Kolkata | CCU |
Frames
An iframe is a page inside a page. Playwright cannot reach inside it with an ordinary locator. You must switch into the frame first using frameLocator().
Simple iframe
Form inside a frame
Two frames on one page
Frame inside a frame
A frame can contain another frame. Each level needs its own frameLocator, chained one after the other.
Element inside nested frames
Browser dialogues
A dialogue blocks the page until it is answered. Playwright auto dismisses them by default, so you must register a handler with page.on("dialog") BEFORE the click that triggers it.
Simple alert
Confirm box
Prompt box
Result
Simple mouse actions
click(), dblclick(), click({ button: "right" }) and hover(). Each one produces a different result below.
Single click
Double click
Right click
Hover
Result
Drag, slide and resize
These need real mouse movement rather than a simple click. Use dragTo(), or mouse.down(), mouse.move() and mouse.up() for full control.
Drag and drop
Slider
Value: 20
Resizable box
Encapsulated components
A shadow root hides its markup from the main document. An ordinary locator cannot see inside it, so Playwright has to pierce the boundary. Open DevTools and look for #shadow-root.
Single shadow root
host id = shadowHost
Input inside shadow root
host id = shadowForm
Nested shadow roots
host id = shadowOuter
Tabs and popups
Practice handling new tabs, reading their titles, and interacting with elements inside them.
Capture the page
Practice visible area, full page and element-level screenshots using Playwright's screenshot() method.
01 — Visible area screenshot
02 — Full page screenshot
03 — Element screenshot
Upload your files
Practice single and multiple file upload using setInputFiles(). Remember: never use fill() or click() on a file input.
01 — Single file upload
02 — Multiple file upload
Download and validate
Practice registering the download event listener before the click, then validating the download using download.failure().
Hover and title tooltips
A tooltip only exists while the pointer is over the element, so you must hover first and then read it. Native title tooltips are rendered by the browser and never appear in the DOM at all.
CSS tooltip
Native title tooltip
Tooltip on a disabled field
Messages that disappear
A toast appears for a moment and then removes itself. If your assertion runs too late the element is already gone, which is why these are a classic source of flaky tests.
Success toast, 3 seconds
Error toast, 3 seconds
Fast toast, 1 second
Delayed toast, appears after 2 seconds
Testing APIs, not just the UI
Real, live endpoints — hit them from curl, Postman, or your own Playwright
request calls. Every example below runs against a small set of demo endpoints
hosted right here on this site, so the response you get back always matches what's shown.
Fetch a student's details by ID. This example is wired to studentId=101.
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
studentId | number | required | ID of the student to fetch. Try 101. |
Headers
Content-Type: application/json
— no auth needed for this endpoint
Request
curl -X GET "https://automatewithbipin.com/api-demo/get-student.php?studentId=101"
Response
{ "studentId": 101, "name": "Riya Sharma", "isActive": true, "attendance_percent": 92.5, "course": { "title": "Playwright + TypeScript", "batch": "Oct-2026", "duration_weeks": 8 } }
Want to see an error response too? This one is genuinely live as well:
curl -X GET "https://automatewithbipin.com/api-demo/get-student-404.php"
{ "error": "Student not found", "message": "No student exists with id 102. Please use studentId=101 for this example." }
Create a new student enrollment. Send the student's details in the request body.
Request Body Parameters
| Name | Type | Required | Description |
|---|---|---|---|
firstname | string | required | Student's first name. |
lastname | string | required | Student's last name. |
course | string | required | Course the student is enrolling in. |
batch | string | required | Batch label, e.g. "Oct-2026". |
feespaid | boolean | optional | Whether the fee has been paid. |
enrollmentdates | object | required | Nested object with startdate and enddate. |
additionalneeds | string | optional | Any extra requirement, e.g. equipment. |
Headers
Content-Type: application/json
— no auth needed for this endpoint
Request
curl -X POST "https://automatewithbipin.com/api-demo/post-response.php" \ -H "Content-Type: application/json" \ -d '{ "firstname": "Riya", "lastname": "Sharma", "course": "Playwright + TypeScript", "batch": "Oct-2026", "feespaid": true, "enrollmentdates": { "startdate": "2026-10-12", "enddate": "2026-12-10" }, "additionalneeds": "Laptop with Chrome installed" }'
Response
{ "studentId": 102, "student": { "firstname": "Riya", "lastname": "Sharma", "course": "Playwright + TypeScript", "batch": "Oct-2026", "feespaid": true, "enrollmentdates": { "startdate": "2026-10-12", "enddate": "2026-12-10" }, "additionalneeds": "Laptop with Chrome installed" } }
Leaving a required field out — like firstname — hits this endpoint instead, also genuinely live:
curl -X POST "https://automatewithbipin.com/api-demo/post-error.php"
{ "error": "Validation failed", "message": "\"firstname\" is required." }
Update an existing student's record (full replace). Protected — get a token first, then send it along with your update request.
Two steps: Step 1 gets you a token, Step 2 uses that token to make the actual update. This is how most real-world protected APIs work.
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
username | string | required | Admin username. Use admin. |
password | string | required | Admin password. Use admin123. |
Request
curl -X POST "https://automatewithbipin.com/api-demo/auth-token.php" \ -H "Content-Type: application/json" \ -d '{ "username": "admin", "password": "admin123" }'
Response
{ "token": "eyJhbGciOiJIUzI1NiJ9.sampletoken.abc123xyz" }
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
batch | string | optional | New batch label. |
feespaid | boolean | optional | Updated fee status. |
Headers
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.sampletoken.abc123xyzRequest
curl -X PUT "https://automatewithbipin.com/api-demo/put-response.php" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.sampletoken.abc123xyz" \ -d '{ "batch": "Dec-2026", "feespaid": true }'
Response
{ "studentId": 101, "student": { "name": "Riya Sharma", "batch": "Dec-2026", "feespaid": true } }
Partially update a student's record — unlike PUT, you only send the fields that are changing. Also protected, so it follows the same two-step flow.
Same two steps as PUT. Notice the request body below only has one field — that's the key difference from PUT.
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
username | string | required | Admin username. Use admin. |
password | string | required | Admin password. Use admin123. |
Request
curl -X POST "https://automatewithbipin.com/api-demo/auth-token.php" \ -H "Content-Type: application/json" \ -d '{ "username": "admin", "password": "admin123" }'
Response
{ "token": "eyJhbGciOiJIUzI1NiJ9.sampletoken.abc123xyz" }
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
feespaid | boolean | optional | Only the field you want to change. |
Headers
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.sampletoken.abc123xyzRequest
curl -X PATCH "https://automatewithbipin.com/api-demo/patch-response.php" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.sampletoken.abc123xyz" \ -d '{ "feespaid": true }'
Response
{ "studentId": 101, "student": { "name": "Riya Sharma", "course": "Playwright + TypeScript", "batch": "Oct-2026", "feespaid": true } }
Remove a student's record. Deleting data is sensitive, so this is protected the same way as PUT and PATCH.
Same two-step flow again. If you've already got a token from PUT or PATCH, you can reuse it here too.
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
username | string | required | Admin username. Use admin. |
password | string | required | Admin password. Use admin123. |
Request
curl -X POST "https://automatewithbipin.com/api-demo/auth-token.php" \ -H "Content-Type: application/json" \ -d '{ "username": "admin", "password": "admin123" }'
Response
{ "token": "eyJhbGciOiJIUzI1NiJ9.sampletoken.abc123xyz" }
Headers
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.sampletoken.abc123xyzRequest
curl -X DELETE "https://automatewithbipin.com/api-demo/delete-response.php" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.sampletoken.abc123xyz"
Response
{ "message": "Student record deleted successfully", "studentId": 101 }
In the real world you'll often see 204 No Content for a successful delete instead — an empty response body. We're showing a small confirmation body here so there's something to assert on while you're learning.
Log in to the student portal admin area. This one's genuinely live too — httpbin actually checks the credentials you send.
If you open this URL directly in a browser, you'll get a native "Sign in" popup — the browser is handling the login prompt itself. In automated code, there's no popup at all: you just send the Authorization header yourself, already base64-encoded, and the server responds directly.
Headers
Authorization: Basic YWRtaW46YWRtaW4xMjM=Request
curl -X GET "https://httpbin.org/basic-auth/admin/admin123" \ -H "Authorization: Basic YWRtaW46YWRtaW4xMjM="
Response
{ "authenticated": true, "user": "admin" }
Wrong username or password — or a missing header — and you'll genuinely get this back instead:
(empty response body)