TWNTY-3549 - Add tests for modules/object-record/field (#3572)

* Add tests for `modules/object-record/field`

Co-authored-by: v1b3m <vibenjamin6@gmail.com>

* Merge main

Co-authored-by: v1b3m <vibenjamin6@gmail.com>

* Move field definitions to separate file

Co-authored-by: v1b3m <vibenjamin6@gmail.com>

---------

Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com>
Co-authored-by: v1b3m <vibenjamin6@gmail.com>
This commit is contained in:
gitstart-app[bot]
2024-01-23 14:17:27 +01:00
committed by GitHub
parent e0943b15c4
commit 2b6d66f1bc
10 changed files with 800 additions and 0 deletions

View File

@ -0,0 +1,100 @@
import { FieldDefinition } from '@/object-record/field/types/FieldDefinition';
import {
FieldBooleanMetadata,
FieldFullNameMetadata,
FieldLinkMetadata,
FieldPhoneMetadata,
FieldRatingMetadata,
FieldRelationMetadata,
FieldSelectMetadata,
FieldTextMetadata,
} from '@/object-record/field/types/FieldMetadata';
export const fieldMetadataId = 'fieldMetadataId';
export const textfieldDefinition: FieldDefinition<FieldTextMetadata> = {
fieldMetadataId,
label: 'User Name',
iconName: 'User',
type: 'TEXT',
metadata: { placeHolder: 'John Doe', fieldName: 'userName' },
};
export const booleanFieldDefinition: FieldDefinition<FieldBooleanMetadata> = {
fieldMetadataId,
label: 'Is Active?',
iconName: 'iconName',
type: 'BOOLEAN',
metadata: {
objectMetadataNameSingular: 'person',
fieldName: 'isActive',
},
};
export const relationFieldDefinition: FieldDefinition<FieldRelationMetadata> = {
fieldMetadataId,
label: 'Contact',
iconName: 'Phone',
type: 'RELATION',
metadata: {
fieldName: 'contact',
relationFieldMetadataId: 'relationFieldMetadataId',
relationObjectMetadataNamePlural: 'users',
relationObjectMetadataNameSingular: 'user',
},
};
export const selectFieldDefinition: FieldDefinition<FieldSelectMetadata> = {
fieldMetadataId,
label: 'Account Owner',
iconName: 'iconName',
type: 'SELECT',
metadata: {
fieldName: 'accountOwner',
options: [{ label: 'Elon Musk', color: 'blue', value: 'userId' }],
},
};
export const fullNameFieldDefinition: FieldDefinition<FieldFullNameMetadata> = {
fieldMetadataId,
label: 'Display Name',
iconName: 'profile',
type: 'FULL_NAME',
metadata: {
fieldName: 'displayName',
placeHolder: 'Mr Miagi',
},
};
export const linkFieldDefinition: FieldDefinition<FieldLinkMetadata> = {
fieldMetadataId,
label: 'LinkedIn URL',
iconName: 'url',
type: 'LINK',
metadata: {
fieldName: 'linkedInURL',
placeHolder: 'https://linkedin.com/user',
},
};
export const phoneFieldDefinition: FieldDefinition<FieldPhoneMetadata> = {
fieldMetadataId,
label: 'Contact',
iconName: 'Phone',
type: 'TEXT',
metadata: {
objectMetadataNameSingular: 'person',
placeHolder: '(+256)-712-345-6789',
fieldName: 'phone',
},
};
export const ratingfieldDefinition: FieldDefinition<FieldRatingMetadata> = {
fieldMetadataId,
label: 'Rating',
iconName: 'iconName',
type: 'RATING',
metadata: {
fieldName: 'rating',
},
};

View File

@ -0,0 +1,62 @@
import { ReactNode } from 'react';
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import {
fieldMetadataId,
textfieldDefinition,
} from '@/object-record/field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/field/contexts/FieldContext';
import { entityFieldInitialValueFamilyState } from '@/object-record/field/states/entityFieldInitialValueFamilyState';
import { useFieldInitialValue } from '../useFieldInitialValue';
const entityId = 'entityId';
const wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>
<FieldContext.Provider
value={{
fieldDefinition: textfieldDefinition,
entityId,
hotkeyScope: 'hotkeyScope',
isLabelIdentifier: false,
}}
>
{children}
</FieldContext.Provider>
</RecoilRoot>
);
describe('useFieldInitialValue', () => {
it('should work as expected', () => {
const { result } = renderHook(
() => {
const setFieldInitialValue = useSetRecoilState(
entityFieldInitialValueFamilyState({
fieldMetadataId,
entityId,
}),
);
return {
setFieldInitialValue,
fieldInitialValue: useFieldInitialValue(),
};
},
{
wrapper,
},
);
expect(result.current.fieldInitialValue).toBeUndefined();
const initialValue = { isEmpty: false, value: 'Sheldon Cooper' };
act(() => {
result.current.setFieldInitialValue(initialValue);
});
expect(result.current.fieldInitialValue).toEqual(initialValue);
});
});

