From 68a8502920a27d904dbb2d4a9902c69361f5f490 Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Thu, 29 Feb 2024 14:01:41 +0100 Subject: [PATCH] TWNTY-3316 - Add tests for `modules/spreadsheet-import` (#4219) Add tests for `modules/spreadsheet-import` Co-authored-by: gitstart-twenty Co-authored-by: RubensRafael --- .../__tests__/useSpreadsheetImport.test.tsx | 67 ++++++ .../useSpreadsheetImportInitialStep.test.ts | 47 ++++ .../useSpreadsheetImportInternal.test.tsx | 19 ++ ...heetImport.tsx => useSpreadsheetImport.ts} | 0 .../utils/__tests__/dataMutations.test.ts | 209 ++++++++++++++++++ .../utils/__tests__/exceedsMaxRecords.test.ts | 45 ++++ .../utils/__tests__/findMatch.test.ts | 90 ++++++++ .../findUnmatchedRequiredFields.test.ts | 89 ++++++++ .../__tests__/generateExampleRow.test.ts | 60 +++++ .../utils/__tests__/getFieldOptions.test.ts | 62 ++++++ .../utils/__tests__/getMatchedColumns.test.ts | 132 +++++++++++ .../utils/__tests__/mapWorkbook.test.ts | 46 ++++ .../__tests__/normalizeCheckboxValue.test.ts | 23 ++ .../__tests__/normalizeTableData.test.ts | 147 ++++++++++++ .../utils/__tests__/readFilesAsync.test.ts | 9 + .../utils/__tests__/setColumn.test.ts | 94 ++++++++ .../utils/__tests__/setIgnoreColumn.test.ts | 22 ++ .../utils/__tests__/setSubColumn.test.ts | 63 ++++++ .../utils/__tests__/uniqueEntries.test.ts | 12 + 19 files changed, 1236 insertions(+) create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImportInitialStep.test.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImportInternal.test.tsx rename packages/twenty-front/src/modules/spreadsheet-import/hooks/{useSpreadsheetImport.tsx => useSpreadsheetImport.ts} (100%) create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/dataMutations.test.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/exceedsMaxRecords.test.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findMatch.test.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findUnmatchedRequiredFields.test.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/generateExampleRow.test.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getFieldOptions.test.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getMatchedColumns.test.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/mapWorkbook.test.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/normalizeCheckboxValue.test.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/normalizeTableData.test.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/readFilesAsync.test.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setColumn.test.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setIgnoreColumn.test.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setSubColumn.test.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/uniqueEntries.test.ts diff --git a/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx b/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx new file mode 100644 index 000000000..c54a5498a --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx @@ -0,0 +1,67 @@ +import { act, renderHook } from '@testing-library/react'; +import { RecoilRoot, useRecoilState } from 'recoil'; + +import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport'; +import { spreadsheetImportState } from '@/spreadsheet-import/states/spreadsheetImportState'; +import { StepType } from '@/spreadsheet-import/steps/components/UploadFlow'; +import { RawData, SpreadsheetOptions } from '@/spreadsheet-import/types'; + +const Wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); +type SpreadsheetKey = 'spreadsheet_key'; + +export const mockedSpreadsheetOptions: SpreadsheetOptions = { + isOpen: true, + onClose: () => {}, + fields: [], + uploadStepHook: async () => [], + selectHeaderStepHook: async (headerValues: RawData, data: RawData[]) => ({ + headerValues, + data, + }), + matchColumnsStepHook: async () => [], + rowHook: () => ({ spreadsheet_key: 'rowHook' }), + tableHook: () => [{ spreadsheet_key: 'tableHook' }], + onSubmit: async () => {}, + allowInvalidSubmit: false, + customTheme: {}, + maxRecords: 10, + maxFileSize: 50, + autoMapHeaders: true, + autoMapDistance: 1, + initialStepState: { + type: StepType.upload, + }, + dateFormat: 'MM/DD/YY', + parseRaw: true, + rtl: false, + selectHeader: true, +}; + +describe('useSpreadsheetImport', () => { + it('should set isOpen to true, and update the options in the Recoil state', async () => { + const { result } = renderHook( + () => ({ + useSpreadsheetImport: useSpreadsheetImport(), + spreadsheetImportState: useRecoilState(spreadsheetImportState)[0], + }), + { + wrapper: Wrapper, + }, + ); + expect(result.current.spreadsheetImportState).toStrictEqual({ + isOpen: false, + options: null, + }); + act(() => { + result.current.useSpreadsheetImport.openSpreadsheetImport( + mockedSpreadsheetOptions, + ); + }); + expect(result.current.spreadsheetImportState).toStrictEqual({ + isOpen: true, + options: mockedSpreadsheetOptions, + }); + }); +}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImportInitialStep.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImportInitialStep.test.ts new file mode 100644 index 000000000..f47959b9d --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImportInitialStep.test.ts @@ -0,0 +1,47 @@ +import { useState } from 'react'; +import { act, renderHook } from '@testing-library/react'; + +import { useSpreadsheetImportInitialStep } from '@/spreadsheet-import/hooks/useSpreadsheetImportInitialStep'; +import { StepType } from '@/spreadsheet-import/steps/components/UploadFlow'; + +describe('useSpreadsheetImportInitialStep', () => { + it('should return correct number for each step type', async () => { + const { result } = renderHook(() => { + const [step, setStep] = useState(); + const { initialStep } = useSpreadsheetImportInitialStep(step); + return { initialStep, setStep }; + }); + + expect(result.current.initialStep).toBe(-1); + + act(() => { + result.current.setStep(StepType.upload); + }); + + expect(result.current.initialStep).toBe(0); + + act(() => { + result.current.setStep(StepType.selectSheet); + }); + + expect(result.current.initialStep).toBe(0); + + act(() => { + result.current.setStep(StepType.selectHeader); + }); + + expect(result.current.initialStep).toBe(0); + + act(() => { + result.current.setStep(StepType.matchColumns); + }); + + expect(result.current.initialStep).toBe(2); + + act(() => { + result.current.setStep(StepType.validateData); + }); + + expect(result.current.initialStep).toBe(3); + }); +}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImportInternal.test.tsx b/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImportInternal.test.tsx new file mode 100644 index 000000000..2e8e8f039 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImportInternal.test.tsx @@ -0,0 +1,19 @@ +import { renderHook } from '@testing-library/react'; + +import { Providers } from '@/spreadsheet-import/components/Providers'; +import { mockedSpreadsheetOptions } from '@/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test'; +import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; + +const Wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +describe('useSpreadsheetImportInternal', () => { + it('should return the value provided by provider component', async () => { + const { result } = renderHook(() => useSpreadsheetImportInternal(), { + wrapper: Wrapper, + }); + + expect(result.current).toBe(mockedSpreadsheetOptions); + }); +}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/hooks/useSpreadsheetImport.tsx b/packages/twenty-front/src/modules/spreadsheet-import/hooks/useSpreadsheetImport.ts similarity index 100% rename from packages/twenty-front/src/modules/spreadsheet-import/hooks/useSpreadsheetImport.tsx rename to packages/twenty-front/src/modules/spreadsheet-import/hooks/useSpreadsheetImport.ts diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/dataMutations.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/dataMutations.test.ts new file mode 100644 index 000000000..224adc168 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/dataMutations.test.ts @@ -0,0 +1,209 @@ +import { + Data, + Field, + Info, + RowHook, + TableHook, +} from '@/spreadsheet-import/types'; +import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations'; + +describe('addErrorsAndRunHooks', () => { + type FullData = Data<'name' | 'age' | 'country'>; + const requiredField: Field<'name'> = { + key: 'name', + label: 'Name', + validations: [{ rule: 'required' }], + icon: null, + fieldType: { type: 'input' }, + }; + + const regexField: Field<'age'> = { + key: 'age', + label: 'Age', + validations: [ + { rule: 'regex', value: '\\d+', errorMessage: 'Regex error' }, + ], + icon: null, + fieldType: { type: 'input' }, + }; + + const uniqueField: Field<'country'> = { + key: 'country', + label: 'Country', + validations: [{ rule: 'unique' }], + icon: null, + fieldType: { type: 'input' }, + }; + + const functionValidationFieldTrue: Field<'email'> = { + key: 'email', + label: 'Email', + validations: [ + { + rule: 'function', + isValid: () => true, + errorMessage: 'Field is invalid', + }, + ], + icon: null, + fieldType: { type: 'input' }, + }; + + const functionValidationFieldFalse: Field<'email'> = { + key: 'email', + label: 'Email', + validations: [ + { + rule: 'function', + isValid: () => false, + errorMessage: 'Field is invalid', + }, + ], + icon: null, + fieldType: { type: 'input' }, + }; + + const validData: Data<'name' | 'age'> = { name: 'John', age: '30' }; + const dataWithoutNameAndInvalidAge: Data<'name' | 'age'> = { + name: '', + age: 'Invalid', + }; + const dataWithDuplicatedValue: FullData = { + name: 'Alice', + age: '40', + country: 'Brazil', + }; + + const data: Data<'name' | 'age'>[] = [ + validData, + dataWithoutNameAndInvalidAge, + ]; + + const basicError: Info = { message: 'Field is invalid', level: 'error' }; + const nameError: Info = { message: 'Name Error', level: 'error' }; + const ageError: Info = { message: 'Age Error', level: 'error' }; + const regexError: Info = { message: 'Regex error', level: 'error' }; + const requiredError: Info = { message: 'Field is required', level: 'error' }; + const duplicatedError: Info = { + message: 'Field must be unique', + level: 'error', + }; + + const rowHook: RowHook<'name' | 'age'> = jest.fn((row, addError) => { + addError('name', nameError); + return row; + }); + const tableHook: TableHook<'name' | 'age'> = jest.fn((table, addError) => { + addError(0, 'age', ageError); + return table; + }); + + it('should correctly call rowHook and tableHook and add errors', () => { + const result = addErrorsAndRunHooks( + data, + [requiredField, regexField], + rowHook, + tableHook, + ); + + expect(rowHook).toHaveBeenCalled(); + expect(tableHook).toHaveBeenCalled(); + expect(result[0].__errors).toStrictEqual({ + name: nameError, + age: ageError, + }); + }); + + it('should overwrite hook errors with validation errors', () => { + const result = addErrorsAndRunHooks( + data, + [requiredField, regexField], + rowHook, + tableHook, + ); + + expect(rowHook).toHaveBeenCalled(); + expect(tableHook).toHaveBeenCalled(); + expect(result[1].__errors).toStrictEqual({ + name: requiredError, + age: regexError, + }); + }); + + it('should add errors for required field', () => { + const result = addErrorsAndRunHooks(data, [requiredField]); + + expect(result[1].__errors).toStrictEqual({ + name: requiredError, + }); + }); + + it('should add errors for regex field', () => { + const result = addErrorsAndRunHooks(data, [regexField]); + + expect(result[1].__errors).toStrictEqual({ + age: regexError, + }); + }); + + it('should add errors for unique field', () => { + const result = addErrorsAndRunHooks( + [ + dataWithDuplicatedValue, + dataWithDuplicatedValue, + ] as unknown as FullData[], + [uniqueField], + ); + + expect(result[0].__errors).toStrictEqual({ + country: duplicatedError, + }); + expect(result[1].__errors).toStrictEqual({ + country: duplicatedError, + }); + }); + + it('should add errors for unique field with empty values', () => { + const result = addErrorsAndRunHooks( + [{ country: '' }, { country: '' }], + [uniqueField], + ); + + expect(result[0].__errors).toStrictEqual({ + country: duplicatedError, + }); + expect(result[1].__errors).toStrictEqual({ + country: duplicatedError, + }); + }); + + it('should not add errors for unique field with empty values if allowEmpty is true', () => { + const result = addErrorsAndRunHooks( + [{ country: '' }, { country: '' }], + [{ ...uniqueField, validations: [{ rule: 'unique', allowEmpty: true }] }], + ); + + expect(result[0].__errors).toBeUndefined(); + expect(result[1].__errors).toBeUndefined(); + }); + + it('should add errors for function validation if result is false', () => { + const result = addErrorsAndRunHooks( + [{ email: 'email' }], + [functionValidationFieldFalse], + ); + + expect(result[0].__errors).toStrictEqual({ + email: basicError, + }); + }); + + it('should not add errors for function validation if result is true', () => { + const result = addErrorsAndRunHooks( + [{ email: 'email' }], + [functionValidationFieldTrue], + ); + + expect(result[0].__errors).toBeUndefined(); + }); +}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/exceedsMaxRecords.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/exceedsMaxRecords.test.ts new file mode 100644 index 000000000..7a2186011 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/exceedsMaxRecords.test.ts @@ -0,0 +1,45 @@ +import { WorkSheet } from 'xlsx-ugnis'; + +import { exceedsMaxRecords } from '@/spreadsheet-import/utils/exceedsMaxRecords'; + +describe('exceedsMaxRecords', () => { + const maxRecords = 5; + + it('should return true if the number of records exceeds the maximum limit', () => { + const workSheet: WorkSheet = { + '!ref': 'A1:A10', + }; + + const result = exceedsMaxRecords(workSheet, maxRecords); + + expect(result).toBe(true); + }); + + it('should return false if the number of records does not exceed the maximum limit', () => { + const workSheet: WorkSheet = { + '!ref': 'A1:A4', + }; + + const result = exceedsMaxRecords(workSheet, maxRecords); + + expect(result).toBe(false); + }); + + it('should return false if the number of records is equal to the maximum limit', () => { + const workSheet: WorkSheet = { + '!ref': 'A1:A5', + }; + + const result = exceedsMaxRecords(workSheet, maxRecords); + + expect(result).toBe(false); + }); + + it('should return false if the worksheet does not have a defined range', () => { + const workSheet: WorkSheet = {}; + + const result = exceedsMaxRecords(workSheet, maxRecords); + + expect(result).toBe(false); + }); +}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findMatch.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findMatch.test.ts new file mode 100644 index 000000000..7514f0c0a --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findMatch.test.ts @@ -0,0 +1,90 @@ +import { Field } from '@/spreadsheet-import/types'; +import { findMatch } from '@/spreadsheet-import/utils/findMatch'; + +describe('findMatch', () => { + const defaultField: Field<'defaultField'> = { + key: 'defaultField', + icon: null, + label: 'label', + fieldType: { + type: 'input', + }, + alternateMatches: ['Full Name', 'First Name'], + }; + + const secondaryField: Field<'secondaryField'> = { + key: 'secondaryField', + icon: null, + label: 'label', + fieldType: { + type: 'input', + }, + }; + + const fields = [defaultField, secondaryField]; + + it('should return the matching field if the header matches exactly with the key', () => { + const autoMapDistance = 0; + + const result = findMatch(defaultField.key, fields, autoMapDistance); + + expect(result).toBe(defaultField.key); + }); + + it('should return the matching field if the header matches exactly one of the alternate matches', () => { + const autoMapDistance = 0; + + const result = findMatch( + defaultField.alternateMatches?.[0] ?? '', + fields, + autoMapDistance, + ); + + expect(result).toBe(defaultField.key); + }); + + it('should return the matching field if the header matches partially one of the alternate matches', () => { + const header = 'First'; + const autoMapDistance = 5; + + const result = findMatch(header, fields, autoMapDistance); + + expect(result).toBe(defaultField.key); + }); + + it('should return the matching field if the header matches partially both of the alternate matches', () => { + const header = 'Name'; + const autoMapDistance = 5; + + const result = findMatch(header, fields, autoMapDistance); + + expect(result).toBe(defaultField.key); + }); + + it('should return undefined if no exact match or alternate match is found within the auto map distance', () => { + const header = 'Header'; + const autoMapDistance = 2; + + const result = findMatch(header, fields, autoMapDistance); + + expect(result).toBeUndefined(); + }); + + it('should return the matching field with the smallest Levenshtein distance if within auto map distance', () => { + const header = 'Name'.split('').reverse().join(''); + const autoMapDistance = 100; + + const result = findMatch(header, fields, autoMapDistance); + + expect(result).toBe(defaultField.key); + }); + + it('should return undefined if no match is found within auto map distance', () => { + const header = 'Name'.split('').reverse().join(''); + const autoMapDistance = 1; + + const result = findMatch(header, fields, autoMapDistance); + + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findUnmatchedRequiredFields.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findUnmatchedRequiredFields.test.ts new file mode 100644 index 000000000..e456f72f5 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findUnmatchedRequiredFields.test.ts @@ -0,0 +1,89 @@ +import { + Column, + ColumnType, +} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; +import { Field, Validation } from '@/spreadsheet-import/types'; +import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields'; + +const nameField: Field<'Name'> = { + key: 'Name', + label: 'Name', + icon: null, + fieldType: { + type: 'input', + }, +}; + +const ageField: Field<'Age'> = { + key: 'Age', + label: 'Age', + icon: null, + fieldType: { + type: 'input', + }, +}; +const validations: Validation[] = [{ rule: 'required' }]; +const nameFieldWithValidations: Field<'Name'> = { ...nameField, validations }; +const ageFieldWithValidations: Field<'Age'> = { ...ageField, validations }; + +type ColumnValues = 'Name' | 'Age'; + +const nameColumn: Column = { + type: ColumnType.matched, + index: 0, + header: '', + value: 'Name', +}; + +const ageColumn: Column = { + type: ColumnType.matched, + index: 0, + header: '', + value: 'Age', +}; + +const extraColumn: Column = { + type: ColumnType.matched, + index: 0, + header: '', + value: 'Age', +}; + +describe('findUnmatchedRequiredFields', () => { + it('should return an empty array if all required fields are matched', () => { + const fields = [nameFieldWithValidations, ageFieldWithValidations]; + const columns = [nameColumn, ageColumn]; + + const result = findUnmatchedRequiredFields(fields, columns); + + expect(result).toStrictEqual([]); + }); + + it('should return an array of labels for required fields that are not matched', () => { + const fields = [nameFieldWithValidations, ageFieldWithValidations]; + const columns = [nameColumn]; + + const result = findUnmatchedRequiredFields(fields, columns); + + expect(result).toStrictEqual(['Age']); + }); + + it('should return an empty array if there are no required fields', () => { + const fields = [nameField, ageField]; + const columns = [nameColumn]; + + const result = findUnmatchedRequiredFields(fields, columns); + + expect(result).toStrictEqual([]); + }); + + it('should return an empty array if all required fields are matched even if there are extra columns', () => { + const fields = [nameFieldWithValidations, ageFieldWithValidations]; + + const columns = [nameColumn, ageColumn, extraColumn]; + + const result = findUnmatchedRequiredFields(fields, columns); + + expect(result).toStrictEqual([]); + }); +}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/generateExampleRow.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/generateExampleRow.test.ts new file mode 100644 index 000000000..fc42f39c5 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/generateExampleRow.test.ts @@ -0,0 +1,60 @@ +import { Field } from '@/spreadsheet-import/types'; +import { generateExampleRow } from '@/spreadsheet-import/utils/generateExampleRow'; + +describe('generateExampleRow', () => { + const defaultField: Field<'defaultField'> = { + key: 'defaultField', + icon: null, + label: 'label', + fieldType: { + type: 'input', + }, + }; + + it('should generate an example row from input field type', () => { + const fields: Field<'defaultField'>[] = [defaultField]; + + const result = generateExampleRow(fields); + + expect(result).toStrictEqual([{ defaultField: 'Text' }]); + }); + + it('should generate an example row from checkbox field type', () => { + const fields: Field<'defaultField'>[] = [ + { + ...defaultField, + fieldType: { type: 'checkbox' }, + }, + ]; + + const result = generateExampleRow(fields); + + expect(result).toStrictEqual([{ defaultField: 'Boolean' }]); + }); + + it('should generate an example row from select field type', () => { + const fields: Field<'defaultField'>[] = [ + { + ...defaultField, + fieldType: { type: 'select', options: [] }, + }, + ]; + + const result = generateExampleRow(fields); + + expect(result).toStrictEqual([{ defaultField: 'Options' }]); + }); + + it('should generate an example row with provided example values for fields', () => { + const fields: Field<'defaultField'>[] = [ + { + ...defaultField, + example: 'Example', + }, + ]; + + const result = generateExampleRow(fields); + + expect(result).toStrictEqual([{ defaultField: 'Example' }]); + }); +}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getFieldOptions.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getFieldOptions.test.ts new file mode 100644 index 000000000..723c7497d --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getFieldOptions.test.ts @@ -0,0 +1,62 @@ +import { Field } from '@/spreadsheet-import/types'; +import { getFieldOptions } from '@/spreadsheet-import/utils/getFieldOptions'; + +describe('getFieldOptions', () => { + const optionsArray = [ + { + label: 'one', + value: 'One', + }, + { + label: 'two', + value: 'Two', + }, + { + label: 'three', + value: 'Three', + }, + ]; + const fields: Field<'Options' | 'Name'>[] = [ + { + key: 'Options', + icon: null, + label: 'options', + fieldType: { + type: 'select', + options: optionsArray, + }, + }, + { + key: 'Name', + icon: null, + label: 'name', + fieldType: { + type: 'input', + }, + }, + ]; + + it('should return an empty array if the field does not exist in the fields list', () => { + const fieldKey = 'NonExistingField'; + + const result = getFieldOptions(fields, fieldKey); + + expect(result).toEqual([]); + }); + + it('should return an empty array if the field is not of type select', () => { + const fieldKey = 'Name'; + + const result = getFieldOptions(fields, fieldKey); + + expect(result).toEqual([]); + }); + + it('should return an array of options if the field is of type select', () => { + const fieldKey = 'Options'; + + const result = getFieldOptions(fields, fieldKey); + + expect(result).toEqual(optionsArray); + }); +}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getMatchedColumns.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getMatchedColumns.test.ts new file mode 100644 index 000000000..538b39667 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getMatchedColumns.test.ts @@ -0,0 +1,132 @@ +import { + Column, + ColumnType, +} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; +import { Field } from '@/spreadsheet-import/types'; +import { getMatchedColumns } from '@/spreadsheet-import/utils/getMatchedColumns'; + +describe('getMatchedColumns', () => { + const columns: Column[] = [ + { index: 0, header: 'Name', type: ColumnType.matched, value: 'Name' }, + { + index: 1, + header: 'Location', + type: ColumnType.matched, + value: 'Location', + }, + { + index: 2, + header: 'Age', + type: ColumnType.matched, + value: 'Age', + }, + ]; + + const fields: Field[] = [ + { + key: 'Name', + label: 'Name', + fieldType: { type: 'input' }, + icon: null, + }, + { + key: 'Location', + label: 'Location', + fieldType: { type: 'select', options: [] }, + icon: null, + }, + { key: 'Age', label: 'Age', fieldType: { type: 'input' }, icon: null }, + ]; + + const data = [ + ['John', 'New York'], + ['Alice', 'Los Angeles'], + ]; + + const autoMapDistance = 2; + + it('should return matched columns for each field', () => { + const result = getMatchedColumns(columns, fields, data, autoMapDistance); + expect(result).toEqual([ + { index: 0, header: 'Name', type: ColumnType.matched, value: 'Name' }, + { + index: 1, + header: 'Location', + type: ColumnType.matchedSelect, + value: 'Location', + matchedOptions: [ + { + entry: 'New York', + }, + { + entry: 'Los Angeles', + }, + ], + }, + { index: 2, header: 'Age', type: ColumnType.matched, value: 'Age' }, + ]); + }); + + it('should handle columns with duplicate values by choosing the closest match', () => { + const columnsWithDuplicates: Column[] = [ + { index: 0, header: 'Name', type: ColumnType.matched, value: 'Name' }, + { index: 1, header: 'Name', type: ColumnType.matched, value: 'Name' }, + { + index: 2, + header: 'Location', + type: ColumnType.matched, + value: 'Location', + }, + ]; + + const result = getMatchedColumns( + columnsWithDuplicates, + fields, + data, + autoMapDistance, + ); + + expect(result[0]).toEqual({ + index: 0, + header: 'Name', + type: ColumnType.empty, + }); + expect(result[1]).toEqual({ + index: 1, + header: 'Name', + type: ColumnType.matched, + value: 'Name', + }); + }); + + it('should return initial columns when no auto match is found', () => { + const unmatchedColumnsData: string[][] = [ + ['John', 'New York', '30'], + ['Alice', 'Los Angeles', '25'], + ]; + + const unmatchedFields: Field[] = [ + { + key: 'Hobby', + label: 'Hobby', + fieldType: { type: 'input' }, + icon: null, + }, + { + key: 'Interest', + label: 'Interest', + fieldType: { type: 'input' }, + icon: null, + }, + ]; + + const result = getMatchedColumns( + columns, + unmatchedFields, + unmatchedColumnsData, + autoMapDistance, + ); + + expect(result).toEqual(columns); + }); +}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/mapWorkbook.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/mapWorkbook.test.ts new file mode 100644 index 000000000..33d84f768 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/mapWorkbook.test.ts @@ -0,0 +1,46 @@ +import * as XLSX from 'xlsx-ugnis'; + +import { mapWorkbook } from '@/spreadsheet-import/utils/mapWorkbook'; + +describe('mapWorkbook', () => { + it('should map the workbook to a 2D array of strings', () => { + const inputWorkbook = XLSX.utils.book_new(); + const inputSheetData = [ + ['Name', 'Age'], + ['John', '30'], + ['Alice', '25'], + ]; + const expectedOutput = inputSheetData; + + const worksheet = XLSX.utils.aoa_to_sheet(inputSheetData); + XLSX.utils.book_append_sheet(inputWorkbook, worksheet, 'Sheet1'); + + const result = mapWorkbook(inputWorkbook); + + expect(result).toEqual(expectedOutput); + }); + + it('should map the specified sheet of the workbook to a 2D array of strings', () => { + const inputWorkbook = XLSX.utils.book_new(); + const inputSheet1Data = [ + ['Name', 'Age'], + ['John', '30'], + ['Alice', '25'], + ]; + const inputSheet2Data = [ + ['City', 'Population'], + ['New York', '8500000'], + ['Los Angeles', '4000000'], + ]; + const expectedOutput = inputSheet2Data; + + const worksheet1 = XLSX.utils.aoa_to_sheet(inputSheet1Data); + const worksheet2 = XLSX.utils.aoa_to_sheet(inputSheet2Data); + XLSX.utils.book_append_sheet(inputWorkbook, worksheet1, 'Sheet1'); + XLSX.utils.book_append_sheet(inputWorkbook, worksheet2, 'Sheet2'); + + const result = mapWorkbook(inputWorkbook, 'Sheet2'); + + expect(result).toEqual(expectedOutput); + }); +}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/normalizeCheckboxValue.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/normalizeCheckboxValue.test.ts new file mode 100644 index 000000000..dad764d37 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/normalizeCheckboxValue.test.ts @@ -0,0 +1,23 @@ +import { normalizeCheckboxValue } from '@/spreadsheet-import/utils/normalizeCheckboxValue'; + +describe('normalizeCheckboxValue', () => { + const testCases = [ + { value: 'yes', expected: true }, + { value: 'Yes', expected: true }, + { value: 'no', expected: false }, + { value: 'No', expected: false }, + { value: 'true', expected: true }, + { value: 'True', expected: true }, + { value: 'false', expected: false }, + { value: 'False', expected: false }, + { value: undefined, expected: false }, + { value: 'invalid', expected: false }, + ]; + + testCases.forEach(({ value, expected }) => { + it(`should return ${expected} for value "${value}"`, () => { + const result = normalizeCheckboxValue(value); + expect(result).toBe(expected); + }); + }); +}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/normalizeTableData.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/normalizeTableData.test.ts new file mode 100644 index 000000000..9bb940267 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/normalizeTableData.test.ts @@ -0,0 +1,147 @@ +import { + Column, + ColumnType, +} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; +import { Field } from '@/spreadsheet-import/types'; +import { normalizeTableData } from '@/spreadsheet-import/utils/normalizeTableData'; + +describe('normalizeTableData', () => { + const columns: Column[] = [ + { index: 0, header: 'Name', type: ColumnType.matched, value: 'name' }, + { index: 1, header: 'Age', type: ColumnType.matched, value: 'age' }, + { + index: 2, + header: 'Active', + type: ColumnType.matchedCheckbox, + value: 'active', + }, + ]; + + const fields: Field[] = [ + { key: 'name', label: 'Name', fieldType: { type: 'input' }, icon: null }, + { key: 'age', label: 'Age', fieldType: { type: 'input' }, icon: null }, + { + key: 'active', + label: 'Active', + fieldType: { + type: 'checkbox', + }, + icon: null, + }, + ]; + + const rawData = [ + ['John', '30', 'Yes'], + ['Alice', '', 'No'], + ['Bob', '25', 'Maybe'], + ]; + + it('should normalize table data according to columns and fields', () => { + const result = normalizeTableData(columns, rawData, fields); + + expect(result).toStrictEqual([ + { name: 'John', age: '30', active: true }, + { name: 'Alice', age: undefined, active: false }, + { name: 'Bob', age: '25', active: false }, + ]); + }); + + it('should normalize matchedCheckbox values and handle booleanMatches', () => { + const columns: Column[] = [ + { + index: 0, + header: 'Active', + type: ColumnType.matchedCheckbox, + value: 'active', + }, + ]; + + const fields: Field[] = [ + { + key: 'active', + label: 'Active', + fieldType: { + type: 'checkbox', + booleanMatches: { yes: true, no: false }, + }, + icon: null, + }, + ]; + + const rawData = [['Yes'], ['No'], ['OtherValue']]; + + const result = normalizeTableData(columns, rawData, fields); + + expect(result).toStrictEqual([{ active: true }, { active: false }, {}]); + }); + + it('should map matchedSelect and matchedSelectOptions values correctly', () => { + const columns: Column[] = [ + { + index: 0, + header: 'Number', + type: ColumnType.matchedSelect, + value: 'number', + matchedOptions: [ + { entry: 'One', value: '1' }, + { entry: 'Two', value: '2' }, + ], + }, + ]; + + const fields: Field[] = [ + { + key: 'number', + label: 'Number', + fieldType: { + type: 'select', + options: [ + { label: 'One', value: '1' }, + { label: 'Two', value: '2' }, + ], + }, + icon: null, + }, + ]; + + const rawData = [['One'], ['Two'], ['OtherValue']]; + + const result = normalizeTableData(columns, rawData, fields); + + expect(result).toStrictEqual([ + { number: '1' }, + { number: '2' }, + { number: undefined }, + ]); + }); + + it('should handle empty and ignored columns', () => { + const columns: Column[] = [ + { index: 0, header: 'Empty', type: ColumnType.empty }, + { index: 1, header: 'Ignored', type: ColumnType.ignored }, + ]; + + const rawData = [['Value1', 'Value2']]; + + const result = normalizeTableData(columns, rawData, []); + + expect(result).toStrictEqual([{}]); + }); + + it('should handle unrecognized column types and return empty object', () => { + const columns: Column[] = [ + { + index: 0, + header: 'Unrecognized', + type: 'Unknown' as unknown as ColumnType.matched, + value: '', + }, + ]; + + const rawData = [['Value']]; + + const result = normalizeTableData(columns, rawData, []); + + expect(result).toStrictEqual([{}]); + }); +}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/readFilesAsync.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/readFilesAsync.test.ts new file mode 100644 index 000000000..4c846dd0d --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/readFilesAsync.test.ts @@ -0,0 +1,9 @@ +import { readFileAsync } from '@/spreadsheet-import/utils/readFilesAsync'; + +describe('readFileAsync', () => { + it('should resolve with the file content as ArrayBuffer', async () => { + const file = new File(['Test content'], 'test.txt', { type: 'text/plain' }); + const result = await readFileAsync(file); + expect(result).toBeInstanceOf(ArrayBuffer); + }); +}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setColumn.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setColumn.test.ts new file mode 100644 index 000000000..012dfbabf --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setColumn.test.ts @@ -0,0 +1,94 @@ +import { + Column, + ColumnType, +} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; +import { Field } from '@/spreadsheet-import/types'; +import { setColumn } from '@/spreadsheet-import/utils/setColumn'; + +describe('setColumn', () => { + const defaultField: Field<'Name'> = { + icon: null, + label: 'label', + key: 'Name', + fieldType: { type: 'input' }, + }; + + const oldColumn: Column<'oldValue'> = { + index: 0, + header: 'Name', + type: ColumnType.matched, + value: 'oldValue', + }; + + it('should return a matchedSelect column if field type is "select"', () => { + const field = { + ...defaultField, + fieldType: { type: 'select' }, + } as Field<'Name'>; + + const data = [['John'], ['Alice']]; + const result = setColumn(oldColumn, field, data); + + expect(result).toEqual({ + index: 0, + header: 'Name', + type: ColumnType.matchedSelect, + value: 'Name', + matchedOptions: [ + { + entry: 'John', + }, + { + entry: 'Alice', + }, + ], + }); + }); + + it('should return a matchedCheckbox column if field type is "checkbox"', () => { + const field = { + ...defaultField, + fieldType: { type: 'checkbox' }, + } as Field<'Name'>; + + const result = setColumn(oldColumn, field); + + expect(result).toEqual({ + index: 0, + header: 'Name', + type: ColumnType.matchedCheckbox, + value: 'Name', + }); + }); + + it('should return a matched column if field type is "input"', () => { + const field = { + ...defaultField, + fieldType: { type: 'input' }, + } as Field<'Name'>; + + const result = setColumn(oldColumn, field); + + expect(result).toEqual({ + index: 0, + header: 'Name', + type: ColumnType.matched, + value: 'Name', + }); + }); + + it('should return an empty column if field type is not recognized', () => { + const field = { + ...defaultField, + fieldType: { type: 'unknown' }, + } as unknown as Field<'Name'>; + + const result = setColumn(oldColumn, field); + + expect(result).toEqual({ + index: 0, + header: 'Name', + type: ColumnType.empty, + }); + }); +}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setIgnoreColumn.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setIgnoreColumn.test.ts new file mode 100644 index 000000000..ea6ed4a1d --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setIgnoreColumn.test.ts @@ -0,0 +1,22 @@ +import { + Column, + ColumnType, +} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; +import { setIgnoreColumn } from '@/spreadsheet-import/utils/setIgnoreColumn'; + +describe('setIgnoreColumn', () => { + it('should return a column with type "ignored"', () => { + const column: Column<'John'> = { + index: 0, + header: 'Name', + type: ColumnType.matched, + value: 'John', + }; + const result = setIgnoreColumn(column); + expect(result).toEqual({ + index: 0, + header: 'Name', + type: ColumnType.ignored, + }); + }); +}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setSubColumn.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setSubColumn.test.ts new file mode 100644 index 000000000..785c89d34 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setSubColumn.test.ts @@ -0,0 +1,63 @@ +import { + Column, + ColumnType, +} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; +import { setSubColumn } from '@/spreadsheet-import/utils/setSubColumn'; + +describe('setSubColumn', () => { + it('should return a matchedSelectColumn with updated matchedOptions', () => { + const oldColumn: Column<'John' | ''> = { + index: 0, + header: 'Name', + type: ColumnType.matchedSelect, + matchedOptions: [ + { entry: 'Name1', value: 'John' }, + { entry: 'Name2', value: '' }, + ], + value: 'John', + }; + + const entry = 'Name1'; + const value = 'John Doe'; + const result = setSubColumn(oldColumn, entry, value); + + expect(result).toEqual({ + index: 0, + header: 'Name', + type: ColumnType.matchedSelect, + matchedOptions: [ + { entry: 'Name1', value: 'John Doe' }, + { entry: 'Name2', value: '' }, + ], + value: 'John', + }); + }); + + it('should return a matchedSelectOptionsColumn with updated matchedOptions', () => { + const oldColumn: Column<'John' | 'Jane'> = { + index: 0, + header: 'Name', + type: ColumnType.matchedSelectOptions, + matchedOptions: [ + { entry: 'Name1', value: 'John' }, + { entry: 'Name2', value: 'Jane' }, + ], + value: 'John', + }; + + const entry = 'Name1'; + const value = 'John Doe'; + const result = setSubColumn(oldColumn, entry, value); + + expect(result).toEqual({ + index: 0, + header: 'Name', + type: ColumnType.matchedSelectOptions, + matchedOptions: [ + { entry: 'Name1', value: 'John Doe' }, + { entry: 'Name2', value: 'Jane' }, + ], + value: 'John', + }); + }); +}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/uniqueEntries.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/uniqueEntries.test.ts new file mode 100644 index 000000000..6329a2684 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/uniqueEntries.test.ts @@ -0,0 +1,12 @@ +import { uniqueEntries } from '@/spreadsheet-import/utils/uniqueEntries'; + +describe('uniqueEntries', () => { + it('should return unique entries from the specified column index', () => { + const data = [['John'], ['Alice'], ['John']]; + const columnIndex = 0; + + const result = uniqueEntries(data, columnIndex); + + expect(result).toStrictEqual([{ entry: 'John' }, { entry: 'Alice' }]); + }); +});