I’m working with Playwright (Node.js) and need to capture notification toasts that appear on the page.
The issue is that the toasts don’t always have the same structure:
-
Some have only a message
-
Some have a title and a message
-
They also change their class (
toast-info
,toast-warning
,toast-error
, etc.)
When I try to capture both the title and the message, Playwright sometimes freezes, waiting for .toast-title
when it doesn’t exist. I need a safe and generic way to capture all possible cases without running into timeouts.
<!-- example of toast with only message -->
<div id="toast-container" class="toast-top-right" aria-live="polite" role="alert">
<div class="toast toast-info" style="display: block;">
<button class="toast-close-button" role="button"><i class="ion-android-close"></i></button>
<div class="toast-message">Nota Lesión Activa: Lesiones 364</div>
</div>
</div>
<!-- example of toast with title and message -->
<div id="toast-container" class="toast-top-right" aria-live="polite" role="alert">
<div class="toast toast-error" style="display: block;">
<button class="toast-close-button" role="button"><i class="ion-android-close"></i></button>
<div class="toast-title">Error</div>
<div class="toast-message">Paciente no encontrado</div>
</div>
</div>
Error observed
Sometimes my test fails with a timeout when trying to get the toast title:
Get text content(
page.locator('#toast-container .toast').first().locator('.toast-title')
)
— 30.0s
waiting for locator('#toast-container .toast').first().locator('.toast-title')
Timeout 30000ms exceeded.
What I tried
I created two functions: one (boton) that clicks a button and checks for toasts, and another (Capturaerror) that extracts the toast-title and toast-message.
async function boton(page, Accion, indice = 0) {
await page.getByRole('button', { name: Accion }).nth(indice).click({ timeout: 1000 });
const toasts = page.locator('#toast-container .toast, #toast-container .toast-error, #toast-container .toast-warning');
const count = await toasts.count();
const mensajes = [];
for (let i = 0; i < count; i++) {
const toast = toasts.nth(i);
const clase = await toast.getAttribute('class') || '';
const mensaje = await toast.locator('.toast-message').textContent().catch(() => '');
if (clase.includes('toast-error')) {
if (mensaje && mensaje.trim() !== '') {
mensajes.push(`${i + 1}.- ${mensaje.trim()}`);
}
} else if (clase.includes('toast-warning')) {
const msj = await Capturaerror(page);
if (msj && msj.length > 0) {
throw new Error(`${msj.join('n')}`);
}
}
}
if (mensajes.length > 0) {
throw new Error(mensajes.join("n"));
}
}
async function Capturaerror(page) {
const toasts = await page.locator('#toast-container .toast').all();
const mensajes = [];
let index = 1;
for (const toast of toasts) {
const titulo = await toast.locator('.toast-title').textContent().catch(() => '');
const mensaje = await toast.locator('.toast-message').textContent().catch(() => '');
if (titulo || mensaje) {
mensajes.push(`${index}.- ${titulo.trim()}: ${mensaje.trim()}`);
index++;
}
}
return mensajes;
}
What I expected
To always capture the visible text of each toast, regardless of whether it has a title, a message, or both.
The .catch(() => '')
should skip missing .toast-title
without freezing.
What actually happens
If a toast does not contain a .toast-title
, Playwright waits for it until the timeout (30s), which causes the test to freeze.
✅ Notes
-
I want a single generic solution that safely captures the title and/or message, or the whole toast text, without timeouts.
-
Node.js / Playwright environment.