View File

@ -0,0 +1,54 @@
import { ReactNode } from 'react';
import { renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import {
phoneFieldDefinition,
relationFieldDefinition,
} from '@/object-record/field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/field/contexts/FieldContext';
import { useGetButtonIcon } from '@/object-record/field/hooks/useGetButtonIcon';
import { FieldDefinition } from '@/object-record/field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/field/types/FieldMetadata';
import { IconPencil } from '@/ui/display/icon';
const entityId = 'entityId';
const getWrapper =
(fieldDefinition: FieldDefinition<FieldMetadata>) =>
({ children }: { children: ReactNode }) => (
<FieldContext.Provider
value={{
fieldDefinition,
entityId,
hotkeyScope: 'hotkeyScope',
isLabelIdentifier: false,
}}
>
<RecoilRoot>{children}</RecoilRoot>
</FieldContext.Provider>
);
const PhoneWrapper = getWrapper(phoneFieldDefinition);
const RelationWrapper = getWrapper(relationFieldDefinition);
describe('useGetButtonIcon', () => {
it('should return undefined', () => {
const { result } = renderHook(() => useGetButtonIcon());
expect(result.current).toBeUndefined();
});
it('should return icon pencil', () => {
const { result } = renderHook(() => useGetButtonIcon(), {
wrapper: PhoneWrapper,
});
expect(result.current).toEqual(IconPencil);
});
it('should return iconPencil for relation field', () => {
const { result } = renderHook(() => useGetButtonIcon(), {
wrapper: RelationWrapper,
});
expect(result.current).toEqual(IconPencil);
});
});

View File

@ -0,0 +1,52 @@
import { ReactNode } from 'react';
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { phoneFieldDefinition } from '@/object-record/field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/field/contexts/FieldContext';
import { useIsFieldEditModeValueEmpty } from '@/object-record/field/hooks/useIsFieldEditModeValueEmpty';
import { entityFieldsEditModeValueFamilyState } from '@/object-record/field/states/entityFieldsEditModeValueFamilyState';
const entityId = 'entityId';
const Wrapper = ({ children }: { children: ReactNode }) => (
<FieldContext.Provider
value={{
fieldDefinition: phoneFieldDefinition,
entityId,
hotkeyScope: 'hotkeyScope',
isLabelIdentifier: false,
}}
>
<RecoilRoot>{children}</RecoilRoot>
</FieldContext.Provider>
);
describe('useIsFieldEditModeValueEmpty', () => {
it('should work as expected', () => {
const { result } = renderHook(
() => {
const setFieldEditModeValue = useSetRecoilState(
entityFieldsEditModeValueFamilyState(entityId),
);
return {
setFieldEditModeValue,
isFieldEditModeValueEmpty: useIsFieldEditModeValueEmpty(),
};
},
{
wrapper: Wrapper,
},
);
expect(result.current.isFieldEditModeValueEmpty).toBe(true);
act(() => {
result.current.setFieldEditModeValue({
phone: '+1 233223',
});
});
expect(result.current.isFieldEditModeValueEmpty).toBe(false);
});
});

View File

@ -0,0 +1,53 @@
import { ReactNode } from 'react';
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { phoneFieldDefinition } from '@/object-record/field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/field/contexts/FieldContext';
import { useIsFieldEmpty } from '@/object-record/field/hooks/useIsFieldEmpty';
import { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState';
const entityId = 'entityId';
const Wrapper = ({ children }: { children: ReactNode }) => (
<FieldContext.Provider
value={{
fieldDefinition: phoneFieldDefinition,
entityId,
hotkeyScope: 'hotkeyScope',
isLabelIdentifier: false,
}}
>
<RecoilRoot>{children}</RecoilRoot>
</FieldContext.Provider>
);
describe('useIsFieldEmpty', () => {
it('should work as expected', () => {
const { result } = renderHook(
() => {
const setFieldState = useSetRecoilState(
entityFieldsFamilyState(entityId),
);
return {
setFieldState,
isFieldEditModeValueEmpty: useIsFieldEmpty(),
};
},
{
wrapper: Wrapper,
},
);
expect(result.current.isFieldEditModeValueEmpty).toBe(true);
act(() => {
result.current.setFieldState({
id: 'id',
phone: '+1 233223',
});
});
expect(result.current.isFieldEditModeValueEmpty).toBe(false);
});
});

View File

@ -0,0 +1,50 @@
import { ReactNode } from 'react';
import { renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import {
phoneFieldDefinition,
ratingfieldDefinition,
} from '@/object-record/field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/field/contexts/FieldContext';
import { useIsFieldInputOnly } from '@/object-record/field/hooks/useIsFieldInputOnly';
import { FieldDefinition } from '@/object-record/field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/field/types/FieldMetadata';
const entityId = 'entityId';
const getWrapper =
(fieldDefinition: FieldDefinition<FieldMetadata>) =>
({ children }: { children: ReactNode }) => (
<FieldContext.Provider
value={{
fieldDefinition,
entityId,
hotkeyScope: 'hotkeyScope',
isLabelIdentifier: false,
}}
>
<RecoilRoot>{children}</RecoilRoot>
</FieldContext.Provider>
);
const RatingWrapper = getWrapper(ratingfieldDefinition);
const PhoneWrapper = getWrapper(phoneFieldDefinition);
describe('useIsFieldInputOnly', () => {
it('should return true', () => {
const { result } = renderHook(() => useIsFieldInputOnly(), {
wrapper: RatingWrapper,
});
expect(result.current).toBe(true);
});
it('should return false', () => {
const { result } = renderHook(() => useIsFieldInputOnly(), {
wrapper: PhoneWrapper,
});
expect(result.current).toBe(false);
});
});

View File

@ -0,0 +1,159 @@
import { ReactNode } from 'react';
import { gql } from '@apollo/client';
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import { act, renderHook, waitFor } from '@testing-library/react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import {
phoneFieldDefinition,
relationFieldDefinition,
} from '@/object-record/field/__mocks__/fieldDefinitions';
import {
FieldContext,
RecordUpdateHook,
RecordUpdateHookParams,
} from '@/object-record/field/contexts/FieldContext';
import { usePersistField } from '@/object-record/field/hooks/usePersistField';
import { entityFieldsFamilySelector } from '@/object-record/field/states/selectors/entityFieldsFamilySelector';
import { FieldDefinition } from '@/object-record/field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/field/types/FieldMetadata';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
jest.mock('@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery', () => ({
useMapFieldMetadataToGraphQLQuery: () => () => '\n',
}));
const query = gql`
mutation UpdateOneWorkspaceMember(
$idToUpdate: ID!
$input: WorkspaceMemberUpdateInput!
) {
updateWorkspaceMember(id: $idToUpdate, data: $input) {
id
}
}
`;
const mocks: MockedResponse[] = [
{
request: {
query,
variables: { idToUpdate: 'entityId', input: { phone: '+1 123 456' } },
},
result: jest.fn(() => ({
data: {
updateWorkspaceMember: {
id: 'entityId',
},
},
})),
},
{
request: {
query,
variables: {
idToUpdate: 'entityId',
input: { contactId: null, contact: { foo: 'bar' } },
},
},
result: jest.fn(() => ({
data: {
updateWorkspaceMember: {
id: 'entityId',
},
},
})),
},
];
const entityId = 'entityId';
const fieldName = 'phone';
const getWrapper =
(fieldDefinition: FieldDefinition<FieldMetadata>) =>
({ children }: { children: ReactNode }) => {
const useUpdateOneRecordMutation: RecordUpdateHook = () => {
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
});
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
updateOneRecord?.({
idToUpdate: variables.where.id as string,
updateOneRecordInput: variables.updateOneRecordInput,
});
};
return [updateEntity, { loading: false }];
};
return (
<MockedProvider mocks={mocks} addTypename={false}>
<FieldContext.Provider
value={{
fieldDefinition,
entityId,
hotkeyScope: 'hotkeyScope',
isLabelIdentifier: false,
useUpdateRecord: useUpdateOneRecordMutation,
}}
>
<RecoilRoot>{children}</RecoilRoot>
</FieldContext.Provider>
</MockedProvider>
);
};
const PhoneWrapper = getWrapper(phoneFieldDefinition);
const RelationWrapper = getWrapper(relationFieldDefinition);
describe('usePersistField', () => {
it('should work as expected', async () => {
const { result } = renderHook(
() => {
const entityFields = useRecoilValue(
entityFieldsFamilySelector({ entityId, fieldName }),
);
return {
persistField: usePersistField(),
entityFields,
};
},
{ wrapper: PhoneWrapper },
);
act(() => {
result.current.persistField('+1 123 456');
});
await waitFor(() => {
expect(mocks[0].result).toHaveBeenCalled();
});
});
it('should persist relation field', async () => {
const { result } = renderHook(
() => {
const entityFields = useRecoilValue(
entityFieldsFamilySelector({ entityId, fieldName }),
);
return {
persistField: usePersistField(),
entityFields,
};
},
{ wrapper: RelationWrapper },
);
act(() => {
result.current.persistField({ foo: 'bar' });
});
await waitFor(() => {
expect(mocks[1].result).toHaveBeenCalled();
});
});
});

