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. Four specs, pushed to the batch repo.

By Pramod Dutta, The Testing Academy. Study notes from the live Playwright 3x class, built from the session recording and the batch repository, which received the class's four specs on Monday, 5 October (commit "feat: add SVG handling module"). Every spec was run headless, and the output shown is what it prints; where a spec has a slip, the page shows it and the fix. 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 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. 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 inside each card's first div, 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:

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

});
Text
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 and nth(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 child div.
  • 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.
  • The commented-out waitForTimeout is the hard wait the class tried before settling on networkidle. Why every title ends in "..." is in the last Flipkart section.
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, 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.

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. The spec reads each bar's quarter and its height, which on this chart equals the value.
TypeScript
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();



    });

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

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_inner']. That is an HTML element, so a plain XPath works. (Its parent, admin1_map_holder, works too: both reach all 36 states.)
  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_inner'] 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_inner']//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. Spec 257 loops over the states, prints each class, and clicks the one that includes INUP, Uttar Pradesh:

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



    });

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

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:

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

        }

    });

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

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

TypeScript
const closeButton = page.getByRole('button', { name: '✕' });
await page.addLocatorHandler(closeButton, async () => {
    await closeButton.click();
});
11

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:

TypeScript
const title: string | null = await titleResults.nth(i).getAttribute('title');
Text
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.

12

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.