first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,488 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import {
|
||||
type IDataTableProjectService,
|
||||
NodeOperationError,
|
||||
type AssignmentCollectionValue,
|
||||
type IExecuteFunctions,
|
||||
type INodeTypes,
|
||||
type NodeParameterValueType,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { GoogleSheet } from '../../Google/Sheet/v2/helpers/GoogleSheet';
|
||||
import { Evaluation } from '../Evaluation/Evaluation.node.ee';
|
||||
|
||||
describe('Test Evaluation', () => {
|
||||
const sheetName = 'Sheet5';
|
||||
const spreadsheetId = '1oqFpPgEPTGDw7BPkp1SfPXq3Cb3Hyr1SROtf-Ec4zvA';
|
||||
|
||||
const mockDataTable = mock<IDataTableProjectService>({
|
||||
getColumns: jest.fn(),
|
||||
addColumn: jest.fn(),
|
||||
updateRows: jest.fn(),
|
||||
});
|
||||
|
||||
const mockExecuteFunctions = mock<IExecuteFunctions>({
|
||||
helpers: { getDataTableProxy: jest.fn().mockResolvedValue(mockDataTable) },
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
(mockExecuteFunctions.getInputData as jest.Mock).mockReturnValue([{ json: {} }]);
|
||||
(mockExecuteFunctions.getNode as jest.Mock).mockReturnValue({ typeVersion: 4.6 });
|
||||
(mockExecuteFunctions.getParentNodes as jest.Mock).mockReturnValue([
|
||||
{ type: 'n8n-nodes-base.evaluationTrigger', name: 'Evaluation' },
|
||||
]);
|
||||
(mockExecuteFunctions.evaluateExpression as jest.Mock).mockReturnValue({
|
||||
row_number: 23,
|
||||
foo: 1,
|
||||
bar: 2,
|
||||
_rowsLeft: 2,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
describe('Test Evaluation Node for Set Output', () => {
|
||||
describe('Data tables', () => {
|
||||
test('should have data table methods defined', async () => {
|
||||
const evaluationNode = new Evaluation();
|
||||
expect(evaluationNode.methods.listSearch.dataTableSearch).toBeDefined();
|
||||
expect(evaluationNode.methods.loadOptions.getConditionsForColumn).toBeDefined();
|
||||
expect(evaluationNode.methods.loadOptions.getDataTableColumns).toBeDefined();
|
||||
});
|
||||
|
||||
test('should throw error if output values is empty', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(
|
||||
(key: string, _: number, fallbackValue?: string | number | boolean | object) => {
|
||||
const mockParams: { [key: string]: unknown } = {
|
||||
source: 'dataTable',
|
||||
'outputs.values': [],
|
||||
dataTableId: 'mockDataTableId',
|
||||
operation: 'setOutputs',
|
||||
};
|
||||
return (mockParams[key] ?? fallbackValue) as NodeParameterValueType;
|
||||
},
|
||||
);
|
||||
|
||||
await expect(new Evaluation().execute.call(mockExecuteFunctions)).rejects.toThrow(
|
||||
'No outputs to set',
|
||||
);
|
||||
|
||||
expect(mockDataTable.getColumns).not.toHaveBeenCalled();
|
||||
expect(mockDataTable.addColumn).not.toHaveBeenCalled();
|
||||
expect(mockDataTable.updateRows).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should return empty when there is no parent evaluation trigger', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(
|
||||
(key: string, _: number, fallbackValue?: string | number | boolean | object) => {
|
||||
const mockParams: { [key: string]: unknown } = {
|
||||
source: 'dataTable',
|
||||
'outputs.values': [{ outputName: 'bob', outputValue: 'clam' }],
|
||||
dataTableId: 'mockDataTableId',
|
||||
operation: 'setOutputs',
|
||||
};
|
||||
return (mockParams[key] ?? fallbackValue) as NodeParameterValueType;
|
||||
},
|
||||
);
|
||||
mockExecuteFunctions.getParentNodes.mockReturnValue([]);
|
||||
|
||||
const result = await new Evaluation().execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toEqual([[{ json: {} }]]);
|
||||
|
||||
expect(mockDataTable.getColumns).not.toHaveBeenCalled();
|
||||
expect(mockDataTable.addColumn).not.toHaveBeenCalled();
|
||||
expect(mockDataTable.updateRows).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should update rows and return input data with existing columns', async () => {
|
||||
(mockExecuteFunctions.evaluateExpression as jest.Mock).mockReturnValue({
|
||||
row_id: 23,
|
||||
row_number: 23,
|
||||
foo: 1,
|
||||
bar: 2,
|
||||
_rowsLeft: 2,
|
||||
});
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(
|
||||
(key: string, _: number, fallbackValue?: string | number | boolean | object) => {
|
||||
const mockParams: { [key: string]: unknown } = {
|
||||
source: 'dataTable',
|
||||
'outputs.values': [{ outputName: 'foo', outputValue: 'clam' }],
|
||||
dataTableId: 'mockDataTableId',
|
||||
operation: 'setOutputs',
|
||||
};
|
||||
return (mockParams[key] ?? fallbackValue) as NodeParameterValueType;
|
||||
},
|
||||
);
|
||||
|
||||
mockDataTable.getColumns.mockResolvedValue([
|
||||
{
|
||||
id: '1',
|
||||
index: 0,
|
||||
name: 'foo',
|
||||
type: 'string',
|
||||
dataTableId: 'mockDataTableId',
|
||||
},
|
||||
]);
|
||||
|
||||
await new Evaluation().execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(mockDataTable.getColumns).toHaveBeenCalled();
|
||||
expect(mockDataTable.addColumn).not.toHaveBeenCalled();
|
||||
expect(mockDataTable.updateRows).toHaveBeenCalledWith({
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'id', condition: 'eq', value: 23 }],
|
||||
},
|
||||
data: { foo: 'clam' },
|
||||
});
|
||||
});
|
||||
|
||||
test('should update rows and return input data with new columns', async () => {
|
||||
(mockExecuteFunctions.evaluateExpression as jest.Mock).mockReturnValue({
|
||||
row_id: 23,
|
||||
row_number: 23,
|
||||
foo: 1,
|
||||
bar: 2,
|
||||
_rowsLeft: 2,
|
||||
});
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(
|
||||
(key: string, _: number, fallbackValue?: string | number | boolean | object) => {
|
||||
const mockParams: { [key: string]: unknown } = {
|
||||
source: 'dataTable',
|
||||
'outputs.values': [
|
||||
{ outputName: 'foo', outputValue: 'clam' },
|
||||
{ outputName: 'bar', outputValue: 'baz' },
|
||||
],
|
||||
dataTableId: 'mockDataTableId',
|
||||
operation: 'setOutputs',
|
||||
};
|
||||
return (mockParams[key] ?? fallbackValue) as NodeParameterValueType;
|
||||
},
|
||||
);
|
||||
|
||||
mockDataTable.getColumns.mockResolvedValue([
|
||||
{
|
||||
id: '1',
|
||||
index: 0,
|
||||
name: 'foo',
|
||||
type: 'string',
|
||||
dataTableId: 'mockDataTableId',
|
||||
},
|
||||
]);
|
||||
|
||||
await new Evaluation().execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(mockDataTable.getColumns).toHaveBeenCalled();
|
||||
expect(mockDataTable.addColumn).toHaveBeenCalledWith({
|
||||
name: 'bar',
|
||||
type: 'string',
|
||||
});
|
||||
expect(mockDataTable.updateRows).toHaveBeenCalledWith({
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'id', condition: 'eq', value: 23 }],
|
||||
},
|
||||
data: { foo: 'clam', bar: 'baz' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Google Sheets', () => {
|
||||
jest.spyOn(GoogleSheet.prototype, 'spreadsheetGetSheet').mockImplementation(async () => {
|
||||
return { sheetId: 1, title: sheetName };
|
||||
});
|
||||
|
||||
jest.spyOn(GoogleSheet.prototype, 'updateRows').mockImplementation(async () => {
|
||||
return { sheetId: 1, title: sheetName };
|
||||
});
|
||||
|
||||
jest.spyOn(GoogleSheet.prototype, 'batchUpdate').mockImplementation(async () => {
|
||||
return { sheetId: 1, title: sheetName };
|
||||
});
|
||||
|
||||
test('credential test for googleApi should be in methods', async () => {
|
||||
const evaluationNode = new Evaluation();
|
||||
expect(evaluationNode.methods.credentialTest.googleApiCredentialTest).toBeDefined();
|
||||
});
|
||||
|
||||
test('should throw error if output values is empty', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(
|
||||
(key: string, _: number, fallbackValue?: string | number | boolean | object) => {
|
||||
const mockParams: { [key: string]: unknown } = {
|
||||
'outputs.values': [],
|
||||
documentId: {
|
||||
mode: 'id',
|
||||
value: spreadsheetId,
|
||||
},
|
||||
sheetName,
|
||||
sheetMode: 'id',
|
||||
operation: 'setOutputs',
|
||||
};
|
||||
return (mockParams[key] ?? fallbackValue) as NodeParameterValueType;
|
||||
},
|
||||
);
|
||||
|
||||
await expect(new Evaluation().execute.call(mockExecuteFunctions)).rejects.toThrow(
|
||||
'No outputs to set',
|
||||
);
|
||||
|
||||
expect(GoogleSheet.prototype.updateRows).not.toBeCalled();
|
||||
|
||||
expect(GoogleSheet.prototype.batchUpdate).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('should update rows and return input data for existing headers', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(
|
||||
(key: string, _: number, fallbackValue?: string | number | boolean | object) => {
|
||||
const mockParams: { [key: string]: unknown } = {
|
||||
source: 'googleSheets',
|
||||
'outputs.values': [{ outputName: 'foo', outputValue: 'clam' }],
|
||||
documentId: {
|
||||
mode: 'id',
|
||||
value: spreadsheetId,
|
||||
},
|
||||
sheetName,
|
||||
sheetMode: 'id',
|
||||
operation: 'setOutputs',
|
||||
};
|
||||
return (mockParams[key] ?? fallbackValue) as NodeParameterValueType;
|
||||
},
|
||||
);
|
||||
|
||||
await new Evaluation().execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(GoogleSheet.prototype.updateRows).toHaveBeenCalledWith(
|
||||
sheetName,
|
||||
[['foo', 'bar']],
|
||||
'RAW',
|
||||
1,
|
||||
);
|
||||
|
||||
expect(GoogleSheet.prototype.batchUpdate).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
range: 'Sheet5!A23',
|
||||
values: [['clam']],
|
||||
},
|
||||
],
|
||||
'RAW',
|
||||
);
|
||||
});
|
||||
|
||||
test('should return empty when there is no parent evaluation trigger', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(
|
||||
(key: string, _: number, fallbackValue?: string | number | boolean | object) => {
|
||||
const mockParams: { [key: string]: unknown } = {
|
||||
source: 'googleSheets',
|
||||
'outputs.values': [{ outputName: 'bob', outputValue: 'clam' }],
|
||||
documentId: {
|
||||
mode: 'id',
|
||||
value: spreadsheetId,
|
||||
},
|
||||
sheetName,
|
||||
sheetMode: 'id',
|
||||
operation: 'setOutputs',
|
||||
};
|
||||
return (mockParams[key] ?? fallbackValue) as NodeParameterValueType;
|
||||
},
|
||||
);
|
||||
mockExecuteFunctions.getParentNodes.mockReturnValue([]);
|
||||
|
||||
const result = await new Evaluation().execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toEqual([[{ json: {} }]]);
|
||||
|
||||
expect(GoogleSheet.prototype.updateRows).not.toBeCalled();
|
||||
|
||||
expect(GoogleSheet.prototype.batchUpdate).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('should update rows and return input data for new headers', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(
|
||||
(key: string, _: number, fallbackValue?: string | number | boolean | object) => {
|
||||
const mockParams: { [key: string]: unknown } = {
|
||||
source: 'googleSheets',
|
||||
'outputs.values': [{ outputName: 'bob', outputValue: 'clam' }],
|
||||
documentId: {
|
||||
mode: 'id',
|
||||
value: spreadsheetId,
|
||||
},
|
||||
sheetName,
|
||||
sheetMode: 'id',
|
||||
operation: 'setOutputs',
|
||||
};
|
||||
return (mockParams[key] ?? fallbackValue) as NodeParameterValueType;
|
||||
},
|
||||
);
|
||||
|
||||
await new Evaluation().execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(GoogleSheet.prototype.updateRows).toHaveBeenCalledWith(
|
||||
sheetName,
|
||||
[['foo', 'bar', 'bob']],
|
||||
'RAW',
|
||||
1,
|
||||
);
|
||||
|
||||
expect(GoogleSheet.prototype.batchUpdate).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
range: 'Sheet5!C23',
|
||||
values: [['clam']],
|
||||
},
|
||||
],
|
||||
'RAW',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test Evaluation Node for Set Metrics', () => {
|
||||
const nodeTypes = mock<INodeTypes>();
|
||||
const evaluationMetricsNode = new Evaluation();
|
||||
|
||||
let mockExecuteFunction: IExecuteFunctions;
|
||||
|
||||
function getMockExecuteFunction(metrics: AssignmentCollectionValue['assignments']) {
|
||||
return {
|
||||
getInputData: jest.fn().mockReturnValue([{}]),
|
||||
|
||||
getNodeParameter: jest.fn((param: string, _: number) => {
|
||||
if (param === 'metrics') {
|
||||
return { assignments: metrics };
|
||||
}
|
||||
if (param === 'operation') {
|
||||
return 'setMetrics';
|
||||
}
|
||||
if (param === 'metric') {
|
||||
return 'customMetrics';
|
||||
}
|
||||
return param;
|
||||
}),
|
||||
|
||||
getNode: jest.fn().mockReturnValue({
|
||||
typeVersion: 1,
|
||||
}),
|
||||
} as unknown as IExecuteFunctions;
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
mockExecuteFunction = getMockExecuteFunction([
|
||||
{
|
||||
id: '1',
|
||||
name: 'Accuracy',
|
||||
value: 0.95,
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Latency',
|
||||
value: 100,
|
||||
type: 'number',
|
||||
},
|
||||
]);
|
||||
nodeTypes.getByName.mockReturnValue(evaluationMetricsNode);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should output the defined metrics', async () => {
|
||||
const result = await evaluationMetricsNode.execute.call(mockExecuteFunction);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toHaveLength(1);
|
||||
|
||||
const outputItem = result[0][0].json;
|
||||
expect(outputItem).toEqual({
|
||||
Accuracy: 0.95,
|
||||
Latency: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle no metrics defined', async () => {
|
||||
mockExecuteFunction = getMockExecuteFunction([]);
|
||||
const result = await evaluationMetricsNode.execute.call(mockExecuteFunction);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toHaveLength(1);
|
||||
expect(result[0][0].json).toEqual({});
|
||||
});
|
||||
|
||||
it('should convert string values to numbers', async () => {
|
||||
const mockExecuteWithStringValues = getMockExecuteFunction([
|
||||
{
|
||||
id: '1',
|
||||
name: 'Accuracy',
|
||||
value: '0.95',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Latency',
|
||||
value: '100',
|
||||
type: 'number',
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await evaluationMetricsNode.execute.call(mockExecuteWithStringValues);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toHaveLength(1);
|
||||
|
||||
const outputItem = result[0][0].json;
|
||||
expect(outputItem).toEqual({
|
||||
Accuracy: 0.95,
|
||||
Latency: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error for non-numeric string values', async () => {
|
||||
const mockExecuteWithInvalidValue = getMockExecuteFunction([
|
||||
{
|
||||
id: '1',
|
||||
name: 'Accuracy',
|
||||
value: 'not-a-number',
|
||||
type: 'number',
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
evaluationMetricsNode.execute.call(mockExecuteWithInvalidValue),
|
||||
).rejects.toThrow(NodeOperationError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test Evaluation Node for Check If Evaluating', () => {
|
||||
beforeEach(() => {
|
||||
(mockExecuteFunctions.getInputData as jest.Mock).mockReturnValue([{ json: {} }]);
|
||||
(mockExecuteFunctions.getNode as jest.Mock).mockReturnValue({ typeVersion: 4.6 });
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(
|
||||
(key: string, _: number, fallbackValue?: string | number | boolean | object) => {
|
||||
const mockParams: { [key: string]: unknown } = {
|
||||
operation: 'checkIfEvaluating',
|
||||
};
|
||||
return (mockParams[key] ?? fallbackValue) as NodeParameterValueType;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
test('should return output in normal branch if normal execution', async () => {
|
||||
(mockExecuteFunctions.getParentNodes as jest.Mock).mockReturnValue([]);
|
||||
const result = await new Evaluation().execute.call(mockExecuteFunctions);
|
||||
expect(result).toEqual([[], [{ json: {} }]]);
|
||||
});
|
||||
|
||||
test('should return output in evaluation branch if evaluation execution', async () => {
|
||||
(mockExecuteFunctions.getParentNodes as jest.Mock).mockReturnValue([
|
||||
{ type: 'n8n-nodes-base.evaluationTrigger', name: 'Evaluation' },
|
||||
]);
|
||||
|
||||
const result = await new Evaluation().execute.call(mockExecuteFunctions);
|
||||
expect(result).toEqual([[{ json: {} }], []]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user