View File

@ -0,0 +1,57 @@
import { ReactNode } from 'react';
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { phoneFieldDefinition } from '@/object-record/field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/field/contexts/FieldContext';
import { useSaveFieldEditModeValue } from '@/object-record/field/hooks/useSaveFieldEditModeValue';
import { entityFieldsEditModeValueFamilySelector } from '@/object-record/field/states/selectors/entityFieldsEditModeValueFamilySelector';
const entityId = 'entityId';
const fieldName = 'phone';
const Wrapper = ({ children }: { children: ReactNode }) => {
return (
<MockedProvider addTypename={false}>
<FieldContext.Provider
value={{
fieldDefinition: phoneFieldDefinition,
entityId,
hotkeyScope: 'hotkeyScope',
isLabelIdentifier: false,
}}
>
<RecoilRoot>{children}</RecoilRoot>
</FieldContext.Provider>
</MockedProvider>
);
};
describe('useSaveFieldEditModeValue', () => {
it('should work as expected', () => {
const {
result: { current },
} = renderHook(
() => {
const entityFieldsEditModeValue = useRecoilValue(
entityFieldsEditModeValueFamilySelector({ entityId, fieldName }),
);
return {
saveFieldEditModeValue: useSaveFieldEditModeValue(),
entityFieldsEditModeValue,
};
},
{ wrapper: Wrapper },
);
expect(current.entityFieldsEditModeValue).toBeUndefined();
act(() => {
current.saveFieldEditModeValue('test');
});
// We expect `current.entityFieldsEditModeValue` to be updated
// but I think it's async
});
});

