first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,292 @@
import {
simpleAssistantResponse,
aiDisabledRequirements,
aiEnabledRequirements,
aiEnabledWithWorkflowRequirements,
aiEnabledWithQuickRepliesRequirements,
aiEnabledWithEndSessionRequirements,
aiEnabledWithSimpleChatRequirements,
} from '../../../config/ai-assistant-fixtures';
import { SCHEDULE_TRIGGER_NODE_NAME } from '../../../config/constants';
import { test, expect } from '../../../fixtures/base';
type ChatRequestBody = {
payload?: {
type?: string;
text?: string;
question?: string;
context?: Record<string, unknown>;
};
};
test.describe('AI Assistant::disabled', {
annotation: [
{ type: 'owner', description: 'AI' },
],
}, () => {
test('does not show assistant button if feature is disabled', async ({
n8n,
setupRequirements,
}) => {
await setupRequirements(aiDisabledRequirements);
await n8n.page.goto('/workflow/new');
await expect(n8n.canvas.canvasPane()).toBeVisible();
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().first()).toContainText(
'Hey, this is an assistant message',
);
});
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);
});
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',
);
});
test('should reset session after it ended and sidebar is closed', async ({
n8n,
setupRequirements,
}) => {
await setupRequirements(aiEnabledRequirements);
await n8n.page.goto('/workflow/new');
await n8n.page.route('**/rest/ai/chat', async (route) => {
const requestBody = route.request().postDataJSON() as ChatRequestBody;
const isInit = requestBody.payload?.type === 'init-support-chat';
const response = isInit
? simpleAssistantResponse
: {
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',
},
],
};
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(response),
});
});
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
await n8n.aiAssistant.sendMessage('Hello', 'enter-key');
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(2);
await n8n.aiAssistant.getCloseChatButton().click();
// Wait for sidebar to close
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeHidden();
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(2);
await n8n.aiAssistant.sendMessage('Thanks, bye', 'enter-key');
await expect(n8n.aiAssistant.getChatMessagesSystem()).toHaveCount(1);
await expect(n8n.aiAssistant.getChatMessagesSystem().first()).toContainText(
'session has ended',
);
await n8n.aiAssistant.getCloseChatButton().click();
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
await expect(n8n.aiAssistant.getPlaceholderMessage()).toBeVisible();
});
test('should not reset assistant session when workflow is saved', async ({
n8n,
setupRequirements,
}) => {
await setupRequirements(aiEnabledWithSimpleChatRequirements);
await n8n.page.goto('/workflow/new');
await n8n.canvas.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
await n8n.ndv.clickBackToCanvasButton();
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
await n8n.aiAssistant.sendMessage('Hello', 'enter-key');
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
await n8n.canvas.openNode(SCHEDULE_TRIGGER_NODE_NAME);
await n8n.ndv.execute();
await expect(n8n.aiAssistant.getPlaceholderMessage()).toHaveCount(0);
});
test('should send message via shift + enter even with global NodeCreator panel opened', async ({
n8n,
setupRequirements,
}) => {
await setupRequirements(aiEnabledWithSimpleChatRequirements);
await n8n.page.goto('/workflow/new');
await n8n.canvas.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
await n8n.ndv.clickBackToCanvasButton();
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
await n8n.canvas.nodeCreator.open();
await n8n.aiAssistant.sendMessage('Hello', 'enter-key');
await expect(n8n.aiAssistant.getPlaceholderMessage()).toHaveCount(0);
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
});
});
@@ -0,0 +1,101 @@
import {
codeDiffSuggestionResponse,
applyCodeDiffResponse,
nodeExecutionSucceededResponse,
aiEnabledWorkflowBaseRequirements,
aiEnabledWithCodeDiffRequirements,
} from '../../../config/ai-assistant-fixtures';
import { test, expect } from '../../../fixtures/base';
test.describe('AI Assistant::enabled', {
annotation: [
{ type: 'owner', description: 'AI' },
],
}, () => {
test.describe('Code Node Error Help', () => {
test('should apply code diff to code node', async ({ n8n, setupRequirements }) => {
await setupRequirements(aiEnabledWithCodeDiffRequirements);
let applySuggestionCalls = 0;
await n8n.page.route('**/rest/ai/chat/apply-suggestion', async (route) => {
applySuggestionCalls += 1;
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(applyCodeDiffResponse),
});
});
await n8n.canvas.openNode('Code');
await n8n.ndv.execute();
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(2);
await expect(n8n.aiAssistant.getCodeDiffs()).toHaveCount(1);
await expect(n8n.aiAssistant.getApplyCodeDiffButtons()).toHaveCount(1);
await n8n.aiAssistant.getApplyCodeDiffButtons().first().click();
await expect(n8n.aiAssistant.getApplyCodeDiffButtons()).toHaveCount(0);
await expect(n8n.aiAssistant.getUndoReplaceCodeButtons()).toHaveCount(1);
await expect(n8n.aiAssistant.getCodeReplacedMessage()).toBeVisible();
await expect(n8n.ndv.getCodeEditor()).toContainText('item.json.myNewField = 1');
await n8n.aiAssistant.getUndoReplaceCodeButtons().first().click();
await expect(n8n.aiAssistant.getApplyCodeDiffButtons()).toHaveCount(1);
await expect(n8n.aiAssistant.getCodeReplacedMessage()).toHaveCount(0);
expect(applySuggestionCalls).toBe(1);
await expect(n8n.ndv.getCodeEditor()).toContainText('item.json.myNewField = 1aaa');
await n8n.aiAssistant.getApplyCodeDiffButtons().first().click();
await expect(n8n.ndv.getCodeEditor()).toContainText('item.json.myNewField = 1');
});
test('should ignore node execution success and error messages after the node run successfully once', async ({
n8n,
setupRequirements,
}) => {
await setupRequirements(aiEnabledWorkflowBaseRequirements);
let chatRequestCount = 0;
await n8n.page.route('**/rest/ai/chat', async (route) => {
chatRequestCount += 1;
const response =
chatRequestCount === 1 ? codeDiffSuggestionResponse : nodeExecutionSucceededResponse;
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(response),
});
});
await n8n.canvas.openNode('Code');
await n8n.ndv.execute();
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
await n8n.ndv
.getCodeEditor()
.fill(
"// 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();",
);
await n8n.ndv.execute();
await n8n.ndv
.getCodeEditor()
.fill(
"// 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 = 1aaaa!;\n}\n\nreturn $input.all();",
);
await n8n.ndv.execute();
await expect(n8n.aiAssistant.getChatMessagesAssistant().nth(2)).toContainText(
'Code node ran successfully, did my solution help resolve your issue?',
);
});
});
});
@@ -0,0 +1,162 @@
import {
aiEnabledRequirements,
aiEnabledWithSimpleChatRequirements,
} from '../../../config/ai-assistant-fixtures';
import {
GMAIL_NODE_NAME,
MANUAL_TRIGGER_NODE_NAME,
SCHEDULE_TRIGGER_NODE_NAME,
} from '../../../config/constants';
import { test, expect } from '../../../fixtures/base';
test.describe(
'AI Assistant::enabled',
{
annotation: [{ type: 'owner', description: 'AI' }],
},
() => {
test.describe('Credential Help', () => {
test('should start credential help from node credential', async ({
n8n,
setupRequirements,
}) => {
await setupRequirements(aiEnabledWithSimpleChatRequirements);
await n8n.page.goto('/workflow/new');
await n8n.canvas.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
await n8n.ndv.clickBackToCanvasButton();
await n8n.canvas.addNode(GMAIL_NODE_NAME, { action: 'Get many messages', closeNDV: false });
await n8n.ndv.clickCreateNewCredential();
await expect(n8n.canvas.credentialModal.getModal()).toBeVisible();
const assistantButton = n8n.aiAssistant
.getCredentialEditAssistantButton()
.locator('button');
await expect(assistantButton).toBeVisible();
await assistantButton.click();
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
await expect(n8n.aiAssistant.getChatMessagesUser().first()).toContainText(
'How do I set up the credentials for Gmail OAuth2 API?',
);
await expect(n8n.aiAssistant.getChatMessagesAssistant().first()).toContainText(
'Hey, this is an assistant message',
);
await expect(assistantButton).toBeDisabled();
});
test('should start credential help from credential list', async ({
n8n,
setupRequirements,
}) => {
await setupRequirements(aiEnabledWithSimpleChatRequirements);
await n8n.navigate.toCredentials();
await n8n.workflows.addResource.credential();
await n8n.credentials.selectCredentialType('Notion API');
const assistantButton = n8n.aiAssistant
.getCredentialEditAssistantButton()
.locator('button');
await expect(assistantButton).toBeVisible();
await assistantButton.click();
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
await expect(n8n.aiAssistant.getChatMessagesUser().first()).toContainText(
'How do I set up the credentials for Notion API?',
);
await expect(n8n.aiAssistant.getChatMessagesAssistant().first()).toContainText(
'Hey, this is an assistant message',
);
await expect(assistantButton).toBeDisabled();
});
test('should not show assistant button if click to connect', async ({
n8n,
setupRequirements,
}) => {
await setupRequirements(aiEnabledRequirements);
await n8n.page.route('**/types/credentials.json', async (route) => {
const response = await route.fetch();
const credentials = (await response.json()) as Array<
{ name?: string } & Record<string, unknown>
>;
const index = credentials.findIndex((c) => c.name === 'slackOAuth2Api');
if (index >= 0) {
credentials[index] = {
...credentials[index],
__overwrittenProperties: ['clientId', 'clientSecret'],
};
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(credentials),
});
});
await n8n.page.goto('/workflow/new');
await n8n.canvas.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.addNode('Slack', { action: 'Get a channel' });
await n8n.ndv.clickCreateNewCredential();
// Default is managed OAuth (click to connect) — no assistant button
await expect(n8n.canvas.credentialModal.oauthConnectButton).toHaveCount(1);
await expect(n8n.canvas.credentialModal.getCredentialInputs()).toHaveCount(2);
await expect(n8n.aiAssistant.getCredentialEditAssistantButton()).toHaveCount(0);
// Switch to custom OAuth via dropdown — assistant button should appear
await n8n.canvas.credentialModal.selectAuthTypeFromDropdown('Custom OAuth2');
await expect(n8n.canvas.credentialModal.getCredentialInputs()).toHaveCount(4);
await expect(n8n.aiAssistant.getCredentialEditAssistantButton()).toHaveCount(1);
});
test('should not show assistant button when click to connect with some fields', async ({
n8n,
setupRequirements,
}) => {
await setupRequirements(aiEnabledRequirements);
await n8n.page.route('**/types/credentials.json', async (route) => {
const response = await route.fetch();
const credentials = (await response.json()) as Array<
{ name?: string } & Record<string, unknown>
>;
const index = credentials.findIndex((c) => c.name === 'microsoftOutlookOAuth2Api');
if (index >= 0) {
credentials[index] = {
...credentials[index],
__overwrittenProperties: [
'authUrl',
'accessTokenUrl',
'clientId',
'clientSecret',
'graphApiBaseUrl',
],
};
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(credentials),
});
});
await n8n.page.goto('/workflow/new');
await n8n.canvas.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.addNode('Microsoft Outlook', { action: 'Get a calendar' });
await n8n.ndv.clickCreateNewCredential();
await expect(n8n.canvas.credentialModal.oauthConnectButton).toHaveCount(1);
await expect(n8n.canvas.credentialModal.getCredentialInputs()).toHaveCount(2);
await expect(n8n.aiAssistant.getCredentialEditAssistantButton()).toHaveCount(0);
});
});
},
);
@@ -0,0 +1,118 @@
import {
simpleAssistantResponse,
aiEnabledWithCodeSnippetRequirements,
aiEnabledWithHttpWorkflowRequirements,
} from '../../../config/ai-assistant-fixtures';
import { HTTP_REQUEST_NODE_NAME } from '../../../config/constants';
import { test, expect } from '../../../fixtures/base';
type ChatRequestBody = {
payload?: {
type?: string;
text?: string;
question?: string;
context?: Record<string, unknown>;
};
};
test.describe('AI Assistant::enabled', {
annotation: [
{ type: 'owner', description: 'AI' },
],
}, () => {
test.describe('Support Chat', () => {
test('assistant returns code snippet', async ({ n8n, setupRequirements }) => {
await setupRequirements(aiEnabledWithCodeSnippetRequirements);
await n8n.page.goto('/workflow/new');
await expect(n8n.aiAssistant.getAskAssistantCanvasActionButton()).toBeVisible();
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeVisible();
await n8n.aiAssistant.sendMessage('Show me an expression');
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(3);
await expect(n8n.aiAssistant.getChatMessagesUser().first()).toContainText(
'Show me an expression',
);
await expect(n8n.aiAssistant.getChatMessagesAssistant().first()).toContainText(
'To use expressions in n8n, follow these steps:',
);
await expect(n8n.aiAssistant.getChatMessagesAssistant().first()).toContainText('New York');
await expect(n8n.aiAssistant.getCodeSnippet()).toHaveText('{{$json.body.city}}');
});
test('should send current context to support chat', async ({ n8n, setupRequirements }) => {
await setupRequirements(aiEnabledWithHttpWorkflowRequirements);
const chatRequests: ChatRequestBody[] = [];
await n8n.page.route('**/rest/ai/chat', async (route) => {
const body = route.request().postDataJSON() as ChatRequestBody;
chatRequests.push(body);
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(simpleAssistantResponse),
});
});
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
await n8n.aiAssistant.sendMessage('What is wrong with this workflow?');
const supportRequest = chatRequests.find(
(request) => request.payload?.question === 'What is wrong with this workflow?',
);
expect(supportRequest).toBeDefined();
const supportContext = supportRequest?.payload?.context;
expect(supportContext).toBeDefined();
expect(supportContext?.currentView).toBeDefined();
expect(supportContext?.currentWorkflow).toBeDefined();
});
test('should not send workflow context if nothing changed', async ({
n8n,
setupRequirements,
}) => {
await setupRequirements(aiEnabledWithHttpWorkflowRequirements);
const chatRequests: ChatRequestBody[] = [];
await n8n.page.route('**/rest/ai/chat', async (route) => {
const body = route.request().postDataJSON() as ChatRequestBody;
chatRequests.push(body);
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(simpleAssistantResponse),
});
});
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
await n8n.aiAssistant.sendMessage('What is wrong with this workflow?', 'enter-key');
// Wait for message to be processed
await expect(n8n.aiAssistant.getChatMessagesAssistant()).toHaveCount(1);
await n8n.aiAssistant.sendMessage('And now?', 'enter-key');
await expect(n8n.aiAssistant.getChatMessagesAssistant()).toHaveCount(2);
const secondRequest = chatRequests.find((request) => request.payload?.text === 'And now?');
const secondContext = secondRequest?.payload?.context;
expect(secondContext?.currentWorkflow).toBeUndefined();
await n8n.canvas.openNode(HTTP_REQUEST_NODE_NAME);
await n8n.ndv.setParameterInputValue('url', 'https://example.com');
await n8n.ndv.close();
await n8n.canvas.clickExecuteWorkflowButton();
await n8n.aiAssistant.sendMessage('What about now?', 'enter-key');
await expect(n8n.aiAssistant.getChatMessagesAssistant()).toHaveCount(3);
const thirdRequest = chatRequests.find(
(request) => request.payload?.text === 'What about now?',
);
const thirdContext = thirdRequest?.payload?.context;
expect(thirdContext?.currentWorkflow).toBeTruthy();
expect(thirdContext?.executionData).toBeTruthy();
});
});
});
@@ -0,0 +1,39 @@
import { test, expect } from '../../../fixtures/base';
test.describe('Chat session ID reset', {
annotation: [
{ type: 'owner', description: 'AI' },
],
}, () => {
test.beforeEach(async ({ n8n }) => {
await n8n.start.fromImportedWorkflow('Test_chat_partial_execution.json');
await n8n.notifications.quickCloseAll();
await n8n.canvas.clickZoomToFitButton();
await n8n.canvas.deselectAll();
});
test('should update session ID in node output when session is reset', async ({ n8n }) => {
await n8n.canvas.logsPanel.open();
await n8n.canvas.logsPanel.sendManualChatMessage('Test message 1');
await expect(n8n.canvas.logsPanel.getManualChatMessages()).toHaveCount(2);
const initialSessionId = await n8n.canvas.logsPanel.getSessionId(n8n.clipboard);
await n8n.canvas.logsPanel.clickLogEntryAtRow(0);
await expect(n8n.canvas.logsPanel.outputPanel.getTbodyCell(0, 1)).toContainText(
initialSessionId,
);
await n8n.canvas.logsPanel.refreshSession();
await expect(n8n.canvas.logsPanel.getManualChatMessages()).not.toBeAttached();
// Step 5: Get the new session ID
const newSessionId = await n8n.canvas.logsPanel.getSessionId(n8n.clipboard);
expect(newSessionId).not.toEqual(initialSessionId);
await n8n.canvas.logsPanel.sendManualChatMessage('Test message 2');
await expect(n8n.canvas.logsPanel.getManualChatMessages()).toHaveCount(2);
// Verify the NEW session ID in the output panel matches the chat header session ID
await expect(n8n.canvas.logsPanel.outputPanel.getTbodyCell(0, 1)).toContainText(newSessionId);
});
});
@@ -0,0 +1,123 @@
import { expect, test } from '../../../fixtures/base';
test.use({ capability: 'proxy' });
test.describe(
'Evaluations @capability:proxy',
{
annotation: [{ type: 'owner', description: 'AI' }],
},
() => {
test.beforeEach(async ({ n8n, services }) => {
await services.proxy.clearAllExpectations();
await n8n.goHome();
});
// @AI team to look at this
test.fixme('should load evaluations workflow and execute twice', async ({ n8n, services }) => {
await services.proxy.loadExpectations('evaluations');
await n8n.api.credentials.createCredentialFromDefinition({
name: 'Test Google Sheets',
type: 'googleApi',
data: {
email: 'email@quickstart-1234.iam.gserviceaccount.com',
// mock private key
privateKey: `-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDx1//AaoSkyHYl
npqS3+uaePYhJXKD/T1h6zGThAUooN7ZzWK46nNcU1vghQMTlPMHfUTbl4xzZxEL
OYjyTPOKpwJvhmy44MU+zTQYJuUaU4dQuOCnnC61CL91Xy+8GJd7PvdUeVRWENWu
zzO825Fxeiy2qnbrOJfhYh+f9znwWM2R8/V6LIp1HSWNBU0h/NCesmVhGwTP2H/P
wGgFPzl9+effW8TgmAukVuZoG+z8pOiqJnZLgTOO++PLyM6UJe560UnAbv0yP4y5
lZ370XwOQ6gVIiB0+8Z2A3tJp6ackfoMfDYbuU+CAhFPqkdvXgbrYciUCr6fzINo
ImK6CcSDAgMBAAECggEAF0+XokdiI7QC11tzUMbuocQZDVbbs+c7/G08KRjnmmPv
NxU599L5baPHTlvj0QZhao5jjbsM2a7MkMVp8tkB/JJehLtzTVq1CHmlFNLi8Geu
ulQnq2A9jEuckMatBjdkmoeWNXlAbM9QmXn1ZbXQThzVpIHH1qJs2Veo7rVYy1bD
+hnzadyeXsHOC518wNAaF3b1UShybI3dlrHbXqqRmkOZP272IKfmvZ2KOcnFC+MT
cWLUGWBTq2YK+UJv09OXHEBnonrm18m2Sku+/PhFwjOiifIK/1MWILss60IB7dFm
7Fe7NAtYQMPZyDEqY5Xo+K4FwWYzfxfHPiJf7k0DqQKBgQD5Rz+HCZC8V5c1oK8/
1hGthyh5JdXxW7C8D1WVuo7W2OHrOJSDXjGhsxMjnKYdq/1YybJl9XpQSvZeumto
YazNiJqAexIlpmEHLW5gDtX3xpM0dujuJudTHYfveugtR8i/EZpWpFKv45/6Rm33
Yt2PaMjLuO7yW0buEjSQInHtHwKBgQD4XW44YujgF+xvMmx8+QyyNI2UNI1ZmnsU
VZLmDAn5+WDz5YtBXN9JGIXIk5279S7xzu9xyq7Ih6uedxE/hmzaHSZ1gl9Xasci
n86FGaGPm6RtEeZ8c68oqha7kddLoBwTPBoZq5NaCCaTh2TQkMPg+Ws3erM0pkyC
fqw1hzkYHQKBgQC2Iv3i3/VV+DXupCqIXRRrkx7abe/FO3aF4jppfXdSugNQR/YT
imZ/PIXWdmXVtk4VasIjx1oIgs1C57kE+qE1SAODrujSg5/Pi71jCFQEh54VLnEB
WYGZ9DDXpRkxxIqEOQtpFQWpqIrCZmWA5Ub3uttEJyrIADNyTfEEA3b0hwKBgHrn
STbQA2t5iz/PlQ4W9GhvRyxzAQu5PXTnj+UVSg6QkKDBE7NJsRjr8LA8FE9B2nRA
sg7+fJWxRYUKaNelvtIEoNZ/qIyKw3Zn3HvTHjcBj1GGDSfC24fk+5Dgb8j1t07x
a/0OAcIIzIYu9v2a1cPLyXnP10STksL0ymVGwEMlAoGBAK2dtYZllhooN/C4ssFW
nmfqICLWEc/UZSxmxau1rOz71GJiiHgXFmQgiZtpf3Qp3wKKtoFkf+sJ6zP2VX35
2tJcTO9lKm6kNa3eaveE/NJrkH5a0IpxrvDT1TvmnapaNEKuGZJAX5BNaggDrfEJ
m82JpEptTfAxFHtd8+Sb0U2G
-----END PRIVATE KEY-----`,
},
});
await n8n.navigate.toWorkflow('new');
// Import the evaluations workflow
await n8n.canvas.importWorkflow('evaluations_loop.json', 'Evaluations');
// Open each node to ensure credentials are set
await n8n.canvas.openNode('When fetching a dataset row');
await n8n.page.keyboard.press('Escape');
// Open each node to ensure credentials are set
await n8n.canvas.openNode('Set outputs');
await n8n.page.keyboard.press('Escape');
// Execute workflow from canvas - first execution
await n8n.canvas.clickExecuteWorkflowButton();
// wait for first run to finish
await n8n.notifications.waitForNotificationAndClose('Successful', { timeout: 10000 });
// wait for second run to finish
await n8n.notifications.waitForNotificationAndClose('Successful', { timeout: 10000 });
// 💡 To update recordings, remove stored expectations, set real credentials above and rerecord here.
// await services.proxy.recordExpectations('evaluations', { host: 'google', dedupe: true });
const batchUpdateRequests = (await services.proxy.getAllRequestsMade()).filter((request) => {
const path = request.httpRequest?.path;
const method = request.httpRequest?.method;
return (
method === 'POST' && typeof path === 'string' && path.endsWith('/values:batchUpdate')
);
});
/**
* Original Table in Google Sheets
* The loop should execute twice over both rows here
* Incrementing each value by 1 (expression in Set Output node)
*
* name email actual
test test 10
hello wolrd 104
*/
// Set output node was called twice in a loop, updating Google sheets output value
expect(batchUpdateRequests.length).toEqual(2);
expect((batchUpdateRequests[0]?.httpRequest?.body as { json: object })?.json).toEqual({
data: [
{
range: 'Sheet2!C2',
values: [[11]],
},
],
valueInputOption: 'RAW',
});
expect((batchUpdateRequests[1]?.httpRequest?.body as { json: object })?.json).toEqual({
data: [
{
range: 'Sheet2!C3',
values: [[105]],
},
],
valueInputOption: 'RAW',
});
});
},
);
@@ -0,0 +1,147 @@
import {
AGENT_NODE_NAME,
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
AI_TOOL_CODE_NODE_NAME,
CHAT_TRIGGER_NODE_DISPLAY_NAME,
HITL_TOOL_SUBCATEGORY,
MANUAL_CHAT_TRIGGER_NODE_NAME,
} from '../../../config/constants';
import { expect, test } from '../../../fixtures/base';
import type { n8nPage } from '../../../pages/n8nPage';
async function addOpenAILanguageModelWithCredentials(
n8n: n8nPage,
parentNode: string,
options: { exactMatch?: boolean; closeNDV?: boolean } = { exactMatch: true, closeNDV: false },
) {
await n8n.canvas.addSupplementalNodeToParent(
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
'ai_languageModel',
parentNode,
options,
);
await n8n.credentialsComposer.createFromNdv({
apiKey: 'abcd',
});
await n8n.ndv.clickBackToCanvasButton();
}
async function waitForWorkflowSuccess(n8n: n8nPage, timeout = 3000) {
await n8n.notifications.waitForNotificationAndClose('Workflow executed successfully', {
timeout,
});
}
async function setEditorText(n8n: n8nPage, parameterName: string, value: string) {
const codeEditor = n8n.ndv.getParameterInput(parameterName).locator('.cm-content');
await codeEditor.click();
await n8n.page.keyboard.press('ControlOrMeta+a');
await n8n.page.keyboard.press('Delete');
await codeEditor.fill(value);
}
const hitlForToolsTestConfig = {
capability: {
services: ['proxy'],
env: {
N8N_COMMUNITY_PACKAGES_ENABLED: 'false',
},
},
} as const;
test.use(hitlForToolsTestConfig);
test.describe('HITL for Tools @capability:proxy', {
annotation: [
{ type: 'owner', description: 'AI' },
],
}, () => {
test.beforeEach(async ({ n8n, services }) => {
await services.proxy.clearAllExpectations();
await services.proxy.loadExpectations('hitl-for-tools');
await n8n.canvas.openNewWorkflow();
});
test('should add a HITL node between Agent and Tool node', async ({ n8n }) => {
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
await addOpenAILanguageModelWithCredentials(n8n, AGENT_NODE_NAME);
await n8n.canvas.addSupplementalNodeToParent(
AI_TOOL_CODE_NODE_NAME,
'ai_tool',
AGENT_NODE_NAME,
{ closeNDV: true },
);
await n8n.canvas.dragNodeToRelativePosition(AI_TOOL_CODE_NODE_NAME, 100, 50);
const specificConnection = n8n.canvas.connectionBetweenNodes(
AI_TOOL_CODE_NODE_NAME,
AGENT_NODE_NAME,
);
await expect(specificConnection).toBeVisible();
// eslint-disable-next-line playwright/no-force-option
await specificConnection.hover({ force: true });
const addNodeButton = n8n.page.getByTestId('add-connection-button');
await expect(addNodeButton).toBeVisible();
await addNodeButton.click();
await n8n.canvas.clickNodeCreatorItemName(MANUAL_CHAT_TRIGGER_NODE_NAME);
await n8n.page.keyboard.press('Escape');
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(5);
await expect(n8n.canvas.nodeConnections()).toHaveCount(4);
});
test('should add a HITL tool node and run it', async ({ n8n }) => {
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
await addOpenAILanguageModelWithCredentials(n8n, AGENT_NODE_NAME);
await n8n.canvas.addSupplementalNodeToParent(
MANUAL_CHAT_TRIGGER_NODE_NAME,
'ai_tool',
AGENT_NODE_NAME,
{ closeNDV: true, subcategory: HITL_TOOL_SUBCATEGORY, exactMatch: true },
);
await n8n.canvas.addSupplementalNodeToParent(
AI_TOOL_CODE_NODE_NAME,
'ai_tool',
MANUAL_CHAT_TRIGGER_NODE_NAME,
{ closeNDV: false },
);
await n8n.ndv.getParameterInput('description').locator('textarea').fill('Send email');
await setEditorText(n8n, 'jsCode', 'return "Email sent";');
await n8n.ndv.setParameterSwitch('specifyInputSchema', true);
await setEditorText(n8n, 'jsonSchemaExample', '{"receiver": "", "body": ""}');
await n8n.ndv.clickBackToCanvasButton();
await n8n.canvas.addNode(MANUAL_CHAT_TRIGGER_NODE_NAME, {
closeNDV: false,
action: 'Send a message',
fromNode: AGENT_NODE_NAME,
});
await n8n.ndv.openExpressionEditorModal('message');
await n8n.ndv.fillExpressionEditorModalInput('{{ $json.output }}');
await n8n.ndv.getExpressionEditorModalOutput().click();
await n8n.page.keyboard.press('Escape');
await n8n.ndv.clickBackToCanvasButton();
await n8n.canvas.openNode(CHAT_TRIGGER_NODE_DISPLAY_NAME);
await n8n.ndv.addParameterOptionByName('Response mode');
await n8n.ndv.selectOptionInParameterDropdown('responseMode', 'Using Response Nodes');
await n8n.ndv.clickBackToCanvasButton();
await n8n.canvas.clickManualChatButton();
await n8n.canvas.logsPanel.sendManualChatMessage('Send welcome email to john@gmail.com');
const approveButton = n8n.page.getByTestId('canvas-chat').getByText('Approve');
await expect(approveButton).toBeVisible({ timeout: 15000 });
await approveButton.click({ button: 'middle' });
await waitForWorkflowSuccess(n8n);
});
});
@@ -0,0 +1,246 @@
import {
AGENT_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME,
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
AI_MEMORY_REDIS_CHAT_NODE_NAME,
AI_TOOL_CALCULATOR_NODE_NAME,
AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME,
AI_TOOL_CODE_NODE_NAME,
AI_TOOL_WIKIPEDIA_NODE_NAME,
SCHEDULE_TRIGGER_NODE_NAME,
TOOL_SUBCATEGORY,
} from '../../../config/constants';
import { test, expect } from '../../../fixtures/base';
import type { n8nPage } from '../../../pages/n8nPage';
// Helper functions for common operations
async function addOpenAILanguageModelWithCredentials(
n8n: n8nPage,
parentNode: string,
options: { exactMatch?: boolean; closeNDV?: boolean } = { exactMatch: true, closeNDV: false },
) {
await n8n.canvas.addSupplementalNodeToParent(
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
'ai_languageModel',
parentNode,
options,
);
await n8n.credentialsComposer.createFromNdv({
apiKey: 'abcd',
});
await n8n.ndv.clickBackToCanvasButton();
}
async function waitForWorkflowSuccess(n8n: n8nPage, timeout = 3000) {
await n8n.notifications.waitForNotificationAndClose('Workflow executed successfully', {
timeout,
});
}
async function executeChatAndWaitForResponse(n8n: n8nPage, message: string) {
await n8n.canvas.logsPanel.sendManualChatMessage(message);
await waitForWorkflowSuccess(n8n);
}
async function verifyChatMessages(n8n: n8nPage, expectedCount: number, inputMessage?: string) {
const messages = n8n.canvas.getManualChatMessages();
await expect(messages).toHaveCount(expectedCount);
if (inputMessage) {
await expect(messages.first()).toContainText(inputMessage);
}
await expect(messages.last()).toBeVisible();
return messages;
}
async function verifyLogsPanelEntries(n8n: n8nPage, expectedEntries: string[]) {
await expect(n8n.canvas.logsPanel.getLogEntries().first()).toBeVisible();
await expect(n8n.canvas.logsPanel.getLogEntries()).toHaveCount(expectedEntries.length);
for (let i = 0; i < expectedEntries.length; i++) {
await expect(n8n.canvas.logsPanel.getLogEntries().nth(i)).toHaveText(expectedEntries[i]);
}
}
async function setupBasicAgentWorkflow(n8n: n8nPage, additionalNodes: string[] = []) {
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
// Add additional nodes if specified
for (const nodeName of additionalNodes) {
await n8n.canvas.addSupplementalNodeToParent(nodeName, 'ai_tool', AGENT_NODE_NAME, {
closeNDV: true,
});
}
// Always add OpenAI Language Model
await addOpenAILanguageModelWithCredentials(n8n, AGENT_NODE_NAME);
}
test.use({ capability: 'proxy' });
test.describe('Langchain Integration @capability:proxy', {
annotation: [
{ type: 'owner', description: 'AI' },
],
}, () => {
test.beforeEach(async ({ n8n, services }) => {
await services.proxy.clearAllExpectations();
await services.proxy.loadExpectations('langchain');
await n8n.canvas.openNewWorkflow();
});
test.describe('Workflow Execution Behavior', () => {
test('should not open chat modal', async ({ n8n }) => {
await n8n.canvas.addNode(EDIT_FIELDS_SET_NODE_NAME, { closeNDV: true });
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
await n8n.canvas.addSupplementalNodeToParent(
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
'ai_languageModel',
AGENT_NODE_NAME,
{ exactMatch: true, closeNDV: true },
);
await n8n.canvas.clickExecuteWorkflowButton();
await expect(n8n.canvas.getManualChatModal()).toBeHidden();
});
test('should remove test workflow button', async ({ n8n }) => {
await n8n.canvas.addNode(SCHEDULE_TRIGGER_NODE_NAME, { closeNDV: true });
await n8n.canvas.addNode(EDIT_FIELDS_SET_NODE_NAME, { closeNDV: true });
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
await n8n.canvas.addSupplementalNodeToParent(
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
'ai_languageModel',
AGENT_NODE_NAME,
{ exactMatch: true, closeNDV: true },
);
await n8n.canvas.disableNodeFromContextMenu(SCHEDULE_TRIGGER_NODE_NAME);
await expect(n8n.canvas.getExecuteWorkflowButton()).toBeHidden();
});
});
test.describe('Node Connection and Configuration', () => {
test('should add nodes to all Agent node input types', async ({ n8n }) => {
const agentSubNodes = [
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
AI_MEMORY_REDIS_CHAT_NODE_NAME,
AI_TOOL_CALCULATOR_NODE_NAME,
AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME,
];
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: false });
await n8n.ndv.checkParameterCheckboxInputByName('hasOutputParser');
await n8n.ndv.clickBackToCanvasButton();
await n8n.canvas.addSupplementalNodeToParent(
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
'ai_languageModel',
AGENT_NODE_NAME,
{ exactMatch: true, closeNDV: true },
);
await n8n.canvas.addSupplementalNodeToParent(
AI_MEMORY_REDIS_CHAT_NODE_NAME,
'ai_memory',
AGENT_NODE_NAME,
{ closeNDV: true },
);
await n8n.canvas.addSupplementalNodeToParent(
AI_TOOL_CALCULATOR_NODE_NAME,
'ai_tool',
AGENT_NODE_NAME,
{ closeNDV: true, subcategory: TOOL_SUBCATEGORY },
);
await n8n.canvas.addSupplementalNodeToParent(
AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME,
'ai_outputParser',
AGENT_NODE_NAME,
{ closeNDV: true },
);
for (const nodeName of agentSubNodes) {
await expect(n8n.canvas.connectionBetweenNodes(nodeName, AGENT_NODE_NAME)).toBeAttached();
}
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2 + agentSubNodes.length); // Chat Trigger + Agent + 4 inputs
});
test('should add multiple tool nodes to Agent node tool input type', async ({ n8n }) => {
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
const tools = [
{ name: AI_TOOL_CALCULATOR_NODE_NAME, subcategory: TOOL_SUBCATEGORY },
{ name: AI_TOOL_CODE_NODE_NAME },
{ name: AI_TOOL_CODE_NODE_NAME },
{ name: AI_TOOL_WIKIPEDIA_NODE_NAME, subcategory: TOOL_SUBCATEGORY },
];
for (const tool of tools) {
await n8n.canvas.addSupplementalNodeToParent(tool.name, 'ai_tool', AGENT_NODE_NAME, {
closeNDV: true,
subcategory: tool.subcategory,
});
await expect(n8n.canvas.connectionBetweenNodes(tool.name, AGENT_NODE_NAME)).toBeAttached();
}
// Chat Trigger + Agent + Tools
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2 + tools.length);
});
});
test.describe('Chat Execution and Interaction', () => {
test('should be able to open and execute Agent node', async ({ n8n }) => {
await setupBasicAgentWorkflow(n8n);
const inputMessage = 'Hello!';
await n8n.canvas.clickManualChatButton();
await executeChatAndWaitForResponse(n8n, inputMessage);
// Verify chat message appears
await expect(n8n.canvas.getManualChatLatestBotMessage()).toBeVisible();
});
test('should add and use Manual Chat Trigger node together with Agent node', async ({
n8n,
}) => {
await setupBasicAgentWorkflow(n8n);
const inputMessage = 'Hello!';
await n8n.canvas.clickManualChatButton();
await executeChatAndWaitForResponse(n8n, inputMessage);
await verifyChatMessages(n8n, 2, inputMessage);
await verifyLogsPanelEntries(n8n, [
'When chat message received',
'AI Agent',
'OpenAI Chat Model',
]);
await n8n.canvas.closeManualChatModal();
await expect(n8n.canvas.logsPanel.getLogEntries()).toBeHidden();
await expect(n8n.canvas.getManualChatInput()).toBeHidden();
});
});
test('should keep the same session when switching tabs', async ({ n8n }) => {
await n8n.start.fromImportedWorkflow('Test_workflow_chat_partial_execution.json');
await n8n.canvas.clickZoomToFitButton();
await n8n.canvas.logsPanel.open();
// Send a message
await n8n.canvas.logsPanel.sendManualChatMessage('Test');
await expect(n8n.canvas.getManualChatLatestBotMessage()).toContainText('this_my_field');
await n8n.canvas.clickExecutionsTab();
await n8n.canvas.clickEditorTab();
await expect(n8n.canvas.getManualChatLatestBotMessage()).toContainText('this_my_field');
// Refresh session
await n8n.canvas.logsPanel.refreshSession();
await expect(n8n.canvas.logsPanel.getManualChatMessages()).not.toBeAttached();
});
});
@@ -0,0 +1,122 @@
import {
AGENT_NODE_NAME,
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
BASIC_LLM_CHAIN_NODE_NAME,
CHAT_TRIGGER_NODE_DISPLAY_NAME,
MANUAL_CHAT_TRIGGER_NODE_NAME,
} from '../../../config/constants';
import { test, expect } from '../../../fixtures/base';
import type { n8nPage } from '../../../pages/n8nPage';
// Helper functions for common operations
async function addOpenAILanguageModelWithCredentials(
n8n: n8nPage,
parentNode: string,
options: { exactMatch?: boolean; closeNDV?: boolean } = { exactMatch: true, closeNDV: false },
) {
await n8n.canvas.addSupplementalNodeToParent(
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
'ai_languageModel',
parentNode,
options,
);
await n8n.credentialsComposer.createFromNdv({
apiKey: 'abcd',
});
await n8n.ndv.clickBackToCanvasButton();
}
async function waitForWorkflowSuccess(n8n: n8nPage, timeout = 3000) {
await n8n.notifications.waitForNotificationAndClose('Workflow executed successfully', {
timeout,
});
}
async function executeChatAndWaitForResponse(n8n: n8nPage, message: string) {
await n8n.canvas.logsPanel.sendManualChatMessage(message);
await waitForWorkflowSuccess(n8n);
}
test.use({ capability: 'proxy' });
test.describe('Langchain Integration @capability:proxy', {
annotation: [
{ type: 'owner', description: 'AI' },
],
}, () => {
test.beforeEach(async ({ n8n, services }) => {
await services.proxy.clearAllExpectations();
await services.proxy.loadExpectations('langchain');
await n8n.canvas.openNewWorkflow();
});
test.describe('Auto-add Behavior', () => {
test('should auto-add chat trigger and basic LLM chain when adding LLM node', async ({
n8n,
}) => {
await n8n.canvas.addNode(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, { closeNDV: true });
await expect(
n8n.canvas.connectionBetweenNodes(
CHAT_TRIGGER_NODE_DISPLAY_NAME,
BASIC_LLM_CHAIN_NODE_NAME,
),
).toBeAttached();
await expect(
n8n.canvas.connectionBetweenNodes(
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
BASIC_LLM_CHAIN_NODE_NAME,
),
).toBeAttached();
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(3);
});
test('should not auto-add nodes if AI nodes are already present', async ({ n8n }) => {
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
await n8n.canvas.addNode(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, { closeNDV: true });
await expect(
n8n.canvas.connectionBetweenNodes(CHAT_TRIGGER_NODE_DISPLAY_NAME, AGENT_NODE_NAME),
).toBeAttached();
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(3);
});
test('should not auto-add nodes if ChatTrigger is already present', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_CHAT_TRIGGER_NODE_NAME, {
closeNDV: true,
trigger: 'On new Chat event',
});
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
await n8n.canvas.addNode(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, { closeNDV: true });
await expect(
n8n.canvas.connectionBetweenNodes(CHAT_TRIGGER_NODE_DISPLAY_NAME, AGENT_NODE_NAME),
).toBeAttached();
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(3);
});
});
test.describe('Chat Execution and Interaction', () => {
test('should be able to open and execute Basic LLM Chain node', async ({ n8n }) => {
await n8n.canvas.addNode(BASIC_LLM_CHAIN_NODE_NAME, { closeNDV: true });
await addOpenAILanguageModelWithCredentials(n8n, BASIC_LLM_CHAIN_NODE_NAME);
await n8n.canvas.openNode(BASIC_LLM_CHAIN_NODE_NAME);
const inputMessage = 'Hello!';
await n8n.ndv.execute();
await executeChatAndWaitForResponse(n8n, inputMessage);
// Verify chat message appears
await expect(n8n.canvas.getManualChatLatestBotMessage()).toBeVisible();
});
});
});
@@ -0,0 +1,181 @@
import {
AGENT_NODE_NAME,
AI_TOOL_CALCULATOR_NODE_NAME,
AI_MEMORY_POSTGRES_NODE_NAME,
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
MANUAL_CHAT_TRIGGER_NODE_NAME,
} from '../../../config/constants';
import { test, expect } from '../../../fixtures/base';
import type { n8nPage } from '../../../pages/n8nPage';
// Helper functions for common operations
async function addOpenAILanguageModelWithCredentials(
n8n: n8nPage,
parentNode: string,
options: { exactMatch?: boolean; closeNDV?: boolean } = { exactMatch: true, closeNDV: false },
) {
await n8n.canvas.addSupplementalNodeToParent(
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
'ai_languageModel',
parentNode,
options,
);
await n8n.credentialsComposer.createFromNdv({
apiKey: 'abcd',
});
await n8n.ndv.clickBackToCanvasButton();
}
async function waitForWorkflowSuccess(n8n: n8nPage, timeout = 3000) {
await n8n.notifications.waitForNotificationAndClose('Workflow executed successfully', {
timeout,
});
}
async function executeChatAndWaitForResponse(n8n: n8nPage, message: string) {
await n8n.canvas.logsPanel.sendManualChatMessage(message);
await waitForWorkflowSuccess(n8n);
}
async function verifyChatMessages(n8n: n8nPage, expectedCount: number, inputMessage?: string) {
const messages = n8n.canvas.getManualChatMessages();
await expect(messages).toHaveCount(expectedCount);
if (inputMessage) {
await expect(messages.first()).toContainText(inputMessage);
}
await expect(messages.last()).toBeVisible();
return messages;
}
test.use({ capability: 'proxy' });
test.describe(
'Langchain Integration @capability:proxy',
{
annotation: [{ type: 'owner', description: 'AI' }],
},
() => {
test.beforeEach(async ({ n8n, services }) => {
await services.proxy.clearAllExpectations();
await services.proxy.loadExpectations('langchain');
await n8n.canvas.openNewWorkflow();
});
// Create a ticket for this for AI team to fix
test.describe('Error Handling and Logs Display', () => {
test.fixme();
// Helper function to set up the agent workflow with Postgres error configuration
async function setupAgentWorkflowWithPostgresError(n8n: n8nPage) {
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
// Add Calculator Tool (required for OpenAI model)
await n8n.canvas.addSupplementalNodeToParent(
AI_TOOL_CALCULATOR_NODE_NAME,
'ai_tool',
AGENT_NODE_NAME,
{ closeNDV: true },
);
// Add and configure Postgres Memory
await n8n.canvas.addSupplementalNodeToParent(
AI_MEMORY_POSTGRES_NODE_NAME,
'ai_memory',
AGENT_NODE_NAME,
{ closeNDV: false },
);
await n8n.credentialsComposer.createFromNdv({
password: 'testtesttest',
});
await n8n.ndv.getParameterInput('sessionIdType').click();
await n8n.page.getByRole('option', { name: 'Define below' }).click();
await n8n.ndv.getParameterInput('sessionKey').locator('input').fill('asdasd');
await n8n.ndv.clickBackToCanvasButton();
// Add and configure OpenAI Language Model
await addOpenAILanguageModelWithCredentials(n8n, AGENT_NODE_NAME);
await n8n.canvas.clickZoomToFitButton();
}
// Helper function to assert logs tab is active
async function assertLogsTabIsActive(n8n: n8nPage) {
await expect(n8n.ndv.getOutputDataContainer()).toBeVisible();
await expect(n8n.ndv.getAiOutputModeToggle()).toBeVisible();
const radioButtons = n8n.ndv.getAiOutputModeToggle().locator('[role="radio"]');
await expect(radioButtons).toHaveCount(2);
await expect(radioButtons.nth(1)).toHaveAttribute('aria-checked', 'true');
}
// Helper function to assert error message is visible
async function assertErrorMessageVisible(n8n: n8nPage) {
await expect(
n8n.ndv.getOutputPanel().getByTestId('node-error-message').first(),
).toBeVisible();
await expect(
n8n.ndv.getOutputPanel().getByTestId('node-error-message').first(),
).toContainText('Error in sub-node');
}
test('should open logs tab by default when there was an error', async ({ n8n }) => {
await setupAgentWorkflowWithPostgresError(n8n);
const inputMessage = 'Test the code tool';
// Execute workflow with chat trigger
await n8n.canvas.clickManualChatButton();
await executeChatAndWaitForResponse(n8n, inputMessage);
// Check that messages and logs are displayed
const messages = await verifyChatMessages(n8n, 2, inputMessage);
await expect(messages.last()).toContainText(
'[ERROR: The service refused the connection - perhaps it is offline]',
);
await expect(n8n.canvas.logsPanel.getLogEntries().first()).toBeVisible();
await expect(n8n.canvas.logsPanel.getLogEntries()).toHaveCount(3);
await expect(n8n.canvas.logsPanel.getSelectedLogEntry()).toHaveText('AI Agent');
await expect(n8n.canvas.logsPanel.outputPanel.get()).toContainText(
AI_MEMORY_POSTGRES_NODE_NAME,
);
await n8n.canvas.closeManualChatModal();
// Open the AI Agent node to see the logs
await n8n.canvas.openNode(AGENT_NODE_NAME);
// Assert that logs tab is active and error is displayed
await assertLogsTabIsActive(n8n);
await assertErrorMessageVisible(n8n);
});
test('should switch to logs tab on error, when NDV is already opened', async ({ n8n }) => {
// Remove the auto-added chat trigger
await n8n.canvas.addNode(MANUAL_CHAT_TRIGGER_NODE_NAME, { closeNDV: false });
// Set manual trigger to output standard pinned data
await n8n.ndv.getEditPinnedDataButton().click();
await n8n.ndv.savePinnedData();
await n8n.ndv.close();
// Set up the same workflow components but with manual trigger
await setupAgentWorkflowWithPostgresError(n8n);
// Open the AI Agent node
await n8n.canvas.openNode(AGENT_NODE_NAME);
await n8n.ndv.getParameterInput('promptType').click();
await n8n.page.getByRole('option', { name: 'Define below' }).click();
await n8n.ndv.getParameterInput('text').locator('textarea').fill('Some text');
await n8n.ndv.execute();
await waitForWorkflowSuccess(n8n);
// Assert that logs tab is active and error is displayed
await assertLogsTabIsActive(n8n);
await assertErrorMessageVisible(n8n);
});
});
},
);
@@ -0,0 +1,114 @@
import {
AGENT_NODE_NAME,
AI_TOOL_CALCULATOR_NODE_NAME,
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
MANUAL_CHAT_TRIGGER_NODE_NAME,
} from '../../../config/constants';
import { test, expect } from '../../../fixtures/base';
import type { n8nPage } from '../../../pages/n8nPage';
// Helper functions for common operations
async function addOpenAILanguageModelWithCredentials(
n8n: n8nPage,
parentNode: string,
options: { exactMatch?: boolean; closeNDV?: boolean } = { exactMatch: true, closeNDV: false },
) {
await n8n.canvas.addSupplementalNodeToParent(
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
'ai_languageModel',
parentNode,
options,
);
await n8n.credentialsComposer.createFromNdv({
apiKey: 'abcd',
});
await n8n.ndv.clickBackToCanvasButton();
}
async function waitForWorkflowSuccess(n8n: n8nPage, timeout = 3000) {
await n8n.notifications.waitForNotificationAndClose('Workflow executed successfully', {
timeout,
});
}
async function executeChatAndWaitForResponse(n8n: n8nPage, message: string) {
await n8n.canvas.logsPanel.sendManualChatMessage(message);
await waitForWorkflowSuccess(n8n);
}
async function setupBasicAgentWorkflow(n8n: n8nPage, additionalNodes: string[] = []) {
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
// Add additional nodes if specified
for (const nodeName of additionalNodes) {
await n8n.canvas.addSupplementalNodeToParent(nodeName, 'ai_tool', AGENT_NODE_NAME, {
closeNDV: true,
});
}
// Always add OpenAI Language Model
await addOpenAILanguageModelWithCredentials(n8n, AGENT_NODE_NAME);
}
test.use({ capability: 'proxy' });
test.describe(
'Langchain Integration @capability:proxy',
{
annotation: [{ type: 'owner', description: 'AI' }],
},
() => {
test.beforeEach(async ({ n8n, services }) => {
await services.proxy.clearAllExpectations();
await services.proxy.loadExpectations('langchain');
await n8n.canvas.openNewWorkflow();
});
// @AI team to look at this
test.describe('Tool Usage Notifications', () => {
test.fixme();
test('should show tool info notice if no existing tools were used during execution', async ({
n8n,
}) => {
await setupBasicAgentWorkflow(n8n, [AI_TOOL_CALCULATOR_NODE_NAME]);
await n8n.canvas.openNode(AGENT_NODE_NAME);
const inputMessage = 'Hello!';
await n8n.ndv.execute();
await executeChatAndWaitForResponse(n8n, inputMessage);
await n8n.canvas.closeManualChatModal();
await n8n.canvas.openNode(AGENT_NODE_NAME);
await expect(n8n.ndv.getRunDataInfoCallout()).toBeVisible();
});
test('should not show tool info notice if tools were used during execution', async ({
n8n,
}) => {
await n8n.canvas.addNode(MANUAL_CHAT_TRIGGER_NODE_NAME, { closeNDV: true });
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: false });
await expect(n8n.ndv.getRunDataInfoCallout()).toBeHidden();
await n8n.ndv.clickBackToCanvasButton();
await addOpenAILanguageModelWithCredentials(n8n, AGENT_NODE_NAME);
await n8n.canvas.addSupplementalNodeToParent(
AI_TOOL_CALCULATOR_NODE_NAME,
'ai_tool',
AGENT_NODE_NAME,
{ closeNDV: true },
);
const inputMessage = 'What is 1000 * 10?';
await n8n.canvas.clickManualChatButton();
await executeChatAndWaitForResponse(n8n, inputMessage);
await n8n.canvas.closeManualChatModal();
await n8n.canvas.openNode(AGENT_NODE_NAME);
await expect(n8n.ndv.getRunDataInfoCallout()).toBeHidden();
});
});
},
);
@@ -0,0 +1,109 @@
import { test, expect } from '../../../fixtures/base';
import type { n8nPage } from '../../../pages/n8nPage';
// Helper functions for common operations
async function waitForWorkflowSuccess(n8n: n8nPage, timeout = 10000) {
await n8n.notifications.waitForNotificationAndClose('Workflow executed successfully', {
timeout,
});
}
test.use({ capability: 'proxy' });
test.describe('Langchain Integration @capability:proxy', {
annotation: [
{ type: 'owner', description: 'AI' },
],
}, () => {
test.beforeEach(async ({ n8n, services }) => {
await services.proxy.clearAllExpectations();
await services.proxy.loadExpectations('langchain');
await n8n.canvas.openNewWorkflow();
});
test.describe('Advanced Workflow Features', () => {
test('should render runItems for sub-nodes and allow switching between them', async ({
n8n,
}) => {
await n8n.start.fromImportedWorkflow('In_memory_vector_store_fake_embeddings.json');
await n8n.canvas.clickZoomToFitButton();
await n8n.canvas.deselectAll();
await n8n.canvas.executeNode('Populate VS');
await waitForWorkflowSuccess(n8n);
const assertInputOutputTextExists = async (text: string) => {
await expect(n8n.ndv.getOutputPanel()).toContainText(text);
await expect(n8n.ndv.getInputPanel()).toContainText(text);
};
const assertInputOutputTextNotExists = async (text: string) => {
await expect(n8n.ndv.getOutputPanel()).not.toContainText(text);
await expect(n8n.ndv.getInputPanel()).not.toContainText(text);
};
await n8n.canvas.openNode('Character Text Splitter');
await expect(n8n.ndv.getOutputRunSelector()).toBeVisible();
await expect(n8n.ndv.getInputRunSelector()).toBeVisible();
await expect(n8n.ndv.getInputRunSelectorInput()).toHaveValue('3 of 3');
await expect(n8n.ndv.getOutputRunSelectorInput()).toHaveValue('3 of 3');
await assertInputOutputTextExists('Kyiv');
await assertInputOutputTextNotExists('Berlin');
await assertInputOutputTextNotExists('Prague');
await n8n.ndv.changeOutputRunSelector('2 of 3');
await assertInputOutputTextExists('Berlin');
await assertInputOutputTextNotExists('Kyiv');
await assertInputOutputTextNotExists('Prague');
await n8n.ndv.changeOutputRunSelector('1 of 3');
await assertInputOutputTextExists('Prague');
await assertInputOutputTextNotExists('Berlin');
await assertInputOutputTextNotExists('Kyiv');
await n8n.ndv.toggleInputRunLinking();
await n8n.ndv.changeOutputRunSelector('2 of 3');
await expect(n8n.ndv.getInputRunSelectorInput()).toHaveValue('1 of 3');
await expect(n8n.ndv.getOutputRunSelectorInput()).toHaveValue('2 of 3');
await expect(n8n.ndv.getInputPanel()).toContainText('Prague');
await expect(n8n.ndv.getInputPanel()).not.toContainText('Berlin');
await expect(n8n.ndv.getOutputPanel()).toContainText('Berlin');
await expect(n8n.ndv.getOutputPanel()).not.toContainText('Prague');
await n8n.ndv.toggleInputRunLinking();
await expect(n8n.ndv.getInputRunSelectorInput()).toHaveValue('1 of 3');
await expect(n8n.ndv.getOutputRunSelectorInput()).toHaveValue('1 of 3');
await assertInputOutputTextExists('Prague');
await assertInputOutputTextNotExists('Berlin');
await assertInputOutputTextNotExists('Kyiv');
});
test('should execute up to Node 1 when using partial execution', async ({ n8n }) => {
await n8n.start.fromImportedWorkflow('Test_workflow_chat_partial_execution.json');
await n8n.canvas.clickZoomToFitButton();
// Check that chat modal is not initially visible
await expect(n8n.canvas.getManualChatModal().getByTestId('canvas-chat-body')).toBeHidden();
// Open Node 1 and execute it
await n8n.canvas.openNode('Node 1');
await n8n.ndv.execute();
// Chat modal should now be visible
await expect(n8n.canvas.getManualChatModal().getByTestId('canvas-chat-body')).toBeVisible();
// Send first message
await n8n.canvas.logsPanel.sendManualChatMessage('Test');
await expect(n8n.canvas.getManualChatLatestBotMessage()).toContainText('this_my_field_1');
// Refresh session
await n8n.canvas.logsPanel.refreshSession();
await expect(n8n.canvas.logsPanel.getManualChatMessages()).not.toBeAttached();
// Send another message
await n8n.canvas.logsPanel.sendManualChatMessage('Another test');
await expect(n8n.canvas.getManualChatLatestBotMessage()).toContainText('this_my_field_3');
await expect(n8n.canvas.getManualChatLatestBotMessage()).toContainText('this_my_field_4');
});
});
});
@@ -0,0 +1,44 @@
import { test, expect } from '../../../fixtures/base';
test.describe('RAG callout experiment', {
annotation: [
{ type: 'owner', description: 'AI' },
],
}, () => {
test.describe('NDV callout', () => {
test('should show callout and open template on click', async ({ n8n }) => {
await n8n.start.fromBlankCanvas();
await n8n.canvas.addNode('Zep Vector Store', {
action: 'Add documents to vector store',
closeNDV: false,
});
await expect(n8n.canvas.getRagCalloutTip()).toBeVisible();
const popupPromise = n8n.page.waitForEvent('popup');
await n8n.canvas.clickRagTemplateLink();
const popup = await popupPromise;
expect(popup.url()).toContain('/workflows/templates/rag-starter-template?fromJson=true');
await popup.close();
});
});
test.describe('search callout', () => {
test('should show callout and open template on click', async ({ n8n }) => {
await n8n.start.fromBlankCanvas();
await n8n.canvas.clickNodeCreatorPlusButton();
await n8n.canvas.fillNodeCreatorSearchBar('rag');
const popupPromise = n8n.page.waitForEvent('popup');
await expect(n8n.canvas.getRagTemplateLink()).toBeVisible();
await n8n.canvas.clickRagTemplateLink();
const popup = await popupPromise;
expect(popup.url()).toContain('/workflows/templates/rag-starter-template?fromJson=true');
await popup.close();
});
});
});
@@ -0,0 +1,123 @@
import { workflowBuilderEnabledRequirements } from '../../../config/ai-builder-fixtures';
import { test, expect } from '../../../fixtures/base';
import type { n8nPage } from '../../../pages/n8nPage';
// Helper to open workflow builder and click a specific suggestion pill
async function openBuilderAndClickSuggestion(n8n: n8nPage, suggestionText: string) {
await n8n.aiBuilder.getCanvasBuildWithAIButton().click();
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeVisible();
await expect(n8n.aiBuilder.getWorkflowSuggestions()).toBeVisible();
// Wait for suggestions to load
await n8n.aiBuilder.getSuggestionPills().first().waitFor({ state: 'visible' });
// Find and click the specific suggestion pill by text
const targetPill = n8n.aiBuilder.getSuggestionPills().filter({ hasText: suggestionText });
await expect(targetPill).toBeVisible();
await targetPill.click();
// Suggestion pill already populated the input, just submit with Enter
await n8n.aiAssistant.sendMessage('', 'enter-key');
}
// Enable proxy server for recording/replaying Anthropic API calls
test.use({
capability: {
services: ['proxy'],
env: {
N8N_AI_ANTHROPIC_KEY: 'sk-ant-test-key-for-mocked-tests',
},
},
});
test.describe(
'Workflow Builder @auth:owner @ai @capability:proxy',
{
annotation: [{ type: 'owner', description: 'AI' }],
},
() => {
test.beforeEach(async ({ setupRequirements, services }) => {
await setupRequirements(workflowBuilderEnabledRequirements);
await services.proxy.clearAllExpectations();
await services.proxy.loadExpectations('workflow-builder');
});
test('should show Build with AI button on empty canvas', async ({ n8n }) => {
await n8n.page.goto('/workflow/new');
await expect(n8n.aiBuilder.getCanvasBuildWithAIButton()).toBeVisible();
});
test('should open workflow builder and show suggestions', async ({ n8n }) => {
await n8n.page.goto('/workflow/new');
await n8n.aiBuilder.getCanvasBuildWithAIButton().click();
await expect(n8n.aiAssistant.getAskAssistantSidebar()).toBeVisible();
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeVisible();
await expect(n8n.aiBuilder.getWorkflowSuggestions()).toBeVisible();
await n8n.aiBuilder.getSuggestionPills().first().waitFor({ state: 'visible' });
const suggestions = n8n.aiBuilder.getSuggestionPills();
await expect(suggestions).toHaveCount(8);
});
// @AI team - investigated issues with this test, the replay of recorded events not working as expected
// doesn't appear to be matching in the correct order/some requests make it past the proxy leading to 401 error
test.fixme('should build workflow from suggested prompt', async ({ n8n }) => {
await n8n.page.goto('/workflow/new');
await openBuilderAndClickSuggestion(n8n, 'YouTube video chapters');
await expect(n8n.aiAssistant.getChatMessagesUser().first()).toBeVisible();
// Wait for workflow to be built
await n8n.aiBuilder.waitForWorkflowBuildComplete();
await expect(n8n.canvas.getCanvasNodes().first()).toBeVisible();
const nodeCount = await n8n.canvas.getCanvasNodes().count();
expect(nodeCount).toBeGreaterThan(0);
// Verify "Execute and refine" button appears after workflow is built
await expect(n8n.page.getByRole('button', { name: 'Execute and refine' })).toBeVisible();
});
// suffers from the same issue as test above
test.fixme('should display assistant messages during workflow generation', async ({ n8n }) => {
await n8n.page.goto('/workflow/new');
await openBuilderAndClickSuggestion(n8n, 'YouTube video chapters');
await expect(n8n.aiAssistant.getChatMessagesUser().first()).toBeVisible();
await n8n.aiAssistant.waitForStreamingComplete();
const assistantMessages = n8n.aiAssistant.getChatMessagesAssistant();
await expect(assistantMessages.first()).toBeVisible();
const messageCount = await assistantMessages.count();
expect(messageCount).toBeGreaterThan(0);
});
test('should stop workflow generation and show task aborted message', async ({ n8n }) => {
await n8n.page.goto('/workflow/new');
await openBuilderAndClickSuggestion(n8n, 'Daily weather report');
await expect(n8n.aiAssistant.getChatMessagesUser().first()).toBeVisible();
// Wait for stop button to be enabled (streaming has started)
const stopButton = n8n.aiAssistant.getSendMessageButton();
await expect(stopButton).toBeEnabled({ timeout: 30000 });
await stopButton.click();
// Verify "Task aborted" message appears (search by text, not test-id)
await expect(n8n.page.getByText('Task aborted')).toBeVisible();
// Verify canvas returns to default state (no nodes added)
const nodeCount = await n8n.canvas.getCanvasNodes().count();
expect(nodeCount).toBe(0);
// Verify the Build with AI button is still visible (canvas is back to default)
await expect(n8n.aiBuilder.getCanvasBuildWithAIButton()).toBeVisible();
});
},
);