TWNTY-3381 - Add tests for modules/apollo (#3530)
Add tests for `modules/apollo` Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com> Co-authored-by: v1b3m <vibenjamin6@gmail.com>
This commit is contained in:
committed by
GitHub
parent
aa8d689e3e
commit
d8c9a94e14
@ -240,6 +240,7 @@
|
|||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-environment-jsdom": "29.7.0",
|
"jest-environment-jsdom": "29.7.0",
|
||||||
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"msw": "^2.0.11",
|
"msw": "^2.0.11",
|
||||||
"msw-storybook-addon": "2.0.0--canary.122.b3ed3b1.0",
|
"msw-storybook-addon": "2.0.0--canary.122.b3ed3b1.0",
|
||||||
"nx": "^17.2.8",
|
"nx": "^17.2.8",
|
||||||
|
|||||||
@ -0,0 +1,96 @@
|
|||||||
|
import { MemoryRouter, useLocation } from 'react-router-dom';
|
||||||
|
import { ApolloError, gql } from '@apollo/client';
|
||||||
|
import { act, renderHook } from '@testing-library/react';
|
||||||
|
import fetchMock, { enableFetchMocks } from 'jest-fetch-mock';
|
||||||
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
|
import { useApolloFactory } from '../useApolloFactory';
|
||||||
|
|
||||||
|
enableFetchMocks();
|
||||||
|
|
||||||
|
const mockNavigate = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => {
|
||||||
|
const initialRouter = jest.requireActual('react-router-dom');
|
||||||
|
|
||||||
|
return {
|
||||||
|
...initialRouter,
|
||||||
|
useNavigate: () => mockNavigate,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<RecoilRoot>
|
||||||
|
<MemoryRouter
|
||||||
|
initialEntries={['/sign-in', '/verify', '/opportunities']}
|
||||||
|
initialIndex={2}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</MemoryRouter>
|
||||||
|
</RecoilRoot>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('useApolloFactory', () => {
|
||||||
|
it('should work as expected', () => {
|
||||||
|
const { result } = renderHook(() => useApolloFactory(), {
|
||||||
|
wrapper: Wrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = result.current;
|
||||||
|
|
||||||
|
expect(res).toBeDefined();
|
||||||
|
expect(res).toHaveProperty('link');
|
||||||
|
expect(res).toHaveProperty('cache');
|
||||||
|
expect(res).toHaveProperty('query');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to /sign-in on unauthenticated error', async () => {
|
||||||
|
const errors = [
|
||||||
|
{
|
||||||
|
extensions: {
|
||||||
|
code: 'UNAUTHENTICATED',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
fetchMock.mockResponse(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
body: JSON.stringify({
|
||||||
|
data: {},
|
||||||
|
errors,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { result } = renderHook(
|
||||||
|
() => {
|
||||||
|
const location = useLocation();
|
||||||
|
return { factory: useApolloFactory(), location };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
wrapper: Wrapper,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current.location.pathname).toBe('/opportunities');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.factory.mutate({
|
||||||
|
mutation: gql`
|
||||||
|
mutation CreateEvent($type: String!, $data: JSON!) {
|
||||||
|
createEvent(type: $type, data: $data) {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toBeInstanceOf(ApolloError);
|
||||||
|
expect((error as ApolloError).message).toBe('Error message not found.');
|
||||||
|
|
||||||
|
expect(mockNavigate).toHaveBeenCalled();
|
||||||
|
expect(mockNavigate).toHaveBeenCalledWith('/sign-in');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||||
|
import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
||||||
|
import { optimisticEffectState } from '@/apollo/optimistic-effect/states/optimisticEffectState';
|
||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
|
||||||
|
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<MockedProvider addTypename={false}>
|
||||||
|
<RecoilRoot>{children}</RecoilRoot>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('useOptimisticEffect', () => {
|
||||||
|
it('should work as expected', async () => {
|
||||||
|
const { result } = renderHook(
|
||||||
|
() => {
|
||||||
|
const optimisticEffect = useRecoilValue(optimisticEffectState);
|
||||||
|
const client = useApolloClient();
|
||||||
|
const { findManyRecordsQuery } = useObjectMetadataItem({
|
||||||
|
objectNameSingular: 'person',
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...useOptimisticEffect({ objectNameSingular: 'person' }),
|
||||||
|
optimisticEffect,
|
||||||
|
cache: client.cache,
|
||||||
|
findManyRecordsQuery,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
wrapper: Wrapper,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
registerOptimisticEffect,
|
||||||
|
unregisterOptimisticEffect,
|
||||||
|
triggerOptimisticEffects,
|
||||||
|
optimisticEffect,
|
||||||
|
findManyRecordsQuery,
|
||||||
|
} = result.current;
|
||||||
|
|
||||||
|
expect(registerOptimisticEffect).toBeDefined();
|
||||||
|
expect(typeof registerOptimisticEffect).toBe('function');
|
||||||
|
expect(optimisticEffect).toEqual({});
|
||||||
|
|
||||||
|
const optimisticEffectDefinition = {
|
||||||
|
variables: {},
|
||||||
|
definition: {
|
||||||
|
typename: 'Person',
|
||||||
|
resolver: () => ({
|
||||||
|
people: [],
|
||||||
|
pageInfo: {
|
||||||
|
endCursor: '',
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
startCursor: '',
|
||||||
|
},
|
||||||
|
edges: [],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
registerOptimisticEffect(optimisticEffectDefinition);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.optimisticEffect).toHaveProperty('Person-{}');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
result.current.cache.readQuery({ query: findManyRecordsQuery }),
|
||||||
|
).toBeNull();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
triggerOptimisticEffects({
|
||||||
|
typename: 'Person',
|
||||||
|
createdRecords: [{ id: 'id-0' }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
result.current.cache.readQuery({ query: findManyRecordsQuery }),
|
||||||
|
).toHaveProperty('people');
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
unregisterOptimisticEffect(optimisticEffectDefinition);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.optimisticEffect).not.toHaveProperty('Person-{}');
|
||||||
|
expect(result.current.optimisticEffect).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import { act, renderHook } from '@testing-library/react';
|
||||||
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
|
import { useOptimisticEvict } from '@/apollo/optimistic-effect/hooks/useOptimisticEvict';
|
||||||
|
|
||||||
|
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<MockedProvider addTypename={false}>
|
||||||
|
<RecoilRoot>{children}</RecoilRoot>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('useOptimisticEvict', () => {
|
||||||
|
it('should perform cache eviction', async () => {
|
||||||
|
const mockedData = {
|
||||||
|
'someType:1': { __typename: 'someType', id: '1', fieldName: 'value1' },
|
||||||
|
'someType:2': { __typename: 'someType', id: '2', fieldName: 'value2' },
|
||||||
|
'otherType:1': { __typename: 'otherType', id: '1', fieldName: 'value3' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result } = renderHook(
|
||||||
|
() => {
|
||||||
|
const { cache } = useApolloClient();
|
||||||
|
cache.restore(mockedData);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...useOptimisticEvict(),
|
||||||
|
cache,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
wrapper: Wrapper,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { performOptimisticEvict, cache } = result.current;
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
performOptimisticEvict('someType', 'fieldName', 'value1');
|
||||||
|
});
|
||||||
|
|
||||||
|
const cacheSnapshot = cache.extract();
|
||||||
|
|
||||||
|
expect(cacheSnapshot).toEqual({
|
||||||
|
'someType:2': { __typename: 'someType', id: '2', fieldName: 'value2' },
|
||||||
|
'otherType:1': { __typename: 'otherType', id: '1', fieldName: 'value3' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,141 @@
|
|||||||
|
import { ApolloError, gql, InMemoryCache } from '@apollo/client';
|
||||||
|
import fetchMock, { enableFetchMocks } from 'jest-fetch-mock';
|
||||||
|
|
||||||
|
import { ApolloFactory, Options } from '../apollo.factory';
|
||||||
|
|
||||||
|
enableFetchMocks();
|
||||||
|
|
||||||
|
jest.mock('@/auth/services/AuthService', () => {
|
||||||
|
const initialAuthService = jest.requireActual('@/auth/services/AuthService');
|
||||||
|
return {
|
||||||
|
...initialAuthService,
|
||||||
|
renewToken: jest.fn().mockReturnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
accessToken: { token: 'newAccessToken', expiresAt: '' },
|
||||||
|
refreshToken: { token: 'newRefreshToken', expiresAt: '' },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockOnError = jest.fn();
|
||||||
|
const mockOnNetworkError = jest.fn();
|
||||||
|
|
||||||
|
const createMockOptions = (): Options<any> => ({
|
||||||
|
uri: 'http://localhost:3000',
|
||||||
|
initialTokenPair: {
|
||||||
|
accessToken: { token: 'mockAccessToken', expiresAt: '' },
|
||||||
|
refreshToken: { token: 'mockRefreshToken', expiresAt: '' },
|
||||||
|
},
|
||||||
|
cache: new InMemoryCache(),
|
||||||
|
isDebugMode: true,
|
||||||
|
onError: mockOnError,
|
||||||
|
onNetworkError: mockOnNetworkError,
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeRequest = async () => {
|
||||||
|
const options = createMockOptions();
|
||||||
|
const apolloFactory = new ApolloFactory(options);
|
||||||
|
|
||||||
|
const client = apolloFactory.getClient();
|
||||||
|
|
||||||
|
await client.mutate({
|
||||||
|
mutation: gql`
|
||||||
|
mutation CreateEvent($type: String!, $data: JSON!) {
|
||||||
|
createEvent(type: $type, data: $data) {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('xApolloFactory', () => {
|
||||||
|
it('should create an instance of ApolloFactory', () => {
|
||||||
|
const options = createMockOptions();
|
||||||
|
const apolloFactory = new ApolloFactory(options);
|
||||||
|
expect(apolloFactory).toBeInstanceOf(ApolloFactory);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onError when encountering "Unauthorized" error', async () => {
|
||||||
|
const errors = [{ message: 'Unauthorized' }];
|
||||||
|
fetchMock.mockResponse(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
body: JSON.stringify({
|
||||||
|
data: {},
|
||||||
|
errors,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await makeRequest();
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toBeInstanceOf(ApolloError);
|
||||||
|
expect((error as ApolloError).message).toBe('Unauthorized');
|
||||||
|
expect(mockOnError).toHaveBeenCalledWith(errors);
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
it('should call onError when encountering "UNAUTHENTICATED" error', async () => {
|
||||||
|
const errors = [
|
||||||
|
{
|
||||||
|
extensions: {
|
||||||
|
code: 'UNAUTHENTICATED',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
fetchMock.mockResponse(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
body: JSON.stringify({
|
||||||
|
data: {},
|
||||||
|
errors,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await makeRequest();
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toBeInstanceOf(ApolloError);
|
||||||
|
expect((error as ApolloError).message).toBe('Error message not found.');
|
||||||
|
expect(mockOnError).toHaveBeenCalledWith(errors);
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
it('should call onNetworkError when encountering a network error', async () => {
|
||||||
|
const errors = [
|
||||||
|
{
|
||||||
|
message: 'Unknown error',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
fetchMock.mockResponse(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
body: JSON.stringify({
|
||||||
|
data: {},
|
||||||
|
errors,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await makeRequest();
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toBeInstanceOf(ApolloError);
|
||||||
|
expect((error as ApolloError).message).toBe('Unknown error');
|
||||||
|
expect(mockOnError).toHaveBeenCalledWith(errors);
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
it('should call renewToken when encountering any error', async () => {
|
||||||
|
const mockError = { message: 'Unknown error' };
|
||||||
|
fetchMock.mockReject(() => Promise.reject(mockError));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await makeRequest();
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toBeInstanceOf(ApolloError);
|
||||||
|
expect((error as ApolloError).message).toBe('Unknown error');
|
||||||
|
expect(mockOnNetworkError).toHaveBeenCalledWith(mockError);
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
});
|
||||||
20
yarn.lock
20
yarn.lock
@ -21095,7 +21095,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"cross-fetch@npm:^3.1.5":
|
"cross-fetch@npm:^3.0.4, cross-fetch@npm:^3.1.5":
|
||||||
version: 3.1.8
|
version: 3.1.8
|
||||||
resolution: "cross-fetch@npm:3.1.8"
|
resolution: "cross-fetch@npm:3.1.8"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -28901,6 +28901,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"jest-fetch-mock@npm:^3.0.3":
|
||||||
|
version: 3.0.3
|
||||||
|
resolution: "jest-fetch-mock@npm:3.0.3"
|
||||||
|
dependencies:
|
||||||
|
cross-fetch: "npm:^3.0.4"
|
||||||
|
promise-polyfill: "npm:^8.1.3"
|
||||||
|
checksum: 21ffe8c902ca5adafa7ed61760e100e4c290e99b0b487645f5bb92938ea64c2d1d9dc8af46e65fb7917d6237586067d53af756583a77330dbb4fbda079a63c29
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"jest-get-type@npm:^29.6.3":
|
"jest-get-type@npm:^29.6.3":
|
||||||
version: 29.6.3
|
version: 29.6.3
|
||||||
resolution: "jest-get-type@npm:29.6.3"
|
resolution: "jest-get-type@npm:29.6.3"
|
||||||
@ -36887,6 +36897,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"promise-polyfill@npm:^8.1.3":
|
||||||
|
version: 8.3.0
|
||||||
|
resolution: "promise-polyfill@npm:8.3.0"
|
||||||
|
checksum: 97141f07a31a6be135ec9ed53700a3423a771cabec0ba5e08fcb2bf8cfda855479ff70e444fceb938f560be42b450cd032c14f4a940ed2ad1e5b4dcb78368dce
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"promise-retry@npm:^2.0.1":
|
"promise-retry@npm:^2.0.1":
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
resolution: "promise-retry@npm:2.0.1"
|
resolution: "promise-retry@npm:2.0.1"
|
||||||
@ -42560,6 +42577,7 @@ __metadata:
|
|||||||
immer: "npm:^10.0.2"
|
immer: "npm:^10.0.2"
|
||||||
jest: "npm:29.7.0"
|
jest: "npm:29.7.0"
|
||||||
jest-environment-jsdom: "npm:29.7.0"
|
jest-environment-jsdom: "npm:29.7.0"
|
||||||
|
jest-fetch-mock: "npm:^3.0.3"
|
||||||
jest-mock-extended: "npm:^3.0.4"
|
jest-mock-extended: "npm:^3.0.4"
|
||||||
js-cookie: "npm:^3.0.5"
|
js-cookie: "npm:^3.0.5"
|
||||||
js-levenshtein: "npm:^1.1.6"
|
js-levenshtein: "npm:^1.1.6"
|
||||||
|
|||||||
Reference in New Issue
Block a user