View File

@ -0,0 +1,95 @@
import { ReactNode } from 'react';
import { gql } from '@apollo/client';
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import { act, renderHook, waitFor } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { booleanFieldDefinition } from '@/object-record/field/__mocks__/fieldDefinitions';
import {
FieldContext,
RecordUpdateHook,
RecordUpdateHookParams,
} from '@/object-record/field/contexts/FieldContext';
import { useToggleEditOnlyInput } from '@/object-record/field/hooks/useToggleEditOnlyInput';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
jest.mock('@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery', () => ({
useMapFieldMetadataToGraphQLQuery: () => () => '\n',
}));
const entityId = 'entityId';
const mocks: MockedResponse[] = [
{
request: {
query: gql`
mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) {
updatePerson(id: $idToUpdate, data: $input) {
id
}
}
`,
variables: { idToUpdate: 'entityId', input: { isActive: true } },
},
result: jest.fn(() => ({
data: {
updateWorkspaceMember: {
id: 'entityId',
},
},
})),
},
];
const Wrapper = ({ children }: { children: ReactNode }) => {
const useUpdateOneRecordMutation: RecordUpdateHook = () => {
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular: CoreObjectNameSingular.Person,
});
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
updateOneRecord?.({
idToUpdate: variables.where.id as string,
updateOneRecordInput: variables.updateOneRecordInput,
});
};
return [updateEntity, { loading: false }];
};
return (
<MockedProvider mocks={mocks} addTypename={false}>
<FieldContext.Provider
value={{
fieldDefinition: booleanFieldDefinition,
entityId,
hotkeyScope: 'hotkeyScope',
isLabelIdentifier: false,
useUpdateRecord: useUpdateOneRecordMutation,
}}
>
<RecoilRoot>{children}</RecoilRoot>
</FieldContext.Provider>
</MockedProvider>
);
};
describe('useToggleEditOnlyInput', () => {
it('should toggle field', async () => {
const { result } = renderHook(
() => ({ toggleField: useToggleEditOnlyInput() }),
{
wrapper: Wrapper,
},
);
act(() => {
result.current.toggleField();
});
await waitFor(() => {
expect(mocks[0].result).toHaveBeenCalled();
});
});
});

