Files
twenty/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-update-with-tabs-and-widgets.integration-spec.ts
T

577 lines
19 KiB
TypeScript

import {
TEST_BAR_CHART_CONFIG_MINIMAL,
TEST_IFRAME_CONFIG,
TEST_IFRAME_CONFIG_ALTERNATIVE,
TEST_NUMBER_CHART_CONFIG_MINIMAL,
} from 'test/integration/constants/widget-configuration-test-data.constants';
import { findPageLayoutOperationFactory } from 'test/integration/graphql/utils/find-page-layout-operation-factory.util';
import {
assertGraphQLErrorResponse,
assertGraphQLSuccessfulResponse,
} from 'test/integration/graphql/utils/graphql-test-assertions.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { createTestPageLayoutWithGraphQL } from 'test/integration/graphql/utils/page-layout-graphql.util';
import { updatePageLayoutWithTabsOperationFactory } from 'test/integration/graphql/utils/update-page-layout-with-tabs-operation-factory.util';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { cleanupPageLayoutTabRecords } from 'test/integration/utils/page-layout-tab-test.util';
import {
assertPageLayoutStructure,
cleanupPageLayoutRecords,
} from 'test/integration/utils/page-layout-test.util';
import { cleanupPageLayoutWidgetRecords } from 'test/integration/utils/page-layout-widget-test.util';
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { type PageLayoutEntity } from 'src/engine/core-modules/page-layout/entities/page-layout.entity';
import { PageLayoutType } from 'src/engine/core-modules/page-layout/enums/page-layout-type.enum';
import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum';
const existingTabId = '20202020-e02c-4292-9994-42695c4e41e8';
const tabToUpdateId = '20202020-974a-4480-b814-1fca24937132';
const tabToDeleteId = '20202020-a510-49e6-a9d9-0ff3e5c6a1ae';
const newTabId = '20202020-1db1-4c18-8804-6c700a8106a6';
const existingWidgetId = '20202020-afbe-46a3-a759-9a10ea7f5888';
const widgetToUpdateId = '20202020-c312-4342-93c0-f02c92c4a609';
const widgetToDeleteId = '20202020-8a0b-4ec2-8784-af4adb27c18a';
const newWidgetId = '20202020-da21-4321-b313-699eeff00798';
const anotherNewWidgetId = '20202020-3937-4617-bc88-8a811f769c90';
describe('Page Layout Update With Tabs And Widgets Integration', () => {
let testObjectMetadataId: string;
beforeAll(async () => {
const {
data: {
createOneObject: { id: objectMetadataId },
},
} = await createOneObjectMetadata({
input: {
nameSingular: 'myTestUpdatePageLayoutObject',
namePlural: 'myTestUpdatePageLayoutObjects',
labelSingular: 'My Test Update Page Layout Object',
labelPlural: 'My Test Update Page Layout Objects',
icon: 'IconUpdate',
},
});
testObjectMetadataId = objectMetadataId;
});
afterAll(async () => {
await updateOneObjectMetadata({
input: {
idToUpdate: testObjectMetadataId,
updatePayload: {
isActive: false,
},
},
});
await deleteOneObjectMetadata({
input: { idToDelete: testObjectMetadataId },
});
});
afterEach(async () => {
await cleanupPageLayoutRecords();
await cleanupPageLayoutTabRecords();
await cleanupPageLayoutWidgetRecords();
});
describe('updatePageLayoutWithTabsAndWidgets', () => {
it('should handle complex page layout update with tab and widget CRUD operations', async () => {
const pageLayout = await createTestPageLayoutWithGraphQL({
name: 'Initial Page Layout',
type: PageLayoutType.RECORD_PAGE,
objectMetadataId: testObjectMetadataId,
});
const initialUpdateInput = {
name: 'Updated Page Layout',
type: PageLayoutType.DASHBOARD,
objectMetadataId: testObjectMetadataId,
tabs: [
{
id: existingTabId,
title: 'Existing Tab',
position: 1,
widgets: [
{
id: existingWidgetId,
pageLayoutTabId: existingTabId,
title: 'Existing Widget',
type: WidgetType.VIEW,
objectMetadataId: testObjectMetadataId,
gridPosition: {
row: 0,
column: 0,
rowSpan: 2,
columnSpan: 2,
},
configuration: null,
},
{
id: widgetToUpdateId,
pageLayoutTabId: existingTabId,
title: 'Widget To Update',
type: WidgetType.GRAPH,
objectMetadataId: testObjectMetadataId,
gridPosition: {
row: 0,
column: 2,
rowSpan: 1,
columnSpan: 1,
},
configuration: TEST_NUMBER_CHART_CONFIG_MINIMAL,
},
{
id: widgetToDeleteId,
pageLayoutTabId: existingTabId,
title: 'Widget To Delete',
type: WidgetType.IFRAME,
objectMetadataId: null,
gridPosition: {
row: 1,
column: 2,
rowSpan: 1,
columnSpan: 1,
},
configuration: TEST_IFRAME_CONFIG,
},
],
},
{
id: tabToUpdateId,
title: 'Tab To Update',
position: 2,
widgets: [],
},
{
id: tabToDeleteId,
title: 'Tab To Delete',
position: 3,
widgets: [],
},
],
};
const initialOperation = updatePageLayoutWithTabsOperationFactory({
pageLayoutId: pageLayout.id,
data: initialUpdateInput,
});
const initialResponse = await makeGraphqlAPIRequest(initialOperation);
assertGraphQLSuccessfulResponse(initialResponse);
const initialResult =
initialResponse.body.data.updatePageLayoutWithTabsAndWidgets;
assertPageLayoutStructure(initialResult, {
id: pageLayout.id,
name: 'Updated Page Layout',
type: PageLayoutType.DASHBOARD,
objectMetadataId: testObjectMetadataId,
});
expect(initialResult.tabs).toHaveLength(3);
expect(initialResult.tabs[0].widgets).toHaveLength(3);
const complexUpdateInput = {
name: 'Final Page Layout',
type: PageLayoutType.RECORD_PAGE,
objectMetadataId: testObjectMetadataId,
tabs: [
{
id: existingTabId,
title: 'Updated Existing Tab',
position: 1,
widgets: [
{
id: existingWidgetId,
pageLayoutTabId: existingTabId,
title: 'Existing Widget',
type: WidgetType.VIEW,
objectMetadataId: testObjectMetadataId,
gridPosition: {
row: 0,
column: 0,
rowSpan: 2,
columnSpan: 2,
},
configuration: null,
},
{
id: widgetToUpdateId,
pageLayoutTabId: existingTabId,
title: 'Updated Widget Title',
type: WidgetType.GRAPH,
objectMetadataId: testObjectMetadataId,
gridPosition: {
row: 0,
column: 2,
rowSpan: 2,
columnSpan: 3,
},
configuration: TEST_BAR_CHART_CONFIG_MINIMAL,
},
{
id: newWidgetId,
pageLayoutTabId: existingTabId,
title: 'New Widget',
type: WidgetType.FIELDS,
objectMetadataId: testObjectMetadataId,
gridPosition: {
row: 2,
column: 0,
rowSpan: 1,
columnSpan: 2,
},
configuration: null,
},
],
},
{
id: tabToUpdateId,
title: 'Completely Updated Tab',
position: 3,
widgets: [
{
id: anotherNewWidgetId,
pageLayoutTabId: tabToUpdateId,
title: 'Another New Widget',
type: WidgetType.IFRAME,
objectMetadataId: null,
gridPosition: {
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
},
configuration: TEST_IFRAME_CONFIG_ALTERNATIVE,
},
],
},
{
id: newTabId,
title: 'Brand New Tab',
position: 2,
widgets: [],
},
],
};
const complexOperation = updatePageLayoutWithTabsOperationFactory({
pageLayoutId: pageLayout.id,
data: complexUpdateInput,
});
const complexResponse = await makeGraphqlAPIRequest(complexOperation);
assertGraphQLSuccessfulResponse(complexResponse);
const finalResult: PageLayoutEntity =
complexResponse.body.data.updatePageLayoutWithTabsAndWidgets;
assertPageLayoutStructure(finalResult, {
id: pageLayout.id,
name: 'Final Page Layout',
type: PageLayoutType.RECORD_PAGE,
objectMetadataId: testObjectMetadataId,
});
expect(finalResult.tabs).toHaveLength(3);
const updatedExistingTab = finalResult.tabs.find(
(tab) => tab.id === existingTabId,
);
const updatedTab = finalResult.tabs.find(
(tab) => tab.id === tabToUpdateId,
);
const newTab = finalResult.tabs.find((tab) => tab.id === newTabId);
const deletedTab = finalResult.tabs.find(
(tab) => tab.id === tabToDeleteId,
);
expect(updatedExistingTab).toBeDefined();
expect(updatedExistingTab?.title).toBe('Updated Existing Tab');
expect(updatedExistingTab?.position).toBe(1);
expect(updatedTab).toBeDefined();
expect(updatedTab?.title).toBe('Completely Updated Tab');
expect(updatedTab?.position).toBe(3);
expect(newTab).toBeDefined();
expect(newTab?.title).toBe('Brand New Tab');
expect(newTab?.position).toBe(2);
expect(deletedTab).toBeUndefined();
const firstTabWidgets = updatedExistingTab?.widgets;
expect(firstTabWidgets).toHaveLength(3);
const existingWidget = firstTabWidgets?.find(
(widget) => widget.id === existingWidgetId,
);
const updatedWidget = firstTabWidgets?.find(
(widget) => widget.id === widgetToUpdateId,
);
const newWidget = firstTabWidgets?.find(
(widget) => widget.id === newWidgetId,
);
const deletedWidget = firstTabWidgets?.find(
(widget) => widget.id === widgetToDeleteId,
);
expect(existingWidget).toBeDefined();
expect(existingWidget?.title).toBe('Existing Widget');
expect(existingWidget?.type).toBe(WidgetType.VIEW);
expect(updatedWidget).toBeDefined();
expect(updatedWidget?.title).toBe('Updated Widget Title');
expect(updatedWidget?.type).toBe(WidgetType.GRAPH);
expect(updatedWidget?.gridPosition.rowSpan).toBe(2);
expect(updatedWidget?.gridPosition.columnSpan).toBe(3);
expect(newWidget).toBeDefined();
expect(newWidget?.title).toBe('New Widget');
expect(newWidget?.type).toBe(WidgetType.FIELDS);
expect(deletedWidget).toBeUndefined();
const secondTabWidgets = updatedTab?.widgets;
expect(secondTabWidgets).toHaveLength(1);
const anotherNewWidget = secondTabWidgets?.find(
(widget) => widget.id === anotherNewWidgetId,
);
expect(anotherNewWidget).toBeDefined();
expect(anotherNewWidget?.title).toBe('Another New Widget');
expect(anotherNewWidget?.type).toBe(WidgetType.IFRAME);
expect(newTab?.widgets).toHaveLength(0);
});
it('should handle transaction rollback on error during complex update', async () => {
const pageLayout = await createTestPageLayoutWithGraphQL({
name: 'Test Transaction Rollback',
type: PageLayoutType.RECORD_PAGE,
});
const invalidUpdateInput = {
name: '',
type: PageLayoutType.DASHBOARD,
objectMetadataId: 'invalid-uuid',
tabs: [
{
id: '20202020-9a4c-4f97-bd63-a5f5843ee610',
title: 'Test Tab',
position: 1,
widgets: [],
},
],
};
const operation = updatePageLayoutWithTabsOperationFactory({
pageLayoutId: pageLayout.id,
data: invalidUpdateInput,
});
const response = await makeGraphqlAPIRequest(operation);
expect(response.body.errors).toBeDefined();
expect(response.body.errors.length).toBeGreaterThan(0);
const finalPageLayout = findPageLayoutOperationFactory({
pageLayoutId: pageLayout.id,
});
const finalResponse = await makeGraphqlAPIRequest(finalPageLayout);
assertGraphQLSuccessfulResponse(finalResponse);
const finalResult = finalResponse.body.data.getPageLayout;
assertPageLayoutStructure(finalResult, {
id: pageLayout.id,
name: 'Test Transaction Rollback',
type: PageLayoutType.RECORD_PAGE,
});
});
it('should throw when empty tabs array is provided', async () => {
const pageLayout = await createTestPageLayoutWithGraphQL({
name: 'Test Empty Tabs',
type: PageLayoutType.RECORD_PAGE,
objectMetadataId: testObjectMetadataId,
});
const updateInput = {
name: 'Updated Layout With No Tabs',
type: PageLayoutType.DASHBOARD,
objectMetadataId: testObjectMetadataId,
tabs: [],
};
const operation = updatePageLayoutWithTabsOperationFactory({
pageLayoutId: pageLayout.id,
data: updateInput,
});
const response = await makeGraphqlAPIRequest(operation);
assertGraphQLErrorResponse(response, ErrorCode.BAD_USER_INPUT);
});
it('should reject invalid widget configurations', async () => {
const pageLayout = await createTestPageLayoutWithGraphQL({
name: 'Test Invalid Config',
type: PageLayoutType.RECORD_PAGE,
objectMetadataId: testObjectMetadataId,
});
const invalidConfigInput = {
name: 'Layout with Invalid Widget Config',
type: PageLayoutType.DASHBOARD,
objectMetadataId: testObjectMetadataId,
tabs: [
{
id: '20202020-1001-4001-a001-000000000001',
title: 'Tab with Invalid Widget',
position: 1,
widgets: [
{
id: '20202020-1002-4002-a002-000000000002',
pageLayoutTabId: '20202020-1001-4001-a001-000000000001',
title: 'Invalid Iframe Widget',
type: WidgetType.IFRAME,
objectMetadataId: null,
gridPosition: {
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
},
configuration: { url: 'not-a-valid-url' },
},
],
},
],
};
const operation = updatePageLayoutWithTabsOperationFactory({
pageLayoutId: pageLayout.id,
data: invalidConfigInput,
});
const response = await makeGraphqlAPIRequest(operation);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toContain(
'Invalid configuration for widget "Invalid Iframe Widget" of type IFRAME',
);
expect(response.body.errors[0].message).toContain('url must be');
});
it('should accept valid widget configurations for each type', async () => {
const pageLayout = await createTestPageLayoutWithGraphQL({
name: 'Test Valid Configs',
type: PageLayoutType.RECORD_PAGE,
objectMetadataId: testObjectMetadataId,
});
const validConfigInput = {
name: 'Layout with Valid Widget Configs',
type: PageLayoutType.DASHBOARD,
objectMetadataId: testObjectMetadataId,
tabs: [
{
id: '20202020-1010-4010-a010-101010101010',
title: 'Tab with Valid Widgets',
position: 1,
widgets: [
{
id: '20202020-1011-4011-a011-111111111111',
pageLayoutTabId: '20202020-1010-4010-a010-101010101010',
title: 'Valid Iframe Widget',
type: WidgetType.IFRAME,
objectMetadataId: null,
gridPosition: {
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
},
configuration: TEST_IFRAME_CONFIG,
},
{
id: '20202020-1012-4012-a012-121212121212',
pageLayoutTabId: '20202020-1010-4010-a010-101010101010',
title: 'Valid Graph Widget',
type: WidgetType.GRAPH,
objectMetadataId: testObjectMetadataId,
gridPosition: {
row: 1,
column: 0,
rowSpan: 2,
columnSpan: 2,
},
configuration: TEST_NUMBER_CHART_CONFIG_MINIMAL,
},
{
id: '20202020-1013-4013-a013-131313131313',
pageLayoutTabId: '20202020-1010-4010-a010-101010101010',
title: 'Valid View Widget',
type: WidgetType.VIEW,
objectMetadataId: testObjectMetadataId,
gridPosition: {
row: 0,
column: 1,
rowSpan: 1,
columnSpan: 1,
},
configuration: null,
},
],
},
],
};
const operation = updatePageLayoutWithTabsOperationFactory({
pageLayoutId: pageLayout.id,
data: validConfigInput,
});
const response = await makeGraphqlAPIRequest(operation);
assertGraphQLSuccessfulResponse(response);
const result = response.body.data.updatePageLayoutWithTabsAndWidgets;
const widgets = result.tabs[0].widgets;
expect(widgets).toHaveLength(3);
const iframeWidget = widgets.find(
(w: any) => w.type === WidgetType.IFRAME,
);
expect(iframeWidget.configuration).toBeDefined();
expect(iframeWidget.configuration.url).toBe(TEST_IFRAME_CONFIG.url);
const graphWidget = widgets.find((w: any) => w.type === WidgetType.GRAPH);
expect(graphWidget.configuration).toBeDefined();
expect(graphWidget.configuration.graphType).toBe(
TEST_NUMBER_CHART_CONFIG_MINIMAL.graphType,
);
const viewWidget = widgets.find((w: any) => w.type === WidgetType.VIEW);
expect(viewWidget.configuration).toBeNull();
});
});
});