diff --git a/packages/twenty-front/jest.config.ts b/packages/twenty-front/jest.config.ts index 3f9960330..4c44ba2a6 100644 --- a/packages/twenty-front/jest.config.ts +++ b/packages/twenty-front/jest.config.ts @@ -8,7 +8,8 @@ export default { '~/(.+)': '/src/$1', '@/(.+)': '/src/modules/$1', '@testing/(.+)': '/src/testing/$1', - '\\.(jpg|jpeg|png|gif|webp|svg)$': '/__mocks__/imageMock.js', + '\\.(jpg|jpeg|png|gif|webp|svg|svg\\?react)$': + '/__mocks__/imageMock.js', }, extensionsToTreatAsEsm: ['.ts', '.tsx'], coverageThreshold: { diff --git a/packages/twenty-front/src/modules/people/hooks/__mocks__/useSpreadsheetPersonImport.ts b/packages/twenty-front/src/modules/people/hooks/__mocks__/useSpreadsheetPersonImport.ts new file mode 100644 index 000000000..cd472f8c8 --- /dev/null +++ b/packages/twenty-front/src/modules/people/hooks/__mocks__/useSpreadsheetPersonImport.ts @@ -0,0 +1,124 @@ +import { gql } from '@apollo/client'; + +export const query = gql` + mutation CreatePeople($data: [PersonCreateInput!]!) { + createPeople(data: $data) { + id + opportunities { + edges { + node { + id + } + } + } + xLink { + label + url + } + id + pointOfContactForOpportunities { + edges { + node { + id + } + } + } + createdAt + company { + id + } + city + email + activityTargets { + edges { + node { + id + } + } + } + jobTitle + favorites { + edges { + node { + id + } + } + } + attachments { + edges { + node { + id + } + } + } + name { + firstName + lastName + } + phone + linkedinLink { + label + url + } + updatedAt + avatarUrl + companyId + } + } +`; + +export const personId = 'cb2e9f4b-20c3-4759-9315-4ffeecfaf71a'; + +export const variables = { + data: [ + { + id: personId, + name: { firstName: 'Sheldon', lastName: ' Cooper' }, + email: undefined, + jobTitle: undefined, + phone: undefined, + city: undefined, + }, + ], +}; + +export const responseData = [ + { + opportunities: { + edges: [], + }, + xLink: { + label: '', + url: '', + }, + pointOfContactForOpportunities: { + edges: [], + }, + createdAt: '', + company: { + id: '', + }, + city: '', + email: '', + activityTargets: { + edges: [], + }, + jobTitle: '', + favorites: { + edges: [], + }, + attachments: { + edges: [], + }, + name: variables.data[0].name, + phone: '', + linkedinLink: { + label: '', + url: '', + }, + updatedAt: '', + avatarUrl: '', + companyId: '', + id: personId, + }, +]; diff --git a/packages/twenty-front/src/modules/people/hooks/__tests__/useSpreadsheetPersonImport.test.tsx b/packages/twenty-front/src/modules/people/hooks/__tests__/useSpreadsheetPersonImport.test.tsx new file mode 100644 index 000000000..f3f1a4680 --- /dev/null +++ b/packages/twenty-front/src/modules/people/hooks/__tests__/useSpreadsheetPersonImport.test.tsx @@ -0,0 +1,107 @@ +import { ReactNode } from 'react'; +import { MockedProvider } from '@apollo/client/testing'; +import { act, renderHook, waitFor } from '@testing-library/react'; +import { RecoilRoot, useRecoilValue } from 'recoil'; + +import { spreadsheetImportState } from '@/spreadsheet-import/states/spreadsheetImportState'; +import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; + +import { + personId, + query, + responseData, + variables, +} from '../__mocks__/useSpreadsheetPersonImport'; +import { useSpreadsheetPersonImport } from '../useSpreadsheetPersonImport'; + +jest.mock('uuid', () => ({ + v4: jest.fn(() => personId), +})); + +const mocks = [ + { + request: { + query, + variables, + }, + result: jest.fn(() => ({ + data: { + createPeople: responseData, + }, + })), + }, +]; + +const Wrapper = ({ children }: { children: ReactNode }) => ( + + + + {children} + + + +); + +const fakeCsv = () => { + const csvContent = 'firstname, lastname\nSheldon, Cooper'; + const blob = new Blob([csvContent], { type: 'text/csv' }); + return new File([blob], 'fakeData.csv', { type: 'text/csv' }); +}; + +describe('useSpreadsheetPersonImport', () => { + it('should work as expected', async () => { + const { result } = renderHook( + () => { + const spreadsheetImport = useRecoilValue(spreadsheetImportState); + const { openPersonSpreadsheetImport } = useSpreadsheetPersonImport(); + return { openPersonSpreadsheetImport, spreadsheetImport }; + }, + { + wrapper: Wrapper, + }, + ); + + const { spreadsheetImport, openPersonSpreadsheetImport } = result.current; + + expect(spreadsheetImport.isOpen).toBe(false); + expect(spreadsheetImport.options).toBeNull(); + + await act(async () => { + openPersonSpreadsheetImport(); + }); + + const { spreadsheetImport: updatedImport } = result.current; + + expect(updatedImport.isOpen).toBe(true); + expect(updatedImport.options).toHaveProperty('onSubmit'); + expect(updatedImport.options?.onSubmit).toBeInstanceOf(Function); + expect(updatedImport.options).toHaveProperty('fields'); + expect(Array.isArray(updatedImport.options?.fields)).toBe(true); + + act(() => { + updatedImport.options?.onSubmit( + { + validData: [ + { + firstName: 'Sheldon', + lastName: ' Cooper', + }, + ], + invalidData: [], + all: [ + { + firstName: 'Sheldon', + lastName: ' Cooper', + __index: 'cbc3985f-dde9-46d1-bae2-c124141700ac', + }, + ], + }, + fakeCsv(), + ); + }); + + await waitFor(() => { + expect(mocks[0].result).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/twenty-front/src/modules/pipeline/hooks/__mocks__/usePipelineSteps.ts b/packages/twenty-front/src/modules/pipeline/hooks/__mocks__/usePipelineSteps.ts new file mode 100644 index 000000000..0b4aa621d --- /dev/null +++ b/packages/twenty-front/src/modules/pipeline/hooks/__mocks__/usePipelineSteps.ts @@ -0,0 +1,66 @@ +import { gql } from '@apollo/client'; + +export const query = gql` + mutation CreateOnePipelineStep($input: PipelineStepCreateInput!) { + createPipelineStep(data: $input) { + id + name + id + createdAt + opportunities { + edges { + node { + id + } + } + } + position + color + updatedAt + } + } +`; + +export const deleteQuery = gql` + mutation DeleteOnePipelineStep($idToDelete: ID!) { + deletePipelineStep(id: $idToDelete) { + id + } + } +`; + +export const mockId = '8f3b2121-f194-4ba4-9fbf-2d5a37126806'; +export const currentPipelineId = 'f088c8c9-05d2-4276-b065-b863cc7d0b33'; + +const data = { + color: 'yellow', + id: 'columnId', + position: 1, + name: 'Column Title', + pipeline: { connect: { id: currentPipelineId } }, + type: 'ongoing', +}; + +export const variables = { + input: { + id: mockId, + variables: { + data, + }, + }, +}; + +export const deleteVariables = { idToDelete: 'columnId' }; + +export const responseData = { + ...data, + createdAt: '', + opportunities: { + edges: [], + }, + updatedAt: '', +}; + +export const deleteResponseData = { + id: 'columnId', +}; diff --git a/packages/twenty-front/src/modules/pipeline/hooks/__tests__/usePipelineSteps.test.tsx b/packages/twenty-front/src/modules/pipeline/hooks/__tests__/usePipelineSteps.test.tsx new file mode 100644 index 000000000..7106a0b92 --- /dev/null +++ b/packages/twenty-front/src/modules/pipeline/hooks/__tests__/usePipelineSteps.test.tsx @@ -0,0 +1,104 @@ +import { ReactNode } from 'react'; +import { act } from 'react-dom/test-utils'; +import { MockedProvider } from '@apollo/client/testing'; +import { renderHook } from '@testing-library/react'; +import { RecoilRoot, useSetRecoilState } from 'recoil'; + +import { BoardColumnDefinition } from '@/object-record/record-board/types/BoardColumnDefinition'; +import { currentPipelineState } from '@/pipeline/states/currentPipelineState'; + +import { + currentPipelineId, + deleteQuery, + deleteResponseData, + deleteVariables, + mockId, + query, + responseData, + variables, +} from '../__mocks__/usePipelineSteps'; +import { usePipelineSteps } from '../usePipelineSteps'; + +const mocks = [ + { + request: { + query, + variables, + }, + result: jest.fn(() => ({ + data: { + createPipelineStep: responseData, + }, + })), + }, + { + request: { + query: deleteQuery, + variables: deleteVariables, + }, + result: jest.fn(() => ({ + data: { + deletePipelineStep: deleteResponseData, + }, + })), + }, +]; + +const Wrapper = ({ children }: { children: ReactNode }) => ( + + + {children} + + +); + +jest.mock('uuid', () => ({ + v4: jest.fn(() => mockId), +})); + +describe('usePipelineSteps', () => { + it('should handlePipelineStepAdd successfully', async () => { + const { result } = renderHook( + () => { + const setCurrentPipeline = useSetRecoilState(currentPipelineState); + setCurrentPipeline({ id: currentPipelineId }); + return usePipelineSteps(); + }, + { + wrapper: Wrapper, + }, + ); + + const boardColumn: BoardColumnDefinition = { + id: 'columnId', + title: 'Column Title', + colorCode: 'yellow', + position: 1, + }; + + await act(async () => { + const res = await result.current.handlePipelineStepAdd(boardColumn); + expect(res).toEqual(responseData); + }); + }); + + it('should handlePipelineStepDelete successfully', async () => { + const { result } = renderHook( + () => { + const setCurrentPipeline = useSetRecoilState(currentPipelineState); + setCurrentPipeline({ id: currentPipelineId }); + return usePipelineSteps(); + }, + { + wrapper: Wrapper, + }, + ); + + const boardColumnId = 'columnId'; + + await act(async () => { + const res = await result.current.handlePipelineStepDelete(boardColumnId); + expect(res).toEqual(deleteResponseData); + }); + }); +}); diff --git a/packages/twenty-front/src/modules/search/hooks/__mocks__/useFilteredSearchEntityQuery.ts b/packages/twenty-front/src/modules/search/hooks/__mocks__/useFilteredSearchEntityQuery.ts new file mode 100644 index 000000000..bd044c28e --- /dev/null +++ b/packages/twenty-front/src/modules/search/hooks/__mocks__/useFilteredSearchEntityQuery.ts @@ -0,0 +1,190 @@ +import { gql } from '@apollo/client'; + +export const query = gql` + query FindManyPeople( + $filter: PersonFilterInput + $orderBy: PersonOrderByInput + $lastCursor: String + $limit: Float = 30 + ) { + people( + filter: $filter + orderBy: $orderBy + first: $limit + after: $lastCursor + ) { + edges { + node { + id + opportunities { + edges { + node { + id + personId + pointOfContactId + updatedAt + companyId + pipelineStepId + probability + closeDate + amount { + amountMicros + currencyCode + } + id + createdAt + } + } + } + xLink { + label + url + } + id + pointOfContactForOpportunities { + edges { + node { + id + personId + pointOfContactId + updatedAt + companyId + pipelineStepId + probability + closeDate + amount { + amountMicros + currencyCode + } + id + createdAt + } + } + } + createdAt + company { + id + xLink { + label + url + } + linkedinLink { + label + url + } + domainName + annualRecurringRevenue { + amountMicros + currencyCode + } + createdAt + address + updatedAt + name + accountOwnerId + employees + id + idealCustomerProfile + } + city + email + activityTargets { + edges { + node { + id + updatedAt + createdAt + personId + activityId + companyId + id + } + } + } + jobTitle + favorites { + edges { + node { + id + id + companyId + createdAt + personId + position + workspaceMemberId + updatedAt + } + } + } + attachments { + edges { + node { + id + updatedAt + createdAt + name + personId + activityId + companyId + id + authorId + type + fullPath + } + } + } + name { + firstName + lastName + } + phone + linkedinLink { + label + url + } + updatedAt + avatarUrl + companyId + } + cursor + } + pageInfo { + hasNextPage + startCursor + endCursor + } + } + } +`; + +export const variables = { + entitiesToSelect: { + limit: 10, + filter: { + and: [ + { and: [{ or: [{ name: { ilike: '%Entity%' } }] }] }, + { not: { id: { in: ['1', '2'] } } }, + ], + }, + orderBy: { name: 'AscNullsLast' }, + }, + filteredSelectedEntities: { + limit: 60, + filter: { + and: [ + { and: [{ or: [{ name: { ilike: '%Entity%' } }] }] }, + { id: { in: ['1'] } }, + ], + }, + orderBy: { name: 'AscNullsLast' }, + }, + selectedEntities: { + limit: 60, + filter: { id: { in: ['1'] } }, + orderBy: { name: 'AscNullsLast' }, + }, +}; + +export const responseData = { + edges: [], +}; diff --git a/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchEntityQuery.test.tsx b/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchEntityQuery.test.tsx new file mode 100644 index 000000000..26c8378aa --- /dev/null +++ b/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchEntityQuery.test.tsx @@ -0,0 +1,109 @@ +import { ReactNode } from 'react'; +import { MockedProvider } from '@apollo/client/testing'; +import { renderHook } from '@testing-library/react'; +import { RecoilRoot, useSetRecoilState } from 'recoil'; + +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; +import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect'; +import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; + +import { + query, + responseData, + variables, +} from '../__mocks__/useFilteredSearchEntityQuery'; +import { useFilteredSearchEntityQuery } from '../useFilteredSearchEntityQuery'; + +const mocks = [ + { + request: { + query, + variables: variables.entitiesToSelect, + }, + result: jest.fn(() => ({ + data: { + people: responseData, + }, + })), + }, + { + request: { + query, + variables: variables.filteredSelectedEntities, + }, + result: jest.fn(() => ({ + data: { + people: responseData, + }, + })), + }, + { + request: { + query, + variables: variables.selectedEntities, + }, + result: jest.fn(() => ({ + data: { + people: responseData, + }, + })), + }, +]; + +const Wrapper = ({ children }: { children: ReactNode }) => ( + + + + {children} + + + +); + +describe('useFilteredSearchEntityQuery', () => { + it('returns the correct result when everything is provided', async () => { + const { result } = renderHook( + () => { + const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); + setCurrentWorkspace({ + id: '32219445-f587-4c40-b2b1-6d3205ed96da', + displayName: 'cool-workspace', + allowImpersonation: false, + subscriptionStatus: 'incomplete', + }); + + const mockObjectMetadataItems = getObjectMetadataItemsMock(); + + const setMetadataItems = useSetRecoilState(objectMetadataItemsState); + + setMetadataItems(mockObjectMetadataItems); + + return useFilteredSearchEntityQuery({ + orderByField: 'name', + filters: [{ fieldNames: ['name'], filter: 'Entity' }], + sortOrder: 'AscNullsLast', + selectedIds: ['1'], + mappingFunction: (entity): any => ({ + value: entity.id, + label: entity.name, + }), + limit: 10, + excludeEntityIds: ['2'], + objectNameSingular: 'person', + }); + }, + { wrapper: Wrapper }, + ); + + const expectedResult: EntitiesForMultipleEntitySelect = { + selectedEntities: [], + filteredSelectedEntities: [], + entitiesToSelect: [], + loading: true, + }; + + expect(result.current).toEqual(expectedResult); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/data-model/hooks/__tests__/useFieldMetadataForm.test.ts b/packages/twenty-front/src/modules/settings/data-model/hooks/__tests__/useFieldMetadataForm.test.ts new file mode 100644 index 000000000..058e2e4d7 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/hooks/__tests__/useFieldMetadataForm.test.ts @@ -0,0 +1,51 @@ +import { act, renderHook } from '@testing-library/react'; + +import { useFieldMetadataForm } from '../useFieldMetadataForm'; + +describe('useFieldMetadataForm', () => { + it('should initialize with default values', () => { + const { result } = renderHook(() => useFieldMetadataForm()); + + expect(result.current.isInitialized).toBe(false); + + act(() => { + result.current.initForm({}); + }); + + expect(result.current.isInitialized).toBe(true); + expect(result.current.formValues).toEqual({ + icon: 'IconUsers', + label: '', + type: 'TEXT', + currency: { currencyCode: 'USD' }, + relation: { + type: 'ONE_TO_MANY', + objectMetadataId: '', + field: { label: '' }, + }, + select: [ + { color: 'green', label: 'Option 1', value: expect.any(String) }, + ], + }); + }); + + it('should handle form changes', () => { + const { result } = renderHook(() => useFieldMetadataForm()); + + act(() => { + result.current.initForm({}); + }); + + expect(result.current.hasFieldFormChanged).toBe(false); + expect(result.current.hasRelationFormChanged).toBe(false); + expect(result.current.hasSelectFormChanged).toBe(false); + + act(() => { + result.current.handleFormChange({ label: 'New Label' }); + }); + + expect(result.current.hasFieldFormChanged).toBe(true); + expect(result.current.hasRelationFormChanged).toBe(false); + expect(result.current.hasSelectFormChanged).toBe(false); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/data-model/hooks/__tests__/useFieldPreview.test.tsx b/packages/twenty-front/src/modules/settings/data-model/hooks/__tests__/useFieldPreview.test.tsx new file mode 100644 index 000000000..c42bb40fb --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/hooks/__tests__/useFieldPreview.test.tsx @@ -0,0 +1,50 @@ +import { ReactNode } from 'react'; +import { MockedProvider } from '@apollo/client/testing'; +import { renderHook } from '@testing-library/react'; +import { RecoilRoot, useSetRecoilState } from 'recoil'; + +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; +import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; + +import { useFieldPreview } from '../useFieldPreview'; + +const Wrapper = ({ children }: { children: ReactNode }) => ( + + + + {children} + + + +); + +const mockObjectMetadataItems = getObjectMetadataItemsMock(); + +describe('useFieldPreview', () => { + it('returns default values', () => { + const objectMetadataItem = mockObjectMetadataItems[1]; + const fieldMetadata = objectMetadataItem.fields[0]; + const { result } = renderHook( + () => { + const setMetadataItems = useSetRecoilState(objectMetadataItemsState); + setMetadataItems(mockObjectMetadataItems); + + return useFieldPreview({ + objectMetadataId: objectMetadataItem.id, + fieldMetadata, + }); + }, + { wrapper: Wrapper }, + ); + + expect(result.current.entityId).toBe(`${objectMetadataItem.id}-field-form`); + expect(result.current.FieldIcon).toBeDefined(); + expect(result.current.fieldName).toBe(fieldMetadata.name); + expect(result.current.ObjectIcon).toBeDefined(); + expect(result.current.fieldName).toBe(fieldMetadata.name); + expect(result.current.objectMetadataItem?.id).toBe(objectMetadataItem.id); + expect(result.current.relationObjectMetadataItem).toBeUndefined(); + expect(result.current.value).toBeDefined(); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/developers/hooks/__tests__/useGeneratedApiKeys.test.ts b/packages/twenty-front/src/modules/settings/developers/hooks/__tests__/useGeneratedApiKeys.test.ts new file mode 100644 index 000000000..b758a903d --- /dev/null +++ b/packages/twenty-front/src/modules/settings/developers/hooks/__tests__/useGeneratedApiKeys.test.ts @@ -0,0 +1,27 @@ +import { act, renderHook } from '@testing-library/react'; +import { RecoilRoot, RecoilState } from 'recoil'; + +import { generatedApiKeyFamilyState } from '@/settings/developers/states/generatedApiKeyFamilyState'; + +import { useGeneratedApiKeys } from '../useGeneratedApiKeys'; + +describe('useGeneratedApiKeys', () => { + test('should set generatedApiKeyFamilyState correctly', () => { + const { result } = renderHook(() => useGeneratedApiKeys(), { + wrapper: RecoilRoot, + }); + + const apiKeyId = 'someId'; + const apiKey = 'someKey'; + + act(() => { + result.current(apiKeyId, apiKey); + }); + + const recoilState: RecoilState = + generatedApiKeyFamilyState(apiKeyId); + + const stateValue = recoilState.key; + expect(stateValue).toContain(apiKeyId); + }); +});