first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { test, expect } from '../../../../fixtures/base';
|
||||
|
||||
test.describe(
|
||||
'Executions Filter',
|
||||
{
|
||||
annotation: [{ type: 'owner', description: 'Catalysts' }],
|
||||
},
|
||||
() => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.start.fromImportedWorkflow('Test_workflow_4_executions_view.json');
|
||||
|
||||
// Create mix of successful and failed executions
|
||||
await n8n.executionsComposer.createExecutions(2);
|
||||
await n8n.canvas.toggleNodeEnabled('Error');
|
||||
await expect(n8n.canvas.disabledNodes()).toHaveCount(0);
|
||||
await n8n.executionsComposer.createExecutions(2);
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
test('should filter executions by status and show filter badge', async ({ n8n }) => {
|
||||
await n8n.executions.openFilter();
|
||||
await expect(n8n.executions.getFilterForm()).toBeVisible();
|
||||
|
||||
// No badge before filtering
|
||||
await expect(n8n.executions.getFilterBadge()).toBeHidden();
|
||||
|
||||
const filterRequestPromise = n8n.page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/rest/executions?filter=') && response.url().includes('error'),
|
||||
);
|
||||
|
||||
await n8n.executions.selectFilterStatus('Error');
|
||||
await filterRequestPromise;
|
||||
|
||||
// Badge shows active filter count
|
||||
await expect(n8n.executions.getFilterBadge()).toBeVisible();
|
||||
|
||||
// Only failed executions visible
|
||||
await expect(n8n.executions.getFailedExecutionItems()).toHaveCount(2);
|
||||
await expect(n8n.executions.getSuccessfulExecutionItems()).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('should reset filter and remove badge', async ({ n8n }) => {
|
||||
await n8n.executions.openFilter();
|
||||
await n8n.executions.selectFilterStatus('Success');
|
||||
await expect(n8n.executions.getFilterBadge()).toBeVisible();
|
||||
|
||||
const resetRequestPromise = n8n.page.waitForResponse((response) =>
|
||||
response.url().includes('/rest/executions?filter='),
|
||||
);
|
||||
|
||||
await n8n.executions.resetFilter();
|
||||
await resetRequestPromise;
|
||||
|
||||
await expect(n8n.executions.getFilterBadge()).toBeHidden();
|
||||
await expect(n8n.executions.getExecutionItems()).toHaveCount(4);
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,286 @@
|
||||
import { test, expect } from '../../../../fixtures/base';
|
||||
import executionOutOfMemoryResponse from '../../../../fixtures/execution-out-of-memory-server-response.json';
|
||||
|
||||
test.describe(
|
||||
'Executions Filter',
|
||||
{
|
||||
annotation: [{ type: 'owner', description: 'Lifecycle & Governance' }],
|
||||
},
|
||||
() => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.start.fromImportedWorkflow('Test_workflow_4_executions_view.json');
|
||||
});
|
||||
|
||||
// There is flakiness in this test. When the dropdown is opened, at times it appears to automatically close even without user interaction.
|
||||
// Manual timeouts have been added to try to mitigate this, but it still happens.
|
||||
test.fixme(
|
||||
'should keep popover open when selecting from dropdown inside it',
|
||||
async ({ n8n }) => {
|
||||
// Regression test: Element Plus dropdowns are teleported to body, causing
|
||||
// Reka UI's DismissableLayer to detect clicks as "outside" and close the popover.
|
||||
// This test verifies the popover stays open during and after dropdown selection.
|
||||
|
||||
// Create some executions first
|
||||
await n8n.executionsComposer.createExecutions(2);
|
||||
|
||||
// Go to executions tab
|
||||
await n8n.canvas.clickExecutionsTab();
|
||||
await expect(n8n.executions.getExecutionItems().first()).toBeVisible();
|
||||
|
||||
// Open filter popover
|
||||
await n8n.executions.openFilter();
|
||||
const filterForm = n8n.executions.getFilterForm();
|
||||
await n8n.page.waitForTimeout(1500);
|
||||
await expect(filterForm).toBeVisible();
|
||||
|
||||
// Click to open the status dropdown
|
||||
await n8n.executions.getStatusSelect().click();
|
||||
// Verify popover is still open while dropdown is open
|
||||
await expect(filterForm).toBeVisible();
|
||||
|
||||
// Set up listener for the filtered executions request
|
||||
const filterRequestPromise = n8n.page.waitForRequest(
|
||||
(request) =>
|
||||
request.url().includes('/rest/executions?filter=') && request.url().includes('success'),
|
||||
);
|
||||
|
||||
await n8n.page.waitForTimeout(500);
|
||||
// Select an option from the dropdown
|
||||
await n8n.page.getByRole('option', { name: 'Success' }).click();
|
||||
|
||||
// Verify the filter request was sent to the backend (confirms selection worked)
|
||||
const filterRequest = await filterRequestPromise;
|
||||
expect(filterRequest.url()).toContain('status');
|
||||
expect(filterRequest.url()).toContain('success');
|
||||
|
||||
// KEY ASSERTION: Verify the popover did NOT close after selecting from dropdown
|
||||
await expect(filterForm).toBeVisible();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
test.fixme(
|
||||
'should show workflow data in executions tab after hard reload and modify name and tags',
|
||||
async () => {
|
||||
// TODO: Migrate from Cypress
|
||||
},
|
||||
);
|
||||
test.fixme('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 n8n.canvas.waitForSaveWorkflowCompleted();
|
||||
await n8n.page.waitForURL(/\/workflow\/.+\/executions$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user