Fix storybook / chromatic tests flakyness and integration tests (#11687)

## Storybook flakyness

### Actor Display image flakyness

<img width="1512" alt="image"
src="https://github.com/user-attachments/assets/875c0738-5e31-4aba-9231-4ba5f78d1355"
/>

**Fix:** stop using a random usage

### Task Groups broken

<img width="1512" alt="image"
src="https://github.com/user-attachments/assets/c67e47a1-a027-43f1-9601-68d61a8052b4"
/>

**Fix:** add missing TabListComponentInstance

## Flaky dates

Add https://github.com/k35o/storybook-addon-mock-date

## Integration tests

Fix broken tests due to relation refactoring
This commit is contained in:
Charles Bochet
2025-04-23 01:57:36 +02:00
committed by GitHub
parent 8694840b92
commit fa5f758228
20 changed files with 153 additions and 155 deletions

View File

@ -183,6 +183,7 @@
"semver": "^7.5.4",
"sharp": "^0.32.1",
"slash": "^5.1.0",
"storybook-addon-mock-date": "^0.6.0",
"stripe": "^17.3.1",
"ts-key-enum": "^2.0.12",
"tslib": "^2.3.0",

View File

@ -48,6 +48,7 @@ const config: StorybookConfig = {
'storybook-dark-mode',
'storybook-addon-cookie',
'storybook-addon-pseudo-states',
'storybook-addon-mock-date'
],
framework: {
name: '@storybook/react-vite',

View File

@ -62,6 +62,7 @@ const preview: Preview = {
date: /Date$/,
},
},
mockingDate: new Date('2024-03-12T09:30:00.000Z'),
options: {
storySort: {
order: ['UI', 'Modules', 'Pages'],

View File

@ -12,11 +12,11 @@ const StyledContainer = styled.div`
overflow: auto;
`;
export const ObjectTasks = ({
targetableObject,
}: {
type ObjectTasksProps = {
targetableObject: ActivityTargetableObject;
}) => {
};
export const ObjectTasks = ({ targetableObject }: ObjectTasksProps) => {
return (
<StyledContainer>
<ObjectFilterDropdownComponentInstanceContext.Provider

View File

@ -3,6 +3,7 @@ import { Meta, StoryObj } from '@storybook/react';
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
import { TabListComponentInstanceContext } from '@/ui/layout/tab/states/contexts/TabListComponentInstanceContext';
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
@ -15,11 +16,15 @@ const meta: Meta<typeof TaskGroups> = {
component: TaskGroups,
decorators: [
(Story) => (
<TabListComponentInstanceContext.Provider
value={{ instanceId: 'entity-tasks-filter-scope' }}
>
<ObjectFilterDropdownComponentInstanceContext.Provider
value={{ instanceId: 'entity-tasks-filter-scope' }}
>
<Story />
</ObjectFilterDropdownComponentInstanceContext.Provider>
</TabListComponentInstanceContext.Provider>
),
ComponentWithRouterDecorator,
ComponentWithRecoilScopeDecorator,

View File

@ -1,20 +1,11 @@
import { ActionConfig } from '@/action-menu/actions/types/ActionConfig';
import { getActionLabel } from '@/action-menu/utils/getActionLabel';
import { isNonEmptyString } from '@sniptt/guards';
import { useDebounce } from 'use-debounce';
export const useFilterActionsWithCommandMenuSearch = ({
commandMenuSearch,
}: {
commandMenuSearch: string;
}) => {
const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300); // 200ms - 500ms
import { useCallback } from 'react';
const checkInShortcuts = (action: ActionConfig, search: string) => {
const concatenatedString = action.hotKeys?.join('') ?? '';
return concatenatedString
.toLowerCase()
.includes(search.toLowerCase().trim());
return concatenatedString.toLowerCase().includes(search.toLowerCase().trim());
};
const checkInLabels = (action: ActionConfig, search: string) => {
@ -25,14 +16,24 @@ export const useFilterActionsWithCommandMenuSearch = ({
return false;
};
const filterActionsWithCommandMenuSearch = (actions: ActionConfig[]) => {
type UseFilterActionsWithCommandMenuSearchProps = {
commandMenuSearch: string;
};
export const useFilterActionsWithCommandMenuSearch = ({
commandMenuSearch,
}: UseFilterActionsWithCommandMenuSearchProps) => {
const filterActionsWithCommandMenuSearch = useCallback(
(actions: ActionConfig[]) => {
return actions.filter((action) =>
deferredCommandMenuSearch.length > 0
? checkInShortcuts(action, deferredCommandMenuSearch) ||
checkInLabels(action, deferredCommandMenuSearch)
commandMenuSearch.length > 0
? checkInShortcuts(action, commandMenuSearch) ||
checkInLabels(action, commandMenuSearch)
: true,
);
};
},
[commandMenuSearch],
);
return {
filterActionsWithCommandMenuSearch,

View File

@ -4,6 +4,7 @@ import { Field, FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldMetadataItem } from '../types/FieldMetadataItem';
import { formatFieldMetadataItemInput } from '../utils/formatFieldMetadataItemInput';
import { isDefined } from 'twenty-shared/utils';
import { useCreateOneFieldMetadataItem } from './useCreateOneFieldMetadataItem';
import { useDeleteOneFieldMetadataItem } from './useDeleteOneFieldMetadataItem';
import { useUpdateOneFieldMetadataItem } from './useUpdateOneFieldMetadataItem';
@ -63,7 +64,8 @@ export const useFieldMetadataItem = () => {
});
const deleteMetadataField = (metadataField: FieldMetadataItem) => {
return metadataField.type === FieldMetadataType.RELATION
return metadataField.type === FieldMetadataType.RELATION &&
!isDefined(metadataField.settings?.relationType)
? deleteOneRelationMetadataItem(
metadataField.relationDefinition?.relationId,
)

View File

@ -15,16 +15,16 @@ import { tableColumnsComponentState } from '@/object-record/record-table/states/
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { within } from '@storybook/test';
import {
ComponentDecorator,
getCanvasElementForDropdownTesting,
} from 'twenty-ui/testing';
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import {
ComponentDecorator,
getCanvasElementForDropdownTesting,
} from 'twenty-ui/testing';
const meta: Meta<typeof MultipleFiltersDropdownButton> = {
title:

View File

@ -37,7 +37,7 @@ export const Catalog: Story = {
},
{
name: 'avatarUrl',
values: [null, 'https://picsum.photos/16'],
values: [null, 'https://picsum.photos/id/237/16/16'],
props: (avatarUrl: string) => ({ avatarUrl }),
},
],

View File

@ -9,8 +9,6 @@ import { graphqlMocks } from '~/testing/graphqlMocks';
import { userEvent, within } from '@storybook/test';
import { SettingsExperience } from '../profile/appearance/components/SettingsExperience';
Date.now = () => new Date('2022-06-13T12:33:37.000Z').getTime();
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/SettingsExperience',
component: SettingsExperience,
@ -44,15 +42,15 @@ export const DateTimeSettingsTimeFormat: Story = {
await canvas.findByText('Date and time');
const timeFormatSelect = await canvas.findByText('24h (08:33)');
const timeFormatSelect = await canvas.findByText('24h (05:30)');
await userEvent.click(timeFormatSelect);
const timeFormatOptions = await canvas.findByText('12h (8:33 AM)');
const timeFormatOptions = await canvas.findByText('12h (5:30 AM)');
await userEvent.click(timeFormatOptions);
await canvas.findByText('12h (8:33 AM)');
await canvas.findByText('12h (5:30 AM)');
},
};
@ -84,14 +82,14 @@ export const DateTimeSettingsDateFormat: Story = {
await canvas.findByText('Date and time');
const timeFormatSelect = await canvas.findByText('13 Jun, 2022');
const timeFormatSelect = await canvas.findByText('12 Mar, 2024');
await userEvent.click(timeFormatSelect);
const timeFormatOptions = await canvas.findByText('Jun 13, 2022');
const timeFormatOptions = await canvas.findByText('Mar 12, 2024');
await userEvent.click(timeFormatOptions);
await canvas.findByText('Jun 13, 2022');
await canvas.findByText('Mar 12, 2024');
},
};

View File

@ -37,11 +37,6 @@ export class WorkspaceSchemaFactory {
return new GraphQLSchema({});
}
const cachedIsNewRelationEnabled =
await this.workspaceCacheStorageService.getIsNewRelationEnabled(
authContext.workspace.id,
);
const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
authContext.workspace.id,
@ -79,35 +74,6 @@ export class WorkspaceSchemaFactory {
);
}
// TODO: remove this after the feature flag is droped
if (
(isNewRelationEnabled && cachedIsNewRelationEnabled === undefined) ||
(isNewRelationEnabled !== cachedIsNewRelationEnabled &&
cachedIsNewRelationEnabled !== undefined)
) {
// eslint-disable-next-line no-console
console.log(
chalk.yellow('Recomputing due to new relation feature flag'),
{
isNewRelationEnabled,
},
);
await this.workspaceCacheStorageService.setIsNewRelationEnabled(
authContext.workspace.id,
isNewRelationEnabled,
);
await this.workspaceMetadataCacheService.recomputeMetadataCache({
workspaceId: authContext.workspace.id,
});
throw new GraphqlQueryRunnerException(
'Metadata cache recomputation required due to relation feature flag change',
GraphqlQueryRunnerExceptionCode.METADATA_CACHE_FEATURE_FLAG_RECOMPUTATION_REQUIRED,
);
}
const objectMetadataMaps =
await this.workspaceCacheStorageService.getObjectMetadataMaps(
authContext.workspace.id,

View File

@ -24,6 +24,7 @@ import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metada
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
@ -41,7 +42,7 @@ import { UpdateFieldInput } from './dtos/update-field.input';
NestjsQueryGraphQLModule.forFeature({
imports: [
NestjsQueryTypeOrmModule.forFeature(
[FieldMetadataEntity, ObjectMetadataEntity],
[FieldMetadataEntity, ObjectMetadataEntity, RelationMetadataEntity],
'metadata',
),
WorkspaceMigrationModule,

View File

@ -161,12 +161,6 @@ export class FieldMetadataResolver {
throw new ValidationError("Active fields can't be deleted");
}
if (fieldMetadata.type === FieldMetadataType.RELATION) {
throw new ValidationError(
"Relation fields can't be deleted, you need to delete the RelationMetadata instead",
);
}
try {
return await this.fieldMetadataService.deleteOneField(input, workspaceId);
} catch (error) {

View File

@ -10,6 +10,8 @@ import { isDefined } from 'twenty-shared/utils';
import { DataSource, FindOneOptions, In, Repository } from 'typeorm';
import { v4 as uuidV4, v4 } from 'uuid';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { settings } from 'src/engine/constants/settings';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
@ -81,6 +83,8 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(RelationMetadataEntity, 'metadata')
private readonly relationMetadataRepository: Repository<RelationMetadataEntity>,
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
@ -350,9 +354,48 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
fieldMetadataId: fieldMetadata.id,
});
await fieldMetadataRepository.delete(fieldMetadata.id);
if (fieldMetadata.type === FieldMetadataType.RELATION) {
const isManyToManyRelation =
(fieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>)
.settings?.relationType === RelationType.MANY_TO_ONE;
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
const targetFieldMetadata =
await this.fieldMetadataRepository.findOneBy({
id: fieldMetadata.relationTargetFieldMetadataId,
});
if (targetFieldMetadata) {
await this.relationMetadataRepository.delete({
fromFieldMetadataId: In([fieldMetadata.id, targetFieldMetadata.id]),
});
await this.relationMetadataRepository.delete({
toFieldMetadataId: In([fieldMetadata.id, targetFieldMetadata.id]),
});
await fieldMetadataRepository.delete({
id: In([fieldMetadata.id, targetFieldMetadata.id]),
});
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`delete-${fieldMetadata.name}`),
workspaceId,
[
{
name: computeObjectTargetTable(objectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP,
columnName: isManyToManyRelation
? `${(fieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>).settings?.joinColumnName}`
: `${(targetFieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>).settings?.joinColumnName}`,
} satisfies WorkspaceMigrationColumnDrop,
],
} satisfies WorkspaceMigrationTableAction,
],
);
}
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
await fieldMetadataRepository.delete(fieldMetadata.id);
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
if (!compositeType) {
@ -383,6 +426,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
],
);
} else {
await fieldMetadataRepository.delete(fieldMetadata.id);
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`delete-${fieldMetadata.name}`),
workspaceId,

View File

@ -310,17 +310,17 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
const updatedObject = await super.updateOne(inputId, inputPayload);
const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
workspaceId,
);
await this.handleObjectNameAndLabelUpdates(
existingObjectMetadata,
existingObjectMetadataCombinedWithUpdateInput,
inputPayload,
);
const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
workspaceId,
);
if (inputPayload.isActive !== undefined) {
// For new relation system, the active status is stitched to the field metadata
if (!isNewRelationEnabled) {

View File

@ -6,6 +6,7 @@ import { capitalize } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { v4 as uuidV4 } from 'uuid';
import { FieldMetadataDefaultSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@ -165,15 +166,30 @@ export class ObjectMetadataFieldRelationService {
objectMetadataId: targetObjectMetadata.id,
workspaceId: workspaceId,
});
const isTargetFieldMetadataManyToOneRelation =
(
targetFieldMetadataToUpdate as FieldMetadataEntity<FieldMetadataType.RELATION>
).settings?.relationType === RelationType.MANY_TO_ONE;
const targetFieldMetadata = await this.fieldMetadataRepository.save({
id: targetFieldMetadataToUpdate.id,
...targetFieldMetadataUpdateData,
settings: {
...(targetFieldMetadataToUpdate.settings as FieldMetadataDefaultSettings),
...(isTargetFieldMetadataManyToOneRelation
? {
joinColumnName: `${sourceObjectMetadata.nameSingular}Id`,
}
: {}),
},
});
const sourceFieldMetadataUpdateData = this.updateSourceFieldMetadata(
sourceObjectMetadata,
targetObjectMetadata,
);
const sourceFieldMetadataToUpdate =
await this.fieldMetadataRepository.findOneByOrFail({
standardId:
@ -181,9 +197,23 @@ export class ObjectMetadataFieldRelationService {
objectMetadataId: sourceObjectMetadata.id,
workspaceId: workspaceId,
});
const isSourceFieldMetadataManyToOneRelation =
(
sourceFieldMetadataToUpdate as FieldMetadataEntity<FieldMetadataType.RELATION>
).settings?.relationType === RelationType.MANY_TO_ONE;
const sourceFieldMetadata = await this.fieldMetadataRepository.save({
id: sourceFieldMetadataToUpdate.id,
...sourceFieldMetadataUpdateData,
settings: {
...(sourceFieldMetadataToUpdate.settings as FieldMetadataDefaultSettings),
...(isSourceFieldMetadataManyToOneRelation
? {
joinColumnName: `${targetObjectMetadata.nameSingular}Id`,
}
: {}),
},
});
return {

View File

@ -276,7 +276,7 @@ export class ObjectMetadataMigrationService {
relationToDelete.toFieldMetadataId,
]);
if (relationToDelete.direction === 'from') {
if (relationToDelete.direction === 'from' && !isNewRelationEnabled) {
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(
`delete-${RELATION_MIGRATION_PRIORITY_PREFIX}-${relationToDelete.fromObjectName}-${relationToDelete.toObjectName}`,

View File

@ -54,6 +54,7 @@ describe('SearchResolver', () => {
},
},
});
const listingObjectMetadata = objectsMetadata.find(
(object) => object.nameSingular === LISTING_NAME_SINGULAR,
);

View File

@ -4,7 +4,6 @@ import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object
import { findManyObjectMetadataQueryFactory } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-query-factory.util';
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { createOneRelationMetadataFactory } from 'test/integration/metadata/suites/utils/create-one-relation-metadata-factory.util';
import { deleteOneRelationMetadataItemFactory } from 'test/integration/metadata/suites/utils/delete-one-relation-metadata-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { FieldMetadataType } from 'twenty-shared/types';
@ -14,7 +13,6 @@ const LISTING_NAME_SINGULAR = 'listing';
describe('Custom object renaming', () => {
let listingObjectId = '';
let customRelationId = '';
const STANDARD_OBJECT_RELATIONS = [
'noteTarget',
@ -108,10 +106,6 @@ describe('Custom object renaming', () => {
const fields = await makeMetadataAPIRequest(fieldsGraphqlOperation);
const foreignKeyFieldsMetadataForListing = fields.body.data.fields.edges
.filter((field) => field.node.name === `${LISTING_NAME_SINGULAR}Id`)
.map((field) => field.node);
const relationFieldsMetadataForListing = fields.body.data.fields.edges
.filter(
(field) =>
@ -120,21 +114,7 @@ describe('Custom object renaming', () => {
)
.map((field) => field.node);
expect(foreignKeyFieldsMetadataForListing.length).toBe(5);
STANDARD_OBJECT_RELATIONS.forEach((relation) => {
// foreignKey field
const foreignKeyFieldMetadataId = foreignKeyFieldsMetadataForListing.find(
(field) =>
field.object.id ===
standardObjectRelationsMap[relation].objectMetadataId,
).id;
expect(foreignKeyFieldMetadataId).not.toBeUndefined();
standardObjectRelationsMap[relation].foreignKeyFieldMetadataId =
foreignKeyFieldMetadataId;
// relation field
const relationFieldMetadataId = relationFieldsMetadataForListing.find(
(field) =>
@ -189,8 +169,6 @@ describe('Custom object renaming', () => {
);
// Assert
customRelationId = relationResponse.body.data.createOneRelationMetadata.id;
relationFieldMetadataOnPersonId =
relationResponse.body.data.createOneRelationMetadata.fromFieldMetadataId;
});
@ -233,29 +211,8 @@ describe('Custom object renaming', () => {
(field) => field.node,
);
expect(
fieldsMetadata.find(
(field) => field.name === `${LISTING_NAME_SINGULAR}Id`,
),
).toBeUndefined();
// standard relations have been updated
STANDARD_OBJECT_RELATIONS.forEach((relation) => {
// foreignKey field
const foreignKeyFieldMetadataId =
standardObjectRelationsMap[relation].foreignKeyFieldMetadataId;
const updatedForeignKeyFieldMetadata = fieldsMetadata.find(
(field) => field.id === foreignKeyFieldMetadataId,
);
expect(updatedForeignKeyFieldMetadata.name).toBe(
`${HOUSE_NAME_SINGULAR}Id`,
);
expect(updatedForeignKeyFieldMetadata.label).toBe(
'House ID (foreign key)',
);
// relation field
const relationFieldMetadataId =
standardObjectRelationsMap[relation].relationFieldMetadataId;
@ -276,19 +233,7 @@ describe('Custom object renaming', () => {
expect(updatedRelationFieldMetadata.name).toBe(RELATION_FROM_NAME);
});
it('4. should delete custom relation', async () => {
const graphqlOperation = deleteOneRelationMetadataItemFactory({
idToDelete: customRelationId,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
const deleteRelationResponse = response.body.data.deleteOneRelation;
expect(deleteRelationResponse.id).toBe(customRelationId);
});
it('5. should delete custom object', async () => {
it('4. should delete custom object', async () => {
const { data } = await deleteOneObjectMetadata({
input: {
idToDelete: listingObjectId,

View File

@ -52921,6 +52921,13 @@ __metadata:
languageName: node
linkType: hard
"storybook-addon-mock-date@npm:^0.6.0":
version: 0.6.0
resolution: "storybook-addon-mock-date@npm:0.6.0"
checksum: 10c0/24c48d65a04eb1931e39495fe42692cea428db4b66874db86ab4e9e6f99cd0bba56cd25c56c9a56b42e53a14edfd38f79bcdef4c1f0e2758096528863d32d408
languageName: node
linkType: hard
"storybook-addon-pseudo-states@npm:^2.1.2":
version: 2.2.1
resolution: "storybook-addon-pseudo-states@npm:2.2.1"
@ -55501,6 +55508,7 @@ __metadata:
source-map-support: "npm:^0.5.20"
storybook: "npm:^7.6.3"
storybook-addon-cookie: "npm:^3.2.0"
storybook-addon-mock-date: "npm:^0.6.0"
storybook-addon-pseudo-states: "npm:^2.1.2"
storybook-dark-mode: "npm:^3.0.3"
stripe: "npm:^17.3.1"