Adding RLC unit tests

This commit is contained in:
Milorad Filipovic 2025-03-04 14:22:57 +01:00
parent 9ba9443460
commit 891ecb798f
No known key found for this signature in database
2 changed files with 273 additions and 0 deletions

View file

@ -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: {},
},
};

View file

@ -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<typeof mockedStore<typeof useNodeTypesStore>>;
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();
});
});