Remove global state dependencies from composables

This commit is contained in:
Oleg Ivaniv 2024-11-07 08:47:19 +01:00
parent 078f1b0045
commit 32b4eb523b
No known key found for this signature in database
9 changed files with 244 additions and 260 deletions

View file

@ -1,44 +1,121 @@
<script setup lang="ts">
import { provide, watch, computed, ref, watchEffect } from 'vue';
import { ChatOptionsSymbol, ChatSymbol } from '@n8n/chat/constants';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useRouter } from 'vue-router';
import { chatEventBus } from '@n8n/chat/event-buses';
import { useCanvasStore } from '@/stores/canvas.store';
import { v4 as uuid } from 'uuid';
// Constants & Symbols
import { ChatOptionsSymbol, ChatSymbol } from '@n8n/chat/constants';
import { VIEWS } from '@/constants';
// Components
import ChatMessagesPanel from './components/ChatMessagesPanel.vue';
import ChatLogsPanel from './components/ChatLogsPanel.vue';
// Composables
import { useChatTrigger } from './composables/useChatTrigger';
import { useChatMessaging } from './composables/useChatMessaging';
import { useResize } from './composables/useResize';
import type { Chat, ChatMessage, ChatOptions } from '@n8n/chat/types';
import { useUIStore } from '@/stores/ui.store';
import { useI18n } from '@/composables/useI18n';
import { v4 as uuid } from 'uuid';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useRunWorkflow } from '@/composables/useRunWorkflow';
// Event Bus
import { chatEventBus } from '@n8n/chat/event-buses';
// Stores
import { useCanvasStore } from '@/stores/canvas.store';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
const router = useRouter();
const uiStore = useUIStore();
const locale = useI18n();
const { getCurrentWorkflow } = useWorkflowHelpers({ router });
const nodeHelpers = useNodeHelpers();
const workflowsStore = useWorkflowsStore();
const canvasStore = useCanvasStore();
// Types
import type { Chat, ChatMessage, ChatOptions } from '@n8n/chat/types';
import type { RunWorkflowChatPayload } from './composables/useChatMessaging';
const messages = ref<ChatMessage[]>([]);
const currentSessionId = ref<string>(uuid().replace(/-/g, ''));
const isDisabled = ref(false);
const container = ref<HTMLElement>();
// Initialize stores and composables
const setupStores = () => {
const router = useRouter();
const uiStore = useUIStore();
const locale = useI18n();
const nodeHelpers = useNodeHelpers();
const workflowsStore = useWorkflowsStore();
const canvasStore = useCanvasStore();
const nodeTypesStore = useNodeTypesStore();
return {
router,
uiStore,
locale,
nodeHelpers,
workflowsStore,
canvasStore,
nodeTypesStore,
};
};
// Component state
const setupState = () => {
const messages = ref<ChatMessage[]>([]);
const currentSessionId = ref<string>(uuid().replace(/-/g, ''));
const isDisabled = ref(false);
const container = ref<HTMLElement>();
return {
messages,
currentSessionId,
isDisabled,
container,
};
};
// Initialize component
const { router, uiStore, locale, nodeHelpers, workflowsStore, canvasStore, nodeTypesStore } =
setupStores();
const { messages, currentSessionId, isDisabled, container } = setupState();
// Computed properties
const setupComputed = () => {
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
const isLoading = computed(() => uiStore.isActionActive.workflowRunning);
const allConnections = computed(() => workflowsStore.allConnections);
const isChatOpen = computed(() => workflowsStore.isChatPanelOpen);
const isLogsOpen = computed(() => workflowsStore.isLogsPanelOpen);
const previousChatMessages = computed(() => workflowsStore.getPastChatMessages);
return {
workflow,
isLoading,
allConnections,
isChatOpen,
isLogsOpen,
previousChatMessages,
};
};
const { workflow, isLoading, allConnections, isChatOpen, isLogsOpen, previousChatMessages } =
setupComputed();
// Initialize features
const { runWorkflow } = useRunWorkflow({ router });
const { chatTriggerNode, connectedNode, allowFileUploads, setChatTriggerNode, setConnectedNode } =
useChatTrigger({ router });
useChatTrigger({
workflow,
getNodeByName: workflowsStore.getNodeByName,
getNodeType: nodeTypesStore.getNodeType,
});
const { sendMessage, getChatMessages } = useChatMessaging({
chatTrigger: chatTriggerNode,
connectedNode,
messages,
router,
sessionId: currentSessionId,
workflow,
isLoading,
executionResultData: computed(() => workflowsStore.getWorkflowExecution?.data?.resultData),
getWorkflowResultDataByNodeName: workflowsStore.getWorkflowResultDataByNodeName,
onRunChatWorkflow,
});
const {
@ -51,69 +128,86 @@ const {
onWindowResize,
} = useResize(container);
const isLoading = computed(() => uiStore.isActionActive.workflowRunning);
const allConnections = computed(() => workflowsStore.allConnections);
const isChatOpen = computed(() => workflowsStore.isChatPanelOpen);
const isLogsOpen = computed(() => workflowsStore.isLogsPanelOpen);
const previousChatMessages = computed(() => workflowsStore.getPastChatMessages);
// Chat configuration
const setupChatConfig = (): { chatConfig: Chat; chatOptions: ChatOptions } => {
const chatConfig: Chat = {
messages,
sendMessage,
initialMessages: ref([]),
currentSessionId,
waitingForResponse: isLoading,
};
const chatConfig: Chat = {
messages,
sendMessage,
initialMessages: ref([]),
currentSessionId,
waitingForResponse: isLoading,
};
const chatOptions: ChatOptions = {
i18n: {
en: {
title: '',
footer: '',
subtitle: '',
inputPlaceholder: locale.baseText('chat.window.chat.placeholder'),
getStarted: '',
closeButtonTooltip: '',
const chatOptions: ChatOptions = {
i18n: {
en: {
title: '',
footer: '',
subtitle: '',
inputPlaceholder: locale.baseText('chat.window.chat.placeholder'),
getStarted: '',
closeButtonTooltip: '',
},
},
},
webhookUrl: '',
mode: 'window',
showWindowCloseButton: true,
disabled: isDisabled,
allowFileUploads,
allowedFilesMimeTypes: '',
webhookUrl: '',
mode: 'window',
showWindowCloseButton: true,
disabled: isDisabled,
allowFileUploads,
allowedFilesMimeTypes: '',
};
return { chatConfig, chatOptions };
};
function displayExecution(executionId: string) {
const workflow = getCurrentWorkflow();
const { chatConfig, chatOptions } = setupChatConfig();
// Methods
const displayExecution = (executionId: string) => {
const route = router.resolve({
name: VIEWS.EXECUTION_PREVIEW,
params: { name: workflow.id, executionId },
params: { name: workflow.value.id, executionId },
});
window.open(route.href, '_blank');
}
};
function refreshSession() {
const refreshSession = () => {
workflowsStore.setWorkflowExecutionData(null);
nodeHelpers.updateNodesExecutionIssues();
messages.value = [];
currentSessionId.value = uuid().replace(/-/g, '');
}
};
function closeLogs() {
const closeLogs = () => {
workflowsStore.setPanelOpen('logs', false);
};
async function onRunChatWorkflow(payload: RunWorkflowChatPayload) {
const response = await runWorkflow({
triggerNode: payload.triggerNode,
nodeData: payload.nodeData,
source: payload.source,
});
workflowsStore.appendChatMessage(payload.message);
return response;
}
// Provide chat context
provide(ChatSymbol, chatConfig);
provide(ChatOptionsSymbol, chatOptions);
// Watchers
watch(
() => isChatOpen.value,
(isOpen) => {
if (isOpen) {
setChatTriggerNode();
setConnectedNode();
messages.value = getChatMessages();
if (messages.value.length === 0) {
messages.value = getChatMessages();
}
setTimeout(() => {
onWindowResize();
@ -138,18 +232,17 @@ watch(
);
watchEffect(() => {
canvasStore.setPanelHeight(isChatOpen.value || isLogsOpen.value ? height.value : 40);
canvasStore.setPanelHeight(isChatOpen.value || isLogsOpen.value ? height.value : 0);
});
</script>
<template>
<n8n-resize-wrapper
v-show="chatTriggerNode"
v-if="chatTriggerNode"
:is-resizing-enabled="isChatOpen || isLogsOpen"
:supported-directions="['top']"
:class="[$style.resizeWrapper, !isChatOpen && !isLogsOpen && $style.empty]"
:height="height"
data-test-id="ask-assistant-sidebar"
:style="rootStyles"
@resize="onResizeDebounced"
>
@ -164,6 +257,7 @@ watchEffect(() => {
>
<div :class="$style.inner">
<ChatMessagesPanel
data-test-id="canvas-chat"
:messages="messages"
:session-id="currentSessionId"
:past-chat-messages="previousChatMessages"
@ -176,6 +270,8 @@ watchEffect(() => {
<div v-if="isLogsOpen && connectedNode" :class="$style.logs">
<ChatLogsPanel
:key="messages.length"
:workflow="workflow"
data-test-id="canvas-chat-logs"
:node="connectedNode"
:slim="logsWidth < 700"
@close="closeLogs"
@ -192,7 +288,6 @@ watchEffect(() => {
min-height: 4rem;
max-height: 90vh;
flex-basis: content;
z-index: 300;
border-top: 1px solid var(--color-foreground-base);
&.empty {
@ -201,6 +296,7 @@ watchEffect(() => {
flex-basis: 0;
}
}
.container {
width: 100%;
height: 100%;
@ -208,12 +304,14 @@ watchEffect(() => {
flex-direction: column;
overflow: hidden;
}
.chatResizer {
display: flex;
width: 100%;
height: 100%;
max-width: 100%;
}
.footer {
border-top: 1px solid var(--color-foreground-base);
width: 100%;

View file

@ -1,22 +1,19 @@
<script setup lang="ts">
import type { INode } from 'n8n-workflow';
import { computed } from 'vue';
import type { INode, Workflow } from 'n8n-workflow';
import RunDataAi from '@/components/RunDataAi/RunDataAi.vue';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useI18n } from '@/composables/useI18n';
const emit = defineEmits<{
close: [];
}>();
const locale = useI18n();
const workflowsStore = useWorkflowsStore();
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
defineProps<{
node: INode | null;
slim?: boolean;
workflow: Workflow;
}>();
const locale = useI18n();
</script>
<template>

View file

@ -159,7 +159,7 @@ function copySessionId() {
icon="redo"
:label="locale.baseText('chat.window.chat.chatMessageOptions.repostMessage')"
placement="left"
@click="repostMessage(message)"
@click.once="repostMessage(message)"
/>
<MessageOptionAction

View file

@ -23,7 +23,7 @@ defineProps({
<template #content>
{{ label }}
</template>
<n8n-icon :class="$style.icon" :icon="icon" size="xsmall" @click="$emit('click')" />
<n8n-icon :class="$style.icon" :icon="icon" size="xsmall" @click="$attrs.onClick" />
</n8n-tooltip>
</div>
</template>

View file

@ -1,5 +1,5 @@
import type { Ref } from 'vue';
import { computed, ref } from 'vue';
import type { ComputedRef, Ref } from 'vue';
import { ref } from 'vue';
import { v4 as uuid } from 'uuid';
import type { ChatMessage, ChatMessageText } from '@n8n/chat/types';
import { NodeConnectionType, CHAT_TRIGGER_NODE_TYPE } from 'n8n-workflow';
@ -10,41 +10,53 @@ import type {
IDataObject,
IBinaryData,
BinaryFileType,
Workflow,
IRunExecutionData,
} from 'n8n-workflow';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useRunWorkflow } from '@/composables/useRunWorkflow';
import { useToast } from '@/composables/useToast';
import { useMessage } from '@/composables/useMessage';
import { usePinnedData } from '@/composables/usePinnedData';
import { get, isEmpty, last } from 'lodash-es';
import { MANUAL_CHAT_TRIGGER_NODE_TYPE, MODAL_CONFIRM } from '@/constants';
import { useI18n } from '@/composables/useI18n';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import type { useRouter } from 'vue-router';
import type { MemoryOutput } from '../types/chat';
import { useUIStore } from '@/stores/ui.store';
import type { INodeUi } from '@/Interface';
import type { IExecutionPushResponse, INodeUi } from '@/Interface';
export function useChatMessaging({
router,
chatTrigger,
messages,
sessionId,
}: {
router: ReturnType<typeof useRouter>;
export type RunWorkflowChatPayload = {
triggerNode: string;
nodeData: ITaskData;
source: string;
message: string;
};
export interface ChatMessagingDependencies {
chatTrigger: Ref<INodeUi | null>;
connectedNode: Ref<INodeUi | null>;
messages: Ref<ChatMessage[]>;
sessionId: Ref<string>;
}) {
const previousMessageIndex = ref(0);
const workflowsStore = useWorkflowsStore();
const { runWorkflow } = useRunWorkflow({ router });
const workflowHelpers = useWorkflowHelpers({ router });
const { showError } = useToast();
const uiStore = useUIStore();
const locale = useI18n();
workflow: ComputedRef<Workflow>;
isLoading: ComputedRef<boolean>;
executionResultData: ComputedRef<IRunExecutionData['resultData'] | undefined>;
getWorkflowResultDataByNodeName: (nodeName: string) => ITaskData[] | null;
onRunChatWorkflow: (
payload: RunWorkflowChatPayload,
) => Promise<IExecutionPushResponse | undefined>;
}
export function useChatMessaging({
chatTrigger,
connectedNode,
messages,
sessionId,
workflow,
isLoading,
executionResultData,
getWorkflowResultDataByNodeName,
onRunChatWorkflow,
}: ChatMessagingDependencies) {
const locale = useI18n();
const { showError } = useToast();
const previousMessageIndex = ref(0);
const isLoading = computed(() => uiStore.isActionActive.workflowRunning);
/** Converts a file to binary data */
async function convertFileToBinaryData(file: File): Promise<IBinaryData> {
const reader = new FileReader();
@ -136,13 +148,19 @@ export function useChatMessaging({
source: [null],
};
const response = await runWorkflow({
const response = await onRunChatWorkflow({
triggerNode: triggerNode.name,
nodeData,
source: 'RunData.ManualChatMessage',
message,
});
// const response = await runWorkflow({
// triggerNode: triggerNode.name,
// nodeData,
// source: 'RunData.ManualChatMessage',
// });
workflowsStore.appendChatMessage(message);
// workflowsStore.appendChatMessage(message);
if (!response?.executionId) {
showError(
new Error('It was not possible to start workflow!'),
@ -160,14 +178,12 @@ export function useChatMessaging({
if (!isLoading.value) {
clearInterval(waitInterval);
const lastNodeExecuted =
workflowsStore.getWorkflowExecution?.data?.resultData.lastNodeExecuted;
const lastNodeExecuted = executionResultData.value?.lastNodeExecuted;
if (!lastNodeExecuted) return;
const nodeResponseDataArray =
get(workflowsStore.getWorkflowExecution?.data?.resultData.runData, lastNodeExecuted) ??
[];
get(executionResultData.value.runData, lastNodeExecuted) ?? [];
const nodeResponseData = nodeResponseDataArray[nodeResponseDataArray.length - 1];
@ -251,18 +267,20 @@ export function useChatMessaging({
}
function getChatMessages(): ChatMessageText[] {
if (!chatTrigger.value) return [];
console.log('Getting chat messages', connectedNode.value);
if (!connectedNode.value) return [];
const workflow = workflowHelpers.getCurrentWorkflow();
const connectedMemoryInputs =
workflow.connectionsByDestinationNode[chatTrigger.value.name]?.[NodeConnectionType.AiMemory];
workflow.value.connectionsByDestinationNode[connectedNode.value.name]?.[
NodeConnectionType.AiMemory
];
if (!connectedMemoryInputs) return [];
const memoryConnection = (connectedMemoryInputs ?? []).find((i) => i.length > 0)?.[0];
if (!memoryConnection) return [];
const nodeResultData = workflowsStore.getWorkflowResultDataByNodeName(memoryConnection.node);
const nodeResultData = getWorkflowResultDataByNodeName(memoryConnection.node);
const memoryOutputData = (nodeResultData ?? [])
.map((data) => get(data, ['data', NodeConnectionType.AiMemory, 0, 0, 'json']) as MemoryOutput)

View file

@ -1,13 +1,17 @@
import type { ComputedRef } from 'vue';
import { ref, computed } from 'vue';
import {
CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE,
NodeConnectionType,
NodeHelpers,
type INode,
type INodeParameters,
type INodeType,
} from 'n8n-workflow';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import type {
INodeTypeDescription,
Workflow,
INode,
INodeParameters,
INodeType,
} from 'n8n-workflow';
import {
AI_CATEGORY_AGENTS,
AI_CATEGORY_CHAINS,
@ -17,19 +21,19 @@ import {
MANUAL_CHAT_TRIGGER_NODE_TYPE,
} from '@/constants';
import type { INodeUi } from '@/Interface';
import type { useRouter } from 'vue-router';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
export function useChatTrigger({ router }: { router: ReturnType<typeof useRouter> }) {
export interface ChatTriggerDependencies {
getNodeByName: (name: string) => INodeUi | null;
getNodeType: (type: string, version: number) => INodeTypeDescription | null;
workflow: ComputedRef<Workflow>;
}
export function useChatTrigger({ getNodeByName, getNodeType, workflow }: ChatTriggerDependencies) {
const chatTriggerName = ref<string | null>(null);
const connectedNode = ref<INode | null>(null);
const workflowsStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore();
const workflowHelpers = useWorkflowHelpers({ router });
const chatTriggerNode = computed(() =>
chatTriggerName.value ? workflowsStore.getNodeByName(chatTriggerName.value) : null,
chatTriggerName.value ? getNodeByName(chatTriggerName.value) : null,
);
const allowFileUploads = computed(() => {
@ -48,11 +52,9 @@ export function useChatTrigger({ router }: { router: ReturnType<typeof useRouter
/** Gets the chat trigger node from the workflow */
function setChatTriggerNode() {
const triggerNode = workflowHelpers
.getCurrentWorkflow()
.queryNodes((nodeType: INodeType) =>
[CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE].includes(nodeType.description.name),
);
const triggerNode = workflow.value.queryNodes((nodeType: INodeType) =>
[CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE].includes(nodeType.description.name),
);
if (!triggerNode.length) {
return;
@ -62,25 +64,24 @@ export function useChatTrigger({ router }: { router: ReturnType<typeof useRouter
/** Sets the connected node after finding the trigger */
function setConnectedNode() {
const workflow = workflowHelpers.getCurrentWorkflow();
const triggerNode = chatTriggerNode.value;
if (!triggerNode) {
return;
}
const chatChildren = workflow.getChildNodes(triggerNode.name);
const chatChildren = workflow.value.getChildNodes(triggerNode.name);
const chatRootNode = chatChildren
.reverse()
.map((nodeName: string) => workflowsStore.getNodeByName(nodeName))
.map((nodeName: string) => getNodeByName(nodeName))
.filter((n): n is INodeUi => n !== null)
// Reverse the nodes to match the last node logs first
.reverse()
.find((storeNode: INodeUi): boolean => {
// Skip summarization nodes
if (storeNode.type === CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE) return false;
const nodeType = nodeTypesStore.getNodeType(storeNode.type, storeNode.typeVersion);
const nodeType = getNodeType(storeNode.type, storeNode.typeVersion);
if (!nodeType) return false;
@ -94,10 +95,10 @@ export function useChatTrigger({ router }: { router: ReturnType<typeof useRouter
let isCustomChainOrAgent = false;
if (nodeType.name === AI_CODE_NODE_TYPE) {
// Get node connection types for inputs and outputs
const inputs = NodeHelpers.getNodeInputs(workflow, storeNode, nodeType);
const inputs = NodeHelpers.getNodeInputs(workflow.value, storeNode, nodeType);
const inputTypes = NodeHelpers.getConnectionTypes(inputs);
const outputs = NodeHelpers.getNodeOutputs(workflow, storeNode, nodeType);
const outputs = NodeHelpers.getNodeOutputs(workflow.value, storeNode, nodeType);
const outputTypes = NodeHelpers.getConnectionTypes(outputs);
// Validate if node has required AI connection types
@ -114,14 +115,14 @@ export function useChatTrigger({ router }: { router: ReturnType<typeof useRouter
if (!isAgent && !isChain && !isCustomChainOrAgent) return false;
// Check if this node is connected to the trigger node
const parentNodes = workflow.getParentNodes(storeNode.name);
const parentNodes = workflow.value.getParentNodes(storeNode.name);
const isChatChild = parentNodes.some(
(parentNodeName) => parentNodeName === triggerNode.name,
);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const resut = Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent));
return resut;
const result = Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent));
return result;
});
connectedNode.value = chatRootNode ?? null;

View file

@ -132,7 +132,6 @@ export function useResize(container: Ref<HTMLElement | undefined>) {
onWindowResize,
onResizeDebounced,
onResizeChatDebounced,
// onResizeChat,
panelToContainerRatio,
};
}

View file

@ -1,127 +0,0 @@
import { createPinia, setActivePinia } from 'pinia';
import { fireEvent, waitFor } from '@testing-library/vue';
import { mock } from 'vitest-mock-extended';
import { NodeConnectionType } from 'n8n-workflow';
import type { IConnections, INode } from 'n8n-workflow';
import WorkflowLMChatModal from '@/components/WorkflowLMChat/WorkflowLMChat.vue';
import { WORKFLOW_LM_CHAT_MODAL_KEY } from '@/constants';
import type { IWorkflowDb } from '@/Interface';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useUIStore } from '@/stores/ui.store';
import { useUsersStore } from '@/stores/users.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { createComponentRenderer } from '@/__tests__/render';
import { setupServer } from '@/__tests__/server';
import { defaultNodeDescriptions, mockNodes } from '@/__tests__/mocks';
import { cleanupAppModals, createAppModals } from '@/__tests__/utils';
const connections: IConnections = {
'Chat Trigger': {
main: [
[
{
node: 'Agent',
type: NodeConnectionType.Main,
index: 0,
},
],
],
},
};
const renderComponent = createComponentRenderer(WorkflowLMChatModal, {
props: {
teleported: false,
appendToBody: false,
},
});
async function createPiniaWithAINodes(options = { withConnections: true, withAgentNode: true }) {
const { withConnections, withAgentNode } = options;
const chatTriggerNode = mockNodes[4];
const agentNode = mockNodes[5];
const nodes: INode[] = [chatTriggerNode];
if (withAgentNode) nodes.push(agentNode);
const workflow = mock<IWorkflowDb>({
nodes,
...(withConnections ? { connections } : {}),
});
const pinia = createPinia();
setActivePinia(pinia);
const workflowsStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore();
const uiStore = useUIStore();
nodeTypesStore.setNodeTypes(defaultNodeDescriptions);
workflowsStore.workflow = workflow;
await useSettingsStore().getSettings();
await useUsersStore().loginWithCookie();
uiStore.openModal(WORKFLOW_LM_CHAT_MODAL_KEY);
return pinia;
}
describe('WorkflowLMChatModal', () => {
let server: ReturnType<typeof setupServer>;
beforeAll(() => {
server = setupServer();
});
beforeEach(() => {
createAppModals();
});
afterEach(() => {
cleanupAppModals();
vi.clearAllMocks();
});
afterAll(() => {
server.shutdown();
});
it('should render correctly', async () => {
const { getByTestId } = renderComponent({
pinia: await createPiniaWithAINodes(),
});
await waitFor(() => expect(getByTestId('lmChat-modal')).toBeInTheDocument());
expect(getByTestId('workflow-lm-chat-dialog')).toBeInTheDocument();
});
it('should send and display chat message', async () => {
const { getByTestId } = renderComponent({
pinia: await createPiniaWithAINodes({
withConnections: true,
withAgentNode: true,
}),
});
await waitFor(() => expect(getByTestId('lmChat-modal')).toBeInTheDocument());
const chatDialog = getByTestId('workflow-lm-chat-dialog');
const chatInputsContainer = getByTestId('lm-chat-inputs');
const chatSendButton = chatInputsContainer.querySelector('.chat-input-send-button');
const chatInput = chatInputsContainer.querySelector('textarea');
if (chatInput && chatSendButton) {
await fireEvent.update(chatInput, 'Hello!');
await fireEvent.click(chatSendButton);
}
await waitFor(() =>
expect(chatDialog.querySelectorAll('.chat-message-from-user')).toHaveLength(1),
);
expect(chatDialog.querySelector('.chat-message-from-user')).toHaveTextContent('Hello!');
});
});

View file

@ -37,7 +37,6 @@ import { get } from 'lodash-es';
import { useExecutionsStore } from '@/stores/executions.store';
import type { PushPayload } from '@n8n/api-types';
import { useLocalStorage } from '@vueuse/core';
import { useCanvasStore } from '@/stores/canvas.store';
const FORM_RELOAD = 'n8n_redirect_to_next_form_test_page';
@ -51,7 +50,6 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore();
const executionsStore = useExecutionsStore();
const canvasStore = useCanvasStore();
// Starts to execute a workflow on server
async function runWorkflowApi(runData: IStartRunData): Promise<IExecutionPushResponse> {
if (!rootStore.pushConnectionActive) {