feat(editor): Add Production checklist for active workflows (#17756)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
This commit is contained in:
Mutasem Aldmour
2025-08-06 11:15:10 +02:00
committed by GitHub
parent b6c7810844
commit 6046d24c74
46 changed files with 3443 additions and 246 deletions

View File

@@ -1,8 +1,14 @@
# Playwright E2E Test Guide
## Development setup
```bash
pnpm install-browsers:local # in playwright directory
pnpm build:docker # from root first to test against local changes
```
## Quick Start
```bash
pnpm test:all # Run all tests (fresh containers, pnpm build:local from root first to ensure local containers)
pnpm test:all # Run all tests (fresh containers, pnpm build:docker from root first to ensure local containers)
pnpm test:local # Starts a local server and runs the UI tests
N8N_BASE_URL=localhost:5068 pnpm test:local # Runs the UI tests against the instance running
```
@@ -21,6 +27,7 @@ pnpm test:chaos # Runs the chaos tests
# Development
pnpm test:all --grep "workflow" # Pattern match, can run across all test types UI/cli-workflow/performance
pnpm test:local --ui # To enable UI debugging and test running mode
```
## Test Tags

View File

@@ -0,0 +1,23 @@
import type { Page } from '@playwright/test';
export const getSuggestedActionsButton = (page: Page) => page.getByTestId('suggested-action-count');
export const getSuggestedActionItem = (page: Page, text?: string) => {
const items = page.getByTestId('suggested-action-item');
if (text) {
return items.getByText(text);
}
return items;
};
export const getSuggestedActionsPopover = (page: Page) =>
page.locator('[data-reka-popper-content-wrapper=""]').filter({ hasText: /./ });
export const getErrorActionItem = (page: Page) =>
getSuggestedActionItem(page, 'Set up error notifications');
export const getTimeSavedActionItem = (page: Page) =>
getSuggestedActionItem(page, 'Track time saved');
export const getEvaluationsActionItem = (page: Page) =>
getSuggestedActionItem(page, 'Test reliability of AI steps');
export const getIgnoreAllButton = (page: Page) => page.getByTestId('suggested-action-ignore-all');

View File

@@ -0,0 +1,14 @@
import { expect } from '@playwright/test';
import type { Page } from '@playwright/test';
export const getActivationModal = (page: Page) => page.getByTestId('activation-modal');
export const closeActivationModal = async (page: Page) => {
await expect(getActivationModal(page)).toBeVisible();
// click checkbox so it does not show again
await getActivationModal(page).getByText("Don't show again").click();
// confirm modal
await getActivationModal(page).getByRole('button', { name: 'Got it' }).click();
};

View File

@@ -0,0 +1,9 @@
import { expect } from '@playwright/test';
import type { Page } from '@playwright/test';
// Helper to open workflow settings modal
export const openWorkflowSettings = async (page: Page) => {
await page.getByTestId('workflow-menu').click();
await page.getByTestId('workflow-menu-item-settings').click();
await expect(page.getByTestId('workflow-settings-dialog')).toBeVisible();
};

View File

@@ -32,6 +32,10 @@ export class CanvasPage extends BasePage {
return this.nodeToolbar(nodeName).getByTestId('delete-node-button');
}
nodeDisableButton(nodeName: string): Locator {
return this.nodeToolbar(nodeName).getByTestId('disable-node-button');
}
async clickCanvasPlusButton(): Promise<void> {
await this.clickByTestId('canvas-plus-button');
}
@@ -58,6 +62,15 @@ export class CanvasPage extends BasePage {
await this.clickNodeCreatorItemName(text);
}
async addNodeAndCloseNDV(text: string, subItemText?: string): Promise<void> {
if (subItemText) {
await this.addNodeToCanvasWithSubItem(text, subItemText);
} else {
await this.addNode(text);
}
await this.page.keyboard.press('Escape');
}
async addNodeToCanvasWithSubItem(searchText: string, subItemText: string): Promise<void> {
await this.addNode(searchText);
await this.nodeCreatorSubItem(subItemText).click();
@@ -148,4 +161,13 @@ export class CanvasPage extends BasePage {
getWorkflowTags() {
return this.page.getByTestId('workflow-tags').locator('.el-tag');
}
async activateWorkflow() {
const responsePromise = this.page.waitForResponse(
(response) =>
response.url().includes('/rest/workflows/') && response.request().method() === 'PATCH',
);
await this.page.getByTestId('workflow-activate-switch').click();
await responsePromise;
}
}

View File

@@ -0,0 +1,187 @@
import {
getErrorActionItem,
getEvaluationsActionItem,
getIgnoreAllButton,
getSuggestedActionItem,
getSuggestedActionsButton,
getSuggestedActionsPopover,
getTimeSavedActionItem,
} from '../../composables/ProductionChecklist';
import { closeActivationModal } from '../../composables/WorkflowActivationModal';
import { openWorkflowSettings } from '../../composables/WorkflowSettingsModal';
import { test, expect } from '../../fixtures/base';
const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
test.describe('Workflow Production Checklist', () => {
test.beforeEach(async ({ n8n }) => {
await n8n.goHome();
await n8n.workflows.clickAddWorkflowButton();
});
test('should show suggested actions automatically when workflow is first activated', async ({
n8n,
}) => {
// Add a schedule trigger node (activatable)
await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME);
await n8n.canvas.saveWorkflow();
// Verify suggested actions button is not visible
await expect(getSuggestedActionsButton(n8n.page)).toBeHidden();
// Activate the workflow
await n8n.canvas.activateWorkflow();
// Activation Modal should be visible since it's first activation
await closeActivationModal(n8n.page);
// Verify suggested actions button and popover is visible
await expect(getSuggestedActionsButton(n8n.page)).toBeVisible();
await expect(getSuggestedActionsPopover(n8n.page)).toBeVisible();
await expect(getSuggestedActionItem(n8n.page)).toHaveCount(2);
await expect(getErrorActionItem(n8n.page)).toBeVisible();
await expect(getTimeSavedActionItem(n8n.page)).toBeVisible();
});
test('should display evaluations action when AI node exists and feature is enabled', async ({
n8n,
api,
}) => {
// Enable evaluations feature
await api.enableFeature('evaluation');
// Add schedule trigger and AI node
await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME);
await n8n.canvas.addNodeAndCloseNDV('OpenAI', 'Create an assistant');
await n8n.canvas.nodeDisableButton('Create an assistant').click();
await n8n.canvas.saveWorkflow();
await n8n.canvas.activateWorkflow();
await closeActivationModal(n8n.page);
// Suggested actions should be open
await expect(getSuggestedActionsPopover(n8n.page)).toBeVisible();
await expect(getSuggestedActionItem(n8n.page)).toHaveCount(3);
// Verify evaluations action is present
await expect(getEvaluationsActionItem(n8n.page)).toBeVisible();
await getEvaluationsActionItem(n8n.page).click();
// Verify navigation to evaluations page
await expect(n8n.page).toHaveURL(/\/evaluation/);
});
test('should open workflow settings modal when error workflow action is clicked', async ({
n8n,
}) => {
// Add schedule trigger
await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME);
await n8n.canvas.saveWorkflow();
await n8n.canvas.activateWorkflow();
await closeActivationModal(n8n.page);
await expect(getSuggestedActionsPopover(n8n.page)).toBeVisible();
// Click error workflow action
const errorAction = getErrorActionItem(n8n.page);
await expect(errorAction).toBeVisible();
await errorAction.click();
// Verify workflow settings modal opens
await expect(n8n.page.getByTestId('workflow-settings-dialog')).toBeVisible();
await expect(n8n.page.getByTestId('workflow-settings-error-workflow')).toBeVisible();
});
test('should open workflow settings modal when time saved action is clicked', async ({ n8n }) => {
// Add schedule trigger
await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME);
await n8n.canvas.saveWorkflow();
await n8n.canvas.activateWorkflow();
await closeActivationModal(n8n.page);
await expect(getSuggestedActionsPopover(n8n.page)).toBeVisible();
// Click time saved action
const timeAction = getTimeSavedActionItem(n8n.page);
await expect(timeAction).toBeVisible();
await timeAction.click();
// Verify workflow settings modal opens
await expect(n8n.page.getByTestId('workflow-settings-dialog')).toBeVisible();
});
test('should allow ignoring individual actions', async ({ n8n }) => {
await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME);
await n8n.canvas.saveWorkflow();
await n8n.canvas.activateWorkflow();
await closeActivationModal(n8n.page);
// Suggested actions popover should be open
await expect(getSuggestedActionsPopover(n8n.page)).toBeVisible();
// Verify error workflow action is visible
await expect(getSuggestedActionItem(n8n.page).first()).toContainText('error');
await getSuggestedActionItem(n8n.page).first().getByTitle('Ignore').click();
await n8n.page.waitForTimeout(500); // items disappear after timeout, not arbitrary
await expect(getErrorActionItem(n8n.page)).toBeHidden();
// Close and reopen popover
await n8n.page.locator('body').click({ position: { x: 0, y: 0 } });
await getSuggestedActionsButton(n8n.page).click();
// Verify error workflow action is still no longer visible
await expect(getErrorActionItem(n8n.page)).toBeHidden();
await expect(getTimeSavedActionItem(n8n.page)).toBeVisible();
});
test('should show completed state for configured actions', async ({ n8n }) => {
// Add schedule trigger and activate workflow
await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME);
await n8n.canvas.saveWorkflow();
await n8n.canvas.activateWorkflow();
await closeActivationModal(n8n.page);
// Open workflow settings and set error workflow
await openWorkflowSettings(n8n.page);
// Set an error workflow (we'll use a dummy value)
await n8n.page.getByTestId('workflow-settings-error-workflow').click();
await n8n.page.getByRole('option', { name: 'My workflow' }).first().click();
await n8n.page.getByRole('button', { name: 'Save' }).click();
await expect(n8n.page.getByTestId('workflow-settings-dialog')).toBeHidden();
// Open suggested actions
await getSuggestedActionsButton(n8n.page).click();
await expect(getSuggestedActionsPopover(n8n.page)).toBeVisible();
// Verify error workflow action shows as completed
await expect(
getSuggestedActionItem(n8n.page).first().locator('svg[data-icon="circle-check"]'),
).toBeVisible();
});
test('should allow ignoring all actions with confirmation', async ({ n8n }) => {
// Add schedule trigger
await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME);
await n8n.canvas.saveWorkflow();
await n8n.canvas.activateWorkflow();
await closeActivationModal(n8n.page);
// Suggested actions should be open
await expect(getSuggestedActionsPopover(n8n.page)).toBeVisible();
// Click ignore all button
await getIgnoreAllButton(n8n.page).click();
// Confirm in the dialog
await expect(n8n.page.locator('.el-message-box')).toBeVisible();
await n8n.page
.locator('.el-message-box__btns button')
.filter({ hasText: /ignore for all workflows/i })
.click();
// Verify suggested actions button is no longer visible
await expect(getSuggestedActionsButton(n8n.page)).toBeHidden();
});
});