View File

@ -0,0 +1,118 @@
import {
booleanFieldDefinition,
fieldMetadataId,
fullNameFieldDefinition,
linkFieldDefinition,
relationFieldDefinition,
selectFieldDefinition,
} from '@/object-record/field/__mocks__/fieldDefinitions';
import { FieldDefinition } from '@/object-record/field/types/FieldDefinition';
import { FieldCurrencyMetadata } from '@/object-record/field/types/FieldMetadata';
import { isFieldValueEmpty } from '../isFieldValueEmpty';
describe('isFieldValueEmpty', () => {
it('should return correct value for boolean field', () => {
expect(
isFieldValueEmpty({
fieldDefinition: booleanFieldDefinition,
fieldValue: null,
}),
).toBe(true);
expect(
isFieldValueEmpty({
fieldDefinition: booleanFieldDefinition,
fieldValue: false,
}),
).toBe(false);
expect(
isFieldValueEmpty({
fieldDefinition: booleanFieldDefinition,
fieldValue: true,
}),
).toBe(false);
});
it('should return correct value for relation field', () => {
expect(
isFieldValueEmpty({
fieldDefinition: relationFieldDefinition,
fieldValue: null,
}),
).toBe(true);
expect(
isFieldValueEmpty({
fieldDefinition: relationFieldDefinition,
fieldValue: { foo: 'bar' },
}),
).toBe(false);
});
it('should return correct value for select field', () => {
// If the value does not match the fieldDefinition, it will always return `false`
// Should it return `false` or `true` if the fieldValue doesn't match?
expect(
isFieldValueEmpty({
fieldDefinition: selectFieldDefinition,
fieldValue: '',
}),
).toBe(false);
});
it('should return correct value for currency field', () => {
const fieldDefinition: FieldDefinition<FieldCurrencyMetadata> = {
fieldMetadataId,
label: 'Annual Income',
iconName: 'cashCow',
type: 'CURRENCY',
metadata: {
fieldName: 'annualIncome',
placeHolder: '100000',
isPositive: true,
},
};
expect(
isFieldValueEmpty({
fieldDefinition,
fieldValue: { currencyCode: 'USD', amountMicros: 1000000 },
}),
).toBe(false);
expect(
isFieldValueEmpty({
fieldDefinition,
fieldValue: { currencyCode: 'USD' },
}),
).toBe(true);
});
it('should return correct value for fullname field', () => {
expect(
isFieldValueEmpty({
fieldDefinition: fullNameFieldDefinition,
fieldValue: { firstName: '', lastName: '' },
}),
).toBe(true);
expect(
isFieldValueEmpty({
fieldDefinition: fullNameFieldDefinition,
fieldValue: { firstName: 'Sheldon', lastName: '' },
}),
).toBe(false);
});
it('should return correct value for link field', () => {
expect(
isFieldValueEmpty({
fieldDefinition: linkFieldDefinition,
fieldValue: { url: '', label: '' },
}),
).toBe(true);
expect(
isFieldValueEmpty({
fieldDefinition: linkFieldDefinition,
fieldValue: { url: 'https://linkedin.com/user-slug', label: '' },
}),
).toBe(false);
});
});