Compare commits

...
Author SHA1 Message Date
ehconitin 956419dc60 Enforce CANVAS tab widget cap 2026-05-26 17:02:08 +05:30
ehconitin ad0662ddb1 Fix page layout widget tab moves 2026-05-26 16:36:26 +05:30
7 changed files with 646 additions and 4 deletions
@@ -930,7 +930,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
universalProperty: undefined,
},
pageLayoutTabId: {
toCompare: false,
toCompare: true,
toStringify: false,
universalProperty: 'pageLayoutTabUniversalIdentifier',
isOverridable: true,
@@ -0,0 +1,193 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { FlatPageLayoutWidgetTypeValidatorService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { type UniversalFlatPageLayoutTab } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-tab.type';
import { FlatPageLayoutWidgetValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service';
const CANVAS_TAB_UI = '00000000-0000-0000-0000-000000000aa1';
const GRID_TAB_UI = '00000000-0000-0000-0000-000000000aa2';
const WIDGET_A_UI = '00000000-0000-0000-0000-000000000111';
const WIDGET_B_UI = '00000000-0000-0000-0000-000000000222';
const CANVAS_CAP_ERROR = /CANVAS layout tab can only contain one widget/;
const tab = (
layoutMode: PageLayoutTabLayoutMode,
universalIdentifier: string,
): UniversalFlatPageLayoutTab =>
({
universalIdentifier,
layoutMode,
}) as unknown as UniversalFlatPageLayoutTab;
const widget = (
universalIdentifier: string,
pageLayoutTabUniversalIdentifier: string,
options?: {
universalOverrides?: {
pageLayoutTabUniversalIdentifier?: string;
} | null;
},
): FlatPageLayoutWidget =>
({
universalIdentifier,
pageLayoutTabUniversalIdentifier,
universalOverrides: options?.universalOverrides ?? null,
title: 'widget',
type: 'FRONT_COMPONENT',
gridPosition: { row: 0, column: 0, rowSpan: 12, columnSpan: 12 },
position: { layoutMode: PageLayoutTabLayoutMode.CANVAS },
}) as unknown as FlatPageLayoutWidget;
const mapsFrom = (entities: { universalIdentifier: string }[]): any => {
const maps = createEmptyFlatEntityMaps() as any;
for (const entity of entities) {
maps.byUniversalIdentifier[entity.universalIdentifier] = entity;
}
return maps;
};
const buildCreateArgs = ({
incoming,
tabs,
existingWidgets,
}: {
incoming: FlatPageLayoutWidget;
tabs: UniversalFlatPageLayoutTab[];
existingWidgets: FlatPageLayoutWidget[];
}) =>
({
flatEntityToValidate: incoming,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPageLayoutTabMaps: mapsFrom(tabs),
flatPageLayoutWidgetMaps: mapsFrom(existingWidgets),
},
additionalCacheDataMaps: { featureFlagsMap: {} },
workspaceId: 'workspace-id',
buildOptions: {} as never,
}) as any;
const buildUpdateArgs = ({
universalIdentifier,
update,
tabs,
existingWidgets,
}: {
universalIdentifier: string;
update: Record<string, unknown>;
tabs: UniversalFlatPageLayoutTab[];
existingWidgets: FlatPageLayoutWidget[];
}) =>
({
universalIdentifier,
flatEntityUpdate: update,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPageLayoutTabMaps: mapsFrom(tabs),
flatPageLayoutWidgetMaps: mapsFrom(existingWidgets),
},
additionalCacheDataMaps: { featureFlagsMap: {} },
workspaceId: 'workspace-id',
buildOptions: {} as never,
}) as any;
const errorsMatching = (errors: { message: string }[], matcher: RegExp) =>
errors.filter((error) => matcher.test(error.message));
describe('FlatPageLayoutWidgetValidatorService — CANVAS widget cap', () => {
let service: FlatPageLayoutWidgetValidatorService;
beforeEach(async () => {
const moduleRef: TestingModule = await Test.createTestingModule({
providers: [
FlatPageLayoutWidgetValidatorService,
{
provide: FlatPageLayoutWidgetTypeValidatorService,
useValue: {
validateFlatPageLayoutWidgetTypeSpecificitiesForCreation: () => [],
validateFlatPageLayoutWidgetTypeSpecificitiesForUpdate: () => [],
},
},
],
}).compile();
service = moduleRef.get(FlatPageLayoutWidgetValidatorService);
});
it('rejects a second widget on a CANVAS tab', async () => {
const canvasTab = tab(PageLayoutTabLayoutMode.CANVAS, CANVAS_TAB_UI);
const result = await service.validateFlatPageLayoutWidgetCreation(
buildCreateArgs({
incoming: widget(WIDGET_B_UI, CANVAS_TAB_UI),
tabs: [canvasTab],
existingWidgets: [widget(WIDGET_A_UI, CANVAS_TAB_UI)],
}),
);
expect(errorsMatching(result.errors, CANVAS_CAP_ERROR)).toHaveLength(1);
});
it('counts widgets assigned to a CANVAS tab through overrides', async () => {
const canvasTab = tab(PageLayoutTabLayoutMode.CANVAS, CANVAS_TAB_UI);
const gridTab = tab(PageLayoutTabLayoutMode.GRID, GRID_TAB_UI);
const result = await service.validateFlatPageLayoutWidgetCreation(
buildCreateArgs({
incoming: widget(WIDGET_B_UI, CANVAS_TAB_UI),
tabs: [canvasTab, gridTab],
existingWidgets: [
widget(WIDGET_A_UI, GRID_TAB_UI, {
universalOverrides: {
pageLayoutTabUniversalIdentifier: CANVAS_TAB_UI,
},
}),
],
}),
);
expect(errorsMatching(result.errors, CANVAS_CAP_ERROR)).toHaveLength(1);
});
it('rejects moving a widget into an occupied CANVAS tab', async () => {
const canvasTab = tab(PageLayoutTabLayoutMode.CANVAS, CANVAS_TAB_UI);
const gridTab = tab(PageLayoutTabLayoutMode.GRID, GRID_TAB_UI);
const result = await service.validateFlatPageLayoutWidgetUpdate(
buildUpdateArgs({
universalIdentifier: WIDGET_B_UI,
update: {
universalOverrides: {
pageLayoutTabUniversalIdentifier: CANVAS_TAB_UI,
},
},
tabs: [canvasTab, gridTab],
existingWidgets: [
widget(WIDGET_A_UI, CANVAS_TAB_UI),
widget(WIDGET_B_UI, GRID_TAB_UI),
],
}),
);
expect(errorsMatching(result.errors, CANVAS_CAP_ERROR)).toHaveLength(1);
});
it('does not apply the cap to non-CANVAS tabs', async () => {
const gridTab = tab(PageLayoutTabLayoutMode.GRID, GRID_TAB_UI);
const result = await service.validateFlatPageLayoutWidgetCreation(
buildCreateArgs({
incoming: widget(WIDGET_B_UI, GRID_TAB_UI),
tabs: [gridTab],
existingWidgets: [widget(WIDGET_A_UI, GRID_TAB_UI)],
}),
);
expect(errorsMatching(result.errors, CANVAS_CAP_ERROR)).toHaveLength(0);
});
});
@@ -13,10 +13,12 @@ import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules
import { FlatPageLayoutWidgetTypeValidatorService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
import { PageLayoutTabExceptionCode } from 'src/engine/metadata-modules/page-layout-tab/exceptions/page-layout-tab.exception';
import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
import { type MetadataUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/metadata-universal-flat-entity-maps.type';
import { validatePageLayoutWidgetGridPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-page-layout-widget-grid-position.util';
import { validatePageLayoutWidgetVerticalListPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-page-layout-widget-vertical-list-position.util';
import { validateWidgetGridPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util';
import { type UniversalFlatPageLayoutTab } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-tab.type';
import { type UniversalFlatPageLayoutWidget } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-widget.type';
import {
FailedFlatEntityValidation,
FlatEntityValidationError,
@@ -83,6 +85,23 @@ export class FlatPageLayoutWidgetValidatorService {
optimisticFlatEntityMapsAndRelatedFlatEntityMaps.flatPageLayoutTabMaps,
});
const effectivePageLayoutTab = findFlatEntityByUniversalIdentifier({
universalIdentifier: this.getEffectivePageLayoutTabUniversalIdentifier(
updatedFlatPageLayoutWidget,
),
flatEntityMaps:
optimisticFlatEntityMapsAndRelatedFlatEntityMaps.flatPageLayoutTabMaps,
});
const canvasTabErrors = this.validateCanvasTabWidgetCap({
pageLayoutTab: effectivePageLayoutTab,
flatPageLayoutWidgetMaps:
optimisticFlatEntityMapsAndRelatedFlatEntityMaps.flatPageLayoutWidgetMaps,
excludeWidgetUniversalIdentifier: universalIdentifier,
});
validationResult.errors.push(...canvasTabErrors);
const gridPositionErrors = this.validateGridPosition({
gridPosition: updatedFlatPageLayoutWidget.gridPosition,
widgetTitle: updatedFlatPageLayoutWidget.title,
@@ -203,6 +222,16 @@ export class FlatPageLayoutWidgetValidatorService {
});
}
const canvasTabErrors = this.validateCanvasTabWidgetCap({
pageLayoutTab: referencedPageLayoutTab,
flatPageLayoutWidgetMaps:
optimisticFlatEntityMapsAndRelatedFlatEntityMaps.flatPageLayoutWidgetMaps,
excludeWidgetUniversalIdentifier:
flatPageLayoutWidgetToValidate.universalIdentifier,
});
validationResult.errors.push(...canvasTabErrors);
const gridPositionErrors = this.validateGridPosition({
gridPosition: flatPageLayoutWidgetToValidate.gridPosition,
widgetTitle: flatPageLayoutWidgetToValidate.title,
@@ -235,6 +264,58 @@ export class FlatPageLayoutWidgetValidatorService {
return validationResult;
}
private validateCanvasTabWidgetCap({
pageLayoutTab,
flatPageLayoutWidgetMaps,
excludeWidgetUniversalIdentifier,
}: {
pageLayoutTab: UniversalFlatPageLayoutTab | undefined;
flatPageLayoutWidgetMaps: MetadataUniversalFlatEntityMaps<'pageLayoutWidget'>;
excludeWidgetUniversalIdentifier?: string;
}): FlatEntityValidationError[] {
if (
!isDefined(pageLayoutTab) ||
pageLayoutTab.layoutMode !== PageLayoutTabLayoutMode.CANVAS
) {
return [];
}
const existingWidgetsOnTab = Object.values(
flatPageLayoutWidgetMaps.byUniversalIdentifier,
).filter(
(widget) =>
isDefined(widget) &&
!isDefined(widget.deletedAt) &&
this.getEffectivePageLayoutTabUniversalIdentifier(widget) ===
pageLayoutTab.universalIdentifier &&
widget.universalIdentifier !== excludeWidgetUniversalIdentifier,
);
if (existingWidgetsOnTab.length === 0) {
return [];
}
return [
{
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
message: t`A CANVAS layout tab can only contain one widget`,
userFriendlyMessage: msg`A CANVAS layout tab can only contain one widget`,
},
];
}
private getEffectivePageLayoutTabUniversalIdentifier(
widget: Pick<
UniversalFlatPageLayoutWidget,
'pageLayoutTabUniversalIdentifier' | 'universalOverrides'
>,
): string {
return (
widget.universalOverrides?.pageLayoutTabUniversalIdentifier ??
widget.pageLayoutTabUniversalIdentifier
);
}
private validateGridPosition({
gridPosition,
widgetTitle,
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Page layout widget creation should fail AGGREGATE_CHART widget configuration validation failures when AGGREGATE_CHART configuration has invalid UUID 1`] = `
{
@@ -44,6 +44,42 @@ exports[`Page layout widget creation should fail BAR_CHART widget configuration
}
`;
exports[`Page layout widget creation should fail CANVAS tab widget cap rejects a second widget on a CANVAS tab 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"pageLayoutWidget": [
{
"errors": [
{
"code": "INVALID_PAGE_LAYOUT_WIDGET_DATA",
"message": "A CANVAS layout tab can only contain one widget",
"userFriendlyMessage": "A CANVAS layout tab can only contain one widget",
},
],
"flatEntityMinimalInformation": {
"pageLayoutTabUniversalIdentifier": Any<String>,
"universalIdentifier": Any<String>,
},
"metadataName": "pageLayoutWidget",
"status": "fail",
"type": "create",
},
],
},
"message": "Validation failed for 1 pageLayoutWidget",
"summary": {
"pageLayoutWidget": 1,
"totalErrors": 1,
},
"userFriendlyMessage": "A CANVAS layout tab can only contain one widget",
},
"message": "Multiple validation errors occurred while creating page layout widget",
"name": "GraphQLError",
}
`;
exports[`Page layout widget creation should fail Edge case configuration validation failures when configuration has missing configurationType 1`] = `
{
"extensions": {
@@ -16,6 +16,7 @@ import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-l
import { createOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget.util';
import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util';
import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util';
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
import { type CreatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
@@ -257,6 +258,56 @@ describe('Page layout widget creation should fail', () => {
});
});
describe('CANVAS tab widget cap', () => {
let canvasTabId: string;
beforeAll(async () => {
const { data } = await createOnePageLayoutTab({
expectToFail: false,
input: {
title: 'CANVAS Tab For Cap Test',
pageLayoutId: testPageLayoutId,
layoutMode: PageLayoutTabLayoutMode.CANVAS,
},
});
canvasTabId = data.createPageLayoutTab.id;
await createOnePageLayoutWidget({
expectToFail: false,
input: {
title: 'First widget on CANVAS tab',
pageLayoutTabId: canvasTabId,
type: WidgetType.IFRAME,
configuration: TEST_IFRAME_CONFIG,
gridPosition: DEFAULT_GRID_POSITION,
},
});
});
afterAll(async () => {
await destroyOnePageLayoutTab({
expectToFail: false,
input: { id: canvasTabId },
});
});
it('rejects a second widget on a CANVAS tab', async () => {
const { errors } = await createOnePageLayoutWidget({
expectToFail: true,
input: {
title: 'Second widget should be rejected',
pageLayoutTabId: canvasTabId,
type: WidgetType.IFRAME,
configuration: TEST_IFRAME_CONFIG,
gridPosition: DEFAULT_GRID_POSITION,
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
});
describe('Edge case configuration validation failures', () => {
it('when configuration is null', async () => {
const { errors } = await createOnePageLayoutWidget({
@@ -11,7 +11,11 @@ import {
import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util';
import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util';
import { updateOnePageLayoutWithTabsAndWidgets } from 'test/integration/metadata/suites/page-layout/utils/update-one-page-layout-with-tabs-and-widgets.util';
import { AggregateOperations, FieldMetadataType } from 'twenty-shared/types';
import {
AggregateOperations,
FieldMetadataType,
PageLayoutTabLayoutMode,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
@@ -58,6 +62,174 @@ describe('Page layout with tabs update should fail', () => {
expectOneNotInternalServerErrorSnapshot({ errors });
});
it('when moving a widget into an occupied CANVAS tab', async () => {
const { data: layoutData } = await createOnePageLayout({
expectToFail: false,
input: {
name: 'Move Into Occupied Canvas Layout',
type: PageLayoutType.RECORD_PAGE,
},
});
const testPageLayoutId = layoutData.createPageLayout.id;
const sourceTabId = v4();
const canvasTabId = v4();
const widgetToMoveId = v4();
const canvasWidgetId = v4();
try {
await updateOnePageLayoutWithTabsAndWidgets({
expectToFail: false,
input: {
id: testPageLayoutId,
name: 'Move Into Occupied Canvas Layout',
type: PageLayoutType.RECORD_PAGE,
objectMetadataId: null,
tabs: [
{
id: sourceTabId,
title: 'Source Tab',
position: 0,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [
{
id: widgetToMoveId,
pageLayoutTabId: sourceTabId,
title: 'Widget To Move',
type: WidgetType.IFRAME,
objectMetadataId: null,
gridPosition: {
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
},
position: {
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index: 0,
},
configuration: {
configurationType: WidgetConfigurationType.IFRAME,
url: 'https://example.com/source',
},
},
],
},
{
id: canvasTabId,
title: 'Canvas Tab',
position: 1,
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [
{
id: canvasWidgetId,
pageLayoutTabId: canvasTabId,
title: 'Canvas Widget',
type: WidgetType.IFRAME,
objectMetadataId: null,
gridPosition: {
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
},
position: {
layoutMode: PageLayoutTabLayoutMode.CANVAS,
},
configuration: {
configurationType: WidgetConfigurationType.IFRAME,
url: 'https://example.com/canvas',
},
},
],
},
],
},
});
const { errors } = await updateOnePageLayoutWithTabsAndWidgets({
expectToFail: true,
input: {
id: testPageLayoutId,
name: 'Move Into Occupied Canvas Layout',
type: PageLayoutType.RECORD_PAGE,
objectMetadataId: null,
tabs: [
{
id: sourceTabId,
title: 'Source Tab',
position: 0,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [],
},
{
id: canvasTabId,
title: 'Canvas Tab',
position: 1,
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [
{
id: canvasWidgetId,
pageLayoutTabId: canvasTabId,
title: 'Canvas Widget',
type: WidgetType.IFRAME,
objectMetadataId: null,
gridPosition: {
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
},
position: {
layoutMode: PageLayoutTabLayoutMode.CANVAS,
},
configuration: {
configurationType: WidgetConfigurationType.IFRAME,
url: 'https://example.com/canvas',
},
},
{
id: widgetToMoveId,
pageLayoutTabId: canvasTabId,
title: 'Widget To Move',
type: WidgetType.IFRAME,
objectMetadataId: null,
gridPosition: {
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
},
position: {
layoutMode: PageLayoutTabLayoutMode.CANVAS,
},
configuration: {
configurationType: WidgetConfigurationType.IFRAME,
url: 'https://example.com/source',
},
},
],
},
],
},
});
expect(errors).toBeDefined();
expect(errors).toHaveLength(1);
const [firstError] = errors ?? [];
expect(firstError?.extensions.code).toBe('BAD_USER_INPUT');
expect(firstError?.message).toContain(
'A CANVAS layout tab can only contain one widget',
);
} finally {
await destroyOnePageLayout({
expectToFail: false,
input: { id: testPageLayoutId },
});
}
});
describe('chart filter validation failures', () => {
let testFieldMetadataIds: TestFieldMetadataIds;
let testPageLayoutId: string | undefined;
@@ -8,7 +8,10 @@ import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { AggregateOperations } from 'twenty-shared/types';
import {
AggregateOperations,
PageLayoutTabLayoutMode,
} from 'twenty-shared/types';
import { v4 } from 'uuid';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
@@ -229,4 +232,110 @@ describe('Page layout with tabs update should succeed', () => {
);
},
);
it('should move a widget to another tab when saving layout tabs and widgets', async () => {
const widgetId = v4();
await updateOnePageLayoutWithTabsAndWidgets({
expectToFail: false,
input: {
id: testPageLayoutId,
name: 'Layout Before Widget Move',
type: PageLayoutType.RECORD_PAGE,
objectMetadataId: null,
tabs: [
{
id: testTabId1,
title: 'Source Tab',
position: 0,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [
{
id: widgetId,
pageLayoutTabId: testTabId1,
title: 'Iframe Widget',
type: WidgetType.IFRAME,
objectMetadataId: null,
gridPosition: {
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
},
position: {
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index: 0,
},
configuration: MOCK_IFRAME_CONFIGURATION,
},
],
},
{
id: testTabId2,
title: 'Destination Tab',
position: 1,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [],
},
],
},
});
const { data } = await updateOnePageLayoutWithTabsAndWidgets({
expectToFail: false,
input: {
id: testPageLayoutId,
name: 'Layout After Widget Move',
type: PageLayoutType.RECORD_PAGE,
objectMetadataId: null,
tabs: [
{
id: testTabId1,
title: 'Source Tab',
position: 0,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [],
},
{
id: testTabId2,
title: 'Destination Tab',
position: 1,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [
{
id: widgetId,
pageLayoutTabId: testTabId2,
title: 'Iframe Widget',
type: WidgetType.IFRAME,
objectMetadataId: null,
gridPosition: {
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
},
position: {
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index: 0,
},
configuration: MOCK_IFRAME_CONFIGURATION,
},
],
},
],
},
});
const updatedTabs = data.updatePageLayoutWithTabsAndWidgets.tabs ?? [];
const sourceTab = updatedTabs.find((tab) => tab.id === testTabId1);
const destinationTab = updatedTabs.find((tab) => tab.id === testTabId2);
expect(sourceTab?.widgets).toHaveLength(0);
expect(destinationTab?.widgets).toHaveLength(1);
expect(destinationTab?.widgets?.[0]).toMatchObject({
id: widgetId,
pageLayoutTabId: testTabId2,
title: 'Iframe Widget',
});
});
});