mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
test: Migrate UI tests from Cypress -> Playwright (no-changelog) (#18201)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -36,3 +36,4 @@ trivy_report*
|
|||||||
compiled
|
compiled
|
||||||
packages/cli/src/modules/my-feature
|
packages/cli/src/modules/my-feature
|
||||||
.secrets
|
.secrets
|
||||||
|
packages/testing/**/.cursor/rules/
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { WorkflowPage, NDV } from '../pages';
|
|||||||
const workflowPage = new WorkflowPage();
|
const workflowPage = new WorkflowPage();
|
||||||
const ndv = new NDV();
|
const ndv = new NDV();
|
||||||
|
|
||||||
describe('Schedule Trigger node', () => {
|
// Migrated to Playwright
|
||||||
|
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||||
|
describe.skip('Schedule Trigger node', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
workflowPage.actions.visit();
|
workflowPage.actions.visit();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { NDV, WorkflowPage } from '../pages';
|
|||||||
const workflowPage = new WorkflowPage();
|
const workflowPage = new WorkflowPage();
|
||||||
const ndv = new NDV();
|
const ndv = new NDV();
|
||||||
|
|
||||||
describe('ADO-2270 Save button resets on webhook node open', () => {
|
// Migrated to Playwright
|
||||||
|
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||||
|
describe.skip('ADO-2270 Save button resets on webhook node open', () => {
|
||||||
it('should not reset the save button if webhook node is opened and closed', () => {
|
it('should not reset the save button if webhook node is opened and closed', () => {
|
||||||
workflowPage.actions.visit();
|
workflowPage.actions.visit();
|
||||||
workflowPage.actions.addInitialNodeToCanvas(WEBHOOK_NODE_NAME);
|
workflowPage.actions.addInitialNodeToCanvas(WEBHOOK_NODE_NAME);
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ const workflowPage = new WorkflowPage();
|
|||||||
const ndv = new NDV();
|
const ndv = new NDV();
|
||||||
const executionsTab = new WorkflowExecutionsTab();
|
const executionsTab = new WorkflowExecutionsTab();
|
||||||
|
|
||||||
describe('Debug', () => {
|
// Migrated to Playwright
|
||||||
|
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||||
|
describe.skip('Debug', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.enableFeature('debugInEditor');
|
cy.enableFeature('debugInEditor');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
const url = '/settings';
|
const url = '/settings';
|
||||||
|
|
||||||
describe('Admin user', { disableAutoLogin: true }, () => {
|
// Migrated to Playwright
|
||||||
|
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||||
|
describe.skip('Admin user', { disableAutoLogin: true }, () => {
|
||||||
it('should see same Settings sub menu items as instance owner', () => {
|
it('should see same Settings sub menu items as instance owner', () => {
|
||||||
cy.signinAsOwner();
|
cy.signinAsOwner();
|
||||||
cy.visit(url);
|
cy.visit(url);
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
|
|||||||
|
|
||||||
const WorkflowsPage = new WorkflowsPageClass();
|
const WorkflowsPage = new WorkflowsPageClass();
|
||||||
|
|
||||||
describe('Become creator CTA', () => {
|
// Migrated to Playwright
|
||||||
|
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||||
|
describe.skip('Become creator CTA', () => {
|
||||||
it('should not show the CTA if user is not eligible', () => {
|
it('should not show the CTA if user is not eligible', () => {
|
||||||
interceptCtaRequestWithResponse(false).as('cta');
|
interceptCtaRequestWithResponse(false).as('cta');
|
||||||
cy.visit(WorkflowsPage.url);
|
cy.visit(WorkflowsPage.url);
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ const credentialsModal = new CredentialsModal();
|
|||||||
const ndv = new NDV();
|
const ndv = new NDV();
|
||||||
const mainSidebar = new MainSidebar();
|
const mainSidebar = new MainSidebar();
|
||||||
|
|
||||||
describe('Projects', { disableAutoLogin: true }, () => {
|
// Migrated to Playwright
|
||||||
|
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||||
|
describe.skip('Projects', { disableAutoLogin: true }, () => {
|
||||||
describe('when starting from scratch', () => {
|
describe('when starting from scratch', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.resetDatabase();
|
cy.resetDatabase();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable n8n-local-rules/no-skipped-tests */
|
||||||
import { type ICredentialType } from 'n8n-workflow';
|
import { type ICredentialType } from 'n8n-workflow';
|
||||||
|
|
||||||
import { clickCreateNewCredential, openCredentialSelect } from '../composables/ndv';
|
import { clickCreateNewCredential, openCredentialSelect } from '../composables/ndv';
|
||||||
@@ -13,7 +14,8 @@ const credentialsPage = new CredentialsPage();
|
|||||||
const credentialsModal = new CredentialsModal();
|
const credentialsModal = new CredentialsModal();
|
||||||
const nodeCreatorFeature = new NodeCreator();
|
const nodeCreatorFeature = new NodeCreator();
|
||||||
|
|
||||||
describe('AI Assistant::disabled', () => {
|
// Migrated to Playwright
|
||||||
|
describe.skip('AI Assistant::disabled', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
aiAssistant.actions.disableAssistant();
|
aiAssistant.actions.disableAssistant();
|
||||||
wf.actions.visit();
|
wf.actions.visit();
|
||||||
@@ -34,7 +36,7 @@ describe('AI Assistant::enabled', () => {
|
|||||||
aiAssistant.actions.disableAssistant();
|
aiAssistant.actions.disableAssistant();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders placeholder UI', () => {
|
it.skip('renders placeholder UI', () => {
|
||||||
aiAssistant.getters.askAssistantCanvasActionButton().should('be.visible');
|
aiAssistant.getters.askAssistantCanvasActionButton().should('be.visible');
|
||||||
aiAssistant.getters.askAssistantCanvasActionButton().click();
|
aiAssistant.getters.askAssistantCanvasActionButton().click();
|
||||||
aiAssistant.getters.askAssistantChat().should('be.visible');
|
aiAssistant.getters.askAssistantChat().should('be.visible');
|
||||||
@@ -80,7 +82,7 @@ describe('AI Assistant::enabled', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should start chat session from node error view', () => {
|
it.skip('should start chat session from node error view', () => {
|
||||||
cy.intercept('POST', '/rest/ai/chat', {
|
cy.intercept('POST', '/rest/ai/chat', {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
fixture: 'aiAssistant/responses/simple_message_response.json',
|
fixture: 'aiAssistant/responses/simple_message_response.json',
|
||||||
@@ -98,7 +100,7 @@ describe('AI Assistant::enabled', () => {
|
|||||||
aiAssistant.getters.nodeErrorViewAssistantButton().should('be.disabled');
|
aiAssistant.getters.nodeErrorViewAssistantButton().should('be.disabled');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render chat input correctly', () => {
|
it.skip('should render chat input correctly', () => {
|
||||||
cy.intercept('POST', '/rest/ai/chat', {
|
cy.intercept('POST', '/rest/ai/chat', {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
fixture: 'aiAssistant/responses/simple_message_response.json',
|
fixture: 'aiAssistant/responses/simple_message_response.json',
|
||||||
@@ -131,7 +133,7 @@ describe('AI Assistant::enabled', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render and handle quick replies', () => {
|
it.skip('should render and handle quick replies', () => {
|
||||||
cy.intercept('POST', '/rest/ai/chat', {
|
cy.intercept('POST', '/rest/ai/chat', {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
fixture: 'aiAssistant/responses/quick_reply_message_response.json',
|
fixture: 'aiAssistant/responses/quick_reply_message_response.json',
|
||||||
@@ -148,7 +150,7 @@ describe('AI Assistant::enabled', () => {
|
|||||||
aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', "Sure, let's do it");
|
aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', "Sure, let's do it");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should warn before starting a new session', () => {
|
it.skip('should warn before starting a new session', () => {
|
||||||
cy.intercept('POST', '/rest/ai/chat', {
|
cy.intercept('POST', '/rest/ai/chat', {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
fixture: 'aiAssistant/responses/simple_message_response.json',
|
fixture: 'aiAssistant/responses/simple_message_response.json',
|
||||||
@@ -273,7 +275,7 @@ describe('AI Assistant::enabled', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should end chat session when `end_session` event is received', () => {
|
it.skip('should end chat session when `end_session` event is received', () => {
|
||||||
cy.intercept('POST', '/rest/ai/chat', {
|
cy.intercept('POST', '/rest/ai/chat', {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
fixture: 'aiAssistant/responses/end_session_response.json',
|
fixture: 'aiAssistant/responses/end_session_response.json',
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { WorkflowsPage } from '../pages';
|
|||||||
|
|
||||||
const workflowsPage = new WorkflowsPage();
|
const workflowsPage = new WorkflowsPage();
|
||||||
|
|
||||||
describe('n8n.io iframe', () => {
|
// Migrated to Playwright
|
||||||
|
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||||
|
describe.skip('n8n.io iframe', () => {
|
||||||
describe('when telemetry is disabled', () => {
|
describe('when telemetry is disabled', () => {
|
||||||
it('should not load the iframe when visiting /home/workflows', () => {
|
it('should not load the iframe when visiting /home/workflows', () => {
|
||||||
cy.overrideSettings({ telemetry: { enabled: false } });
|
cy.overrideSettings({ telemetry: { enabled: false } });
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import {
|
|||||||
} from '../composables/workflow';
|
} from '../composables/workflow';
|
||||||
import { AGENT_NODE_NAME, AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME } from '../constants';
|
import { AGENT_NODE_NAME, AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME } from '../constants';
|
||||||
|
|
||||||
describe('AI-716 Correctly set up agent model shows error', () => {
|
// Migrated to Playwright
|
||||||
|
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||||
|
describe.skip('AI-716 Correctly set up agent model shows error', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit(getCredentialsPageUrl());
|
cy.visit(getCredentialsPageUrl());
|
||||||
createNewCredential('OpenAi', 'OpenAI Account', 'API Key', 'sk-123', true);
|
createNewCredential('OpenAi', 'OpenAI Account', 'API Key', 'sk-123', true);
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import {
|
|||||||
} from '../composables/workflow';
|
} from '../composables/workflow';
|
||||||
import Workflow from '../fixtures/Test_9999-SUG-38.json';
|
import Workflow from '../fixtures/Test_9999-SUG-38.json';
|
||||||
|
|
||||||
describe('SUG-38 Inline expression previews are not displayed in NDV', () => {
|
// Migrated to Playwright
|
||||||
|
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||||
|
describe.skip('SUG-38 Inline expression previews are not displayed in NDV', () => {
|
||||||
it("should show resolved inline expression preview in NDV if the node's input data is populated", () => {
|
it("should show resolved inline expression preview in NDV if the node's input data is populated", () => {
|
||||||
navigateToNewWorkflowPage();
|
navigateToNewWorkflowPage();
|
||||||
pasteWorkflow(Workflow);
|
pasteWorkflow(Workflow);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
describe('Environment Feature Flags', () => {
|
// Migrated to Playwright
|
||||||
|
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||||
|
describe.skip('Environment Feature Flags', () => {
|
||||||
it('should set feature flags at runtime and load it back in envFeatureFlags from backend settings', () => {
|
it('should set feature flags at runtime and load it back in envFeatureFlags from backend settings', () => {
|
||||||
cy.setEnvFeatureFlags({
|
cy.setEnvFeatureFlags({
|
||||||
N8N_ENV_FEAT_TEST: true,
|
N8N_ENV_FEAT_TEST: true,
|
||||||
|
|||||||
@@ -1,6 +1,152 @@
|
|||||||
|
import type { FrontendSettings } from '@n8n/api-types';
|
||||||
|
|
||||||
export class TestError extends Error {
|
export class TestError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = 'TestError';
|
this.name = 'TestError';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test requirements for Playwright tests.
|
||||||
|
*
|
||||||
|
* This interface allows you to declaratively specify all test setup requirements
|
||||||
|
* in one place, making tests more readable and maintainable.
|
||||||
|
* If a workflow is specified, the starting point for the test is now the canvas after the workflow is imported.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const requirements: TestRequirements = {
|
||||||
|
* config: {
|
||||||
|
* features: {
|
||||||
|
* aiAssistant: true,
|
||||||
|
* debugInEditor: true,
|
||||||
|
* sharing: true
|
||||||
|
* },
|
||||||
|
* settings: { telemetry: { enabled: false } }
|
||||||
|
* },
|
||||||
|
* workflow: {
|
||||||
|
* 'ai_assistant_test_workflow.json': 'AI Assistant Test Workflow'
|
||||||
|
* },
|
||||||
|
* intercepts: {
|
||||||
|
* 'ai-chat': {
|
||||||
|
* url: '*\/rest/ai/chat',
|
||||||
|
* response: { sessionId: '1', messages: [] }
|
||||||
|
* }
|
||||||
|
* },
|
||||||
|
* storage: {
|
||||||
|
* 'n8n-telemetry': '{"enabled": true}'
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export interface TestRequirements {
|
||||||
|
/**
|
||||||
|
* Configuration settings for the test environment
|
||||||
|
*/
|
||||||
|
config?: {
|
||||||
|
/** Frontend settings to override (merged with default settings) */
|
||||||
|
settings?: Partial<FrontendSettings>;
|
||||||
|
|
||||||
|
/** Feature flags to enable/disable for the test */
|
||||||
|
features?: Record<string, boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API route intercepts and their mock responses
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* intercepts: {
|
||||||
|
* 'ai-chat': {
|
||||||
|
* url: '*\/rest/ai/chat',
|
||||||
|
* response: {
|
||||||
|
* sessionId: '1',
|
||||||
|
* messages: [{ role: 'assistant', type: 'message', text: 'Hello!' }]
|
||||||
|
* }
|
||||||
|
* },
|
||||||
|
* 'become-creator': {
|
||||||
|
* url: '*\/rest/cta/become-creator',
|
||||||
|
* response: true
|
||||||
|
* },
|
||||||
|
* 'credentials-test': {
|
||||||
|
* url: '*\/rest/credentials/test',
|
||||||
|
* response: { data: { status: 'success', message: 'Tested successfully' } }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
intercepts?: Record<string, InterceptConfig>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single workflow to import for the test
|
||||||
|
*
|
||||||
|
* Key: Import file location (relative to workflows folder)
|
||||||
|
* Value: Name to give the workflow when imported
|
||||||
|
*
|
||||||
|
* Note: Only one workflow is supported. Multiple workflows will throw an error.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* workflow: {
|
||||||
|
* 'ai_assistant_test_workflow.json': 'AI Assistant Test Workflow'
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
workflow?: Record<string, string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browser storage values to set before the test
|
||||||
|
*
|
||||||
|
* Supports localStorage, sessionStorage, and other browser storage APIs
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* storage: {
|
||||||
|
* 'n8n-telemetry': '{"enabled": true}',
|
||||||
|
* 'n8n-instance-id': 'test-instance-id'
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
storage?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for API route interception in Playwright
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* {
|
||||||
|
* url: '*\/rest/ai/chat',
|
||||||
|
* response: { sessionId: '1', messages: [] },
|
||||||
|
* status: 200
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example Network error simulation
|
||||||
|
* ```typescript
|
||||||
|
* {
|
||||||
|
* url: '*\/rest/credentials/test',
|
||||||
|
* forceNetworkError: true
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export interface InterceptConfig {
|
||||||
|
/** URL pattern to intercept (supports wildcards) */
|
||||||
|
url: string;
|
||||||
|
|
||||||
|
/** Mock response data */
|
||||||
|
response?: unknown;
|
||||||
|
|
||||||
|
/** HTTP status code to return (default: 200) */
|
||||||
|
status?: number;
|
||||||
|
|
||||||
|
/** HTTP headers to return */
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
|
||||||
|
/** Content type for the response (default: 'application/json') */
|
||||||
|
contentType?: string;
|
||||||
|
|
||||||
|
/** Force network error instead of mock response */
|
||||||
|
forceNetworkError?: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
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');
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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();
|
|
||||||
};
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
import type { n8nPage } from '../pages/n8nPage';
|
import type { n8nPage } from '../pages/n8nPage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,7 +49,7 @@ export class WorkflowComposer {
|
|||||||
fileName: string,
|
fileName: string,
|
||||||
name?: string,
|
name?: string,
|
||||||
): Promise<{ workflowName: string }> {
|
): Promise<{ workflowName: string }> {
|
||||||
const workflowName = name ?? `Imported Workflow ${Date.now()}`;
|
const workflowName = name ?? `Imported Workflow ${nanoid(8)}`;
|
||||||
await this.n8n.goHome();
|
await this.n8n.goHome();
|
||||||
await this.n8n.workflows.clickAddWorkflowButton();
|
await this.n8n.workflows.clickAddWorkflowButton();
|
||||||
await this.n8n.canvas.importWorkflow(fileName, workflowName);
|
await this.n8n.canvas.importWorkflow(fileName, workflowName);
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
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();
|
|
||||||
};
|
|
||||||
@@ -1,18 +1,36 @@
|
|||||||
import type { FrontendSettings } from '@n8n/api-types';
|
|
||||||
import type { BrowserContext, Route } from '@playwright/test';
|
import type { BrowserContext, Route } from '@playwright/test';
|
||||||
import cloneDeep from 'lodash/cloneDeep';
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
|
|
||||||
export let settings: Partial<FrontendSettings>;
|
const contextSettings = new Map<BrowserContext, Partial<Record<string, unknown>>>();
|
||||||
|
|
||||||
|
export function setContextSettings(
|
||||||
|
context: BrowserContext,
|
||||||
|
settings: Partial<Record<string, unknown>>,
|
||||||
|
) {
|
||||||
|
contextSettings.set(context, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getContextSettings(context: BrowserContext) {
|
||||||
|
return contextSettings.get(context);
|
||||||
|
}
|
||||||
|
|
||||||
export async function setupDefaultInterceptors(target: BrowserContext) {
|
export async function setupDefaultInterceptors(target: BrowserContext) {
|
||||||
|
// Global /rest/settings intercept - always active like Cypress
|
||||||
await target.route('**/rest/settings', async (route: Route) => {
|
await target.route('**/rest/settings', async (route: Route) => {
|
||||||
try {
|
try {
|
||||||
const originalResponse = await route.fetch();
|
const originalResponse = await route.fetch();
|
||||||
const originalJson = await originalResponse.json();
|
const originalJson = await originalResponse.json();
|
||||||
|
|
||||||
|
// Get settings stored for this specific context
|
||||||
|
const testSettings = getContextSettings(target);
|
||||||
|
|
||||||
|
// Deep merge test settings with backend settings (like Cypress)
|
||||||
const modifiedData = {
|
const modifiedData = {
|
||||||
data: merge(cloneDeep(originalJson.data), settings),
|
data:
|
||||||
|
testSettings && Object.keys(testSettings).length > 0
|
||||||
|
? merge(cloneDeep(originalJson.data), testSettings)
|
||||||
|
: originalJson.data,
|
||||||
};
|
};
|
||||||
|
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export default [
|
|||||||
...baseConfig,
|
...baseConfig,
|
||||||
playwrightPlugin.configs['flat/recommended'],
|
playwrightPlugin.configs['flat/recommended'],
|
||||||
{
|
{
|
||||||
ignores: ['playwright-report/**/*'],
|
ignores: ['playwright-report/**/*', 'ms-playwright-cache/**/*'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
@@ -22,6 +22,43 @@ export default [
|
|||||||
'playwright/max-nested-describe': 'warn',
|
'playwright/max-nested-describe': 'warn',
|
||||||
'playwright/no-conditional-in-test': 'error',
|
'playwright/no-conditional-in-test': 'error',
|
||||||
'playwright/no-skipped-test': 'warn',
|
'playwright/no-skipped-test': 'warn',
|
||||||
|
// Allow any naming convention for TestRequirements object properties
|
||||||
|
// This is specifically for workflow filenames and intercept keys that may not follow camelCase
|
||||||
|
'@typescript-eslint/naming-convention': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
selector: 'default',
|
||||||
|
format: ['camelCase'],
|
||||||
|
leadingUnderscore: 'allow',
|
||||||
|
trailingUnderscore: 'allow',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'variable',
|
||||||
|
format: ['camelCase', 'UPPER_CASE'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'typeLike',
|
||||||
|
format: ['PascalCase'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'property',
|
||||||
|
format: ['camelCase', 'snake_case', 'UPPER_CASE'],
|
||||||
|
filter: {
|
||||||
|
// Allow any format for properties in TestRequirements objects (workflow files, intercept keys, etc.)
|
||||||
|
regex: '^(workflow|intercepts|storage|config)$',
|
||||||
|
match: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'objectLiteralProperty',
|
||||||
|
format: null, // Allow any format for object literal properties in TestRequirements
|
||||||
|
filter: {
|
||||||
|
// This allows workflow filenames and intercept keys to use any naming convention
|
||||||
|
regex: '\\.(json|spec\\.ts)$|[a-zA-Z0-9_-]+',
|
||||||
|
match: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
'import-x/no-extraneous-dependencies': [
|
'import-x/no-extraneous-dependencies': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { test as base, expect, type TestInfo } from '@playwright/test';
|
import { test as base, expect } from '@playwright/test';
|
||||||
import type { N8NStack } from 'n8n-containers/n8n-test-container-creation';
|
import type { N8NStack } from 'n8n-containers/n8n-test-container-creation';
|
||||||
import { createN8NStack } from 'n8n-containers/n8n-test-container-creation';
|
import { createN8NStack } from 'n8n-containers/n8n-test-container-creation';
|
||||||
import { ContainerTestHelpers } from 'n8n-containers/n8n-test-container-helpers';
|
import { ContainerTestHelpers } from 'n8n-containers/n8n-test-container-helpers';
|
||||||
@@ -7,12 +7,14 @@ import { setTimeout as wait } from 'node:timers/promises';
|
|||||||
import { setupDefaultInterceptors } from '../config/intercepts';
|
import { setupDefaultInterceptors } from '../config/intercepts';
|
||||||
import { n8nPage } from '../pages/n8nPage';
|
import { n8nPage } from '../pages/n8nPage';
|
||||||
import { ApiHelpers } from '../services/api-helper';
|
import { ApiHelpers } from '../services/api-helper';
|
||||||
import { TestError } from '../Types';
|
import { TestError, type TestRequirements } from '../Types';
|
||||||
|
import { setupTestRequirements } from '../utils/requirements';
|
||||||
|
|
||||||
type TestFixtures = {
|
type TestFixtures = {
|
||||||
n8n: n8nPage;
|
n8n: n8nPage;
|
||||||
api: ApiHelpers;
|
api: ApiHelpers;
|
||||||
baseURL: string;
|
baseURL: string;
|
||||||
|
setupRequirements: (requirements: TestRequirements) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type WorkerFixtures = {
|
type WorkerFixtures = {
|
||||||
@@ -40,8 +42,10 @@ interface ContainerConfig {
|
|||||||
export const test = base.extend<TestFixtures, WorkerFixtures>({
|
export const test = base.extend<TestFixtures, WorkerFixtures>({
|
||||||
// Container configuration from the project use options
|
// Container configuration from the project use options
|
||||||
containerConfig: [
|
containerConfig: [
|
||||||
async ({}, use, testInfo: TestInfo) => {
|
async ({}, use, workerInfo) => {
|
||||||
const config = (testInfo.project.use?.containerConfig as ContainerConfig) || {};
|
const config =
|
||||||
|
(workerInfo.project.use as unknown as { containerConfig?: ContainerConfig })
|
||||||
|
?.containerConfig ?? {};
|
||||||
config.env = {
|
config.env = {
|
||||||
...config.env,
|
...config.env,
|
||||||
E2E_TESTS: 'true',
|
E2E_TESTS: 'true',
|
||||||
@@ -122,6 +126,11 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
|||||||
// Browser, baseURL, and dbSetup are required here to ensure they run first.
|
// Browser, baseURL, and dbSetup are required here to ensure they run first.
|
||||||
// This is how Playwright does dependency graphs
|
// This is how Playwright does dependency graphs
|
||||||
context: async ({ context, browser, baseURL, dbSetup }, use) => {
|
context: async ({ context, browser, baseURL, dbSetup }, use) => {
|
||||||
|
// Dependencies: browser, baseURL, dbSetup (ensure they run first)
|
||||||
|
void browser;
|
||||||
|
void baseURL;
|
||||||
|
void dbSetup;
|
||||||
|
|
||||||
await setupDefaultInterceptors(context);
|
await setupDefaultInterceptors(context);
|
||||||
await use(context);
|
await use(context);
|
||||||
},
|
},
|
||||||
@@ -145,6 +154,14 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
|||||||
const api = new ApiHelpers(context.request);
|
const api = new ApiHelpers(context.request);
|
||||||
await use(api);
|
await use(api);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setupRequirements: async ({ page, context }, use) => {
|
||||||
|
const setupFunction = async (requirements: TestRequirements): Promise<void> => {
|
||||||
|
await setupTestRequirements(page, context, requirements);
|
||||||
|
};
|
||||||
|
|
||||||
|
await use(setupFunction);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export { expect };
|
export { expect };
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
"eslint-plugin-playwright": "2.2.2",
|
"eslint-plugin-playwright": "2.2.2",
|
||||||
"generate-schema": "2.6.0",
|
"generate-schema": "2.6.0",
|
||||||
"n8n-containers": "workspace:*",
|
"n8n-containers": "workspace:*",
|
||||||
"tsx": "catalog:"
|
"nanoid": "catalog:",
|
||||||
|
"tsx": "catalog:",
|
||||||
|
"@n8n/api-types": "workspace:^"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
66
packages/testing/playwright/pages/AIAssistantPage.ts
Normal file
66
packages/testing/playwright/pages/AIAssistantPage.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { BasePage } from './BasePage';
|
||||||
|
|
||||||
|
export class AIAssistantPage extends BasePage {
|
||||||
|
getAskAssistantFloatingButton() {
|
||||||
|
return this.page.getByTestId('ask-assistant-floating-button');
|
||||||
|
}
|
||||||
|
|
||||||
|
getAskAssistantCanvasActionButton() {
|
||||||
|
return this.page.getByTestId('ask-assistant-canvas-action-button');
|
||||||
|
}
|
||||||
|
|
||||||
|
getAskAssistantChat() {
|
||||||
|
return this.page.getByTestId('ask-assistant-chat');
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlaceholderMessage() {
|
||||||
|
return this.page.getByTestId('placeholder-message');
|
||||||
|
}
|
||||||
|
|
||||||
|
getChatInput() {
|
||||||
|
return this.page.getByTestId('chat-input');
|
||||||
|
}
|
||||||
|
|
||||||
|
getSendMessageButton() {
|
||||||
|
return this.page.getByTestId('send-message-button');
|
||||||
|
}
|
||||||
|
|
||||||
|
getCloseChatButton() {
|
||||||
|
return this.page.getByTestId('close-chat-button');
|
||||||
|
}
|
||||||
|
|
||||||
|
getAskAssistantSidebarResizer() {
|
||||||
|
return this.page
|
||||||
|
.getByTestId('ask-assistant-sidebar')
|
||||||
|
.locator('[class*="_resizer"][data-dir="left"]')
|
||||||
|
.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
getNodeErrorViewAssistantButton() {
|
||||||
|
return this.page.getByTestId('node-error-view-ask-assistant-button').locator('button').first();
|
||||||
|
}
|
||||||
|
|
||||||
|
getChatMessagesAll() {
|
||||||
|
return this.page.locator('[data-test-id^="chat-message"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
getChatMessagesAssistant() {
|
||||||
|
return this.page.getByTestId('chat-message-assistant');
|
||||||
|
}
|
||||||
|
|
||||||
|
getChatMessagesUser() {
|
||||||
|
return this.page.getByTestId('chat-message-user');
|
||||||
|
}
|
||||||
|
|
||||||
|
getChatMessagesSystem() {
|
||||||
|
return this.page.getByTestId('chat-message-system');
|
||||||
|
}
|
||||||
|
|
||||||
|
getQuickReplyButtons() {
|
||||||
|
return this.page.getByTestId('quick-replies').locator('button');
|
||||||
|
}
|
||||||
|
|
||||||
|
getNewAssistantSessionModal() {
|
||||||
|
return this.page.getByTestId('new-assistant-session-modal');
|
||||||
|
}
|
||||||
|
}
|
||||||
15
packages/testing/playwright/pages/BecomeCreatorCTAPage.ts
Normal file
15
packages/testing/playwright/pages/BecomeCreatorCTAPage.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { BasePage } from './BasePage';
|
||||||
|
|
||||||
|
export class BecomeCreatorCTAPage extends BasePage {
|
||||||
|
getBecomeTemplateCreatorCta() {
|
||||||
|
return this.page.getByTestId('become-template-creator-cta');
|
||||||
|
}
|
||||||
|
|
||||||
|
getCloseBecomeTemplateCreatorCtaButton() {
|
||||||
|
return this.page.getByTestId('close-become-template-creator-cta');
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeBecomeTemplateCreatorCta() {
|
||||||
|
await this.getCloseBecomeTemplateCreatorCtaButton().click();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Locator } from '@playwright/test';
|
import type { Locator } from '@playwright/test';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
import { BasePage } from './BasePage';
|
import { BasePage } from './BasePage';
|
||||||
import { resolveFromRoot } from '../utils/path-helper';
|
import { resolveFromRoot } from '../utils/path-helper';
|
||||||
@@ -8,6 +9,10 @@ export class CanvasPage extends BasePage {
|
|||||||
return this.page.getByRole('button', { name: 'Save' });
|
return this.page.getByRole('button', { name: 'Save' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
workflowSaveButton(): Locator {
|
||||||
|
return this.page.getByTestId('workflow-save-button');
|
||||||
|
}
|
||||||
|
|
||||||
canvasAddButton(): Locator {
|
canvasAddButton(): Locator {
|
||||||
return this.page.getByTestId('canvas-add-button');
|
return this.page.getByTestId('canvas-add-button');
|
||||||
}
|
}
|
||||||
@@ -132,7 +137,6 @@ export class CanvasPage extends BasePage {
|
|||||||
async clickExecutionsTab(): Promise<void> {
|
async clickExecutionsTab(): Promise<void> {
|
||||||
await this.page.getByRole('radio', { name: 'Executions' }).click();
|
await this.page.getByRole('radio', { name: 'Executions' }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async setWorkflowName(name: string): Promise<void> {
|
async setWorkflowName(name: string): Promise<void> {
|
||||||
await this.clickByTestId('inline-edit-preview');
|
await this.clickByTestId('inline-edit-preview');
|
||||||
await this.fillByTestId('inline-edit-input', name);
|
await this.fillByTestId('inline-edit-input', name);
|
||||||
@@ -161,7 +165,6 @@ export class CanvasPage extends BasePage {
|
|||||||
getWorkflowTags() {
|
getWorkflowTags() {
|
||||||
return this.page.getByTestId('workflow-tags').locator('.el-tag');
|
return this.page.getByTestId('workflow-tags').locator('.el-tag');
|
||||||
}
|
}
|
||||||
|
|
||||||
async activateWorkflow() {
|
async activateWorkflow() {
|
||||||
const responsePromise = this.page.waitForResponse(
|
const responsePromise = this.page.waitForResponse(
|
||||||
(response) =>
|
(response) =>
|
||||||
@@ -170,4 +173,90 @@ export class CanvasPage extends BasePage {
|
|||||||
await this.page.getByTestId('workflow-activate-switch').click();
|
await this.page.getByTestId('workflow-activate-switch').click();
|
||||||
await responsePromise;
|
await responsePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async clickZoomToFitButton(): Promise<void> {
|
||||||
|
await this.clickByTestId('zoom-to-fit');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get node issues for a specific node
|
||||||
|
*/
|
||||||
|
getNodeIssuesByName(nodeName: string) {
|
||||||
|
return this.nodeByName(nodeName).getByTestId('node-issues');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add tags to the workflow
|
||||||
|
* @param count - The number of tags to add
|
||||||
|
* @returns An array of tag names
|
||||||
|
*/
|
||||||
|
async addTags(count: number = 1): Promise<string[]> {
|
||||||
|
const tags: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const tag = `tag-${nanoid(8)}-${i}`;
|
||||||
|
tags.push(tag);
|
||||||
|
|
||||||
|
if (i === 0) {
|
||||||
|
await this.clickByText('Add tag');
|
||||||
|
} else {
|
||||||
|
await this.page
|
||||||
|
.getByTestId('tags-dropdown')
|
||||||
|
.getByText(tags[i - 1])
|
||||||
|
.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.page.getByRole('combobox').first().fill(tag);
|
||||||
|
await this.page.getByRole('combobox').first().press('Enter');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.page.click('body');
|
||||||
|
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Production Checklist methods
|
||||||
|
getProductionChecklistButton(): Locator {
|
||||||
|
return this.page.getByTestId('suggested-action-count');
|
||||||
|
}
|
||||||
|
|
||||||
|
getProductionChecklistPopover(): Locator {
|
||||||
|
return this.page.locator('[data-reka-popper-content-wrapper=""]').filter({ hasText: /./ });
|
||||||
|
}
|
||||||
|
|
||||||
|
getProductionChecklistActionItem(text?: string): Locator {
|
||||||
|
const items = this.page.getByTestId('suggested-action-item');
|
||||||
|
if (text) {
|
||||||
|
return items.getByText(text);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
getProductionChecklistIgnoreAllButton(): Locator {
|
||||||
|
return this.page.getByTestId('suggested-action-ignore-all');
|
||||||
|
}
|
||||||
|
|
||||||
|
getErrorActionItem(): Locator {
|
||||||
|
return this.getProductionChecklistActionItem('Set up error notifications');
|
||||||
|
}
|
||||||
|
|
||||||
|
getTimeSavedActionItem(): Locator {
|
||||||
|
return this.getProductionChecklistActionItem('Track time saved');
|
||||||
|
}
|
||||||
|
|
||||||
|
getEvaluationsActionItem(): Locator {
|
||||||
|
return this.getProductionChecklistActionItem('Test reliability of AI steps');
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickProductionChecklistButton(): Promise<void> {
|
||||||
|
await this.getProductionChecklistButton().click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickProductionChecklistIgnoreAll(): Promise<void> {
|
||||||
|
await this.getProductionChecklistIgnoreAllButton().click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickProductionChecklistAction(actionText: string): Promise<void> {
|
||||||
|
await this.getProductionChecklistActionItem(actionText).click();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
packages/testing/playwright/pages/IframePage.ts
Normal file
15
packages/testing/playwright/pages/IframePage.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { BasePage } from './BasePage';
|
||||||
|
|
||||||
|
export class IframePage extends BasePage {
|
||||||
|
getIframe() {
|
||||||
|
return this.page.locator('iframe');
|
||||||
|
}
|
||||||
|
|
||||||
|
getIframeBySrc(src: string) {
|
||||||
|
return this.page.locator(`iframe[src="${src}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForIframeRequest(url: string) {
|
||||||
|
await this.page.waitForResponse(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,4 +35,16 @@ export class NodeDisplayViewPage extends BasePage {
|
|||||||
async close() {
|
async close() {
|
||||||
await this.clickBackToCanvasButton();
|
await this.clickBackToCanvasButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async execute() {
|
||||||
|
await this.clickByTestId('node-execute-button');
|
||||||
|
}
|
||||||
|
|
||||||
|
getOutputPanel() {
|
||||||
|
return this.page.getByTestId('output-panel');
|
||||||
|
}
|
||||||
|
|
||||||
|
getParameterExpressionPreviewValue() {
|
||||||
|
return this.page.getByTestId('parameter-expression-preview-value');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
packages/testing/playwright/pages/SettingsPage.ts
Normal file
15
packages/testing/playwright/pages/SettingsPage.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { BasePage } from './BasePage';
|
||||||
|
|
||||||
|
export class SettingsPage extends BasePage {
|
||||||
|
getMenuItems() {
|
||||||
|
return this.page.getByTestId('menu-item');
|
||||||
|
}
|
||||||
|
|
||||||
|
getMenuItem(id: string) {
|
||||||
|
return this.page.getByTestId('menu-item').getByTestId(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async goToSettings() {
|
||||||
|
await this.page.goto('/settings');
|
||||||
|
}
|
||||||
|
}
|
||||||
35
packages/testing/playwright/pages/VersionsPage.ts
Normal file
35
packages/testing/playwright/pages/VersionsPage.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { BasePage } from './BasePage';
|
||||||
|
|
||||||
|
export class VersionsPage extends BasePage {
|
||||||
|
getVersionUpdatesPanelOpenButton() {
|
||||||
|
return this.page.getByTestId('version-update-next-versions-link');
|
||||||
|
}
|
||||||
|
|
||||||
|
getVersionUpdatesPanel() {
|
||||||
|
return this.page.getByTestId('version-updates-panel');
|
||||||
|
}
|
||||||
|
|
||||||
|
getVersionUpdatesPanelCloseButton() {
|
||||||
|
return this.getVersionUpdatesPanel().getByRole('button', { name: 'Close' });
|
||||||
|
}
|
||||||
|
|
||||||
|
getVersionCard() {
|
||||||
|
return this.page.getByTestId('version-card');
|
||||||
|
}
|
||||||
|
|
||||||
|
getWhatsNewMenuItem() {
|
||||||
|
return this.page.getByTestId('menu-item').getByTestId('whats-new');
|
||||||
|
}
|
||||||
|
|
||||||
|
async openWhatsNewMenu() {
|
||||||
|
await this.getWhatsNewMenuItem().click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async openVersionUpdatesPanel() {
|
||||||
|
await this.getVersionUpdatesPanelOpenButton().click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeVersionUpdatesPanel() {
|
||||||
|
await this.getVersionUpdatesPanelCloseButton().click();
|
||||||
|
}
|
||||||
|
}
|
||||||
31
packages/testing/playwright/pages/WorkflowActivationModal.ts
Normal file
31
packages/testing/playwright/pages/WorkflowActivationModal.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type { Locator } from '@playwright/test';
|
||||||
|
|
||||||
|
import { BasePage } from './BasePage';
|
||||||
|
|
||||||
|
export class WorkflowActivationModal extends BasePage {
|
||||||
|
getModal(): Locator {
|
||||||
|
return this.page.getByTestId('activation-modal');
|
||||||
|
}
|
||||||
|
|
||||||
|
getDontShowAgainCheckbox(): Locator {
|
||||||
|
return this.getModal().getByText("Don't show again");
|
||||||
|
}
|
||||||
|
|
||||||
|
getGotItButton(): Locator {
|
||||||
|
return this.getModal().getByRole('button', { name: 'Got it' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
await this.getDontShowAgainCheckbox().click();
|
||||||
|
|
||||||
|
await this.getGotItButton().click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickDontShowAgain(): Promise<void> {
|
||||||
|
await this.getDontShowAgainCheckbox().click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickGotIt(): Promise<void> {
|
||||||
|
await this.getGotItButton().click();
|
||||||
|
}
|
||||||
|
}
|
||||||
39
packages/testing/playwright/pages/WorkflowSettingsModal.ts
Normal file
39
packages/testing/playwright/pages/WorkflowSettingsModal.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { Locator } from '@playwright/test';
|
||||||
|
|
||||||
|
import { BasePage } from './BasePage';
|
||||||
|
|
||||||
|
export class WorkflowSettingsModal extends BasePage {
|
||||||
|
getModal(): Locator {
|
||||||
|
return this.page.getByTestId('workflow-settings-dialog');
|
||||||
|
}
|
||||||
|
|
||||||
|
getWorkflowMenu(): Locator {
|
||||||
|
return this.page.getByTestId('workflow-menu');
|
||||||
|
}
|
||||||
|
|
||||||
|
getSettingsMenuItem(): Locator {
|
||||||
|
return this.page.getByTestId('workflow-menu-item-settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
getErrorWorkflowField(): Locator {
|
||||||
|
return this.page.getByTestId('workflow-settings-error-workflow');
|
||||||
|
}
|
||||||
|
|
||||||
|
getSaveButton(): Locator {
|
||||||
|
return this.page.getByRole('button', { name: 'Save' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async open(): Promise<void> {
|
||||||
|
await this.getWorkflowMenu().click();
|
||||||
|
await this.getSettingsMenuItem().click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickSave(): Promise<void> {
|
||||||
|
await this.getSaveButton().click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectErrorWorkflow(workflowName: string): Promise<void> {
|
||||||
|
await this.getErrorWorkflowField().click();
|
||||||
|
await this.page.getByRole('option', { name: workflowName }).first().click();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,19 @@
|
|||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
|
|
||||||
|
import { AIAssistantPage } from './AIAssistantPage';
|
||||||
|
import { BecomeCreatorCTAPage } from './BecomeCreatorCTAPage';
|
||||||
import { CanvasPage } from './CanvasPage';
|
import { CanvasPage } from './CanvasPage';
|
||||||
import { CredentialsPage } from './CredentialsPage';
|
import { CredentialsPage } from './CredentialsPage';
|
||||||
import { ExecutionsPage } from './ExecutionsPage';
|
import { ExecutionsPage } from './ExecutionsPage';
|
||||||
|
import { IframePage } from './IframePage';
|
||||||
import { NodeDisplayViewPage } from './NodeDisplayViewPage';
|
import { NodeDisplayViewPage } from './NodeDisplayViewPage';
|
||||||
import { NotificationsPage } from './NotificationsPage';
|
import { NotificationsPage } from './NotificationsPage';
|
||||||
import { ProjectSettingsPage } from './ProjectSettingsPage';
|
import { ProjectSettingsPage } from './ProjectSettingsPage';
|
||||||
|
import { SettingsPage } from './SettingsPage';
|
||||||
import { SidebarPage } from './SidebarPage';
|
import { SidebarPage } from './SidebarPage';
|
||||||
|
import { VersionsPage } from './VersionsPage';
|
||||||
|
import { WorkflowActivationModal } from './WorkflowActivationModal';
|
||||||
|
import { WorkflowSettingsModal } from './WorkflowSettingsModal';
|
||||||
import { WorkflowSharingModal } from './WorkflowSharingModal';
|
import { WorkflowSharingModal } from './WorkflowSharingModal';
|
||||||
import { WorkflowsPage } from './WorkflowsPage';
|
import { WorkflowsPage } from './WorkflowsPage';
|
||||||
import { CanvasComposer } from '../composables/CanvasComposer';
|
import { CanvasComposer } from '../composables/CanvasComposer';
|
||||||
@@ -18,18 +25,28 @@ export class n8nPage {
|
|||||||
readonly page: Page;
|
readonly page: Page;
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
|
readonly aiAssistant: AIAssistantPage;
|
||||||
|
readonly becomeCreatorCTA: BecomeCreatorCTAPage;
|
||||||
readonly canvas: CanvasPage;
|
readonly canvas: CanvasPage;
|
||||||
|
|
||||||
|
readonly iframe: IframePage;
|
||||||
readonly ndv: NodeDisplayViewPage;
|
readonly ndv: NodeDisplayViewPage;
|
||||||
readonly projectSettings: ProjectSettingsPage;
|
readonly projectSettings: ProjectSettingsPage;
|
||||||
|
readonly settings: SettingsPage;
|
||||||
|
readonly versions: VersionsPage;
|
||||||
readonly workflows: WorkflowsPage;
|
readonly workflows: WorkflowsPage;
|
||||||
readonly notifications: NotificationsPage;
|
readonly notifications: NotificationsPage;
|
||||||
readonly credentials: CredentialsPage;
|
readonly credentials: CredentialsPage;
|
||||||
readonly executions: ExecutionsPage;
|
readonly executions: ExecutionsPage;
|
||||||
readonly sideBar: SidebarPage;
|
readonly sideBar: SidebarPage;
|
||||||
|
|
||||||
|
// Modals
|
||||||
|
readonly workflowActivationModal: WorkflowActivationModal;
|
||||||
|
readonly workflowSettingsModal: WorkflowSettingsModal;
|
||||||
|
readonly workflowSharingModal: WorkflowSharingModal;
|
||||||
|
|
||||||
// Composables
|
// Composables
|
||||||
readonly workflowComposer: WorkflowComposer;
|
readonly workflowComposer: WorkflowComposer;
|
||||||
readonly workflowSharingModal: WorkflowSharingModal;
|
|
||||||
readonly projectComposer: ProjectComposer;
|
readonly projectComposer: ProjectComposer;
|
||||||
readonly canvasComposer: CanvasComposer;
|
readonly canvasComposer: CanvasComposer;
|
||||||
|
|
||||||
@@ -37,9 +54,15 @@ export class n8nPage {
|
|||||||
this.page = page;
|
this.page = page;
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
|
this.aiAssistant = new AIAssistantPage(page);
|
||||||
|
this.becomeCreatorCTA = new BecomeCreatorCTAPage(page);
|
||||||
this.canvas = new CanvasPage(page);
|
this.canvas = new CanvasPage(page);
|
||||||
|
|
||||||
|
this.iframe = new IframePage(page);
|
||||||
this.ndv = new NodeDisplayViewPage(page);
|
this.ndv = new NodeDisplayViewPage(page);
|
||||||
this.projectSettings = new ProjectSettingsPage(page);
|
this.projectSettings = new ProjectSettingsPage(page);
|
||||||
|
this.settings = new SettingsPage(page);
|
||||||
|
this.versions = new VersionsPage(page);
|
||||||
this.workflows = new WorkflowsPage(page);
|
this.workflows = new WorkflowsPage(page);
|
||||||
this.notifications = new NotificationsPage(page);
|
this.notifications = new NotificationsPage(page);
|
||||||
this.credentials = new CredentialsPage(page);
|
this.credentials = new CredentialsPage(page);
|
||||||
@@ -47,6 +70,10 @@ export class n8nPage {
|
|||||||
this.sideBar = new SidebarPage(page);
|
this.sideBar = new SidebarPage(page);
|
||||||
this.workflowSharingModal = new WorkflowSharingModal(page);
|
this.workflowSharingModal = new WorkflowSharingModal(page);
|
||||||
|
|
||||||
|
// Modals
|
||||||
|
this.workflowActivationModal = new WorkflowActivationModal(page);
|
||||||
|
this.workflowSettingsModal = new WorkflowSettingsModal(page);
|
||||||
|
|
||||||
// Composables
|
// Composables
|
||||||
this.workflowComposer = new WorkflowComposer(this);
|
this.workflowComposer = new WorkflowComposer(this);
|
||||||
this.projectComposer = new ProjectComposer(this);
|
this.projectComposer = new ProjectComposer(this);
|
||||||
|
|||||||
@@ -144,6 +144,41 @@ export class ApiHelpers {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== FEATURE FLAG METHODS =====
|
||||||
|
|
||||||
|
async setEnvFeatureFlags(flags: Record<string, string>): Promise<{
|
||||||
|
data: {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
flags: Record<string, string>;
|
||||||
|
};
|
||||||
|
}> {
|
||||||
|
const response = await this.request.patch('/rest/e2e/env-feature-flags', {
|
||||||
|
data: { flags },
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearEnvFeatureFlags(): Promise<{
|
||||||
|
data: {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
flags: Record<string, string>;
|
||||||
|
};
|
||||||
|
}> {
|
||||||
|
const response = await this.request.patch('/rest/e2e/env-feature-flags', {
|
||||||
|
data: { flags: {} },
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEnvFeatureFlags(): Promise<{
|
||||||
|
data: Record<string, string>;
|
||||||
|
}> {
|
||||||
|
const response = await this.request.get('/rest/e2e/env-feature-flags');
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
// ===== CONVENIENCE METHODS =====
|
// ===== CONVENIENCE METHODS =====
|
||||||
|
|
||||||
async enableFeature(feature: string): Promise<void> {
|
async enableFeature(feature: string): Promise<void> {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
// @ts-expect-error - 'generate-schema' is not typed, so we ignore the TS error.
|
// @ts-expect-error - 'generate-schema' is not typed, so we ignore the TS error.
|
||||||
import GenerateSchema from 'generate-schema';
|
import generateSchema from 'generate-schema';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import { findPackagesRoot } from '../../utils/path-helper';
|
import { findPackagesRoot } from '../../utils/path-helper';
|
||||||
@@ -136,7 +136,7 @@ test.describe('Workflow Tests', () => {
|
|||||||
|
|
||||||
// Optionally, validate the output against a JSON schema snapshot if enabled.
|
// Optionally, validate the output against a JSON schema snapshot if enabled.
|
||||||
if (SCHEMA_MODE && result.data && workflow.enableSchemaValidation) {
|
if (SCHEMA_MODE && result.data && workflow.enableSchemaValidation) {
|
||||||
const schema = GenerateSchema.json(result.data);
|
const schema = generateSchema.json(result.data);
|
||||||
expect(JSON.stringify(schema, null, 2)).toMatchSnapshot(
|
expect(JSON.stringify(schema, null, 2)).toMatchSnapshot(
|
||||||
`workflow-${workflow.id}-schema.snap`,
|
`workflow-${workflow.id}-schema.snap`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
import { test, expect } from '../../fixtures/base';
|
import { test, expect } from '../../fixtures/base';
|
||||||
|
|
||||||
const NOTIFICATIONS = {
|
const NOTIFICATIONS = {
|
||||||
@@ -18,7 +20,9 @@ test.describe('Workflows', () => {
|
|||||||
await expect(n8n.canvas.getWorkflowTags()).toHaveText(['some-tag-1', 'some-tag-2']);
|
await expect(n8n.canvas.getWorkflowTags()).toHaveText(['some-tag-1', 'some-tag-2']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should create a new workflow using add workflow button', async ({ n8n }) => {
|
test('should create a new workflow using add workflow button and save successfully', async ({
|
||||||
|
n8n,
|
||||||
|
}) => {
|
||||||
await n8n.workflows.clickAddWorkflowButton();
|
await n8n.workflows.clickAddWorkflowButton();
|
||||||
|
|
||||||
const workflowName = `Test Workflow ${Date.now()}`;
|
const workflowName = `Test Workflow ${Date.now()}`;
|
||||||
@@ -31,9 +35,9 @@ test.describe('Workflows', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should search for workflows', async ({ n8n }) => {
|
test('should search for workflows', async ({ n8n }) => {
|
||||||
const date = Date.now();
|
const uniqueId = nanoid(8);
|
||||||
const specificName = `Specific Test ${date}`;
|
const specificName = `Specific Test ${uniqueId}`;
|
||||||
const genericName = `Generic Test ${date}`;
|
const genericName = `Generic Test ${uniqueId}`;
|
||||||
|
|
||||||
await n8n.workflowComposer.createWorkflow(specificName);
|
await n8n.workflowComposer.createWorkflow(specificName);
|
||||||
await n8n.goHome();
|
await n8n.goHome();
|
||||||
@@ -47,7 +51,7 @@ test.describe('Workflows', () => {
|
|||||||
|
|
||||||
// Search with partial term
|
// Search with partial term
|
||||||
await n8n.workflows.clearSearch();
|
await n8n.workflows.clearSearch();
|
||||||
await n8n.workflows.searchWorkflows(date.toString());
|
await n8n.workflows.searchWorkflows(uniqueId);
|
||||||
await expect(n8n.workflows.getWorkflowItems()).toHaveCount(2);
|
await expect(n8n.workflows.getWorkflowItems()).toHaveCount(2);
|
||||||
|
|
||||||
// Search for non-existent
|
// Search for non-existent
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { test, expect } from '../../fixtures/base';
|
||||||
|
|
||||||
|
test.describe('Schedule Trigger node', () => {
|
||||||
|
test.beforeEach(async ({ n8n }) => {
|
||||||
|
await n8n.goHome();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should execute schedule trigger node and return timestamp in output', async ({ n8n }) => {
|
||||||
|
await n8n.workflows.clickAddWorkflowButton();
|
||||||
|
await n8n.canvas.addNode('Schedule Trigger');
|
||||||
|
|
||||||
|
await n8n.ndv.execute();
|
||||||
|
|
||||||
|
await expect(n8n.ndv.getOutputPanel()).toContainText('timestamp');
|
||||||
|
|
||||||
|
await n8n.ndv.clickBackToCanvasButton();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { test, expect } from '../../fixtures/base';
|
||||||
|
|
||||||
|
test.describe('ADO-2270 Save button resets on webhook node open', () => {
|
||||||
|
test('should not reset the save button if webhook node is opened and closed', async ({ n8n }) => {
|
||||||
|
await n8n.goHome();
|
||||||
|
|
||||||
|
await n8n.workflows.clickAddWorkflowButton();
|
||||||
|
await n8n.canvas.addNode('Webhook');
|
||||||
|
|
||||||
|
await n8n.page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
await n8n.canvas.clickSaveWorkflowButton();
|
||||||
|
|
||||||
|
await n8n.canvas.openNode('Webhook');
|
||||||
|
|
||||||
|
await n8n.ndv.clickBackToCanvasButton();
|
||||||
|
|
||||||
|
await expect(n8n.canvas.workflowSaveButton()).toContainText('Saved');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { test, expect } from '../../fixtures/base';
|
||||||
|
|
||||||
|
test.describe('Admin user', () => {
|
||||||
|
test('should see same Settings sub menu items as instance owner', async ({ n8n, api }) => {
|
||||||
|
await api.setupTest('signin-only', 'owner');
|
||||||
|
await n8n.settings.goToSettings();
|
||||||
|
|
||||||
|
const ownerMenuItems = await n8n.settings.getMenuItems().count();
|
||||||
|
|
||||||
|
await api.setupTest('signin-only', 'admin');
|
||||||
|
await n8n.settings.goToSettings();
|
||||||
|
|
||||||
|
await expect(n8n.settings.getMenuItems()).toHaveCount(ownerMenuItems);
|
||||||
|
});
|
||||||
|
});
|
||||||
276
packages/testing/playwright/tests/ui/45-ai-assistant.spec.ts
Normal file
276
packages/testing/playwright/tests/ui/45-ai-assistant.spec.ts
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import { test, expect } from '../../fixtures/base';
|
||||||
|
import type { TestRequirements } from '../../Types';
|
||||||
|
|
||||||
|
const aiDisabledRequirements: TestRequirements = {
|
||||||
|
config: {
|
||||||
|
features: { aiAssistant: false },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const aiEnabledRequirements: TestRequirements = {
|
||||||
|
config: {
|
||||||
|
features: { aiAssistant: true },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const aiEnabledWithWorkflowRequirements: TestRequirements = {
|
||||||
|
config: {
|
||||||
|
features: { aiAssistant: true },
|
||||||
|
},
|
||||||
|
workflow: {
|
||||||
|
'ai_assistant_test_workflow.json': 'AI_Assistant_Test_Workflow',
|
||||||
|
},
|
||||||
|
intercepts: {
|
||||||
|
aiChat: {
|
||||||
|
url: '**/rest/ai/chat',
|
||||||
|
response: {
|
||||||
|
sessionId: '1',
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'message',
|
||||||
|
text: 'Hey, this is an assistant message',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const aiEnabledWithQuickRepliesRequirements: TestRequirements = {
|
||||||
|
config: {
|
||||||
|
features: { aiAssistant: true },
|
||||||
|
},
|
||||||
|
workflow: {
|
||||||
|
'ai_assistant_test_workflow.json': 'AI_Assistant_Test_Workflow',
|
||||||
|
},
|
||||||
|
intercepts: {
|
||||||
|
aiChat: {
|
||||||
|
url: '**/rest/ai/chat',
|
||||||
|
response: {
|
||||||
|
sessionId: '1',
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'message',
|
||||||
|
text: 'Hey, this is an assistant message',
|
||||||
|
quickReplies: [
|
||||||
|
{
|
||||||
|
text: "Sure, let's do it",
|
||||||
|
type: 'yes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Nah, doesn't sound good",
|
||||||
|
type: 'no',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const aiEnabledWithEndSessionRequirements: TestRequirements = {
|
||||||
|
config: {
|
||||||
|
features: { aiAssistant: true },
|
||||||
|
},
|
||||||
|
workflow: {
|
||||||
|
'ai_assistant_test_workflow.json': 'AI_Assistant_Test_Workflow',
|
||||||
|
},
|
||||||
|
intercepts: {
|
||||||
|
aiChat: {
|
||||||
|
url: '**/rest/ai/chat',
|
||||||
|
response: {
|
||||||
|
sessionId: '1',
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'message',
|
||||||
|
title: 'Glad to Help',
|
||||||
|
text: "I'm glad I could help. If you have any more questions or need further assistance with your n8n workflows, feel free to ask!",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'event',
|
||||||
|
eventName: 'end-session',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
test.describe('AI Assistant::disabled', () => {
|
||||||
|
test('does not show assistant button if feature is disabled', async ({
|
||||||
|
n8n,
|
||||||
|
setupRequirements,
|
||||||
|
}) => {
|
||||||
|
await setupRequirements(aiDisabledRequirements);
|
||||||
|
await expect(n8n.aiAssistant.getAskAssistantFloatingButton()).toHaveCount(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('AI Assistant::enabled', () => {
|
||||||
|
test('renders placeholder UI', async ({ n8n, setupRequirements }) => {
|
||||||
|
await setupRequirements(aiEnabledRequirements);
|
||||||
|
await n8n.page.goto('/workflow/new');
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getAskAssistantCanvasActionButton()).toBeVisible();
|
||||||
|
|
||||||
|
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeVisible();
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getPlaceholderMessage()).toBeVisible();
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getChatInput()).toBeVisible();
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getSendMessageButton()).toBeDisabled();
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getCloseChatButton()).toBeVisible();
|
||||||
|
|
||||||
|
await n8n.aiAssistant.getCloseChatButton().click();
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeHidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show resizer when chat is open', async ({ n8n, setupRequirements }) => {
|
||||||
|
await setupRequirements(aiEnabledRequirements);
|
||||||
|
await n8n.page.goto('/workflow/new');
|
||||||
|
|
||||||
|
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getAskAssistantSidebarResizer()).toBeVisible();
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeVisible();
|
||||||
|
|
||||||
|
await n8n.aiAssistant.getAskAssistantSidebarResizer().hover();
|
||||||
|
|
||||||
|
await n8n.aiAssistant.getCloseChatButton().click();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should start chat session from node error view', async ({ n8n, setupRequirements }) => {
|
||||||
|
await setupRequirements(aiEnabledWithWorkflowRequirements);
|
||||||
|
|
||||||
|
await n8n.canvas.openNode('Stop and Error');
|
||||||
|
|
||||||
|
await n8n.ndv.execute();
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getNodeErrorViewAssistantButton()).toBeVisible();
|
||||||
|
await expect(n8n.aiAssistant.getNodeErrorViewAssistantButton()).toBeEnabled();
|
||||||
|
|
||||||
|
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(1);
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(1);
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getChatMessagesAll().first()).toContainText(
|
||||||
|
'Hey, this is an assistant message',
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getNodeErrorViewAssistantButton()).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render chat input correctly', async ({ n8n, setupRequirements }) => {
|
||||||
|
await setupRequirements(aiEnabledWithWorkflowRequirements);
|
||||||
|
|
||||||
|
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeVisible();
|
||||||
|
await expect(n8n.aiAssistant.getChatInput()).toBeVisible();
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getSendMessageButton()).toBeDisabled();
|
||||||
|
|
||||||
|
await n8n.aiAssistant.getChatInput().fill('Test message');
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getChatInput()).toHaveValue('Test message');
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getSendMessageButton()).toBeEnabled();
|
||||||
|
|
||||||
|
await n8n.aiAssistant.getSendMessageButton().click();
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getChatInput()).toHaveValue('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render and handle quick replies', async ({ n8n, setupRequirements }) => {
|
||||||
|
await setupRequirements(aiEnabledWithQuickRepliesRequirements);
|
||||||
|
|
||||||
|
await n8n.canvas.openNode('Stop and Error');
|
||||||
|
|
||||||
|
await n8n.ndv.execute();
|
||||||
|
|
||||||
|
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getQuickReplyButtons()).toHaveCount(2);
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getQuickReplyButtons()).toHaveCount(2);
|
||||||
|
|
||||||
|
await n8n.aiAssistant.getQuickReplyButtons().first().click();
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getChatMessagesUser().first()).toContainText("Sure, let's do it");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should warn before starting a new session', async ({ n8n, setupRequirements }) => {
|
||||||
|
await setupRequirements(aiEnabledWithWorkflowRequirements);
|
||||||
|
|
||||||
|
await n8n.canvas.openNode('Edit Fields');
|
||||||
|
|
||||||
|
await n8n.ndv.execute();
|
||||||
|
|
||||||
|
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(1);
|
||||||
|
|
||||||
|
await n8n.aiAssistant.getCloseChatButton().click();
|
||||||
|
|
||||||
|
await n8n.ndv.clickBackToCanvasButton();
|
||||||
|
|
||||||
|
await n8n.canvas.openNode('Stop and Error');
|
||||||
|
|
||||||
|
await n8n.ndv.execute();
|
||||||
|
|
||||||
|
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getNewAssistantSessionModal()).toBeVisible();
|
||||||
|
|
||||||
|
await n8n.aiAssistant
|
||||||
|
.getNewAssistantSessionModal()
|
||||||
|
.getByRole('button', { name: 'Start new session' })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(1);
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should end chat session when `end_session` event is received', async ({
|
||||||
|
n8n,
|
||||||
|
setupRequirements,
|
||||||
|
}) => {
|
||||||
|
await setupRequirements(aiEnabledWithEndSessionRequirements);
|
||||||
|
|
||||||
|
await n8n.canvas.openNode('Stop and Error');
|
||||||
|
|
||||||
|
await n8n.ndv.execute();
|
||||||
|
|
||||||
|
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getChatMessagesSystem()).toHaveCount(1);
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getChatMessagesSystem()).toHaveCount(1);
|
||||||
|
|
||||||
|
await expect(n8n.aiAssistant.getChatMessagesSystem().first()).toContainText(
|
||||||
|
'session has ended',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { test, expect } from '../../fixtures/base';
|
||||||
|
import type { TestRequirements } from '../../Types';
|
||||||
|
|
||||||
|
const telemetryDisabledRequirements: TestRequirements = {
|
||||||
|
config: {
|
||||||
|
settings: {
|
||||||
|
telemetry: { enabled: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
'n8n-telemetry': JSON.stringify({ enabled: false }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const telemetryEnabledRequirements: TestRequirements = {
|
||||||
|
config: {
|
||||||
|
settings: {
|
||||||
|
telemetry: { enabled: true },
|
||||||
|
instanceId: 'test-instance-id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
'n8n-telemetry': JSON.stringify({ enabled: true }),
|
||||||
|
'n8n-instance-id': 'test-instance-id',
|
||||||
|
},
|
||||||
|
intercepts: {
|
||||||
|
iframeRequest: {
|
||||||
|
url: 'https://n8n.io/self-install*',
|
||||||
|
response: '<html><body>Test iframe content</body></html>',
|
||||||
|
contentType: 'text/html',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
test.describe('n8n.io iframe', () => {
|
||||||
|
test.describe('when telemetry is disabled', () => {
|
||||||
|
test('should not load the iframe when visiting /home/workflows', async ({
|
||||||
|
n8n,
|
||||||
|
setupRequirements,
|
||||||
|
}) => {
|
||||||
|
await setupRequirements(telemetryDisabledRequirements);
|
||||||
|
|
||||||
|
await n8n.page.goto('/');
|
||||||
|
await n8n.page.waitForLoadState();
|
||||||
|
await expect(n8n.iframe.getIframe()).not.toBeAttached();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('when telemetry is enabled', () => {
|
||||||
|
test('should load the iframe when visiting /home/workflows @auth:owner', async ({
|
||||||
|
n8n,
|
||||||
|
setupRequirements,
|
||||||
|
api,
|
||||||
|
}) => {
|
||||||
|
await setupRequirements(telemetryEnabledRequirements);
|
||||||
|
|
||||||
|
// Get current user ID from the API
|
||||||
|
const currentUser = await api.get('/rest/login');
|
||||||
|
const testInstanceId = 'test-instance-id';
|
||||||
|
const testUserId = currentUser.id;
|
||||||
|
const iframeUrl = `https://n8n.io/self-install?instanceId=${testInstanceId}&userId=${testUserId}`;
|
||||||
|
|
||||||
|
await n8n.page.goto('/');
|
||||||
|
await n8n.page.waitForLoadState();
|
||||||
|
|
||||||
|
const iframeElement = n8n.iframe.getIframeBySrc(iframeUrl);
|
||||||
|
await expect(iframeElement).toBeAttached();
|
||||||
|
|
||||||
|
await expect(iframeElement).toHaveAttribute('src', iframeUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,14 +1,3 @@
|
|||||||
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';
|
import { test, expect } from '../../fixtures/base';
|
||||||
|
|
||||||
const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
|
const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
|
||||||
@@ -22,35 +11,29 @@ test.describe('Workflow Production Checklist', () => {
|
|||||||
test('should show suggested actions automatically when workflow is first activated', async ({
|
test('should show suggested actions automatically when workflow is first activated', async ({
|
||||||
n8n,
|
n8n,
|
||||||
}) => {
|
}) => {
|
||||||
// Add a schedule trigger node (activatable)
|
|
||||||
await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME);
|
await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
await n8n.canvas.saveWorkflow();
|
await n8n.canvas.saveWorkflow();
|
||||||
|
|
||||||
// Verify suggested actions button is not visible
|
await expect(n8n.canvas.getProductionChecklistButton()).toBeHidden();
|
||||||
await expect(getSuggestedActionsButton(n8n.page)).toBeHidden();
|
|
||||||
|
|
||||||
// Activate the workflow
|
|
||||||
await n8n.canvas.activateWorkflow();
|
await n8n.canvas.activateWorkflow();
|
||||||
|
|
||||||
// Activation Modal should be visible since it's first activation
|
await expect(n8n.workflowActivationModal.getModal()).toBeVisible();
|
||||||
await closeActivationModal(n8n.page);
|
await n8n.workflowActivationModal.close();
|
||||||
|
|
||||||
// Verify suggested actions button and popover is visible
|
await expect(n8n.canvas.getProductionChecklistButton()).toBeVisible();
|
||||||
await expect(getSuggestedActionsButton(n8n.page)).toBeVisible();
|
await expect(n8n.canvas.getProductionChecklistPopover()).toBeVisible();
|
||||||
await expect(getSuggestedActionsPopover(n8n.page)).toBeVisible();
|
await expect(n8n.canvas.getProductionChecklistActionItem()).toHaveCount(2);
|
||||||
await expect(getSuggestedActionItem(n8n.page)).toHaveCount(2);
|
await expect(n8n.canvas.getErrorActionItem()).toBeVisible();
|
||||||
await expect(getErrorActionItem(n8n.page)).toBeVisible();
|
await expect(n8n.canvas.getTimeSavedActionItem()).toBeVisible();
|
||||||
await expect(getTimeSavedActionItem(n8n.page)).toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should display evaluations action when AI node exists and feature is enabled', async ({
|
test('should display evaluations action when AI node exists and feature is enabled', async ({
|
||||||
n8n,
|
n8n,
|
||||||
api,
|
api,
|
||||||
}) => {
|
}) => {
|
||||||
// Enable evaluations feature
|
|
||||||
await api.enableFeature('evaluation');
|
await api.enableFeature('evaluation');
|
||||||
|
|
||||||
// Add schedule trigger and AI node
|
|
||||||
await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME);
|
await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
await n8n.canvas.addNodeAndCloseNDV('OpenAI', 'Create an assistant');
|
await n8n.canvas.addNodeAndCloseNDV('OpenAI', 'Create an assistant');
|
||||||
|
|
||||||
@@ -58,56 +41,50 @@ test.describe('Workflow Production Checklist', () => {
|
|||||||
|
|
||||||
await n8n.canvas.saveWorkflow();
|
await n8n.canvas.saveWorkflow();
|
||||||
await n8n.canvas.activateWorkflow();
|
await n8n.canvas.activateWorkflow();
|
||||||
await closeActivationModal(n8n.page);
|
await expect(n8n.workflowActivationModal.getModal()).toBeVisible();
|
||||||
|
await n8n.workflowActivationModal.close();
|
||||||
|
|
||||||
// Suggested actions should be open
|
await expect(n8n.canvas.getProductionChecklistPopover()).toBeVisible();
|
||||||
await expect(getSuggestedActionsPopover(n8n.page)).toBeVisible();
|
await expect(n8n.canvas.getProductionChecklistActionItem()).toHaveCount(3);
|
||||||
await expect(getSuggestedActionItem(n8n.page)).toHaveCount(3);
|
|
||||||
|
|
||||||
// Verify evaluations action is present
|
await expect(n8n.canvas.getEvaluationsActionItem()).toBeVisible();
|
||||||
await expect(getEvaluationsActionItem(n8n.page)).toBeVisible();
|
await n8n.canvas.getEvaluationsActionItem().click();
|
||||||
await getEvaluationsActionItem(n8n.page).click();
|
|
||||||
|
|
||||||
// Verify navigation to evaluations page
|
|
||||||
await expect(n8n.page).toHaveURL(/\/evaluation/);
|
await expect(n8n.page).toHaveURL(/\/evaluation/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should open workflow settings modal when error workflow action is clicked', async ({
|
test('should open workflow settings modal when error workflow action is clicked', async ({
|
||||||
n8n,
|
n8n,
|
||||||
}) => {
|
}) => {
|
||||||
// Add schedule trigger
|
|
||||||
await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME);
|
await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
await n8n.canvas.saveWorkflow();
|
await n8n.canvas.saveWorkflow();
|
||||||
await n8n.canvas.activateWorkflow();
|
await n8n.canvas.activateWorkflow();
|
||||||
await closeActivationModal(n8n.page);
|
await expect(n8n.workflowActivationModal.getModal()).toBeVisible();
|
||||||
|
await n8n.workflowActivationModal.close();
|
||||||
|
|
||||||
await expect(getSuggestedActionsPopover(n8n.page)).toBeVisible();
|
await expect(n8n.canvas.getProductionChecklistPopover()).toBeVisible();
|
||||||
|
|
||||||
// Click error workflow action
|
const errorAction = n8n.canvas.getErrorActionItem();
|
||||||
const errorAction = getErrorActionItem(n8n.page);
|
|
||||||
await expect(errorAction).toBeVisible();
|
await expect(errorAction).toBeVisible();
|
||||||
await errorAction.click();
|
await errorAction.click();
|
||||||
|
|
||||||
// Verify workflow settings modal opens
|
|
||||||
await expect(n8n.page.getByTestId('workflow-settings-dialog')).toBeVisible();
|
await expect(n8n.page.getByTestId('workflow-settings-dialog')).toBeVisible();
|
||||||
await expect(n8n.page.getByTestId('workflow-settings-error-workflow')).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 }) => {
|
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.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
await n8n.canvas.saveWorkflow();
|
await n8n.canvas.saveWorkflow();
|
||||||
await n8n.canvas.activateWorkflow();
|
await n8n.canvas.activateWorkflow();
|
||||||
await closeActivationModal(n8n.page);
|
await expect(n8n.workflowActivationModal.getModal()).toBeVisible();
|
||||||
|
await n8n.workflowActivationModal.close();
|
||||||
|
|
||||||
await expect(getSuggestedActionsPopover(n8n.page)).toBeVisible();
|
await expect(n8n.canvas.getProductionChecklistPopover()).toBeVisible();
|
||||||
|
|
||||||
// Click time saved action
|
const timeAction = n8n.canvas.getTimeSavedActionItem();
|
||||||
const timeAction = getTimeSavedActionItem(n8n.page);
|
|
||||||
await expect(timeAction).toBeVisible();
|
await expect(timeAction).toBeVisible();
|
||||||
await timeAction.click();
|
await timeAction.click();
|
||||||
|
|
||||||
// Verify workflow settings modal opens
|
|
||||||
await expect(n8n.page.getByTestId('workflow-settings-dialog')).toBeVisible();
|
await expect(n8n.page.getByTestId('workflow-settings-dialog')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,73 +92,64 @@ test.describe('Workflow Production Checklist', () => {
|
|||||||
await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME);
|
await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
await n8n.canvas.saveWorkflow();
|
await n8n.canvas.saveWorkflow();
|
||||||
await n8n.canvas.activateWorkflow();
|
await n8n.canvas.activateWorkflow();
|
||||||
await closeActivationModal(n8n.page);
|
await expect(n8n.workflowActivationModal.getModal()).toBeVisible();
|
||||||
|
await n8n.workflowActivationModal.close();
|
||||||
|
|
||||||
// Suggested actions popover should be open
|
await expect(n8n.canvas.getProductionChecklistPopover()).toBeVisible();
|
||||||
await expect(getSuggestedActionsPopover(n8n.page)).toBeVisible();
|
|
||||||
|
|
||||||
// Verify error workflow action is visible
|
await expect(n8n.canvas.getProductionChecklistActionItem().first()).toContainText('error');
|
||||||
await expect(getSuggestedActionItem(n8n.page).first()).toContainText('error');
|
await n8n.canvas.getProductionChecklistActionItem().first().getByTitle('Ignore').click();
|
||||||
await getSuggestedActionItem(n8n.page).first().getByTitle('Ignore').click();
|
await expect(n8n.canvas.getErrorActionItem()).toBeHidden();
|
||||||
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 n8n.page.locator('body').click({ position: { x: 0, y: 0 } });
|
||||||
await getSuggestedActionsButton(n8n.page).click();
|
await n8n.canvas.clickProductionChecklistButton();
|
||||||
|
|
||||||
// Verify error workflow action is still no longer visible
|
await expect(n8n.canvas.getErrorActionItem()).toBeHidden();
|
||||||
await expect(getErrorActionItem(n8n.page)).toBeHidden();
|
await expect(n8n.canvas.getTimeSavedActionItem()).toBeVisible();
|
||||||
await expect(getTimeSavedActionItem(n8n.page)).toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show completed state for configured actions', async ({ n8n }) => {
|
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.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
await n8n.canvas.saveWorkflow();
|
await n8n.canvas.saveWorkflow();
|
||||||
await n8n.canvas.activateWorkflow();
|
await n8n.canvas.activateWorkflow();
|
||||||
await closeActivationModal(n8n.page);
|
await expect(n8n.workflowActivationModal.getModal()).toBeVisible();
|
||||||
|
await n8n.workflowActivationModal.close();
|
||||||
|
|
||||||
// Open workflow settings and set error workflow
|
await n8n.workflowSettingsModal.open();
|
||||||
await openWorkflowSettings(n8n.page);
|
await expect(n8n.workflowSettingsModal.getModal()).toBeVisible();
|
||||||
|
|
||||||
// Set an error workflow (we'll use a dummy value)
|
await n8n.workflowSettingsModal.selectErrorWorkflow('My workflow');
|
||||||
await n8n.page.getByTestId('workflow-settings-error-workflow').click();
|
await n8n.workflowSettingsModal.clickSave();
|
||||||
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();
|
await expect(n8n.page.getByTestId('workflow-settings-dialog')).toBeHidden();
|
||||||
|
|
||||||
// Open suggested actions
|
await n8n.canvas.clickProductionChecklistButton();
|
||||||
await getSuggestedActionsButton(n8n.page).click();
|
await expect(n8n.canvas.getProductionChecklistPopover()).toBeVisible();
|
||||||
await expect(getSuggestedActionsPopover(n8n.page)).toBeVisible();
|
|
||||||
|
|
||||||
// Verify error workflow action shows as completed
|
|
||||||
await expect(
|
await expect(
|
||||||
getSuggestedActionItem(n8n.page).first().locator('svg[data-icon="circle-check"]'),
|
n8n.canvas
|
||||||
|
.getProductionChecklistActionItem()
|
||||||
|
.first()
|
||||||
|
.locator('svg[data-icon="circle-check"]'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should allow ignoring all actions with confirmation', async ({ n8n }) => {
|
test('should allow ignoring all actions with confirmation', async ({ n8n }) => {
|
||||||
// Add schedule trigger
|
|
||||||
await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME);
|
await n8n.canvas.addNodeAndCloseNDV(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
await n8n.canvas.saveWorkflow();
|
await n8n.canvas.saveWorkflow();
|
||||||
await n8n.canvas.activateWorkflow();
|
await n8n.canvas.activateWorkflow();
|
||||||
await closeActivationModal(n8n.page);
|
await expect(n8n.workflowActivationModal.getModal()).toBeVisible();
|
||||||
|
await n8n.workflowActivationModal.close();
|
||||||
|
|
||||||
// Suggested actions should be open
|
await expect(n8n.canvas.getProductionChecklistPopover()).toBeVisible();
|
||||||
await expect(getSuggestedActionsPopover(n8n.page)).toBeVisible();
|
|
||||||
|
|
||||||
// Click ignore all button
|
await n8n.canvas.clickProductionChecklistIgnoreAll();
|
||||||
await getIgnoreAllButton(n8n.page).click();
|
|
||||||
|
|
||||||
// Confirm in the dialog
|
|
||||||
await expect(n8n.page.locator('.el-message-box')).toBeVisible();
|
await expect(n8n.page.locator('.el-message-box')).toBeVisible();
|
||||||
await n8n.page
|
await n8n.page
|
||||||
.locator('.el-message-box__btns button')
|
.locator('.el-message-box__btns button')
|
||||||
.filter({ hasText: /ignore for all workflows/i })
|
.filter({ hasText: /ignore for all workflows/i })
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
// Verify suggested actions button is no longer visible
|
await expect(n8n.canvas.getProductionChecklistButton()).toBeHidden();
|
||||||
await expect(getSuggestedActionsButton(n8n.page)).toBeHidden();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { test, expect } from '../../fixtures/base';
|
||||||
|
|
||||||
|
test.describe('AI-716 Correctly set up agent model shows error', () => {
|
||||||
|
test('should not show error when adding a sub-node with credential set-up', async ({ n8n }) => {
|
||||||
|
await n8n.goHome();
|
||||||
|
await n8n.workflows.clickAddWorkflowButton();
|
||||||
|
|
||||||
|
await n8n.canvas.addNode('AI Agent');
|
||||||
|
|
||||||
|
await n8n.page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
await n8n.canvas.addNode('OpenAI Chat Model');
|
||||||
|
|
||||||
|
await n8n.credentials.createAndSaveNewCredential('apiKey', 'sk-123');
|
||||||
|
|
||||||
|
await n8n.page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
await expect(n8n.canvas.getNodeIssuesByName('OpenAI Chat Model')).toHaveCount(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { test, expect } from '../../fixtures/base';
|
||||||
|
import type { TestRequirements } from '../../Types';
|
||||||
|
|
||||||
|
const requirements: TestRequirements = {
|
||||||
|
workflow: {
|
||||||
|
'Test_9999_SUG_38.json': 'SUG_38_Test_Workflow',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
test.describe('SUG-38 Inline expression previews are not displayed in NDV', () => {
|
||||||
|
test("should show resolved inline expression preview in NDV if the node's input data is populated", async ({
|
||||||
|
n8n,
|
||||||
|
setupRequirements,
|
||||||
|
}) => {
|
||||||
|
await setupRequirements(requirements);
|
||||||
|
|
||||||
|
await n8n.canvas.clickZoomToFitButton();
|
||||||
|
|
||||||
|
await n8n.workflowComposer.executeWorkflowAndWaitForNotification(
|
||||||
|
'Workflow executed successfully',
|
||||||
|
);
|
||||||
|
|
||||||
|
await n8n.canvas.openNode('Repro1');
|
||||||
|
|
||||||
|
await expect(n8n.ndv.getParameterExpressionPreviewValue()).toBeVisible();
|
||||||
|
|
||||||
|
await expect(n8n.ndv.getParameterExpressionPreviewValue()).toHaveText('hello there');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,26 +1,18 @@
|
|||||||
import { test, expect } from '../../fixtures/base';
|
import { test, expect } from '../../fixtures/base';
|
||||||
|
|
||||||
test('default signin is as owner', async ({ n8n }) => {
|
test.describe('Authentication', () => {
|
||||||
await n8n.goHome();
|
const testCases = [
|
||||||
await expect(n8n.page).toHaveURL(/\/workflow/);
|
{ role: 'default', expectedUrl: /\/workflow/, auth: '' },
|
||||||
});
|
{ role: 'owner', expectedUrl: /\/workflow/, auth: '@auth:owner' },
|
||||||
|
{ role: 'admin', expectedUrl: /\/workflow/, auth: '@auth:admin' },
|
||||||
|
{ role: 'member', expectedUrl: /\/workflow/, auth: '@auth:member' },
|
||||||
|
{ role: 'none', expectedUrl: /\/signin/, auth: '@auth:none' },
|
||||||
|
];
|
||||||
|
|
||||||
test('owner can access dashboard @auth:owner', async ({ n8n }) => {
|
for (const { role, expectedUrl, auth } of testCases) {
|
||||||
await n8n.goHome();
|
test(`${role} authentication ${auth}`, async ({ n8n }) => {
|
||||||
await expect(n8n.page).toHaveURL(/\/workflow/);
|
await n8n.goHome();
|
||||||
});
|
await expect(n8n.page).toHaveURL(expectedUrl);
|
||||||
|
});
|
||||||
test('admin can access dashboard @auth:admin', async ({ n8n }) => {
|
}
|
||||||
await n8n.goHome();
|
|
||||||
await expect(n8n.page).toHaveURL(/\/workflow/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('member can access dashboard @auth:member', async ({ n8n }) => {
|
|
||||||
await n8n.goHome();
|
|
||||||
await expect(n8n.page).toHaveURL(/\/workflow/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('no auth can not access dashboard @auth:none', async ({ n8n }) => {
|
|
||||||
await n8n.goHome();
|
|
||||||
await expect(n8n.page).toHaveURL(/\/signin/);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { test, expect } from '../../fixtures/base';
|
||||||
|
|
||||||
|
test.describe
|
||||||
|
.serial('Environment Feature Flags', () => {
|
||||||
|
test('should set feature flags at runtime and load it back in envFeatureFlags from backend settings', async ({
|
||||||
|
api,
|
||||||
|
}) => {
|
||||||
|
const setResponse = await api.setEnvFeatureFlags({
|
||||||
|
N8N_ENV_FEAT_TEST: 'true',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(setResponse.data.success).toBe(true);
|
||||||
|
expect(setResponse.data.message).toBe('Environment feature flags updated');
|
||||||
|
|
||||||
|
expect(setResponse.data.flags).toBeInstanceOf(Object);
|
||||||
|
expect(setResponse.data.flags['N8N_ENV_FEAT_TEST']).toBe('true');
|
||||||
|
|
||||||
|
const currentFlags = await api.getEnvFeatureFlags();
|
||||||
|
|
||||||
|
expect(currentFlags).toBeInstanceOf(Object);
|
||||||
|
expect(currentFlags.data['N8N_ENV_FEAT_TEST']).toBe('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should reset feature flags at runtime', async ({ api }) => {
|
||||||
|
const setResponse1 = await api.setEnvFeatureFlags({
|
||||||
|
N8N_ENV_FEAT_TEST: 'true',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(setResponse1.data.success).toBe(true);
|
||||||
|
expect(setResponse1.data.flags['N8N_ENV_FEAT_TEST']).toBe('true');
|
||||||
|
|
||||||
|
const clearResponse = await api.clearEnvFeatureFlags();
|
||||||
|
|
||||||
|
expect(clearResponse.data.success).toBe(true);
|
||||||
|
|
||||||
|
expect(clearResponse.data.flags).toBeInstanceOf(Object);
|
||||||
|
expect(clearResponse.data.flags['N8N_ENV_FEAT_TEST']).toBeUndefined();
|
||||||
|
|
||||||
|
const currentFlags = await api.getEnvFeatureFlags();
|
||||||
|
|
||||||
|
expect(currentFlags).toBeInstanceOf(Object);
|
||||||
|
expect(currentFlags.data['N8N_ENV_FEAT_TEST']).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import { expect, test } from '../../fixtures/base';
|
import { expect, test } from '../../fixtures/base';
|
||||||
|
|
||||||
// Example of importing a workflow from a file
|
|
||||||
test.describe('PDF Test', () => {
|
test.describe('PDF Test', () => {
|
||||||
// eslint-disable-next-line playwright/no-skipped-test
|
test('Can read and write PDF files and extract text', async ({ n8n }) => {
|
||||||
test.skip('Can read and write PDF files and extract text', async ({ n8n }) => {
|
|
||||||
await n8n.goHome();
|
await n8n.goHome();
|
||||||
await n8n.workflows.clickAddWorkflowButton();
|
await n8n.workflows.clickAddWorkflowButton();
|
||||||
await n8n.canvas.importWorkflow('test_pdf_workflow.json', 'PDF Workflow');
|
await n8n.canvas.importWorkflow('test_pdf_workflow.json', 'PDF Workflow');
|
||||||
|
|||||||
@@ -44,5 +44,6 @@ const playwrightRoot = findProjectRoot('playwright.config.ts');
|
|||||||
* @returns An absolute path to the file or directory.
|
* @returns An absolute path to the file or directory.
|
||||||
*/
|
*/
|
||||||
export function resolveFromRoot(...pathSegments: string[]): string {
|
export function resolveFromRoot(...pathSegments: string[]): string {
|
||||||
|
// eslint-disable-next-line n8n-local-rules/no-argument-spread
|
||||||
return path.join(playwrightRoot, ...pathSegments);
|
return path.join(playwrightRoot, ...pathSegments);
|
||||||
}
|
}
|
||||||
|
|||||||
70
packages/testing/playwright/utils/requirements.ts
Normal file
70
packages/testing/playwright/utils/requirements.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import type { Page, BrowserContext } from '@playwright/test';
|
||||||
|
|
||||||
|
import { setContextSettings } from '../config/intercepts';
|
||||||
|
import { n8nPage } from '../pages/n8nPage';
|
||||||
|
import { ApiHelpers } from '../services/api-helper';
|
||||||
|
import { TestError, type TestRequirements } from '../Types';
|
||||||
|
|
||||||
|
export async function setupTestRequirements(
|
||||||
|
page: Page,
|
||||||
|
context: BrowserContext,
|
||||||
|
requirements: TestRequirements,
|
||||||
|
): Promise<void> {
|
||||||
|
const n8n = new n8nPage(page);
|
||||||
|
const api = new ApiHelpers(context.request);
|
||||||
|
|
||||||
|
// 1. Setup frontend settings override
|
||||||
|
if (requirements.config?.settings) {
|
||||||
|
// Store settings for this context
|
||||||
|
setContextSettings(context, requirements.config.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Setup feature flags
|
||||||
|
if (requirements.config?.features) {
|
||||||
|
for (const [feature, enabled] of Object.entries(requirements.config.features)) {
|
||||||
|
if (enabled) {
|
||||||
|
await api.enableFeature(feature);
|
||||||
|
} else {
|
||||||
|
await api.disableFeature(feature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Setup API intercepts
|
||||||
|
if (requirements.intercepts) {
|
||||||
|
for (const [name, config] of Object.entries(requirements.intercepts)) {
|
||||||
|
await page.route(config.url, async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: config.status ?? 200,
|
||||||
|
contentType: config.contentType ?? 'application/json',
|
||||||
|
body:
|
||||||
|
typeof config.response === 'string' ? config.response : JSON.stringify(config.response),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Setup workflows
|
||||||
|
if (requirements.workflow) {
|
||||||
|
for (const [name, workflowData] of Object.entries(requirements.workflow)) {
|
||||||
|
try {
|
||||||
|
// Import workflow using the n8n page object
|
||||||
|
await n8n.goHome();
|
||||||
|
await n8n.workflows.clickAddWorkflowButton();
|
||||||
|
await n8n.canvas.importWorkflow(name, workflowData);
|
||||||
|
} catch (error) {
|
||||||
|
throw new TestError(`Failed to create workflow ${name}: ${String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Setup browser storage
|
||||||
|
if (requirements.storage) {
|
||||||
|
await context.addInitScript((storage) => {
|
||||||
|
// Set localStorage items
|
||||||
|
for (const [key, value] of Object.entries(storage)) {
|
||||||
|
window.localStorage.setItem(key, value);
|
||||||
|
}
|
||||||
|
}, requirements.storage);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
packages/testing/playwright/workflows/Manual_wait_set.json
Normal file
72
packages/testing/playwright/workflows/Manual_wait_set.json
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"name": "Manual wait set",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"amount": 2,
|
||||||
|
"unit": "seconds"
|
||||||
|
},
|
||||||
|
"id": "ed6e0168-1145-43d0-9082-970b8a8f3cb5",
|
||||||
|
"name": "Wait",
|
||||||
|
"type": "n8n-nodes-base.wait",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [900, 580],
|
||||||
|
"webhookId": "0f6f94a4-c28d-46f9-8468-6ab315a9fec9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "59467b99-4e7c-4f19-8fc2-4329788f0951",
|
||||||
|
"name": "Manual",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [680, 580]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "6ddf089f-4d01-4691-928f-6de168e3b089",
|
||||||
|
"name": "Set",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [1120, 580]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pinData": {},
|
||||||
|
"connections": {
|
||||||
|
"Wait": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Set",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Manual": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Wait",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": false,
|
||||||
|
"settings": {
|
||||||
|
"saveExecutionProgress": true,
|
||||||
|
"saveManualExecutions": true,
|
||||||
|
"callerPolicy": "workflowsFromSameOwner"
|
||||||
|
},
|
||||||
|
"versionId": "f11ff1bf-4273-46cb-bbec-65c7b2fa13cb",
|
||||||
|
"id": "1037",
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "8a47b83b4479b11330fdf21ccc96d4a8117035a968612e452b4c87bfd09c16c7"
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
80
packages/testing/playwright/workflows/Test_9999_SUG_38.json
Normal file
80
packages/testing/playwright/workflows/Test_9999_SUG_38.json
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [-240, 180],
|
||||||
|
"id": "cd9b8124-567e-43d9-b4d1-638b111cd049",
|
||||||
|
"name": "When clicking ‘Execute workflow’"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "3a40d9f2-0eed-4a92-9287-9d6ec9ce90e8",
|
||||||
|
"name": "message",
|
||||||
|
"value": "hello there",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [-20, 180],
|
||||||
|
"id": "6e58ae14-4851-4e9d-9465-4155b6e2f278",
|
||||||
|
"name": "Edit Fields1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "9e957377-c5f2-4254-89d8-334d32a8cfb6",
|
||||||
|
"name": "test",
|
||||||
|
"value": "={{ $json.message }}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [200, 180],
|
||||||
|
"id": "c4e9d792-51e9-4296-ba66-afac3cf378dd",
|
||||||
|
"name": "Repro1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"When clicking ‘Execute workflow’": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Edit Fields1",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Edit Fields1": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Repro1",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pinData": {},
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "cdc3bfdf3e6244f221ab6e71b2115a631406ae45a034bfca5e9731cf64f4eb64"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "bd816131-d8ad-4b4c-90d6-59fdab2e6307",
|
||||||
|
"name": "Set",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [720, 460]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "23fc3930-b8f9-41d9-89db-b647291a2201",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "82fe0f6c-854a-4eb9-b311-d7b43025c047",
|
||||||
|
"name": "Webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [460, 460],
|
||||||
|
"webhookId": "23fc3930-b8f9-41d9-89db-b647291a2201"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Set",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pinData": {
|
||||||
|
"Webhook": [
|
||||||
|
{
|
||||||
|
"headers": {
|
||||||
|
"host": "localhost:5678",
|
||||||
|
"content-length": "37",
|
||||||
|
"accept": "*/*",
|
||||||
|
"content-type": "application/json",
|
||||||
|
"accept-encoding": "gzip"
|
||||||
|
},
|
||||||
|
"params": {},
|
||||||
|
"query": {},
|
||||||
|
"body": {
|
||||||
|
"here": "be",
|
||||||
|
"dragons": true
|
||||||
|
},
|
||||||
|
"webhookUrl": "http://localhost:5678/webhook-test/23fc3930-b8f9-41d9-89db-b647291a2201",
|
||||||
|
"executionMode": "test"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "ebfced75-2ce1-4c41-a971-6c3b83522c4d",
|
||||||
|
"name": "When clicking 'Execute workflow'",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [360, 220]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"errorMessage": "This is an error message"
|
||||||
|
},
|
||||||
|
"id": "f2e60459-401a-49d5-acfc-7b2b31cfdcf7",
|
||||||
|
"name": "Stop and Error",
|
||||||
|
"type": "n8n-nodes-base.stopAndError",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [1020, 220]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "// Loop over input items and add a new field 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": "b54d4db9-b257-41a8-862f-26d293115bad",
|
||||||
|
"name": "Code",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [840, 320]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "053ada73-f7db-4e6a-8cc8-85756cc6ca4e",
|
||||||
|
"name": "age",
|
||||||
|
"value": "={{ 32asd }}",
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "5fd89612-a871-4679-b7b0-d659e09c6a0e",
|
||||||
|
"name": "Edit Fields",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [600, 100]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"When clicking 'Execute workflow'": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Stop and Error",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Code",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Edit Fields",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pinData": {}
|
||||||
|
}
|
||||||
743
pnpm-lock.yaml
generated
743
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user