The Testing Academy · Class Notes Thursday, 1 October (IST)
Live class · study guide

Keyboard presses, hover, drag and drop, right-click menus, and JavaScript alerts with page.once() and page.on()

How to press keys and shortcuts with page.keyboard, open a menu with hover(), drag with dragTo() and, where that does nothing, with page.mouse and boundingBox(), right-click and read a context menu, and handle JavaScript alerts with page.once() and page.on(), plus grouping tests with test.describe. Six 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 specs as the class ended (commit "feat: add input, drag-drop and dialog modules"). Every spec on this page was run, and the output shown is what it prints. The hover demo did not reach the repo, so it is reconstructed from the recording and was run against the live site. The Eraser deck was not reachable while this page was written.

01

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
02

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:

TypeScript
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 .gitignore now lists A.png, O.png and ArrowLeft.png. Give it a folder, such as screenshots/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.

03

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:

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

04

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.

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

05

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:

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

BOUNDINGBOX() TURNS AN ELEMENT INTO PIXELS width height (x, y) center x + width / 2 y + height / 2 the ! tells TypeScript the box is not null: boundingBox() returns null for an element that is not rendered THEN DRIVE THE MOUSE 1 page.mouse.move(source center) 2 page.mouse.down() pick the card up 3 page.mouse.move(target center, { steps: 10 }) 4 page.mouse.up() drop it the card moves to In Progress dragTo() on the same card leaves it in To Do
The center is where a person would grab the card. Moving the mouse there, down, across and up is the drag, one event at a time.
  • The center is x + width / 2 across and y + height / 2 down: 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 ! after boundingBox() 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:

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

06

Right-click and the context menu

click({ button: 'right' }) is a right-click, and allInnerTexts() reads every option in the menu that opens:

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

07

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) and prompt (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 the dialog event.
  • 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".

08

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.

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

Text
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, and dialog.dismiss() is Cancel.
  • Prompt: dialog.accept(text) types into the box before pressing OK, and dialog.defaultValue() is whatever the box held to start with.
  • dialog.type() and dialog.message() tell you which kind of dialog opened and what it said.
09

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.

LISTENER FIRST page.once('dialog') click() alert opens handler runs dialog.accept() test continues the handler's logs and expect() run CLICK FIRST, AS IN THE PUSHED TEST 1 click() alert opens no listener yet: Playwright dismisses it page.once('dialog') registered too late never runs the test passes without checking anything
Register the listener, then trigger the dialog. In the other order, the dialog has come and gone before anything is listening.

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:

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

10

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.

11

Tasks and announcements

  • Code: all six specs are in the batch repo, in tests/10_Keyboard_Hover_Drag_Drop_Calender and tests/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.