The Testing Academy · Class Notes Saturday, 3 October (IST)
Live class · study guide

Basic auth in the URL, SVG elements with name() and local-name(), and debugging with --debug and --ui

How to get past a basic auth prompt by putting the credentials in the URL, what an SVG is built from, and why XPath needs name() or local-name() to reach SVG elements. Three exercises put it to work: Flipkart's SVG search icon, the practice page's shapes, bar chart and star rating, and all 36 states on a SimpleMaps India map, with test.step and debugging in --debug and --ui mode along the way.

By Pramod Dutta, The Testing Academy. Study notes from the live Playwright 3x class, built from the session recording. The batch repository had not received this class's specs when this page was written (its latest commit is from Thursday's class), so every spec here is reconstructed from the recording and was run against the live sites on Sunday, 4 October; the output shown is what it printed. Run again on Sunday, the Flipkart spec hit problems the class did not, and a section near the end shows them with the fixes. The Eraser deck was not reachable while this page was written.

01

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
02

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:

Text
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:

TypeScript
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());
});
Text
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:

Text
BASIC_AUTH_USERNAME=<username>
BASIC_AUTH_PASSWORD=<password>
03

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.

THE SEARCH ICON path 1: the circle path 2: the handle THE SAME ICON AS TAGS <svg> the drawing area <g> groups the shapes <path d="..."/> path 1: the circle <path d="..."/> path 2: the handle </g> </svg> Everything from <svg> down is in the SVG namespace.
The search icon the class inspected on Flipkart: a g (group) inside the svg, holding two paths. The circle was drawn first, then the handle.
04

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():

Text
//*[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:

TypeScript
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());
});
Text
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.

05

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 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 in class was the search icon. No name() is needed, because this is CSS.
  • The titles. Every result card is a div with a data-id attribute, and the IDs start with a short code: MPC, CPU, COM, ACC, MP. One XPath with or conditions matches all of them, and the second link inside each card holds the title.

The instructor has started using an AI assistant for boilerplate such as the describe block. The spec, reconstructed from the recording, with the wait the class added after debugging:

TypeScript
import { test, expect } from '@playwright/test';

const URL = 'https://www.flipkart.com/';

test.describe('SVG elements', () => {
    test.beforeEach(async ({ page }) => {
        await page.goto(URL);
    });

    test('Verify the SVG elements', async ({ page }) => {
        await page.locator('input[name="q"]').fill('Mac Mini');
        await page.locator('svg').first().click();
        await page.waitForLoadState('networkidle');

        const titles = page.locator("//div[starts-with(@data-id,'CPU') or starts-with(@data-id,'ACC') or starts-with(@data-id,'COM') or starts-with(@data-id,'MP')]//a[2]");
        const count = await titles.count();
        console.log('Total titles:', count);
        for (let i = 0; i < count; i++) {
            console.log(await titles.nth(i).textContent());
        }
    });
});
  • count() gives the number of matches and nth(i) the match at index i, so the loop prints every title.
  • //a[2] means an a that is the second a inside its parent. It works here because the links in a card are siblings. If they were not, /descendant::a[2] counts across the whole card.
  • beforeEach opens 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.
06

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: beforeEach and afterEach run around every test in the group. (The class called them test.before and test.after. Playwright's names are beforeEach and afterEach, plus beforeAll and afterAll, which run once per group.)
  • test is one test case, and a group can hold several.
  • test.step breaks one test into named steps.
test.describe('SVG elements') a group of tests test.beforeEach runs before every test, for example page.goto(URL) test('first test') test.step('step 1') test.step('step 2') test.step('step 3') test('second test') test.step('step 1') test.step('step 2') test.afterEach runs after every test
A describe block groups tests and gives them shared hooks; each test can be broken into named steps. The steps are what the report and the trace show, one by one.

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:

TypeScript
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 () => { });
    });
});
07

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:

Terminal
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: a page that keeps polling in the background never goes quiet. Waiting for the thing you need is sturdier. On Sunday, await page.waitForURL(/\/search/) in its place returned within about a second, with every title already 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.

