From c06ce765f11dcde4731d3739e1aa5f27351c3cc2 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Wed, 19 Mar 2025 17:18:54 +0100 Subject: [PATCH] feat(editor): Add variables and context section to schema view (#13875) --- .../frontend/editor-ui/src/__tests__/setup.ts | 3 + .../components/ExpressionEditModal.test.ts | 17 +- .../editor-ui/src/components/RunData.test.ts | 5 + .../src/components/VirtualSchema.test.ts | 101 +- .../src/components/VirtualSchema.vue | 167 ++- .../src/components/VirtualSchemaHeader.vue | 8 +- .../src/components/VirtualSchemaItem.vue | 33 +- .../__snapshots__/VirtualSchema.test.ts.snap | 1246 ++++++++++++++++- .../src/composables/useDataSchema.test.ts | 7 +- .../src/composables/useDataSchema.ts | 87 +- .../src/plugins/i18n/locales/en.json | 4 + .../editor-ui/src/utils/mappingUtils.ts | 17 +- 12 files changed, 1573 insertions(+), 122 deletions(-) diff --git a/packages/frontend/editor-ui/src/__tests__/setup.ts b/packages/frontend/editor-ui/src/__tests__/setup.ts index 748814ccd0..d6ad6482fb 100644 --- a/packages/frontend/editor-ui/src/__tests__/setup.ts +++ b/packages/frontend/editor-ui/src/__tests__/setup.ts @@ -3,6 +3,9 @@ import 'fake-indexeddb/auto'; import { configure } from '@testing-library/vue'; import 'core-js/proposals/set-methods-v2'; +// Avoid tests failing because of difference between local and GitHub actions timezone +process.env.TZ = 'UTC'; + configure({ testIdAttribute: 'data-test-id' }); window.ResizeObserver = diff --git a/packages/frontend/editor-ui/src/components/ExpressionEditModal.test.ts b/packages/frontend/editor-ui/src/components/ExpressionEditModal.test.ts index e0d59fe79c..b310e460be 100644 --- a/packages/frontend/editor-ui/src/components/ExpressionEditModal.test.ts +++ b/packages/frontend/editor-ui/src/components/ExpressionEditModal.test.ts @@ -3,6 +3,9 @@ import { cleanupAppModals, createAppModals } from '@/__tests__/utils'; import ExpressionEditModal from '@/components/ExpressionEditModal.vue'; import { createTestingPinia } from '@pinia/testing'; import { waitFor, within } from '@testing-library/vue'; +import { setActivePinia, type Pinia } from 'pinia'; +import { defaultSettings } from '../__tests__/defaults'; +import { useSettingsStore } from '../stores/settings.store'; vi.mock('vue-router', () => { const push = vi.fn(); @@ -15,11 +18,21 @@ vi.mock('vue-router', () => { }; }); +vi.mock('@/composables/useWorkflowHelpers', async (importOriginal) => { + const actual: object = await importOriginal(); + return { ...actual, resolveParameter: vi.fn(() => 123) }; +}); + const renderModal = createComponentRenderer(ExpressionEditModal); describe('ExpressionEditModal', () => { + let pinia: Pinia; + beforeEach(() => { createAppModals(); + pinia = createTestingPinia({ stubActions: false }); + setActivePinia(pinia); + useSettingsStore().setSettings(defaultSettings); }); afterEach(() => { @@ -28,8 +41,6 @@ describe('ExpressionEditModal', () => { }); it('renders correctly', async () => { - const pinia = createTestingPinia(); - const { getByTestId } = renderModal({ pinia, props: { @@ -52,8 +63,6 @@ describe('ExpressionEditModal', () => { }); it('is read only', async () => { - const pinia = createTestingPinia(); - const { getByTestId } = renderModal({ pinia, props: { diff --git a/packages/frontend/editor-ui/src/components/RunData.test.ts b/packages/frontend/editor-ui/src/components/RunData.test.ts index dc10f24566..6ab08c5ca4 100644 --- a/packages/frontend/editor-ui/src/components/RunData.test.ts +++ b/packages/frontend/editor-ui/src/components/RunData.test.ts @@ -34,6 +34,11 @@ vi.mock('@/composables/useExecutionHelpers', () => ({ }), })); +vi.mock('@/composables/useWorkflowHelpers', async (importOriginal) => { + const actual: object = await importOriginal(); + return { ...actual, resolveParameter: vi.fn(() => 123) }; +}); + describe('RunData', () => { beforeAll(() => { resolveRelatedExecutionUrl.mockReturnValue('execution.url/123'); diff --git a/packages/frontend/editor-ui/src/components/VirtualSchema.test.ts b/packages/frontend/editor-ui/src/components/VirtualSchema.test.ts index d35de855fa..f4630a1fe4 100644 --- a/packages/frontend/editor-ui/src/components/VirtualSchema.test.ts +++ b/packages/frontend/editor-ui/src/components/VirtualSchema.test.ts @@ -20,11 +20,14 @@ import { type INodeExecutionData, } from 'n8n-workflow'; import * as nodeHelpers from '@/composables/useNodeHelpers'; +import * as workflowHelpers from '@/composables/useWorkflowHelpers'; import { useNDVStore } from '@/stores/ndv.store'; import { fireEvent } from '@testing-library/dom'; import { useTelemetry } from '@/composables/useTelemetry'; import { useSchemaPreviewStore } from '../stores/schemaPreview.store'; import { usePostHog } from '../stores/posthog.store'; +import { useSettingsStore } from '../stores/settings.store'; +import { defaultSettings } from '../__tests__/defaults'; const mockNode1 = createTestNode({ name: 'Manual Trigger', @@ -86,6 +89,8 @@ async function setupStore() { const workflowsStore = useWorkflowsStore(); const nodeTypesStore = useNodeTypesStore(); + const settingsStore = useSettingsStore(); + settingsStore.setSettings(defaultSettings); nodeTypesStore.setNodeTypes([ ...defaultNodeDescriptions, @@ -141,6 +146,8 @@ describe('VirtualSchema.vue', () => { beforeEach(async () => { cleanup(); + vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue(123); + vi.setSystemTime('2025-01-01'); renderComponent = createComponentRenderer(VirtualSchema, { global: { stubs: { @@ -195,7 +202,7 @@ describe('VirtualSchema.vue', () => { const { getAllByTestId } = renderComponent(); await waitFor(() => { const headers = getAllByTestId('run-data-schema-header'); - expect(headers.length).toBe(2); + expect(headers.length).toBe(3); expect(headers[0]).toHaveTextContent('Manual Trigger'); expect(headers[0]).toHaveTextContent('2 items'); expect(headers[1]).toHaveTextContent('Set2'); @@ -286,16 +293,15 @@ describe('VirtualSchema.vue', () => { }); it('renders disabled nodes correctly', async () => { - const { getByTestId } = renderComponent({ + const { getAllByTestId } = renderComponent({ props: { nodes: [{ name: disabledNode.name, indicies: [], depth: 1 }], }, }); - await waitFor(() => - expect(getByTestId('run-data-schema-header')).toHaveTextContent( - `${disabledNode.name} (Deactivated)`, - ), - ); + await waitFor(() => { + const headers = getAllByTestId('run-data-schema-header'); + expect(headers[0]).toHaveTextContent(`${disabledNode.name} (Deactivated)`); + }); }); it('renders schema for correct output branch', async () => { @@ -307,16 +313,17 @@ describe('VirtualSchema.vue', () => { ], 1, ); - const { getByTestId } = renderComponent({ + const { getAllByTestId } = renderComponent({ props: { nodes: [{ name: 'If', indicies: [1], depth: 2 }], }, }); await waitFor(() => { - expect(getByTestId('run-data-schema-header')).toHaveTextContent('If'); - expect(getByTestId('run-data-schema-header')).toHaveTextContent('2 items'); - expect(getByTestId('run-data-schema-header')).toMatchSnapshot(); + const headers = getAllByTestId('run-data-schema-header'); + expect(headers[0]).toHaveTextContent('If'); + expect(headers[0]).toHaveTextContent('2 items'); + expect(headers[0]).toMatchSnapshot(); }); }); @@ -329,7 +336,7 @@ describe('VirtualSchema.vue', () => { ], 0, ); - const { getByTestId } = renderComponent({ + const { getAllByTestId } = renderComponent({ props: { nodes: [ { @@ -343,9 +350,10 @@ describe('VirtualSchema.vue', () => { }); await waitFor(() => { - expect(getByTestId('run-data-schema-header')).toHaveTextContent('If'); - expect(getByTestId('run-data-schema-header')).toHaveTextContent('2 items'); - expect(getByTestId('run-data-schema-header')).toMatchSnapshot(); + const headers = getAllByTestId('run-data-schema-header'); + expect(headers[0]).toHaveTextContent('If'); + expect(headers[0]).toHaveTextContent('2 items'); + expect(headers[0]).toMatchSnapshot(); }); }); @@ -414,7 +422,7 @@ describe('VirtualSchema.vue', () => { await waitFor(() => { const headers = getAllByTestId('run-data-schema-header'); - expect(headers.length).toBe(2); + expect(headers.length).toBe(3); expect(headers[0]).toHaveTextContent('Input 0'); expect(headers[1]).toHaveTextContent('Inputs 0, 1, 2'); }); @@ -428,6 +436,7 @@ describe('VirtualSchema.vue', () => { fireEvent(window, new MouseEvent('mousemove', { bubbles: true })); expect(reset).toHaveBeenCalled(); + vi.useRealTimers(); vi.useFakeTimers({ toFake: ['setTimeout'] }); fireEvent(window, new MouseEvent('mouseup', { bubbles: true })); vi.advanceTimersByTime(250); @@ -538,18 +547,22 @@ describe('VirtualSchema.vue', () => { }); const { getAllByTestId, queryAllByTestId, rerender } = renderComponent(); + + let headers: HTMLElement[] = []; await waitFor(async () => { - const headers = getAllByTestId('run-data-schema-header'); - expect(headers.length).toBe(2); - expect(getAllByTestId('run-data-schema-item').length).toBe(2); - - // Collapse all nodes - await Promise.all(headers.map(async ($header) => await userEvent.click($header))); - - expect(queryAllByTestId('run-data-schema-item').length).toBe(0); - await rerender({ search: 'John' }); + headers = getAllByTestId('run-data-schema-header'); + expect(headers.length).toBe(3); expect(getAllByTestId('run-data-schema-item').length).toBe(2); }); + + // Collapse all nodes (Variables & context is collapsed by default) + await Promise.all(headers.slice(0, -1).map(async (header) => await userEvent.click(header))); + + expect(queryAllByTestId('run-data-schema-item').length).toBe(0); + + await rerender({ search: 'John' }); + + expect(getAllByTestId('run-data-schema-item').length).toBe(13); }); it('renders preview schema when enabled and available', async () => { @@ -583,11 +596,47 @@ describe('VirtualSchema.vue', () => { const { getAllByTestId, queryAllByText, container } = renderComponent({}); await waitFor(() => { - expect(getAllByTestId('run-data-schema-header')).toHaveLength(2); + expect(getAllByTestId('run-data-schema-header')).toHaveLength(3); }); expect(queryAllByText("No fields - item(s) exist, but they're empty")).toHaveLength(0); expect(getAllByTestId('schema-preview-warning')).toHaveLength(2); expect(container).toMatchSnapshot(); }); + + it('renders variables and context section', async () => { + useWorkflowsStore().pinData({ + node: mockNode1, + data: [], + }); + + const { getAllByTestId, container } = renderComponent({ + props: { + nodes: [{ name: mockNode1.name, indicies: [], depth: 1 }], + }, + }); + + let headers: HTMLElement[] = []; + + await waitFor(() => { + headers = getAllByTestId('run-data-schema-header'); + expect(headers).toHaveLength(2); + expect(headers[1]).toHaveTextContent('Variables and context'); + }); + + await userEvent.click(headers[1]); + + await waitFor(() => { + const items = getAllByTestId('run-data-schema-item'); + + expect(items).toHaveLength(11); + expect(items[0]).toHaveTextContent('$now'); + expect(items[1]).toHaveTextContent('$today'); + expect(items[2]).toHaveTextContent('$vars'); + expect(items[3]).toHaveTextContent('$execution'); + expect(items[7]).toHaveTextContent('$workflow'); + }); + + expect(container).toMatchSnapshot(); + }); }); diff --git a/packages/frontend/editor-ui/src/components/VirtualSchema.vue b/packages/frontend/editor-ui/src/components/VirtualSchema.vue index 7737371231..0bf2900b74 100644 --- a/packages/frontend/editor-ui/src/components/VirtualSchema.vue +++ b/packages/frontend/editor-ui/src/components/VirtualSchema.vue @@ -1,38 +1,53 @@