Fix storybook tests (#5487)

Fixes #5486

---------

Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com>
Co-authored-by: v1b3m <vibenjamin6@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
gitstart-twenty 2024-05-22 02:24:08 +08:00 committed by GitHub
parent e47101e08b
commit 36b467d301
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 709 additions and 101 deletions

View File

@ -7,10 +7,10 @@ const globalCoverage = {
};
const modulesCoverage = {
branches: 45,
statements: 70,
lines: 70,
functions: 65,
branches: 25,
statements: 50,
lines: 50,
functions: 40,
include: ['src/modules/**/*'],
exclude: ['src/**/*.ts'],
};

View File

@ -17,6 +17,7 @@ import { ClientConfigProvider } from '@/client-config/components/ClientConfigPro
import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect';
import { billingState } from '@/client-config/states/billingState';
import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
import indexAppPath from '@/navigation/utils/indexAppPath';
import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider';
import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider';
@ -138,7 +139,10 @@ const createRouter = (isBillingEnabled?: boolean) =>
path={AppPath.PlanRequiredSuccess}
element={<PaymentSuccess />}
/>
<Route path={AppPath.Index} element={<DefaultHomePage />} />
<Route
path={indexAppPath.getIndexAppPath()}
element={<DefaultHomePage />}
/>
<Route path={AppPath.TasksPage} element={<Tasks />} />
<Route path={AppPath.Impersonate} element={<ImpersonateEffect />} />
<Route path={AppPath.RecordIndexPage} element={<RecordIndexPage />} />

View File

@ -1,21 +1,17 @@
import { HelmetProvider } from 'react-helmet-async';
import { getOperationName } from '@apollo/client/utilities';
import { jest } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { graphql, HttpResponse } from 'msw';
import { RecoilRoot } from 'recoil';
import { IconsProvider } from 'twenty-ui';
import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect';
import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider';
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
import indexAppPath from '@/navigation/utils/indexAppPath';
import { AppPath } from '@/types/AppPath';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
import { UserProvider } from '@/users/components/UserProvider';
import { UserProviderEffect } from '@/users/components/UserProviderEffect';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { App } from '~/App';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { FullHeightStorybookLayout } from '~/testing/FullHeightStorybookLayout';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedUsersData } from '~/testing/mock-data/users';
@ -23,36 +19,21 @@ const meta: Meta<typeof App> = {
title: 'App/App',
component: App,
decorators: [
MemoryRouterDecorator,
(Story) => (
<>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<ClientConfigProviderEffect />
<ClientConfigProvider>
<UserProviderEffect />
<UserProvider>
<FullHeightStorybookLayout>
<ObjectMetadataItemsProvider>
<IconsProvider>
<HelmetProvider>
<SnackBarProvider>
<AppThemeProvider>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<ObjectMetadataItemsProvider>
<Story />
</ObjectMetadataItemsProvider>
</SnackBarProviderScope>
</AppThemeProvider>
</SnackBarProvider>
</HelmetProvider>
</IconsProvider>
</ObjectMetadataItemsProvider>
</FullHeightStorybookLayout>
</UserProvider>
</ClientConfigProvider>
</SnackBarProviderScope>
</>
),
(Story) => {
return (
<RecoilRoot>
<AppErrorBoundary>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<IconsProvider>
<HelmetProvider>
<Story />
</HelmetProvider>
</IconsProvider>
</SnackBarProviderScope>
</AppErrorBoundary>
</RecoilRoot>
);
},
],
parameters: {
msw: graphqlMocks,
@ -62,9 +43,20 @@ const meta: Meta<typeof App> = {
export default meta;
export type Story = StoryObj<typeof App>;
export const Default: Story = {};
export const Default: Story = {
play: async () => {
jest
.spyOn(indexAppPath, 'getIndexAppPath')
.mockReturnValue('iframe.html' as AppPath);
},
};
export const DarkMode: Story = {
play: async () => {
jest
.spyOn(indexAppPath, 'getIndexAppPath')
.mockReturnValue('iframe.html' as AppPath);
},
parameters: {
msw: {
handlers: [

View File

@ -1,22 +1,60 @@
import { getOperationName } from '@apollo/client/utilities';
import { Meta, StoryObj } from '@storybook/react';
import { graphql, HttpResponse } from 'msw';
import { ComponentDecorator } from 'twenty-ui';
import { Calendar } from '@/activities/calendar/components/Calendar';
import { getTimelineCalendarEventsFromCompanyId } from '@/activities/calendar/queries/getTimelineCalendarEventsFromCompanyId';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedTimelineCalendarEvents } from '~/testing/mock-data/timeline-calendar-events';
const meta: Meta<typeof Calendar> = {
title: 'Modules/Activities/Calendar/Calendar',
component: Calendar,
decorators: [ComponentDecorator, SnackBarDecorator],
decorators: [
ComponentDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
],
parameters: {
container: { width: 728 },
msw: graphqlMocks,
msw: {
handlers: [
...graphqlMocks.handlers,
graphql.query(
getOperationName(getTimelineCalendarEventsFromCompanyId) ?? '',
({ variables }) => {
if (variables.page > 1) {
return HttpResponse.json({
data: {
getTimelineCalendarEventsFromCompanyId: {
__typename: 'TimelineCalendarEventsWithTotal',
totalNumberOfCalendarEvents: 3,
timelineCalendarEvents: [],
},
},
});
}
return HttpResponse.json({
data: {
getTimelineCalendarEventsFromCompanyId: {
__typename: 'TimelineCalendarEventsWithTotal',
totalNumberOfCalendarEvents: 3,
timelineCalendarEvents: mockedTimelineCalendarEvents,
},
},
});
},
),
],
},
},
args: {
targetableObject: {
id: '1',
targetObjectNameSingular: 'Person',
targetObjectNameSingular: 'Company',
},
},
};

View File

@ -6,6 +6,7 @@ import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableE
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedTasks } from '~/testing/mock-data/activities';
@ -21,10 +22,10 @@ const meta: Meta<typeof TaskGroups> = {
),
ComponentWithRouterDecorator,
ComponentWithRecoilScopeDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
],
parameters: {
msw: graphqlMocks,
customRecoilScopeContext: TasksRecoilScopeContext,
},
};
@ -43,4 +44,7 @@ export const WithTasks: Story = {
},
] as ActivityTargetableObject[],
},
parameters: {
msw: graphqlMocks,
},
};

