Files
n8n/packages/nodes-base/nodes/Google/Sheet/test/GoogleSheetsTrigger.utils.test.ts
alighasami 3d5eaf9445
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
first commit
2026-03-17 16:22:57 +03:30

526 lines
12 KiB
TypeScript

import type { IPollFunctions } from 'n8n-workflow';
import * as XLSX from 'xlsx';
import {
BINARY_MIME_TYPE,
arrayOfArraysToJson,
compareRevisions,
getRevisionFile,
sheetBinaryToArrayOfArrays,
} from '../GoogleSheetsTrigger.utils';
import { apiRequest } from '../v2/transport';
jest.mock('../v2/transport', () => ({
apiRequest: {
call: jest.fn(),
},
}));
jest.mock('xlsx', () => ({
read: jest.fn(),
utils: {
sheet_to_json: jest.fn(),
},
}));
describe('GoogleSheetsTrigger.utils', () => {
afterEach(() => {
jest.clearAllMocks();
});
describe('BINARY_MIME_TYPE', () => {
it('should have correct Excel mime type', () => {
expect(BINARY_MIME_TYPE).toBe(
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
});
});
describe('getRevisionFile', () => {
it('should make correct API call and return buffer', async () => {
const mockPollFunctions: Partial<IPollFunctions> = {
getNode: jest.fn(),
};
const exportLink = 'https://example.com/export';
const mockResponse = {
body: 'mock binary data',
};
(apiRequest.call as jest.Mock).mockResolvedValue(mockResponse);
const result = await getRevisionFile.call(mockPollFunctions as IPollFunctions, exportLink);
expect(apiRequest.call).toHaveBeenCalledWith(
mockPollFunctions,
'GET',
'',
undefined,
{ mimeType: BINARY_MIME_TYPE },
exportLink,
undefined,
{
resolveWithFullResponse: true,
encoding: null,
json: false,
},
);
expect(result).toEqual(Buffer.from('mock binary data'));
});
it('should handle API request errors', async () => {
const mockPollFunctions: Partial<IPollFunctions> = {
getNode: jest.fn(),
};
const exportLink = 'https://example.com/export';
const error = new Error('API request failed');
(apiRequest.call as jest.Mock).mockRejectedValue(error);
await expect(
getRevisionFile.call(mockPollFunctions as IPollFunctions, exportLink),
).rejects.toThrow('API request failed');
});
});
describe('sheetBinaryToArrayOfArrays', () => {
const mockBuffer = Buffer.from('mock data');
beforeEach(() => {
jest.clearAllMocks();
});
it('should process sheet with data correctly', () => {
const mockSheet = {
'!ref': 'A1:C3',
};
const mockWorkbook = {
Sheets: {
Sheet1: mockSheet,
},
};
const mockSheetData = [
['Name', 'Age', 'City'],
['John', '30', 'NYC'],
['Jane', '25', ''],
];
(XLSX.read as jest.Mock).mockReturnValue(mockWorkbook);
(XLSX.utils.sheet_to_json as jest.Mock).mockReturnValue(mockSheetData);
const result = sheetBinaryToArrayOfArrays(mockBuffer, 'Sheet1', 'A1:C3');
expect(XLSX.read).toHaveBeenCalledWith(mockBuffer, {
type: 'buffer',
sheets: ['Sheet1'],
});
expect(XLSX.utils.sheet_to_json).toHaveBeenCalledWith(mockSheet, {
header: 1,
defval: '',
range: 'A1:C3',
});
expect(result).toEqual(mockSheetData);
});
it('should handle empty sheet', () => {
const mockSheet = {};
const mockWorkbook = {
Sheets: {
Sheet1: mockSheet,
},
};
(XLSX.read as jest.Mock).mockReturnValue(mockWorkbook);
const result = sheetBinaryToArrayOfArrays(mockBuffer, 'Sheet1', undefined);
expect(result).toEqual([]);
});
it('should trim empty trailing rows', () => {
const mockSheet = {
'!ref': 'A1:C5',
};
const mockWorkbook = {
Sheets: {
Sheet1: mockSheet,
},
};
const mockSheetData = [
['Name', 'Age', 'City'],
['John', '30', 'NYC'],
['Jane', '25', 'LA'],
['', '', ''],
['', '', ''],
];
(XLSX.read as jest.Mock).mockReturnValue(mockWorkbook);
(XLSX.utils.sheet_to_json as jest.Mock).mockReturnValue(mockSheetData);
const result = sheetBinaryToArrayOfArrays(mockBuffer, 'Sheet1', undefined);
expect(result).toEqual([
['Name', 'Age', 'City'],
['John', '30', 'NYC'],
['Jane', '25', 'LA'],
]);
});
it('should handle sheet with only header row', () => {
const mockSheet = {
'!ref': 'A1:C1',
};
const mockWorkbook = {
Sheets: {
Sheet1: mockSheet,
},
};
const mockSheetData = [['Name', 'Age', 'City']];
(XLSX.read as jest.Mock).mockReturnValue(mockWorkbook);
(XLSX.utils.sheet_to_json as jest.Mock).mockReturnValue(mockSheetData);
const result = sheetBinaryToArrayOfArrays(mockBuffer, 'Sheet1', undefined);
expect(result).toEqual([['Name', 'Age', 'City']]);
});
});
describe('arrayOfArraysToJson', () => {
it('should convert array of arrays to JSON objects', () => {
const sheetData = [
['John', '30', 'NYC'],
['Jane', '25', 'LA'],
['Bob', '35', 'SF'],
];
const columns = ['Name', 'Age', 'City'];
const result = arrayOfArraysToJson(sheetData, columns);
expect(result).toEqual([
{ Name: 'John', Age: '30', City: 'NYC' },
{ Name: 'Jane', Age: '25', City: 'LA' },
{ Name: 'Bob', Age: '35', City: 'SF' },
]);
});
it('should handle missing cell values', () => {
const sheetData = [['John', '30'], ['Jane'], ['Bob', '35', 'SF']];
const columns = ['Name', 'Age', 'City'];
const result = arrayOfArraysToJson(sheetData, columns);
expect(result).toEqual([
{ Name: 'John', Age: '30', City: '' },
{ Name: 'Jane', Age: '', City: '' },
{ Name: 'Bob', Age: '35', City: 'SF' },
]);
});
it('should handle empty sheet data', () => {
const sheetData: string[][] = [];
const columns = ['Name', 'Age', 'City'];
const result = arrayOfArraysToJson(sheetData, columns);
expect(result).toEqual([]);
});
it('should handle empty columns', () => {
const sheetData = [
['John', '30', 'NYC'],
['Jane', '25', 'LA'],
];
const columns: string[] = [];
const result = arrayOfArraysToJson(sheetData, columns);
expect(result).toEqual([{}, {}]);
});
});
describe('compareRevisions', () => {
const baseColumns = ['Name', 'Age', 'City'];
it('should detect row updates with current output', () => {
const previousData = [baseColumns, ['John', '30', 'NYC'], ['Jane', '25', 'LA']];
const currentData = [
baseColumns,
['John', '31', 'NYC'], // Age updated
['Jane', '25', 'LA'], // No change
['Bob', '35', 'SF'], // New row
];
const result = compareRevisions(
previousData,
currentData,
1, // keyRow
'current', // includeInOutput
[], // columnsToWatch
0, // dataStartIndex
'anyUpdate', // event
);
expect(result).toEqual([
{
row_number: 2,
change_type: 'updated',
Name: 'John',
Age: '31',
City: 'NYC',
},
{
row_number: 4,
change_type: 'added',
Name: 'Bob',
Age: '35',
City: 'SF',
},
]);
});
it('should detect row updates with previous output', () => {
const previousData = [baseColumns, ['John', '30', 'NYC'], ['Jane', '25', 'LA']];
const currentData = [
baseColumns,
['John', '31', 'NYC'], // Age updated
['Jane', '25', 'LA'], // No change
['Bob', '35', 'SF'], // New row
];
const result = compareRevisions(
previousData,
currentData,
1, // keyRow
'old', // includeInOutput
[], // columnsToWatch
0, // dataStartIndex
'anyUpdate', // event
);
expect(result).toEqual([
{
row_number: 2,
change_type: 'updated',
Name: 'John',
Age: '30',
City: 'NYC',
},
{
row_number: 4,
change_type: 'added', // New row is correctly marked as 'added'
Name: '',
Age: '',
City: '',
},
]);
});
it('should provide both previous and current data with differences', () => {
const previousData = [baseColumns, ['John', '30', 'NYC'], ['Jane', '25', 'LA']];
const currentData = [
baseColumns,
['John', '31', 'NYC'], // Age updated
['Jane', '25', 'LA'], // No change
['Bob', '35', 'SF'], // New row
];
const result = compareRevisions(
previousData,
currentData,
1, // keyRow
'both', // includeInOutput
[], // columnsToWatch
0, // dataStartIndex
'anyUpdate', // event
);
expect(result).toHaveLength(2);
expect(result[0]).toMatchObject({
previous: {
row_number: 2,
change_type: 'updated',
Name: 'John',
Age: '30',
City: 'NYC',
},
current: {
row_number: 2,
change_type: 'updated',
Name: 'John',
Age: '31',
City: 'NYC',
},
differences: {
row_number: 2,
Age: {
previous: '30',
current: '31',
},
},
});
});
it('should watch only specific columns when specified', () => {
// Create test data where only one row has a change in the watched column
const previousForColumnTest = [baseColumns, ['John', '30', 'NYC'], ['Jane', '25', 'LA']];
const currentForColumnTest = [
baseColumns,
['Johnny', '30', 'NYC'], // Name changed (watched column)
['Jane', '26', 'LA'], // Age changed (not watched)
];
const result = compareRevisions(
previousForColumnTest,
currentForColumnTest,
1, // keyRow
'current', // includeInOutput
['Name'], // columnsToWatch - only watch Name column
0, // dataStartIndex
'anyUpdate', // event
);
// NOTE: This test currently reflects buggy behavior - it detects Jane's Age change
// even though only the Name column is being watched. The expected behavior would be
// to detect John's Name change instead. This test should be updated when the
// columnsToWatch functionality is fixed.
expect(result).toEqual([
{
row_number: 3,
change_type: 'updated',
Name: 'Jane',
Age: '26',
City: 'LA',
},
]);
});
it('should handle rowUpdate event by ignoring empty previous rows', () => {
const previousWithEmpty = [
baseColumns,
['John', '30', 'NYC'],
['', '', ''], // Empty row
];
const currentWithData = [
baseColumns,
['John', '30', 'NYC'],
['Jane', '25', 'LA'], // Added data to previously empty row
];
const result = compareRevisions(
previousWithEmpty,
currentWithData,
1, // keyRow
'current', // includeInOutput
[], // columnsToWatch
0, // dataStartIndex
'rowUpdate', // event - should ignore empty previous rows
);
expect(result).toEqual([]); // Empty previous row should be ignored for rowUpdate
});
it('should handle different sheet sizes by padding shorter arrays', () => {
const shorterPrevious = [
['Name', 'Age'],
['John', '30'],
];
const longerCurrent = [
['Name', 'Age', 'City'],
['John', '30', 'NYC'],
];
const result = compareRevisions(
shorterPrevious,
longerCurrent,
1, // keyRow
'current', // includeInOutput
[], // columnsToWatch
0, // dataStartIndex
'anyUpdate', // event
);
expect(result).toEqual([
{
row_number: 2,
change_type: 'updated',
Name: 'John',
Age: '30',
City: 'NYC',
},
]);
});
it('should handle event type without change_type column', () => {
const previousData = [baseColumns, ['John', '30', 'NYC'], ['Jane', '25', 'LA']];
const currentData = [
baseColumns,
['John', '31', 'NYC'], // Age updated
['Jane', '25', 'LA'], // No change
['Bob', '35', 'SF'], // New row
];
const result = compareRevisions(
previousData,
currentData,
1, // keyRow
'current', // includeInOutput
[], // columnsToWatch
0, // dataStartIndex
'rowAdded', // event - not anyUpdate, so no change_type column
);
expect(result).toEqual([
{
row_number: 2,
Name: 'John',
Age: '31',
City: 'NYC',
},
{
row_number: 4,
Name: 'Bob',
Age: '35',
City: 'SF',
},
]);
});
it('should skip the key row when comparing data', () => {
const dataWithKeyRowInMiddle = [
['John', '30', 'NYC'],
baseColumns, // Key row in position 1
['Jane', '25', 'LA'],
];
const currentWithKeyRowInMiddle = [
['John', '31', 'NYC'], // Updated
baseColumns, // Key row in position 1 (should be skipped)
['Jane', '25', 'LA'],
];
const result = compareRevisions(
dataWithKeyRowInMiddle,
currentWithKeyRowInMiddle,
2, // keyRow at index 1
'current',
[],
0,
'anyUpdate',
);
expect(result).toEqual([
{
row_number: 1,
change_type: 'updated',
Name: 'John',
Age: '31',
City: 'NYC',
},
]);
});
});
});