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,61 @@
|
||||
import type { MockProxy } from 'jest-mock-extended';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../../../v2/actions/sheet/append.operation';
|
||||
import type { GoogleSheet } from '../../../v2/helpers/GoogleSheet';
|
||||
|
||||
describe('Google Sheet - Append', () => {
|
||||
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
|
||||
let mockGoogleSheet: MockProxy<GoogleSheet>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mock<IExecuteFunctions>();
|
||||
mockGoogleSheet = mock<GoogleSheet>();
|
||||
});
|
||||
|
||||
it('should insert input data if sheet is empty', async () => {
|
||||
const inputData = [
|
||||
{
|
||||
json: {
|
||||
row_number: 3,
|
||||
name: 'NEW NAME',
|
||||
text: 'NEW TEXT',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
input: undefined,
|
||||
},
|
||||
},
|
||||
];
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce(inputData);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValueOnce(mock<INode>({ typeVersion: 4.5 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // valueInputMode
|
||||
.mockReturnValueOnce({}); // options
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('defineBelow'); // dataMode
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce(undefined);
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce(undefined);
|
||||
mockGoogleSheet.updateRows.mockResolvedValueOnce(undefined);
|
||||
|
||||
mockGoogleSheet.updateRows.mockResolvedValueOnce([]);
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
expect(mockGoogleSheet.updateRows).toHaveBeenCalledWith('Sheet1', [['name', 'text']], 'RAW', 1);
|
||||
expect(mockGoogleSheet.appendEmptyRowsOrColumns).toHaveBeenCalledWith('1234', 1, 0);
|
||||
expect(mockGoogleSheet.appendSheetData).toHaveBeenCalledWith({
|
||||
inputData: [{ name: 'NEW NAME', text: 'NEW TEXT' }],
|
||||
keyRowIndex: 1,
|
||||
lastRow: 2,
|
||||
range: 'Sheet1',
|
||||
valueInputMode: 'USER_ENTERED',
|
||||
});
|
||||
expect(inputData[0].json).toEqual({ row_number: 3, name: 'NEW NAME', text: 'NEW TEXT' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,859 @@
|
||||
import type { MockProxy } from 'jest-mock-extended';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../../../v2/actions/sheet/appendOrUpdate.operation';
|
||||
import type { GoogleSheet } from '../../../v2/helpers/GoogleSheet';
|
||||
|
||||
describe('Google Sheet - Append or Update', () => {
|
||||
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
|
||||
let mockGoogleSheet: MockProxy<GoogleSheet>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mock<IExecuteFunctions>();
|
||||
mockGoogleSheet = mock<GoogleSheet>();
|
||||
});
|
||||
|
||||
it('should insert input data if sheet is empty', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: {
|
||||
row_number: 3,
|
||||
name: 'NEW NAME',
|
||||
text: 'NEW TEXT',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
input: undefined,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValueOnce(mock<INode>({ typeVersion: 4.5 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // valueInputMode
|
||||
.mockReturnValueOnce({}); // options
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('defineBelow'); // dataMode
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce([]); // columns.schema
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(['row_number']); // columnsToMatchOn
|
||||
mockExecuteFunctions.getNode.mockReturnValueOnce(mock<INode>());
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce([]); // columns.matchingColumns
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce(undefined);
|
||||
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.updateRows.mockResolvedValueOnce([]);
|
||||
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [
|
||||
{
|
||||
row_number: 3,
|
||||
name: 'NEW NAME',
|
||||
text: 'NEW TEXT',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
expect(mockGoogleSheet.getColumnValues).toHaveBeenCalledWith({
|
||||
dataStartRowIndex: 1,
|
||||
keyIndex: -1,
|
||||
range: 'Sheet1!A:Z',
|
||||
sheetData: [['name', 'text']],
|
||||
valueRenderMode: 'UNFORMATTED_VALUE',
|
||||
});
|
||||
|
||||
expect(mockGoogleSheet.updateRows).toHaveBeenCalledWith(
|
||||
'Sheet1',
|
||||
[['name', 'text']],
|
||||
'USER_ENTERED',
|
||||
1,
|
||||
);
|
||||
expect(mockGoogleSheet.prepareDataForUpdateOrUpsert).toHaveBeenCalledWith({
|
||||
columnNamesList: [['name', 'text']],
|
||||
columnValuesList: [],
|
||||
dataStartRowIndex: 1,
|
||||
indexKey: 'row_number',
|
||||
inputData: [{ name: 'NEW NAME', row_number: 3, text: 'NEW TEXT' }],
|
||||
keyRowIndex: 0,
|
||||
range: 'Sheet1!A:Z',
|
||||
upsert: true,
|
||||
valueRenderMode: 'UNFORMATTED_VALUE',
|
||||
});
|
||||
expect(mockGoogleSheet.appendEmptyRowsOrColumns).toHaveBeenCalledWith('1234', 1, 0);
|
||||
expect(mockGoogleSheet.appendSheetData).toHaveBeenCalledWith({
|
||||
columnNamesList: [['name', 'text']],
|
||||
inputData: [{ name: 'NEW NAME', row_number: 3, text: 'NEW TEXT' }],
|
||||
keyRowIndex: 1,
|
||||
lastRow: 2,
|
||||
range: 'Sheet1!A:Z',
|
||||
valueInputMode: 'USER_ENTERED',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error when no column names can be retrieved and sheet has data', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 3 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // options.cellFormat
|
||||
.mockReturnValueOnce({}) // options
|
||||
.mockReturnValueOnce('defineBelow') // dataMode
|
||||
.mockReturnValueOnce('id') // columnToMatchOn
|
||||
.mockReturnValueOnce('1') // valueToMatchOn
|
||||
.mockReturnValueOnce([{ column: 'name', fieldValue: 'Test Name' }]); // fieldsUi.values
|
||||
|
||||
// Mock sheet with data but no header row at keyRowIndex
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([
|
||||
undefined as any, // No header row at keyRowIndex 0
|
||||
['some', 'data', 'here'], // Has data but not at header position
|
||||
]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [],
|
||||
});
|
||||
|
||||
await expect(
|
||||
execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234'),
|
||||
).rejects.toThrow(NodeOperationError);
|
||||
});
|
||||
|
||||
it('should handle custom header row and first data row positions', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 3 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // options.cellFormat
|
||||
.mockReturnValueOnce({
|
||||
locationDefine: {
|
||||
values: {
|
||||
headerRow: '3',
|
||||
firstDataRow: '5',
|
||||
},
|
||||
},
|
||||
}) // options with custom positions
|
||||
.mockReturnValueOnce('defineBelow') // dataMode
|
||||
.mockReturnValueOnce('id') // columnToMatchOn
|
||||
.mockReturnValueOnce('1') // valueToMatchOn
|
||||
.mockReturnValueOnce([{ column: 'name', fieldValue: 'Test Name' }]); // fieldsUi.values
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([
|
||||
[],
|
||||
[],
|
||||
['id', 'name'], // Header at row 3 (index 2)
|
||||
[],
|
||||
['1', 'Old Name'], // Data starts at row 5 (index 4)
|
||||
]);
|
||||
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce(['1']);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [{ id: '1', name: 'Test Name' }],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
expect(mockGoogleSheet.getColumnValues).toHaveBeenCalledWith({
|
||||
dataStartRowIndex: 4, // firstDataRow - 1
|
||||
keyIndex: 0,
|
||||
range: 'Sheet1!A:Z',
|
||||
sheetData: expect.any(Array),
|
||||
valueRenderMode: 'UNFORMATTED_VALUE',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle data mode "nothing" by skipping processing', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{ json: { id: 1, name: 'Test' }, pairedItem: { item: 0, input: undefined } },
|
||||
{ json: { id: 2, name: 'Test2' }, pairedItem: { item: 1, input: undefined } },
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 3 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // options.cellFormat
|
||||
.mockReturnValueOnce({}) // options
|
||||
.mockReturnValueOnce('nothing') // dataMode
|
||||
.mockReturnValueOnce('id'); // columnToMatchOn
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [],
|
||||
});
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
// Should return original items since dataMode is 'nothing'
|
||||
expect(result).toEqual([
|
||||
{ json: { id: 1, name: 'Test' }, pairedItem: { item: 0 } },
|
||||
{ json: { id: 2, name: 'Test2' }, pairedItem: { item: 1 } },
|
||||
]);
|
||||
|
||||
// Should not call update or append operations
|
||||
expect(mockGoogleSheet.batchUpdate).not.toHaveBeenCalled();
|
||||
expect(mockGoogleSheet.appendSheetData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle autoMapInputData with ignoreIt option', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test', extraField: 'should be ignored' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 3 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // options.cellFormat
|
||||
.mockReturnValueOnce({ handlingExtraData: 'ignoreIt' }) // options
|
||||
.mockReturnValueOnce('autoMapInputData'); // dataMode
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [{ id: 1, name: 'Test', extraField: 'should be ignored' }],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
expect(mockGoogleSheet.prepareDataForUpdateOrUpsert).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
inputData: [{ id: 1, name: 'Test', extraField: 'should be ignored' }],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle autoMapInputData with error option and throw on unexpected fields', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test', unexpectedField: 'error' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 3 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // options.cellFormat
|
||||
.mockReturnValueOnce({ handlingExtraData: 'error' }) // options
|
||||
.mockReturnValueOnce('autoMapInputData'); // dataMode
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(
|
||||
execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234'),
|
||||
).rejects.toThrow(NodeOperationError);
|
||||
});
|
||||
|
||||
it('should handle autoMapInputData with insertInNewColumn option', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test', newField: 'new column value' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 3 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // options.cellFormat
|
||||
.mockReturnValueOnce({ handlingExtraData: 'insertInNewColumn' }) // options
|
||||
.mockReturnValueOnce('autoMapInputData'); // dataMode
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.updateRows.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [{ id: 1, name: 'Test', newField: 'new column value' }],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
// Should update header row with new column
|
||||
expect(mockGoogleSheet.updateRows).toHaveBeenCalledWith(
|
||||
'Sheet1',
|
||||
[['id', 'name', 'newField']],
|
||||
'RAW',
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when valueToMatchOn is empty in defineBelow mode (v3)', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 3 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // options.cellFormat
|
||||
.mockReturnValueOnce({}) // options
|
||||
.mockReturnValueOnce('defineBelow') // dataMode
|
||||
.mockReturnValueOnce('id') // columnToMatchOn
|
||||
.mockReturnValueOnce(''); // empty valueToMatchOn
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(
|
||||
execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234'),
|
||||
).rejects.toThrow(NodeOperationError);
|
||||
});
|
||||
|
||||
it('should throw error when no values are provided in defineBelow mode (v3)', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 3 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // options.cellFormat
|
||||
.mockReturnValueOnce({}) // options
|
||||
.mockReturnValueOnce('defineBelow') // dataMode
|
||||
.mockReturnValueOnce('id') // columnToMatchOn
|
||||
.mockReturnValueOnce('1') // valueToMatchOn
|
||||
.mockReturnValueOnce([]); // empty fieldsUi.values
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(
|
||||
execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234'),
|
||||
).rejects.toThrow(NodeOperationError);
|
||||
});
|
||||
|
||||
it('should handle newColumn type in defineBelow mode (v3)', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 3 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // options.cellFormat
|
||||
.mockReturnValueOnce({}) // options
|
||||
.mockReturnValueOnce('defineBelow') // dataMode
|
||||
.mockReturnValueOnce('id') // columnToMatchOn
|
||||
.mockReturnValueOnce('1') // valueToMatchOn
|
||||
.mockReturnValueOnce([
|
||||
{ column: 'name', fieldValue: 'Updated Name' },
|
||||
{ column: 'newColumn', columnName: 'description', fieldValue: 'New Description' },
|
||||
]); // fieldsUi.values with new column
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.updateRows.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [{ id: '1', name: 'Updated Name', description: 'New Description' }],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
// Should update header with new column
|
||||
expect(mockGoogleSheet.updateRows).toHaveBeenCalledWith(
|
||||
'Sheet1',
|
||||
[['id', 'name', 'description']],
|
||||
'RAW',
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle both update and append data', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 4.5 }));
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, _itemIndex, fallback) => {
|
||||
const params: Record<string, any> = {
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
options: {},
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
'columns.schema': [],
|
||||
'columns.matchingColumns': ['id'],
|
||||
'columns.value': { id: 1, name: 'Updated Name' },
|
||||
'columns.value[id]': 1,
|
||||
};
|
||||
return params[paramName] ?? fallback;
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce(['1']);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [{ range: 'A2:B2', values: [['1', 'Updated Name']] }],
|
||||
appendData: [{ id: 2, name: 'New Item' }],
|
||||
});
|
||||
|
||||
mockGoogleSheet.batchUpdate.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
expect(mockGoogleSheet.batchUpdate).toHaveBeenCalledWith(
|
||||
[{ range: 'A2:B2', values: [['1', 'Updated Name']] }],
|
||||
'USER_ENTERED',
|
||||
);
|
||||
expect(mockGoogleSheet.appendSheetData).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use useAppend option when appending data', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 4.5 }));
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, _itemIndex, fallback) => {
|
||||
const params: Record<string, any> = {
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
options: { useAppend: true },
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
'columns.schema': [],
|
||||
'columns.matchingColumns': ['id'],
|
||||
'columns.value': { id: 1, name: 'Test Name' },
|
||||
'columns.value[id]': 1,
|
||||
};
|
||||
return params[paramName] ?? fallback;
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [{ id: 1, name: 'Test Name' }],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
expect(mockGoogleSheet.appendSheetData).toHaveBeenCalledWith({
|
||||
columnNamesList: [['id', 'name']],
|
||||
inputData: [{ id: 1, name: 'Test Name' }],
|
||||
keyRowIndex: 1,
|
||||
lastRow: 2,
|
||||
range: 'Sheet1!A:Z',
|
||||
useAppend: true,
|
||||
valueInputMode: 'USER_ENTERED',
|
||||
});
|
||||
|
||||
// Should NOT call appendEmptyRowsOrColumns when useAppend is true
|
||||
expect(mockGoogleSheet.appendEmptyRowsOrColumns).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle v4 with empty columns.value and throw error', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 4.5 }));
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, _itemIndex, fallback) => {
|
||||
const params: Record<string, any> = {
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
options: {},
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
'columns.schema': [],
|
||||
'columns.matchingColumns': ['id'],
|
||||
'columns.value': {},
|
||||
};
|
||||
return params[paramName] ?? fallback;
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(
|
||||
execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234'),
|
||||
).rejects.toThrow(NodeOperationError);
|
||||
});
|
||||
|
||||
it('should handle v4 with null/undefined values by converting to empty string', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 4.5 }));
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, _itemIndex, fallback) => {
|
||||
const params: Record<string, any> = {
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
options: {},
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
'columns.schema': [],
|
||||
'columns.matchingColumns': ['id'],
|
||||
'columns.value': { id: 1, name: null, description: undefined },
|
||||
'columns.value[id]': 1,
|
||||
};
|
||||
return params[paramName] ?? fallback;
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name', 'description']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [{ id: 1, name: '', description: '' }],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
expect(mockGoogleSheet.prepareDataForUpdateOrUpsert).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
inputData: [{ id: 1, name: '', description: '' }],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return mapped values for v4+ in defineBelow mode', async () => {
|
||||
const mappedValues = [
|
||||
{ id: 1, name: 'Test Name' },
|
||||
{ id: 2, name: 'Another Test' },
|
||||
];
|
||||
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{ json: { id: 1 }, pairedItem: { item: 0, input: undefined } },
|
||||
{ json: { id: 2 }, pairedItem: { item: 1, input: undefined } },
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 4.5 }));
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, itemIndex, fallback) => {
|
||||
const params: Record<string, any> = {
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
options: {},
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
'columns.schema': [],
|
||||
'columns.matchingColumns': ['id'],
|
||||
'columns.value[id]': itemIndex === 0 ? 1 : 2,
|
||||
};
|
||||
|
||||
// Handle per-item columns.value calls
|
||||
if (paramName === 'columns.value' && typeof itemIndex === 'number') {
|
||||
return mappedValues[itemIndex];
|
||||
}
|
||||
|
||||
return params[paramName] ?? fallback;
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
|
||||
// Mock prepareDataForUpdateOrUpsert to be called for each item
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert
|
||||
.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [mappedValues[0]],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [mappedValues[1]],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
expect(result).toEqual([
|
||||
{ json: mappedValues[0], pairedItem: { item: 0 } },
|
||||
{ json: mappedValues[1], pairedItem: { item: 1 } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle custom valueRenderMode option', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 4.5 }));
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, _itemIndex, fallback) => {
|
||||
const params: Record<string, any> = {
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
options: { valueRenderMode: 'FORMATTED_VALUE' },
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
'columns.schema': [],
|
||||
'columns.matchingColumns': ['id'],
|
||||
'columns.value': { id: 1, name: 'Test Name' },
|
||||
'columns.value[id]': 1,
|
||||
};
|
||||
return params[paramName] ?? fallback;
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [{ id: 1, name: 'Test Name' }],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
expect(mockGoogleSheet.getColumnValues).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
valueRenderMode: 'FORMATTED_VALUE',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockGoogleSheet.prepareDataForUpdateOrUpsert).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
valueRenderMode: 'FORMATTED_VALUE',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should call checkForSchemaChanges for node version >= 4.4', async () => {
|
||||
const mockSchema = [
|
||||
{ id: 'id', displayName: 'ID' },
|
||||
{ id: 'name', displayName: 'Name' },
|
||||
];
|
||||
const mockNode = mock<INode>({ typeVersion: 4.5 });
|
||||
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, _itemIndex, fallback) => {
|
||||
const params: Record<string, any> = {
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
options: {},
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
'columns.schema': mockSchema,
|
||||
'columns.matchingColumns': ['id'],
|
||||
'columns.value': { id: 1, name: 'Test Name' },
|
||||
'columns.value[id]': 1,
|
||||
};
|
||||
return params[paramName] ?? fallback;
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [{ id: 1, name: 'Test Name' }],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
// Mock the checkForSchemaChanges import to verify it's called
|
||||
const GoogleSheetsUtils = await import('../../../v2/helpers/GoogleSheets.utils');
|
||||
jest.spyOn(GoogleSheetsUtils, 'checkForSchemaChanges').mockImplementation(() => {});
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
expect(GoogleSheetsUtils.checkForSchemaChanges).toHaveBeenCalledWith(
|
||||
mockNode, // node
|
||||
['id', 'name'], // columnNames
|
||||
mockSchema, // schema
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Google Sheet - Append or Update v4.6 vs v4.7 Behavior', () => {
|
||||
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
|
||||
let mockGoogleSheet: MockProxy<GoogleSheet>;
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('v4.6: empty string in UI gets filtered out, field not sent to backend', async () => {
|
||||
mockExecuteFunctions = mock<IExecuteFunctions>();
|
||||
mockGoogleSheet = mock<GoogleSheet>();
|
||||
|
||||
mockExecuteFunctions.getNode
|
||||
.mockReturnValueOnce(mock<INode>({ typeVersion: 4.6 }))
|
||||
.mockReturnValueOnce(mock<INode>({ typeVersion: 4.6 }));
|
||||
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: {},
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
options: {},
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
'columns.schema': [],
|
||||
'columns.matchingColumns': ['id'],
|
||||
'columns.value': {
|
||||
id: 1,
|
||||
name: 'John',
|
||||
// email field is NOT present here because user typed '' in UI
|
||||
// and v4.6 frontend filtered it out (allowEmptyValues: false)
|
||||
},
|
||||
};
|
||||
return params[parameterName];
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([
|
||||
['id', 'name', 'email'],
|
||||
['1', 'Old Name', 'old@email.com'],
|
||||
]);
|
||||
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce(['1']);
|
||||
mockGoogleSheet.updateRows.mockResolvedValueOnce([]);
|
||||
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'John',
|
||||
// email is not included, so it keeps old value
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
// v4.6: Only fields with non-empty values are sent to prepareDataForUpdateOrUpsert
|
||||
expect(mockGoogleSheet.prepareDataForUpdateOrUpsert).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
inputData: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'John',
|
||||
// email is NOT in the inputData, so cell keeps old value
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('v4.7: empty string in UI is preserved and sent to backend to clear cell', async () => {
|
||||
mockExecuteFunctions = mock<IExecuteFunctions>();
|
||||
mockGoogleSheet = mock<GoogleSheet>();
|
||||
|
||||
mockExecuteFunctions.getNode
|
||||
.mockReturnValueOnce(mock<INode>({ typeVersion: 4.7 }))
|
||||
.mockReturnValueOnce(mock<INode>({ typeVersion: 4.7 }));
|
||||
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: {},
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
options: {},
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
'columns.schema': [],
|
||||
'columns.matchingColumns': ['id'],
|
||||
'columns.value': {
|
||||
id: 1,
|
||||
name: 'John',
|
||||
email: '', // Empty string is preserved in v4.7 (allowEmptyValues: true)
|
||||
},
|
||||
};
|
||||
return params[parameterName];
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([
|
||||
['id', 'name', 'email'],
|
||||
['1', 'Old Name', 'old@email.com'],
|
||||
]);
|
||||
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce(['1']);
|
||||
mockGoogleSheet.updateRows.mockResolvedValueOnce([]);
|
||||
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'John',
|
||||
email: '', // Empty string will clear the cell
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
// v4.7: Empty strings are preserved and sent to prepareDataForUpdateOrUpsert
|
||||
expect(mockGoogleSheet.prepareDataForUpdateOrUpsert).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
inputData: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'John',
|
||||
email: '', // Empty string is preserved and will clear the cell
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../../../v2/actions/sheet/clear.operation';
|
||||
import type { GoogleSheet } from '../../../v2/helpers/GoogleSheet';
|
||||
|
||||
describe('Google Sheet - Clear', () => {
|
||||
let mockExecuteFunctions: Partial<IExecuteFunctions>;
|
||||
let mockSheet: Partial<GoogleSheet>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = {
|
||||
getInputData: jest.fn().mockReturnValue([{ json: {} }]),
|
||||
getNodeParameter: jest.fn(),
|
||||
} as Partial<IExecuteFunctions>;
|
||||
|
||||
mockSheet = {
|
||||
clearData: jest.fn(),
|
||||
getData: jest.fn().mockResolvedValue([['Header1', 'Header2']]), // Mock first-row data
|
||||
updateRows: jest.fn(),
|
||||
} as Partial<GoogleSheet>;
|
||||
});
|
||||
|
||||
test('should clear the whole sheet', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'clear') return 'wholeSheet';
|
||||
return false;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
|
||||
expect(mockSheet.clearData).toHaveBeenCalledWith('Sheet1');
|
||||
});
|
||||
|
||||
test('should clear specific rows', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'clear') return 'specificRows';
|
||||
if (param === 'startIndex') return 2;
|
||||
if (param === 'rowsToDelete') return 3;
|
||||
return false;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
|
||||
expect(mockSheet.clearData).toHaveBeenCalledWith('Sheet1!2:4');
|
||||
});
|
||||
|
||||
test('should clear specific columns', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'clear') return 'specificColumns';
|
||||
if (param === 'startIndex') return 'B';
|
||||
if (param === 'columnsToDelete') return 2;
|
||||
return false;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
|
||||
expect(mockSheet.clearData).toHaveBeenCalledWith('Sheet1!B:C');
|
||||
});
|
||||
|
||||
test('should clear a specific range', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'clear') return 'specificRange';
|
||||
if (param === 'range') return 'A1:C5';
|
||||
return false;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
|
||||
expect(mockSheet.clearData).toHaveBeenCalledWith('Sheet1!A1:C5');
|
||||
});
|
||||
|
||||
test('should keep the first row when clearing the whole sheet', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'clear') return 'wholeSheet';
|
||||
if (param === 'keepFirstRow') return true;
|
||||
return false;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
|
||||
expect(mockSheet.getData).toHaveBeenCalledWith('Sheet1!1:1', 'FORMATTED_VALUE');
|
||||
expect(mockSheet.clearData).toHaveBeenCalledWith('Sheet1');
|
||||
expect(mockSheet.updateRows).toHaveBeenCalledWith('Sheet1', [['Header1', 'Header2']], 'RAW', 1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../../../v2/actions/sheet/create.operation';
|
||||
import type { GoogleSheet } from '../../../v2/helpers/GoogleSheet';
|
||||
import { getExistingSheetNames, hexToRgb } from '../../../v2/helpers/GoogleSheets.utils';
|
||||
import { apiRequest } from '../../../v2/transport';
|
||||
|
||||
jest.mock('../../../v2/helpers/GoogleSheets.utils', () => ({
|
||||
getExistingSheetNames: jest.fn(),
|
||||
hexToRgb: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../v2/transport', () => ({
|
||||
apiRequest: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Google Sheet - Create', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
const mockExecuteFunctions = {
|
||||
getInputData: jest.fn(),
|
||||
getNodeParameter: jest.fn(),
|
||||
helpers: {
|
||||
constructExecutionMetaData: jest.fn(),
|
||||
},
|
||||
} as unknown as Partial<IExecuteFunctions>;
|
||||
|
||||
const sheet = {} as Partial<GoogleSheet>;
|
||||
const sheetName = 'test-sheet';
|
||||
|
||||
test('should create a new sheet with given title and options', async () => {
|
||||
const items = [{ json: {} }];
|
||||
const existingSheetNames = ['existing-sheet'];
|
||||
const sheetTitle = 'new-sheet';
|
||||
const options = { tabColor: '0aa55c' };
|
||||
const rgbColor = { red: 10, green: 165, blue: 92 };
|
||||
const responseData = {
|
||||
replies: [{ addSheet: { properties: { title: sheetTitle } } }],
|
||||
};
|
||||
|
||||
(mockExecuteFunctions.getInputData as jest.Mock).mockReturnValue(items);
|
||||
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
|
||||
if (paramName === 'title') return sheetTitle;
|
||||
if (paramName === 'options') return options;
|
||||
});
|
||||
(getExistingSheetNames as jest.Mock).mockResolvedValue(existingSheetNames);
|
||||
(hexToRgb as jest.Mock).mockReturnValue(rgbColor);
|
||||
(apiRequest as jest.Mock).mockResolvedValue(responseData);
|
||||
(mockExecuteFunctions as IExecuteFunctions).helpers.constructExecutionMetaData = jest
|
||||
.fn()
|
||||
.mockReturnValue([{ json: responseData }]);
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
sheet as GoogleSheet,
|
||||
sheetName,
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ json: responseData }]);
|
||||
expect(getExistingSheetNames).toHaveBeenCalledWith(sheet);
|
||||
expect(apiRequest).toHaveBeenCalledWith('POST', `/v4/spreadsheets/${sheetName}:batchUpdate`, {
|
||||
requests: [
|
||||
{
|
||||
addSheet: {
|
||||
properties: {
|
||||
title: sheetTitle,
|
||||
tabColor: { red: 10 / 255, green: 165 / 255, blue: 92 / 255 },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('should skip creating a sheet if the title already exists', async () => {
|
||||
const items = [{ json: {} }];
|
||||
const existingSheetNames = ['existing-sheet'];
|
||||
const sheetTitle = 'existing-sheet';
|
||||
|
||||
(mockExecuteFunctions as IExecuteFunctions).getInputData = jest.fn().mockReturnValue(items);
|
||||
(mockExecuteFunctions as IExecuteFunctions).getNodeParameter = jest
|
||||
.fn()
|
||||
.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'title') return sheetTitle;
|
||||
if (paramName === 'options') return {};
|
||||
});
|
||||
(getExistingSheetNames as jest.Mock).mockResolvedValue(existingSheetNames);
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
sheet as GoogleSheet,
|
||||
sheetName,
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(getExistingSheetNames).toHaveBeenCalledWith(sheet);
|
||||
expect(apiRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../../../v2/actions/sheet/delete.operation';
|
||||
import type { GoogleSheet } from '../../../v2/helpers/GoogleSheet';
|
||||
|
||||
describe('Google Sheet - Delete', () => {
|
||||
let mockExecuteFunctions: Partial<IExecuteFunctions>;
|
||||
let mockSheet: Partial<GoogleSheet>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = {
|
||||
getInputData: jest.fn().mockReturnValue([{ json: {} }]),
|
||||
getNodeParameter: jest.fn(),
|
||||
helpers: {
|
||||
constructExecutionMetaData: jest.fn((data) => ({ json: data })),
|
||||
},
|
||||
} as unknown as Partial<IExecuteFunctions>;
|
||||
|
||||
mockSheet = {
|
||||
spreadsheetBatchUpdate: jest.fn(),
|
||||
} as Partial<GoogleSheet>;
|
||||
});
|
||||
|
||||
test('should delete a single row', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'toDelete') return 'rows';
|
||||
if (param === 'startIndex') return 2;
|
||||
if (param === 'numberToDelete') return 1;
|
||||
return null;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
|
||||
expect(mockSheet.spreadsheetBatchUpdate).toHaveBeenCalledWith([
|
||||
{
|
||||
deleteDimension: {
|
||||
range: {
|
||||
sheetId: 'Sheet1',
|
||||
dimension: 'ROWS',
|
||||
startIndex: 1, // Adjusted for zero-based index
|
||||
endIndex: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should delete multiple rows', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'toDelete') return 'rows';
|
||||
if (param === 'startIndex') return 3;
|
||||
if (param === 'numberToDelete') return 2;
|
||||
return null;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
|
||||
expect(mockSheet.spreadsheetBatchUpdate).toHaveBeenCalledWith([
|
||||
{
|
||||
deleteDimension: {
|
||||
range: {
|
||||
sheetId: 'Sheet1',
|
||||
dimension: 'ROWS',
|
||||
startIndex: 2,
|
||||
endIndex: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should delete a single column', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'toDelete') return 'columns';
|
||||
if (param === 'startIndex') return 'B';
|
||||
if (param === 'numberToDelete') return 1;
|
||||
return null;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
|
||||
expect(mockSheet.spreadsheetBatchUpdate).toHaveBeenCalledWith([
|
||||
{
|
||||
deleteDimension: {
|
||||
range: {
|
||||
sheetId: 'Sheet1',
|
||||
dimension: 'COLUMNS',
|
||||
startIndex: 1, // 'B' corresponds to index 1 (zero-based)
|
||||
endIndex: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should delete multiple columns', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'toDelete') return 'columns';
|
||||
if (param === 'startIndex') return 'C';
|
||||
if (param === 'numberToDelete') return 3;
|
||||
return null;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
|
||||
expect(mockSheet.spreadsheetBatchUpdate).toHaveBeenCalledWith([
|
||||
{
|
||||
deleteDimension: {
|
||||
range: {
|
||||
sheetId: 'Sheet1',
|
||||
dimension: 'COLUMNS',
|
||||
startIndex: 2, // 'C' corresponds to index 2 (zero-based)
|
||||
endIndex: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should return wrapped success response', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'toDelete') return 'rows';
|
||||
if (param === 'startIndex') return 2;
|
||||
if (param === 'numberToDelete') return 1;
|
||||
return null;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
mockExecuteFunctions.helpers = {
|
||||
constructExecutionMetaData: jest.fn((data) => data),
|
||||
} as unknown as IExecuteFunctions['helpers'];
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
expect(result).toEqual([{ json: { success: true } }]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../../../v2/actions/sheet/read.operation';
|
||||
import type { GoogleSheet } from '../../../v2/helpers/GoogleSheet';
|
||||
|
||||
describe('Google Sheet - Read', () => {
|
||||
let mockExecuteFunctions: Partial<IExecuteFunctions>;
|
||||
let mockSheet: Partial<GoogleSheet>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = {
|
||||
getInputData: jest.fn().mockReturnValue([{ json: {} }]),
|
||||
getNode: jest.fn().mockReturnValue({ typeVersion: 4.5 }),
|
||||
getNodeParameter: jest.fn((param) => {
|
||||
const mockParams: { [key: string]: unknown } = {
|
||||
options: {},
|
||||
'filtersUI.values': [],
|
||||
combineFilters: 'AND',
|
||||
};
|
||||
return mockParams[param];
|
||||
}),
|
||||
} as Partial<IExecuteFunctions>;
|
||||
|
||||
mockSheet = {
|
||||
getData: jest.fn().mockResolvedValue([
|
||||
['Header1', 'Header2'],
|
||||
['Value1', 'Value2'],
|
||||
]),
|
||||
lookupValues: jest.fn().mockResolvedValue([{ Header1: 'Value1', Header2: 'Value2' }]),
|
||||
structureArrayDataByColumn: jest
|
||||
.fn()
|
||||
.mockReturnValue([{ Header1: 'Value1', Header2: 'Value2' }]),
|
||||
};
|
||||
});
|
||||
|
||||
test('should return structured sheet data when no filters are applied', async () => {
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
expect(mockSheet.getData).toHaveBeenCalled();
|
||||
expect(mockSheet.structureArrayDataByColumn).toHaveBeenCalled();
|
||||
expect(result).toEqual([
|
||||
{
|
||||
json: { Header1: 'Value1', Header2: 'Value2' },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should call lookupValues when filters are provided', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param) => {
|
||||
if (param === 'filtersUI.values') return [{ lookupColumn: 'Header1', lookupValue: 'Value1' }];
|
||||
return '';
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
expect(mockSheet.lookupValues).toHaveBeenCalled();
|
||||
expect(result).toEqual([
|
||||
{
|
||||
json: { Header1: 'Value1', Header2: 'Value2' },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should return an empty array when sheet data is empty', async () => {
|
||||
mockSheet.getData = jest.fn().mockResolvedValue([]);
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../../../v2/actions/sheet/remove.operation';
|
||||
import type { GoogleSheet } from '../../../v2/helpers/GoogleSheet';
|
||||
import { apiRequest } from '../../../v2/transport';
|
||||
|
||||
jest.mock('../../../v2/transport', () => ({
|
||||
apiRequest: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Google Sheet - Remove', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
const mockExecuteFunctions = {
|
||||
getInputData: jest.fn(),
|
||||
getNodeParameter: jest.fn(),
|
||||
helpers: {
|
||||
constructExecutionMetaData: jest.fn(),
|
||||
},
|
||||
} as unknown as Partial<IExecuteFunctions>;
|
||||
|
||||
const sheet = {} as Partial<GoogleSheet>;
|
||||
const sheetName = 'spreadsheet123||sheet456';
|
||||
|
||||
test('should process a single item', async () => {
|
||||
const items = [{ json: {} }];
|
||||
((mockExecuteFunctions as IExecuteFunctions).getInputData as jest.Mock).mockReturnValue(items);
|
||||
|
||||
const apiResponse = { replies: [{ some: 'data' }], foo: 'bar' };
|
||||
(apiRequest as jest.Mock).mockResolvedValue(apiResponse);
|
||||
|
||||
const constructedData = [{ json: { foo: 'bar', index: 0 } }];
|
||||
(
|
||||
(mockExecuteFunctions as IExecuteFunctions).helpers.constructExecutionMetaData as jest.Mock
|
||||
).mockReturnValue(constructedData);
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
sheet as GoogleSheet,
|
||||
sheetName,
|
||||
);
|
||||
|
||||
expect(apiRequest).toHaveBeenCalledTimes(1);
|
||||
expect(apiRequest).toHaveBeenCalledWith('POST', '/v4/spreadsheets/spreadsheet123:batchUpdate', {
|
||||
requests: [
|
||||
{
|
||||
deleteSheet: {
|
||||
sheetId: 'sheet456',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).toEqual(constructedData);
|
||||
});
|
||||
|
||||
test('should process multiple items', async () => {
|
||||
const items = [{ json: {} }, { json: {} }];
|
||||
((mockExecuteFunctions as IExecuteFunctions).getInputData as jest.Mock).mockReturnValue(items);
|
||||
|
||||
const apiResponses = [
|
||||
{ replies: [{ some: 'data1' }], foo: 'bar1' },
|
||||
{ replies: [{ some: 'data2' }], foo: 'bar2' },
|
||||
];
|
||||
(apiRequest as jest.Mock)
|
||||
.mockResolvedValueOnce(apiResponses[0])
|
||||
.mockResolvedValueOnce(apiResponses[1]);
|
||||
|
||||
const constructedDataItem0 = [{ json: { foo: 'bar1', index: 0 } }];
|
||||
const constructedDataItem1 = [{ json: { foo: 'bar2', index: 1 } }];
|
||||
((mockExecuteFunctions as IExecuteFunctions).helpers.constructExecutionMetaData as jest.Mock)
|
||||
.mockReturnValueOnce(constructedDataItem0)
|
||||
.mockReturnValueOnce(constructedDataItem1);
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
sheet as GoogleSheet,
|
||||
sheetName,
|
||||
);
|
||||
|
||||
expect(apiRequest).toHaveBeenCalledTimes(2);
|
||||
expect(apiRequest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'POST',
|
||||
'/v4/spreadsheets/spreadsheet123:batchUpdate',
|
||||
{
|
||||
requests: [
|
||||
{
|
||||
deleteSheet: {
|
||||
sheetId: 'sheet456',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
expect(apiRequest).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'POST',
|
||||
'/v4/spreadsheets/spreadsheet123:batchUpdate',
|
||||
{
|
||||
requests: [
|
||||
{
|
||||
deleteSheet: {
|
||||
sheetId: 'sheet456',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
expect(result).toEqual([...constructedDataItem0, ...constructedDataItem1]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,468 @@
|
||||
import type { MockProxy } from 'jest-mock-extended';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../../../v2/actions/sheet/update.operation';
|
||||
import type { GoogleSheet } from '../../../v2/helpers/GoogleSheet';
|
||||
|
||||
describe('Google Sheet - Update', () => {
|
||||
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
|
||||
let mockGoogleSheet: MockProxy<GoogleSheet>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mock<IExecuteFunctions>();
|
||||
mockGoogleSheet = mock<GoogleSheet>();
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValueOnce(mock<INode>({ typeVersion: 4.5 }));
|
||||
|
||||
mockGoogleSheet.batchUpdate.mockResolvedValueOnce([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should update by row_number and not insert it as a new column', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: {
|
||||
row_number: 3,
|
||||
name: 'NEW NAME',
|
||||
text: 'NEW TEXT',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
input: undefined,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // valueInputMode
|
||||
.mockReturnValueOnce({}); // options
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(['row_number']); // columnsToMatchOn
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('autoMapInputData'); // dataMode
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([
|
||||
['id', 'name', 'text'],
|
||||
['1', 'a', 'a'],
|
||||
['2', 'x', 'x'],
|
||||
['3', 'b', 'b'],
|
||||
]);
|
||||
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
|
||||
mockGoogleSheet.prepareDataForUpdatingByRowNumber.mockReturnValueOnce({
|
||||
updateData: [
|
||||
{
|
||||
range: 'Sheet1!B3',
|
||||
values: [['NEW NAME']],
|
||||
},
|
||||
{
|
||||
range: 'Sheet1!C3',
|
||||
values: [['NEW TEXT']],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1');
|
||||
|
||||
expect(mockGoogleSheet.getData).toHaveBeenCalledWith('Sheet1', 'FORMATTED_VALUE');
|
||||
expect(mockGoogleSheet.getColumnValues).toHaveBeenCalledWith({
|
||||
range: 'Sheet1!A:Z',
|
||||
keyIndex: -1,
|
||||
dataStartRowIndex: 1,
|
||||
valueRenderMode: 'UNFORMATTED_VALUE',
|
||||
sheetData: [
|
||||
['id', 'name', 'text'],
|
||||
['1', 'a', 'a'],
|
||||
['2', 'x', 'x'],
|
||||
['3', 'b', 'b'],
|
||||
],
|
||||
});
|
||||
expect(mockGoogleSheet.prepareDataForUpdatingByRowNumber).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
row_number: 3,
|
||||
name: 'NEW NAME',
|
||||
text: 'NEW TEXT',
|
||||
},
|
||||
],
|
||||
'Sheet1!A:Z',
|
||||
[['id', 'name', 'text']],
|
||||
);
|
||||
|
||||
expect(mockGoogleSheet.batchUpdate).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
range: 'Sheet1!B3',
|
||||
values: [['NEW NAME']],
|
||||
},
|
||||
{
|
||||
range: 'Sheet1!C3',
|
||||
values: [['NEW TEXT']],
|
||||
},
|
||||
],
|
||||
'USER_ENTERED',
|
||||
);
|
||||
});
|
||||
|
||||
it('should update rows by column values with special character', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: {
|
||||
row_number: 3,
|
||||
name: '** δ$% " []',
|
||||
text: 'δ$% " []',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
input: undefined,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
const params: { [key: string]: string | object } = {
|
||||
options: {},
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
'columns.matchingColumns': ['row_number'],
|
||||
'columns.value': {
|
||||
'ha []': 'kayyyy$',
|
||||
macarena: 'baile',
|
||||
'Real.1': 't&c',
|
||||
'21 "': 'Σ',
|
||||
},
|
||||
dataMode: 'autoMapInputData',
|
||||
};
|
||||
return params[parameterName];
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([
|
||||
['Real.1', '21 "', 'dfd', 'ha []', 'macarena'],
|
||||
['aye.2', '"book"', 'ee', 'dd', 'dance'],
|
||||
['t&c', 'Σ', 'baz', 'kayyyy$', 'baile'],
|
||||
['fudge.2', '9080', 'live', 'dog', 'brazil'],
|
||||
]);
|
||||
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
|
||||
mockGoogleSheet.prepareDataForUpdatingByRowNumber.mockReturnValueOnce({
|
||||
updateData: [
|
||||
{
|
||||
range: 'Sheet1!B3',
|
||||
values: [['** δ$% " []']],
|
||||
},
|
||||
{
|
||||
range: 'Sheet1!C3',
|
||||
values: [['δ$% " []']],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1');
|
||||
|
||||
expect(mockGoogleSheet.getData).toHaveBeenCalledWith('Sheet1', 'FORMATTED_VALUE');
|
||||
|
||||
expect(mockGoogleSheet.getColumnValues).toHaveBeenCalledWith({
|
||||
range: 'Sheet1!A:Z',
|
||||
keyIndex: -1,
|
||||
dataStartRowIndex: 1,
|
||||
valueRenderMode: 'UNFORMATTED_VALUE',
|
||||
sheetData: [
|
||||
['Real.1', '21 "', 'dfd', 'ha []', 'macarena'],
|
||||
['aye.2', '"book"', 'ee', 'dd', 'dance'],
|
||||
['t&c', 'Σ', 'baz', 'kayyyy$', 'baile'],
|
||||
['fudge.2', '9080', 'live', 'dog', 'brazil'],
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockGoogleSheet.prepareDataForUpdatingByRowNumber).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
'21 "': 'Σ',
|
||||
'Real.1': 't&c',
|
||||
'ha []': 'kayyyy$',
|
||||
macarena: 'baile',
|
||||
},
|
||||
],
|
||||
'Sheet1!A:Z',
|
||||
[['Real.1', '21 "', 'dfd', 'ha []', 'macarena']],
|
||||
);
|
||||
|
||||
expect(mockGoogleSheet.batchUpdate).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
range: 'Sheet1!B3',
|
||||
values: [['** δ$% " []']],
|
||||
},
|
||||
{
|
||||
range: 'Sheet1!C3',
|
||||
values: [['δ$% " []']],
|
||||
},
|
||||
],
|
||||
'USER_ENTERED',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Google Sheet - Update 4.6', () => {
|
||||
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
|
||||
let mockGoogleSheet: MockProxy<GoogleSheet>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mock<IExecuteFunctions>();
|
||||
mockGoogleSheet = mock<GoogleSheet>();
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValueOnce(mock<INode>({ typeVersion: 4.6 }));
|
||||
|
||||
mockGoogleSheet.batchUpdate.mockResolvedValueOnce([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('row_number input error', () => {
|
||||
it.each([{ rowNumber: undefined }])(
|
||||
'displays a helpful error message when row_number is $rowNumber',
|
||||
async ({ rowNumber }) => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: {
|
||||
row_number: rowNumber,
|
||||
name: 'name',
|
||||
text: 'txt',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
input: undefined,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
const params: { [key: string]: string | object } = {
|
||||
options: {},
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
'columns.matchingColumns': ['row_number'],
|
||||
'columns.value': {
|
||||
row_number: rowNumber, // TODO: Test for undefined
|
||||
},
|
||||
dataMode: 'defineBelow',
|
||||
};
|
||||
return params[parameterName];
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['macarena'], ['boomboom']]);
|
||||
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1')).rejects.toEqual(
|
||||
expect.objectContaining({
|
||||
message: 'row_number is null or undefined',
|
||||
description:
|
||||
"Since it's being used to determine the row to update, it cannot be null or undefined",
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('non-row_number undefined', () => {
|
||||
it.each([{ nonRowNumber: undefined }])(
|
||||
'displays a helpful error message when row_number is $rowNumber',
|
||||
async ({ nonRowNumber }) => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: {
|
||||
row_number: 2,
|
||||
nonRowNumber: 'name',
|
||||
text: 'txt',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
input: undefined,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
const params: { [key: string]: string | object } = {
|
||||
options: {},
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
'columns.matchingColumns': ['nonRowNumber'],
|
||||
'columns.value': {
|
||||
nonRowNumber,
|
||||
},
|
||||
dataMode: 'defineBelow',
|
||||
};
|
||||
return params[parameterName];
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['macarena'], ['boomboom']]);
|
||||
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [],
|
||||
});
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1');
|
||||
|
||||
expect(mockExecuteFunctions.addExecutionHints).toHaveBeenCalledWith({
|
||||
message: 'Warning: The value of column to match is null or undefined',
|
||||
location: 'outputPane',
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Google Sheet - Update v4.6 vs v4.7 Behavior', () => {
|
||||
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
|
||||
let mockGoogleSheet: MockProxy<GoogleSheet>;
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('v4.6: empty string in UI gets filtered out, field not sent to backend', async () => {
|
||||
mockExecuteFunctions = mock<IExecuteFunctions>();
|
||||
mockGoogleSheet = mock<GoogleSheet>();
|
||||
mockExecuteFunctions.getNode.mockReturnValueOnce(mock<INode>({ typeVersion: 4.6 }));
|
||||
mockGoogleSheet.batchUpdate.mockResolvedValueOnce([]);
|
||||
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: {},
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
const params: { [key: string]: string | object } = {
|
||||
options: {},
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
'columns.matchingColumns': ['id'],
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
'columns.value': {
|
||||
id: 1,
|
||||
name: 'John',
|
||||
// email field is NOT present here because user typed '' in UI
|
||||
// and v4.6 frontend filtered it out (allowEmptyStrings: false)
|
||||
},
|
||||
};
|
||||
return params[parameterName];
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([
|
||||
['id', 'name', 'email'],
|
||||
['1', 'Old Name', 'old@email.com'],
|
||||
]);
|
||||
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce(['1']);
|
||||
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [
|
||||
{
|
||||
range: 'Sheet1!B2',
|
||||
values: [['John']],
|
||||
},
|
||||
// No update for email column - it keeps its old value
|
||||
],
|
||||
appendData: [],
|
||||
});
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1');
|
||||
|
||||
// v4.6: Only name field is updated, email is not included in the update
|
||||
expect(mockGoogleSheet.prepareDataForUpdateOrUpsert).toHaveBeenCalledWith({
|
||||
inputData: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'John',
|
||||
// email is NOT in the inputData, so cell keeps old value
|
||||
},
|
||||
],
|
||||
indexKey: 'id',
|
||||
range: 'Sheet1!A:Z',
|
||||
keyRowIndex: 0,
|
||||
dataStartRowIndex: 1,
|
||||
valueRenderMode: 'UNFORMATTED_VALUE',
|
||||
columnNamesList: [['id', 'name', 'email']],
|
||||
columnValuesList: ['1'],
|
||||
});
|
||||
});
|
||||
|
||||
it('v4.7: empty string in UI is preserved and sent to backend to clear cell', async () => {
|
||||
mockExecuteFunctions = mock<IExecuteFunctions>();
|
||||
mockGoogleSheet = mock<GoogleSheet>();
|
||||
mockExecuteFunctions.getNode.mockReturnValueOnce(mock<INode>({ typeVersion: 4.7 }));
|
||||
mockGoogleSheet.batchUpdate.mockResolvedValueOnce([]);
|
||||
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: {},
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
const params: { [key: string]: string | object } = {
|
||||
options: {},
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
'columns.matchingColumns': ['id'],
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
'columns.value': {
|
||||
id: 1,
|
||||
name: 'John',
|
||||
email: '', // Empty string is preserved in v4.7 (allowEmptyStrings: true)
|
||||
},
|
||||
};
|
||||
return params[parameterName];
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([
|
||||
['id', 'name', 'email'],
|
||||
['1', 'Old Name', 'old@email.com'],
|
||||
]);
|
||||
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce(['1']);
|
||||
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [
|
||||
{
|
||||
range: 'Sheet1!B2',
|
||||
values: [['John']],
|
||||
},
|
||||
{
|
||||
range: 'Sheet1!C2',
|
||||
values: [['']],
|
||||
},
|
||||
],
|
||||
appendData: [],
|
||||
});
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1');
|
||||
|
||||
// v4.7: Both name and email fields are updated, email is cleared with empty string
|
||||
expect(mockGoogleSheet.prepareDataForUpdateOrUpsert).toHaveBeenCalledWith({
|
||||
inputData: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'John',
|
||||
email: '', // Empty string is preserved and will clear the cell
|
||||
},
|
||||
],
|
||||
indexKey: 'id',
|
||||
range: 'Sheet1!A:Z',
|
||||
keyRowIndex: 0,
|
||||
dataStartRowIndex: 1,
|
||||
valueRenderMode: 'UNFORMATTED_VALUE',
|
||||
columnNamesList: [['id', 'name', 'email']],
|
||||
columnValuesList: ['1'],
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user