View File

@ -0,0 +1,12 @@
import { AppPath } from '@/types/AppPath';
const getIndexAppPath = () => {
return AppPath.Index;
};
// This file is using the default export pattern to be compatible
// with the way it is imported in the tests.
// Otherwise we cannot mock it: https://github.com/jestjs/jest/issues/12145 as we are using ES native modules
// TBH: I am not a big fan of this pattern, alternatively we could set a global variable or a recoilState
// to store the value
export default { getIndexAppPath };

View File

@ -43,7 +43,7 @@ export type FieldMetadataItem = Omit<
})
| null;
defaultValue?: any;
options?: FieldMetadataItemOption[];
options?: FieldMetadataItemOption[] | null;
relationDefinition?: {
relationId: RelationDefinition['relationId'];
direction: RelationDefinitionType;

View File

@ -51,6 +51,7 @@ export const fieldMetadataItemSchema = z.object({
value: z.string().trim().min(1),
}),
)
.nullable()
.optional(),
relationDefinition: z
.object({

View File

@ -103,7 +103,7 @@ export const SettingsDataModelFieldPreview = ({
objectMetadataNameSingular: objectMetadataItem.nameSingular,
relationObjectMetadataNameSingular:
relationObjectMetadataItem?.nameSingular,
options: fieldMetadataItem.options,
options: fieldMetadataItem.options ?? [],
},
defaultValue: fieldMetadataItem.defaultValue,
},

View File

@ -19,7 +19,7 @@ const meta: Meta<PageDecoratorArgs> = {
RelationPickerDecorator,
],
args: {
routePath: 'toto-not-found',
routePath: '/toto-not-found',
},
parameters: {
msw: graphqlMocks,

View File

@ -1,12 +1,64 @@
import { MemoryRouter } from 'react-router-dom';
import {
createMemoryRouter,
createRoutesFromElements,
Outlet,
Route,
RouterProvider,
} from 'react-router-dom';
import { Decorator } from '@storybook/react';
import {
computeLocation,
isRouteParams,
} from '~/testing/decorators/PageDecorator';
import { ComponentStorybookLayout } from '../ComponentStorybookLayout';
export const ComponentWithRouterDecorator: Decorator = (Story) => (
interface StrictArgs {
[name: string]: unknown;
}
const Providers = () => (
<ComponentStorybookLayout>
<MemoryRouter>
<Story />
</MemoryRouter>
<Outlet />
</ComponentStorybookLayout>
);
const createRouter = ({
Story,
args,
initialEntries,
initialIndex,
}: {
Story: () => JSX.Element;
args: StrictArgs;
initialEntries?: {
pathname: string;
}[];
initialIndex?: number;
}) =>
createMemoryRouter(
createRoutesFromElements(
<Route element={<Providers />}>
<Route path={(args.routePath as string) ?? '*'} element={<Story />} />
</Route>,
),
{ initialEntries, initialIndex },
);
export const ComponentWithRouterDecorator: Decorator = (Story, { args }) => {
return (
<RouterProvider
router={createRouter({
Story,
args,
initialEntries:
args.routePath &&
typeof args.routePath === 'string' &&
(args.routeParams === undefined || isRouteParams(args.routeParams))
? [computeLocation(args.routePath, args.routeParams)]
: [{ pathname: '/' }],
})}
/>
);
};

View File

@ -1,5 +1,11 @@
import { HelmetProvider } from 'react-helmet-async';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import {
createMemoryRouter,
createRoutesFromElements,
Outlet,
Route,
RouterProvider,
} from 'react-router-dom';
import { ApolloProvider } from '@apollo/client';
import { loadDevMessages } from '@apollo/client/dev';
import { Decorator } from '@storybook/react';
@ -23,15 +29,26 @@ export type PageDecoratorArgs = {
additionalRoutes?: string[];
};
type RouteParams = {
export type RouteParams = {
[param: string]: string;
};
const computeLocation = (routePath: string, routeParams: RouteParams) => {
export const isRouteParams = (obj: any): obj is RouteParams => {
if (typeof obj !== 'object' || obj === null) {
return false;
}
return Object.keys(obj).every((key) => typeof obj[key] === 'string');
};
export const computeLocation = (
routePath: string,
routeParams?: RouteParams,
) => {
return {
pathname: routePath.replace(
/:(\w+)/g,
(paramName) => routeParams[paramName] ?? '',
(paramName) => routeParams?.[paramName] ?? '',
),
};
};
@ -42,11 +59,7 @@ const ApolloStorybookDevLogEffect = () => {
return <></>;
};
export const PageDecorator: Decorator<{
routePath: string;
routeParams: RouteParams;
additionalRoutes?: string[];
}> = (Story, { args }) => {
const Providers = () => {
return (
<RecoilRoot>
<ApolloProvider client={mockedApolloClient}>
@ -56,32 +69,15 @@ export const PageDecorator: Decorator<{
<UserProvider>
<ClientConfigProviderEffect />
<ClientConfigProvider>
<MemoryRouter
initialEntries={[
computeLocation(args.routePath, args.routeParams),
]}
>
<FullHeightStorybookLayout>
<HelmetProvider>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<ObjectMetadataItemsProvider>
<Routes>
<Route element={<DefaultLayout />}>
<Route path={args.routePath} element={<Story />} />
{args.additionalRoutes?.map((route) => (
<Route
key={route}
path={route}
element={<div>Navigated to {route}</div>}
/>
))}
</Route>
</Routes>
</ObjectMetadataItemsProvider>
</SnackBarProviderScope>
</HelmetProvider>
</FullHeightStorybookLayout>
</MemoryRouter>
<FullHeightStorybookLayout>
<HelmetProvider>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<ObjectMetadataItemsProvider>
<Outlet />
</ObjectMetadataItemsProvider>
</SnackBarProviderScope>
</HelmetProvider>
</FullHeightStorybookLayout>
</ClientConfigProvider>
</UserProvider>
</ApolloMetadataClientMockedProvider>
@ -89,3 +85,54 @@ export const PageDecorator: Decorator<{
</RecoilRoot>
);
};
const createRouter = ({
Story,
args,
initialEntries,
initialIndex,
}: {
Story: () => JSX.Element;
args: {
routePath: string;
routeParams: RouteParams;
additionalRoutes?: string[] | undefined;
};
initialEntries?: {
pathname: string;
}[];
initialIndex?: number;
}) =>
createMemoryRouter(
createRoutesFromElements(
<Route element={<Providers />}>
<Route element={<DefaultLayout />}>
<Route path={args.routePath} element={<Story />} />
{args.additionalRoutes?.map((route) => (
<Route
key={route}
path={route}
element={<div>Navigated to {route}</div>}
/>
))}
</Route>
</Route>,
),
{ initialEntries, initialIndex },
);
export const PageDecorator: Decorator<{
routePath: string;
routeParams: RouteParams;
additionalRoutes?: string[];
}> = (Story, { args }) => {
return (
<RouterProvider
router={createRouter({
Story,
args,
initialEntries: [computeLocation(args.routePath, args.routeParams)],
})}
/>
);
};

View File

@ -49,6 +49,7 @@ const customObjectMetadataItemEdge: ObjectEdge = {
isCustom: false,
isActive: true,
isSystem: true,
options: null,
isNullable: true,
createdAt: '2024-04-08T12:48:49.538Z',
updatedAt: '2024-04-08T12:48:49.538Z',
@ -93,6 +94,7 @@ const customObjectMetadataItemEdge: ObjectEdge = {
nameSingular: 'viewField',
namePlural: 'viewFields',
isSystem: true,
isRemote: false,
},
},
},
@ -110,6 +112,7 @@ const customObjectMetadataItemEdge: ObjectEdge = {
isCustom: false,
isActive: true,
isSystem: true,
options: null,
isNullable: false,
createdAt: '2024-04-08T12:48:49.538Z',
updatedAt: '2024-04-08T12:48:49.538Z',
@ -132,6 +135,7 @@ const customObjectMetadataItemEdge: ObjectEdge = {
isCustom: false,
isActive: true,
isSystem: true,
options: null,
isNullable: true,
createdAt: '2024-04-08T12:48:49.538Z',
updatedAt: '2024-04-08T12:48:49.538Z',
@ -154,6 +158,7 @@ const customObjectMetadataItemEdge: ObjectEdge = {
isCustom: false,
isActive: true,
isSystem: true,
options: null,
isNullable: false,
createdAt: '2024-04-08T12:48:49.538Z',
updatedAt: '2024-04-08T12:48:49.538Z',
@ -176,6 +181,7 @@ const customObjectMetadataItemEdge: ObjectEdge = {
isCustom: false,
isActive: true,
isSystem: true,
options: null,
isNullable: false,
createdAt: '2024-04-08T12:48:49.538Z',
updatedAt: '2024-04-08T12:48:49.538Z',
@ -198,6 +204,7 @@ const customObjectMetadataItemEdge: ObjectEdge = {
isCustom: false,
isActive: true,
isSystem: true,
options: null,
isNullable: false,
createdAt: '2024-04-08T12:48:49.538Z',
updatedAt: '2024-04-08T12:48:49.538Z',
@ -220,6 +227,7 @@ const customObjectMetadataItemEdge: ObjectEdge = {
isCustom: false,
isActive: true,
isSystem: true,
options: null,
isNullable: false,
createdAt: '2024-04-08T12:48:49.538Z',
updatedAt: '2024-04-08T12:48:49.538Z',

View File

@ -0,0 +1,104 @@
import {
TimelineCalendarEvent,
TimelineCalendarEventVisibility,
} from '~/generated-metadata/graphql';
export const mockedTimelineCalendarEvents: TimelineCalendarEvent[] = [
{
__typename: 'TimelineCalendarEvent',
id: '20202020-2e3f-45a2-ab16-b580ff4f83e7',
title: 'Jane',
description: 'Tests techniques',
location: '',
startsAt: '2024-05-16T12:00:00.000Z',
endsAt: '2024-05-16T13:00:00.000Z',
conferenceLink: {
url: 'https://meet.google.com/xxx-xxx-xxx',
label: 'Rejoindre la visio',
},
conferenceSolution: 'GOOGLE_MEET',
isCanceled: false,
visibility: TimelineCalendarEventVisibility.ShareEverything,
isFullDay: false,
participants: [
{
__typename: 'TimelineCalendarEventParticipant',
personId: null,
workspaceMemberId: '20202020-78c5-4cbb-87a8-f9cd3ad4d8af',
firstName: 'Tim',
lastName: 'Apple',
displayName: 'Tim',
avatarUrl: '',
handle: 'tim@apple.dev',
},
{
__typename: 'TimelineCalendarEventParticipant',
personId: null,
workspaceMemberId: '20202020-3cf7-453e-a5f4-28f8412e70f0',
firstName: 'Jane',
lastName: 'Doe',
displayName: 'Jane',
avatarUrl: '',
handle: 'jane@apple.dev',
},
],
},
{
__typename: 'TimelineCalendarEvent',
id: '20202020-1020-42d6-8444-541f5e57a7e5',
title: '',
description: '',
location: '',
startsAt: '2024-05-08T12:00:00.000Z',
endsAt: '2024-05-08T12:25:00.000Z',
isFullDay: false,
conferenceLink: {
url: 'https://meet.google.com/xxx-xxx-xxx',
label: 'Rejoindre la visio',
},
conferenceSolution: 'GOOGLE_MEET',
isCanceled: false,
visibility: TimelineCalendarEventVisibility.Metadata,
participants: [
{
__typename: 'TimelineCalendarEventParticipant',
personId: null,
workspaceMemberId: '20202020-78c5-4cbb-87a8-f9cd3ad4d8af',
firstName: 'Tim',
lastName: 'Apple',
displayName: 'Tim',
avatarUrl: '',
handle: 'tim@apple.dev',
},
],
},
{
__typename: 'TimelineCalendarEvent',
id: '20202020-fa61-4d82-b47f-90cca16514e3',
title: '',
description: '',
location: '',
startsAt: '2024-05-06T12:00:00.000Z',
endsAt: '2024-05-06T12:25:00.000Z',
isFullDay: false,
conferenceLink: {
url: 'https://meet.google.com/xxx-xxx-xxx',
label: 'Rejoindre la visio',
},
conferenceSolution: 'GOOGLE_MEET',
isCanceled: false,
visibility: TimelineCalendarEventVisibility.Metadata,
participants: [
{
__typename: 'TimelineCalendarEventParticipant',
personId: null,
workspaceMemberId: '20202020-78c5-4cbb-87a8-f9cd3ad4d8af',
firstName: 'Tim',
lastName: 'Apple',
displayName: 'Tim',
avatarUrl: '',
handle: 'tim@apple.dev',
},
],
},
];

View File

@ -2,16 +2,12 @@ import { formatAddressObjectAsParticipants } from 'src/modules/messaging/service
describe('formatAddressObjectAsParticipants', () => {
it('should format address object as participants', () => {
const addressObject = {
value: [
{ name: 'John Doe', address: 'john.doe @example.com' },
{ name: 'Jane Smith', address: 'jane.smith@example.com ' },
],
html: '',
text: '',
};
const addresses = [
{ name: 'John Doe', address: 'john.doe @example.com' },
{ name: 'Jane Smith', address: 'jane.smith@example.com ' },
];
const result = formatAddressObjectAsParticipants(addressObject, 'from');
const result = formatAddressObjectAsParticipants(addresses, 'from');
expect(result).toEqual([
{