test: Migrate small Cypress tests to Playwright (#18922)

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
shortstacked
2025-08-29 09:04:47 +01:00
committed by GitHub
parent 1bc317dce5
commit 7dd89d77d9
18 changed files with 388 additions and 190 deletions

View File

@@ -0,0 +1,31 @@
import { BasePage } from './BasePage';
export class DemoPage extends BasePage {
async visitDemoPage(theme?: 'dark' | 'light') {
const query = theme ? `?theme=${theme}` : '';
await this.page.goto('/workflows/demo' + query);
await this.getBody().waitFor({ state: 'visible' });
// eslint-disable-next-line playwright/no-networkidle
await this.page.waitForLoadState('networkidle');
await this.page.evaluate(() => {
// @ts-expect-error - this is a custom property added by the demo page
window.preventNodeViewBeforeUnload = true;
});
}
/**
* Import a workflow into the demo page
* @param workflow - The workflow to import
*/
async importWorkflow(workflow: object) {
const OPEN_WORKFLOW = { command: 'openWorkflow', workflow };
await this.page.evaluate((message) => {
console.log('Posting message:', JSON.stringify(message));
window.postMessage(JSON.stringify(message), '*');
}, OPEN_WORKFLOW);
}
getBody() {
return this.page.locator('body');
}
}

View File

@@ -552,4 +552,39 @@ export class NodeDetailsViewPage extends BasePage {
// eslint-disable-next-line playwright/no-wait-for-timeout
await this.page.waitForTimeout(2500);
}
// Pagination methods for output panel
getOutputPagination() {
return this.getOutputPanel().getByTestId('ndv-data-pagination');
}
getOutputPaginationPages() {
return this.getOutputPagination().locator('.el-pager li.number');
}
async navigateToOutputPage(pageNumber: number): Promise<void> {
const pages = this.getOutputPaginationPages();
await pages.nth(pageNumber - 1).click();
}
async getCurrentOutputPage(): Promise<number> {
const activePage = this.getOutputPagination().locator('.el-pager li.is-active').first();
const pageText = await activePage.textContent();
return parseInt(pageText ?? '1', 10);
}
async getOutputPageContent(row: number = 0, col: number = 0): Promise<string> {
return (await this.getOutputTbodyCell(row, col).textContent()) ?? '';
}
/**
* Set parameter input value by clearing and filling (for parameters without standard test-id)
* @param parameterName - The parameter name
* @param value - The value to set
*/
async setParameterInputValue(parameterName: string, value: string): Promise<void> {
const input = this.getParameterInput(parameterName).locator('input');
await input.clear();
await input.fill(value);
}
}

View File

@@ -9,6 +9,10 @@ export class SettingsPage extends BasePage {
return this.page.getByTestId('menu-item').getByTestId(id);
}
getMenuItemByText(text: string) {
return this.page.getByTestId('menu-item').getByText(text, { exact: true });
}
async goToSettings() {
await this.page.goto('/settings');
}

View File

