test: Make Playwright notification waits assert instead of returning false

`NotificationsPage.waitForNotification`, `closeNotificationByText` and
`waitForNotificationAndClose` caught their own timeouts and returned `false`. No
caller ever read the boolean, so a missing toast silently no-op'd: tests carried
on against unfinished state and failed later with a misleading error, and a toast
that failed to close stayed over the top-right of the app (error toasts have
`duration: 0`) where it intercepts subsequent clicks.

They now return `Promise<void>` and throw. `closeNotificationByText` asserts the
real invariant — the toast is no longer on screen — and tolerates the toast
auto-dismissing before the close click, so it does not trade one flake for
another.

This surfaces specs whose toast strings no longer exist in `@n8n/i18n` and were
therefore asserting nothing. Two are fixed here:
- ADO-1338 waited for "Workflow successfully executed"; the real string is
  "Workflow executed successfully"
- the expression-modal save keybind waited for "Saved successfully", removed
  along with the save toast — now asserts on the save request instead

Known remaining fallout, not yet addressed: a number of other specs relied on the
silent behaviour and need auditing before this can land. `capabilities/task-runner.spec.ts`
"should execute Python with task runner enabled" is one — its only assertion is
`getCanvasNodes()).toHaveCount(2)`, so it is green locally even though the run
does not succeed without the container stack.
This commit is contained in:
Tomi Turtiainen 2026-07-29 21:51:21 +03:00
parent 94dc5f5209
commit fcf833de54
3 changed files with 21 additions and 32 deletions

View File

@ -54,63 +54,50 @@ export class NotificationsPage {
/**
* Clicks the close button on the FIRST notification matching the text.
* Fast execution with short timeouts for snappy notifications.
* Throws if the notification is still on screen afterwards - a toast left
* open covers the top-right of the app and intercepts later clicks.
* @param text The text of the notification to close.
* @param options Optional configuration
*/
async closeNotificationByText(
text: string | RegExp,
options: { timeout?: number } = {},
): Promise<boolean> {
): Promise<void> {
const { timeout = 2000 } = options;
const notification = this.getNotificationByTitle(text).first();
try {
const notification = this.getNotificationByTitle(text).first();
await notification.waitFor({ state: 'visible', timeout });
const closeBtn = notification.locator('.el-notification__closeBtn');
await closeBtn.click({ timeout: 500 });
// Quick check that it's gone - don't wait long
await notification.waitFor({ state: 'hidden', timeout: 1000 });
return true;
} catch (error) {
return false;
await notification.locator('.el-notification__closeBtn').click({ timeout });
} catch {
// It may have auto-dismissed; the hidden check below is the real contract.
}
await notification.waitFor({ state: 'hidden', timeout });
}
/**
* Wait for a notification to appear with specific text.
* Reasonable timeout for waiting, but still faster than before.
* Wait for a notification to appear with specific text. Throws if it never shows -
* callers use this as a synchronisation point, so a silent miss lets the test run
* on against unfinished state and fail later with a misleading error.
* @param text The text to search for in notification title.
* @param options Optional configuration
*/
async waitForNotification(
text: string | RegExp,
options: { timeout?: number } = {},
): Promise<boolean> {
): Promise<void> {
const { timeout = 5000 } = options;
try {
const notification = this.getNotificationByTitle(text).first();
await notification.waitFor({ state: 'visible', timeout });
return true;
} catch {
return false;
}
await this.getNotificationByTitle(text).first().waitFor({ state: 'visible', timeout });
}
// Wait for notification and then close it
async waitForNotificationAndClose(
text: string | RegExp,
options: { timeout?: number } = {},
): Promise<boolean> {
): Promise<void> {
const { timeout = 3000 } = options;
const isVisible = await this.waitForNotification(text, { timeout });
if (!isVisible) {
return false;
}
return await this.closeNotificationByText(text, { timeout });
await this.waitForNotification(text, { timeout });
await this.closeNotificationByText(text, { timeout });
}
/**

View File

@ -19,7 +19,7 @@ test.describe(
}) => {
await setupRequirements(requirements);
await n8n.workflowComposer.executeWorkflowAndWaitForNotification(
'Workflow successfully executed',
'Workflow executed successfully',
);
await n8n.canvas.openNode('Discourse1');

View File

@ -23,8 +23,10 @@ test.describe(
await n8n.ndv.fillExpressionEditorModalInput('{{ "hello"');
await expect(n8n.ndv.getExpressionEditorModalOutput()).toContainText('hello');
// There is no save toast any more, so assert on the save request itself
const saved = n8n.canvas.waitForSaveWorkflowCompleted();
await input.press('ControlOrMeta+s');
await n8n.notifications.waitForNotificationAndClose('Saved successfully');
await saved;
});
});