mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-01 15:47:41 +08:00
test: Migrate workflow executions tests from cypress to playwright (#20363)
This commit is contained in:
@@ -1,315 +0,0 @@
|
||||
import type { RouteHandler } from 'cypress/types/net-stubbing';
|
||||
|
||||
import executionOutOfMemoryServerResponse from '../../fixtures/responses/execution-out-of-memory-server-response.json';
|
||||
import { WorkflowPage } from '../../pages';
|
||||
import { WorkflowExecutionsTab } from '../../pages/workflow-executions-tab';
|
||||
import { getVisibleSelect } from '../../utils';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const executionsTab = new WorkflowExecutionsTab();
|
||||
const executionsRefreshInterval = 4000;
|
||||
|
||||
// Test suite for executions tab
|
||||
describe('Workflow Executions', () => {
|
||||
describe('when workflow is saved', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json');
|
||||
});
|
||||
|
||||
it('should render executions tab correctly', () => {
|
||||
createMockExecutions();
|
||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
|
||||
cy.wait(['@getExecutions']);
|
||||
|
||||
executionsTab.getters.executionsList().scrollTo(0, 500).wait(0);
|
||||
|
||||
executionsTab.getters.executionListItems().should('have.length', 30);
|
||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 28);
|
||||
executionsTab.getters.failedExecutionListItems().should('have.length', 2);
|
||||
executionsTab.getters
|
||||
.executionListItems()
|
||||
.first()
|
||||
.invoke('attr', 'class')
|
||||
.should('match', /_active_/);
|
||||
});
|
||||
|
||||
it('should not redirect back to execution tab when request is not done before leaving the page', () => {
|
||||
cy.intercept('GET', '/rest/executions?filter=*');
|
||||
cy.intercept('GET', '/rest/executions/active?filter=*');
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
cy.wait(executionsRefreshInterval);
|
||||
cy.url().should('not.include', '/executions');
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
cy.wait(executionsRefreshInterval);
|
||||
cy.url().should('not.include', '/executions');
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
cy.wait(1000);
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
cy.wait(executionsRefreshInterval);
|
||||
cy.url().should('not.include', '/executions');
|
||||
});
|
||||
|
||||
it('should not redirect back to execution tab when slow request is not done before leaving the page', () => {
|
||||
const throttleResponse: RouteHandler = async (req) => {
|
||||
return await new Promise((resolve) => {
|
||||
setTimeout(() => resolve(req.continue()), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
cy.intercept('GET', '/rest/executions?filter=*', throttleResponse);
|
||||
cy.intercept('GET', '/rest/executions/active?filter=*', throttleResponse);
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
cy.wait(executionsRefreshInterval);
|
||||
cy.url().should('not.include', '/executions');
|
||||
});
|
||||
|
||||
it('should error toast when server error message returned without stack trace', () => {
|
||||
executionsTab.actions.createManualExecutions(1);
|
||||
const message = 'Workflow did not finish, possible out-of-memory issue';
|
||||
cy.intercept('GET', '/rest/executions/*', {
|
||||
statusCode: 200,
|
||||
body: executionOutOfMemoryServerResponse,
|
||||
}).as('getExecution');
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
cy.wait(['@getExecution']);
|
||||
|
||||
executionsTab.getters
|
||||
.workflowExecutionPreviewIframe()
|
||||
.should('be.visible')
|
||||
.its('0.contentDocument.body') // Access the body of the iframe document
|
||||
.should('not.be.empty') // Ensure the body is not empty
|
||||
|
||||
.then(cy.wrap)
|
||||
.find('.el-notification:has(.el-notification--error)')
|
||||
.should('be.visible')
|
||||
.filter(`:contains("${message}")`)
|
||||
.should('be.visible');
|
||||
});
|
||||
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||
it.skip('should show workflow data in executions tab after hard reload and modify name and tags', () => {
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
|
||||
workflowPage.getters.workflowTags().click();
|
||||
getVisibleSelect().find('li:contains("Manage tags")').click();
|
||||
cy.get('button:contains("Add new")').click();
|
||||
cy.getByTestId('tags-table').find('input').type('nutag').type('{enter}');
|
||||
cy.get('button:contains("Done")').click();
|
||||
|
||||
cy.reload();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.workflowTags().click();
|
||||
workflowPage.getters.tagsInDropdown().first().should('have.text', 'nutag').click();
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
|
||||
let newWorkflowName = 'Renamed workflow';
|
||||
workflowPage.actions.renameWorkflow(newWorkflowName);
|
||||
workflowPage.getters.isWorkflowSaved();
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
newWorkflowName = 'New workflow';
|
||||
workflowPage.actions.renameWorkflow(newWorkflowName);
|
||||
workflowPage.getters.isWorkflowSaved();
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
workflowPage.getters.workflowTags().click();
|
||||
workflowPage.getters.tagsDropdown().find('.el-tag__close').first().click();
|
||||
cy.get('body').click(0, 0);
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
});
|
||||
// This should be a component test. Abstracting this away into to ensure our lists work.
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||
it.skip('should load items and auto scroll after filter change', () => {
|
||||
createMockExecutions();
|
||||
createMockExecutions();
|
||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
|
||||
cy.wait(['@getExecutions']);
|
||||
|
||||
executionsTab.getters.executionsList().scrollTo(0, 500).wait(0);
|
||||
|
||||
executionsTab.getters.executionListItems().eq(10).click();
|
||||
|
||||
cy.getByTestId('executions-filter-button').click();
|
||||
cy.getByTestId('executions-filter-status-select').should('be.visible').click();
|
||||
getVisibleSelect().find('li:contains("Error")').click();
|
||||
|
||||
executionsTab.getters.executionListItems().should('have.length', 5);
|
||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 1);
|
||||
executionsTab.getters.failedExecutionListItems().should('have.length', 4);
|
||||
|
||||
cy.getByTestId('executions-filter-button').click();
|
||||
cy.getByTestId('executions-filter-status-select').should('be.visible').click();
|
||||
getVisibleSelect().find('li:contains("Success")').click();
|
||||
|
||||
// check if the list is scrolled
|
||||
executionsTab.getters.executionListItems().eq(10).should('be.visible');
|
||||
executionsTab.getters.executionsList().then(($el) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = $el[0];
|
||||
expect(scrollTop).to.be.greaterThan(0);
|
||||
expect(scrollTop + clientHeight).to.be.lessThan(scrollHeight);
|
||||
|
||||
// scroll to the bottom
|
||||
$el[0].scrollTo(0, scrollHeight);
|
||||
executionsTab.getters.executionListItems().should('have.length', 18);
|
||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 18);
|
||||
executionsTab.getters.failedExecutionListItems().should('have.length', 0);
|
||||
});
|
||||
|
||||
cy.getByTestId('executions-filter-button').click();
|
||||
cy.getByTestId('executions-filter-reset-button').should('be.visible').click();
|
||||
executionsTab.getters.executionListItems().eq(11).should('be.visible');
|
||||
});
|
||||
|
||||
it('should redirect back to editor after seeing a couple of execution using browser back button', () => {
|
||||
createMockExecutions();
|
||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
|
||||
cy.wait(['@getExecutions']);
|
||||
executionsTab.getters.workflowExecutionPreviewIframe().should('exist');
|
||||
|
||||
executionsTab.getters.executionListItems().eq(2).click();
|
||||
executionsTab.getters.workflowExecutionPreviewIframe().should('exist');
|
||||
executionsTab.getters.executionListItems().eq(4).click();
|
||||
executionsTab.getters.workflowExecutionPreviewIframe().should('exist');
|
||||
executionsTab.getters.executionListItems().eq(6).click();
|
||||
executionsTab.getters.workflowExecutionPreviewIframe().should('exist');
|
||||
|
||||
cy.go('back');
|
||||
executionsTab.getters.workflowExecutionPreviewIframe().should('exist');
|
||||
cy.go('back');
|
||||
executionsTab.getters.workflowExecutionPreviewIframe().should('exist');
|
||||
cy.go('back');
|
||||
executionsTab.getters.workflowExecutionPreviewIframe().should('exist');
|
||||
cy.go('back');
|
||||
|
||||
cy.url().should('not.include', '/executions');
|
||||
cy.url().should('include', '/workflow/');
|
||||
workflowPage.getters.nodeViewRoot().should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when new workflow is not saved', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
});
|
||||
|
||||
it('should open executions tab', () => {
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.getters.executionsSidebar().should('be.visible');
|
||||
executionsTab.getters.executionsEmptyList().should('be.visible');
|
||||
cy.getByTestId('workflow-execution-no-trigger-content').should('be.visible');
|
||||
cy.get('button:contains("Add first step")').should('be.visible').click();
|
||||
|
||||
cy.getByTestId('node-creator-item-name')
|
||||
.should('be.visible')
|
||||
.filter(':contains("Trigger manually")')
|
||||
.click();
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.getters.executionsSidebar().should('be.visible');
|
||||
executionsTab.getters.executionsEmptyList().should('be.visible');
|
||||
cy.getByTestId('workflow-execution-no-content').should('be.visible');
|
||||
|
||||
workflowPage.getters.saveButton().find('button').should('be.enabled').click();
|
||||
workflowPage.getters.isWorkflowSaved();
|
||||
workflowPage.getters.nodeViewRoot().should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const createMockExecutions = () => {
|
||||
executionsTab.actions.createManualExecutions(15);
|
||||
// This wait is added to allow time for the notifications to expire
|
||||
cy.wait(2000);
|
||||
// Make some failed executions by enabling Code node with syntax error
|
||||
executionsTab.actions.toggleNodeEnabled('Error');
|
||||
workflowPage.getters.disabledNodes().should('have.length', 0);
|
||||
executionsTab.actions.createManualExecutions(2);
|
||||
// This wait is added to allow time for the notifications to expire
|
||||
cy.wait(2000);
|
||||
|
||||
// Then add some more successful ones
|
||||
executionsTab.actions.toggleNodeEnabled('Error');
|
||||
workflowPage.getters.disabledNodes().should('have.length', 1);
|
||||
executionsTab.actions.createManualExecutions(15);
|
||||
};
|
||||
|
||||
const checkMainHeaderELements = () => {
|
||||
workflowPage.getters.workflowNameInputContainer().should('be.visible');
|
||||
workflowPage.getters.workflowTagsContainer().should('be.visible');
|
||||
workflowPage.getters.workflowMenu().should('be.visible');
|
||||
workflowPage.getters.saveButton().should('be.visible');
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { n8nPage } from '../pages/n8nPage';
|
||||
|
||||
/**
|
||||
* A class for user interactions with workflow executions that go across multiple pages.
|
||||
*/
|
||||
export class ExecutionsComposer {
|
||||
constructor(private readonly n8n: n8nPage) {}
|
||||
|
||||
/**
|
||||
* Creates workflow executions by executing the workflow multiple times.
|
||||
* Waits for each execution to complete (by waiting for the POST /rest/workflows/:id/run response)
|
||||
* before starting the next one.
|
||||
*
|
||||
* @param count - Number of executions to create
|
||||
* @example
|
||||
* // Create 10 executions
|
||||
* await n8n.executionsComposer.createExecutions(10);
|
||||
*/
|
||||
async createExecutions(count: number): Promise<void> {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const responsePromise = this.n8n.page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/rest/workflows/') &&
|
||||
response.url().includes('/run') &&
|
||||
response.request().method() === 'POST',
|
||||
);
|
||||
|
||||
await this.n8n.canvas.clickExecuteWorkflowButton();
|
||||
await responsePromise;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -660,6 +660,15 @@ export class CanvasPage extends BasePage {
|
||||
.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle node enabled/disabled state using keyboard shortcut
|
||||
* @param nodeName - The name of the node to toggle
|
||||
*/
|
||||
async toggleNodeEnabled(nodeName: string): Promise<void> {
|
||||
await this.nodeByName(nodeName).click();
|
||||
await this.page.keyboard.press('d');
|
||||
}
|
||||
|
||||
// Chat open/close buttons (manual chat)
|
||||
async clickManualChatButton(): Promise<void> {
|
||||
await this.page.getByTestId('workflow-chat-button').click();
|
||||
|
||||
@@ -43,4 +43,42 @@ export class ExecutionsPage extends BasePage {
|
||||
async handlePinnedNodesConfirmation(action: 'Unpin' | 'Cancel'): Promise<void> {
|
||||
await this.page.getByRole('button', { name: action }).click();
|
||||
}
|
||||
|
||||
getExecutionsList(): Locator {
|
||||
return this.page.getByTestId('current-executions-list');
|
||||
}
|
||||
|
||||
getExecutionsSidebar(): Locator {
|
||||
return this.page.getByTestId('executions-sidebar');
|
||||
}
|
||||
|
||||
getExecutionsEmptyList(): Locator {
|
||||
return this.page.getByTestId('execution-list-empty');
|
||||
}
|
||||
|
||||
getSuccessfulExecutionItems(): Locator {
|
||||
return this.page.locator('[data-test-execution-status="success"]');
|
||||
}
|
||||
|
||||
getFailedExecutionItems(): Locator {
|
||||
return this.page.locator('[data-test-execution-status="error"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the executions list to the bottom to trigger lazy loading
|
||||
*/
|
||||
async scrollExecutionsListToBottom(): Promise<void> {
|
||||
await this.getExecutionsList().evaluate((el) => el.scrollTo(0, el.scrollHeight));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error notifications in the preview iframe
|
||||
*/
|
||||
getErrorNotificationsInPreview(): Locator {
|
||||
return this.getPreviewIframe().locator('.el-notification:has(.el-notification--error)');
|
||||
}
|
||||
|
||||
getFirstExecutionItem(): Locator {
|
||||
return this.getExecutionItems().first();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import { WorkflowSharingModal } from './WorkflowSharingModal';
|
||||
import { WorkflowsPage } from './WorkflowsPage';
|
||||
import { CanvasComposer } from '../composables/CanvasComposer';
|
||||
import { CredentialsComposer } from '../composables/CredentialsComposer';
|
||||
import { ExecutionsComposer } from '../composables/ExecutionsComposer';
|
||||
import { MfaComposer } from '../composables/MfaComposer';
|
||||
import { NodeDetailsViewComposer } from '../composables/NodeDetailsViewComposer';
|
||||
import { PartialExecutionComposer } from '../composables/PartialExecutionComposer';
|
||||
@@ -88,6 +89,7 @@ export class n8nPage {
|
||||
readonly projectComposer: ProjectComposer;
|
||||
readonly canvasComposer: CanvasComposer;
|
||||
readonly credentialsComposer: CredentialsComposer;
|
||||
readonly executionsComposer: ExecutionsComposer;
|
||||
readonly mfaComposer: MfaComposer;
|
||||
readonly partialExecutionComposer: PartialExecutionComposer;
|
||||
readonly ndvComposer: NodeDetailsViewComposer;
|
||||
@@ -141,6 +143,7 @@ export class n8nPage {
|
||||
this.projectComposer = new ProjectComposer(this);
|
||||
this.canvasComposer = new CanvasComposer(this);
|
||||
this.credentialsComposer = new CredentialsComposer(this);
|
||||
this.executionsComposer = new ExecutionsComposer(this);
|
||||
this.mfaComposer = new MfaComposer(this);
|
||||
this.partialExecutionComposer = new PartialExecutionComposer(this);
|
||||
this.ndvComposer = new NodeDetailsViewComposer(this);
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
import executionOutOfMemoryResponse from '../../fixtures/execution-out-of-memory-server-response.json';
|
||||
|
||||
const ERROR_MESSAGES = {
|
||||
OUT_OF_MEMORY: 'Workflow did not finish, possible out-of-memory issue',
|
||||
} as const;
|
||||
|
||||
const TIMEOUTS = {
|
||||
EXECUTIONS_REFRESH_INTERVAL: 4000,
|
||||
} as const;
|
||||
|
||||
test.describe('Workflow Executions', () => {
|
||||
test.describe('when workflow is saved', () => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.start.fromImportedWorkflow('Test_workflow_4_executions_view.json');
|
||||
});
|
||||
|
||||
test('should render executions tab correctly', async ({ n8n }) => {
|
||||
await n8n.executionsComposer.createExecutions(15);
|
||||
|
||||
await n8n.canvas.toggleNodeEnabled('Error');
|
||||
await expect(n8n.canvas.disabledNodes()).toHaveCount(0);
|
||||
await n8n.executionsComposer.createExecutions(2);
|
||||
|
||||
await n8n.canvas.toggleNodeEnabled('Error');
|
||||
await expect(n8n.canvas.disabledNodes()).toHaveCount(1);
|
||||
await n8n.executionsComposer.createExecutions(15);
|
||||
|
||||
const executionsResponsePromise = n8n.page.waitForResponse((response) =>
|
||||
response.url().includes('/rest/executions?filter='),
|
||||
);
|
||||
|
||||
await n8n.canvas.clickExecutionsTab();
|
||||
await executionsResponsePromise;
|
||||
|
||||
await expect(n8n.executions.getExecutionItems().first()).toBeVisible();
|
||||
|
||||
const loadMoreResponsePromise = n8n.page.waitForResponse((response) =>
|
||||
response.url().includes('/rest/executions?filter='),
|
||||
);
|
||||
await n8n.executions.scrollExecutionsListToBottom();
|
||||
await loadMoreResponsePromise;
|
||||
|
||||
await expect(n8n.executions.getExecutionItems()).toHaveCount(30, { timeout: 15000 });
|
||||
await expect(n8n.executions.getSuccessfulExecutionItems()).toHaveCount(28);
|
||||
await expect(n8n.executions.getFailedExecutionItems()).toHaveCount(2);
|
||||
await expect(n8n.executions.getFirstExecutionItem()).toHaveClass(/_active_/);
|
||||
});
|
||||
|
||||
test('should not redirect back to execution tab when request is not done before leaving the page', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.page.route('**/rest/executions?filter=*', (route) => route.continue());
|
||||
await n8n.page.route('**/rest/executions/active?filter=*', (route) => route.continue());
|
||||
|
||||
await n8n.canvas.clickExecutionsTab();
|
||||
await n8n.canvas.clickEditorTab();
|
||||
await expect(n8n.page).toHaveURL(/\/workflow\/[^/]+$/, {
|
||||
timeout: TIMEOUTS.EXECUTIONS_REFRESH_INTERVAL,
|
||||
});
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await n8n.canvas.clickExecutionsTab();
|
||||
await n8n.canvas.clickEditorTab();
|
||||
}
|
||||
await expect(n8n.page).toHaveURL(/\/workflow\/[^/]+$/, {
|
||||
timeout: TIMEOUTS.EXECUTIONS_REFRESH_INTERVAL,
|
||||
});
|
||||
|
||||
await n8n.canvas.clickExecutionsTab();
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await n8n.page.waitForTimeout(1000);
|
||||
await n8n.canvas.clickEditorTab();
|
||||
await expect(n8n.page).toHaveURL(/\/workflow\/[^/]+$/, {
|
||||
timeout: TIMEOUTS.EXECUTIONS_REFRESH_INTERVAL,
|
||||
});
|
||||
});
|
||||
|
||||
test('should not redirect back to execution tab when slow request is not done before leaving the page', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.page.route('**/rest/executions?filter=*', async (route) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
await route.continue();
|
||||
});
|
||||
|
||||
await n8n.page.route('**/rest/executions/active?filter=*', async (route) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
await route.continue();
|
||||
});
|
||||
|
||||
await n8n.canvas.clickExecutionsTab();
|
||||
await n8n.page.waitForURL(/\/executions/);
|
||||
await n8n.canvas.clickEditorTab();
|
||||
await n8n.page.waitForURL(/\/workflow\/[^/]+$/);
|
||||
|
||||
await expect(n8n.page).toHaveURL(/\/workflow\/[^/]+$/, {
|
||||
timeout: TIMEOUTS.EXECUTIONS_REFRESH_INTERVAL,
|
||||
});
|
||||
});
|
||||
|
||||
test('should error toast when server error message returned without stack trace', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
const responsePromise = n8n.page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/rest/workflows/') &&
|
||||
response.url().includes('/run') &&
|
||||
response.request().method() === 'POST',
|
||||
);
|
||||
await n8n.canvas.clickExecuteWorkflowButton();
|
||||
await responsePromise;
|
||||
|
||||
await n8n.page.route('**/rest/executions/*', async (route) => {
|
||||
if (route.request().url().includes('?filter=')) {
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(executionOutOfMemoryResponse),
|
||||
});
|
||||
});
|
||||
|
||||
const executionDetailPromise = n8n.page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/rest/executions/') && !response.url().includes('?filter='),
|
||||
);
|
||||
await n8n.canvas.clickExecutionsTab();
|
||||
await executionDetailPromise;
|
||||
|
||||
const iframe = n8n.executions.getPreviewIframe();
|
||||
await expect(iframe.locator('body')).not.toBeEmpty();
|
||||
|
||||
await n8n.executions.getErrorNotificationsInPreview().first().waitFor({ timeout: 5000 });
|
||||
|
||||
const errorNotification = n8n.executions
|
||||
.getErrorNotificationsInPreview()
|
||||
.filter({ hasText: ERROR_MESSAGES.OUT_OF_MEMORY });
|
||||
await expect(errorNotification).toBeVisible();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip('should show workflow data in executions tab after hard reload and modify name and tags', async () => {
|
||||
// TODO: Migrate from Cypress
|
||||
});
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip('should load items and auto scroll after filter change', async () => {
|
||||
// TODO: This should be a component test
|
||||
});
|
||||
|
||||
test('should redirect back to editor after seeing a couple of execution using browser back button', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.executionsComposer.createExecutions(15);
|
||||
|
||||
await n8n.canvas.toggleNodeEnabled('Error');
|
||||
await expect(n8n.canvas.disabledNodes()).toHaveCount(0);
|
||||
await n8n.executionsComposer.createExecutions(2);
|
||||
|
||||
await n8n.canvas.toggleNodeEnabled('Error');
|
||||
await expect(n8n.canvas.disabledNodes()).toHaveCount(1);
|
||||
await n8n.executionsComposer.createExecutions(15);
|
||||
|
||||
const executionsResponsePromise = n8n.page.waitForResponse((response) =>
|
||||
response.url().includes('/rest/executions?filter='),
|
||||
);
|
||||
|
||||
await n8n.canvas.clickExecutionsTab();
|
||||
await executionsResponsePromise;
|
||||
|
||||
const iframe = n8n.executions.getPreviewIframe();
|
||||
await expect(iframe.locator('body')).toBeAttached();
|
||||
|
||||
await n8n.executions.getExecutionItems().nth(2).click();
|
||||
await expect(iframe.locator('body')).toBeAttached();
|
||||
|
||||
await n8n.executions.getExecutionItems().nth(4).click();
|
||||
await expect(iframe.locator('body')).toBeAttached();
|
||||
|
||||
await n8n.executions.getExecutionItems().nth(6).click();
|
||||
await expect(iframe.locator('body')).toBeAttached();
|
||||
|
||||
await n8n.page.goBack();
|
||||
await expect(iframe.locator('body')).toBeAttached();
|
||||
|
||||
await n8n.page.goBack();
|
||||
await expect(iframe.locator('body')).toBeAttached();
|
||||
|
||||
await n8n.page.goBack();
|
||||
await expect(iframe.locator('body')).toBeAttached();
|
||||
|
||||
await n8n.page.goBack();
|
||||
|
||||
await expect(n8n.page).not.toHaveURL(/\/executions/);
|
||||
await expect(n8n.page).toHaveURL(/\/workflow\//);
|
||||
await expect(n8n.canvas.canvasPane()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('when new workflow is not saved', () => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.start.fromBlankCanvas();
|
||||
});
|
||||
|
||||
test('should open executions tab', async ({ n8n }) => {
|
||||
await n8n.canvas.clickExecutionsTab();
|
||||
await expect(n8n.executions.getExecutionsSidebar()).toBeVisible();
|
||||
await expect(n8n.executions.getExecutionsEmptyList()).toBeVisible();
|
||||
await expect(n8n.page.getByTestId('workflow-execution-no-trigger-content')).toBeVisible();
|
||||
|
||||
await n8n.page.getByRole('button', { name: 'Add first step' }).click();
|
||||
await n8n.canvas.nodeCreatorItemByName('Trigger manually').click();
|
||||
|
||||
await n8n.canvas.clickExecutionsTab();
|
||||
await expect(n8n.executions.getExecutionsSidebar()).toBeVisible();
|
||||
await expect(n8n.executions.getExecutionsEmptyList()).toBeVisible();
|
||||
await expect(n8n.page.getByTestId('workflow-execution-no-content')).toBeVisible();
|
||||
|
||||
await expect(n8n.canvas.saveWorkflowButton()).toBeEnabled();
|
||||
await n8n.canvas.saveWorkflowButton().click();
|
||||
await n8n.page.waitForURL(/\/workflow\/[^/]+$/);
|
||||
await expect(n8n.canvas.canvasPane()).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"meta": {
|
||||
"instanceId": "6b85439d79c07750ea49eced4bc2a12b283cfcba0ab2917cd4f3fee36080e869"
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Loop over input items and add a new field\n// called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.myNewField = 1;\n error\n}\n\nreturn $input.all();"
|
||||
},
|
||||
"id": "d0ab7e12-0e1b-4c08-8081-83107794f37d",
|
||||
"name": "Error",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [680, 460],
|
||||
"disabled": true
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "f5026145-66c1-463c-8ac8-46a1309a6632",
|
||||
"name": "On clicking 'execute'",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [460, 460]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Loop over input items and add a new field\n// called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.myNewField = 1;\n}\n\nreturn $input.all();"
|
||||
},
|
||||
"id": "9926f884-348a-4af0-872e-dd7c8b3da811",
|
||||
"name": "Code",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [900, 460]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Error": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"On clicking 'execute'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Error",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "some-tag-1",
|
||||
"createdAt": "2022-11-10T13:43:34.001Z",
|
||||
"updatedAt": "2022-11-10T13:43:34.001Z",
|
||||
"id": "6"
|
||||
},
|
||||
{
|
||||
"name": "some-tag-2",
|
||||
"createdAt": "2022-11-10T13:43:39.778Z",
|
||||
"updatedAt": "2022-11-10T13:43:39.778Z",
|
||||
"id": "7"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user