import { TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID } from 'test/integration/constants/test-page-layout-widget-ids.constants'; import { createPageLayoutWidgetOperationFactory } from 'test/integration/graphql/utils/create-page-layout-widget-operation-factory.util'; import { deletePageLayoutTabOperationFactory } from 'test/integration/graphql/utils/delete-page-layout-tab-operation-factory.util'; import { deletePageLayoutWidgetOperationFactory } from 'test/integration/graphql/utils/delete-page-layout-widget-operation-factory.util'; import { destroyPageLayoutWidgetOperationFactory } from 'test/integration/graphql/utils/destroy-page-layout-widget-operation-factory.util'; import { findPageLayoutWidgetOperationFactory } from 'test/integration/graphql/utils/find-page-layout-widget-operation-factory.util'; import { findPageLayoutWidgetsOperationFactory } from 'test/integration/graphql/utils/find-page-layout-widgets-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 { cleanupPageLayoutRecordsWithGraphQL, createTestPageLayoutWithGraphQL, } from 'test/integration/graphql/utils/page-layout-graphql.util'; import { createTestPageLayoutTabWithGraphQL } from 'test/integration/graphql/utils/page-layout-tab-graphql.util'; import { cleanupPageLayoutWidgetRecordsWithGraphQL, createTestPageLayoutWidgetWithGraphQL, } from 'test/integration/graphql/utils/page-layout-widget-graphql.util'; import { restorePageLayoutWidgetOperationFactory } from 'test/integration/graphql/utils/restore-page-layout-widget-operation-factory.util'; import { updatePageLayoutWidgetOperationFactory } from 'test/integration/graphql/utils/update-page-layout-widget-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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; import { assertPageLayoutWidgetStructure } from 'test/integration/utils/page-layout-widget-test.util'; import { isDefined } from 'twenty-shared/utils'; import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; import { type PageLayoutWidgetEntity } from 'src/engine/core-modules/page-layout/entities/page-layout-widget.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'; import { PageLayoutWidgetExceptionMessageKey, generatePageLayoutWidgetExceptionMessage, } from 'src/engine/core-modules/page-layout/exceptions/page-layout-widget.exception'; describe('Page Layout Widget Resolver', () => { let testObjectMetadataId: string; let testPageLayoutId: string; let testPageLayoutTabId: string; beforeAll(async () => { const { data: { createOneObject: { id: objectMetadataId }, }, } = await createOneObjectMetadata({ input: { nameSingular: 'myTestPageLayoutWidgetObject', namePlural: 'myTestPageLayoutWidgetObjects', labelSingular: 'My Test Page Layout Widget Object', labelPlural: 'My Test Page Layout Widget Objects', icon: 'IconWidget', }, }); testObjectMetadataId = objectMetadataId; const pageLayout = await createTestPageLayoutWithGraphQL({ name: 'Test Page Layout for Widgets', type: PageLayoutType.RECORD_PAGE, objectMetadataId: testObjectMetadataId, }); testPageLayoutId = pageLayout.id; const pageLayoutTab = await createTestPageLayoutTabWithGraphQL({ title: 'Test Tab for Widgets', position: 0, pageLayoutId: testPageLayoutId, }); testPageLayoutTabId = pageLayoutTab.id; }); afterAll(async () => { await cleanupPageLayoutRecordsWithGraphQL(); await updateOneObjectMetadata({ expectToFail: false, input: { idToUpdate: testObjectMetadataId, updatePayload: { isActive: false, }, }, }); await deleteOneObjectMetadata({ input: { idToDelete: testObjectMetadataId }, }); }); beforeEach(() => { jest.clearAllMocks(); }); afterEach(async () => { await cleanupPageLayoutWidgetRecordsWithGraphQL(testPageLayoutTabId); }); describe('getPageLayoutWidgets', () => { it('should return empty array when no page layout widgets exist', async () => { const operation = findPageLayoutWidgetsOperationFactory({ pageLayoutTabId: testPageLayoutTabId, }); const response = await makeGraphqlAPIRequest(operation); assertGraphQLSuccessfulResponse(response); expect(response.body.data.getPageLayoutWidgets).toEqual([]); }); it('should return all page layout widgets for a specific page layout tab', async () => { const widgetTitle1 = 'Widget 1'; const widgetTitle2 = 'Widget 2'; const [{ id: widget1Id }, { id: widget2Id }] = await Promise.all([ createTestPageLayoutWidgetWithGraphQL({ title: widgetTitle1, type: WidgetType.VIEW, pageLayoutTabId: testPageLayoutTabId, gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 3 }, }), createTestPageLayoutWidgetWithGraphQL({ title: widgetTitle2, type: WidgetType.GRAPH, pageLayoutTabId: testPageLayoutTabId, gridPosition: { row: 0, column: 3, rowSpan: 1, columnSpan: 2 }, }), ]); const operation = findPageLayoutWidgetsOperationFactory({ pageLayoutTabId: testPageLayoutTabId, }); const response = await makeGraphqlAPIRequest(operation); assertGraphQLSuccessfulResponse(response); expect(response.body.data.getPageLayoutWidgets).toHaveLength(2); const widgets: PageLayoutWidgetEntity[] = response.body.data.getPageLayoutWidgets ?? []; const widget1 = widgets.find((widget) => widget.id === widget1Id); const widget2 = widgets.find((widget) => widget.id === widget2Id); if (!isDefined(widget1) || !isDefined(widget2)) { throw new Error('Widget not found'); } assertPageLayoutWidgetStructure(widget1, { title: widgetTitle1, type: WidgetType.VIEW, pageLayoutTabId: testPageLayoutTabId, }); assertPageLayoutWidgetStructure(widget2, { title: widgetTitle2, type: WidgetType.GRAPH, pageLayoutTabId: testPageLayoutTabId, }); }); }); describe('getPageLayoutWidget', () => { it('should throw when page layout widget does not exist', async () => { const operation = findPageLayoutWidgetOperationFactory({ pageLayoutWidgetId: TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, }); const response = await makeGraphqlAPIRequest(operation); assertGraphQLErrorResponse( response, ErrorCode.NOT_FOUND, generatePageLayoutWidgetExceptionMessage( PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND, TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, ), ); }); it('should return page layout widget when it exists', async () => { const widgetTitle = 'Widget'; const input = { title: widgetTitle, type: WidgetType.FIELDS, pageLayoutTabId: testPageLayoutTabId, objectMetadataId: testObjectMetadataId, gridPosition: { row: 1, column: 1, rowSpan: 1, columnSpan: 2 }, configuration: null, }; const widget = await createTestPageLayoutWidgetWithGraphQL(input); const operation = findPageLayoutWidgetOperationFactory({ pageLayoutWidgetId: widget.id, }); const response = await makeGraphqlAPIRequest(operation); assertGraphQLSuccessfulResponse(response); assertPageLayoutWidgetStructure( response.body.data.getPageLayoutWidget, input, ); }); }); describe('createPageLayoutWidget', () => { it('should create a new page layout widget with all properties', async () => { const input = { title: 'New Widget', type: WidgetType.FIELDS, pageLayoutTabId: testPageLayoutTabId, objectMetadataId: testObjectMetadataId, gridPosition: { row: 2, column: 1, rowSpan: 3, columnSpan: 4 }, configuration: null, }; const operation = createPageLayoutWidgetOperationFactory({ data: input }); const response = await makeGraphqlAPIRequest(operation); assertGraphQLSuccessfulResponse(response); const createdWidget = response.body.data.createPageLayoutWidget; assertPageLayoutWidgetStructure(createdWidget, input); }); it('should create a page layout widget with minimum required fields', async () => { const input = { title: 'Widget', type: WidgetType.VIEW, pageLayoutTabId: testPageLayoutTabId, gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 }, }; const operation = createPageLayoutWidgetOperationFactory({ data: input }); const response = await makeGraphqlAPIRequest(operation); assertGraphQLSuccessfulResponse(response); const createdWidget = response.body.data.createPageLayoutWidget; assertPageLayoutWidgetStructure(createdWidget, input); }); }); describe('updatePageLayoutWidget', () => { it('should update an existing page layout widget', async () => { const widget = await createTestPageLayoutWidgetWithGraphQL({ title: 'Widget', type: WidgetType.VIEW, pageLayoutTabId: testPageLayoutTabId, gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 }, }); const updateInput = { title: 'Updated Widget', type: WidgetType.IFRAME, gridPosition: { row: 1, column: 2, rowSpan: 2, columnSpan: 3 }, configuration: null, }; const operation = updatePageLayoutWidgetOperationFactory({ pageLayoutWidgetId: widget.id, data: updateInput, }); const response = await makeGraphqlAPIRequest(operation); assertGraphQLSuccessfulResponse(response); assertPageLayoutWidgetStructure( response.body.data.updatePageLayoutWidget, updateInput, ); }); it('should update only provided fields', async () => { const widget = await createTestPageLayoutWidgetWithGraphQL({ title: 'Widget', type: WidgetType.VIEW, pageLayoutTabId: testPageLayoutTabId, gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 }, }); const updateInput = { title: 'Updated Widget', }; const operation = updatePageLayoutWidgetOperationFactory({ pageLayoutWidgetId: widget.id, data: updateInput, }); const response = await makeGraphqlAPIRequest(operation); assertGraphQLSuccessfulResponse(response); assertPageLayoutWidgetStructure( response.body.data.updatePageLayoutWidget, updateInput, ); }); it('should throw error when updating non-existent page layout widget', async () => { const operation = updatePageLayoutWidgetOperationFactory({ pageLayoutWidgetId: TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, data: { title: 'Non-existent Widget' }, }); const response = await makeGraphqlAPIRequest(operation); assertGraphQLErrorResponse( response, ErrorCode.NOT_FOUND, generatePageLayoutWidgetExceptionMessage( PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND, TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, ), ); }); }); describe('deletePageLayoutWidget', () => { it('should delete an existing page layout widget (soft delete)', async () => { const input = { title: 'Widget', type: WidgetType.VIEW, pageLayoutTabId: testPageLayoutTabId, gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 }, }; const widget = await createTestPageLayoutWidgetWithGraphQL(input); const deleteOperation = deletePageLayoutWidgetOperationFactory({ pageLayoutWidgetId: widget.id, }); const deleteResponse = await makeGraphqlAPIRequest(deleteOperation); assertGraphQLSuccessfulResponse(deleteResponse); assertPageLayoutWidgetStructure( deleteResponse.body.data.deletePageLayoutWidget, input, ); const getOperation = findPageLayoutWidgetOperationFactory({ pageLayoutWidgetId: widget.id, }); const getResponse = await makeGraphqlAPIRequest(getOperation); assertGraphQLErrorResponse( getResponse, ErrorCode.NOT_FOUND, generatePageLayoutWidgetExceptionMessage( PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND, widget.id, ), ); }); it('should throw an error when deleting non-existent page layout widget', async () => { const operation = deletePageLayoutWidgetOperationFactory({ pageLayoutWidgetId: TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, }); const response = await makeGraphqlAPIRequest(operation); assertGraphQLErrorResponse( response, ErrorCode.NOT_FOUND, generatePageLayoutWidgetExceptionMessage( PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND, TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, ), ); }); }); describe('destroyPageLayoutWidget', () => { it('should destroy an existing page layout widget (hard delete)', async () => { const input = { title: 'Widget', type: WidgetType.VIEW, pageLayoutTabId: testPageLayoutTabId, gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 }, }; const widget = await createTestPageLayoutWidgetWithGraphQL(input); const destroyOperation = destroyPageLayoutWidgetOperationFactory({ pageLayoutWidgetId: widget.id, }); const destroyResponse = await makeGraphqlAPIRequest(destroyOperation); assertGraphQLSuccessfulResponse(destroyResponse); expect(destroyResponse.body.data.destroyPageLayoutWidget).toBe(true); }); it('should throw an error when destroying non-existent page layout widget', async () => { const operation = destroyPageLayoutWidgetOperationFactory({ pageLayoutWidgetId: TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, }); const response = await makeGraphqlAPIRequest(operation); assertGraphQLErrorResponse( response, ErrorCode.NOT_FOUND, generatePageLayoutWidgetExceptionMessage( PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND, TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, ), ); }); }); describe('restorePageLayoutWidget', () => { it('should restore a soft deleted page layout widget', async () => { const input = { title: 'Widget', type: WidgetType.GRAPH, pageLayoutTabId: testPageLayoutTabId, gridPosition: { row: 2, column: 1, rowSpan: 1, columnSpan: 2 }, configuration: null, }; const widget = await createTestPageLayoutWidgetWithGraphQL(input); const deleteOperation = deletePageLayoutWidgetOperationFactory({ pageLayoutWidgetId: widget.id, }); const deleteResponse = await makeGraphqlAPIRequest(deleteOperation); assertGraphQLSuccessfulResponse(deleteResponse); const restoreOperation = restorePageLayoutWidgetOperationFactory({ pageLayoutWidgetId: widget.id, }); const restoreResponse = await makeGraphqlAPIRequest(restoreOperation); assertGraphQLSuccessfulResponse(restoreResponse); assertPageLayoutWidgetStructure( restoreResponse.body.data.restorePageLayoutWidget, input, ); const getOperation = findPageLayoutWidgetOperationFactory({ pageLayoutWidgetId: widget.id, }); const getResponse = await makeGraphqlAPIRequest(getOperation); assertGraphQLSuccessfulResponse(getResponse); assertPageLayoutWidgetStructure( getResponse.body.data.getPageLayoutWidget, input, ); }); it('should throw an error when restoring non-existent page layout widget', async () => { const operation = restorePageLayoutWidgetOperationFactory({ pageLayoutWidgetId: TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, }); const response = await makeGraphqlAPIRequest(operation); assertGraphQLErrorResponse( response, ErrorCode.NOT_FOUND, generatePageLayoutWidgetExceptionMessage( PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND, TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, ), ); }); it('should throw an error when restoring widget with deleted parent tab', async () => { const separateTab = await createTestPageLayoutTabWithGraphQL({ title: 'Tab for Widget Restore Test', pageLayoutId: testPageLayoutId, position: 1, }); const input = { title: 'Widget with Deleted Tab', type: WidgetType.GRAPH, pageLayoutTabId: separateTab.id, gridPosition: { row: 1, column: 1, rowSpan: 1, columnSpan: 1 }, configuration: null, }; const widget = await createTestPageLayoutWidgetWithGraphQL(input); const deleteWidgetOperation = deletePageLayoutWidgetOperationFactory({ pageLayoutWidgetId: widget.id, }); await makeGraphqlAPIRequest(deleteWidgetOperation); const deleteTabOperation = deletePageLayoutTabOperationFactory({ pageLayoutTabId: separateTab.id, }); await makeGraphqlAPIRequest(deleteTabOperation); const restoreOperation = restorePageLayoutWidgetOperationFactory({ pageLayoutWidgetId: widget.id, }); const restoreResponse = await makeGraphqlAPIRequest(restoreOperation); assertGraphQLErrorResponse( restoreResponse, ErrorCode.BAD_USER_INPUT, generatePageLayoutWidgetExceptionMessage( PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND, ), ); }); }); });