08

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-output box describes what was clicked.
  • A bar by its role. Each bar is an SVG rect with role="button" and an aria-label that starts "Q3 bar". With a role, getByRole works 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 polygon with role="radio" and a name such as "4 stars".
  • Every bar's value. .bar matches all the bars, and each carries its quarter and value as attributes.
TypeScript
import { test, expect } from '@playwright/test';

const URL = 'https://app.thetestingacademy.com/playwright/widgets/svg';

test.describe('Advance SVG', () => {
    test.beforeEach(async ({ page }) => {
        await page.goto(URL);
    });

    test('Click a shape by its id', async ({ page }) => {
        await page.locator('#circle-blue').click();
        await expect(page.locator('#shapes-output')).toContainText('Blue circle');
        console.log(await page.locator('#shapes-output').textContent());
    });

    test('Click the Q3 bar by role', async ({ page }) => {
        await page.getByRole('button', { name: /Q3 bar/ }).click();
        console.log(await page.locator('#bars-output').textContent());
    });

    test('Give a four star rating', async ({ page }) => {
        await page.getByRole('radio', { name: '4 stars' }).click();
        await expect(page.locator('#stars-readout')).toHaveText('Rating: 4 / 5');
        console.log(await page.locator('#stars-readout').textContent());
    });

    test('Read every bar value', async ({ page }) => {
        const bars = page.locator('.bar');
        const count = await bars.count();
        console.log('Total bars:', count);
        for (let i = 0; i < count; i++) {
            const quarter = await bars.nth(i).getAttribute('data-quarter');
            const value = await bars.nth(i).getAttribute('data-value');
            console.log(quarter, value);
        }
    });
});
Text
{
  "id": "circle-blue",
  "testid": "shape-circle-blue",
  "ariaLabel": "Blue circle"
}
{
  "quarter": "Q3",
  "value": 78,
  "testid": "bar-q3"
}
Rating: 4 / 5
Total bars: 5
Q1 42
Q2 64
Q3 78
Q4 92
Q5 56
  • toContainText() is case-sensitive. The output says "Blue circle", so 'blue circle' fails.
  • 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:

TypeScript
await page.getByRole('button', { name: /'Q3 bar'/ }).click();
Text
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.

09

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:

  1. 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_holder']. That is an HTML element, so a plain XPath works.
  2. Then the map: //*[name()='svg'] inside that div.
  3. Then the states. They are path elements whose class contains sm_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.

ONE XPATH, BUILT IN THREE STEPS 1 //div[@id='admin1_map_holder'] the map's container an HTML div: a plain name works 2 //*[name()='svg'] the map itself SVG namespace: needs name() 3 //*[name()='path' and contains(@class,'sm_state')] 36 matches 28 states and 8 union territories one of the 36: <path class="sm_state sm_state_INUP" d="..."/> IN + UP: Uttar Pradesh the path the loop clicks the same steps with plain names: //div[@id='admin1_map_holder']//svg//path 0 matches plain names miss the SVG namespace
Joined together, the three steps are the XPath the class used. Only the div step can use a plain name; the svg and path steps go through name().

Each state's class names it: sm_state_INPB is IN plus PB, Punjab. The first test loops over the states, prints each class, and clicks the one that includes INUP, Uttar Pradesh. The second test is the shorter version a learner suggested: skip the XPath, take every path on the page, and let the class check do the rest.

TypeScript
import { test } from '@playwright/test';

const URL = 'https://simplemaps.com/svg/country/in';

test.describe('Advance map', () => {
    test.beforeEach(async ({ page }) => {
        await page.goto(URL);
    });

    test('Find every state on the India map', async ({ page }) => {
        const states = await page.locator("//div[@id='admin1_map_holder']//*[name()='svg']//*[name()='path' and contains(@class,'sm_state')]").all();
        console.log('Total states:', states.length);
        for (const state of states) {
            const classState = await state.getAttribute('class');
            console.log(classState);
            if (classState?.includes('INUP')) {
                await state.click();
            }
        }
    });

    test('Every path on the page', async ({ page }) => {
        const paths = await page.locator('path').all();
        console.log('Total paths:', paths.length);
        for (const path of paths) {
            const classState = await path.getAttribute('class');
            if (classState?.includes('INUP')) {
                await path.click();
                console.log('Clicked', classState);
            }
        }
    });
});
Text
Total states: 36
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)
Total paths: 48
Clicked sm_state sm_state_INUP

