import userEvent from '@testing-library/user-event'; import { createTestingPinia } from '@pinia/testing'; import { waitFor, screen } from '@testing-library/vue'; import { createComponentRenderer } from '@/__tests__/render'; import { cleanupAppModals, createAppModals, mockedStore, type MockedStore, } from '@/__tests__/utils'; import { useUIStore } from '@/stores/ui.store'; import { WHATS_NEW_MODAL_KEY, VERSIONS_MODAL_KEY } from '@/constants'; import { useVersionsStore } from '@/stores/versions.store'; import type { Version } from '@n8n/rest-api-client/api/versions'; import WhatsNewModal from './WhatsNewModal.vue'; import { useTelemetry } from '@/composables/useTelemetry'; import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper'; vi.mock('@/composables/usePageRedirectionHelper', () => { const goToVersions = vi.fn(); return { usePageRedirectionHelper: vi.fn().mockReturnValue({ goToVersions, }), }; }); vi.mock('@/composables/useTelemetry', () => { const track = vi.fn(); return { useTelemetry: () => { return { track, }; }, }; }); const renderComponent = createComponentRenderer(WhatsNewModal, { props: { modalName: WHATS_NEW_MODAL_KEY, }, }); let uiStore: MockedStore; let versionsStore: MockedStore; const telemetry = useTelemetry(); const pageRedirectionHelper = usePageRedirectionHelper(); const currentVersion: Version = { name: '1.100.0', nodes: [], createdAt: '2025-06-24T00:00:00Z', description: 'Latest version description', documentationUrl: 'https://docs.n8n.io', hasBreakingChange: false, hasSecurityFix: false, hasSecurityIssue: false, securityIssueFixVersion: '', }; describe('WhatsNewModal', () => { beforeEach(() => { createAppModals(); createTestingPinia(); uiStore = mockedStore(useUIStore); uiStore.modalsById = { [WHATS_NEW_MODAL_KEY]: { open: true, }, }; versionsStore = mockedStore(useVersionsStore); versionsStore.hasVersionUpdates = false; versionsStore.currentVersion = currentVersion; versionsStore.latestVersion = currentVersion; versionsStore.nextVersions = []; versionsStore.whatsNew = { createdAt: '2025-06-19T12:35:14.454Z', updatedAt: null, title: "What's New in n8n 1.100.0", calloutText: 'Convert large workflows into sub-workflows for better modularity and performance.', footer: 'This release contains performance improvements and bug fixes.', items: [ { id: 1, title: 'Convert to sub-workflow', content: 'Large, monolithic workflows can slow things down. They’re harder to maintain, ' + 'tougher to debug, and more difficult to scale. With sub-workflows, you can take a ' + 'more modular approach, breaking up big workflows into smaller, manageable parts that ' + 'are easier to reuse, test, understand, and explain.\n\nUntil now, creating sub-workflows ' + 'required copying and pasting nodes manually, setting up a new workflow from scratch, and ' + 'reconnecting everything by hand. **Convert to sub-workflow** allows you to simplify this ' + 'process into a single action, so you can spend more time building and less time restructuring.\n\n' + '### How it works\n\n1. Highlight the nodes you want to convert to a sub-workflow. These must:\n' + ' - Be fully connected, meaning no missing steps in between them\n' + ' - Start from a single starting node\n' + ' - End with a single node\n' + '2. Right-click to open the context menu and select ' + '**Convert to sub-workflow**\n' + ' - Or use the shortcut: `Alt + X`\n' + '3. n8n will:\n' + ' - Open a new tab containing the selected nodes\n' + ' - Preserve all node parameters as-is\n' + ' - Replace the selected nodes in the original workflow with a **Call My Sub-workflow** node\n\n' + '_Note:_ You will need to manually adjust the field types in the Start and Return nodes in the new sub-workflow.\n\n' + 'This makes it easier to keep workflows modular, performant, and easier to maintain.\n\n' + 'Learn more about [sub-workflows](https://docs.n8n.io/flow-logic/subworkflows/).\n\n' + 'This release contains performance improvements and bug fixes.\n\n' + '@[youtube](ZCuL2e4zC_4)\n\n' + 'Fusce malesuada diam eget tincidunt ultrices. Mauris quis mauris mollis, venenatis risus ut.\n\n' + '## Second level title\n\n### Third level title\n\nThis **is bold**, this _in italics_.\n' + "~~Strikethrough is also something we support~~.\n\nHere's a peace of code:\n\n" + '```typescript\nconst props = defineProps<{\n\tmodalName: string;\n\tdata: {\n\t\tarticleId: number;\n\t};\n}>();\n```\n\n' + 'Inline `code also works` withing text.\n\nThis is a list:\n- first\n- second\n- third\n\nAnd this list is ordered\n' + '1. foo\n2. bar\n3. qux\n\nDividers:\n\nThree or more...\n\n---\n\nHyphens\n\n***\n\nAsterisks\n\n___\n\nUnderscores\n\n---\n\n' + '
\nFixes (4)\n\n' + '- **Credential Storage Issue** Resolved an issue where credentials would occasionally become inaccessible after server restarts\n' + '- **Webhook Timeout Handling** Fixed timeout issues with long-running webhook requests\n' + '- **Node Connection Validation** Improved validation for node connections to prevent invalid workflow configurations\n' + '- **Memory Leak in Execution Engine** Fixed memory leak that could occur during long-running workflow executions\n\n
\n\n', createdAt: '2025-06-19T12:35:14.454Z', updatedAt: '2025-06-19T12:41:53.220Z', publishedAt: '2025-06-19T12:41:53.216Z', }, ], }; }); afterEach(() => { cleanupAppModals(); vi.clearAllMocks(); }); it('should render with update button disabled', async () => { const { getByTestId, queryByTestId } = renderComponent({ props: { data: { articleId: 1, }, }, }); await waitFor(() => expect(getByTestId('whatsNew-modal')).toBeInTheDocument()); await waitFor(() => expect(getByTestId('whats-new-item-1')).toBeInTheDocument()); expect(screen.getByText("What's New in n8n 1.100.0")).toBeInTheDocument(); expect(getByTestId('whats-new-item-1')).toMatchSnapshot(); expect(getByTestId('whats-new-modal-update-button')).toBeDisabled(); expect(queryByTestId('whats-new-modal-next-versions-link')).not.toBeInTheDocument(); }); it('should render with update button enabled', async () => { versionsStore.hasVersionUpdates = true; versionsStore.nextVersions = [ { name: '1.100.1', nodes: [], createdAt: '2025-06-24T00:00:00Z', description: 'Next version description', documentationUrl: 'https://docs.n8n.io', hasBreakingChange: false, hasSecurityFix: false, hasSecurityIssue: false, securityIssueFixVersion: '', }, ]; const { getByTestId } = renderComponent({ props: { data: { articleId: 1, }, }, }); await waitFor(() => expect(getByTestId('whatsNew-modal')).toBeInTheDocument()); await waitFor(() => expect(getByTestId('whats-new-item-1')).toBeInTheDocument()); expect(getByTestId('whats-new-modal-update-button')).toBeEnabled(); expect(getByTestId('whats-new-modal-next-versions-link')).toBeInTheDocument(); expect(getByTestId('whats-new-modal-next-versions-link')).toHaveTextContent('1 version behind'); }); it('should take user to update page when Update is clicked', async () => { versionsStore.hasVersionUpdates = true; const { getByTestId } = renderComponent({ props: { data: { articleId: 1, }, }, }); await waitFor(() => expect(getByTestId('whatsNew-modal')).toBeInTheDocument()); await waitFor(() => expect(getByTestId('whats-new-item-1')).toBeInTheDocument()); await userEvent.click(getByTestId('whats-new-modal-update-button')); expect(telemetry.track).toHaveBeenCalledWith('User clicked on update button', { source: 'whats-new-modal', }); expect(pageRedirectionHelper.goToVersions).toHaveBeenCalledWith(); }); it('should open the next versions drawer when clicking on the next versions link', async () => { versionsStore.hasVersionUpdates = true; const { getByTestId } = renderComponent({ props: { data: { articleId: 1, }, }, }); await waitFor(() => expect(getByTestId('whatsNew-modal')).toBeInTheDocument()); await waitFor(() => expect(getByTestId('whats-new-item-1')).toBeInTheDocument()); await userEvent.click(getByTestId('whats-new-modal-next-versions-link')); expect(uiStore.openModal).toHaveBeenCalledWith(VERSIONS_MODAL_KEY); }); });