@@ -18,7 +18,7 @@ export class VersionsPage extends BasePage {
}
getWhatsNewMenuItem() {
return this.page.getByTestId('menu-item').getByTestId('whats-new');
return this.page.getByText('Whats New');
}
async openWhatsNewMenu() {

View File

@@ -0,0 +1,27 @@
import { BasePage } from './BasePage';
export class WorkerViewPage extends BasePage {
getWorkerCards() {
return this.page.getByTestId('worker-card');
}
getWorkerCard(workerId: string) {
return this.getWorkerCards().filter({ hasText: workerId });
}
getWorkerViewLicensed() {
return this.page.getByTestId('worker-view-licensed');
}
getWorkerViewUnlicensed() {
return this.page.getByTestId('worker-view-unlicensed');
}
getWorkerMenuItem() {
return this.page.getByTestId('menu-item').getByText('Workers', { exact: true });
}
async visitWorkerView() {
await this.page.goto('/settings/workers');
}
}

View File

@@ -4,6 +4,7 @@ import { AIAssistantPage } from './AIAssistantPage';
import { BecomeCreatorCTAPage } from './BecomeCreatorCTAPage';
import { CanvasPage } from './CanvasPage';
import { CredentialsPage } from './CredentialsPage';
import { DemoPage } from './DemoPage';
import { ExecutionsPage } from './ExecutionsPage';
import { IframePage } from './IframePage';
import { InteractionsPage } from './InteractionsPage';
@@ -14,6 +15,7 @@ import { ProjectSettingsPage } from './ProjectSettingsPage';
import { SettingsPage } from './SettingsPage';
import { SidebarPage } from './SidebarPage';
import { VersionsPage } from './VersionsPage';
import { WorkerViewPage } from './WorkerViewPage';
import { WorkflowActivationModal } from './WorkflowActivationModal';
import { WorkflowSettingsModal } from './WorkflowSettingsModal';
import { WorkflowSharingModal } from './WorkflowSharingModal';
@@ -33,7 +35,7 @@ export class n8nPage {
readonly aiAssistant: AIAssistantPage;
readonly becomeCreatorCTA: BecomeCreatorCTAPage;
readonly canvas: CanvasPage;
readonly demo: DemoPage;
readonly iframe: IframePage;
readonly interactions: InteractionsPage;
readonly ndv: NodeDetailsViewPage;
@@ -41,6 +43,7 @@ export class n8nPage {
readonly projectSettings: ProjectSettingsPage;
readonly settings: SettingsPage;
readonly versions: VersionsPage;
readonly workerView: WorkerViewPage;
readonly workflows: WorkflowsPage;
readonly notifications: NotificationsPage;
readonly credentials: CredentialsPage;
@@ -66,7 +69,7 @@ export class n8nPage {
this.aiAssistant = new AIAssistantPage(page);
this.becomeCreatorCTA = new BecomeCreatorCTAPage(page);
this.canvas = new CanvasPage(page);
this.demo = new DemoPage(page);
this.iframe = new IframePage(page);
this.interactions = new InteractionsPage(page);
this.ndv = new NodeDetailsViewPage(page);
@@ -74,6 +77,7 @@ export class n8nPage {
this.projectSettings = new ProjectSettingsPage(page);
this.settings = new SettingsPage(page);
this.versions = new VersionsPage(page);
this.workerView = new WorkerViewPage(page);
this.workflows = new WorkflowsPage(page);
this.notifications = new NotificationsPage(page);
this.credentials = new CredentialsPage(page);

View File

@@ -59,11 +59,16 @@ export class WorkflowApiHelper {
const webhookPrefix = options?.webhookPrefix ?? 'test-webhook';
const uniqueSuffix = nanoid(idLength);
// Make workflow name unique
if (workflow.name) {
// Make workflow name unique; add a default if missing
if (workflow.name && workflow.name.trim().length > 0) {
workflow.name = `${workflow.name} (Test ${uniqueSuffix})`;
} else {
workflow.name = `Test Workflow ${uniqueSuffix}`;
}
// Ensure workflow is inactive by default when not specified
workflow.active ??= false;
// Check if workflow has webhook nodes and process them
let webhookId: string | undefined;
let webhookPath: string | undefined;

View File

@@ -0,0 +1,35 @@
import { test, expect } from '../../fixtures/base';
test.describe('ADO-2230 NDV Pagination Reset', () => {
test('should reset pagination if data size changes to less than current page', async ({
n8n,
}) => {
await n8n.start.fromImportedWorkflow('NDV-debug-generate-data.json');
await n8n.canvas.openNode('DebugHelper');
await n8n.ndv.execute();
await n8n.notifications.quickCloseAll();
const outputPagination = n8n.ndv.getOutputPagination();
await expect(outputPagination).toBeVisible();
await expect(n8n.ndv.getOutputPaginationPages()).toHaveCount(5);
await expect(n8n.ndv.getOutputTbodyCell(0, 0)).not.toBeEmpty();
const firstPageContent = await n8n.ndv.getOutputPageContent(0, 0);
await n8n.ndv.navigateToOutputPage(4);
const fourthPageContent = await n8n.ndv.getOutputPageContent(0, 0);
expect(fourthPageContent).not.toBe(firstPageContent);
await n8n.ndv.setParameterInputValue('randomDataCount', '20');
await n8n.ndv.execute();
await n8n.notifications.quickCloseAll();
await expect(n8n.ndv.getOutputPaginationPages()).toHaveCount(2);
await expect(n8n.ndv.getOutputTbodyCell(0, 0)).not.toBeEmpty();
});
});

View File

@@ -0,0 +1,46 @@
import { test, expect } from '../../fixtures/base';
import type { TestRequirements } from '../../Types';
import simpleWorkflow from '../../workflows/Manual_wait_set.json';
import workflowWithPinned from '../../workflows/Webhook_set_pinned.json';
const requirements: TestRequirements = {
config: {
settings: {
previewMode: true,
},
},
};
test.describe('Demo', () => {
test.beforeEach(async ({ setupRequirements }) => {
await setupRequirements(requirements);
});
test('can import template', async ({ n8n }) => {
await n8n.demo.visitDemoPage();
expect(await n8n.notifications.getAllNotificationTexts()).toHaveLength(0);
await n8n.demo.importWorkflow(simpleWorkflow);
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(3);
});
test('can import workflow with pin data', async ({ n8n }) => {
await n8n.demo.visitDemoPage();
await n8n.demo.importWorkflow(workflowWithPinned);
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
await n8n.canvas.openNode('Webhook');
await expect(n8n.ndv.getOutputTableHeaders().first()).toContainText('headers');
await expect(n8n.ndv.getOutputTableCell(1, 3)).toContainText('dragons');
});
test('can override theme to dark', async ({ n8n }) => {
await n8n.demo.visitDemoPage('dark');
await expect(n8n.demo.getBody()).toHaveAttribute('data-theme', 'dark');
expect(await n8n.notifications.getAllNotificationTexts()).toHaveLength(0);
});
test('can override theme to light', async ({ n8n }) => {
await n8n.demo.visitDemoPage('light');
await expect(n8n.demo.getBody()).toHaveAttribute('data-theme', 'light');
expect(await n8n.notifications.getAllNotificationTexts()).toHaveLength(0);
});
});

View File

@@ -0,0 +1,39 @@
import { test, expect } from '../../fixtures/base';
test.describe
.serial('Worker View', () => {
test.describe('unlicensed', () => {
test.beforeEach(async ({ api }) => {
await api.disableFeature('workerView');
await api.setQueueMode(false);
});
test('should not show up in the menu sidebar', async ({ n8n }) => {
await n8n.workerView.visitWorkerView();
await expect(n8n.workerView.getWorkerMenuItem()).toBeHidden();
});
test('should show action box', async ({ n8n }) => {
await n8n.workerView.visitWorkerView();
await expect(n8n.workerView.getWorkerViewUnlicensed()).toBeVisible();
});
});
test.describe('licensed', () => {
test.beforeEach(async ({ api }) => {
await api.enableFeature('workerView');
await api.setQueueMode(true);
});
test('should show up in the menu sidebar', async ({ n8n }) => {
await n8n.goHome();
await n8n.workerView.visitWorkerView();
await expect(n8n.workerView.getWorkerMenuItem()).toBeVisible();
});
test('should show worker list view', async ({ n8n }) => {
await n8n.workerView.visitWorkerView();
await expect(n8n.workerView.getWorkerViewLicensed()).toBeVisible();
});
});
});

View File

@@ -0,0 +1,75 @@
import { test, expect } from '../../fixtures/base';
import type { TestRequirements } from '../../Types';
const requirements: TestRequirements = {
config: {
settings: {
releaseChannel: 'stable',
versionCli: '1.0.0',
versionNotifications: {
enabled: true,
endpoint: 'https://api.n8n.io/api/versions/',
whatsNewEnabled: true,
whatsNewEndpoint: 'https://api.n8n.io/api/whats-new',
infoUrl: 'https://docs.n8n.io/getting-started/installation/updating.html',
},
},
},
intercepts: {
versions: {
url: '**/api/versions/**',
response: [
{
name: '1.0.0',
nodes: [],
createdAt: '2025-06-01T00:00:00Z',
description: 'Current version',
documentationUrl: 'https://docs.n8n.io',
hasBreakingChange: false,
hasSecurityFix: false,
hasSecurityIssue: false,
securityIssueFixVersion: '',
},
{
name: '1.0.1',
nodes: [],
createdAt: '2025-06-15T00:00:00Z',
description: 'Version 1.0.1',
documentationUrl: 'https://docs.n8n.io',
hasBreakingChange: false,
hasSecurityFix: false,
hasSecurityIssue: false,
securityIssueFixVersion: '',
},
{
name: '1.0.2',
nodes: [],
createdAt: '2025-06-30T00:00:00Z',
description: 'Version 1.0.2',
documentationUrl: 'https://docs.n8n.io',
hasBreakingChange: false,
hasSecurityFix: false,
hasSecurityIssue: false,
securityIssueFixVersion: '',
},
],
},
},
};
test.describe('Versions', () => {
test('should open updates panel', async ({ n8n, setupRequirements }) => {
await setupRequirements(requirements);
await n8n.goHome();
await n8n.versions.openWhatsNewMenu();
await expect(n8n.versions.getVersionUpdatesPanelOpenButton()).toContainText(
'2 versions behind',
);
await n8n.versions.openVersionUpdatesPanel();
await expect(n8n.versions.getVersionCard()).toHaveCount(2);
await n8n.versions.closeVersionUpdatesPanel();
await expect(n8n.versions.getVersionUpdatesPanel()).toBeHidden();
});
});

View File

@@ -0,0 +1,40 @@
import { test, expect } from '../../fixtures/base';
import type { TestRequirements } from '../../Types';
test.describe('Become creator CTA', () => {
test('should not show the CTA if user is not eligible', async ({ n8n, setupRequirements }) => {
const notEligibleRequirements: TestRequirements = {
intercepts: {
cta: {
url: '**/rest/cta/become-creator',
response: false,
},
},
};
await setupRequirements(notEligibleRequirements);
await n8n.goHome();
await expect(n8n.becomeCreatorCTA.getBecomeTemplateCreatorCta()).toBeHidden();
});
test('should show the CTA if the user is eligible', async ({ n8n, setupRequirements }) => {
const eligibleRequirements: TestRequirements = {
intercepts: {
cta: {
url: '**/rest/cta/become-creator',
response: true,
},
},
};
await setupRequirements(eligibleRequirements);
await n8n.goHome();
await expect(n8n.becomeCreatorCTA.getBecomeTemplateCreatorCta()).toBeVisible();
await n8n.becomeCreatorCTA.closeBecomeTemplateCreatorCta();
await expect(n8n.becomeCreatorCTA.getBecomeTemplateCreatorCta()).toBeHidden();
});
});

View File

@@ -0,0 +1,42 @@
{
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "5b397bc122efafc165b2a6e67d5e8d75b8138f0d24d6352fac713e4845b002a6"
},
"nodes": [
{
"parameters": {},
"id": "df260de7-6f28-4d07-b7b5-29588e27335b",
"name": "When clicking \"Execute workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [780, 500]
},
{
"parameters": {
"category": "randomData",
"randomDataSeed": "0",
"randomDataCount": 100
},
"id": "9e9a0708-86dc-474f-a60e-4315e757c08e",
"name": "DebugHelper",
"type": "n8n-nodes-base.debugHelper",
"typeVersion": 1,
"position": [1000, 500]
}
],
"connections": {
"When clicking \"Execute workflow\"": {
"main": [
[
{
"node": "DebugHelper",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {}
}