mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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');
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user