mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-26 21:53:48 +03:00
TWNTY-3316 - Add tests for modules/spreadsheet-import
(#4219)
Add tests for `modules/spreadsheet-import` Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com> Co-authored-by: RubensRafael <rubensrafael2@live.com>
This commit is contained in:
parent
bc11cf80fa
commit
68a8502920
@ -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 }) => (
|
||||
<RecoilRoot>{children}</RecoilRoot>
|
||||
);
|
||||
type SpreadsheetKey = 'spreadsheet_key';
|
||||
|
||||
export const mockedSpreadsheetOptions: SpreadsheetOptions<SpreadsheetKey> = {
|
||||
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<SpreadsheetKey>(),
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
@ -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<StepType | undefined>();
|
||||
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);
|
||||
});
|
||||
});
|
@ -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 }) => (
|
||||
<Providers values={mockedSpreadsheetOptions}>{children}</Providers>
|
||||
);
|
||||
|
||||
describe('useSpreadsheetImportInternal', () => {
|
||||
it('should return the value provided by provider component', async () => {
|
||||
const { result } = renderHook(() => useSpreadsheetImportInternal(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
expect(result.current).toBe(mockedSpreadsheetOptions);
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
@ -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<ColumnValues> = {
|
||||
type: ColumnType.matched,
|
||||
index: 0,
|
||||
header: '',
|
||||
value: 'Name',
|
||||
};
|
||||
|
||||
const ageColumn: Column<ColumnValues> = {
|
||||
type: ColumnType.matched,
|
||||
index: 0,
|
||||
header: '',
|
||||
value: 'Age',
|
||||
};
|
||||
|
||||
const extraColumn: Column<ColumnValues> = {
|
||||
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([]);
|
||||
});
|
||||
});
|
@ -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' }]);
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
@ -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<string>[] = [
|
||||
{ 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<string>[] = [
|
||||
{
|
||||
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<string>[] = [
|
||||
{ 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<string>[] = [
|
||||
{
|
||||
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);
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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<string>[] = [
|
||||
{ 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<string>[] = [
|
||||
{ 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<string>[] = [
|
||||
{
|
||||
index: 0,
|
||||
header: 'Active',
|
||||
type: ColumnType.matchedCheckbox,
|
||||
value: 'active',
|
||||
},
|
||||
];
|
||||
|
||||
const fields: Field<string>[] = [
|
||||
{
|
||||
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<string>[] = [
|
||||
{
|
||||
index: 0,
|
||||
header: 'Number',
|
||||
type: ColumnType.matchedSelect,
|
||||
value: 'number',
|
||||
matchedOptions: [
|
||||
{ entry: 'One', value: '1' },
|
||||
{ entry: 'Two', value: '2' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const fields: Field<string>[] = [
|
||||
{
|
||||
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<string>[] = [
|
||||
{ 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<string>[] = [
|
||||
{
|
||||
index: 0,
|
||||
header: 'Unrecognized',
|
||||
type: 'Unknown' as unknown as ColumnType.matched,
|
||||
value: '',
|
||||
},
|
||||
];
|
||||
|
||||
const rawData = [['Value']];
|
||||
|
||||
const result = normalizeTableData(columns, rawData, []);
|
||||
|
||||
expect(result).toStrictEqual([{}]);
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
@ -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' }]);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user