import type { Locator, Page } from '@playwright/test'; export class NotificationsPage { readonly page: Page; constructor(page: Page) { this.page = page; } /** * Gets the main container locator for a notification by searching in its title text. * @param text The text or a regular expression to find within the notification's title. * @returns A Locator for the notification container element. */ getNotificationByTitle(text: string | RegExp): Locator { return this.page.getByRole('alert').filter({ has: this.page.locator('.el-notification__title').filter({ hasText: text }), }); } /** * Gets the main container locator for a notification by searching in its content/body text. * This is useful for finding notifications where the detailed message is in the content * rather than the title (e.g., error messages with detailed descriptions). * @param text The text or a regular expression to find within the notification's content. * @returns A Locator for the notification container element. */ getNotificationByContent(text: string | RegExp): Locator { return this.page.getByRole('alert').filter({ has: this.page.locator('.el-notification__content').filter({ hasText: text }), }); } /** * Gets the main container locator for a notification by searching in both title and content. * This is the most flexible method as it will find notifications regardless of whether * the text appears in the title or content section. * @param text The text or a regular expression to find within the notification's title or content. * @returns A Locator for the notification container element. */ getNotificationByTitleOrContent(text: string | RegExp): Locator { return this.page.getByRole('alert').filter({ hasText: text }); } /** * Gets the main container locator for a notification by searching in both title and content, * filtered to a specific node name. This is useful when multiple notifications might be present * and you want to ensure you're checking the right one for a specific node. * @param text The text or a regular expression to find within the notification's title or content. * @param nodeName The name of the node to filter notifications for. * @returns A Locator for the notification container element. */ getNotificationByTitleOrContentForNode(text: string | RegExp, nodeName: string): Locator { return this.page.getByRole('alert').filter({ hasText: text }).filter({ hasText: nodeName }); } /** * Clicks the close button on the FIRST notification matching the text. * Fast execution with short timeouts for snappy notifications. * @param text The text of the notification to close. * @param options Optional configuration */ async closeNotificationByText( text: string | RegExp, options: { timeout?: number } = {}, ): Promise { const { timeout = 2000 } = options; 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; } } /** * Closes ALL currently visible notifications that match the given text. * Uses aggressive polling for fast cleanup. * @param text The text of the notifications to close. * @param options Optional configuration */ async closeAllNotificationsWithText( text: string | RegExp, options: { timeout?: number; maxRetries?: number } = {}, ): Promise { const { maxRetries = 15 } = options; let closedCount = 0; let retries = 0; while (retries < maxRetries) { try { const notifications = this.getNotificationByTitle(text); const count = await notifications.count(); if (count === 0) { break; } // Close the first visible notification quickly const firstNotification = notifications.first(); if (await firstNotification.isVisible({ timeout: 200 })) { const closeBtn = firstNotification.locator('.el-notification__closeBtn'); await closeBtn.click({ timeout: 300 }); // Brief wait for disappearance, then continue await firstNotification.waitFor({ state: 'hidden', timeout: 500 }).catch(() => {}); closedCount++; } else { // If not visible, likely already gone break; } } catch (error) { // Continue quickly on any error break; } retries++; } return closedCount; } /** * Check if a notification is visible based on text. * Fast check with short timeout. * @param text The text to search for in notification title. * @param options Optional configuration */ async isNotificationVisible( text: string | RegExp, options: { timeout?: number } = {}, ): Promise { const { timeout = 500 } = options; try { const notification = this.getNotificationByTitle(text).first(); await notification.waitFor({ state: 'visible', timeout }); return true; } catch { return false; } } /** * Wait for a notification to appear with specific text. * Reasonable timeout for waiting, but still faster than before. * @param text The text to search for in notification title. * @param options Optional configuration */ async waitForNotification( text: string | RegExp, options: { timeout?: number } = {}, ): Promise { const { timeout = 5000 } = options; try { const notification = this.getNotificationByTitle(text).first(); await notification.waitFor({ state: 'visible', timeout }); return true; } catch { return false; } } // Wait for notification and then close it async waitForNotificationAndClose( text: string | RegExp, options: { timeout?: number } = {}, ): Promise { const { timeout = 3000 } = options; await this.waitForNotification(text, { timeout }); await this.closeNotificationByText(text, { timeout }); return true; } /** * Get all visible notification texts. * @returns Array of notification title texts */ async getAllNotificationTexts(): Promise { try { const titles = this.page.getByRole('alert').locator('.el-notification__title'); return await titles.allTextContents(); } catch { return []; } } /** * Wait for all notifications to disappear. * Fast check with short timeout. * @param options Optional configuration */ async waitForAllNotificationsToDisappear(options: { timeout?: number } = {}): Promise { const { timeout = 2000 } = options; try { // Wait for no alerts to be visible await this.page.getByRole('alert').first().waitFor({ state: 'detached', timeout, }); return true; } catch { // Check if any are still visible const count = await this.getNotificationCount(); return count === 0; } } /** * Get the count of visible notifications. * @param text Optional text to filter notifications */ async getNotificationCount(text?: string | RegExp): Promise { try { const notifications = text ? this.getNotificationByTitle(text) : this.page.getByRole('alert'); return await notifications.count(); } catch { return 0; } } /** * Quick utility to close any notification and continue. * Uses the most aggressive timeouts for maximum speed. * @param text The text of the notification to close. */ async quickClose(text: string | RegExp): Promise { try { const notification = this.getNotificationByTitle(text).first(); if (await notification.isVisible({ timeout: 100 })) { await notification.locator('.el-notification__closeBtn').click({ timeout: 200 }); } } catch { // Silent fail for speed } } /** * Nuclear option: Close everything as fast as possible. * No waiting, no error handling, just close and move on. */ async quickCloseAll(): Promise { try { const closeButtons = this.page.locator('.el-notification__closeBtn'); const count = await closeButtons.count(); for (let i = 0; i < count; i++) { try { await closeButtons.nth(i).click({ timeout: 100 }); } catch { // Continue silently } } } catch { // Silent fail } } }