What this class covered
- Keyboard presses and shortcuts, checked with screenshots
- Hover: open a menu, then click what it reveals
- Drag and drop with dragTo()
- The Kanban interview question: when dragTo() does nothing, drive the mouse with boundingBox()
- A full HD viewport in the config
- Right-click, and reading a context menu
- JavaScript alerts, and how they differ from modals and pop-ups
- test.describe and beforeEach
- page.once() versus page.on(), and why the listener goes first
- Running the tests in a describe block in parallel or in series
- Three tasks: the hover menu, the second Kanban card, and Automation Challenge 1
Keyboard presses and shortcuts
page.keyboard is an interface hanging off the page, with a method for every kind of key event. The first spec presses keys on a key code viewer and takes a screenshot after each one to prove it landed:
import { test, expect, Locator } from '@playwright/test';
test('Verify the TestCase', async ({ page }) => {
await page.goto("https://keycode.info");
await page.keyboard.press('A');
await page.screenshot({ path: 'A.png' });
await page.keyboard.press('ArrowLeft');
await page.screenshot({ path: 'ArrowLeft.png' });
await page.keyboard.press('Shift+O');
await page.screenshot({ path: 'O.png' });
await page.keyboard.up("Shift");
await page.keyboard.down("Shift");
await page.pause();
});
- press() takes a character (
'A'), a named key ('ArrowLeft'), or a combination ('Shift+O'). In a combination, Playwright holds the modifier down and releases it for you. - screenshot({ path }) saves a PNG. A bare file name lands in the folder you ran the tests from, which is why the repo's
.gitignorenow listsA.png,O.pngandArrowLeft.png. Give it a folder, such asscreenshots/A.png, to keep them together.
The last two keyboard lines are in the wrong order. down() holds a key and up() releases it, so a pair goes down first, then up. As pushed, up('Shift') runs first and does nothing, and down('Shift') then leaves Shift held when the test ends.
Hover: open a menu, then click what it reveals
The hover demo used SpiceJet's site: hover the Add-ons menu, then click the FlyEarly item it reveals. The spec file was overwritten during class, so this version is reconstructed from the recording:
await page.goto('https://www.spicejet.com/');
await page.getByText('Add-ons', { exact: true }).hover();
await page.getByText('FlyEarly', { exact: true }).click();
There is no mouse down, mouse up or Actions class as in Selenium: find the element and call hover(). exact: true makes getByText() match the whole string rather than any text containing it.
On the live site the item is written as one word, FlyEarly, so exact: true needs that spelling; "Fly Early" with a space matches nothing. Clicking it opens a new tab, which belongs to the windows topic still to come.
The same pattern works on the hover menu practice page, which is today's first task.
Drag and drop with dragTo()
For a simple drag, dragTo() is one line: locate the source, locate the target, and drag one onto the other.
import { test, expect, Locator } from '@playwright/test';
test('Verify Hover for the spicejet', async ({ page }) => {
await page.goto('https://the-internet.herokuapp.com/drag_and_drop');
const columnA = page.locator('#column-a');
const columnB = page.locator('#column-b');
await columnA.dragTo(columnB);
await page.pause();
await page.pause();
});
After the drag the two column headers read B and A: the boxes have swapped. The test title is left over from the hover spec.
Why 250_Hover_TestCase.spec.ts times out. It is the class's first attempt at this drag. It opens the Testing Academy board page, where no #column-a exists, so dragTo() waits for the full test timeout. The ids belong to the page above, and 251 is the version that worked. Its { force: true } skips the actionability checks, but it cannot help when the element does not exist.
The Kanban interview question
The interview version: on the Testing Academy Kanban board, move a card from To Do to In Progress, as you would in Jira. Each card is an article holding a heading, a div and spans. dragTo() with the card and the In Progress column runs without an error, and the card stays exactly where it was.
What works is driving the mouse yourself:
import { test, expect, Locator } from '@playwright/test';
test('Verify Drag and Drop in Kanban Board', async ({ page }) => {
await page.goto('https://app.thetestingacademy.com/playwright/widgets/dnd');
let source:Locator = page.locator('#card-write-spec');
const sBox = (await source.boundingBox())!;
let target: Locator = page.locator('[data-status="in-progress"]');
const tBox = (await target.boundingBox())!;
await page.mouse.move(sBox.x + sBox.width / 2, sBox.y + sBox.height / 2);
await page.mouse.down();
await page.mouse.move(tBox.x + tBox.width / 2, tBox.y + tBox.height / 2, { steps: 10 });
await page.mouse.up();
await page.pause();
});
The card ends up in the In Progress column. page.mouse works in pixels, not locators, which is what boundingBox() is for: it returns the element's x, y, width and height on the page.
- The center is
x + width / 2across andy + height / 2down: not a formula to memorise, just half the box each way. The class compared it to lifting a person by their center of mass rather than their head. - steps: 10 moves the mouse in ten small increments instead of one jump. The class called the number trial and error.
- The
!afterboundingBox()tells TypeScript the result is not null. It returns null for an element that is not rendered.
Why dragTo() fails here. The class put it down to where dragTo() grabs the card, since the card is built from several elements. Testing the board does not bear that out. The manual drag also works from the card's corner, and in a single step. Meanwhile dragTo(), and even a drag driven by locator.hover(), leave the card in To Do. The reliable fact for an interview: some drag widgets ignore dragTo(), and moving page.mouse to coordinates from boundingBox() gets past them.
A full HD window. While the board was on screen, the class set the viewport to 1920 by 1080 in playwright.config.ts. The pushed config sets it in two places, and the comment explains why:
use: {
/* Base URL to use in actions like `await page.goto('')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
headless: false,
/* Full HD. The project below re-applies it after the device spread, because
devices['Desktop Chrome'] carries its own 1280x720 viewport and
project-level `use` wins over this config-level `use`. */
viewport: { width: 1920, height: 1080 },
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
viewport: { width: 1920, height: 1080 },
},
}]
That checks out: with the viewport set only at the top level, the page opens at 1280 by 720.
Right-click and the context menu
click({ button: 'right' }) is a right-click, and allInnerTexts() reads every option in the menu that opens:
import { test, expect, Locator } from '@playwright/test';
test('Verify Drag and Drop in Kanban Board', async ({ page }) => {
await page.goto('https://app.thetestingacademy.com/playwright/widgets/context-menu');
await page.locator('span.context-menu-one').first().click({ button: 'right' });
const allOptions: string[] = await page
.locator('ul.context-menu-list span')
.allInnerTexts();
console.log(allOptions);
await page.getByText('Copy', { exact: true }).first().click();
await page.pause();
});
[
'Edit', '⌘E',
'Cut', '⌘X',
'Copy', '⌘C',
'Paste', '⌘V',
'Delete', '⌫',
'Quit', '⌘Q'
]
Six menu items, twelve strings: each item holds a label span and a shortcut span, and the selector catches both. ul.context-menu-list span:not(.ctx-shortcut) returns just the six labels. Double-click works the same way, with dblclick().
JavaScript alerts, modals and pop-ups
Three things get called "pop-ups", and only one of them is a JavaScript dialog:
- JavaScript dialogs:
alert(OK),confirm(OK or Cancel) andprompt(a text box, then OK or Cancel). The page's JavaScript opens them, but the browser draws them, so they are not in the DOM and you cannot locate them. Playwright hands them to you through thedialogevent. - Modals, such as the login overlay on MakeMyTrip, are plain HTML on the page. Locate their buttons like any other element.
- Pop-ups, new windows and tabs, are a separate topic, coming up.
With no listener registered, Playwright dismisses every dialog itself: an alert just closes, and a confirm reports "You clicked: Cancel".
test.describe and beforeEach
The alerts spec puts three tests in one test.describe group, with a beforeEach that opens the page before every one of them, so the goto runs three times. For anyone coming from Selenium, it works like a TestNG before-method annotation.
import { test, expect, Locator } from '@playwright/test';
test.describe('Javascript Alerts', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://the-internet.herokuapp.com/javascript_alerts');
});
test('JS Alert accept 1', async ({ page }) => {
await page.getByRole('button', { name: "Click for JS Alert" }).click();
page.once('dialog', async dialog => {
console.log('Alert type:', dialog.type());
console.log('Alert message:', dialog.message());
expect(dialog.message()).toBe('I am a JS Alert');
await dialog.accept();
});
await page.pause();
});
test('JS Alert accept 2', async ({ page }) => {
page.once('dialog', async dialog => {
console.log('Alert type:', dialog.type());
expect(dialog.type()).toBe('confirm');
console.log('Alert message:', dialog.message());
expect(dialog.message()).toBe('I am a JS Confirm');
await dialog.accept();
//await dialog.dismiss();
});
await page.locator('button', { hasText: 'Click for JS Confirm' }).click();
await expect(page.locator('#result')).toHaveText('You clicked: Ok');
});
test('JS Alert accept 3', async ({ page }) => {
const inputText = 'Hello from The Testing Academy';
page.once('dialog', async dialog => {
expect(dialog.type()).toBe('prompt');
expect(dialog.defaultValue()).toBe('');
await dialog.accept(inputText);
//await dialog.dismiss();
});
await page.locator('button', { hasText: 'Click for JS Prompt' }).click();
await expect(page.locator('#result')).toHaveText(`You entered: ${inputText}`);
});
});
All three pass, and the run prints:
Alert type: confirm
Alert message: I am a JS Confirm
Only test 2 printed anything. Test 1 has the same two console.log lines and printed nothing, which is the subject of the next section.
- Confirm:
dialog.accept()is OK, anddialog.dismiss()is Cancel. - Prompt:
dialog.accept(text)types into the box before pressing OK, anddialog.defaultValue()is whatever the box held to start with. dialog.type()anddialog.message()tell you which kind of dialog opened and what it said.
page.once() versus page.on(), and why the listener goes first
Both register a listener for an event; the difference is how long it lasts.
| page.once('dialog', fn) | page.on('dialog', fn) | |
|---|---|---|
| Runs for | the first dialog only | every dialog |
| Afterwards | removes itself | stays until page.off() |
| Three alerts in a row | runs once | runs three times |
| Use it for | one expected dialog | repeated dialogs, or logging them all |
Clicking an alert button three times confirms the last row: page.on ran three times, and page.once ran once.
The listener goes first. Test 1 registers its listener after the click. The alert opens during the click, finds no listener, and Playwright dismisses it. The page.once() registered afterwards waits for a dialog that never comes, so its logs and its expect() never run, and the test passes without checking anything. The class tried this order live and it appeared to work, for exactly that reason. Moving the listener above the click gives:
Alert type: alert
Alert message: I am a JS Alert
An expect() inside a handler that does run fails the test as you would hope, whether it comes before or after accept(). The trap is only the handler that never runs.
The dialog event is one of many a page emits. The others include console, request, response, popup, download, pageerror, framenavigated, websocket, worker, close, crash and filechooser, all registered the same way.
Parallel or serial
The batch config sets fullyParallel: true, so the three tests in the describe block run in parallel, spread across workers. The class first described them as sequential by default, then corrected itself: it is the config that makes them parallel. To run a group in order, use test.describe.serial(...) in place of test.describe(...).
Shadow DOM comes later. Playwright's CSS and getBy locators reach inside shadow DOM on their own, where Selenium needed workarounds. The class flagged it for an upcoming session.
Tasks and announcements
- Code: all six specs are in the batch repo, in
tests/10_Keyboard_Hover_Drag_Drop_Calenderandtests/11_JS_Alerts. - Practise more on the Playwright practice pages: right-click, keyboard, windows and tabs, upload and download, and toast notifications.
- Still to come: pop-ups and new windows, toast messages, and shadow DOM.
Task 1, the hover menu. On the hover menu practice page, hover over Add-ons, click Wi-Fi, and then verify that Wi-Fi was clicked.
Task 2, the second Kanban card. On the Kanban board, move the second card in To Do into another column using the bounding-box approach. The class did the first card; a learner in an earlier batch pointed out that the second one is the real test.
Task 3, Automation Challenge 1. Log in with the details in the task post, total the amounts on the page, and assert that the total matches the figure the task gives. A video walkthrough is attached to the task post. It is a known interview question, so write it in Playwright rather than Selenium.