[TEST] Covering useDeleteOne relations optimistic cache behavior (#10238)
## Introduction Added coverage on the `useDeleteOneRecord` hooks, especially its optimistic behavior feature. Introduced a new testing tool `InMemoryTestingCacheInstance` that has builtin very basic expectors in order to avoid future duplication when covering others record hooks `update, create, destroy` etc etc ## Notes Added few comments in this PR regarding some builtin functions I've created around companies and people mocked object model and that I think could be cool to spread and centralize within a dedicated "class template" Also put in light that unless I'm mistaken some tests are running on `RecordNode` and not `RecordObject` Took few directions on my own that as I always I would suggestion nor remarks on them ! Let me know ## Misc - Should we refactor `useDeleteOneRecord` tests to follow `eachTesting` pattern ? => I feel like this is inappropriate as this hooks is already high level, the only plus value would be less tests code despite readability IMO
This commit is contained in:
113
packages/twenty-front/src/testing/cache/inMemoryTestingCacheInstance.ts
vendored
Normal file
113
packages/twenty-front/src/testing/cache/inMemoryTestingCacheInstance.ts
vendored
Normal file
@ -0,0 +1,113 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
|
||||
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
|
||||
import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { InMemoryCache, NormalizedCacheObject } from '@apollo/client';
|
||||
import { expect } from '@storybook/jest';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
type ObjectMetadataItemAndRecordId = {
|
||||
recordId: string;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
type RecordsWithObjectMetadataItem = {
|
||||
records: ObjectRecord[];
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}[];
|
||||
|
||||
type GetMockCachedRecord = {
|
||||
recordId: string;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
matchObject?: Record<string, unknown>;
|
||||
snapshotPropertyMatchers?: {
|
||||
deletedAt?: any;
|
||||
updatedAt?: any;
|
||||
createdAt?: any;
|
||||
};
|
||||
};
|
||||
type InMemoryTestingCacheInstanceArgs = {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
initialRecordsInCache?: RecordsWithObjectMetadataItem;
|
||||
};
|
||||
|
||||
export class InMemoryTestingCacheInstance {
|
||||
private _cache: InMemoryCache;
|
||||
private objectMetadataItems: ObjectMetadataItem[];
|
||||
private initialStateExtract: NormalizedCacheObject;
|
||||
|
||||
constructor({
|
||||
objectMetadataItems,
|
||||
initialRecordsInCache = [],
|
||||
}: InMemoryTestingCacheInstanceArgs) {
|
||||
this.objectMetadataItems = objectMetadataItems;
|
||||
this._cache = new InMemoryCache();
|
||||
|
||||
this.populateRecordsInCache(initialRecordsInCache);
|
||||
this.initialStateExtract = this._cache.extract();
|
||||
}
|
||||
|
||||
public populateRecordsInCache = (
|
||||
recordsWithObjectMetadataItem: RecordsWithObjectMetadataItem,
|
||||
) => {
|
||||
recordsWithObjectMetadataItem.forEach(({ objectMetadataItem, records }) =>
|
||||
records.forEach((record) =>
|
||||
updateRecordFromCache({
|
||||
cache: this._cache,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems: this.objectMetadataItems,
|
||||
record,
|
||||
recordGqlFields: computeDepthOneRecordGqlFieldsFromRecord({
|
||||
objectMetadataItem,
|
||||
record,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
public assertCachedRecordIsNull = ({
|
||||
objectMetadataItem,
|
||||
recordId,
|
||||
}: ObjectMetadataItemAndRecordId) => {
|
||||
const cachedRecord = getRecordFromCache({
|
||||
cache: this._cache,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems: this.objectMetadataItems,
|
||||
recordId,
|
||||
});
|
||||
expect(cachedRecord).toBeNull();
|
||||
};
|
||||
|
||||
public assertCachedRecordMatchSnapshot = ({
|
||||
objectMetadataItem,
|
||||
recordId,
|
||||
matchObject,
|
||||
snapshotPropertyMatchers,
|
||||
}: GetMockCachedRecord) => {
|
||||
const cachedRecord = getRecordFromCache({
|
||||
cache: this._cache,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems: this.objectMetadataItems,
|
||||
recordId,
|
||||
});
|
||||
expect(cachedRecord).not.toBeNull();
|
||||
|
||||
if (cachedRecord === null) {
|
||||
throw new Error('Should never occurs, cachedRecord is null');
|
||||
}
|
||||
|
||||
if (isDefined(matchObject)) {
|
||||
expect(cachedRecord).toMatchObject(matchObject);
|
||||
}
|
||||
expect(cachedRecord).toMatchSnapshot(snapshotPropertyMatchers ?? {});
|
||||
};
|
||||
|
||||
public restoreCacheToInitialState = async () => {
|
||||
return this._cache.restore(this.initialStateExtract);
|
||||
};
|
||||
|
||||
public get cache() {
|
||||
return this._cache;
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ import { contextStoreCurrentObjectMetadataItemComponentState } from '@/context-s
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
import { getCompanyObjectMetadataItem } from '~/testing/mock-data/companies';
|
||||
import { getMockCompanyObjectMetadataItem } from '~/testing/mock-data/companies';
|
||||
|
||||
export const ContextStoreDecorator: Decorator = (Story, context) => {
|
||||
const { contextStore } = context.parameters;
|
||||
@ -24,7 +24,7 @@ export const ContextStoreDecorator: Decorator = (Story, context) => {
|
||||
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
const objectMetadataItem = getCompanyObjectMetadataItem();
|
||||
const objectMetadataItem = getMockCompanyObjectMetadataItem();
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentObjectMetadataItem(objectMetadataItem);
|
||||
|
||||
@ -14,7 +14,7 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { getCompaniesMock } from '~/testing/mock-data/companies';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import { getPeopleMock } from '~/testing/mock-data/people';
|
||||
import { getPeopleRecordConnectionMock } from '~/testing/mock-data/people';
|
||||
import { mockedTasks } from '~/testing/mock-data/tasks';
|
||||
|
||||
const RecordMockSetterEffect = ({
|
||||
@ -71,7 +71,7 @@ export const getFieldDecorator =
|
||||
]
|
||||
: companiesMock;
|
||||
|
||||
const peopleMock = getPeopleMock();
|
||||
const peopleMock = getPeopleRecordConnectionMock();
|
||||
|
||||
const people =
|
||||
objectNameSingular === 'person' && isDefined(fieldValue)
|
||||
|
||||
@ -14,7 +14,7 @@ import { mockedClientConfig } from '~/testing/mock-data/config';
|
||||
import { mockedFavoritesData } from '~/testing/mock-data/favorite';
|
||||
import { mockedFavoriteFoldersData } from '~/testing/mock-data/favorite-folders';
|
||||
import { mockedNotes } from '~/testing/mock-data/notes';
|
||||
import { getPeopleMock } from '~/testing/mock-data/people';
|
||||
import { getPeopleRecordConnectionMock } from '~/testing/mock-data/people';
|
||||
import { mockedRemoteTables } from '~/testing/mock-data/remote-tables';
|
||||
import { mockedUserData } from '~/testing/mock-data/users';
|
||||
import { mockedViewsData } from '~/testing/mock-data/views';
|
||||
@ -33,7 +33,7 @@ import {
|
||||
import { mockedRemoteServers } from './mock-data/remote-servers';
|
||||
import { mockedViewFieldsData } from './mock-data/view-fields';
|
||||
|
||||
const peopleMock = getPeopleMock();
|
||||
const peopleMock = getPeopleRecordConnectionMock();
|
||||
const companiesMock = getCompaniesMock();
|
||||
const duplicateCompanyMock = getCompanyDuplicateMock();
|
||||
|
||||
|
||||
@ -1,50 +1,7 @@
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
|
||||
export const getCompaniesMock = () => {
|
||||
return companiesQueryResult.companies.edges.map((edge) => edge.node);
|
||||
};
|
||||
|
||||
export const getCompanyObjectMetadataItem = () => {
|
||||
const companyObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'company',
|
||||
);
|
||||
|
||||
if (!companyObjectMetadataItem) {
|
||||
throw new Error('Company object metadata item not found');
|
||||
}
|
||||
|
||||
return companyObjectMetadataItem;
|
||||
};
|
||||
export const getCompanyDuplicateMock = () => {
|
||||
return {
|
||||
...companiesQueryResult.companies.edges[0].node,
|
||||
id: '8b40856a-2ec9-4c03-8bc0-c032c89e1824',
|
||||
};
|
||||
};
|
||||
|
||||
export const getEmptyCompanyMock = () => {
|
||||
return {
|
||||
id: '9231e6ee-4cc2-4c7b-8c55-dff16f4d968a',
|
||||
name: '',
|
||||
domainName: {
|
||||
__typename: 'Links',
|
||||
primaryLinkUrl: '',
|
||||
primaryLinkLabel: '',
|
||||
secondaryLinks: [],
|
||||
},
|
||||
address: {},
|
||||
accountOwner: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
employees: null,
|
||||
idealCustomerProfile: null,
|
||||
linkedinLink: null,
|
||||
xLink: null,
|
||||
_activityCount: null,
|
||||
__typename: 'Company',
|
||||
};
|
||||
};
|
||||
|
||||
export const companiesQueryResult = {
|
||||
companies: {
|
||||
__typename: 'CompanyConnection',
|
||||
@ -774,3 +731,75 @@ export const companiesQueryResult = {
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const allMockedCompanyRecords = companiesQueryResult.companies.edges.map(
|
||||
(edge) => edge.node,
|
||||
);
|
||||
export const getCompaniesMock = () => {
|
||||
return [...allMockedCompanyRecords];
|
||||
};
|
||||
|
||||
export const getMockCompanyObjectMetadataItem = () => {
|
||||
const companyObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'company',
|
||||
);
|
||||
|
||||
if (!companyObjectMetadataItem) {
|
||||
throw new Error('Company object metadata item not found');
|
||||
}
|
||||
|
||||
return companyObjectMetadataItem;
|
||||
};
|
||||
export const getCompanyDuplicateMock = () => {
|
||||
return {
|
||||
...companiesQueryResult.companies.edges[0].node,
|
||||
id: '8b40856a-2ec9-4c03-8bc0-c032c89e1824',
|
||||
};
|
||||
};
|
||||
|
||||
export const getMockCompanyRecord = (
|
||||
overrides?: Partial<ObjectRecord>,
|
||||
index = 0,
|
||||
) => {
|
||||
return {
|
||||
...allMockedCompanyRecords[index],
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
export const findMockCompanyRecord = ({
|
||||
id: queriedCompanyId,
|
||||
}: Pick<ObjectRecord, 'id'>) => {
|
||||
const company = allMockedCompanyRecords.find(
|
||||
({ id: currentCompanyId }) => currentCompanyId === queriedCompanyId,
|
||||
);
|
||||
|
||||
if (!isDefined(company)) {
|
||||
throw new Error(`Could not find company with id, ${queriedCompanyId}`);
|
||||
}
|
||||
|
||||
return company;
|
||||
};
|
||||
|
||||
export const getEmptyCompanyMock = () => {
|
||||
return {
|
||||
id: '9231e6ee-4cc2-4c7b-8c55-dff16f4d968a',
|
||||
name: '',
|
||||
domainName: {
|
||||
__typename: 'Links',
|
||||
primaryLinkUrl: '',
|
||||
primaryLinkLabel: '',
|
||||
secondaryLinks: [],
|
||||
},
|
||||
address: {},
|
||||
accountOwner: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
employees: null,
|
||||
idealCustomerProfile: null,
|
||||
linkedinLink: null,
|
||||
xLink: null,
|
||||
_activityCount: null,
|
||||
__typename: 'Company',
|
||||
};
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,73 +1,9 @@
|
||||
import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode';
|
||||
import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { FieldMetadataType } from 'twenty-shared';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
|
||||
export const getPeopleMock = (): ObjectRecord[] => {
|
||||
const peopleMock = peopleQueryResult.people.edges.map((edge) => edge.node);
|
||||
|
||||
return peopleMock;
|
||||
};
|
||||
|
||||
export const getPersonObjectMetadataItem = () => {
|
||||
const personObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'person',
|
||||
);
|
||||
|
||||
if (!personObjectMetadataItem) {
|
||||
throw new Error('Person object metadata item not found');
|
||||
}
|
||||
|
||||
return personObjectMetadataItem;
|
||||
};
|
||||
|
||||
export const getPersonFieldMetadataItem = (
|
||||
fieldMetadataType: FieldMetadataType,
|
||||
objectMetadataItem = getPersonObjectMetadataItem(),
|
||||
) => {
|
||||
const result = objectMetadataItem.fields.find(
|
||||
(field) => field.type === fieldMetadataType,
|
||||
);
|
||||
if (!result) {
|
||||
throw new Error(
|
||||
`Person fieldmetadata item type ${fieldMetadataType} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getPersonRecord = (
|
||||
overrides?: Partial<ObjectRecord>,
|
||||
index = 0,
|
||||
) => {
|
||||
const personRecords = getPeopleMock();
|
||||
return {
|
||||
...personRecords[index],
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
export const mockedEmptyPersonData = {
|
||||
id: 'ce7f0a37-88d7-4cd8-8b41-6721c57195b5',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phone: null,
|
||||
email: null,
|
||||
city: null,
|
||||
createdBy: null,
|
||||
displayName: null,
|
||||
avatarUrl: null,
|
||||
createdAt: null,
|
||||
jobTitle: null,
|
||||
linkedinUrl: null,
|
||||
xUrl: null,
|
||||
_activityCount: null,
|
||||
company: null,
|
||||
deletedAt: null,
|
||||
__typename: 'Person',
|
||||
};
|
||||
|
||||
export const peopleQueryResult = {
|
||||
people: {
|
||||
__typename: 'PersonConnection',
|
||||
@ -1762,3 +1698,71 @@ export const peopleQueryResult = {
|
||||
],
|
||||
},
|
||||
} satisfies { people: RecordGqlConnection };
|
||||
|
||||
export const allMockPersonRecords = peopleQueryResult.people.edges.map((edge) =>
|
||||
getRecordFromRecordNode({ recordNode: edge.node }),
|
||||
);
|
||||
|
||||
export const getPeopleRecordConnectionMock = () => {
|
||||
const peopleMock = peopleQueryResult.people.edges.map((edge) => edge.node);
|
||||
|
||||
return peopleMock;
|
||||
};
|
||||
|
||||
export const getMockPersonObjectMetadataItem = () => {
|
||||
const personObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'person',
|
||||
);
|
||||
|
||||
if (!personObjectMetadataItem) {
|
||||
throw new Error('Person object metadata item not found');
|
||||
}
|
||||
|
||||
return personObjectMetadataItem;
|
||||
};
|
||||
|
||||
export const getMockPersonFieldMetadataItem = (
|
||||
fieldMetadataType: FieldMetadataType,
|
||||
objectMetadataItem = getMockPersonObjectMetadataItem(),
|
||||
) => {
|
||||
const result = objectMetadataItem.fields.find(
|
||||
(field) => field.type === fieldMetadataType,
|
||||
);
|
||||
if (!result) {
|
||||
throw new Error(
|
||||
`Person fieldmetadata item type ${fieldMetadataType} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getMockPersonRecord = (
|
||||
overrides?: Partial<ObjectRecord>,
|
||||
index = 0,
|
||||
) => {
|
||||
return {
|
||||
...allMockPersonRecords[index],
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
export const mockedEmptyPersonData = {
|
||||
id: 'ce7f0a37-88d7-4cd8-8b41-6721c57195b5',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phone: null,
|
||||
email: null,
|
||||
city: null,
|
||||
createdBy: null,
|
||||
displayName: null,
|
||||
avatarUrl: null,
|
||||
createdAt: null,
|
||||
jobTitle: null,
|
||||
linkedinUrl: null,
|
||||
xUrl: null,
|
||||
_activityCount: null,
|
||||
company: null,
|
||||
deletedAt: null,
|
||||
__typename: 'Person',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user