From 891ecb798f287d209b3ebec9a58f46a88cea322b Mon Sep 17 00:00:00 2001 From: Milorad Filipovic Date: Tue, 4 Mar 2025 14:22:57 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20Adding=20RLC=20unit=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ResourceLocator.test.constants.ts | 84 ++++++++ .../ResourceLocator/ResourceLocator.test.ts | 189 ++++++++++++++++++ 2 files changed, 273 insertions(+) create mode 100644 packages/frontend/editor-ui/src/components/ResourceLocator/ResourceLocator.test.constants.ts create mode 100644 packages/frontend/editor-ui/src/components/ResourceLocator/ResourceLocator.test.ts diff --git a/packages/frontend/editor-ui/src/components/ResourceLocator/ResourceLocator.test.constants.ts b/packages/frontend/editor-ui/src/components/ResourceLocator/ResourceLocator.test.constants.ts new file mode 100644 index 0000000000..57d161b752 --- /dev/null +++ b/packages/frontend/editor-ui/src/components/ResourceLocator/ResourceLocator.test.constants.ts @@ -0,0 +1,84 @@ +import type { INode, INodeParameterResourceLocator, INodeProperties } from 'n8n-workflow'; + +export const TEST_MODEL_VALUE: INodeParameterResourceLocator = { + __rl: true, + value: 'test', + mode: 'list', + cachedResultName: 'table', + cachedResultUrl: 'https://test.com/test', +}; + +export const TEST_PARAMETER_MULTI_MODE: INodeProperties = { + displayName: 'Test Parameter', + name: 'testParamMultiMode', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { searchListMethod: 'testSearch', searchable: true }, + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + placeholder: 'https://test.com/test', + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + placeholder: 'id', + }, + ], +}; + +export const TEST_PARAMETER_SINGLE_MODE: INodeProperties = { + ...TEST_PARAMETER_MULTI_MODE, + name: 'testParameterSingleMode', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { searchListMethod: 'testSearch', searchable: true }, + }, + ], +}; + +export const TEST_NODE_MULTI_MODE: INode = { + type: 'n8n-nodes-base.airtable', + typeVersion: 2.1, + position: [80, -260], + id: '377e4287-b1e0-44cc-ba0f-7bb3d676d60c', + name: 'Test Node - Multi Mode', + parameters: { + authentication: 'testAuth', + resource: 'test', + operation: 'get', + testParamMultiMode: TEST_MODEL_VALUE, + id: '', + options: {}, + }, + credentials: { + testAuth: { + id: '1234', + name: 'Test Account', + }, + }, +}; + +export const TEST_NODE_SINGLE_MODE: INode = { + ...TEST_NODE_MULTI_MODE, + parameters: { + authentication: 'testAuth', + resource: 'test', + operation: 'get', + testParameterSingleMode: TEST_MODEL_VALUE, + id: '', + options: {}, + }, +}; diff --git a/packages/frontend/editor-ui/src/components/ResourceLocator/ResourceLocator.test.ts b/packages/frontend/editor-ui/src/components/ResourceLocator/ResourceLocator.test.ts new file mode 100644 index 0000000000..c8f5a8de01 --- /dev/null +++ b/packages/frontend/editor-ui/src/components/ResourceLocator/ResourceLocator.test.ts @@ -0,0 +1,189 @@ +import { createComponentRenderer } from '@/__tests__/render'; +import { useNodeTypesStore } from '@/stores/nodeTypes.store'; +import ResourceLocator from './ResourceLocator.vue'; +import { createTestingPinia } from '@pinia/testing'; +import userEvent from '@testing-library/user-event'; +import { screen, waitFor } from '@testing-library/vue'; +import { mockedStore } from '@/__tests__/utils'; +import { + TEST_MODEL_VALUE, + TEST_NODE_MULTI_MODE, + TEST_NODE_SINGLE_MODE, + TEST_PARAMETER_MULTI_MODE, + TEST_PARAMETER_SINGLE_MODE, +} from './ResourceLocator.test.constants'; + +vi.mock('vue-router', async () => { + const actual = await vi.importActual('vue-router'); + const params = {}; + const location = {}; + return { + ...actual, + useRouter: () => ({ + push: vi.fn(), + }), + useRoute: () => ({ + params, + location, + }), + }; +}); +vi.mock('@/composables/useWorkflowHelpers', () => { + return { + useWorkflowHelpers: vi.fn(() => ({ + resolveExpression: vi.fn().mockImplementation((val) => val), + resolveRequiredParameters: vi.fn().mockImplementation((_, params) => params), + })), + }; +}); +vi.mock('@/composables/useTelemetry', () => ({ + useTelemetry: () => ({ track: vi.fn() }), +})); + +let nodeTypesStore: ReturnType>; + +const renderComponent = createComponentRenderer(ResourceLocator, { + props: { + modelValue: TEST_MODEL_VALUE, + parameter: TEST_PARAMETER_MULTI_MODE, + path: `parameters.${TEST_PARAMETER_MULTI_MODE.name}`, + node: TEST_NODE_MULTI_MODE, + displayTitle: 'Test Resource Locator', + expressionComputedValue: '', + isValueExpression: false, + }, + global: { + stubs: { + ResourceLocatorDropdown: false, + ExpressionParameterInput: true, + ParameterIssues: true, + N8nCallout: true, + 'font-awesome-icon': true, + FromAiOverrideField: true, + FromAiOverrideButton: true, + ParameterOverrideSelectableList: true, + }, + }, +}); + +describe('ResourceLocator', () => { + beforeEach(() => { + createTestingPinia(); + nodeTypesStore = mockedStore(useNodeTypesStore); + nodeTypesStore.getNodeType = vi.fn().mockReturnValue({ displayName: 'Test Node' }); + }); + afterEach(() => { + vi.clearAllMocks(); + }); + + it('renders multi-mode correctly', async () => { + const { getByTestId } = renderComponent(); + expect(getByTestId(`resource-locator-${TEST_PARAMETER_MULTI_MODE.name}`)).toBeInTheDocument(); + // Should render mode selector with all available modes + expect(getByTestId('rlc-mode-selector')).toBeInTheDocument(); + await userEvent.click(getByTestId('rlc-mode-selector')); + TEST_PARAMETER_MULTI_MODE.modes?.forEach((mode) => { + expect(screen.getByTestId(`mode-${mode.name}`)).toBeInTheDocument(); + }); + }); + + it('renders single mode correctly', async () => { + const { getByTestId, queryByTestId } = renderComponent({ + props: { + modelValue: TEST_MODEL_VALUE, + parameter: TEST_PARAMETER_SINGLE_MODE, + path: `parameters.${TEST_PARAMETER_SINGLE_MODE.name}`, + node: TEST_NODE_SINGLE_MODE, + displayTitle: 'Test Resource Locator', + expressionComputedValue: '', + }, + }); + expect(getByTestId(`resource-locator-${TEST_PARAMETER_SINGLE_MODE.name}`)).toBeInTheDocument(); + // Should not render mode selector + expect(queryByTestId('rlc-mode-selector')).not.toBeInTheDocument(); + }); + + it('renders fetched resources correctly', async () => { + const TEST_ITEMS = [ + { name: 'Test Resource', value: 'test-resource', url: 'https://test.com/test-resource' }, + { + name: 'Test Resource 2', + value: 'test-resource-2', + url: 'https://test.com/test-resource-2', + }, + ]; + nodeTypesStore.getResourceLocatorResults.mockResolvedValue({ + results: TEST_ITEMS, + paginationToken: null, + }); + const { getByTestId, getByText, getAllByTestId } = renderComponent(); + + expect(getByTestId(`resource-locator-${TEST_PARAMETER_MULTI_MODE.name}`)).toBeInTheDocument(); + // Click on the input to fetch resources + await userEvent.click(getByTestId('rlc-input')); + // Wait for the resources to be fetched + await waitFor(() => { + expect(nodeTypesStore.getResourceLocatorResults).toHaveBeenCalled(); + }); + // Expect the items to be rendered + expect(getAllByTestId('rlc-item')).toHaveLength(TEST_ITEMS.length); + // We should be getting one item for each result + TEST_ITEMS.forEach((item) => { + expect(getByText(item.name)).toBeInTheDocument(); + }); + }); + + it('renders permission error correctly', async () => { + const TEST_401_ERROR = { + message: 'Failed to load resources', + httpCode: '401', + description: 'Authentication failed. Please check your credentials.', + }; + + nodeTypesStore.getResourceLocatorResults.mockRejectedValue(TEST_401_ERROR); + + const { getByTestId, findByTestId } = renderComponent(); + + expect(getByTestId(`resource-locator-${TEST_PARAMETER_MULTI_MODE.name}`)).toBeInTheDocument(); + + await userEvent.click(getByTestId('rlc-input')); + await waitFor(() => { + expect(nodeTypesStore.getResourceLocatorResults).toHaveBeenCalled(); + }); + + const errorContainer = await findByTestId('rlc-error-container'); + expect(errorContainer).toBeInTheDocument(); + + expect(errorContainer).toHaveTextContent(TEST_401_ERROR.httpCode); + expect(errorContainer).toHaveTextContent(TEST_401_ERROR.message); + + expect(getByTestId('permission-error-link')).toBeInTheDocument(); + }); + + it('renders generic error correctly', async () => { + const TEST_500_ERROR = { + message: 'Whoops', + httpCode: '500', + description: 'Something went wrong. Please try again later.', + }; + + nodeTypesStore.getResourceLocatorResults.mockRejectedValue(TEST_500_ERROR); + const { getByTestId, findByTestId, queryByTestId } = renderComponent(); + + expect(getByTestId(`resource-locator-${TEST_PARAMETER_MULTI_MODE.name}`)).toBeInTheDocument(); + + await userEvent.click(getByTestId('rlc-input')); + + await waitFor(() => { + expect(nodeTypesStore.getResourceLocatorResults).toHaveBeenCalled(); + }); + + const errorContainer = await findByTestId('rlc-error-container'); + expect(errorContainer).toBeInTheDocument(); + + expect(errorContainer).toHaveTextContent(TEST_500_ERROR.httpCode); + expect(errorContainer).toHaveTextContent(TEST_500_ERROR.message); + + expect(queryByTestId('permission-error-link')).not.toBeInTheDocument(); + }); +});