From a8e7a05856d9f2190d80df30719c442f06ea4a71 Mon Sep 17 00:00:00 2001 From: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:07:39 +0000 Subject: [PATCH] test(Google Sheets Node): Add Tests for Google Sheet Node (no-changelog) (#12217) --- .../Sheet/test/v2/methods/loadOptions.test.ts | 169 ++++++++++++++++ .../Google/Sheet/test/v2/utils/utils.test.ts | 191 +++++++++++++++++- 2 files changed, 358 insertions(+), 2 deletions(-) create mode 100644 packages/nodes-base/nodes/Google/Sheet/test/v2/methods/loadOptions.test.ts diff --git a/packages/nodes-base/nodes/Google/Sheet/test/v2/methods/loadOptions.test.ts b/packages/nodes-base/nodes/Google/Sheet/test/v2/methods/loadOptions.test.ts new file mode 100644 index 0000000000..ce22c9783d --- /dev/null +++ b/packages/nodes-base/nodes/Google/Sheet/test/v2/methods/loadOptions.test.ts @@ -0,0 +1,169 @@ +import type { ILoadOptionsFunctions } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; + +import { + getSheetHeaderRow, + getSheetHeaderRowAndAddColumn, + getSheetHeaderRowAndSkipEmpty, + getSheetHeaderRowWithGeneratedColumnNames, + getSheets, +} from '../../../v2/methods/loadOptions'; + +jest.mock('../../../v2/helpers/GoogleSheets.utils'); + +const mockGoogleSheetInstance = { + spreadsheetGetSheets: jest.fn(), + spreadsheetGetSheet: jest.fn(), + getData: jest.fn(), + testFilter: jest.fn(), +}; + +jest.mock('../../../v2/helpers/GoogleSheet', () => ({ + GoogleSheet: jest.fn().mockImplementation(() => mockGoogleSheetInstance), +})); + +describe('Google Sheets Functions', () => { + let mockLoadOptionsFunctions: Partial; + + beforeEach(() => { + jest.clearAllMocks(); + + mockLoadOptionsFunctions = { + getNodeParameter: jest.fn((paramName: string) => { + if (paramName === 'documentId') { + return { mode: 'mode', value: 'value' }; + } + if (paramName === 'sheetName') { + return { mode: 'Sheet1', value: 'Sheet1' }; + } + }), + getNode: jest.fn(), + }; + }); + + describe('getSheets', () => { + it('should return an empty array if documentId is null', async () => { + mockLoadOptionsFunctions.getNodeParameter = jest.fn().mockReturnValue(null); + + const result = await getSheets.call(mockLoadOptionsFunctions as ILoadOptionsFunctions); + expect(result).toEqual([]); + }); + + it('should throw an error if no data is returned', async () => { + mockGoogleSheetInstance.spreadsheetGetSheets.mockResolvedValue(undefined); + + await expect( + getSheets.call(mockLoadOptionsFunctions as ILoadOptionsFunctions), + ).rejects.toThrow(NodeOperationError); + }); + + it('should return sheets with GRID type', async () => { + mockGoogleSheetInstance.spreadsheetGetSheets.mockResolvedValue({ + sheets: [ + { properties: { sheetType: 'GRID', title: 'Sheet1', sheetId: '123' } }, + { properties: { sheetType: 'OTHER', title: 'Sheet2', sheetId: '456' } }, + ], + }); + + const result = await getSheets.call(mockLoadOptionsFunctions as ILoadOptionsFunctions); + expect(result).toEqual([{ name: 'Sheet1', value: '123' }]); + }); + }); + + describe('getSheetHeaderRow', () => { + it('should return an empty array if documentId is null', async () => { + mockLoadOptionsFunctions.getNodeParameter = jest.fn().mockReturnValue(null); + + const result = await getSheetHeaderRow.call( + mockLoadOptionsFunctions as ILoadOptionsFunctions, + ); + expect(result).toEqual([]); + }); + + it('should throw an error if no data is returned', async () => { + mockGoogleSheetInstance.spreadsheetGetSheet.mockResolvedValue({ + title: 'Sheet1', + }); + mockGoogleSheetInstance.getData.mockResolvedValue(undefined); + + await expect( + getSheetHeaderRow.call(mockLoadOptionsFunctions as ILoadOptionsFunctions), + ).rejects.toThrow(NodeOperationError); + }); + + it('should return column headers', async () => { + mockGoogleSheetInstance.spreadsheetGetSheet.mockResolvedValue({ + title: 'Sheet1', + }); + mockGoogleSheetInstance.getData.mockResolvedValue([['Header1', 'Header2', 'Header3']]); + mockGoogleSheetInstance.testFilter.mockReturnValue(['Header1', 'Header2', 'Header3']); + + const result = await getSheetHeaderRow.call( + mockLoadOptionsFunctions as ILoadOptionsFunctions, + ); + expect(result).toEqual([ + { name: 'Header1', value: 'Header1' }, + { name: 'Header2', value: 'Header2' }, + { name: 'Header3', value: 'Header3' }, + ]); + }); + }); + + describe('getSheetHeaderRowAndAddColumn', () => { + it('should add a new column and exclude the column to match on', async () => { + mockGoogleSheetInstance.spreadsheetGetSheet.mockResolvedValue({ + title: 'Sheet1', + }); + mockGoogleSheetInstance.getData.mockResolvedValue([['Header1']]); + mockGoogleSheetInstance.testFilter.mockReturnValue(['Header1']); + + const result = await getSheetHeaderRowAndAddColumn.call( + mockLoadOptionsFunctions as ILoadOptionsFunctions, + ); + + expect(result).toEqual([ + { name: 'Header1', value: 'Header1' }, + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + { name: 'New column ...', value: 'newColumn' }, + ]); + }); + }); + + describe('getSheetHeaderRowWithGeneratedColumnNames', () => { + it('should generate column names for empty values', async () => { + mockGoogleSheetInstance.spreadsheetGetSheet.mockResolvedValue({ + title: 'Sheet1', + }); + mockGoogleSheetInstance.getData.mockResolvedValue([['', 'Header1', '']]); + mockGoogleSheetInstance.testFilter.mockReturnValue(['', 'Header1', '']); + + const result = await getSheetHeaderRowWithGeneratedColumnNames.call( + mockLoadOptionsFunctions as ILoadOptionsFunctions, + ); + + expect(result).toEqual([ + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + { name: 'col_1', value: 'col_1' }, + { name: 'Header1', value: 'Header1' }, + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + { name: 'col_3', value: 'col_3' }, + ]); + }); + }); + + describe('getSheetHeaderRowAndSkipEmpty', () => { + it('should skip columns with empty values', async () => { + mockGoogleSheetInstance.spreadsheetGetSheet.mockResolvedValue({ + title: 'Sheet1', + }); + mockGoogleSheetInstance.getData.mockResolvedValue([['', 'Header1', '']]); + mockGoogleSheetInstance.testFilter.mockReturnValue(['', 'Header1', '']); + + const result = await getSheetHeaderRowAndSkipEmpty.call( + mockLoadOptionsFunctions as ILoadOptionsFunctions, + ); + + expect(result).toEqual([{ name: 'Header1', value: 'Header1' }]); + }); + }); +}); diff --git a/packages/nodes-base/nodes/Google/Sheet/test/v2/utils/utils.test.ts b/packages/nodes-base/nodes/Google/Sheet/test/v2/utils/utils.test.ts index c725d03bb9..4c3f9861be 100644 --- a/packages/nodes-base/nodes/Google/Sheet/test/v2/utils/utils.test.ts +++ b/packages/nodes-base/nodes/Google/Sheet/test/v2/utils/utils.test.ts @@ -5,12 +5,20 @@ import { type ResourceMapperField, } from 'n8n-workflow'; +import { GOOGLE_SHEETS_SHEET_URL_REGEX } from '../../../../constants'; import { GoogleSheet } from '../../../v2/helpers/GoogleSheet'; import { addRowNumber, autoMapInputData, checkForSchemaChanges, + getColumnName, + getColumnNumber, + getExistingSheetNames, + getRangeString, + getSheetId, getSpreadsheetId, + hexToRgb, + mapFields, prepareSheetData, removeEmptyColumns, removeEmptyRows, @@ -18,8 +26,6 @@ import { trimToFirstEmptyRow, } from '../../../v2/helpers/GoogleSheets.utils'; -import { GOOGLE_SHEETS_SHEET_URL_REGEX } from '../../../../constants'; - describe('Test Google Sheets, addRowNumber', () => { it('should add row nomber', () => { const data = [ @@ -523,3 +529,184 @@ describe('Test Google Sheets, Google Sheets Sheet URL Regex', () => { } }); }); + +describe('Test Google Sheets, getColumnNumber', () => { + it('should return the correct number for single-letter columns', () => { + expect(getColumnNumber('A')).toBe(1); + expect(getColumnNumber('Z')).toBe(26); + }); + + it('should return the correct number for multi-letter columns', () => { + expect(getColumnNumber('AA')).toBe(27); + expect(getColumnNumber('AZ')).toBe(52); + expect(getColumnNumber('BA')).toBe(53); + expect(getColumnNumber('ZZ')).toBe(702); + expect(getColumnNumber('AAA')).toBe(703); + }); +}); + +describe('Test Google Sheets, hexToRgb', () => { + it('should correctly convert a full hex code to RGB', () => { + expect(hexToRgb('#0033FF')).toEqual({ red: 0, green: 51, blue: 255 }); + expect(hexToRgb('#FF5733')).toEqual({ red: 255, green: 87, blue: 51 }); + }); + + it('should correctly convert a shorthand hex code to RGB', () => { + expect(hexToRgb('#03F')).toEqual({ red: 0, green: 51, blue: 255 }); + expect(hexToRgb('#F00')).toEqual({ red: 255, green: 0, blue: 0 }); + }); + + it('should return null for invalid hex codes', () => { + expect(hexToRgb('#XYZ123')).toBeNull(); // Invalid characters + expect(hexToRgb('#12345')).toBeNull(); // Incorrect length + expect(hexToRgb('')).toBeNull(); // Empty input + expect(hexToRgb('#')).toBeNull(); // Just a hash + }); +}); + +describe('Test Google Sheets, getRangeString', () => { + it('should return the range in A1 notation when "specifyRangeA1" is set', () => { + const result = getRangeString('Sheet1', { rangeDefinition: 'specifyRangeA1', range: 'A1:B2' }); + expect(result).toBe('Sheet1!A1:B2'); + }); + + it('should return only the sheet name if no range is specified', () => { + const result = getRangeString('Sheet1', { rangeDefinition: 'specifyRangeA1', range: '' }); + expect(result).toBe('Sheet1'); + }); + + it('should return only the sheet name if rangeDefinition is not "specifyRangeA1"', () => { + const result = getRangeString('Sheet1', { rangeDefinition: 'detectAutomatically' }); + expect(result).toBe('Sheet1'); + }); +}); + +describe('Test Google Sheets, getExistingSheetNames', () => { + const mockGoogleSheetInstance: Partial = { + spreadsheetGetSheets: jest.fn(), + }; + it('should return an array of sheet names', async () => { + mockGoogleSheetInstance.spreadsheetGetSheets = jest.fn().mockResolvedValue({ + sheets: [{ properties: { title: 'Sheet1' } }, { properties: { title: 'Sheet2' } }], + }); + const result = await getExistingSheetNames(mockGoogleSheetInstance as GoogleSheet); + expect(result).toEqual(['Sheet1', 'Sheet2']); + }); + + it('should return an empty array if no sheets are present', async () => { + mockGoogleSheetInstance.spreadsheetGetSheets = jest.fn().mockResolvedValue({ sheets: [] }); + const result = await getExistingSheetNames(mockGoogleSheetInstance as GoogleSheet); + expect(result).toEqual([]); + }); + + it('should handle a case where sheets are undefined', async () => { + mockGoogleSheetInstance.spreadsheetGetSheets = jest.fn().mockResolvedValue({}); + const result = await getExistingSheetNames(mockGoogleSheetInstance as GoogleSheet); + expect(result).toEqual([]); + }); +}); + +describe('Test Google Sheets, mapFields', () => { + const fakeExecuteFunction: Partial = {}; + + beforeEach(() => { + fakeExecuteFunction.getNode = jest.fn(); + fakeExecuteFunction.getNodeParameter = jest.fn(); + }); + + it('should map fields for node version < 4', () => { + fakeExecuteFunction.getNode = jest.fn().mockReturnValue({ typeVersion: 3 }); + fakeExecuteFunction.getNodeParameter = jest.fn().mockImplementation((_, i) => [ + { fieldId: 'field1', fieldValue: `value${i}` }, + { fieldId: 'field2', fieldValue: `value${i * 2}` }, + ]); + + const result = mapFields.call(fakeExecuteFunction as IExecuteFunctions, 2); + expect(result).toEqual([ + { field1: 'value0', field2: 'value0' }, + { field1: 'value1', field2: 'value2' }, + ]); + expect(fakeExecuteFunction.getNodeParameter).toHaveBeenCalledTimes(2); + expect(fakeExecuteFunction.getNodeParameter).toHaveBeenCalledWith( + 'fieldsUi.fieldValues', + 0, + [], + ); + }); + + it('should map columns for node version >= 4', () => { + fakeExecuteFunction.getNode = jest.fn().mockReturnValue({ typeVersion: 4 }); + fakeExecuteFunction.getNodeParameter = jest.fn().mockImplementation((_, i) => ({ + column1: `value${i}`, + column2: `value${i * 2}`, + })); + + const result = mapFields.call(fakeExecuteFunction as IExecuteFunctions, 2); + expect(result).toEqual([ + { column1: 'value0', column2: 'value0' }, + { column1: 'value1', column2: 'value2' }, + ]); + expect(fakeExecuteFunction.getNodeParameter).toHaveBeenCalledTimes(2); + expect(fakeExecuteFunction.getNodeParameter).toHaveBeenCalledWith('columns.value', 0); + }); + + it('should throw an error if no values are added in version >= 4', () => { + fakeExecuteFunction.getNode = jest.fn().mockReturnValue({ typeVersion: 4 }); + fakeExecuteFunction.getNodeParameter = jest.fn().mockReturnValue({}); + + expect(() => mapFields.call(fakeExecuteFunction as IExecuteFunctions, 1)).toThrow( + "At least one value has to be added under 'Values to Send'", + ); + }); + + it('should return an empty array when inputSize is 0', () => { + const result = mapFields.call(fakeExecuteFunction as IExecuteFunctions, 0); + expect(result).toEqual([]); + }); +}); + +describe('Test Google Sheets, getSheetId', () => { + it('should return 0 when value is "gid=0"', () => { + expect(getSheetId('gid=0')).toBe(0); + }); + + it('should return a parsed integer when value is a numeric string', () => { + expect(getSheetId('123')).toBe(123); + expect(getSheetId('456')).toBe(456); + }); + + it('should return NaN for non-numeric strings', () => { + expect(getSheetId('abc')).toBeNaN(); + expect(getSheetId('gid=abc')).toBeNaN(); + }); +}); + +describe('Test Google Sheets, getColumnName', () => { + it('should return "A" for column number 1', () => { + expect(getColumnName(1)).toBe('A'); + }); + + it('should return "Z" for column number 26', () => { + expect(getColumnName(26)).toBe('Z'); + }); + + it('should return "AA" for column number 27', () => { + expect(getColumnName(27)).toBe('AA'); + }); + + it('should return "AZ" for column number 52', () => { + expect(getColumnName(52)).toBe('AZ'); + }); + + it('should return "BA" for column number 53', () => { + expect(getColumnName(53)).toBe('BA'); + }); + + it('should return "ZZ" for column number 702', () => { + expect(getColumnName(702)).toBe('ZZ'); + }); + + it('should return "AAA" for column number 703', () => { + expect(getColumnName(703)).toBe('AAA'); + }); +});