mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
refactor(editor): Clean up feature flag for the log view (#15606)
This commit is contained in:
@@ -0,0 +1,779 @@
|
||||
import { renderComponent } from '@/__tests__/render';
|
||||
import { fireEvent, waitFor, within } from '@testing-library/vue';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import LogsPanel from '@/features/logs/components/LogsPanel.vue';
|
||||
import { createTestingPinia, type TestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { computed, h, nextTick, ref } from 'vue';
|
||||
import {
|
||||
aiAgentNode,
|
||||
aiChatExecutionResponse,
|
||||
aiChatWorkflow,
|
||||
aiManualWorkflow,
|
||||
chatTriggerNode,
|
||||
nodeTypes,
|
||||
} from '../__test__/data';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { IN_PROGRESS_EXECUTION_ID } from '@/constants';
|
||||
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { deepCopy } from 'n8n-workflow';
|
||||
import { createTestTaskData } from '@/__tests__/mocks';
|
||||
import { useLogsStore } from '@/stores/logs.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { LOGS_PANEL_STATE } from '../logs.constants';
|
||||
import { ChatOptionsSymbol, ChatSymbol } from '@n8n/chat/constants';
|
||||
import { userEvent } from '@testing-library/user-event';
|
||||
import type { ChatMessage } from '@n8n/chat/types';
|
||||
import * as useChatMessaging from '@/features/logs/composables/useChatMessaging';
|
||||
import { chatEventBus } from '@n8n/chat/event-buses';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
|
||||
vi.mock('@/composables/useToast', () => {
|
||||
const showMessage = vi.fn();
|
||||
const showError = vi.fn();
|
||||
return {
|
||||
useToast: () => {
|
||||
return {
|
||||
showMessage,
|
||||
showError,
|
||||
clearAllStickyNotifications: vi.fn(),
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/stores/pushConnection.store', () => ({
|
||||
usePushConnectionStore: vi.fn().mockReturnValue({
|
||||
isConnected: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('LogsPanel', () => {
|
||||
const VIEWPORT_HEIGHT = 800;
|
||||
|
||||
let pinia: TestingPinia;
|
||||
let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
|
||||
let nodeTypeStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
|
||||
let logsStore: ReturnType<typeof mockedStore<typeof useLogsStore>>;
|
||||
let ndvStore: ReturnType<typeof mockedStore<typeof useNDVStore>>;
|
||||
let uiStore: ReturnType<typeof mockedStore<typeof useUIStore>>;
|
||||
|
||||
function render() {
|
||||
return renderComponent(LogsPanel, {
|
||||
global: {
|
||||
provide: {
|
||||
[ChatSymbol as symbol]: {},
|
||||
[ChatOptionsSymbol as symbol]: {},
|
||||
},
|
||||
plugins: [
|
||||
createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [{ path: '/', component: () => h('div') }],
|
||||
}),
|
||||
pinia,
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
pinia = createTestingPinia({ stubActions: false, fakeApp: true });
|
||||
|
||||
setActivePinia(pinia);
|
||||
|
||||
workflowsStore = mockedStore(useWorkflowsStore);
|
||||
workflowsStore.setWorkflowExecutionData(null);
|
||||
|
||||
logsStore = mockedStore(useLogsStore);
|
||||
logsStore.toggleOpen(false);
|
||||
|
||||
nodeTypeStore = mockedStore(useNodeTypesStore);
|
||||
nodeTypeStore.setNodeTypes(nodeTypes);
|
||||
|
||||
ndvStore = mockedStore(useNDVStore);
|
||||
|
||||
uiStore = mockedStore(useUIStore);
|
||||
|
||||
Object.defineProperty(document.body, 'offsetHeight', {
|
||||
configurable: true,
|
||||
get() {
|
||||
return VIEWPORT_HEIGHT;
|
||||
},
|
||||
});
|
||||
vi.spyOn(document.body, 'getBoundingClientRect').mockReturnValue({
|
||||
y: 0,
|
||||
height: VIEWPORT_HEIGHT,
|
||||
} as DOMRect);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render collapsed panel by default', async () => {
|
||||
const rendered = render();
|
||||
|
||||
expect(await rendered.findByTestId('logs-overview-header')).toBeInTheDocument();
|
||||
expect(rendered.queryByTestId('logs-overview-empty')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should only render logs panel if the workflow has no chat trigger', async () => {
|
||||
workflowsStore.setWorkflow(aiManualWorkflow);
|
||||
|
||||
const rendered = render();
|
||||
|
||||
expect(await rendered.findByTestId('logs-overview-header')).toBeInTheDocument();
|
||||
expect(rendered.queryByTestId('chat-header')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render chat panel and logs panel if the workflow has chat trigger', async () => {
|
||||
workflowsStore.setWorkflow(aiChatWorkflow);
|
||||
|
||||
const rendered = render();
|
||||
|
||||
expect(await rendered.findByTestId('logs-overview-header')).toBeInTheDocument();
|
||||
expect(await rendered.findByTestId('chat-header')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens collapsed panel when clicked', async () => {
|
||||
workflowsStore.setWorkflow(aiChatWorkflow);
|
||||
|
||||
const rendered = render();
|
||||
|
||||
await fireEvent.click(await rendered.findByTestId('logs-overview-header'));
|
||||
|
||||
expect(await rendered.findByTestId('logs-overview-empty')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should toggle panel when chevron icon button in the overview panel is clicked', async () => {
|
||||
workflowsStore.setWorkflow(aiChatWorkflow);
|
||||
|
||||
const rendered = render();
|
||||
|
||||
const overviewPanel = await rendered.findByTestId('logs-overview-header');
|
||||
|
||||
await fireEvent.click(within(overviewPanel).getByLabelText('Open panel'));
|
||||
expect(rendered.getByTestId('logs-overview-empty')).toBeInTheDocument();
|
||||
|
||||
await fireEvent.click(within(overviewPanel).getByLabelText('Collapse panel'));
|
||||
expect(rendered.queryByTestId('logs-overview-empty')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open log details panel when a log entry is clicked in the logs overview panel', async () => {
|
||||
workflowsStore.setWorkflow(aiChatWorkflow);
|
||||
workflowsStore.setWorkflowExecutionData(aiChatExecutionResponse);
|
||||
|
||||
const rendered = render();
|
||||
|
||||
await fireEvent.click(await rendered.findByTestId('logs-overview-header'));
|
||||
await fireEvent.click(await rendered.findByText('AI Agent'));
|
||||
expect(rendered.getByTestId('log-details')).toBeInTheDocument();
|
||||
|
||||
// Click again to close the panel
|
||||
await fireEvent.click(
|
||||
await within(rendered.getByTestId('logs-overview-body')).findByText('AI Agent'),
|
||||
);
|
||||
expect(rendered.queryByTestId('log-details')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the button to toggle panel in the header of log details panel when it's opened", async () => {
|
||||
workflowsStore.setWorkflow(aiChatWorkflow);
|
||||
workflowsStore.setWorkflowExecutionData(aiChatExecutionResponse);
|
||||
|
||||
const rendered = render();
|
||||
|
||||
await fireEvent.click(await rendered.findByTestId('logs-overview-header'));
|
||||
await fireEvent.click(await rendered.findByText('AI Agent'));
|
||||
|
||||
// Click the toggle button to close the panel
|
||||
await fireEvent.click(
|
||||
within(rendered.getByTestId('log-details')).getByLabelText('Collapse panel'),
|
||||
);
|
||||
expect(rendered.queryByTestId('chat-messages-empty')).not.toBeInTheDocument();
|
||||
expect(rendered.queryByTestId('logs-overview-body')).not.toBeInTheDocument();
|
||||
|
||||
// Click again to open the panel
|
||||
await fireEvent.click(
|
||||
within(rendered.getByTestId('logs-overview')).getByLabelText('Open panel'),
|
||||
);
|
||||
expect(await rendered.findByTestId('chat-messages-empty')).toBeInTheDocument();
|
||||
expect(await rendered.findByTestId('logs-overview-body')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open itself by pulling up the resizer', async () => {
|
||||
logsStore.toggleOpen(false);
|
||||
|
||||
const rendered = render();
|
||||
|
||||
expect(logsStore.state).toBe(LOGS_PANEL_STATE.CLOSED);
|
||||
expect(rendered.queryByTestId('logs-overview-body')).not.toBeInTheDocument();
|
||||
|
||||
await fireEvent.mouseDown(rendered.getByTestId('resize-handle'));
|
||||
|
||||
window.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: 0, clientY: 0 }));
|
||||
window.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: 0, clientY: 0 }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(logsStore.state).toBe(LOGS_PANEL_STATE.ATTACHED);
|
||||
expect(rendered.queryByTestId('logs-overview-body')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should close itself by pulling down the resizer', async () => {
|
||||
logsStore.toggleOpen(true);
|
||||
|
||||
const rendered = render();
|
||||
|
||||
expect(logsStore.state).toBe(LOGS_PANEL_STATE.ATTACHED);
|
||||
expect(rendered.queryByTestId('logs-overview-body')).toBeInTheDocument();
|
||||
|
||||
await fireEvent.mouseDown(rendered.getByTestId('resize-handle'));
|
||||
|
||||
window.dispatchEvent(
|
||||
new MouseEvent('mousemove', { bubbles: true, clientX: 0, clientY: VIEWPORT_HEIGHT }),
|
||||
);
|
||||
window.dispatchEvent(
|
||||
new MouseEvent('mouseup', { bubbles: true, clientX: 0, clientY: VIEWPORT_HEIGHT }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(logsStore.state).toBe(LOGS_PANEL_STATE.CLOSED);
|
||||
expect(rendered.queryByTestId('logs-overview-body')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should reflect changes to execution data in workflow store if execution is in progress', async () => {
|
||||
logsStore.toggleOpen(true);
|
||||
workflowsStore.setWorkflow(aiChatWorkflow);
|
||||
workflowsStore.setWorkflowExecutionData({
|
||||
...aiChatExecutionResponse,
|
||||
id: IN_PROGRESS_EXECUTION_ID,
|
||||
status: 'running',
|
||||
finished: false,
|
||||
startedAt: new Date('2025-04-20T12:34:50.000Z'),
|
||||
stoppedAt: undefined,
|
||||
data: {
|
||||
resultData: { runData: { Chat: [createTestTaskData()] } },
|
||||
},
|
||||
});
|
||||
|
||||
const rendered = render();
|
||||
|
||||
await fireEvent.click(rendered.getByText('Overview'));
|
||||
|
||||
expect(rendered.getByText(/Running/)).toBeInTheDocument();
|
||||
expect(rendered.queryByText('AI Agent')).not.toBeInTheDocument();
|
||||
|
||||
workflowsStore.addNodeExecutionStartedData({
|
||||
nodeName: 'AI Agent',
|
||||
executionId: '567',
|
||||
data: { executionIndex: 0, startTime: Date.parse('2025-04-20T12:34:51.000Z'), source: [] },
|
||||
});
|
||||
|
||||
const lastTreeItem = await waitFor(() => {
|
||||
const items = rendered.getAllByRole('treeitem');
|
||||
|
||||
expect(items).toHaveLength(2);
|
||||
return within(items[1]);
|
||||
});
|
||||
|
||||
expect(lastTreeItem.getByText('AI Agent')).toBeInTheDocument();
|
||||
expect(lastTreeItem.getByText(/Running/)).toBeInTheDocument();
|
||||
|
||||
workflowsStore.updateNodeExecutionData({
|
||||
nodeName: 'AI Agent',
|
||||
executionId: '567',
|
||||
data: {
|
||||
executionIndex: 0,
|
||||
startTime: Date.parse('2025-04-20T12:34:51.000Z'),
|
||||
source: [],
|
||||
executionTime: 33,
|
||||
executionStatus: 'success',
|
||||
},
|
||||
});
|
||||
expect(await lastTreeItem.findByText('AI Agent')).toBeInTheDocument();
|
||||
expect(lastTreeItem.getByText('Success in 33ms')).toBeInTheDocument();
|
||||
|
||||
workflowsStore.setWorkflowExecutionData({
|
||||
...workflowsStore.workflowExecutionData!,
|
||||
id: '1234',
|
||||
status: 'success',
|
||||
finished: true,
|
||||
startedAt: new Date('2025-04-20T12:34:50.000Z'),
|
||||
stoppedAt: new Date('2025-04-20T12:34:56.000Z'),
|
||||
});
|
||||
|
||||
expect(await rendered.findByText('Success in 6s')).toBeInTheDocument();
|
||||
expect(rendered.queryByText('AI Agent')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should still show logs for a removed node', async () => {
|
||||
const operations = useCanvasOperations();
|
||||
|
||||
logsStore.toggleOpen(true);
|
||||
workflowsStore.setWorkflow(deepCopy(aiChatWorkflow));
|
||||
workflowsStore.setWorkflowExecutionData({
|
||||
...aiChatExecutionResponse,
|
||||
id: '2345',
|
||||
status: 'success',
|
||||
finished: true,
|
||||
startedAt: new Date('2025-04-20T12:34:50.000Z'),
|
||||
stoppedAt: new Date('2025-04-20T12:34:56.000Z'),
|
||||
});
|
||||
|
||||
const rendered = render();
|
||||
|
||||
expect(await rendered.findByText('AI Agent')).toBeInTheDocument();
|
||||
|
||||
operations.deleteNode(aiAgentNode.id);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(workflowsStore.nodesByName['AI Agent']).toBeUndefined();
|
||||
expect(rendered.queryByText('AI Agent')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open NDV if the button is clicked', async () => {
|
||||
logsStore.toggleOpen(true);
|
||||
workflowsStore.setWorkflow(aiChatWorkflow);
|
||||
workflowsStore.setWorkflowExecutionData(aiChatExecutionResponse);
|
||||
|
||||
const rendered = render();
|
||||
const aiAgentRow = (await rendered.findAllByRole('treeitem'))[0];
|
||||
|
||||
expect(ndvStore.activeNodeName).toBe(null);
|
||||
expect(ndvStore.output.run).toBe(undefined);
|
||||
|
||||
await fireEvent.click(within(aiAgentRow).getAllByLabelText('Open...')[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(ndvStore.activeNodeName).toBe('AI Agent');
|
||||
expect(ndvStore.output.run).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should toggle subtree when chevron icon button is pressed', async () => {
|
||||
logsStore.toggleOpen(true);
|
||||
workflowsStore.setWorkflow(aiChatWorkflow);
|
||||
workflowsStore.setWorkflowExecutionData(aiChatExecutionResponse);
|
||||
|
||||
const rendered = render();
|
||||
const overview = within(rendered.getByTestId('logs-overview'));
|
||||
|
||||
await waitFor(() => expect(overview.queryAllByRole('treeitem')).toHaveLength(2));
|
||||
expect(overview.queryByText('AI Agent')).toBeInTheDocument();
|
||||
expect(overview.queryByText('AI Model')).toBeInTheDocument();
|
||||
|
||||
// Close subtree of AI Agent
|
||||
await fireEvent.click(overview.getAllByLabelText('Toggle row')[0]);
|
||||
|
||||
await waitFor(() => expect(overview.queryAllByRole('treeitem')).toHaveLength(1));
|
||||
expect(overview.queryByText('AI Agent')).toBeInTheDocument();
|
||||
expect(overview.queryByText('AI Model')).not.toBeInTheDocument();
|
||||
|
||||
// Re-open subtree of AI Agent
|
||||
await fireEvent.click(overview.getAllByLabelText('Toggle row')[0]);
|
||||
|
||||
await waitFor(() => expect(overview.queryAllByRole('treeitem')).toHaveLength(2));
|
||||
expect(overview.queryByText('AI Agent')).toBeInTheDocument();
|
||||
expect(overview.queryByText('AI Model')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should toggle input and output panel when the button is clicked', async () => {
|
||||
logsStore.toggleOpen(true);
|
||||
logsStore.toggleInputOpen(false);
|
||||
logsStore.toggleOutputOpen(true);
|
||||
workflowsStore.setWorkflow(aiChatWorkflow);
|
||||
workflowsStore.setWorkflowExecutionData(aiChatExecutionResponse);
|
||||
|
||||
const rendered = render();
|
||||
|
||||
const header = within(rendered.getByTestId('log-details-header'));
|
||||
|
||||
expect(rendered.queryByTestId('log-details-input')).not.toBeInTheDocument();
|
||||
expect(rendered.queryByTestId('log-details-output')).toBeInTheDocument();
|
||||
|
||||
await fireEvent.click(header.getByText('Input'));
|
||||
|
||||
expect(rendered.queryByTestId('log-details-input')).toBeInTheDocument();
|
||||
expect(rendered.queryByTestId('log-details-output')).toBeInTheDocument();
|
||||
|
||||
await fireEvent.click(header.getByText('Output'));
|
||||
|
||||
expect(rendered.queryByTestId('log-details-input')).toBeInTheDocument();
|
||||
expect(rendered.queryByTestId('log-details-output')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('selection', () => {
|
||||
beforeEach(() => {
|
||||
logsStore.toggleOpen(true);
|
||||
workflowsStore.setWorkflow(aiChatWorkflow);
|
||||
workflowsStore.setWorkflowExecutionData(aiChatExecutionResponse);
|
||||
});
|
||||
|
||||
it('should allow to select previous and next row via keyboard shortcut', async () => {
|
||||
const { getByTestId, findByRole } = render();
|
||||
const overview = getByTestId('logs-overview');
|
||||
|
||||
expect(await findByRole('treeitem', { selected: true })).toHaveTextContent(/AI Model/);
|
||||
await fireEvent.keyDown(overview, { key: 'K' });
|
||||
expect(await findByRole('treeitem', { selected: true })).toHaveTextContent(/AI Agent/);
|
||||
await fireEvent.keyDown(overview, { key: 'J' });
|
||||
expect(await findByRole('treeitem', { selected: true })).toHaveTextContent(/AI Model/);
|
||||
});
|
||||
|
||||
it('should not select a log for the selected node on canvas if sync is disabled', async () => {
|
||||
logsStore.toggleLogSelectionSync(false);
|
||||
|
||||
const { findByRole, rerender } = render();
|
||||
|
||||
expect(await findByRole('treeitem', { selected: true })).toHaveTextContent(/AI Model/);
|
||||
uiStore.lastSelectedNode = 'AI Agent';
|
||||
await rerender({});
|
||||
expect(await findByRole('treeitem', { selected: true })).toHaveTextContent(/AI Model/);
|
||||
});
|
||||
|
||||
it('should automatically select a log for the selected node on canvas if sync is enabled', async () => {
|
||||
logsStore.toggleLogSelectionSync(true);
|
||||
|
||||
const { rerender, findByRole } = render();
|
||||
|
||||
expect(await findByRole('treeitem', { selected: true })).toHaveTextContent(/AI Model/);
|
||||
uiStore.lastSelectedNode = 'AI Agent';
|
||||
await rerender({});
|
||||
expect(await findByRole('treeitem', { selected: true })).toHaveTextContent(/AI Agent/);
|
||||
});
|
||||
|
||||
it('should automatically expand and select a log for the selected node on canvas if the log entry is collapsed', async () => {
|
||||
logsStore.toggleLogSelectionSync(true);
|
||||
|
||||
const { rerender, findByRole, getByLabelText, findByText, queryByText } = render();
|
||||
|
||||
await fireEvent.click(await findByText('AI Agent'));
|
||||
expect(await findByRole('treeitem', { selected: true })).toHaveTextContent(/AI Agent/);
|
||||
await fireEvent.click(getByLabelText('Toggle row'));
|
||||
await rerender({});
|
||||
expect(queryByText(/AI Model/)).not.toBeInTheDocument();
|
||||
uiStore.lastSelectedNode = 'AI Model';
|
||||
await rerender({});
|
||||
expect(await findByRole('treeitem', { selected: true })).toHaveTextContent(/AI Model/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chat', () => {
|
||||
beforeEach(() => {
|
||||
logsStore.toggleOpen(true);
|
||||
workflowsStore.setWorkflow(aiChatWorkflow);
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should render chat when panel is open', () => {
|
||||
const { getByTestId } = render();
|
||||
expect(getByTestId('canvas-chat-body')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render chat when panel is closed', async () => {
|
||||
const { queryByTestId } = render();
|
||||
logsStore.toggleOpen(false);
|
||||
await waitFor(() => expect(queryByTestId('canvas-chat-body')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should show correct input placeholder', async () => {
|
||||
const { findByTestId } = render();
|
||||
expect(await findByTestId('chat-input')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('message handling', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(chatEventBus, 'emit');
|
||||
workflowsStore.runWorkflow.mockResolvedValue({ executionId: 'test-execution-id' });
|
||||
});
|
||||
|
||||
it('should send message and show response', async () => {
|
||||
const { findByTestId, findByText, getByText } = render();
|
||||
|
||||
// Send message
|
||||
const input = await findByTestId('chat-input');
|
||||
await userEvent.type(input, 'Hello AI!');
|
||||
|
||||
await userEvent.keyboard('{Enter}');
|
||||
|
||||
// Verify message and response
|
||||
expect(await findByText('Hello AI!')).toBeInTheDocument();
|
||||
workflowsStore.setWorkflowExecutionData({ ...aiChatExecutionResponse, status: 'success' });
|
||||
await waitFor(() => expect(getByText('AI response message')).toBeInTheDocument());
|
||||
|
||||
// Verify workflow execution
|
||||
expect(workflowsStore.runWorkflow).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
runData: undefined,
|
||||
triggerToStartFrom: {
|
||||
name: 'Chat',
|
||||
data: {
|
||||
data: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
json: {
|
||||
action: 'sendMessage',
|
||||
chatInput: 'Hello AI!',
|
||||
sessionId: expect.any(String),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
executionIndex: 0,
|
||||
executionStatus: 'success',
|
||||
executionTime: 0,
|
||||
source: [null],
|
||||
startTime: expect.any(Number),
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should show loading state during message processing', async () => {
|
||||
const { findByTestId, queryByTestId } = render();
|
||||
|
||||
// Send message
|
||||
const input = await findByTestId('chat-input');
|
||||
await userEvent.type(input, 'Test message');
|
||||
await userEvent.keyboard('{Enter}');
|
||||
|
||||
await waitFor(() => expect(queryByTestId('chat-message-typing')).toBeInTheDocument());
|
||||
|
||||
workflowsStore.setActiveExecutionId(undefined);
|
||||
workflowsStore.setWorkflowExecutionData({ ...aiChatExecutionResponse, status: 'success' });
|
||||
|
||||
await waitFor(() => expect(queryByTestId('chat-message-typing')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should handle workflow execution errors', async () => {
|
||||
workflowsStore.runWorkflow.mockRejectedValueOnce(new Error());
|
||||
|
||||
const { findByTestId } = render();
|
||||
|
||||
const input = await findByTestId('chat-input');
|
||||
await userEvent.type(input, 'Hello AI!');
|
||||
await userEvent.keyboard('{Enter}');
|
||||
|
||||
const toast = useToast();
|
||||
expect(toast.showError).toHaveBeenCalledWith(new Error(), 'Problem running workflow');
|
||||
});
|
||||
});
|
||||
|
||||
describe('session management', () => {
|
||||
const mockMessages: ChatMessage[] = [
|
||||
{
|
||||
id: '1',
|
||||
text: 'Existing message',
|
||||
sender: 'user',
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(useChatMessaging, 'useChatMessaging').mockImplementation(({ messages }) => {
|
||||
messages.value.push(...mockMessages);
|
||||
|
||||
return {
|
||||
sendMessage: vi.fn(),
|
||||
previousMessageIndex: ref(0),
|
||||
isLoading: computed(() => false),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow copying session ID', async () => {
|
||||
const clipboardSpy = vi.fn();
|
||||
document.execCommand = clipboardSpy;
|
||||
const { getByTestId } = render();
|
||||
|
||||
await userEvent.click(getByTestId('chat-session-id'));
|
||||
const toast = useToast();
|
||||
expect(clipboardSpy).toHaveBeenCalledWith('copy');
|
||||
expect(toast.showMessage).toHaveBeenCalledWith({
|
||||
message: '',
|
||||
title: 'Copied to clipboard',
|
||||
type: 'success',
|
||||
});
|
||||
});
|
||||
|
||||
it('should refresh session when messages exist', async () => {
|
||||
const { getByTestId } = render();
|
||||
|
||||
const originalSessionId = getByTestId('chat-session-id').textContent;
|
||||
await userEvent.click(getByTestId('refresh-session-button'));
|
||||
|
||||
expect(getByTestId('chat-session-id').textContent).not.toEqual(originalSessionId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('file handling', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(useChatMessaging, 'useChatMessaging').mockReturnValue({
|
||||
sendMessage: vi.fn(),
|
||||
previousMessageIndex: ref(0),
|
||||
isLoading: computed(() => false),
|
||||
});
|
||||
|
||||
logsStore.state = LOGS_PANEL_STATE.ATTACHED;
|
||||
workflowsStore.allowFileUploads = true;
|
||||
});
|
||||
|
||||
it('should enable file uploads when allowed by chat trigger node', async () => {
|
||||
workflowsStore.setNodes(aiChatWorkflow.nodes);
|
||||
workflowsStore.setNodeParameters({
|
||||
name: chatTriggerNode.name,
|
||||
value: { options: { allowFileUploads: true } },
|
||||
});
|
||||
|
||||
const { getByTestId, queryByTestId } = render();
|
||||
|
||||
expect(getByTestId('canvas-chat')).toBeInTheDocument();
|
||||
expect(getByTestId('chat-attach-file-button')).toBeInTheDocument();
|
||||
|
||||
workflowsStore.setNodeParameters({
|
||||
name: chatTriggerNode.name,
|
||||
value: { options: { allowFileUploads: false } },
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(queryByTestId('chat-attach-file-button')).not.toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('message history handling', () => {
|
||||
it('should properly navigate through message history with wrap-around', async () => {
|
||||
workflowsStore.resetChatMessages();
|
||||
workflowsStore.appendChatMessage('Message 1');
|
||||
workflowsStore.appendChatMessage('Message 2');
|
||||
workflowsStore.appendChatMessage('Message 3');
|
||||
|
||||
const { findByTestId } = render();
|
||||
const input = await findByTestId('chat-input');
|
||||
|
||||
chatEventBus.emit('focusInput');
|
||||
|
||||
// First up should show most recent message
|
||||
await userEvent.keyboard('{ArrowUp}');
|
||||
expect(input).toHaveValue('Message 3');
|
||||
|
||||
// Second up should show second most recent
|
||||
await userEvent.keyboard('{ArrowUp}');
|
||||
expect(input).toHaveValue('Message 2');
|
||||
|
||||
// Third up should show oldest message
|
||||
await userEvent.keyboard('{ArrowUp}');
|
||||
expect(input).toHaveValue('Message 1');
|
||||
|
||||
// Fourth up should wrap around to most recent
|
||||
await userEvent.keyboard('{ArrowUp}');
|
||||
expect(input).toHaveValue('Message 3');
|
||||
|
||||
// Down arrow should go in reverse
|
||||
await userEvent.keyboard('{ArrowDown}');
|
||||
expect(input).toHaveValue('Message 1');
|
||||
});
|
||||
|
||||
it('should reset message history navigation on new input', async () => {
|
||||
workflowsStore.resetChatMessages();
|
||||
workflowsStore.appendChatMessage('Message 1');
|
||||
workflowsStore.appendChatMessage('Message 2');
|
||||
|
||||
const { findByTestId } = render();
|
||||
const input = await findByTestId('chat-input');
|
||||
|
||||
chatEventBus.emit('focusInput');
|
||||
|
||||
// Navigate to oldest message
|
||||
await userEvent.keyboard('{ArrowUp}'); // Most recent
|
||||
await userEvent.keyboard('{ArrowUp}'); // Oldest
|
||||
expect(input).toHaveValue('Message 1');
|
||||
|
||||
await userEvent.type(input, 'New message');
|
||||
await userEvent.keyboard('{Enter}');
|
||||
|
||||
await userEvent.keyboard('{ArrowUp}');
|
||||
expect(input).toHaveValue('Message 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('message reuse and repost', () => {
|
||||
const sendMessageSpy = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
const mockMessages: ChatMessage[] = [
|
||||
{
|
||||
id: '1',
|
||||
text: 'Original message',
|
||||
sender: 'user',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
text: 'AI response',
|
||||
sender: 'bot',
|
||||
},
|
||||
];
|
||||
vi.spyOn(useChatMessaging, 'useChatMessaging').mockImplementation(({ messages }) => {
|
||||
messages.value.push(...mockMessages);
|
||||
|
||||
return {
|
||||
sendMessage: sendMessageSpy,
|
||||
previousMessageIndex: ref(0),
|
||||
isLoading: computed(() => false),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it('should repost user message with new execution', async () => {
|
||||
const { findByTestId } = render();
|
||||
const repostButton = await findByTestId('repost-message-button');
|
||||
|
||||
await userEvent.click(repostButton);
|
||||
|
||||
expect(sendMessageSpy).toHaveBeenCalledWith('Original message');
|
||||
});
|
||||
|
||||
it('should show message options only for appropriate messages', async () => {
|
||||
const { findByText, container } = render();
|
||||
|
||||
await findByText('Original message');
|
||||
const userMessage = container.querySelector('.chat-message-from-user');
|
||||
expect(
|
||||
userMessage?.querySelector('[data-test-id="repost-message-button"]'),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
userMessage?.querySelector('[data-test-id="reuse-message-button"]'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await findByText('AI response');
|
||||
const botMessage = container.querySelector('.chat-message-from-bot');
|
||||
expect(
|
||||
botMessage?.querySelector('[data-test-id="repost-message-button"]'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
botMessage?.querySelector('[data-test-id="reuse-message-button"]'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyboard shortcuts', () => {
|
||||
it('should handle Enter key with modifier to start new line', async () => {
|
||||
const { findByTestId } = render();
|
||||
|
||||
const input = await findByTestId('chat-input');
|
||||
await userEvent.type(input, 'Line 1');
|
||||
await userEvent.keyboard('{Shift>}{Enter}{/Shift}');
|
||||
await userEvent.type(input, 'Line 2');
|
||||
|
||||
expect(input).toHaveValue('Line 1\nLine 2');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user