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

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,955 @@
import type { IExecuteFunctions } from 'n8n-workflow';
import { GoogleSheet } from '../../../v2/helpers/GoogleSheet';
import { apiRequest } from '../../../v2/transport';
jest.mock('../../../v2/transport', () => ({
apiRequest: {
call: jest.fn(),
},
}));
describe('GoogleSheet', () => {
let googleSheet: GoogleSheet;
const mockExecuteFunctions: Partial<IExecuteFunctions> = {
getNode: jest.fn(),
};
const spreadsheetId = 'test-spreadsheet-id';
beforeEach(() => {
jest.clearAllMocks();
googleSheet = new GoogleSheet(spreadsheetId, mockExecuteFunctions as IExecuteFunctions);
});
describe('clearData', () => {
it('should make correct API call to clear data', async () => {
const range = 'Sheet1!A1:B2';
await googleSheet.clearData(range);
expect(apiRequest.call).toHaveBeenCalledWith(
mockExecuteFunctions,
'POST',
`/v4/spreadsheets/${spreadsheetId}/values/${range}:clear`,
{ spreadsheetId, range },
);
});
});
describe('getData', () => {
it('should retrieve data with correct parameters', async () => {
const range = 'Sheet1!A1:B2';
const valueRenderMode = 'UNFORMATTED_VALUE';
const mockResponse = {
values: [
['1', '2'],
['3', '4'],
],
};
(apiRequest.call as jest.Mock).mockResolvedValue(mockResponse);
const result = await googleSheet.getData(range, valueRenderMode);
expect(apiRequest.call).toHaveBeenCalledWith(
mockExecuteFunctions,
'GET',
`/v4/spreadsheets/${spreadsheetId}/values/${range}`,
{},
{
valueRenderOption: valueRenderMode,
dateTimeRenderOption: 'FORMATTED_STRING',
},
);
expect(result).toEqual(mockResponse.values);
});
});
describe('convertSheetDataArrayToObjectArray', () => {
it('should convert sheet data to object array correctly', () => {
const data = [
['name', 'age'],
['John', '30'],
['Jane', '25'],
];
const result = googleSheet.convertSheetDataArrayToObjectArray(data, 1, ['name', 'age']);
expect(result).toEqual([
{ name: 'John', age: '30' },
{ name: 'Jane', age: '25' },
]);
});
it('should handle empty rows when addEmpty is false', () => {
const data = [
['name', 'age'],
['John', '30'],
['', ''],
['Jane', '25'],
];
const result = googleSheet.convertSheetDataArrayToObjectArray(
data,
1,
['name', 'age'],
false,
);
expect(result).toEqual([
{ name: 'John', age: '30' },
// this row should be skipped but the code does not handle it
{ name: '', age: '' },
{ name: 'Jane', age: '25' },
]);
});
it('should handle empty columns when includeHeadersWithEmptyCells is true', () => {
const data = [
['name', 'age'],
['John', '30'],
['MARY', ''],
['Jane', '25'],
];
const result = googleSheet.convertSheetDataArrayToObjectArray(
data,
1,
['name', 'age'],
false,
true,
);
expect(result).toEqual([
{ name: 'John', age: '30' },
{ name: 'MARY', age: '' },
{ name: 'Jane', age: '25' },
]);
});
it('should handle zero values correctly', () => {
const data = [
['name', 'age'],
['John', 30],
['Jane', 0],
];
const result = googleSheet.convertSheetDataArrayToObjectArray(data, 1, ['name', 'age']);
expect(result).toEqual([
{ name: 'John', age: 30 },
{ name: 'Jane', age: 0 },
]);
});
it('should handle nullish values correctly', () => {
const data = [
['name', 'age'],
['John', null as unknown as number],
['Jane', undefined as unknown as number],
];
const result = googleSheet.convertSheetDataArrayToObjectArray(data, 1, ['name', 'age']);
expect(result).toEqual([
{ name: 'John', age: '' },
{ name: 'Jane', age: '' },
]);
});
});
describe('lookupValues', () => {
const inputData = [
['name', 'age', 'city'],
['John', '30', 'NY'],
['Jane', '25', 'LA'],
['Bob', '30', 'SF'],
];
it('should find matching rows with OR combination', async () => {
const lookupValues = [{ lookupColumn: 'age', lookupValue: '30' }];
const result = await googleSheet.lookupValues({
inputData,
keyRowIndex: 0,
dataStartRowIndex: 1,
lookupValues,
returnAllMatches: true,
combineFilters: 'OR',
nodeVersion: 4.5,
});
expect(result).toEqual([
{ name: 'John', age: '30', city: 'NY' },
{ name: 'Bob', age: '30', city: 'SF' },
]);
});
it('should find matching rows with OR combination and returnAllMatches is falsy at version 4.5', async () => {
const lookupValues = [
{ lookupColumn: 'age', lookupValue: '30' },
{ lookupColumn: 'name', lookupValue: 'Jane' },
];
const result = await googleSheet.lookupValues({
inputData,
keyRowIndex: 0,
dataStartRowIndex: 1,
lookupValues,
combineFilters: 'OR',
nodeVersion: 4.5,
});
expect(result).toEqual([
{ name: 'John', age: '30', city: 'NY' },
{ name: 'Jane', age: '25', city: 'LA' },
]);
});
it('should find matching rows with OR combination and returnAllMatches is falsy at version 4.6', async () => {
const lookupValues = [
{ lookupColumn: 'age', lookupValue: '30' },
{ lookupColumn: 'name', lookupValue: 'Jane' },
];
const result = await googleSheet.lookupValues({
inputData,
keyRowIndex: 0,
dataStartRowIndex: 1,
lookupValues,
combineFilters: 'OR',
nodeVersion: 4.6,
});
expect(result).toEqual([{ name: 'John', age: '30', city: 'NY' }]);
});
it('should find matching rows with AND combination', async () => {
const lookupValues = [
{ lookupColumn: 'age', lookupValue: '30' },
{ lookupColumn: 'city', lookupValue: 'NY' },
];
const result = await googleSheet.lookupValues({
inputData,
keyRowIndex: 0,
dataStartRowIndex: 1,
lookupValues,
returnAllMatches: true,
combineFilters: 'AND',
nodeVersion: 4.5,
});
expect(result).toEqual([{ name: 'John', age: '30', city: 'NY' }]);
});
it('should throw error for invalid key row', async () => {
const lookupValues = [{ lookupColumn: 'age', lookupValue: '30' }];
await expect(
googleSheet.lookupValues({
inputData: [['name', 'age']],
keyRowIndex: -1,
dataStartRowIndex: 1,
lookupValues,
nodeVersion: 4.5,
}),
).rejects.toThrow('The key row does not exist');
});
});
describe('appendSheetData', () => {
it('should correctly prepare and append data', async () => {
const inputData = [
{ name: 'John', age: '30' },
{ name: 'Jane', age: '25' },
];
const mockAppendResponse = {
range: 'Sheet1!A1:B3',
majorDimension: 'ROWS',
values: [
['name', 'age'],
['John', '30'],
['Jane', '25'],
],
};
(apiRequest.call as jest.Mock).mockResolvedValue(mockAppendResponse);
await googleSheet.appendSheetData({
inputData,
range: 'Sheet1!A:B',
keyRowIndex: 0,
valueInputMode: 'USER_ENTERED',
});
expect(apiRequest.call).toHaveBeenCalled();
});
});
describe('appendEmptyRowsOrColumns', () => {
it('should throw error when no rows or columns specified', async () => {
await expect(googleSheet.appendEmptyRowsOrColumns('sheet1', 0, 0)).rejects.toThrow(
'Must specify at least one column or row to add',
);
});
it('should make correct API call to append rows and columns', async () => {
const sheetId = 'sheet1';
await googleSheet.appendEmptyRowsOrColumns(sheetId, 2, 3);
expect(apiRequest.call).toHaveBeenCalledWith(
mockExecuteFunctions,
'POST',
`/v4/spreadsheets/${spreadsheetId}:batchUpdate`,
{
requests: [
{
appendDimension: {
sheetId,
dimension: 'ROWS',
length: 2,
},
},
{
appendDimension: {
sheetId,
dimension: 'COLUMNS',
length: 3,
},
},
],
},
);
});
});
describe('getData with dateTimeRenderOption', () => {
it('should use custom dateTimeRenderOption when provided', async () => {
const range = 'Sheet1!A1:B2';
const valueRenderMode = 'FORMATTED_VALUE';
const dateTimeRenderOption = 'SERIAL_NUMBER';
await googleSheet.getData(range, valueRenderMode, dateTimeRenderOption);
expect(apiRequest.call).toHaveBeenCalledWith(
mockExecuteFunctions,
'GET',
`/v4/spreadsheets/${spreadsheetId}/values/${range}`,
{},
{
valueRenderOption: valueRenderMode,
dateTimeRenderOption,
},
);
});
});
describe('spreadsheetGetSheets', () => {
it('should retrieve spreadsheet sheets with correct parameters', async () => {
const mockResponse = {
sheets: [
{ properties: { title: 'Sheet1', sheetId: 0 } },
{ properties: { title: 'Sheet2', sheetId: 1 } },
],
};
(apiRequest.call as jest.Mock).mockResolvedValue(mockResponse);
const result = await googleSheet.spreadsheetGetSheets();
expect(apiRequest.call).toHaveBeenCalledWith(
mockExecuteFunctions,
'GET',
`/v4/spreadsheets/${spreadsheetId}`,
{},
{ fields: 'sheets.properties' },
);
expect(result).toEqual(mockResponse);
});
});
describe('spreadsheetGetSheet', () => {
const mockResponse = {
sheets: [
{ properties: { title: 'Sheet1', sheetId: 0 } },
{ properties: { title: 'TestSheet', sheetId: 123456789 } },
],
};
beforeEach(() => {
(apiRequest.call as jest.Mock).mockResolvedValue(mockResponse);
});
it('should find sheet by name', async () => {
const mockNode = { type: 'test-node' } as any;
const result = await googleSheet.spreadsheetGetSheet(mockNode, 'name', 'TestSheet');
expect(result).toEqual({ title: 'TestSheet', sheetId: 123456789 });
});
it('should find sheet by ID', async () => {
const mockNode = { type: 'test-node' } as any;
const result = await googleSheet.spreadsheetGetSheet(mockNode, 'id', '123456789');
expect(result).toEqual({ title: 'TestSheet', sheetId: 123456789 });
});
it('should throw error when sheet not found by name', async () => {
const mockNode = { type: 'test-node' } as any;
await expect(
googleSheet.spreadsheetGetSheet(mockNode, 'name', 'NonExistentSheet'),
).rejects.toThrow('Sheet with name NonExistentSheet not found');
});
it('should throw error when sheet not found by ID', async () => {
const mockNode = { type: 'test-node' } as any;
await expect(googleSheet.spreadsheetGetSheet(mockNode, 'id', '999999999')).rejects.toThrow(
'Sheet with ID 999999999 not found',
);
});
});
describe('getDataRange', () => {
it('should return grid properties for sheet', async () => {
const mockResponse = {
sheets: [
{
properties: {
sheetId: '123',
gridProperties: {
rowCount: 100,
columnCount: 26,
},
},
},
],
};
(apiRequest.call as jest.Mock).mockResolvedValue(mockResponse);
const result = await googleSheet.getDataRange('123');
expect(apiRequest.call).toHaveBeenCalledWith(
mockExecuteFunctions,
'GET',
`/v4/spreadsheets/${spreadsheetId}`,
{},
{ fields: 'sheets.properties' },
);
expect(result).toEqual({ rowCount: 100, columnCount: 26 });
});
});
describe('spreadsheetBatchUpdate', () => {
it('should make correct API call for batch update', async () => {
const requests = [
{
updateSheetProperties: {
properties: { title: 'New Title' },
fields: 'title',
},
},
];
await googleSheet.spreadsheetBatchUpdate(requests);
expect(apiRequest.call).toHaveBeenCalledWith(
mockExecuteFunctions,
'POST',
`/v4/spreadsheets/${spreadsheetId}:batchUpdate`,
{ requests },
);
});
});
describe('batchUpdate', () => {
it('should make correct API call for batch value update', async () => {
const updateData = [
{
range: 'Sheet1!A1:B2',
values: [
['Name', 'Age'],
['John', '30'],
],
},
];
await googleSheet.batchUpdate(updateData, 'USER_ENTERED');
expect(apiRequest.call).toHaveBeenCalledWith(
mockExecuteFunctions,
'POST',
`/v4/spreadsheets/${spreadsheetId}/values:batchUpdate`,
{
data: updateData,
valueInputOption: 'USER_ENTERED',
},
);
});
});
describe('appendData', () => {
beforeEach(() => {
// Mock getData to return existing data
(apiRequest.call as jest.Mock).mockImplementation(async (_, method, _url) => {
if (method === 'GET') {
return { values: [['existing', 'row']] };
}
return { range: 'Sheet1!A2:B2' };
});
});
it('should append data with calculated last row', async () => {
const data = [
['John', '30'],
['Jane', '25'],
];
const result = await googleSheet.appendData('Sheet1!A:B', data, 'USER_ENTERED');
expect(result).toBeDefined();
});
it('should use provided last row', async () => {
const data = [['John', '30']];
await googleSheet.appendData('Sheet1!A:B', data, 'USER_ENTERED', 5);
// Should use row 5 instead of calculating
expect(apiRequest.call).toHaveBeenCalledWith(
mockExecuteFunctions,
'PUT',
expect.stringContaining('Sheet1!5:5'),
expect.any(Object),
expect.any(Object),
);
});
it('should use append mode when useAppend is true', async () => {
const data = [['John', '30']];
await googleSheet.appendData('Sheet1!A:B', data, 'USER_ENTERED', 5, true);
expect(apiRequest.call).toHaveBeenCalledWith(
mockExecuteFunctions,
'POST',
expect.stringContaining(':append'),
expect.any(Object),
expect.any(Object),
);
});
});
describe('updateRows', () => {
it('should make PUT request when useAppend is false', async () => {
const data = [['John', '30']];
await googleSheet.updateRows('Sheet1', data, 'USER_ENTERED', 2);
expect(apiRequest.call).toHaveBeenCalledWith(
mockExecuteFunctions,
'PUT',
`/v4/spreadsheets/${spreadsheetId}/values/Sheet1!2:2`,
{
range: 'Sheet1!2:2',
values: data,
},
{ valueInputOption: 'USER_ENTERED' },
);
});
it('should make POST request when useAppend is true', async () => {
const data = [['John', '30']];
await googleSheet.updateRows('Sheet1', data, 'USER_ENTERED', 2, 2, true);
expect(apiRequest.call).toHaveBeenCalledWith(
mockExecuteFunctions,
'POST',
`/v4/spreadsheets/${spreadsheetId}/values/Sheet1!2:3:append`,
{
range: 'Sheet1!2:3',
values: data,
},
{ valueInputOption: 'USER_ENTERED' },
);
});
it('should handle range with rowsLength', async () => {
const data = [
['John', '30'],
['Jane', '25'],
];
await googleSheet.updateRows('Sheet1', data, 'USER_ENTERED', 2, 2);
expect(apiRequest.call).toHaveBeenCalledWith(
mockExecuteFunctions,
'PUT',
`/v4/spreadsheets/${spreadsheetId}/values/Sheet1!2:3`,
{
range: 'Sheet1!2:3',
values: data,
},
{ valueInputOption: 'USER_ENTERED' },
);
});
});
describe('structureArrayDataByColumn', () => {
it('should structure data using key row', () => {
const inputData = [
['Name', 'Age', 'City'],
['John', '30', 'NYC'],
['Jane', '25', 'LA'],
];
const result = googleSheet.structureArrayDataByColumn(inputData, 0, 1);
expect(result).toEqual([
{ Name: 'John', Age: '30', City: 'NYC' },
{ Name: 'Jane', Age: '25', City: 'LA' },
]);
});
it('should return empty array for invalid key row', () => {
const inputData = [
['Name', 'Age'],
['John', '30'],
];
const result = googleSheet.structureArrayDataByColumn(inputData, -1, 1);
expect(result).toEqual([]);
});
it('should return empty array when dataStartRow < keyRow', () => {
const inputData = [
['Name', 'Age'],
['John', '30'],
];
const result = googleSheet.structureArrayDataByColumn(inputData, 1, 0);
expect(result).toEqual([]);
});
it('should handle missing column names with fallback', () => {
const inputData = [
['Name', '', 'City'], // Empty column name
['John', '30', 'NYC'],
];
const result = googleSheet.structureArrayDataByColumn(inputData, 0, 1);
expect(result).toEqual([{ Name: 'John', col_1: '30', City: 'NYC' }]);
});
it('should handle uneven row lengths', () => {
const inputData = [
['Name', 'Age', 'City'],
['John', '30'], // Shorter row
['Jane', '25', 'LA', 'Extra'], // Longer row
];
const result = googleSheet.structureArrayDataByColumn(inputData, 0, 1);
// The function uses the longest row to create keys, generating col_3 for the extra column
// Only properties with values are included, empty cells are omitted
expect(result).toEqual([
{ Name: 'John', Age: '30' },
{ Name: 'Jane', Age: '25', City: 'LA', col_3: 'Extra' },
]);
});
});
describe('testFilter', () => {
it('should return column keys from key row', () => {
const inputData = [
['Name', 'Age', 'City'],
['John', '30', 'NYC'],
];
const result = googleSheet.testFilter(inputData, 0, 1);
expect(result).toEqual(['Name', 'Age', 'City']);
});
it('should return empty array for invalid key row', () => {
const inputData = [['Name', 'Age']];
const result = googleSheet.testFilter(inputData, -1, 1);
expect(result).toEqual([]);
});
it('should return empty array when keyRow >= inputData.length', () => {
const inputData = [['Name', 'Age']];
const result = googleSheet.testFilter(inputData, 2, 1);
expect(result).toEqual([]);
});
});
describe('getColumnWithOffset', () => {
it('should calculate column with positive offset', () => {
const result = googleSheet.getColumnWithOffset('A', 2);
expect(result).toBe('C');
});
it('should calculate column with zero offset', () => {
const result = googleSheet.getColumnWithOffset('B', 0);
expect(result).toBe('B');
});
it('should handle double letter columns', () => {
const result = googleSheet.getColumnWithOffset('Z', 1);
expect(result).toBe('AA');
});
});
describe('getColumnValues', () => {
beforeEach(() => {
(apiRequest.call as jest.Mock).mockResolvedValue({
values: [['header'], ['value1'], ['value2']],
});
});
it('should get column values from sheet data when provided', async () => {
const sheetData = [
['Name', 'Age'],
['John', '30'],
['Jane', '25'],
];
const result = await googleSheet.getColumnValues({
range: 'Sheet1!A:B',
keyIndex: 0,
dataStartRowIndex: 1,
valueRenderMode: 'UNFORMATTED_VALUE',
sheetData,
});
expect(result).toEqual(['John', 'Jane']);
});
it('should make API call when sheet data not provided', async () => {
const result = await googleSheet.getColumnValues({
range: 'Sheet1!A1:B10',
keyIndex: 0,
dataStartRowIndex: 1,
valueRenderMode: 'UNFORMATTED_VALUE',
});
expect(apiRequest.call).toHaveBeenCalledWith(
mockExecuteFunctions,
'GET',
expect.stringContaining('/values/Sheet1!A1:A10'),
{},
{ valueRenderOption: 'UNFORMATTED_VALUE', dateTimeRenderOption: 'FORMATTED_STRING' },
);
expect(result).toEqual(['value1', 'value2']);
});
it('should throw error when column data cannot be retrieved', async () => {
(apiRequest.call as jest.Mock).mockResolvedValue({ values: undefined });
await expect(
googleSheet.getColumnValues({
range: 'Sheet1!A:B',
keyIndex: 0,
dataStartRowIndex: 1,
valueRenderMode: 'UNFORMATTED_VALUE',
}),
).rejects.toThrow('Could not retrieve the data from key column');
});
});
describe('prepareDataForUpdateOrUpsert', () => {
beforeEach(() => {
// Mock getData responses
(apiRequest.call as jest.Mock).mockImplementation(
(_: unknown, _method: unknown, url: string) => {
if (url.includes('/values/Sheet1!A1:C1')) {
return { values: [['Name', 'Age', 'City']] };
}
// Match the actual URL pattern generated by getColumnValues
if (url.includes('/values/Sheet1!A1:A10') || url.includes('/values/Sheet1%21A1%3AA10')) {
return { values: [['Name'], ['John'], ['Jane']] };
}
return {};
},
);
});
it('should prepare update data for existing records', async () => {
const inputData = [
{ Name: 'John', Age: '31', City: 'NYC' }, // Update existing
{ Name: 'Bob', Age: '25', City: 'LA' }, // New record
];
const result = await googleSheet.prepareDataForUpdateOrUpsert({
inputData,
indexKey: 'Name',
range: 'Sheet1!A1:C10',
keyRowIndex: 0,
dataStartRowIndex: 1,
valueRenderMode: 'UNFORMATTED_VALUE',
upsert: true,
});
expect(result.updateData).toHaveLength(2); // Age and City updates for John
expect(result.appendData).toHaveLength(1); // Bob should be appended
expect(result.appendData[0]).toEqual({ Name: 'Bob', Age: '25', City: 'LA' });
});
it('should throw error when index key not found and upsert is false', async () => {
const inputData = [{ Name: 'John', Age: '31' }];
await expect(
googleSheet.prepareDataForUpdateOrUpsert({
inputData,
indexKey: 'NonExistentKey',
range: 'Sheet1!A1:C10',
keyRowIndex: 0,
dataStartRowIndex: 1,
valueRenderMode: 'UNFORMATTED_VALUE',
upsert: false,
}),
).rejects.toThrow('Could not find column for key "NonExistentKey"');
});
it('should throw error when key row cannot be retrieved', async () => {
(apiRequest.call as jest.Mock).mockResolvedValue({ values: undefined });
const inputData = [{ Name: 'John' }];
await expect(
googleSheet.prepareDataForUpdateOrUpsert({
inputData,
indexKey: 'Name',
range: 'Sheet1!A1:C10',
keyRowIndex: 0,
dataStartRowIndex: 1,
valueRenderMode: 'UNFORMATTED_VALUE',
}),
).rejects.toThrow('Could not retrieve the key row');
});
it('should handle items without index key when upsert is true', async () => {
const inputData = [{ Age: '30', City: 'NYC' }]; // No Name field
const result = await googleSheet.prepareDataForUpdateOrUpsert({
inputData,
indexKey: 'Name',
range: 'Sheet1!A1:C10',
keyRowIndex: 0,
dataStartRowIndex: 1,
valueRenderMode: 'UNFORMATTED_VALUE',
upsert: true,
});
expect(result.updateData).toHaveLength(0);
expect(result.appendData).toHaveLength(1);
});
it('should stringify object values', async () => {
const inputData = [{ Name: 'John', Age: { years: 30 }, City: 'NYC' }];
const result = await googleSheet.prepareDataForUpdateOrUpsert({
inputData,
indexKey: 'Name',
range: 'Sheet1!A1:C10',
keyRowIndex: 0,
dataStartRowIndex: 1,
valueRenderMode: 'UNFORMATTED_VALUE',
upsert: true,
});
const ageUpdate = result.updateData.find((update) => update.range.includes('B'));
expect(ageUpdate?.values[0][0]).toBe('{"years":30}');
});
});
describe('prepareDataForUpdatingByRowNumber', () => {
it('should prepare update data using row numbers', () => {
const inputData = [
{ row_number: 2, Name: 'John', Age: '31' },
{ row_number: 3, Name: 'Jane', Age: '26' },
];
const columnNamesList = [['row_number', 'Name', 'Age', 'City']];
const result = googleSheet.prepareDataForUpdatingByRowNumber(
inputData,
'Sheet1!A1:D10',
columnNamesList,
);
expect(result.updateData).toHaveLength(4); // 2 items × 2 fields each
expect(result.updateData[0]).toEqual({
range: 'Sheet1!B2',
values: [['John']],
});
expect(result.updateData[1]).toEqual({
range: 'Sheet1!C2',
values: [['31']],
});
});
it('should skip row_number field and null/undefined values', () => {
const inputData = [{ row_number: 2, Name: 'John', Age: null, City: undefined }];
const columnNamesList = [['row_number', 'Name', 'Age', 'City']];
const result = googleSheet.prepareDataForUpdatingByRowNumber(
inputData,
'Sheet1!A1:D10',
columnNamesList,
);
expect(result.updateData).toHaveLength(1); // Only Name field
expect(result.updateData[0].range).toBe('Sheet1!B2');
});
it('should stringify object values', () => {
const inputData = [{ row_number: 2, Name: { first: 'John', last: 'Doe' } }];
const columnNamesList = [['row_number', 'Name']];
const result = googleSheet.prepareDataForUpdatingByRowNumber(
inputData,
'Sheet1!A1:B10',
columnNamesList,
);
expect(result.updateData[0].values[0][0]).toBe('{"first":"John","last":"Doe"}');
});
});
describe('private method testing via clearData (encodeRange)', () => {
it('should encode sheet name with special characters', async () => {
const range = 'Sheet with spaces!A1:B2';
await googleSheet.clearData(range);
expect(apiRequest.call).toHaveBeenCalledWith(
mockExecuteFunctions,
'POST',
`/v4/spreadsheets/${spreadsheetId}/values/Sheet%20with%20spaces!A1:B2:clear`,
{ spreadsheetId, range },
);
});
it('should encode range without sheet reference', async () => {
const range = 'Sheet with spaces';
await googleSheet.clearData(range);
expect(apiRequest.call).toHaveBeenCalledWith(
mockExecuteFunctions,
'POST',
expect.stringContaining(encodeURIComponent("'Sheet with spaces'")),
{ spreadsheetId, range },
);
});
});
});