diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.test.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.test.ts new file mode 100644 index 00000000000..ab898890f47 --- /dev/null +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.test.ts @@ -0,0 +1,205 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { ref, shallowRef, defineComponent, h } from 'vue'; +import { setActivePinia } from 'pinia'; +import { createTestingPinia } from '@pinia/testing'; +import { render } from '@testing-library/vue'; + +import { useWorkflowInitialization } from './useWorkflowInitialization'; +import { WorkflowDocumentStoreKey } from '@/app/constants/injectionKeys'; +import type { WorkflowState } from '@/app/composables/useWorkflowState'; +import type { IWorkflowDb } from '@/Interface'; + +const mockSetDocumentTitle = vi.hoisted(() => vi.fn()); +const mockResetDocumentTitle = vi.hoisted(() => vi.fn()); +const mockSetTitle = vi.hoisted(() => vi.fn()); + +vi.mock('@/app/composables/useDocumentTitle', () => ({ + useDocumentTitle: vi.fn(() => ({ + set: mockSetTitle, + reset: mockResetDocumentTitle, + setDocumentTitle: mockSetDocumentTitle, + getDocumentState: vi.fn(), + })), +})); + +const mockResetWorkspace = vi.hoisted(() => vi.fn()); +const mockInitializeWorkspace = vi.hoisted(() => + vi.fn().mockResolvedValue({ + workflowDocumentStore: { workflowId: 'wf-1', workflowVersion: 1 }, + }), +); +const mockOpenWorkflowTemplate = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); +const mockOpenWorkflowTemplateFromJSON = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); +const mockFitView = vi.hoisted(() => vi.fn()); + +vi.mock('@/app/composables/useCanvasOperations', () => ({ + useCanvasOperations: vi.fn(() => ({ + resetWorkspace: mockResetWorkspace, + initializeWorkspace: mockInitializeWorkspace, + fitView: mockFitView, + openWorkflowTemplate: mockOpenWorkflowTemplate, + openWorkflowTemplateFromJSON: mockOpenWorkflowTemplateFromJSON, + })), +})); + +vi.mock('@/features/execution/executions/composables/useExecutionDebugging', () => ({ + useExecutionDebugging: vi.fn(() => ({ + applyExecutionData: vi.fn(), + })), +})); + +vi.mock('@/app/composables/useExternalHooks', () => ({ + useExternalHooks: vi.fn(() => ({ + run: vi.fn().mockResolvedValue(undefined), + })), +})); + +vi.mock('@/app/composables/useToast', () => ({ + useToast: vi.fn(() => ({ + showError: vi.fn(), + showMessage: vi.fn(), + })), +})); + +vi.mock('@n8n/i18n', () => ({ + useI18n: vi.fn(() => ({ + baseText: (key: string) => key, + })), +})); + +const mockBuilderStore = vi.hoisted(() => ({ streaming: false })); +vi.mock('@/features/ai/assistant/builder.store', () => ({ + useBuilderStore: vi.fn(() => mockBuilderStore), +})); + +vi.mock('@/features/core/folders/composables/useParentFolder', () => ({ + useParentFolder: vi.fn(() => ({ + fetchParentFolder: vi.fn().mockResolvedValue(null), + })), +})); + +vi.mock('@/app/composables/useTelemetry', () => ({ + useTelemetry: vi.fn(() => ({ + track: vi.fn(), + })), +})); + +vi.mock('@/app/composables/useWorkflowId', () => ({ + useWorkflowId: vi.fn(() => ref('wf-1')), +})); + +const mockNDVStore = vi.hoisted(() => ({})); +vi.mock('@/features/ndv/shared/ndv.store', () => ({ + useNDVStore: vi.fn(() => mockNDVStore), + disposeNDVStore: vi.fn(), +})); + +const mockWorkflowDocumentStore = vi.hoisted(() => ({ + workflowId: 'wf-1', + workflowVersion: 1, + name: 'New Workflow', + setName: vi.fn(), + setHomeProject: vi.fn(), + setScopes: vi.fn(), + setParentFolder: vi.fn(), + onNameChange: vi.fn(), +})); +vi.mock('@/app/stores/workflowDocument.store', () => ({ + useWorkflowDocumentStore: vi.fn(() => mockWorkflowDocumentStore), + createWorkflowDocumentId: vi.fn((id: string) => id), + disposeWorkflowDocumentStore: vi.fn(), +})); + +const mockRoute = vi.hoisted(() => ({ + name: 'workflow' as string | symbol, + params: {} as Record, + query: {} as Record, + meta: {} as Record, +})); +vi.mock('vue-router', async (importOriginal) => { + const actual = (await importOriginal()) as object; + return { + ...actual, + useRoute: vi.fn(() => mockRoute), + useRouter: vi.fn(() => ({ + replace: vi.fn().mockResolvedValue(undefined), + push: vi.fn().mockResolvedValue(undefined), + })), + }; +}); + +function createWorkflowState(): WorkflowState { + return { + getNewWorkflowData: vi.fn().mockResolvedValue({ name: 'New Workflow', settings: {} }), + } as unknown as WorkflowState; +} + +function renderWithComposable( + callback: (init: ReturnType) => void, +) { + const TestComponent = defineComponent({ + setup() { + const init = useWorkflowInitialization(createWorkflowState()); + callback(init); + return () => h('div'); + }, + }); + + return render(TestComponent, { + global: { + provide: { + [WorkflowDocumentStoreKey as symbol]: shallowRef(mockWorkflowDocumentStore), + }, + }, + }); +} + +describe('useWorkflowInitialization', () => { + beforeEach(() => { + setActivePinia(createTestingPinia()); + vi.clearAllMocks(); + mockBuilderStore.streaming = false; + mockRoute.name = 'workflow'; + mockRoute.params = {}; + mockRoute.query = {}; + mockRoute.meta = {}; + }); + + describe('document title', () => { + it('sets the title to the workflow name when an existing workflow is opened', async () => { + let openWorkflow!: (data: IWorkflowDb) => Promise; + renderWithComposable((init) => { + openWorkflow = init.openWorkflow; + }); + + await openWorkflow({ id: 'wf-1', name: 'My Test Workflow' } as IWorkflowDb); + + expect(mockSetDocumentTitle).toHaveBeenCalledWith('My Test Workflow', 'IDLE'); + expect(mockResetDocumentTitle).not.toHaveBeenCalled(); + }); + + it('sets the AI_BUILDING title when builder is streaming', async () => { + mockBuilderStore.streaming = true; + + let openWorkflow!: (data: IWorkflowDb) => Promise; + renderWithComposable((init) => { + openWorkflow = init.openWorkflow; + }); + + await openWorkflow({ id: 'wf-1', name: 'My Test Workflow' } as IWorkflowDb); + + expect(mockSetDocumentTitle).toHaveBeenCalledWith('My Test Workflow', 'AI_BUILDING'); + }); + + it('sets the title to the new workflow name on a fresh editor', async () => { + let initializeWorkspaceForNewWorkflow!: () => Promise; + renderWithComposable((init) => { + initializeWorkspaceForNewWorkflow = init.initializeWorkspaceForNewWorkflow; + }); + + await initializeWorkspaceForNewWorkflow(); + + expect(mockSetDocumentTitle).toHaveBeenCalledWith('New Workflow', 'IDLE'); + }); + }); +}); diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.ts index 12779c8fa88..72c9454ac30 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.ts @@ -181,6 +181,7 @@ export function useWorkflowInitialization(workflowState: WorkflowState) { const workflowDocumentId = createWorkflowDocumentId(currentWorkflowId); currentWorkflowDocumentStore.value = useWorkflowDocumentStore(workflowDocumentId); currentNDVStore.value = useNDVStore(workflowDocumentId); + documentTitle.setDocumentTitle(currentWorkflowDocumentStore.value.name, 'IDLE'); } return true; @@ -324,6 +325,7 @@ export function useWorkflowInitialization(workflowState: WorkflowState) { parentFolderId, ); currentWorkflowDocumentStore.value.setName(workflowData.name); + documentTitle.setDocumentTitle(workflowData.name, 'IDLE'); const homeProject = projectsStore.currentProject ?? projectsStore.personalProject ?? null; currentWorkflowDocumentStore.value.setHomeProject(homeProject); diff --git a/packages/frontend/editor-ui/src/app/views/NodeView.vue b/packages/frontend/editor-ui/src/app/views/NodeView.vue index 74256891275..e91ef688e4b 100644 --- a/packages/frontend/editor-ui/src/app/views/NodeView.vue +++ b/packages/frontend/editor-ui/src/app/views/NodeView.vue @@ -1780,8 +1780,6 @@ onBeforeRouteLeave(async (to, from, next) => { */ onMounted(async () => { - documentTitle.reset(); - // Register callback for collaboration store to refresh canvas when workflow updates arrive collaborationStore.setRefreshCanvasCallback(async (workflow) => { await initializeWorkspace(workflow);