What this class covered
- Basic auth: the credentials go in the URL, and belong in a .env file
- What an SVG is, and the tags it is built from
- Why XPath needs name() or local-name() for SVG elements
- The Flipkart exercise, from a Wingify interview: type Mac Mini, click the SVG search icon, print every title
- test.describe, test and test.step, and what steps add to a report
- Wrong titles: debugging with --debug and --ui, and the fix, waitForLoadState('networkidle')
- Practice page SVGs: a shape by its id, a chart bar and a star rating by role, and every bar's value
- The SimpleMaps India map: an XPath through div, svg and path that finds all 36 states, then a click on Uttar Pradesh
- Closing Flipkart's login modal with getByRole
- Two locator battles, the DSLR task, and Sunday's mandatory Playwright test
Basic auth: credentials in the URL
Some sites show a browser sign-in prompt before the page loads at all. That is HTTP basic authentication, and the class demo was the basic auth page on the-internet.herokuapp.com. The easiest way past it is to put the username and password in the URL, in front of the host:
https://<username>:<password>@the-internet.herokuapp.com/basic_auth
Open that and the page loads already signed in. The demo's login was shown in class and is not repeated here.
- Where you meet it: rarely in production. It is mostly on non-production sites: a company's staging or QA environment often sits behind a basic auth prompt. Ask for the username and password, and put them in the URL.
- In CI: asked whether it works in a CI pipeline too, the answer was yes, it works everywhere.
- It is not a JavaScript alert. The prompt looks like one, but it is the browser's own sign-in box, so the
page.on('dialog')listener from the last class never sees it.
The same URL works in a Playwright test. I ran this with the demo login in place of the placeholders; the second test opens the page without credentials, to show what comes back:
import { test, expect } from '@playwright/test';
test('Basic auth through the URL', async ({ page }) => {
await page.goto('https://<username>:<password>@the-internet.herokuapp.com/basic_auth');
const message = page.locator('#content p');
await expect(message).toContainText('Congratulations');
console.log(await message.innerText());
});
test('Basic auth without credentials', async ({ page }) => {
const response = await page.goto('https://the-internet.herokuapp.com/basic_auth');
console.log('Status without credentials:', response?.status());
});
Congratulations! You must have the proper credentials.
Status without credentials: 401
Without credentials the server answers 401 Unauthorized, and there is no dialog for Playwright to handle.
Do not hardcode the credentials. Asked whether hardcoding them is acceptable in a real project, the answer was no: keep them in a .env file and read them with the dotenv library. Loading the file comes in a later class. The file itself is only names and values:
BASIC_AUTH_USERNAME=<username>
BASIC_AUTH_PASSWORD=<password>
What an SVG is
SVG stands for scalable vector graphics: 2D graphics written as XML tags inside an <svg> element in the HTML. Two advantages came up:
- Small files. An icon as an SVG is much smaller than the same icon as a PNG or JPG.
- No pixelation. An SVG is drawn from shapes and coordinates rather than stored as pixels, so it stays sharp however far you zoom in. Zoom into a PNG and it blurs.
The shapes are tags of their own: rect, circle, ellipse, line, polyline, polygon and path, the free-form shape most icons are drawn with. A g (graphics) tag groups shapes together. An <img> tag is an image; the SVG elements you can locate one by one are the ones inside an <svg> tag.
On Flipkart, the class inspected the search icon. It is an svg holding a g, holding two path elements: one draws the circle of the magnifying glass and the other draws the handle.
SVG elements and XPath: name() and local-name()
Everything inside an <svg> tag lives in the SVG namespace, not the HTML one. CSS selectors are not affected: svg, path and #circle-blue work as usual. XPath is: a plain //svg finds nothing. The fix is the XPath function name(), or local-name(), used the same way as text() or contains():
//*[name()='svg']
//*[local-name()='svg']
* means any element, and the condition keeps the ones whose tag name is svg. The practice page shows the difference in four counts:
import { test } from '@playwright/test';
test('SVG elements and XPath', async ({ page }) => {
await page.goto('https://app.thetestingacademy.com/playwright/widgets/svg');
await page.locator('#circle-blue').waitFor();
console.log("CSS svg: ", await page.locator('svg').count());
console.log("XPath //svg: ", await page.locator('//svg').count());
console.log("XPath //*[name()='svg']: ", await page.locator("//*[name()='svg']").count());
console.log("XPath //*[local-name()='svg']:", await page.locator("//*[local-name()='svg']").count());
});
CSS svg: 65
XPath //svg: 0
XPath //*[name()='svg']: 65
XPath //*[local-name()='svg']: 65
The 65 is every SVG on the page, icons included, not just the exercise shapes.
The Flipkart exercise: search Mac Mini, print the titles
The problem statement, which came up in a Wingify interview: open Flipkart, type Mac Mini into the search box, click the search icon (an SVG), and print the title of every result.
The class worked from Flipkart's search page and found the locators in DevTools:
- The search box. Its class looks generated, so the class used its name instead:
input[name="q"]. - The search icon.
page.locator('svg')finds every SVG on the page, and.first()takes the first, which on the search page is the icon inside the search button. Noname()is needed, because this is CSS. - The titles. Every result card is a
divwith adata-idattribute, and the IDs start with a short code: MPC, CPU, COM, ACC, MP. One XPath with or conditions matches all of them, and inside each card's firstdiv, the second link holds the title.
The instructor has started using an AI assistant for boilerplate such as the describe block. The spec as pushed, with the wait the class added after debugging:
import { test, expect, Locator } from '@playwright/test';
test.describe('SVG Elements', () => {
const URL = 'https://www.flipkart.com/search'
test.beforeEach(async ({ page }) => {
console.log("Before running any Testcase!")
await page.goto(URL);
});
test('Verify the SVG elements', async ({ page }) => {
await page.locator('input[name="q"]').fill("macmini");
const svgElements: Locator = page.locator('svg');
await svgElements.first().click();
await page.waitForLoadState('networkidle');
// await page.waitForTimeout(500);
const titleResults: Locator = page.locator("//div[contains(@data-id,'CPU') or contains(@data-id,'ACC') or contains(@data-id,'COM') or contains(@data-id,'MP')]/div/a[2]");
const count: number = await titleResults.count();
for (let i = 0; i < count; i++) {
const title: string | null = await titleResults.nth(i).textContent();
console.log(title);
}
await page.pause();
});
});
Before running any Testcase!
Apple Mac mini MHQK4HN/A - Mac OS, M6, M6, 0 GB Graphic...
Apple Mac mini MHQM4HN/A - Mac OS, M6, M6, 0 GB Graphic...
Apple Mac mini MHQL4HN/A - Mac OS, M6, M6, 0 GB Graphic...
Apple Mac Mini (MGNT3HN/A) M1 Chip (8 GB RAM/integrated...
Apple Mac Studio MHL64HN/A - Mac OS, M5 Max, M5 Max, 0 ...
Apple Mac mini MHQN4HN/A - Mac OS, M5 Pro, M5 Pro, 0 GB...
(33 more lines)
count()gives the number of matches andnth(i)the match at index i, so the loop prints every title.contains(@data-id,'MP')also covers the MPC cards, and/div/a[2]is the second link inside each card's childdiv.beforeEachopens the URL before every test in the group. That is why the spec has a describe block: more tests were coming, all starting from the same page.- The commented-out
waitForTimeoutis the hard wait the class tried before settling on networkidle. Why every title ends in "..." is in the last Flipkart section.
test.describe, test and test.step
Asked what test.step is for, the class went back over how a spec file is structured:
- test.describe is a group of tests. Its advantage is hooks:
beforeEachandafterEachrun around every test in the group. (The class called them test.before and test.after. Playwright's names arebeforeEachandafterEach, plusbeforeAllandafterAll, which run once per group.) - test is one test case, and a group can hold several.
- test.step breaks one test into named steps.
The point of steps is reporting. Each step appears by name, with its own time, in the HTML report and in the trace, so a failure points at a step rather than at the whole test. The class added that screenshots, traces and videos can be taken separately as well. The shape of a file that uses all three:
import { test } from '@playwright/test';
test.describe('SVG elements', () => {
test.beforeEach(async ({ page }) => {
// runs before every test, for example page.goto(URL)
});
test.afterEach(async ({ page }) => {
// runs after every test
});
test('first test', async ({ page }) => {
await test.step('step 1', async () => { });
await test.step('step 2', async () => { });
await test.step('step 3', async () => { });
});
test('second test', async ({ page }) => {
await test.step('step 1', async () => { });
await test.step('step 2', async () => { });
});
});
Wrong titles: --debug, --ui, and waiting for the results
The first run printed titles, but not Mac Minis: an LG TV was among them. To find out why, the class debugged it:
npx playwright test --debug
npx playwright test --ui
- --debug opens the Playwright Inspector. The test pauses, and each press of Step over runs the next action: go to the URL, fill, click, read a title. It only goes forward.
- --ui opens UI mode, which the instructor prefers. It lists every action with a snapshot of the page, and after the run you can click back to any earlier step to see what the page looked like then. Asked whether you can try out a locator while debugging: yes.
UI mode showed the fill and the click working. The problem was timing: the titles were read while the results were still loading. waitForLoadState('domcontentloaded') did not help. waitForLoadState('networkidle'), which waits until the page has made no network requests for 500 ms, did. In class the results took about 6 seconds to arrive.
page.waitForTimeout(ms) came up too: a fixed wait, which the class called a hard wait. Sometimes you need one while elements load, but it always waits the full time.
Playwright's documentation discourages networkidle for tests, because a page that keeps polling in the background never goes quiet. On this page it works: on Tuesday the run printed every title. The tempting replacement, waiting for the URL to change, does not work here: the search URL updates before the results render, so waitForURL(/q=macmini/) returned at once, with only 3 titles on the page.
Two more questions came up. The titles also looked cut off, in the terminal and again in the HTML report; the last Flipkart section shows where that comes from. And asked how to stop a report being written on every debug run: a flag that skips it for debug and UI runs would do it, but ideally keep the report and delete the old ones.
SVGs on the practice page
The SVG practice page has three widgets: shapes, a bar chart and a star rating. None of them needs name(), which was the point: try the Playwright locators first.
- A shape by its id. The blue circle is
<circle id="circle-blue">, and with an id there is nothing to work out. After a click, the#shapes-outputbox describes what was clicked. - A bar by its role. Each bar is an SVG
rectwithrole="button"and an aria-label that starts "Q3 bar". With a role,getByRoleworks on an SVG element like on any other, and the class called this the place where Playwright wins over Selenium, which has no role-based locator. The name here is a regular expression:/Q3 bar/matches any name that contains Q3 bar. - A star rating. Each star is a
polygonwithrole="radio"and a name such as "4 stars". - Every bar's value.
.barmatches all the bars. The spec reads each bar's quarter and its height, which on this chart equals the value.
import { test, expect, Locator } from '@playwright/test';
const URL = 'https://app.thetestingacademy.com/playwright/widgets/svg'; // replace with target page
test.describe('SVG handling', () => {
test.beforeEach(async ({ page }) => {
await page.goto(URL);
});
test('locate SVG root and assert visible', async ({ page }) => {
const circleShape: Locator = page.locator('#circle-blue');
await circleShape.click();
const output = await page.locator('#shapes-output').innerText();
expect(output).toContain('Blue circle');
await page.getByRole('button', { name: /Q3 bar/ }).click();
await page.getByRole('radio', { name: '4 stars' }).click();
let allBars = await page.locator(".bar").all();
for (const bar of allBars) {
// logic which is the hegiht, low ......click on that.
const q = await bar.getAttribute('data-quarter');
const h = await bar.getAttribute('height');
console.log(q);
console.log(h);
}
await page.pause();
});
});
Q1
42
Q2
64
Q3
78
Q4
92
Q5
56
toContain()is case-sensitive. The output says "Blue circle", so'blue circle'fails.- The comment in the loop sketches the next step: compare the heights and click the tallest or the lowest bar.
- The page has five bars, Q1 to Q5. In class, the DevTools search box reported nine matches for
.bar; that box also matches plain text, so count with the locator.
The first try at the Q3 bar waited until it timed out. It had quotes inside the regular expression, so it was looking for a name that contains the quote marks too:
await page.getByRole('button', { name: /'Q3 bar'/ }).click();
Test timeout of 30000ms exceeded.
Error: locator.click: Test timeout of 30000ms exceeded.
Call log:
- waiting for getByRole('button', { name: /'Q3 bar'/ })
A learner spotted it: a regular expression needs no quotes. Removing them gives the passing test above.
A plain string works here too. getByRole matches a string name as a case-insensitive substring unless you pass exact: true, so { name: 'Q3 bar' } finds the same bar.
The SimpleMaps India map
SimpleMaps' India map is where the class needed real SVG XPath. Hover over a state and its name appears; click one and the map zooms in. The state shapes have no role and no aria-label (I checked all 36), so getByRole has nothing to match. Asked about exactly that, the class's answer was the same: getByRole works only where there is a role.
The XPath, built one step at a time:
- Start from the container.
//*[name()='svg']on its own matches several SVGs on the page, so anchor to the map's container first://div[@id='admin1_map_inner']. That is an HTML element, so a plain XPath works. (Its parent,admin1_map_holder, works too: both reach all 36 states.) - Then the map:
//*[name()='svg']inside that div. - Then the states. They are
pathelements whose class containssm_state://*[name()='path' and contains(@class,'sm_state')].
There were two slips on the way: a missing closing square bracket, and then the path step written twice, which a learner spotted. With both fixed, the XPath found 36 of 36: 28 states and 8 union territories.
Each state's class names it: sm_state_INPB is IN plus PB, Punjab. Spec 257 loops over the states, prints each class, and clicks the one that includes INUP, Uttar Pradesh:
import { test, expect, Locator } from '@playwright/test';
const SimpleMaps = "https://simplemaps.com/svg/country/in";
test.describe('SVG handling', () => {
test.beforeEach(async ({ page }) => {
await page.goto(SimpleMaps);
});
test('locate SVG root and assert visible', async ({ page }) => {
const states = await page
.locator(
"//div[@id='admin1_map_inner']//*[name()='svg']//*[name()='path' and contains(@class,'sm_state')]",
)
.all();
for(const state of states){
const classState = await state.getAttribute("class");
console.log(classState);
if (classState?.includes("INUP")) {
state.click();
}
}
await page.pause();
});
});
sm_state sm_state_INMP
sm_state sm_state_INOR
sm_state sm_state_INMZ
sm_state sm_state_INSK
sm_state sm_state_INGA
sm_state sm_state_INKL
sm_state sm_state_INKA
sm_state sm_state_INMH
(28 more lines, one per state)
Error: locator.click: Test ended.
Call log:
- waiting for locator('//div[@id=\'admin1_map_inner\']//*[name()=\'svg\']//*[name()=\'path\' and contains(@class,\'sm_state\')]').nth(28)
- locator resolved to <path ... class="sm_state sm_state_INUP" ...></path>
- attempting click action
The first eight match the order read out in class: Madhya Pradesh, Odisha, Mizoram, Sikkim, Goa, Kerala, Karnataka, Maharashtra (attributes shortened in the error). Then the run fails, and the reason is one missing word.
state.click() has no await, so the loop moves on and the test ends while the click is still in flight: "Test ended". With the batch config the browser is headed, and the page.pause() at the end holds the test open long enough for the click to land, which hides the slip. Headless, or in CI, page.pause() does nothing and the test fails. With await state.click(); it passes headless too, and the map zooms into Uttar Pradesh.
.all()returns the matches as an array of locators, which is whatfor...ofneeds.getAttribute('class')returns the whole class attribute, "sm_state sm_state_INUP", soincludes('INUP')picks out the right one.- The
?.is there becausegetAttribute()returns null when the attribute is missing.
The shorter version. A learner suggested skipping the XPath: take every path on the page and let the class check do the rest. That is spec 258:
import { test, expect, Locator } from '@playwright/test';
const SimpleMaps = "https://simplemaps.com/svg/country/in";
test.describe('SVG handling', () => {
test.beforeEach(async ({ page }) => {
await page.goto(SimpleMaps);
});
test('locate SVG root and assert visible', async ({ page }) => {
let allStates = await page.locator('path').all();
for (const state of allStates) {
const className = await state.getAttribute('class');
if (className) { // checks if className is not null
console.log(className);
if (className.endsWith('UP')) { // checks if stateEnd ends with 'UP'
await state.click();
console.log("Clicked on UP");
}
}
await page.pause();
}
});
});
sm_state sm_state_INMP
sm_state sm_state_INOR
sm_state sm_state_INMZ
(25 more lines)
sm_state sm_state_INUP
Clicked on UP
sm_state sm_state_INUT
(6 more lines)
The page.pause() sits inside the loop, so in a headed run the test stops once for every path on the page, 48 times on Tuesday, and you have to press Resume each time. Headless it does nothing, which is why the run above went straight through. Move it below the loop.
- "Every path" is more than the states: the page has 48 paths, and the 12 that are not states have no class at all, which is why 258 checks
if (className)first. endsWith('UP')is safe here because Uttar Pradesh is the only state code ending in UP.
The instructor's view: the shorter version works, but the traditional XPath is the one that teaches you what is going on. CSS reaches the states too, with no namespace trick: #admin1_map_inner path.sm_state finds the same 36.
Closing Flipkart's login modal
A learner reported that Flipkart opens a login modal on launch. Inspect it, and the close control is a button, so getByRole finds it; the instructor also asked an AI assistant for the locator, and it suggested the same one. The safeguard is to click it only if it is visible:
const closeButton = page.getByRole('button', { name: '✕' });
if (await closeButton.isVisible()) {
await closeButton.click();
}
The name was said aloud as X, but the button's text is the ✕ symbol (U+2715), not the letter. { name: 'X' } matches nothing on Flipkart, so copy the symbol from the page.
The class's spec starts on the search page, where the modal did not appear in my runs. On the home page it does, a moment after the page loads, and that shows a gap in the safeguard: isVisible() checks once and does not wait. In two of my four runs on the home page, the check ran before the modal opened, and the modal then blocked the next click. page.addLocatorHandler() closes it whenever it shows up, before any action, and it passed eight runs out of eight:
const closeButton = page.getByRole('button', { name: '✕' });
await page.addLocatorHandler(closeButton, async () => {
await closeButton.click();
});
Running the pushed Flipkart spec
Run headless on Tuesday, 6 October, spec 255 passes. Two things in its output are worth knowing.
Every title ends in "...". That is the cut the class saw in the terminal and then in the HTML report, and it is not the log: Flipkart cuts the link text itself. The full name is in the link's title attribute, so read that instead. Changing the one line in the loop:
const title: string | null = await titleResults.nth(i).getAttribute('title');
Before running any Testcase!
Apple Mac mini MHQK4HN/A - Mac OS, M6, M6, 0 GB Graphics Card, 16 GB NA, 256 GB SSD Mini PC
Apple Mac mini MHQM4HN/A - Mac OS, M6, M6, 0 GB Graphics Card, 24 GB NA, 512 GB SSD Mini PC
Apple Mac mini MHQL4HN/A - Mac OS, M6, M6, 0 GB Graphics Card, 16 GB NA, 512 GB SSD Mini PC
Apple Mac Mini (MGNT3HN/A) M1 Chip (8 GB RAM/integrated 8-core GPU Graphics/512 GB SSD Capacity/Mac OS Big Sur) Microtower
Apple Mac Studio MHL64HN/A - Mac OS, M5 Max, M5 Max, 0 GB Graphics Card, 36 GB NA, 512 GB SSD Mini PC
Apple Mac mini MHQN4HN/A - Mac OS, M5 Pro, M5 Pro, 0 GB Graphics Card, 24 GB NA, 512 GB SSD Mini PC
(33 more lines)
The prefix list can miss cards. On Tuesday the search returned 40 cards: 7 with IDs starting MPC, 3 CPU, 29 COM and 1 AIO. The four codes in the XPath matched 39 of them; the AIO card was left out. The cards and their codes change from day to day, so check the IDs on the day and add any codes you find. The results also include other brands' mini PCs, because the search term was macmini.
Tasks and announcements
- Code: all four specs are in the batch repo, in
tests/12_Handle_SVG: 255 for Flipkart, 256 for the practice page, and 257 and 258 for the map. - Practise on the SVG practice page.
- Sunday: a mandatory 12-hour Playwright test. Take it as early as you can, so the results come back sooner.
- Still to come: shadow DOM, file upload and download, data-driven tests, and loading the .env file.
Task 1, two locator battles. A locator strategy battle with five questions, and a second, easier selector battle. Both are timed: the clock starts when you open the link, a hint costs 5 points, and the best score wins. The links are shared in the batch group.
Task 2, the DSLR task. The wait from the Flipkart exercise applies to the DSLR pagination task too. A learner's follow-up, printing the camera titles from the DSLR search, goes to the group as a problem statement: everyone tries it first, then a solution follows.