n8n/packages/editor-ui/src/components/RunData.test.ts

690 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createTestWorkflowObject, defaultNodeDescriptions } from '@/__tests__/mocks';
import { createComponentRenderer } from '@/__tests__/render';
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
import RunData from '@/components/RunData.vue';
import { SET_NODE_TYPE, STORES } from '@/constants';
import type { INodeUi, IRunDataDisplayMode, NodePanelType } from '@/Interface';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { createTestingPinia } from '@pinia/testing';
import userEvent from '@testing-library/user-event';
import { waitFor } from '@testing-library/vue';
import type { INodeExecutionData, ITaskData, ITaskMetadata } from 'n8n-workflow';
import { setActivePinia } from 'pinia';
import { useNodeTypesStore } from '../stores/nodeTypes.store';
const MOCK_EXECUTION_URL = 'execution.url/123';
const { trackOpeningRelatedExecution, resolveRelatedExecutionUrl } = vi.hoisted(() => ({
trackOpeningRelatedExecution: vi.fn(),
resolveRelatedExecutionUrl: vi.fn(),
}));
vi.mock('vue-router', () => {
return {
useRouter: () => ({}),
useRoute: () => ({ meta: {} }),
RouterLink: vi.fn(),
};
});
vi.mock('@/composables/useExecutionHelpers', () => ({
useExecutionHelpers: () => ({
trackOpeningRelatedExecution,
resolveRelatedExecutionUrl,
}),
}));
describe('RunData', () => {
beforeAll(() => {
resolveRelatedExecutionUrl.mockReturnValue('execution.url/123');
});
it("should render pin button in output panel disabled when there's binary data", () => {
const { getByTestId } = render({
defaultRunItems: [
{
json: {},
binary: {
data: {
fileName: 'test.xyz',
mimeType: 'application/octet-stream',
data: '',
},
},
},
],
displayMode: 'binary',
});
expect(getByTestId('ndv-pin-data')).toBeInTheDocument();
expect(getByTestId('ndv-pin-data')).toHaveAttribute('disabled');
});
it("should not render pin button in input panel when there's binary data", () => {
const { queryByTestId } = render({
defaultRunItems: [
{
json: {},
binary: {
data: {
fileName: 'test.xyz',
mimeType: 'application/octet-stream',
data: '',
},
},
},
],
displayMode: 'binary',
paneType: 'input',
});
expect(queryByTestId('ndv-pin-data')).not.toBeInTheDocument();
});
it('should render data correctly even when "item.json" has another "json" key', async () => {
const { getByText, getAllByTestId, getByTestId } = render({
defaultRunItems: [
{
json: {
id: 1,
name: 'Test 1',
json: {
data: 'Json data 1',
},
},
},
{
json: {
id: 2,
name: 'Test 2',
json: {
data: 'Json data 2',
},
},
},
],
displayMode: 'schema',
});
await userEvent.click(getByTestId('ndv-pin-data'));
await waitFor(() => getAllByTestId('run-data-schema-item'), { timeout: 1000 });
expect(getByText('Test 1')).toBeInTheDocument();
expect(getByText('Json data 1')).toBeInTheDocument();
});
it('should render view and download buttons for PDFs', async () => {
const { getByTestId } = render({
defaultRunItems: [
{
json: {},
binary: {
data: {
fileName: 'test.pdf',
fileType: 'pdf',
mimeType: 'application/pdf',
data: '',
},
},
},
],
displayMode: 'binary',
});
await waitFor(() => {
expect(getByTestId('ndv-view-binary-data')).toBeInTheDocument();
expect(getByTestId('ndv-download-binary-data')).toBeInTheDocument();
expect(getByTestId('ndv-binary-data_0')).toBeInTheDocument();
});
});
it('should not render a view button for unknown content-type', async () => {
const { getByTestId, queryByTestId } = render({
defaultRunItems: [
{
json: {},
binary: {
data: {
fileName: 'test.xyz',
mimeType: 'application/octet-stream',
data: '',
},
},
},
],
displayMode: 'binary',
});
await waitFor(() => {
expect(queryByTestId('ndv-view-binary-data')).not.toBeInTheDocument();
expect(getByTestId('ndv-download-binary-data')).toBeInTheDocument();
expect(getByTestId('ndv-binary-data_0')).toBeInTheDocument();
});
});
it('should not render pin data button when there is no output data', async () => {
const { queryByTestId } = render({ defaultRunItems: [], displayMode: 'table' });
expect(queryByTestId('ndv-pin-data')).not.toBeInTheDocument();
});
it('should disable pin data button when data is pinned', async () => {
const { getByTestId } = render({
defaultRunItems: [],
displayMode: 'table',
pinnedData: [{ json: { name: 'Test' } }],
});
const pinDataButton = getByTestId('ndv-pin-data');
expect(pinDataButton).toBeDisabled();
});
it('should render callout when data is pinned in output panel', async () => {
const { getByTestId } = render({
defaultRunItems: [],
displayMode: 'table',
pinnedData: [{ json: { name: 'Test' } }],
paneType: 'output',
});
const pinnedDataCallout = getByTestId('ndv-pinned-data-callout');
expect(pinnedDataCallout).toBeInTheDocument();
});
it('should not render callout when data is pinned in input panel', async () => {
const { queryByTestId } = render({
defaultRunItems: [],
displayMode: 'table',
pinnedData: [{ json: { name: 'Test' } }],
paneType: 'input',
});
const pinnedDataCallout = queryByTestId('ndv-pinned-data-callout');
expect(pinnedDataCallout).not.toBeInTheDocument();
});
it('should enable pin data button when data is not pinned', async () => {
const { getByTestId } = render({
defaultRunItems: [{ json: { name: 'Test' } }],
displayMode: 'table',
});
const pinDataButton = getByTestId('ndv-pin-data');
expect(pinDataButton).toBeEnabled();
});
it('should not render pagination on binary tab', async () => {
const { queryByTestId } = render({
defaultRunItems: Array.from({ length: 11 }).map((_, i) => ({
json: {
data: {
id: i,
name: `Test ${i}`,
},
},
binary: {
data: {
a: 'b',
data: '',
mimeType: '',
},
},
})),
displayMode: 'binary',
});
expect(queryByTestId('ndv-data-pagination')).not.toBeInTheDocument();
});
it('should render pagination with binary data on non-binary tab', async () => {
const { getByTestId } = render({
defaultRunItems: Array.from({ length: 11 }).map((_, i) => ({
json: {
data: {
id: i,
name: `Test ${i}`,
},
},
binary: {
data: {
a: 'b',
data: '',
mimeType: '',
},
},
})),
displayMode: 'json',
});
expect(getByTestId('ndv-data-pagination')).toBeInTheDocument();
});
it('should render sub-execution link in header', async () => {
const metadata: ITaskMetadata = {
subExecution: {
workflowId: 'xyz',
executionId: '123',
},
subExecutionsCount: 1,
};
const { getByTestId } = render({
defaultRunItems: [
{
json: {},
},
],
displayMode: 'table',
paneType: 'output',
metadata,
});
expect(getByTestId('related-execution-link')).toBeInTheDocument();
expect(getByTestId('related-execution-link')).toHaveTextContent('View sub-execution');
expect(resolveRelatedExecutionUrl).toHaveBeenCalledWith(metadata);
expect(getByTestId('related-execution-link')).toHaveAttribute('href', MOCK_EXECUTION_URL);
expect(getByTestId('ndv-items-count')).toHaveTextContent('1 item, 1 sub-execution');
getByTestId('related-execution-link').click();
expect(trackOpeningRelatedExecution).toHaveBeenCalledWith(metadata, 'table');
});
it('should render parent-execution link in header', async () => {
const metadata: ITaskMetadata = {
parentExecution: {
workflowId: 'xyz',
executionId: '123',
},
};
const { getByTestId } = render({
defaultRunItems: [
{
json: {},
},
],
displayMode: 'table',
paneType: 'output',
metadata,
});
expect(getByTestId('related-execution-link')).toBeInTheDocument();
expect(getByTestId('related-execution-link')).toHaveTextContent('View parent execution');
expect(resolveRelatedExecutionUrl).toHaveBeenCalledWith(metadata);
expect(getByTestId('related-execution-link')).toHaveAttribute('href', MOCK_EXECUTION_URL);
expect(getByTestId('ndv-items-count')).toHaveTextContent('1 item');
getByTestId('related-execution-link').click();
expect(trackOpeningRelatedExecution).toHaveBeenCalledWith(metadata, 'table');
});
it('should render sub-execution link in header with multiple items', async () => {
const metadata: ITaskMetadata = {
subExecution: {
workflowId: 'xyz',
executionId: '123',
},
subExecutionsCount: 3,
};
const { getByTestId } = render({
defaultRunItems: [
{
json: {},
},
{
json: {},
},
],
displayMode: 'json',
paneType: 'output',
metadata,
});
expect(getByTestId('related-execution-link')).toBeInTheDocument();
expect(getByTestId('related-execution-link')).toHaveTextContent('View sub-execution 123');
expect(resolveRelatedExecutionUrl).toHaveBeenCalledWith(metadata);
expect(getByTestId('related-execution-link')).toHaveAttribute('href', MOCK_EXECUTION_URL);
expect(getByTestId('ndv-items-count')).toHaveTextContent('2 items, 3 sub-executions');
getByTestId('related-execution-link').click();
expect(trackOpeningRelatedExecution).toHaveBeenCalledWith(metadata, 'json');
});
it('should render sub-execution link in header with multiple runs', async () => {
const metadata: ITaskMetadata = {
subExecution: {
workflowId: 'xyz',
executionId: '123',
},
subExecutionsCount: 3,
};
const { getByTestId, queryByTestId } = render({
runs: [
{
startTime: new Date().getTime(),
executionTime: new Date().getTime(),
data: {
main: [[{ json: {} }]],
},
source: [null],
metadata,
},
{
startTime: new Date().getTime(),
executionTime: new Date().getTime(),
data: {
main: [[{ json: {} }]],
},
source: [null],
metadata,
},
],
displayMode: 'json',
paneType: 'output',
metadata,
});
expect(getByTestId('related-execution-link')).toBeInTheDocument();
expect(getByTestId('related-execution-link')).toHaveTextContent('View sub-execution 123');
expect(queryByTestId('ndv-items-count')).not.toBeInTheDocument();
expect(getByTestId('run-selector')).toBeInTheDocument();
getByTestId('related-execution-link').click();
expect(trackOpeningRelatedExecution).toHaveBeenCalledWith(metadata, 'json');
});
it('should render sub-execution link in header with sub-node error', async () => {
const metadata = {
subExecution: {
workflowId: 'xyz',
executionId: '123',
},
subExecutionsCount: 1,
};
const { getByTestId } = render({
defaultRunItems: [
{
json: {},
},
],
displayMode: 'table',
paneType: 'output',
runs: [
{
hints: [],
startTime: 1737643696893,
executionTime: 2,
source: [
{
previousNode: 'When clicking Test workflow',
},
],
executionStatus: 'error',
error: {
level: 'error',
errorResponse: {
...metadata.subExecution,
},
} as never,
},
],
});
expect(getByTestId('related-execution-link')).toBeInTheDocument();
expect(getByTestId('related-execution-link')).toHaveTextContent('View sub-execution');
expect(resolveRelatedExecutionUrl).toHaveBeenCalledWith(metadata);
expect(getByTestId('related-execution-link')).toHaveAttribute('href', MOCK_EXECUTION_URL);
expect(getByTestId('ndv-items-count')).toHaveTextContent(
'1 item, 1 sub-execution View sub-execution',
);
getByTestId('related-execution-link').click();
expect(trackOpeningRelatedExecution).toHaveBeenCalledWith(metadata, 'table');
});
it('should render input selector when input node has error', async () => {
const testNodes = [
{
id: '1',
name: 'When clicking Test workflow',
type: 'n8n-nodes-base.manualTrigger',
typeVersion: 1,
position: [80, -180],
disabled: false,
parameters: { notice: '' },
},
{
id: '2',
name: 'Edit Fields',
type: 'n8n-nodes-base.set',
parameters: {
mode: 'manual',
duplicateItem: false,
assignments: {
_custom: { type: 'reactive', stateTypeName: 'Reactive', value: {} },
},
includeOtherFields: false,
options: {},
},
typeVersion: 3.4,
position: [500, -180],
},
{
id: '3',
name: 'Test Node',
type: 'n8n-nodes-base.code',
parameters: {
mode: 'runOnceForAllItems',
language: 'javaScript',
jsCode: "throw Error('yo')",
notice: '',
},
typeVersion: 2,
position: [300, -180],
issues: {
_custom: {
type: 'reactive',
stateTypeName: 'Reactive',
value: { execution: true },
},
},
},
] as INodeUi[];
const { getByTestId } = render({
workflowNodes: testNodes,
runs: [
{
hints: [],
startTime: 1737643696893,
executionTime: 2,
source: [
{
previousNode: 'When clicking Test workflow',
},
],
executionStatus: 'error',
// @ts-expect-error allow missing properties in test
error: {
level: 'error',
tags: {
packageName: 'nodes-base',
},
description: null,
lineNumber: 1,
node: {
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [300, -180],
id: 'e41f12e0-d178-4294-8748-da5a6a531be6',
name: 'Test Node',
parameters: {
mode: 'runOnceForAllItems',
language: 'javaScript',
jsCode: "throw Error('yo')",
notice: '',
},
},
message: 'yo [line 1]',
stack: 'Error: yo\n n8n/packages/core/src/execution-engine/workflow-execute.ts:2066:11',
},
},
],
defaultRunItems: [
{
hints: [],
startTime: 1737641598215,
executionTime: 3,
// @ts-expect-error allow missing properties in test
source: [{ previousNode: 'Execute Workflow Trigger' }],
// @ts-expect-error allow missing properties in test
executionStatus: 'error',
// @ts-expect-error allow missing properties in test
error: {
level: 'error',
tags: { packageName: 'nodes-base' },
description: null,
lineNumber: 1,
node: {
id: 'e41f12e0-d178-4294-8748-da5a6a531be6',
name: 'Test Node',
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [300, -180],
parameters: {
mode: 'runOnceForAllItems',
language: 'javaScript',
jsCode: "throw Error('yo')",
notice: '',
},
},
message: 'yo [line 1]',
stack: 'Error: yo\n n8n/packages/core/src/execution-engine/workflow-execute.ts:2066:11',
},
},
],
});
expect(getByTestId('ndv-items-count')).toBeInTheDocument();
});
// Default values for the render function
const nodes = [
{
id: '1',
typeVersion: 3,
name: 'Test Node',
position: [0, 0],
type: SET_NODE_TYPE,
parameters: {},
},
] as INodeUi[];
const render = ({
defaultRunItems,
workflowNodes = nodes,
displayMode,
pinnedData,
paneType = 'output',
metadata,
runs,
}: {
defaultRunItems?: INodeExecutionData[];
workflowNodes?: INodeUi[];
displayMode: IRunDataDisplayMode;
pinnedData?: INodeExecutionData[];
paneType?: NodePanelType;
metadata?: ITaskMetadata;
runs?: ITaskData[];
}) => {
const defaultRun: ITaskData = {
startTime: new Date().getTime(),
executionTime: new Date().getTime(),
data: {
main: [defaultRunItems ?? [{ json: {} }]],
},
source: [null],
metadata,
};
const pinia = createTestingPinia({
stubActions: false,
initialState: {
[STORES.SETTINGS]: SETTINGS_STORE_DEFAULT_STATE,
[STORES.NDV]: {
outputPanelDisplayMode: displayMode,
activeNodeName: 'Test Node',
},
[STORES.WORKFLOWS]: {
workflow: {
workflowNodes,
},
workflowExecutionData: {
id: '1',
finished: true,
mode: 'trigger',
startedAt: new Date(),
workflowData: {
id: '1',
name: 'Test Workflow',
versionId: '1',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
active: false,
nodes: [],
connections: {},
},
data: {
resultData: {
runData: {
'Test Node': runs ?? [defaultRun],
},
},
},
},
},
},
});
setActivePinia(pinia);
const workflowsStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore();
nodeTypesStore.setNodeTypes(defaultNodeDescriptions);
vi.mocked(workflowsStore).getNodeByName.mockReturnValue(workflowNodes[0]);
if (pinnedData) {
vi.mocked(workflowsStore).pinDataByNodeName.mockReturnValue(pinnedData);
}
return createComponentRenderer(RunData, {
props: {
node: {
name: 'Test Node',
},
workflow: createTestWorkflowObject({
// @ts-expect-error allow missing properties in test
workflowNodes,
}),
},
global: {
stubs: {
RunDataPinButton: { template: '<button data-test-id="ndv-pin-data"></button>' },
},
},
})({
props: {
node: {
id: '1',
name: 'Test Node',
type: SET_NODE_TYPE,
position: [0, 0],
},
nodes: [{ name: 'Test Node', indicies: [], depth: 1 }],
runIndex: 0,
paneType,
isExecuting: false,
mappingEnabled: true,
distanceFromActive: 0,
},
pinia,
});
};
});