refactor: Centralize Element Plus el-message-box selectors into a (#32879)

Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
n8n-cat-bot[bot] 2026-06-23 18:15:29 +00:00 committed by GitHub
parent 9dce29821b
commit 192b5b8436
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 72 additions and 44 deletions

View File

@ -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",

View File

@ -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<void> {
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<void> {

View File

@ -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();
}

View File

@ -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() {

View File

@ -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<void> {
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

View File

@ -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<void> {
@ -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();
}
}

View File

@ -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<void> {
@ -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<void> {

View File

@ -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<void> {
const messageBox = this.getMessageBox();
await messageBox.waitFor({ state: 'visible' });
await messageBox.locator('.btn--cancel').click();
await new MessageBox(messageBox).cancelButton.click();
}
}

View File

@ -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(

View File

@ -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<void> {

View File

@ -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<void> {
await this.page.locator('.btn--confirm').click();
await new MessageBox(this.page).confirmButton.click();
}
// Duplicate modal (opened by openDuplicate)

View File

@ -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 `<body>` 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
* `<body>`-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 });
}
}

View File

@ -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();