The first eight match the order read out in class: Madhya Pradesh, Odisha, Mizoram, Sikkim, Goa, Kerala, Karnataka, Maharashtra. Both clicks land on Uttar Pradesh, and the map zooms into it.

  • .all() returns the matches as an array of locators, which is what for...of needs.
  • getAttribute('class') returns the whole class attribute, "sm_state sm_state_INUP", so includes('INUP') picks out the right one.
  • The ?. is there because getAttribute() returns null when the attribute is missing.
  • "Every path" is more than the states. On Sunday the page had 48 paths, and the ones that are not states have no class at all.

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_holder path.sm_state finds the same 36.

10

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:

TypeScript
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 next section shows why the isVisible() check is not enough on its own.

11

The Flipkart spec, run again on Sunday

I ran the reconstructed spec against Flipkart on Sunday, 4 October, headless, at the batch config's 1920 by 1080. It did not get past its first line (attributes shortened):

Text
Error: locator.fill: Error: strict mode violation: locator('input[name="q"]') resolved to 2 elements:
    1) <input name="q" value="" type="text" ... /> aka getByRole('textbox', { name: 'Search for Products, Brands' })
    2) <input readonly name="q" value="" type="text" ... /> aka locator('a').filter({ hasText: 'Search Icon' }).getByPlaceholder('Search for Products, Brands')

Fixing that exposed the rest:

What happened Why Fix
fill() failed with a strict mode violation The page had a second, hidden, read-only input[name="q"] .first(), or the getByRole('textbox') locator the error suggests
svg.first() clicked, but no search ran The first SVG on the page was the delivery location icon in the header Scope it: button[type="submit"] svg
A click timed out because a div "intercepts pointer events" The login modal opened after isVisible() had already returned false page.addLocatorHandler(), below
Every title ended in "..." Flipkart cuts the link text; the full name is in the link's title attribute getAttribute('title')
38 titles from 40 cards, in some runs Two cards had IDs starting AIO, which none of the four prefixes match Add the prefix

The cut titles, as textContent() printed them:

Text
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...

That is the cut the class saw in the terminal and then in the report. It is in Flipkart's text, not in the log.

A modal that turns up late. isVisible() checks once, and does not wait. On Sunday the modal opened about a third of a second after the page's HTML had loaded. In two of my four runs that relied on the check, the check ran first, the modal opened afterwards, and it blocked the next click. page.addLocatorHandler() registers the close action once: from then on, Playwright checks for the modal before every action and closes it whenever it is showing. The corrected spec:

TypeScript
import { test, expect } from '@playwright/test';

const URL = 'https://www.flipkart.com/';

test.describe('SVG elements', () => {
    test.beforeEach(async ({ page }) => {
        const closeButton = page.getByRole('button', { name: '✕' });
        await page.addLocatorHandler(closeButton, async () => {
            await closeButton.click();
        });
        await page.goto(URL);
    });

    test('Verify the SVG elements', async ({ page }) => {
        await page.locator('input[name="q"]').first().fill('Mac Mini');
        await page.locator('button[type="submit"] svg').click();
        await page.waitForLoadState('networkidle');

        const titles = page.locator("//div[starts-with(@data-id,'CPU') or starts-with(@data-id,'ACC') or starts-with(@data-id,'COM') or starts-with(@data-id,'MP')]//a[2]");
        const count = await titles.count();
        console.log('Total titles:', count);
        for (let i = 0; i < count; i++) {
            console.log(await titles.nth(i).getAttribute('title'));
        }
    });
});
Text
Total titles: 38
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
(32 more lines)

It passed eight runs out of eight. The titles and their number change from day to day: they are live search results.

12

Tasks and announcements

  • Code: the class's specs (files 256 and 257 among them) had not reached the batch repo when this page was written; tests/12_Handle_SVG is the folder waiting for them. Every spec above is reconstructed from the recording and was run.
  • 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.