page.on()Stays subscribed for the lifetime of the page. Reach for it when you care about all occurrences.
// fires every time, until you remove it
page.on("console", (msg) => {
console.log(msg.text());
});Subscribe to dialogs, console messages, errors, downloads, and new tabs.
The browser is constantly emitting "things". If you watch closely, you'll find a console.log here, an uncaught exception there, a dialog popping up, a file starting to download, or a target="_blank" link opening a new tab. Playwright exposes these as events on the Page class. Subscribing lets your tests assert on side effects the DOM doesn't show.
You've already met two events, request and response, in the network lesson. This lesson covers the rest.
You can listen to every page event in two ways. Pick one based on whether you care about all occurrences or the next one.
page.on()Stays subscribed for the lifetime of the page. Reach for it when you care about all occurrences.
// fires every time, until you remove it
page.on("console", (msg) => {
console.log(msg.text());
});page.waitForEvent()Returns a Promise that resolves with the next match. Reach for it when you care about the next one.
// resolves with the next matching event
const downloadPromise =
page.waitForEvent("download");
await page.getByRole("link", {
name: "Export CSV",
}).click();
const download = await downloadPromise;Register the listener before the action that triggers the event.
waitForEvent() must be set up before the click, and page.on() only catches
events fired after on() runs.
console eventFires for every console.log, console.warn, console.error, etc. on the page. The handler receives a ConsoleMessage with .text(), .type() ("log", "warning", "error", …), and .args().
Use page.on('console') if you want to catch every message during a test run and don't care about a specific one.
const messages = [];
page.on("console", (msg) => messages.push(msg.text()));
await page.goto("/");
// ...
// ...
// ...
// Assert that there were no `console.log` messages at the end of the test
expect(messages).toEqual([]);
This approach works great if you want to make sure that no leftover console.log messages make it to production. .text() is enough to find out if someone or something is spamming the JS console.
pageerror eventFires when an uncaught exception bubbles up to the window object. pageerror is the most useful "your app broke and the test should know" hook in Playwright land.
You can collect errors the same way you collected console.log messages above. Collect page errors, let your test do whatever it needs to do, and check for errors at the end of the test.
const errors = [];
page.on("pageerror", (error) => errors.push(error));
await page.goto("/");
await page.getByRole("button", { name: "Add to cart" }).click();
// ...
// ...
// ...
// Assert that there were no errors at the end of the test
expect(errors).toEqual([]);
A common pattern is to register these handlers in a beforeEach (or a custom fixture) so every test fails on unexpected runtime errors without having to opt in.
Catch the runtime errorSelect this container via test id: pageerror-exercise
The button below throws an uncaught Error from its onClick handler.
page.on("pageerror") and push every error into an array.data-testid).message matches what you see below.const errors = [];
page.on("pageerror", (error) => errors.push(error));
await page.goto("/lessons/writing-tests/10-page-events");
await page
.getByTestId("pageerror-exercise")
.getByRole("button", { name: /Trigger an error/ })
.click();
expect(errors).toHaveLength(1);
expect(errors[0].message).toBe("Boom! Something went wrong.");
dialog eventFires for alert(), confirm(), prompt(), and beforeunload. The Dialog object tells you the type and message, and lets you respond.
Remember to register your page.on event handler early because it must be in place when the dialog appears.
page.on("dialog", async (dialog) => {
console.log(dialog.type(), dialog.message()); // "confirm", "Delete this item?"
if (dialog.type() === "prompt") {
await dialog.accept("Playwright");
} else {
await dialog.dismiss();
}
});
await page.goto('data:text/html,<script>alert("hi")</script>');
If no dialog listener is registered, Playwright auto-dismisses the
dialog and the action that triggered it (e.g. a click) hangs until it times
out.
Always register a handler for pages that can open dialogs even if all
you do is dialog.dismiss().
Answer a prompt() with a nameSelect this container via test id: prompt-exercise
The button below opens window.prompt("What's your name?"). The text
underneath flips from No name yet… to Hi, <name>! once you
answer.
page.on("dialog") and call dialog.accept("Stefan") to answer the prompt.data-testid)."Hi, Stefan!".No name yet…
page.on("dialog", async (dialog) => {
expect(dialog.type()).toBe("prompt");
expect(dialog.message()).toBe("What's your name?");
await dialog.accept("Stefan");
});
await page.goto("/lessons/writing-tests/10-page-events");
const exercise = page.getByTestId("prompt-exercise");
await exercise.getByRole("button", { name: "Tell me your name" }).click();
await expect(exercise.getByTestId("prompt-greeting")).toHaveText("Hi, Stefan!");
dialog.accept(value) is what makes a prompt() return a string. Pass
nothing and the page sees "". Call dismiss() and it sees null.
download eventFires when the browser starts a download. Use waitForEvent here because the download is triggered by a specific click.
const downloadPromise = page.waitForEvent("download");
await page.getByRole("link", { name: "Download invoice" }).click();
const download = await downloadPromise;
await download.saveAs(`./downloads/${download.suggestedFilename()}`);
Download also exposes .path() (the temp file Playwright wrote) and .failure() for inspecting failed downloads.
Download and inspect an invoiceSelect this container via test id: download-exercise
The link below downloads a small text invoice instead of navigating to it.
page.waitForEvent("download") before the click, and don't await it yet.data-testid).invoice-0042.txt.download.path() and assert that it
contains Total: $42.00.import { readFile } from "node:fs/promises";
// ...
await page.goto("/lessons/writing-tests/10-page-events");
const downloadPromise = page.waitForEvent("download");
await page
.getByTestId("download-exercise")
.getByRole("link", { name: "Download invoice" })
.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe("invoice-0042.txt");
const content = await readFile(await download.path(), "utf-8");
expect(content).toContain("Total: $42.00");
download.path() resolves once the download has finished, so there's no
need to wait for anything else before reading the file. Playwright deletes
downloaded files when the browser context closes. Call download.saveAs()
if you want to keep one around.
popup eventFires when the page opens a new tab. It could be triggered by clicking a target="_blank" link, window.open(), or a server-issued redirect that lands in a new window. The handler receives a fresh Page you can drive like any other.
If a specific click opens the tab, use waitForEvent to catch this one-time action.
const popupPromise = page.waitForEvent("popup");
await page.getByRole("link", { name: "Open in new tab" }).click();
const popup = await popupPromise;
await popup.waitForLoadState();
await expect(popup).toHaveTitle(/Docs/);
To catch new pages opened from any page in the browser context (e.g. a deep link that opens a tab from JS you didn't trigger), use browserContext.on("page") instead.
There are plenty more events available on Page: crash, close,
domcontentloaded, load, worker, websocket, frameattached, etc.
See the full list in the Playwright API reference.
Fail the test on unexpected console output
?log=true to any page URL and the Logger component prints
four messages to the console.page.on("console") and collect every message into an
array.?log=true and watch it pass.Catch the Bluesky share popup
/product/the-multi-managed-snowboard).bsky.app/intent/compose in a
new tab.page.waitForEvent("popup") before the click to grab the new
page.bsky.app.