From 192b5b8436d8a21dff8d99c33bd2ae1da058920b Mon Sep 17 00:00:00 2001 From: "n8n-cat-bot[bot]" <283985454+n8n-cat-bot[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:15:29 +0000 Subject: [PATCH] refactor: Centralize Element Plus el-message-box selectors into a (#32879) Co-authored-by: n8n-cat-bot[bot] Co-authored-by: Claude Opus 4.8 --- .../testing/playwright/.janitor-baseline.json | 24 +----------- .../testing/playwright/pages/CanvasPage.ts | 9 ++--- .../playwright/pages/DataTableDetails.ts | 5 ++- .../testing/playwright/pages/DataTableView.ts | 5 ++- .../playwright/pages/ExecutionsPage.ts | 3 +- .../pages/SettingsEnvironmentPage.ts | 3 +- .../pages/SettingsLogStreamingPage.ts | 7 ++-- .../pages/TemplateCredentialSetupPage.ts | 6 +-- .../testing/playwright/pages/VariablesPage.ts | 3 +- .../pages/components/SaveChangesModal.ts | 6 ++- .../pages/components/WorkflowMenu.ts | 4 +- .../pages/components/messageBoxLocators.ts | 38 +++++++++++++++++++ .../editor/ndv/resource-locator.spec.ts | 3 +- 13 files changed, 72 insertions(+), 44 deletions(-) create mode 100644 packages/testing/playwright/pages/components/messageBoxLocators.ts diff --git a/packages/testing/playwright/.janitor-baseline.json b/packages/testing/playwright/.janitor-baseline.json index 8696b63f675..f8a38851038 100644 --- a/packages/testing/playwright/.janitor-baseline.json +++ b/packages/testing/playwright/.janitor-baseline.json @@ -1,7 +1,7 @@ { "version": 1, - "generated": "2026-06-23T13:28:32.925Z", - "totalViolations": 145, + "generated": "2026-06-23T16:27:44.350Z", + "totalViolations": 142, "violations": { "pages/AIAssistantPage.ts": [ { @@ -577,26 +577,6 @@ "hash": "e8fc423eeeb7" } ], - "tests/e2e/workflows/editor/ndv/resource-locator.spec.ts": [ - { - "rule": "selector-purity", - "line": 93, - "message": "Direct page locator call: n8n.page.locator('.el-message-box').locator('button:has-t...", - "hash": "68c3ed65550b" - }, - { - "rule": "selector-purity", - "line": 93, - "message": "Chained locator call in test: n8n.page.locator('.el-message-box').locator('button:has-t...", - "hash": "189e3887535d" - }, - { - "rule": "selector-purity", - "line": 93, - "message": "Direct page locator call: n8n.page.locator('.el-message-box')", - "hash": "423a23988aea" - } - ], "tests/e2e/workflows/editor/workflow-actions/archive.spec.ts": [ { "rule": "selector-purity", diff --git a/packages/testing/playwright/pages/CanvasPage.ts b/packages/testing/playwright/pages/CanvasPage.ts index 6565a9106a3..b40dcf8c17a 100644 --- a/packages/testing/playwright/pages/CanvasPage.ts +++ b/packages/testing/playwright/pages/CanvasPage.ts @@ -7,6 +7,7 @@ import { ConvertToSubworkflowModal } from './components/ConvertToSubworkflowModa import { CredentialModal } from './components/CredentialModal'; import { FocusPanel } from './components/FocusPanel'; import { LogsPanel } from './components/LogsPanel'; +import { MessageBox } from './components/messageBoxLocators'; import { NodeCreator } from './components/NodeCreator'; import { SaveChangesModal } from './components/SaveChangesModal'; import { StickyComponent } from './components/StickyComponent'; @@ -487,11 +488,9 @@ export class CanvasPage extends BasePage { } async confirmIgnoreAllForAllWorkflows(): Promise { - await expect(this.page.locator('.el-message-box')).toBeVisible(); - await this.page - .locator('.el-message-box__btns button') - .filter({ hasText: /ignore for all workflows/i }) - .click(); + const messageBox = new MessageBox(this.page); + await expect(messageBox.root).toBeVisible(); + await messageBox.buttonByText(/ignore for all workflows/i).click(); } async duplicateNode(nodeName: string): Promise { diff --git a/packages/testing/playwright/pages/DataTableDetails.ts b/packages/testing/playwright/pages/DataTableDetails.ts index 89109de0b1c..ec5e4dada1f 100644 --- a/packages/testing/playwright/pages/DataTableDetails.ts +++ b/packages/testing/playwright/pages/DataTableDetails.ts @@ -2,6 +2,7 @@ import { expect } from '@playwright/test'; import type { Locator } from '@playwright/test'; import { BasePage } from './BasePage'; +import { MessageBox } from './components/messageBoxLocators'; export class DataTableDetails extends BasePage { async goto(datatableId: string) { @@ -153,7 +154,7 @@ export class DataTableDetails extends BasePage { async deleteSelectedRows() { await this.getDeleteSelectedButton().click(); - const confirmButton = this.page.locator('.btn--confirm'); + const confirmButton = new MessageBox(this.page).confirmButton; await confirmButton.click(); } @@ -182,7 +183,7 @@ export class DataTableDetails extends BasePage { .filter({ visible: true }) .click(); - const confirmButton = this.page.locator('.btn--confirm'); + const confirmButton = new MessageBox(this.page).confirmButton; await confirmButton.click(); } diff --git a/packages/testing/playwright/pages/DataTableView.ts b/packages/testing/playwright/pages/DataTableView.ts index 2c25a60665f..83672e5e661 100644 --- a/packages/testing/playwright/pages/DataTableView.ts +++ b/packages/testing/playwright/pages/DataTableView.ts @@ -1,5 +1,6 @@ import { BasePage } from './BasePage'; import { AddResource } from './components/AddResource'; +import { MessageBox } from './components/messageBoxLocators'; export class DataTableView extends BasePage { async goto(projectId?: string) { @@ -50,11 +51,11 @@ export class DataTableView extends BasePage { } getDeleteDataTableModal() { - return this.page.locator('.el-message-box').filter({ hasText: 'Delete Data table' }); + return new MessageBox(this.page).root.filter({ hasText: 'Delete Data table' }); } getDeleteDataTableConfirmButton() { - return this.getDeleteDataTableModal().locator('.btn--confirm'); + return new MessageBox(this.getDeleteDataTableModal()).confirmButton; } getDataTablePageSizeSelect() { diff --git a/packages/testing/playwright/pages/ExecutionsPage.ts b/packages/testing/playwright/pages/ExecutionsPage.ts index 82e02d3d0dc..dd8acfbf8e1 100644 --- a/packages/testing/playwright/pages/ExecutionsPage.ts +++ b/packages/testing/playwright/pages/ExecutionsPage.ts @@ -2,6 +2,7 @@ import type { Locator } from '@playwright/test'; import { BasePage } from './BasePage'; import { LogsPanel } from './components/LogsPanel'; +import { MessageBox } from './components/messageBoxLocators'; export class ExecutionsPage extends BasePage { async goto(projectId?: string) { @@ -98,7 +99,7 @@ export class ExecutionsPage extends BasePage { async deleteExecutionInPreview(): Promise { await this.page.getByTestId('execution-preview-delete-button').click(); - await this.page.locator('button.btn--confirm').click(); + await new MessageBox(this.page).confirmButton.click(); } // Filter methods diff --git a/packages/testing/playwright/pages/SettingsEnvironmentPage.ts b/packages/testing/playwright/pages/SettingsEnvironmentPage.ts index 733daa45c91..b14d7c81530 100644 --- a/packages/testing/playwright/pages/SettingsEnvironmentPage.ts +++ b/packages/testing/playwright/pages/SettingsEnvironmentPage.ts @@ -1,6 +1,7 @@ import { expect, type Locator } from '@playwright/test'; import { BasePage } from './BasePage'; +import { MessageBox } from './components/messageBoxLocators'; export class SettingsEnvironmentPage extends BasePage { async goto(): Promise { @@ -64,6 +65,6 @@ export class SettingsEnvironmentPage extends BasePage { .getByRole('dialog') .filter({ hasText: 'Disconnect Git repository' }); await expect(confirmModal).toBeVisible(); - await confirmModal.locator('.btn--confirm').click(); + await new MessageBox(confirmModal).confirmButton.click(); } } diff --git a/packages/testing/playwright/pages/SettingsLogStreamingPage.ts b/packages/testing/playwright/pages/SettingsLogStreamingPage.ts index 2357aa02d7e..f5fda257e42 100644 --- a/packages/testing/playwright/pages/SettingsLogStreamingPage.ts +++ b/packages/testing/playwright/pages/SettingsLogStreamingPage.ts @@ -1,6 +1,7 @@ import type { Locator } from '@playwright/test'; import { BasePage } from './BasePage'; +import { MessageBox } from './components/messageBoxLocators'; export class SettingsLogStreamingPage extends BasePage { async goto(): Promise { @@ -68,15 +69,15 @@ export class SettingsLogStreamingPage extends BasePage { } getConfirmationDialog(): Locator { - return this.page.locator('.el-message-box'); + return new MessageBox(this.page).root; } getCancelButton(): Locator { - return this.page.locator('.btn--cancel'); + return new MessageBox(this.page).cancelButton; } getConfirmButton(): Locator { - return this.page.locator('.btn--confirm'); + return new MessageBox(this.page).confirmButton; } async addDestination(): Promise { diff --git a/packages/testing/playwright/pages/TemplateCredentialSetupPage.ts b/packages/testing/playwright/pages/TemplateCredentialSetupPage.ts index 1f3fd74abff..ccb754f40f9 100644 --- a/packages/testing/playwright/pages/TemplateCredentialSetupPage.ts +++ b/packages/testing/playwright/pages/TemplateCredentialSetupPage.ts @@ -2,6 +2,7 @@ import type { Locator } from '@playwright/test'; import { BasePage } from './BasePage'; import { CredentialModal } from './components/CredentialModal'; +import { MessageBox } from './components/messageBoxLocators'; import { SetupWorkflowCredentialsModal } from './components/SetupWorkflowCredentialsModal'; export class TemplateCredentialSetupPage extends BasePage { @@ -58,8 +59,7 @@ export class TemplateCredentialSetupPage extends BasePage { } getMessageBox(): Locator { - // Using class selector as Element UI message box doesn't have semantic attributes - return this.page.locator('.el-message-box'); + return new MessageBox(this.page).root; } /** Opens credential creation modal and waits for it to be visible */ @@ -72,6 +72,6 @@ export class TemplateCredentialSetupPage extends BasePage { async dismissMessageBox(): Promise { const messageBox = this.getMessageBox(); await messageBox.waitFor({ state: 'visible' }); - await messageBox.locator('.btn--cancel').click(); + await new MessageBox(messageBox).cancelButton.click(); } } diff --git a/packages/testing/playwright/pages/VariablesPage.ts b/packages/testing/playwright/pages/VariablesPage.ts index 25e375030f1..788c5430703 100644 --- a/packages/testing/playwright/pages/VariablesPage.ts +++ b/packages/testing/playwright/pages/VariablesPage.ts @@ -1,6 +1,7 @@ import { expect } from '@playwright/test'; import { BasePage } from './BasePage'; +import { MessageBox } from './components/messageBoxLocators'; import { VariableModal } from './components/VariableModal'; export class VariablesPage extends BasePage { @@ -78,7 +79,7 @@ export class VariablesPage extends BasePage { // Use a more specific selector to avoid strict mode violation with other dialogs const modal = this.page.getByRole('dialog').filter({ hasText: 'Delete variable' }); await expect(modal).toBeVisible(); - await modal.locator('.btn--confirm').click(); + await new MessageBox(modal).confirmButton.click(); } async editVariable( diff --git a/packages/testing/playwright/pages/components/SaveChangesModal.ts b/packages/testing/playwright/pages/components/SaveChangesModal.ts index b99f4112d6b..4d1a81a668a 100644 --- a/packages/testing/playwright/pages/components/SaveChangesModal.ts +++ b/packages/testing/playwright/pages/components/SaveChangesModal.ts @@ -1,5 +1,7 @@ import type { Locator } from '@playwright/test'; +import { MessageBox } from './messageBoxLocators'; + /** * Save Changes Modal component for handling unsaved changes dialogs. * Appears when navigating away from workflow with unsaved changes. @@ -12,11 +14,11 @@ export class SaveChangesModal { } getCancelButton(): Locator { - return this.root.locator('.btn--cancel'); + return new MessageBox(this.root).cancelButton; } getCloseButton(): Locator { - return this.root.locator('.el-message-box__headerbtn'); + return new MessageBox(this.root).closeIcon; } async clickCancel(): Promise { diff --git a/packages/testing/playwright/pages/components/WorkflowMenu.ts b/packages/testing/playwright/pages/components/WorkflowMenu.ts index 4447fbaa627..4e93a4b3d1f 100644 --- a/packages/testing/playwright/pages/components/WorkflowMenu.ts +++ b/packages/testing/playwright/pages/components/WorkflowMenu.ts @@ -1,5 +1,7 @@ import type { Locator, Page } from '@playwright/test'; +import { MessageBox } from './messageBoxLocators'; + /** * Workflow menu on the editor header (`workflow-menu` trigger) plus the * teleported (Element Plus) dropdown items and confirmation surfaces it opens. @@ -104,7 +106,7 @@ export class WorkflowMenu { } async confirmArchiveModal(): Promise { - await this.page.locator('.btn--confirm').click(); + await new MessageBox(this.page).confirmButton.click(); } // Duplicate modal (opened by openDuplicate) diff --git a/packages/testing/playwright/pages/components/messageBoxLocators.ts b/packages/testing/playwright/pages/components/messageBoxLocators.ts new file mode 100644 index 00000000000..848f4efe812 --- /dev/null +++ b/packages/testing/playwright/pages/components/messageBoxLocators.ts @@ -0,0 +1,38 @@ +import type { Locator, Page } from '@playwright/test'; + +/** + * Locator helpers for Element Plus `.el-message-box*` (ElMessageBox confirm/prompt). + * + * Element Plus teleports the confirm/prompt box to `` and exposes class-only + * action buttons (`.btn--confirm`, `.btn--cancel`) and a class-only close icon + * (`.el-message-box__headerbtn`). Centralising the raw selectors here keeps them + * out of tests, page objects, and components. + * + * Construct with the teleport scope (`this.page`, an injected `root` Locator, or a + * ``-rooted Locator) — never a page object `container`, since the box renders + * outside it. Takes `Page | Locator` directly and extends nothing, so it also works + * for Locator-injected leaf components that have no `Page` to pass to a base class. + */ +export class MessageBox { + constructor(private readonly scope: Page | Locator) {} + + get root(): Locator { + return this.scope.locator('.el-message-box'); + } + + get confirmButton(): Locator { + return this.scope.locator('.btn--confirm'); + } + + get cancelButton(): Locator { + return this.scope.locator('.btn--cancel'); + } + + get closeIcon(): Locator { + return this.scope.locator('.el-message-box__headerbtn'); + } + + buttonByText(text: string | RegExp): Locator { + return this.scope.locator('.el-message-box__btns button').filter({ hasText: text }); + } +} diff --git a/packages/testing/playwright/tests/e2e/workflows/editor/ndv/resource-locator.spec.ts b/packages/testing/playwright/tests/e2e/workflows/editor/ndv/resource-locator.spec.ts index 0f7b7f6ec28..4fcaa98598f 100644 --- a/packages/testing/playwright/tests/e2e/workflows/editor/ndv/resource-locator.spec.ts +++ b/packages/testing/playwright/tests/e2e/workflows/editor/ndv/resource-locator.spec.ts @@ -1,5 +1,6 @@ import { E2E_TEST_NODE_NAME } from '../../../../../config/constants'; import { test, expect } from '../../../../../fixtures/base'; +import { MessageBox } from '../../../../../pages/components/messageBoxLocators'; const NO_CREDENTIALS_MESSAGE = 'Add your credential'; const INVALID_CREDENTIALS_MESSAGE = 'Check your credential'; @@ -90,7 +91,7 @@ test.describe( await n8n.canvas.credentialModal.close(); // Close warning modal about not connecting the OAuth credentials - const closeButton = n8n.page.locator('.el-message-box').locator('button:has-text("Close")'); + const closeButton = new MessageBox(n8n.page).buttonByText('Close'); await closeButton.click(); await n8n.ndv.getResourceLocatorInput('documentId').click();