mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
test: Migrate Cypress test for the log view (#19108)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
11
packages/testing/playwright/CLAUDE.md
Normal file
11
packages/testing/playwright/CLAUDE.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# CLAUDE.md
|
||||
|
||||
## Commands
|
||||
|
||||
- Use `pnpm test:local --reporter=line --grep="..."` to execute tests.
|
||||
|
||||
|
||||
## Code Styles
|
||||
|
||||
- In writing locators, use specialized methods when available.
|
||||
For example, prefer `page.getByRole('button')` over `page.locator('[role=button]')`.
|
||||
@@ -93,7 +93,7 @@ export interface TestRequirements {
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
workflow?: Record<string, string>;
|
||||
workflow?: string | Record<string, string>;
|
||||
|
||||
/**
|
||||
* Browser storage values to set before the test
|
||||
|
||||
@@ -105,7 +105,7 @@ export const test = base.extend<CloudOnlyFixtures>({
|
||||
},
|
||||
|
||||
// Browser context with cloud container URL and interceptors
|
||||
context: async ({ context, baseURL }, use) => {
|
||||
context: async ({ context }, use) => {
|
||||
await setupDefaultInterceptors(context);
|
||||
await use(context);
|
||||
},
|
||||
@@ -123,8 +123,8 @@ export const test = base.extend<CloudOnlyFixtures>({
|
||||
},
|
||||
|
||||
// n8n page object
|
||||
n8n: async ({ page }, use) => {
|
||||
const n8nInstance = new n8nPage(page);
|
||||
n8n: async ({ page, api }, use) => {
|
||||
const n8nInstance = new n8nPage(page, api);
|
||||
await use(n8nInstance);
|
||||
},
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
"install-browsers:ci": "PLAYWRIGHT_BROWSERS_PATH=./ms-playwright-cache playwright install chromium --with-deps",
|
||||
"browsers:uninstall": "playwright uninstall --all",
|
||||
"lint": "eslint . --quiet",
|
||||
"lint:fix": "eslint . --fix"
|
||||
"lint:fix": "eslint . --fix",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@currents/playwright": "^1.15.3",
|
||||
|
||||
@@ -360,6 +360,10 @@ export class CanvasPage extends BasePage {
|
||||
return this.page.getByTestId('canvas-wrapper');
|
||||
}
|
||||
|
||||
canvasBody(): Locator {
|
||||
return this.page.getByTestId('canvas');
|
||||
}
|
||||
|
||||
toggleFocusPanelButton(): Locator {
|
||||
return this.page.getByTestId('toggle-focus-panel-button');
|
||||
}
|
||||
@@ -526,8 +530,24 @@ export class CanvasPage extends BasePage {
|
||||
return this.getManualChatModal().locator('.chat-messages-list .chat-message');
|
||||
}
|
||||
|
||||
getNodesWithSpinner(): Locator {
|
||||
return this.page.getByTestId('canvas-node').filter({
|
||||
has: this.page.locator('[data-icon=refresh-cw]'),
|
||||
});
|
||||
}
|
||||
|
||||
getWaitingNodes(): Locator {
|
||||
return this.page.getByTestId('canvas-node').filter({
|
||||
has: this.page.locator('[data-icon=clock]'),
|
||||
});
|
||||
}
|
||||
|
||||
async sendManualChatMessage(message: string): Promise<void> {
|
||||
await this.getManualChatInput().fill(message);
|
||||
await this.getManualChatModal().locator('.chat-input-send-button').click();
|
||||
}
|
||||
|
||||
async openExecutions() {
|
||||
await this.page.getByTestId('radio-button-executions').click();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,26 @@ export class ExecutionsPage extends BasePage {
|
||||
return executionItems.nth(0);
|
||||
}
|
||||
|
||||
getAutoRefreshButton() {
|
||||
return this.page.getByTestId('auto-refresh-checkbox');
|
||||
}
|
||||
|
||||
getPreviewIframe() {
|
||||
return this.page.getByTestId('workflow-preview-iframe').contentFrame();
|
||||
}
|
||||
|
||||
getManualChatMessages(): Locator {
|
||||
return this.getPreviewIframe().locator('.chat-messages-list .chat-message');
|
||||
}
|
||||
|
||||
getLogsOverviewStatus() {
|
||||
return this.getPreviewIframe().getByTestId('logs-overview-status');
|
||||
}
|
||||
|
||||
getLogEntries(): Locator {
|
||||
return this.getPreviewIframe().getByTestId('logs-overview-body').getByRole('treeitem');
|
||||
}
|
||||
|
||||
async clickLastExecutionItem(): Promise<void> {
|
||||
const executionItem = this.getLastExecutionItem();
|
||||
await executionItem.click();
|
||||
@@ -30,7 +50,6 @@ export class ExecutionsPage extends BasePage {
|
||||
* @param action - The action to take.
|
||||
*/
|
||||
async handlePinnedNodesConfirmation(action: 'Unpin' | 'Cancel'): Promise<void> {
|
||||
const confirmDialog = this.page.locator('.matching-pinned-nodes-confirmation');
|
||||
await this.page.getByRole('button', { name: action }).click();
|
||||
}
|
||||
}
|
||||
|
||||
92
packages/testing/playwright/pages/LogsPage.ts
Normal file
92
packages/testing/playwright/pages/LogsPage.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class LogsPage extends BasePage {
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessors
|
||||
*/
|
||||
|
||||
getOverviewStatus(): Locator {
|
||||
return this.page.getByTestId('logs-overview-status');
|
||||
}
|
||||
|
||||
getClearExecutionButton(): Locator {
|
||||
return this.page
|
||||
.getByTestId('logs-overview-header')
|
||||
.locator('button')
|
||||
.filter({ hasText: 'Clear execution' });
|
||||
}
|
||||
|
||||
getLogEntries(): Locator {
|
||||
return this.page.getByTestId('logs-overview-body').getByRole('treeitem');
|
||||
}
|
||||
|
||||
getSelectedLogEntry(): Locator {
|
||||
return this.page.getByTestId('logs-overview-body').getByRole('treeitem', { selected: true });
|
||||
}
|
||||
|
||||
getInputPanel(): Locator {
|
||||
return this.page.getByTestId('log-details-input');
|
||||
}
|
||||
|
||||
getInputTableRows(): Locator {
|
||||
return this.getInputPanel().locator('table tr');
|
||||
}
|
||||
|
||||
getInputTbodyCell(row: number, col: number): Locator {
|
||||
return this.getInputPanel().locator('table tbody tr').nth(row).locator('td').nth(col);
|
||||
}
|
||||
|
||||
getNodeErrorMessageHeader(): Locator {
|
||||
return this.getOutputPanel().getByTestId('node-error-message');
|
||||
}
|
||||
|
||||
getOutputPanel(): Locator {
|
||||
return this.page.getByTestId('log-details-output');
|
||||
}
|
||||
|
||||
getOutputTbodyCell(row: number, col: number): Locator {
|
||||
return this.getOutputPanel().locator('table tbody tr').nth(row).locator('td').nth(col);
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
|
||||
async openLogsPanel(): Promise<void> {
|
||||
await this.page.getByTestId('logs-overview-header').click();
|
||||
}
|
||||
|
||||
async clickLogEntryAtRow(rowIndex: number): Promise<void> {
|
||||
await this.getLogEntries().nth(rowIndex).click();
|
||||
}
|
||||
|
||||
async toggleInputPanel(): Promise<void> {
|
||||
await this.page.getByTestId('log-details-header').getByText('Input').click();
|
||||
}
|
||||
|
||||
async clickOpenNdvAtRow(rowIndex: number): Promise<void> {
|
||||
await this.getLogEntries().nth(rowIndex).hover();
|
||||
await this.getLogEntries().nth(rowIndex).getByLabel('Open...').click();
|
||||
}
|
||||
|
||||
async clickTriggerPartialExecutionAtRow(rowIndex: number): Promise<void> {
|
||||
await this.getLogEntries().nth(rowIndex).hover();
|
||||
await this.getLogEntries().nth(rowIndex).getByLabel('Execute step').click();
|
||||
}
|
||||
|
||||
async setInputDisplayMode(mode: 'table' | 'ai' | 'json' | 'schema'): Promise<void> {
|
||||
await this.getInputPanel().hover();
|
||||
await this.getInputPanel().getByTestId(`radio-button-${mode}`).click();
|
||||
}
|
||||
|
||||
async setOutputDisplayMode(mode: 'table' | 'ai' | 'json' | 'schema'): Promise<void> {
|
||||
await this.getOutputPanel().hover();
|
||||
await this.getOutputPanel().getByTestId(`radio-button-${mode}`).click();
|
||||
}
|
||||
}
|
||||
@@ -513,12 +513,8 @@ export class NodeDetailsViewPage extends BasePage {
|
||||
return this.getInputPanel().locator('table th').nth(index);
|
||||
}
|
||||
|
||||
getInputTableCell(row: number, col: number) {
|
||||
return this.getInputPanel().locator('table tbody tr').nth(row).locator('td').nth(col);
|
||||
}
|
||||
|
||||
getInputTbodyCell(row: number, col: number) {
|
||||
return this.getInputTableCell(row, col);
|
||||
return this.getInputPanel().locator('table tbody tr').nth(row).locator('td').nth(col);
|
||||
}
|
||||
|
||||
getAssignmentName(paramName: string, index = 0) {
|
||||
@@ -581,7 +577,7 @@ export class NodeDetailsViewPage extends BasePage {
|
||||
}
|
||||
|
||||
getInputTableCellSpan(row: number, col: number, dataName: string) {
|
||||
return this.getInputTableCell(row, col).locator(`span[data-name="${dataName}"]`).first();
|
||||
return this.getInputTbodyCell(row, col).locator(`span[data-name="${dataName}"]`).first();
|
||||
}
|
||||
|
||||
getAddFieldToSortByButton() {
|
||||
@@ -630,4 +626,16 @@ export class NodeDetailsViewPage extends BasePage {
|
||||
await input.clear();
|
||||
await input.fill(value);
|
||||
}
|
||||
|
||||
getInputSelect() {
|
||||
return this.page.getByTestId('ndv-input-select').locator('input');
|
||||
}
|
||||
|
||||
getInputTableRows() {
|
||||
return this.getInputTable().locator('tr');
|
||||
}
|
||||
|
||||
getOutputRunSelectorInput() {
|
||||
return this.getOutputPanel().locator('[data-test-id="run-selector"] input');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ export class NotificationsPage {
|
||||
text: string | RegExp,
|
||||
options: { timeout?: number; maxRetries?: number } = {},
|
||||
): Promise<number> {
|
||||
const { timeout = 1500, maxRetries = 15 } = options;
|
||||
const { maxRetries = 15 } = options;
|
||||
let closedCount = 0;
|
||||
let retries = 0;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DemoPage } from './DemoPage';
|
||||
import { ExecutionsPage } from './ExecutionsPage';
|
||||
import { IframePage } from './IframePage';
|
||||
import { InteractionsPage } from './InteractionsPage';
|
||||
import { LogsPage } from './LogsPage';
|
||||
import { NodeDetailsViewPage } from './NodeDetailsViewPage';
|
||||
import { NotificationsPage } from './NotificationsPage';
|
||||
import { NpsSurveyPage } from './NpsSurveyPage';
|
||||
@@ -38,6 +39,7 @@ export class n8nPage {
|
||||
readonly demo: DemoPage;
|
||||
readonly iframe: IframePage;
|
||||
readonly interactions: InteractionsPage;
|
||||
readonly logs: LogsPage;
|
||||
readonly ndv: NodeDetailsViewPage;
|
||||
readonly npsSurvey: NpsSurveyPage;
|
||||
readonly projectSettings: ProjectSettingsPage;
|
||||
@@ -72,6 +74,7 @@ export class n8nPage {
|
||||
this.demo = new DemoPage(page);
|
||||
this.iframe = new IframePage(page);
|
||||
this.interactions = new InteractionsPage(page);
|
||||
this.logs = new LogsPage(page);
|
||||
this.ndv = new NodeDetailsViewPage(page);
|
||||
this.npsSurvey = new NpsSurveyPage(page);
|
||||
this.projectSettings = new ProjectSettingsPage(page);
|
||||
|
||||
254
packages/testing/playwright/tests/ui/50-logs.spec.ts
Normal file
254
packages/testing/playwright/tests/ui/50-logs.spec.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
|
||||
// Node name constants
|
||||
const NODES = {
|
||||
MANUAL_TRIGGER: 'When clicking ‘Execute workflow’',
|
||||
CODE: 'Code',
|
||||
LOOP_OVER_ITEMS: 'Loop Over Items',
|
||||
WAIT: 'Wait',
|
||||
CODE1: 'Code1',
|
||||
SCHEDULE_TRIGGER: 'Schedule Trigger',
|
||||
EDIT_FIELDS: 'Edit Fields',
|
||||
IF: 'If',
|
||||
WAIT_NODE: 'Wait node',
|
||||
};
|
||||
|
||||
test.describe('Logs', () => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.goHome();
|
||||
});
|
||||
|
||||
test('should populate logs as manual execution progresses', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements({ workflow: 'Workflow_loop.json' });
|
||||
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
await n8n.logs.openLogsPanel();
|
||||
await expect(n8n.logs.getLogEntries()).toHaveCount(0);
|
||||
|
||||
await n8n.canvas.clickExecuteWorkflowButton();
|
||||
await expect(n8n.logs.getOverviewStatus().filter({ hasText: 'Running' })).toBeVisible();
|
||||
|
||||
await expect(n8n.logs.getLogEntries()).toHaveCount(4);
|
||||
await expect(n8n.logs.getLogEntries().nth(0)).toContainText(NODES.MANUAL_TRIGGER);
|
||||
await expect(n8n.logs.getLogEntries().nth(1)).toContainText(NODES.CODE);
|
||||
await expect(n8n.logs.getLogEntries().nth(2)).toContainText(NODES.LOOP_OVER_ITEMS);
|
||||
await expect(n8n.logs.getLogEntries().nth(3)).toContainText(NODES.WAIT);
|
||||
|
||||
await expect(n8n.logs.getLogEntries()).toHaveCount(6);
|
||||
await expect(n8n.logs.getLogEntries().nth(4)).toContainText(NODES.LOOP_OVER_ITEMS);
|
||||
await expect(n8n.logs.getLogEntries().nth(5)).toContainText(NODES.WAIT);
|
||||
|
||||
await expect(n8n.logs.getLogEntries()).toHaveCount(8);
|
||||
await expect(n8n.logs.getLogEntries().nth(6)).toContainText(NODES.LOOP_OVER_ITEMS);
|
||||
await expect(n8n.logs.getLogEntries().nth(7)).toContainText(NODES.WAIT);
|
||||
|
||||
await expect(n8n.logs.getLogEntries()).toHaveCount(10);
|
||||
await expect(n8n.logs.getLogEntries().nth(8)).toContainText(NODES.LOOP_OVER_ITEMS);
|
||||
await expect(n8n.logs.getLogEntries().nth(9)).toContainText(NODES.CODE1);
|
||||
await expect(
|
||||
n8n.logs.getOverviewStatus().filter({ hasText: /Error in [\d.]+s/ }),
|
||||
).toBeVisible();
|
||||
await expect(n8n.logs.getSelectedLogEntry()).toContainText(NODES.CODE1); // Errored node is automatically selected
|
||||
await expect(n8n.logs.getNodeErrorMessageHeader()).toContainText('test!!! [line 1]');
|
||||
await expect(n8n.canvas.getNodeIssuesByName(NODES.CODE1)).toBeVisible();
|
||||
|
||||
await n8n.logs.getClearExecutionButton().click();
|
||||
await expect(n8n.logs.getLogEntries()).toHaveCount(0);
|
||||
await expect(n8n.canvas.getNodeIssuesByName(NODES.CODE1)).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should allow to trigger partial execution', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements({ workflow: 'Workflow_if.json' });
|
||||
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
await n8n.logs.openLogsPanel();
|
||||
|
||||
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Successful');
|
||||
await expect(n8n.logs.getLogEntries()).toHaveCount(6);
|
||||
await expect(n8n.logs.getLogEntries().nth(0)).toContainText(NODES.SCHEDULE_TRIGGER);
|
||||
await expect(n8n.logs.getLogEntries().nth(1)).toContainText(NODES.CODE);
|
||||
await expect(n8n.logs.getLogEntries().nth(2)).toContainText(NODES.EDIT_FIELDS);
|
||||
await expect(n8n.logs.getLogEntries().nth(3)).toContainText(NODES.IF);
|
||||
await expect(n8n.logs.getLogEntries().nth(4)).toContainText(NODES.EDIT_FIELDS);
|
||||
await expect(n8n.logs.getLogEntries().nth(5)).toContainText(NODES.EDIT_FIELDS);
|
||||
|
||||
await n8n.logs.clickTriggerPartialExecutionAtRow(3);
|
||||
await expect(n8n.logs.getLogEntries()).toHaveCount(3);
|
||||
await expect(n8n.logs.getLogEntries().nth(0)).toContainText(NODES.SCHEDULE_TRIGGER);
|
||||
await expect(n8n.logs.getLogEntries().nth(1)).toContainText(NODES.CODE);
|
||||
await expect(n8n.logs.getLogEntries().nth(2)).toContainText(NODES.IF);
|
||||
});
|
||||
|
||||
// TODO: make it possible to test workflows with AI model end-to-end
|
||||
test.skip('should show input and output data in the selected display mode', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements({ workflow: 'Workflow_ai_agent.json' });
|
||||
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
await n8n.logs.openLogsPanel();
|
||||
await n8n.canvas.sendManualChatMessage('Hi!');
|
||||
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Successful');
|
||||
await expect(n8n.canvas.getManualChatMessages().nth(0)).toContainText('Hi!');
|
||||
await expect(n8n.canvas.getManualChatMessages().nth(1)).toContainText(
|
||||
'Hello from e2e model!!!',
|
||||
);
|
||||
await expect(n8n.logs.getLogEntries().nth(2)).toHaveText('E2E Chat Model');
|
||||
await n8n.logs.getLogEntries().nth(2).click();
|
||||
|
||||
await expect(n8n.logs.getOutputPanel()).toContainText('Hello from e2e model!!!');
|
||||
await n8n.logs.setOutputDisplayMode('table');
|
||||
await expect(n8n.logs.getOutputTbodyCell(0, 0)).toContainText(
|
||||
'text:Hello from **e2e** model!!!',
|
||||
);
|
||||
await expect(n8n.logs.getOutputTbodyCell(0, 1)).toContainText('completionTokens:20');
|
||||
await n8n.logs.setOutputDisplayMode('schema');
|
||||
await expect(n8n.logs.getOutputPanel()).toContainText('generations[0]');
|
||||
await expect(n8n.logs.getOutputPanel()).toContainText('Hello from **e2e** model!!!');
|
||||
await n8n.logs.setOutputDisplayMode('json');
|
||||
await expect(n8n.logs.getOutputPanel()).toContainText('[{"response": {"generations": [');
|
||||
|
||||
await n8n.logs.toggleInputPanel();
|
||||
await expect(n8n.logs.getInputPanel()).toContainText('Human: Hi!');
|
||||
await n8n.logs.setInputDisplayMode('table');
|
||||
await expect(n8n.logs.getInputTbodyCell(0, 0)).toContainText('0:Human: Hi!');
|
||||
await n8n.logs.setInputDisplayMode('schema');
|
||||
await expect(n8n.logs.getInputPanel()).toContainText('messages[0]');
|
||||
await expect(n8n.logs.getInputPanel()).toContainText('Human: Hi!');
|
||||
await n8n.logs.setInputDisplayMode('json');
|
||||
await expect(n8n.logs.getInputPanel()).toContainText('[{"messages": ["Human: Hi!"],');
|
||||
});
|
||||
|
||||
test('should show input and output data of correct run index and branch', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements({ workflow: 'Workflow_if.json' });
|
||||
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
await n8n.logs.openLogsPanel();
|
||||
await n8n.canvas.clickExecuteWorkflowButton();
|
||||
|
||||
await n8n.logs.clickLogEntryAtRow(2); // Run #1 of 'Edit Fields' node; input is 'Code' node
|
||||
await n8n.logs.toggleInputPanel();
|
||||
await n8n.logs.setInputDisplayMode('table');
|
||||
await expect(n8n.logs.getInputTableRows()).toHaveCount(11);
|
||||
await expect(n8n.logs.getInputTbodyCell(0, 0)).toContainText('0');
|
||||
await expect(n8n.logs.getInputTbodyCell(9, 0)).toContainText('9');
|
||||
await n8n.logs.clickOpenNdvAtRow(2);
|
||||
await n8n.ndv.switchInputMode('Table');
|
||||
await expect(n8n.ndv.getInputSelect()).toHaveValue(`${NODES.CODE} `);
|
||||
await expect(n8n.ndv.getInputTableRows()).toHaveCount(11);
|
||||
await expect(n8n.ndv.getInputTbodyCell(0, 0)).toContainText('0');
|
||||
await expect(n8n.ndv.getInputTbodyCell(9, 0)).toContainText('9');
|
||||
await expect(n8n.ndv.getOutputRunSelectorInput()).toHaveValue('1 of 3 (10 items)');
|
||||
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
|
||||
await n8n.logs.clickLogEntryAtRow(4); // Run #2 of 'Edit Fields' node; input is false branch of 'If' node
|
||||
await expect(n8n.logs.getInputTableRows()).toHaveCount(6);
|
||||
await expect(n8n.logs.getInputTbodyCell(0, 0)).toContainText('5');
|
||||
await expect(n8n.logs.getInputTbodyCell(4, 0)).toContainText('9');
|
||||
await n8n.logs.clickOpenNdvAtRow(4);
|
||||
await expect(n8n.ndv.getInputSelect()).toHaveValue(`${NODES.IF} `);
|
||||
await expect(n8n.ndv.getInputTableRows()).toHaveCount(6);
|
||||
await expect(n8n.ndv.getInputTbodyCell(0, 0)).toContainText('5');
|
||||
await expect(n8n.ndv.getInputTbodyCell(4, 0)).toContainText('9');
|
||||
await expect(n8n.ndv.getOutputRunSelectorInput()).toHaveValue('2 of 3 (5 items)');
|
||||
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
|
||||
await n8n.logs.clickLogEntryAtRow(5); // Run #3 of 'Edit Fields' node; input is true branch of 'If' node
|
||||
await expect(n8n.logs.getInputTableRows()).toHaveCount(6);
|
||||
await expect(n8n.logs.getInputTbodyCell(0, 0)).toContainText('0');
|
||||
await expect(n8n.logs.getInputTbodyCell(4, 0)).toContainText('4');
|
||||
await n8n.logs.clickOpenNdvAtRow(5);
|
||||
await expect(n8n.ndv.getInputSelect()).toHaveValue(`${NODES.IF} `);
|
||||
await expect(n8n.ndv.getInputTableRows()).toHaveCount(6);
|
||||
await expect(n8n.ndv.getInputTbodyCell(0, 0)).toContainText('0');
|
||||
await expect(n8n.ndv.getInputTbodyCell(4, 0)).toContainText('4');
|
||||
await expect(n8n.ndv.getOutputRunSelectorInput()).toHaveValue('3 of 3 (5 items)');
|
||||
});
|
||||
|
||||
test('should keep populated logs unchanged when workflow get edits after the execution', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements({ workflow: 'Workflow_if.json' });
|
||||
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
await n8n.logs.openLogsPanel();
|
||||
|
||||
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Successful');
|
||||
await expect(n8n.logs.getLogEntries()).toHaveCount(6);
|
||||
await n8n.canvas.nodeDisableButton(NODES.EDIT_FIELDS).click();
|
||||
await expect(n8n.logs.getLogEntries()).toHaveCount(6);
|
||||
await n8n.canvas.deleteNodeByName(NODES.IF);
|
||||
await expect(n8n.logs.getLogEntries()).toHaveCount(6);
|
||||
});
|
||||
|
||||
// TODO: make it possible to test workflows with AI model end-to-end
|
||||
test.skip('should show logs for a past execution', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements({ workflow: 'Workflow_ai_agent.json' });
|
||||
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
await n8n.logs.openLogsPanel();
|
||||
|
||||
await n8n.canvas.sendManualChatMessage('Hi!');
|
||||
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Successful');
|
||||
await n8n.canvas.openExecutions();
|
||||
await n8n.executions.getAutoRefreshButton().click();
|
||||
await expect(n8n.executions.getManualChatMessages().nth(0)).toContainText('Hi!');
|
||||
await expect(n8n.executions.getManualChatMessages().nth(1)).toContainText(
|
||||
'Hello from e2e model!!!',
|
||||
);
|
||||
await expect(
|
||||
n8n.executions.getLogsOverviewStatus().filter({ hasText: /Success in [\d.]+m?s/ }),
|
||||
).toBeVisible();
|
||||
await expect(n8n.executions.getLogEntries()).toHaveCount(3);
|
||||
await expect(n8n.executions.getLogEntries().nth(0)).toContainText('When chat message received');
|
||||
await expect(n8n.executions.getLogEntries().nth(1)).toContainText('AI Agent');
|
||||
await expect(n8n.executions.getLogEntries().nth(2)).toContainText('E2E Chat Model');
|
||||
});
|
||||
|
||||
test('should show logs for a workflow with a node that waits for webhook', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements({ workflow: 'Workflow_wait_for_webhook.json' });
|
||||
|
||||
await n8n.canvas.canvasBody().click({ position: { x: 0, y: 0 } }); // click logs panel to deselect nodes in canvas
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
await n8n.logs.openLogsPanel();
|
||||
|
||||
await n8n.canvas.clickExecuteWorkflowButton();
|
||||
|
||||
await expect(n8n.canvas.getNodesWithSpinner()).toContainText(NODES.WAIT_NODE);
|
||||
await expect(n8n.canvas.getWaitingNodes()).toContainText(NODES.WAIT_NODE);
|
||||
await expect(n8n.logs.getLogEntries()).toHaveCount(2);
|
||||
await expect(n8n.logs.getLogEntries().nth(1)).toContainText(NODES.WAIT_NODE);
|
||||
await expect(n8n.logs.getLogEntries().nth(1)).toContainText('Waiting');
|
||||
|
||||
await n8n.canvas.openNode(NODES.WAIT_NODE);
|
||||
const webhookUrl = await n8n.ndv.getOutputDataContainer().locator('a').getAttribute('href');
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
|
||||
// Trigger the webhook
|
||||
const response = await n8n.page.request.get(webhookUrl!);
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
await expect(n8n.canvas.getNodesWithSpinner()).not.toBeVisible();
|
||||
await expect(n8n.canvas.getWaitingNodes()).not.toBeVisible();
|
||||
await expect(
|
||||
n8n.logs.getOverviewStatus().filter({ hasText: /Success in [\d.]+m?s/ }),
|
||||
).toBeVisible();
|
||||
await n8n.logs.getLogEntries().nth(1).click(); // click selected row to deselect
|
||||
await expect(n8n.logs.getLogEntries()).toHaveCount(2);
|
||||
await expect(n8n.logs.getLogEntries().nth(1)).toContainText(NODES.WAIT_NODE);
|
||||
await expect(n8n.logs.getLogEntries().nth(1)).toContainText('Success');
|
||||
});
|
||||
});
|
||||
@@ -42,7 +42,7 @@ export async function setupTestRequirements(
|
||||
|
||||
// 3. Setup API intercepts
|
||||
if (requirements.intercepts) {
|
||||
for (const [name, config] of Object.entries(requirements.intercepts)) {
|
||||
for (const config of Object.values(requirements.intercepts)) {
|
||||
await page.route(config.url, async (route) => {
|
||||
await route.fulfill({
|
||||
status: config.status ?? 200,
|
||||
@@ -56,7 +56,12 @@ export async function setupTestRequirements(
|
||||
|
||||
// 4. Setup workflows
|
||||
if (requirements.workflow) {
|
||||
for (const [name, workflowData] of Object.entries(requirements.workflow)) {
|
||||
const entries =
|
||||
typeof requirements.workflow === 'string'
|
||||
? [[requirements.workflow, requirements.workflow]]
|
||||
: Object.entries(requirements.workflow);
|
||||
|
||||
for (const [name, workflowData] of entries) {
|
||||
try {
|
||||
// Import workflow using the n8n page object
|
||||
await n8n.goHome();
|
||||
|
||||
66
packages/testing/playwright/workflows/Workflow_ai_agent.json
Normal file
66
packages/testing/playwright/workflows/Workflow_ai_agent.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "5eb6b347-b34e-4112-9601-f7aa94f26575",
|
||||
"name": "When chat message received",
|
||||
"type": "@n8n/n8n-nodes-langchain.chatTrigger",
|
||||
"typeVersion": 1.1,
|
||||
"position": [0, 0],
|
||||
"webhookId": "4fb58136-3481-494a-a30f-d9e064dac186"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 1.9,
|
||||
"position": [220, 0],
|
||||
"id": "32534841-9474-4890-9998-65d6a56bdf0c",
|
||||
"name": "AI Agent"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"response": "Hello from **e2e** model!!!"
|
||||
},
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatE2eTest",
|
||||
"typeVersion": 1,
|
||||
"position": [308, 220],
|
||||
"id": "2f239d5b-95ef-4949-92b6-5a7541e1029f",
|
||||
"name": "E2E Chat Model"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When chat message received": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "AI Agent",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"AI Agent": {
|
||||
"main": [[]]
|
||||
},
|
||||
"E2E Chat Model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "AI Agent",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {},
|
||||
"meta": {
|
||||
"instanceId": "14f5bd03485879885c6d92999d35d4d24556536fa2b675f932eb27193691e2b2"
|
||||
}
|
||||
}
|
||||
149
packages/testing/playwright/workflows/Workflow_if.json
Normal file
149
packages/testing/playwright/workflows/Workflow_if.json
Normal file
@@ -0,0 +1,149 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"rule": {
|
||||
"interval": [{}]
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.scheduleTrigger",
|
||||
"typeVersion": 1.2,
|
||||
"position": [0, 0],
|
||||
"id": "4c4f02e3-f0cc-4e1a-b084-9888dd75ccaf",
|
||||
"name": "Schedule Trigger"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "553f50d9-5023-433f-8f62-eebc9c9e2269",
|
||||
"leftValue": "={{ $json.data }}",
|
||||
"rightValue": 5,
|
||||
"operator": {
|
||||
"type": "number",
|
||||
"operation": "lt"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"position": [660, 80],
|
||||
"id": "3273dcdb-845c-43cc-8ed0-ffaef7b68b1c",
|
||||
"name": "If"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "71475f04-571e-4e99-bdf8-adff367533fb",
|
||||
"name": "data",
|
||||
"value": "={{ $json.data }}",
|
||||
"type": "number"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [880, 0],
|
||||
"id": "0522012f-fe99-41e0-ba19-7e18f22758d9",
|
||||
"name": "Edit Fields"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "return Array.from({length:10}).map((_,i)=>({data:i}))"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [440, 0],
|
||||
"id": "268c2da4-23c8-46c5-8992-37a92b8e4aad",
|
||||
"name": "Code"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [220, 0],
|
||||
"id": "0e2d1621-607d-4d53-98a3-07afc0d6230d",
|
||||
"name": "Edit Fields1",
|
||||
"disabled": true
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Schedule Trigger": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"If": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "If",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Edit Fields1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {},
|
||||
"meta": {
|
||||
"instanceId": "ddb4b2493e5f30d4af3bb43dd3c9acf1f60cbaa91c89e84d2967725474533c77"
|
||||
}
|
||||
}
|
||||
110
packages/testing/playwright/workflows/Workflow_loop.json
Normal file
110
packages/testing/playwright/workflows/Workflow_loop.json
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, -10],
|
||||
"id": "cc68bd44-e150-403c-afaf-bd0bac0459dd",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "return [{data:1},{data:2},{data:3}]"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [220, -10],
|
||||
"id": "461b130c-0efb-4201-8210-d5e794e88ed8",
|
||||
"name": "Code"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.splitInBatches",
|
||||
"typeVersion": 3,
|
||||
"position": [440, -10],
|
||||
"id": "16860d82-1b2c-4882-ad76-43cc2042d695",
|
||||
"name": "Loop Over Items"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"amount": 2
|
||||
},
|
||||
"type": "n8n-nodes-base.wait",
|
||||
"typeVersion": 1.1,
|
||||
"position": [660, 40],
|
||||
"id": "9ede6c97-a3c5-4f42-adcc-dfe66fc7a2a8",
|
||||
"name": "Wait",
|
||||
"webhookId": "36d32e2d-a9cf-4dc7-b138-70d7966c96d7"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "throw Error('test!!!')"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [660, -160],
|
||||
"id": "05ad4f4d-10fc-4bec-9145-0e5c20f455c4",
|
||||
"name": "Code1"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Loop Over Items",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Loop Over Items": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Wait",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Wait": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Loop Over Items",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {},
|
||||
"meta": {
|
||||
"instanceId": "74f18f142a6ddf6880a6f8c5b30685f621743a7a66d8d94c8f164937f4dd5515"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "42c6e003-10e7-4100-aff8-8865c49f384c",
|
||||
"name": "When clicking ‘Test workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resume": "webhook",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.wait",
|
||||
"typeVersion": 1.1,
|
||||
"position": [220, 0],
|
||||
"id": "77614c15-c41e-4b8c-95d6-084d48fed328",
|
||||
"name": "Wait node",
|
||||
"webhookId": "62aad98c-81b3-4c44-9adb-b33a23d1271d"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking ‘Test workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Wait node",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {},
|
||||
"meta": {
|
||||
"instanceId": "eea4a7b09aa7ee308bc067003a65466862f88be8d9309a2bb16297f6bb2616ec"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user