From cd71e5666a8ef51ed351adac29f9b0fce8038ac3 Mon Sep 17 00:00:00 2001 From: Lotte Pitcher Date: Tue, 22 Apr 2025 05:51:08 +0100 Subject: [PATCH 01/17] Incorrect forum and security urls when raising issue (#19080) --- .github/ISSUE_TEMPLATE/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index ecf10b8854..0681900d88 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -4,11 +4,11 @@ contact_links: url: https://github.com/umbraco/Umbraco-CMS/discussions/new?category=features-and-ideas about: Start a new discussion when you have ideas or feature requests, eventually discussions can turn into plans - name: ⁉️ Support Question - url: https://our.umbraco.com + url: https://forum.umbraco.com about: This issue tracker is NOT meant for support questions. If you have a question, please join us on the forum. - name: 📖 Documentation Issue url: https://github.com/umbraco/UmbracoDocs/issues about: Documentation issues should be reported on the Umbraco documentation repository. - name: 🔐 Security Issue - url: https://umbraco.com/about-us/trust-center/security-and-umbraco/how-to-report-a-vulnerability-in-umbraco/ + url: https://umbraco.com/trust-center/security-and-umbraco/how-to-report-a-vulnerability-in-umbraco/ about: Discovered a Security Issue in Umbraco? From 15643135bace98282a25aa6fcff339406032fb4a Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Thu, 24 Apr 2025 11:25:58 +0100 Subject: [PATCH 02/17] Add 'ManifestWithDynamicConditions' to ManifestHeaderApp so Header Apps can be conditionally shown/loaded (#19124) --- .../extension-registry/extensions/header-app.extension.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extensions/header-app.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extensions/header-app.extension.ts index 58ac1f5d7d..b76d659278 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extensions/header-app.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extensions/header-app.extension.ts @@ -1,10 +1,10 @@ -import type { ManifestElement } from '@umbraco-cms/backoffice/extension-api'; +import type { ManifestElement, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; /** * Header apps are displayed in the top right corner of the backoffice * The two provided header apps are the search and the user menu */ -export interface ManifestHeaderApp extends ManifestElement { +export interface ManifestHeaderApp extends ManifestElement, ManifestWithDynamicConditions { type: 'headerApp'; //meta: MetaHeaderApp; } From 3b6e4a96f141b6fa3d053ece92543cd892d6b3b9 Mon Sep 17 00:00:00 2001 From: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> Date: Sat, 26 Apr 2025 09:59:03 +0700 Subject: [PATCH 03/17] V15 QA Added acceptance tests for bulk trash dialog (#19125) * Added tests for bulk trash content dialog * Updated tests for trash content dialog * Added tests for trash and bulk trash media dialog * Moved trash content tests into a folder * Bumped version * Make trash tests run in the pipeline * Make trash tests run in the pipeline * Fixed comments * Reverted npm command --- .../package-lock.json | 20 ++-- .../Umbraco.Tests.AcceptanceTest/package.json | 4 +- .../TrashContent/BulkTrashContent.spec.ts | 93 +++++++++++++++++++ .../{ => TrashContent}/TrashContent.spec.ts | 2 +- .../tests/DefaultConfig/Media/Media.spec.ts | 78 ++++++++++++++++ 5 files changed, 183 insertions(+), 14 deletions(-) create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent/BulkTrashContent.spec.ts rename tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/{ => TrashContent}/TrashContent.spec.ts (99%) diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index b6ee78da3a..635fb500f2 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -7,8 +7,8 @@ "name": "acceptancetest", "hasInstallScript": true, "dependencies": { - "@umbraco/json-models-builders": "^2.0.31", - "@umbraco/playwright-testhelpers": "^15.0.44", + "@umbraco/json-models-builders": "^2.0.33", + "@umbraco/playwright-testhelpers": "^15.0.47", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" @@ -58,21 +58,19 @@ } }, "node_modules/@umbraco/json-models-builders": { - "version": "2.0.32", - "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.32.tgz", - "integrity": "sha512-Aw7yBu8ePNxdjS7Q61j5KPFsiOS+IGCYxBX0H4KWbjXTdvL/PsB98KiqbDHHKFnp0fF1b2ffwJAI6jmvnxPBzg==", - "license": "MIT", + "version": "2.0.33", + "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.33.tgz", + "integrity": "sha512-FAQxQIHoY6PGxWuodp4LSQxNufnOiqnaRNmtG8Ejn01r9lJJdA27CJKJ0bCs8U0W3cdN+Z+j7jBLd8H025/THw==", "dependencies": { "camelize": "^1.0.1" } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "15.0.44", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.44.tgz", - "integrity": "sha512-rbStBJG0bdVfs9pzEjAhArhcPoQbuvrUpvDJLbEMBcg54lRFH0I5nzcAimdZaY4UWh2r0i9ANfjv9E25NJxRUg==", - "license": "MIT", + "version": "15.0.47", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.47.tgz", + "integrity": "sha512-rYr3IU1O/mcOw74zHxXyqa7eRkxci2G6yMgWmTDKdYrB1jPFrElWvn5GQVVP6r9znH4z1VkzPT0rWmuPI8rP/w==", "dependencies": { - "@umbraco/json-models-builders": "2.0.32", + "@umbraco/json-models-builders": "2.0.33", "node-fetch": "^2.6.7" } }, diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 524acec6eb..beffdfba62 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -20,8 +20,8 @@ "typescript": "^4.8.3" }, "dependencies": { - "@umbraco/json-models-builders": "^2.0.31", - "@umbraco/playwright-testhelpers": "^15.0.44", + "@umbraco/json-models-builders": "^2.0.33", + "@umbraco/playwright-testhelpers": "^15.0.47", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent/BulkTrashContent.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent/BulkTrashContent.spec.ts new file mode 100644 index 0000000000..957c330e65 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent/BulkTrashContent.spec.ts @@ -0,0 +1,93 @@ +import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +let collectionId = ''; +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const childDocumentTypeName = 'TestChildDocumentType'; +const firstChildContentName = 'First Child Content'; +const secondChildContentName = 'Second Child Content'; +const collectionDataTypeName = 'List View - Content'; +const referenceHeadline = ConstantHelper.trashDeleteDialogMessage.bulkReferenceHeadline; +const documentPickerName = ['TestPicker', 'DocumentTypeForPicker']; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + const collectionDataTypeData = await umbracoApi.dataType.getByName(collectionDataTypeName); + collectionId = collectionDataTypeData.id; +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.documentType.ensureNameNotExists(childDocumentTypeName); + await umbracoApi.document.emptyRecycleBin(); +}); + +test('can bulk trash content nodes without a relation', async ({umbracoApi, umbracoUi}) => { + // Arrange + const childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNodeAndCollectionId(documentTypeName, childDocumentTypeId, collectionId); + const contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoApi.document.createDefaultDocumentWithParent(firstChildContentName, childDocumentTypeId, contentId); + await umbracoApi.document.createDefaultDocumentWithParent(secondChildContentName, childDocumentTypeId, contentId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.selectContentWithNameInListView(firstChildContentName); + await umbracoUi.content.selectContentWithNameInListView(secondChildContentName); + await umbracoUi.content.clickTrashSelectedListItems(); + // Verify the references list not displayed + await umbracoUi.content.isReferenceHeadlineVisible(false); + await umbracoUi.content.clickConfirmTrashButton(); + + // // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(firstChildContentName)).toBeFalsy(); + expect(await umbracoApi.document.doesNameExist(secondChildContentName)).toBeFalsy(); + await umbracoUi.content.isItemVisibleInRecycleBin(firstChildContentName); + await umbracoUi.content.isItemVisibleInRecycleBin(secondChildContentName); + expect(await umbracoApi.document.doesItemExistInRecycleBin(firstChildContentName)).toBeTruthy(); + expect(await umbracoApi.document.doesItemExistInRecycleBin(secondChildContentName)).toBeTruthy(); +}); + +test('can bulk trash content nodes with a relation', async ({umbracoApi, umbracoUi}) => { + // Arrange + const childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNodeAndCollectionId(documentTypeName, childDocumentTypeId, collectionId); + const contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoApi.document.publish(contentId); + const firstChildContentId = await umbracoApi.document.createDefaultDocumentWithParent(firstChildContentName, childDocumentTypeId, contentId); + await umbracoApi.document.publish(firstChildContentId); + await umbracoApi.document.createDefaultDocumentWithParent(secondChildContentName, childDocumentTypeId, contentId); + // Create a document that has a document picker with firstChildContentName + await umbracoApi.document.createDefaultDocumentWithOneDocumentLink(documentPickerName[0], firstChildContentName, firstChildContentId, documentPickerName[1]); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.selectContentWithNameInListView(firstChildContentName); + await umbracoUi.content.selectContentWithNameInListView(secondChildContentName); + await umbracoUi.content.clickTrashSelectedListItems(); + // Verify the references list + await umbracoUi.content.doesReferenceHeadlineHaveText(referenceHeadline); + await umbracoUi.content.doesReferenceItemsHaveCount(1); + await umbracoUi.content.isReferenceItemNameVisible(firstChildContentName); + await umbracoUi.content.clickConfirmTrashButton(); + + // // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(firstChildContentName)).toBeFalsy(); + expect(await umbracoApi.document.doesNameExist(secondChildContentName)).toBeFalsy(); + await umbracoUi.content.isItemVisibleInRecycleBin(firstChildContentName); + await umbracoUi.content.isItemVisibleInRecycleBin(secondChildContentName); + expect(await umbracoApi.document.doesItemExistInRecycleBin(firstChildContentName)).toBeTruthy(); + expect(await umbracoApi.document.doesItemExistInRecycleBin(secondChildContentName)).toBeTruthy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(documentPickerName[1]); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent/TrashContent.spec.ts similarity index 99% rename from tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent.spec.ts rename to tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent/TrashContent.spec.ts index c008ea38ba..4214ff691a 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent/TrashContent.spec.ts @@ -6,7 +6,7 @@ const contentName = 'TestContent'; const documentTypeName = 'TestDocumentTypeForContent'; const dataTypeName = 'Textstring'; const contentText = 'This is test content text'; -const referenceHeadline = 'The following items depend on this'; +const referenceHeadline = ConstantHelper.trashDeleteDialogMessage.referenceHeadline; const documentPickerName = ['TestPicker', 'DocumentTypeForPicker']; test.beforeEach(async ({umbracoApi}) => { diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts index 8e7f878166..8eaedb8313 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts @@ -174,6 +174,8 @@ test('can trash a media item', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.media.clickActionsMenuForName(mediaFileName); await umbracoUi.media.clickTrashButton(); + // Verify the references list not displayed + await umbracoUi.content.isReferenceHeadlineVisible(false); await umbracoUi.media.clickConfirmTrashButton(); // Assert @@ -245,3 +247,79 @@ test('can empty the recycle bin', async ({umbracoApi, umbracoUi}) => { expect(await umbracoApi.media.doesNameExist(mediaFileName)).toBeFalsy(); expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(mediaFileName)).toBeFalsy(); }); + +test('can trash a media node with a relation', async ({umbracoApi, umbracoUi}) => { + // Arrange + const documentPickerName = ['TestPicker', 'DocumentTypeForPicker']; + await umbracoApi.media.emptyRecycleBin(); + await umbracoApi.media.createDefaultMediaFile(mediaFileName); + await umbracoApi.media.doesNameExist(mediaFileName); + // Create a document that have media picker is firstMediaFileName + await umbracoApi.document.createDefaultDocumentWithOneMediaLink(documentPickerName[0], mediaFileName, documentPickerName[1]); + await umbracoUi.media.goToSection(ConstantHelper.sections.media); + + // Act + await umbracoUi.media.clickActionsMenuForName(mediaFileName); + await umbracoUi.media.clickTrashButton(); + // Verify the references list + await umbracoUi.media.doesReferenceHeadlineHaveText(ConstantHelper.trashDeleteDialogMessage.referenceHeadline); + await umbracoUi.media.doesReferenceItemsHaveCount(1); + await umbracoUi.media.isReferenceItemNameVisible(documentPickerName[0]); + await umbracoUi.media.clickConfirmTrashButton(); + + // Assert + await umbracoUi.media.doesSuccessNotificationHaveText(NotificationConstantHelper.success.movedToRecycleBin); + await umbracoUi.media.isItemVisibleInRecycleBin(mediaFileName); + expect(await umbracoApi.media.doesNameExist(mediaFileName)).toBeFalsy(); + expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(mediaFileName)).toBeTruthy(); + + // Clean + await umbracoApi.media.emptyRecycleBin(); + await umbracoApi.document.ensureNameNotExists(documentPickerName[0]); + await umbracoApi.documentType.ensureNameNotExists(documentPickerName[1]); +}); + +test('can bulk trash media nodes with a relation', async ({umbracoApi, umbracoUi}) => { + // Arrange + const firstMediaFileName = 'FirstMediaFile'; + const secondMediaFileName = 'SecondMediaFile'; + const documentPickerName1 = ['TestPicker1', 'DocumentTypeForPicker1']; + const documentPickerName2 = ['TestPicker2', 'DocumentTypeForPicker2']; + await umbracoApi.media.emptyRecycleBin(); + await umbracoApi.media.createDefaultMediaFile(firstMediaFileName); + await umbracoApi.media.createDefaultMediaFile(secondMediaFileName); + // Create a document that has a media picker with firstMediaFileName + await umbracoApi.document.createDefaultDocumentWithOneMediaLink(documentPickerName1[0], firstMediaFileName, documentPickerName1[1]); + // Create a document that has a media picker with secondMediaFileName + await umbracoApi.document.createDefaultDocumentWithOneMediaLink(documentPickerName2[0], secondMediaFileName, documentPickerName2[1]); + + // Act + await umbracoUi.media.goToSection(ConstantHelper.sections.media); + await umbracoUi.media.selectMediaWithName(firstMediaFileName); + await umbracoUi.media.selectMediaWithName(secondMediaFileName); + await umbracoUi.media.clickBulkTrashButton(); + // Verify the references list + await umbracoUi.media.doesReferenceHeadlineHaveText(ConstantHelper.trashDeleteDialogMessage.bulkReferenceHeadline); + await umbracoUi.media.doesReferenceItemsHaveCount(2); + await umbracoUi.media.isReferenceItemNameVisible(firstMediaFileName); + await umbracoUi.media.isReferenceItemNameVisible(secondMediaFileName); + await umbracoUi.media.clickConfirmTrashButton(); + + // Assert + await umbracoUi.media.isSuccessNotificationVisible(); + expect(await umbracoApi.media.doesNameExist(firstMediaFileName)).toBeFalsy(); + expect(await umbracoApi.media.doesNameExist(secondMediaFileName)).toBeFalsy(); + expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(firstMediaFileName)).toBeTruthy(); + expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(secondMediaFileName)).toBeTruthy(); + await umbracoUi.media.isItemVisibleInRecycleBin(firstMediaFileName); + await umbracoUi.media.isItemVisibleInRecycleBin(secondMediaFileName, true, false); + + // Clean + await umbracoApi.media.ensureNameNotExists(firstMediaFileName); + await umbracoApi.media.ensureNameNotExists(secondMediaFileName); + await umbracoApi.document.ensureNameNotExists(documentPickerName1[0]); + await umbracoApi.documentType.ensureNameNotExists(documentPickerName1[1]); + await umbracoApi.document.ensureNameNotExists(documentPickerName2[0]); + await umbracoApi.documentType.ensureNameNotExists(documentPickerName2[1]); + await umbracoApi.media.emptyRecycleBin(); +}); \ No newline at end of file From 3579c4ac9e4e618e07393cbc1a79924242cf5e36 Mon Sep 17 00:00:00 2001 From: Lotte Pitcher Date: Sat, 19 Apr 2025 22:20:45 +0100 Subject: [PATCH 04/17] readme shield for forum --- .github/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/README.md b/.github/README.md index f4e3f76009..63b908daed 100644 --- a/.github/README.md +++ b/.github/README.md @@ -4,8 +4,8 @@ [![NuGet Version](https://img.shields.io/nuget/v/Umbraco.Cms)](https://www.nuget.org/packages/Umbraco.Cms) [![Build status](https://img.shields.io/azure-devops/build/umbraco/Umbraco%2520Cms/301?logo=azurepipelines&label=Azure%20Pipelines)](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=301) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) +[![Forum](https://img.shields.io/badge/help-forum-blue)](https://forum.umbraco.com) [![Chat about Umbraco on Discord](https://img.shields.io/discord/869656431308189746?logo=discord&logoColor=fff)](https://discord.gg/umbraco) -[![Read what's going on in the Umbraco Discord chat now](https://img.shields.io/badge/read-discord-blue)](https://discord-chats.umbraco.com) ![Mastodon Follow](https://img.shields.io/mastodon/follow/110661369750014952?domain=https%3A%2F%2Fumbracocommunity.social) From dfc6ead6d5b1463165a9334ee22c7b3839b477b0 Mon Sep 17 00:00:00 2001 From: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:39:56 +0700 Subject: [PATCH 05/17] V15 Added acceptance tests for tiptap statusbar (#19131) * Updated tests for tiptap RTE * Moved tests for titptap toolbar to another class * Added tests for titptap toolbar * Added tests for tiptap statusbar * Bumped version * Make tiptap tests run in the pipeline * Bumped version * Reverted npm command --- .../package-lock.json | 8 +- .../Umbraco.Tests.AcceptanceTest/package.json | 2 +- .../RichTextEditor/ContentWithTiptap.spec.ts | 132 ------------ .../RichTextEditor/TiptapToolbar.spec.ts | 190 ++++++++++++++++++ .../DataType/RichTextEditor.spec.ts | 4 +- .../DefaultConfig/DataType/Tiptap.spec.ts | 38 ++++ 6 files changed, 235 insertions(+), 139 deletions(-) create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/TiptapToolbar.spec.ts diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 635fb500f2..1353eb665a 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -8,7 +8,7 @@ "hasInstallScript": true, "dependencies": { "@umbraco/json-models-builders": "^2.0.33", - "@umbraco/playwright-testhelpers": "^15.0.47", + "@umbraco/playwright-testhelpers": "^15.0.49", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" @@ -66,9 +66,9 @@ } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "15.0.47", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.47.tgz", - "integrity": "sha512-rYr3IU1O/mcOw74zHxXyqa7eRkxci2G6yMgWmTDKdYrB1jPFrElWvn5GQVVP6r9znH4z1VkzPT0rWmuPI8rP/w==", + "version": "15.0.49", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.49.tgz", + "integrity": "sha512-1At/e057u6rB3T3iH8tR6SLXnYRZJsCVjmm8jm+6sftJDvgB0Q5kXKaSDyLTU6wVuLALiDNUuNuJ86FgOOdUJw==", "dependencies": { "@umbraco/json-models-builders": "2.0.33", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index beffdfba62..98d612ca81 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^2.0.33", - "@umbraco/playwright-testhelpers": "^15.0.47", + "@umbraco/playwright-testhelpers": "^15.0.49", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/ContentWithTiptap.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/ContentWithTiptap.spec.ts index cbac5c752e..8652b59137 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/ContentWithTiptap.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/ContentWithTiptap.spec.ts @@ -84,136 +84,4 @@ test('can publish content with RTE Tiptap property editor', async ({umbracoApi, const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.variants[0].state).toBe(expectedState); expect(contentData.values[0].value.markup).toEqual('

' + inputText + '

'); -}); - -test.fixme('can add a media in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { - // Arrange - const iconTitle = 'Media Picker'; - const imageName = 'Test Image For Content'; - await umbracoApi.media.ensureNameNotExists(imageName); - await umbracoApi.media.createDefaultMediaWithImage(imageName); - const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); - await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); - await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - - // Act - await umbracoUi.content.goToContentWithName(contentName); - await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); - // fix this - await umbracoUi.content.selectMediaWithName(imageName); - await umbracoUi.content.clickChooseModalButton(); - await umbracoUi.content.clickMediaCaptionAltTextModalSubmitButton(); - await umbracoUi.content.clickSaveButton(); - - // Assert - await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); - expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); - const contentData = await umbracoApi.document.getByName(contentName); - expect(contentData.values[0].value.markup).toContain(' { - // Arrange - const iconTitle = 'Embed'; - const videoURL = 'https://www.youtube.com/watch?v=Yu29dE-0OoI'; - const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); - await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); - await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - - // Act - await umbracoUi.content.goToContentWithName(contentName); - await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); - await umbracoUi.content.enterEmbeddedURL(videoURL); - await umbracoUi.content.clickEmbeddedRetrieveButton(); - await umbracoUi.content.waitForEmbeddedPreviewVisible(); - await umbracoUi.content.clickEmbeddedMediaModalConfirmButton(); - await umbracoUi.content.clickSaveButton(); - - // Assert - await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); - expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); - const contentData = await umbracoApi.document.getByName(contentName); - expect(contentData.values[0].value.markup).toContain('data-embed-url'); - expect(contentData.values[0].value.markup).toContain(videoURL); -}); - -test('cannot submit an empty link in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { - // Arrange - const iconTitle = 'Link'; - const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); - await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); - await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - - // Act - await umbracoUi.content.goToContentWithName(contentName); - await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); - await umbracoUi.content.clickManualLinkButton(); - await umbracoUi.content.enterLink(''); - await umbracoUi.content.enterAnchorOrQuerystring(''); - await umbracoUi.content.enterLinkTitle(''); - await umbracoUi.content.clickAddButton(); - - // Assert - await umbracoUi.content.isTextWithMessageVisible(ConstantHelper.validationMessages.emptyLinkPicker); -}); - -// TODO: Remove skip when the front-end ready. Currently it still accept the empty link with an anchor or querystring -// Issue link: https://github.com/umbraco/Umbraco-CMS/issues/17411 -test.skip('cannot submit an empty URL with an anchor or querystring in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { - // Arrange - const iconTitle = 'Link'; - const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); - await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); - await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - - // Act - await umbracoUi.content.goToContentWithName(contentName); - await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); - await umbracoUi.content.clickManualLinkButton(); - await umbracoUi.content.enterLink(''); - await umbracoUi.content.enterAnchorOrQuerystring('#value'); - await umbracoUi.content.clickAddButton(); - - // Assert - await umbracoUi.content.isTextWithMessageVisible(ConstantHelper.validationMessages.emptyLinkPicker); -}); - -// TODO: Remove skip when the front-end ready. Currently it is impossible to link to unpublished document -// Issue link: https://github.com/umbraco/Umbraco-CMS/issues/17974 -test.skip('can insert a link to an unpublished document in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { - // Arrange - const iconTitle = 'Link'; - const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); - await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); - // Create a document to link - const documentTypeForLinkedDocumentName = 'TestDocumentType'; - const documentTypeForLinkedDocumentId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeForLinkedDocumentName); - const linkedDocumentName = 'LinkedDocument'; - await umbracoApi.document.createDefaultDocument(linkedDocumentName, documentTypeForLinkedDocumentId); - await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - - // Act - await umbracoUi.content.goToContentWithName(contentName); - await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); - await umbracoUi.content.clickDocumentLinkButton(); - await umbracoUi.content.selectLinkByName(linkedDocumentName); - await umbracoUi.content.clickButtonWithName('Choose'); - await umbracoUi.content.clickAddButton(); - await umbracoUi.content.clickSaveButton(); - - // Assert - await umbracoUi.content.isSuccessNotificationVisible(); - - // Clean - await umbracoApi.documentType.ensureNameNotExists(documentTypeForLinkedDocumentName); - await umbracoApi.document.ensureNameNotExists(linkedDocumentName); }); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/TiptapToolbar.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/TiptapToolbar.spec.ts new file mode 100644 index 0000000000..3f4cf6d572 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/TiptapToolbar.spec.ts @@ -0,0 +1,190 @@ +import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const customDataTypeName = 'Test RTE Tiptap'; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + +test('can add a media in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const iconTitle = 'Media Picker'; + const imageName = 'Test Image For Content'; + await umbracoApi.media.ensureNameNotExists(imageName); + await umbracoApi.media.createDefaultMediaWithImage(imageName); + const customDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customDataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); + await umbracoUi.content.selectMediaWithName(imageName); + await umbracoUi.content.clickChooseModalButton(); + await umbracoUi.content.clickMediaCaptionAltTextModalSubmitButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].value.markup).toContain(' { + // Arrange + const iconTitle = 'Embed'; + const videoURL = 'https://www.youtube.com/watch?v=Yu29dE-0OoI'; + const customDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customDataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); + await umbracoUi.content.enterEmbeddedURL(videoURL); + await umbracoUi.content.clickEmbeddedRetrieveButton(); + await umbracoUi.content.waitForEmbeddedPreviewVisible(); + await umbracoUi.content.clickEmbeddedMediaModalConfirmButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].value.markup).toContain('data-embed-url'); + expect(contentData.values[0].value.markup).toContain(videoURL); +}); + +test('cannot submit an empty link in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const iconTitle = 'Link'; + const customDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customDataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); + await umbracoUi.content.clickManualLinkButton(); + await umbracoUi.content.enterLink(''); + await umbracoUi.content.enterAnchorOrQuerystring(''); + await umbracoUi.content.enterLinkTitle(''); + await umbracoUi.content.clickAddButton(); + + // Assert + await umbracoUi.content.isTextWithMessageVisible(ConstantHelper.validationMessages.emptyLinkPicker); +}); + +// TODO: Remove skip when the front-end ready. Currently it still accept the empty link with an anchor or querystring +// Issue link: https://github.com/umbraco/Umbraco-CMS/issues/17411 +test.skip('cannot submit an empty URL with an anchor or querystring in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const iconTitle = 'Link'; + const customDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customDataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); + await umbracoUi.content.clickManualLinkButton(); + await umbracoUi.content.enterLink(''); + await umbracoUi.content.enterAnchorOrQuerystring('#value'); + await umbracoUi.content.clickAddButton(); + + // Assert + await umbracoUi.content.isTextWithMessageVisible(ConstantHelper.validationMessages.emptyLinkPicker); +}); + +// TODO: Remove skip when the front-end ready. Currently it is impossible to link to unpublished document +// Issue link: https://github.com/umbraco/Umbraco-CMS/issues/17974 +test.skip('can insert a link to an unpublished document in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const iconTitle = 'Link'; + const customDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customDataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + // Create a document to link + const documentTypeForLinkedDocumentName = 'TestDocumentType'; + const documentTypeForLinkedDocumentId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeForLinkedDocumentName); + const linkedDocumentName = 'LinkedDocument'; + await umbracoApi.document.createDefaultDocument(linkedDocumentName, documentTypeForLinkedDocumentId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); + await umbracoUi.content.clickDocumentLinkButton(); + await umbracoUi.content.selectLinkByName(linkedDocumentName); + await umbracoUi.content.clickButtonWithName('Choose'); + await umbracoUi.content.clickAddButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(documentTypeForLinkedDocumentName); + await umbracoApi.document.ensureNameNotExists(linkedDocumentName); +}); + +test('can view word count', async ({umbracoApi, umbracoUi}) => { + // Arrange + const inputText = 'Test Tiptap here!!!'; + const expectedWordCount = 3; + const customDataTypeId = await umbracoApi.dataType.createTiptapDataTypeWithWordCountStatusbar(customDataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.enterRTETipTapEditor(inputText); + + // Assert + await umbracoUi.content.doesTiptapHaveWordCount(expectedWordCount); +}); + +test('can view element path', async ({umbracoApi, umbracoUi}) => { + // Arrange + const inputText = 'This is Tiptap test'; + const expectedElementPath = 'p'; + const customDataTypeId = await umbracoApi.dataType.createTiptapDataTypeWithElementPathStatusbar(customDataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.enterRTETipTapEditor(inputText); + + // Assert + await umbracoUi.content.doesElementPathHaveText(expectedElementPath); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/RichTextEditor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/RichTextEditor.spec.ts index 62b8dc3928..2660e106d0 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/RichTextEditor.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/RichTextEditor.spec.ts @@ -61,8 +61,8 @@ test('tiptap is the default property editor in rich text editor', async ({umbrac await umbracoUi.dataType.goToDataType(dataTypeName); // Assert - //await umbracoUi.dataType.doesSettingHaveValue(ConstantHelper.tipTapSettings); - //await umbracoUi.dataType.doesSettingItemsHaveCount(ConstantHelper.tipTapSettings); + await umbracoUi.dataType.doesSettingHaveValue(ConstantHelper.tipTapSettings); + await umbracoUi.dataType.doesSettingItemsHaveCount(ConstantHelper.tipTapSettings); await umbracoUi.dataType.doesPropertyEditorHaveName(tipTapPropertyEditorName); await umbracoUi.dataType.doesPropertyEditorHaveAlias(tipTapAlias); await umbracoUi.dataType.doesPropertyEditorHaveUiAlias(tipTapUiAlias); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts index 650859289e..e52f09b5ec 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts @@ -121,12 +121,15 @@ test('can add an available block', async ({umbracoApi, umbracoUi}) => { await umbracoUi.dataType.goToDataType(tipTapName); // Act + await umbracoUi.dataType.isExtensionItemChecked('Block', false); await umbracoUi.dataType.addAvailableBlocks(elementTypeName); await umbracoUi.dataType.clickSaveButton(); // Assert await umbracoUi.dataType.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); expect(await umbracoApi.dataType.doesRTEContainBlocks(tipTapName, [elementTypeId])).toBeTruthy(); + // Verify that "Block" extension is enable + await umbracoUi.dataType.isExtensionItemChecked('Block'); // Clean await umbracoApi.documentType.ensureNameNotExists(elementTypeName); @@ -226,3 +229,38 @@ test('can disable extensions item', async ({umbracoApi, umbracoUi}) => { expect(extensionsValue.value.length).toBe(extensionsCount - 1); expect(extensionsValue.value).not.toContain(extensionItemName); }); + +test('can add a statusbar', async ({umbracoApi, umbracoUi}) => { + // Arrange + const statusbarName = 'Word Count'; + const statusbarApiValue = 'Umb.Tiptap.Statusbar.WordCount'; + await umbracoApi.dataType.createDefaultTiptapDataType(tipTapName); + await umbracoUi.dataType.goToDataType(tipTapName); + + // Act + await umbracoUi.dataType.clickStatusbarItemInToolboxWithName(statusbarName); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + const tipTapData = await umbracoApi.dataType.getByName(tipTapName); + const statusbarValue = tipTapData.values.find(value => value.alias === 'statusbar'); + expect(statusbarValue.value).toEqual([[statusbarApiValue]]); +}); + +test('can remove a statusbar', async ({umbracoApi, umbracoUi}) => { + // Arrange + const statusbarName = 'Word Count'; + await umbracoApi.dataType.createTiptapDataTypeWithWordCountStatusbar(tipTapName); + await umbracoUi.dataType.goToDataType(tipTapName); + + // Act + await umbracoUi.dataType.clickStatusbarItemWithName(statusbarName); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + const tipTapData = await umbracoApi.dataType.getByName(tipTapName); + const statusbarValue = tipTapData.values.find(value => value.alias === 'statusbar'); + expect(statusbarValue).toBeFalsy(); +}); \ No newline at end of file From 38e9781fc52a9a607cc3cd53dfa8caef64b41a59 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 29 Apr 2025 13:04:24 +0200 Subject: [PATCH 06/17] build: restores some of the behavior from V13 in relation to StaticAssets (#19189) In v13, the StaticAssets build was only triggered based on the existence of either the output folder or a preserve.* marker file. Here, we also additionally check for the node_modules/.package-lock.json file before reinstalling npm dependencies. We also now only run `npm install` rather than `npm ci` to optimise the build. --- .../Umbraco.Cms.StaticAssets.csproj | 77 +++++++++++++++---- 1 file changed, 62 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj b/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj index d6fbf7ab37..3193533266 100644 --- a/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj +++ b/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj @@ -26,23 +26,36 @@ - + + + + + + + + ..\Umbraco.Web.UI.Client\ - wwwroot\umbraco\backoffice + $(ProjectDir)wwwroot\umbraco\backoffice - - - - + + + + + - + + + + + + @@ -61,27 +74,46 @@ - + + + + + + + + + + + + + + + + + + ..\Umbraco.Web.UI.Login\ - wwwroot\umbraco\login + $(ProjectDir)wwwroot\umbraco\login - - - - + + + + + + - + - + @@ -99,4 +131,19 @@ + + + + + + + + + + + + + + + From 14f60a108ab5834c67a968c8c422648f1561075c Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 29 Apr 2025 18:10:44 +0200 Subject: [PATCH 07/17] V13: Clear Member Username Cache in Load Balanced Environments (#19191) * Clear usernamekey * Odd explaining comment * Update src/Umbraco.Core/Cache/Refreshers/Implement/MemberCacheRefresher.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Make UserNameCachePrefix readonly for better immutabilityly * Move prefix to CacheKeys constants --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> # Conflicts: # src/Umbraco.Core/Cache/CacheKeys.cs --- src/Umbraco.Core/Cache/CacheKeys.cs | 2 ++ .../Implement/MemberCacheRefresher.cs | 17 ++++++++++++++--- .../Repositories/Implement/MemberRepository.cs | 7 +++---- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index 7f8484fca4..0e75b6820d 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -22,4 +22,6 @@ public static class CacheKeys public const string PreviewPropertyCacheKeyPrefix = "Cache.Property.CacheValues[D:"; public const string PropertyCacheKeyPrefix = "Cache.Property.CacheValues[P:"; + + public const string MemberUserNameCachePrefix = "uRepo_userNameKey+"; } diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/MemberCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/MemberCacheRefresher.cs index 18809a6bbe..1c19f62576 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/MemberCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/MemberCacheRefresher.cs @@ -71,11 +71,22 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase(p.Id)); - memberCache.Result?.Clear(RepositoryCacheKeys.GetKey(p.Username)); + continue; } + + memberCache.Result?.Clear(RepositoryCacheKeys.GetKey(p.Id)); + memberCache.Result?.Clear(RepositoryCacheKeys.GetKey(p.Username)); + + // This specific cache key was introduced to fix an issue where the member username could not be the same as the member id, because the cache keys collided. + // This is done in a bit of a hacky way, because the cache key is created internally in the repository, but we need to clear it here. + // Ideally, we want to use a shared way of generating the key between this and the repository. + // Additionally, the RepositoryCacheKeys actually caches the string to avoid re-allocating memory; we would like to also use this in the repository + // See: + // https://github.com/umbraco/Umbraco-CMS/pull/17350 + // https://github.com/umbraco/Umbraco-CMS/pull/17815 + memberCache.Result?.Clear(RepositoryCacheKeys.GetKey(CacheKeys.MemberUserNameCachePrefix + p.Username)); } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index 75ea4f365d..9adc60426f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -39,7 +39,6 @@ public class MemberRepository : ContentRepositoryBase - _memberByUsernameCachePolicy.GetByUserName(UsernameCacheKey, username, PerformGetByUsername, PerformGetAllByUsername); + _memberByUsernameCachePolicy.GetByUserName(CacheKeys.MemberUserNameCachePrefix, username, PerformGetByUsername, PerformGetAllByUsername); public int[] GetMemberIds(string[] usernames) { @@ -609,7 +608,7 @@ public class MemberRepository : ContentRepositoryBase Date: Wed, 30 Apr 2025 09:57:55 +0200 Subject: [PATCH 08/17] Updates Examine to latest patch release. (#19193) --- Directory.Packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 0817e00c6a..48cae8dc62 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -45,8 +45,8 @@ - - + + @@ -100,4 +100,4 @@ - \ No newline at end of file + From 15a8b4066abdf4f1feae4d554441b23bff6a2400 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 30 Apr 2025 15:04:08 +0200 Subject: [PATCH 09/17] Bumped version to 15.4.0. --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index ec32c8345a..54cdf6d288 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "15.4.0-rc3", + "version": "15.4.0", "assemblyVersion": { "precision": "build" }, From fc1455e0d8d71475e272c764eb17746159a0e30b Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sat, 3 May 2025 17:28:44 +0200 Subject: [PATCH 10/17] Bumped version to 15.4.1. --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 54cdf6d288..3780adcd49 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "15.4.0", + "version": "15.4.1", "assemblyVersion": { "precision": "build" }, From 0ad020f0ce62ab32eae127262efb31acd1137790 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 5 May 2025 07:38:31 +0200 Subject: [PATCH 11/17] Restored interface methods on obsolete IBackgroundTaskQueue (#19223) * Restore interface methods on obsolete IBackgroundTaskQueue. * Fixed typos in comment. --- src/Umbraco.Core/HostedServices/IBackgroundTaskQueue.cs | 2 +- .../HostedServices/IBackgroundTaskQueue.cs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/HostedServices/IBackgroundTaskQueue.cs b/src/Umbraco.Core/HostedServices/IBackgroundTaskQueue.cs index 0fef380e8f..de51cd45e0 100644 --- a/src/Umbraco.Core/HostedServices/IBackgroundTaskQueue.cs +++ b/src/Umbraco.Core/HostedServices/IBackgroundTaskQueue.cs @@ -9,7 +9,7 @@ namespace Umbraco.Cms.Core.HostedServices; public interface IBackgroundTaskQueue { /// - /// Enqueue a work item to be executed on in the background. + /// Enqueue a work item to be executed in the background. /// void QueueBackgroundWorkItem(Func workItem); diff --git a/src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs b/src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs index 983af4be9a..8e4a6f6a23 100644 --- a/src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs +++ b/src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs @@ -9,4 +9,13 @@ namespace Umbraco.Cms.Infrastructure.HostedServices; [Obsolete("This has been relocated into Umbraco.Cms.Core. This definition in Umbraco.Cms.Infrastructure is scheduled for removal in Umbraco 17.")] public interface IBackgroundTaskQueue : Core.HostedServices.IBackgroundTaskQueue { + /// + /// Enqueue a work item to be executed in the background. + /// + void QueueBackgroundWorkItem(Func workItem); + + /// + /// Dequeue the first item on the queue. + /// + Task?> DequeueAsync(CancellationToken cancellationToken); } From 103631de15a0dfd6c8fbc813f50be4eb89b50ef0 Mon Sep 17 00:00:00 2001 From: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> Date: Tue, 6 May 2025 13:59:32 +0700 Subject: [PATCH 12/17] V15 QA Updated the build stage to align with the azure pipelines yml (#19235) --- build/nightly-E2E-test-pipelines.yml | 55 +++++++++++++++++++--------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/build/nightly-E2E-test-pipelines.yml b/build/nightly-E2E-test-pipelines.yml index 8cbb065cd7..54af8efc06 100644 --- a/build/nightly-E2E-test-pipelines.yml +++ b/build/nightly-E2E-test-pipelines.yml @@ -32,25 +32,17 @@ stages: - job: A displayName: Build Umbraco CMS pool: - vmImage: 'ubuntu-latest' + vmImage: "windows-latest" steps: - checkout: self - fetchDepth: 0 - submodules: true + submodules: false + lfs: false, + fetchDepth: 500 + - template: templates/backoffice-install.yml - task: UseDotNet@2 displayName: Use .NET SDK from global.json inputs: useGlobalJson: true - - template: templates/backoffice-install.yml - - script: npm run build:for:cms - displayName: Run build (Bellissima) - workingDirectory: src/Umbraco.Web.UI.Client - - script: npm ci --no-fund --no-audit --prefer-offline - displayName: Run npm ci (Login) - workingDirectory: src/Umbraco.Web.UI.Login - - script: npm run build - displayName: Run npm build (Login) - workingDirectory: src/Umbraco.Web.UI.Login - task: DotNetCoreCLI@2 displayName: Run dotnet restore inputs: @@ -62,7 +54,7 @@ stages: inputs: command: build projects: $(solution) - arguments: '--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg' + arguments: "--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg" - task: PublishPipelineArtifact@1 displayName: Publish nupkg inputs: @@ -74,6 +66,33 @@ stages: targetPath: $(Build.SourcesDirectory) artifactName: build_output + - job: B + displayName: Build Bellissima Package + pool: + vmImage: "ubuntu-latest" + steps: + - checkout: self + submodules: false + lfs: false, + fetchDepth: 500 + - template: templates/backoffice-install.yml + - script: npm run build:for:npm + displayName: Run build:for:npm + workingDirectory: src/Umbraco.Web.UI.Client + - bash: | + echo "##[command]Running npm pack" + echo "##[debug]Output directory: $(Build.ArtifactStagingDirectory)" + mkdir $(Build.ArtifactStagingDirectory)/npm + npm pack --pack-destination $(Build.ArtifactStagingDirectory)/npm + mv .npmrc $(Build.ArtifactStagingDirectory)/npm/ + displayName: Run npm pack + workingDirectory: src/Umbraco.Web.UI.Client + - task: PublishPipelineArtifact@1 + displayName: Publish Bellissima npm artifact + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/npm + artifactName: npm + - stage: E2E displayName: E2E Tests dependsOn: Build @@ -209,8 +228,8 @@ stages: workingDirectory: tests/Umbraco.Tests.AcceptanceTest # Install Playwright and dependencies - - pwsh: npx playwright install --with-deps - displayName: Install Playwright + - pwsh: npx playwright install chromium + displayName: Install Playwright only with Chromium browser workingDirectory: tests/Umbraco.Tests.AcceptanceTest # Test @@ -381,8 +400,8 @@ stages: workingDirectory: tests/Umbraco.Tests.AcceptanceTest # Install Playwright and dependencies - - pwsh: npx playwright install --with-deps - displayName: Install Playwright + - pwsh: npx playwright install chromium + displayName: Install Playwright only with Chromium browser workingDirectory: tests/Umbraco.Tests.AcceptanceTest # Test From b106305167ede089e978b88ecef25a36018e889b Mon Sep 17 00:00:00 2001 From: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> Date: Tue, 6 May 2025 14:15:30 +0700 Subject: [PATCH 13/17] V15 QA Added acceptance tests for tiptap style select (#19234) --- .../package-lock.json | 8 +- .../Umbraco.Tests.AcceptanceTest/package.json | 2 +- .../RichTextEditor/TiptapStyleSelect.spec.ts | 114 ++++++++++++++++++ 3 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/TiptapStyleSelect.spec.ts diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 1353eb665a..f975ad0cbd 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -8,7 +8,7 @@ "hasInstallScript": true, "dependencies": { "@umbraco/json-models-builders": "^2.0.33", - "@umbraco/playwright-testhelpers": "^15.0.49", + "@umbraco/playwright-testhelpers": "^15.0.50", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" @@ -66,9 +66,9 @@ } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "15.0.49", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.49.tgz", - "integrity": "sha512-1At/e057u6rB3T3iH8tR6SLXnYRZJsCVjmm8jm+6sftJDvgB0Q5kXKaSDyLTU6wVuLALiDNUuNuJ86FgOOdUJw==", + "version": "15.0.50", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.50.tgz", + "integrity": "sha512-gi5bb4DShw3lmEdmQhKpPdkS6Uzg4CdNkrJDSkkUTE8CKY7T5goyE4QBTU8kj+LMHR2DnB7qyRUXrYWyS1ECiQ==", "dependencies": { "@umbraco/json-models-builders": "2.0.33", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 98d612ca81..0396992d5c 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^2.0.33", - "@umbraco/playwright-testhelpers": "^15.0.49", + "@umbraco/playwright-testhelpers": "^15.0.50", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/TiptapStyleSelect.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/TiptapStyleSelect.spec.ts new file mode 100644 index 0000000000..faab11fe5f --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/TiptapStyleSelect.spec.ts @@ -0,0 +1,114 @@ +import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const customDataTypeName = 'Test RTE Tiptap Style Select'; +const inputText = 'This is Tiptap test'; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + const customDataTypeId = await umbracoApi.dataType.createTiptapDataTypeWithStyleSelect(customDataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.enterRTETipTapEditor(inputText); + await umbracoUi.content.selectAllRTETipTapEditorText(); +}) + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + +test('can apply page header format', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.content.clickStyleSelectButton(); + + // Act + await umbracoUi.content.hoverCascadingMenuItemWithName('Headers'); + await umbracoUi.content.clickCascadingMenuItemWithName('Page header'); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].value.markup).toEqual('

' + inputText + '

'); +}); + +test('can apply section header format', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.content.clickStyleSelectButton(); + + // Act + await umbracoUi.content.hoverCascadingMenuItemWithName('Headers'); + await umbracoUi.content.clickCascadingMenuItemWithName('Section header'); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].value.markup).toEqual('

' + inputText + '

'); +}); + +test('can apply paragraph header format', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.content.clickStyleSelectButton(); + + // Act + await umbracoUi.content.hoverCascadingMenuItemWithName('Headers'); + await umbracoUi.content.clickCascadingMenuItemWithName('Paragraph header'); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].value.markup).toEqual('

' + inputText + '

'); +}); + +test('can apply paragraph blocks format', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.content.clickStyleSelectButton(); + + // Act + await umbracoUi.content.hoverCascadingMenuItemWithName('Blocks'); + await umbracoUi.content.clickCascadingMenuItemWithName('Paragraph'); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].value.markup).toEqual('

' + inputText + '

'); +}); + +test('can apply block quote format', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.content.clickStyleSelectButton(); + + // Act + await umbracoUi.content.hoverCascadingMenuItemWithName('Containers'); + await umbracoUi.content.clickCascadingMenuItemWithName('Block quote'); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].value.markup).toEqual('

' + inputText + '

'); +}); + +test('can apply code block format', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.content.clickStyleSelectButton(); + + // Act + await umbracoUi.content.hoverCascadingMenuItemWithName('Containers'); + await umbracoUi.content.clickCascadingMenuItemWithName('Code block'); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].value.markup).toEqual('
' + inputText + '

'); +}); \ No newline at end of file From b01def08724ce10e7164a2f467192cf47e145a56 Mon Sep 17 00:00:00 2001 From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Date: Thu, 8 May 2025 08:53:29 +0200 Subject: [PATCH 14/17] V15 QA added a fix for flaky integration tests run on SQL Server Linux (#18965) --- build/azure-pipelines.yml | 27 ++++ build/nightly-E2E-test-pipelines.yml | 218 ++++++++++++++++++++++++++- 2 files changed, 241 insertions(+), 4 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index a33ee744f7..92621c30d3 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -432,6 +432,33 @@ stages: displayName: Start SQL Server Docker image (Linux) condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + - powershell: | + $maxAttempts = 12 + $attempt = 0 + $status = "" + + while (($status -ne 'running') -and ($attempt -lt $maxAttempts)) { + Start-Sleep -Seconds 5 + # We use the docker inspect command to check the status of the container. If the container is not running, we wait 5 seconds and try again. And if reaches 12 attempts, we fail the build. + $status = docker inspect -f '{{.State.Status}}' mssql + + if ($status -ne 'running') { + Write-Host "Waiting for SQL Server to be ready... Attempt $($attempt + 1)" + $attempt++ + } + } + + if ($status -eq 'running') { + Write-Host "SQL Server container is running" + docker ps -a + } else { + Write-Host "SQL Server did not become ready in time. Last known status: $status" + docker logs mssql + exit 1 + } + displayName: Wait for SQL Server to be ready (Linux) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + - pwsh: SqlLocalDB start MSSQLLocalDB displayName: Start SQL Server LocalDB (Windows) condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) diff --git a/build/nightly-E2E-test-pipelines.yml b/build/nightly-E2E-test-pipelines.yml index 54af8efc06..0740eed585 100644 --- a/build/nightly-E2E-test-pipelines.yml +++ b/build/nightly-E2E-test-pipelines.yml @@ -93,6 +93,189 @@ stages: targetPath: $(Build.ArtifactStagingDirectory)/npm artifactName: npm + - stage: Integration + displayName: Integration Tests + dependsOn: Build + jobs: + # Integration Tests (SQLite) + - job: + timeoutInMinutes: 180 + displayName: Integration Tests (SQLite) + strategy: + matrix: + # Windows: + # vmImage: 'windows-latest' + # We split the tests into 3 parts for each OS to reduce the time it takes to run them on the pipeline + LinuxPart1Of3: + vmImage: "ubuntu-latest" + # Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace + testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)" + LinuxPart2Of3: + vmImage: "ubuntu-latest" + # Filter tests that are part of the Umbraco.Infrastructure.Service namespace + testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)" + LinuxPart3Of3: + vmImage: "ubuntu-latest" + # Filter tests that are not part of the Umbraco.Infrastructure namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace + testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)" + macOSPart1Of3: + vmImage: "macOS-latest" + # Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace + testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)" + macOSPart2Of3: + vmImage: "macOS-latest" + # Filter tests that are part of the Umbraco.Infrastructure.Service namespace + testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)" + macOSPart3Of3: + vmImage: "macOS-latest" + # Filter tests that are not part of the Umbraco.Infrastructure namespace. + testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)" + pool: + vmImage: $(vmImage) + variables: + Tests__Database__DatabaseType: "Sqlite" + steps: + - checkout: self + submodules: false + lfs: false, + fetchDepth: 1 + fetchFilter: tree:0 + # Setup test environment + - task: DownloadPipelineArtifact@2 + displayName: Download build artifacts + inputs: + artifact: build_output + path: $(Build.SourcesDirectory) + + - task: UseDotNet@2 + displayName: Use .NET SDK from global.json + inputs: + useGlobalJson: true + + # Test + - task: DotNetCoreCLI@2 + displayName: Run dotnet test + inputs: + command: test + projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj" + testRunTitle: Integration Tests SQLite - $(Agent.OS) + arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build' + + # Integration Tests (SQL Server) + - job: + timeoutInMinutes: 180 + displayName: Integration Tests (SQL Server) + strategy: + matrix: + # We split the tests into 3 parts for each OS to reduce the time it takes to run them on the pipeline + WindowsPart1Of3: + vmImage: "windows-latest" + Tests__Database__DatabaseType: LocalDb + Tests__Database__SQLServerMasterConnectionString: N/A + # Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace + testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)" + WindowsPart2Of3: + vmImage: "windows-latest" + Tests__Database__DatabaseType: LocalDb + Tests__Database__SQLServerMasterConnectionString: N/A + # Filter tests that are part of the Umbraco.Infrastructure.Service namespace + testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)" + WindowsPart3Of3: + vmImage: "windows-latest" + Tests__Database__DatabaseType: LocalDb + Tests__Database__SQLServerMasterConnectionString: N/A + # Filter tests that are not part of the Umbraco.Infrastructure namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace + testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)" + LinuxPart1Of3: + vmImage: "ubuntu-latest" + SA_PASSWORD: UmbracoIntegration123! + Tests__Database__DatabaseType: SqlServer + Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True" + # Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace + testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)" + LinuxPart2Of3: + vmImage: "ubuntu-latest" + SA_PASSWORD: UmbracoIntegration123! + Tests__Database__DatabaseType: SqlServer + Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True" + # Filter tests that are part of the Umbraco.Infrastructure.Service namespace + testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)" + LinuxPart3Of3: + vmImage: "ubuntu-latest" + SA_PASSWORD: UmbracoIntegration123! + Tests__Database__DatabaseType: SqlServer + Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True" + # Filter tests that are not part of the Umbraco.Infrastructure namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace + testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)" + pool: + vmImage: $(vmImage) + steps: + # Setup test environment + - task: DownloadPipelineArtifact@2 + displayName: Download build artifacts + inputs: + artifact: build_output + path: $(Build.SourcesDirectory) + + - task: UseDotNet@2 + displayName: Use .NET SDK from global.json + inputs: + useGlobalJson: true + + # Start SQL Server + - powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$(SA_PASSWORD)" mcr.microsoft.com/mssql/server:2022-latest + displayName: Start SQL Server Docker image (Linux) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + + - powershell: | + $maxAttempts = 12 + $attempt = 0 + $status = "" + + while (($status -ne 'running') -and ($attempt -lt $maxAttempts)) { + Start-Sleep -Seconds 5 + # We use the docker inspect command to check the status of the container. If the container is not running, we wait 5 seconds and try again. And if reaches 12 attempts, we fail the build. + $status = docker inspect -f '{{.State.Status}}' mssql + + if ($status -ne 'running') { + Write-Host "Waiting for SQL Server to be ready... Attempt $($attempt + 1)" + $attempt++ + } + } + + if ($status -eq 'running') { + Write-Host "SQL Server container is running" + docker ps -a + } else { + Write-Host "SQL Server did not become ready in time. Last known status: $status" + docker logs mssql + exit 1 + } + displayName: Wait for SQL Server to be ready (Linux) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + + - pwsh: SqlLocalDB start MSSQLLocalDB + displayName: Start SQL Server LocalDB (Windows) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) + + # Test + - task: DotNetCoreCLI@2 + displayName: Run dotnet test + inputs: + command: test + projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj" + testRunTitle: Integration Tests SQL Server - $(Agent.OS) + arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build' + + # Stop SQL Server + - pwsh: docker stop mssql + displayName: Stop SQL Server Docker image (Linux) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + + - pwsh: SqlLocalDB stop MSSQLLocalDB + displayName: Stop SQL Server LocalDB (Windows) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) + - stage: E2E displayName: E2E Tests dependsOn: Build @@ -290,17 +473,17 @@ stages: testCommand: "npm run testSqlite -- --shard=1/3" vmImage: "ubuntu-latest" SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD) - CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True" + CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True" LinuxPart2Of3: testCommand: "npm run testSqlite -- --shard=2/3" vmImage: "ubuntu-latest" SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD) - CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True" + CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True" LinuxPart3Of3: testCommand: "npm run testSqlite -- --shard=3/3" vmImage: "ubuntu-latest" SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD) - CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True" + CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True" WindowsPart1Of3: vmImage: "windows-latest" testCommand: "npm run testSqlite -- --shard=1/3" @@ -371,6 +554,33 @@ stages: displayName: Start SQL Server Docker image (Linux) condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + - powershell: | + $maxAttempts = 12 + $attempt = 0 + $status = "" + + while (($status -ne 'running') -and ($attempt -lt $maxAttempts)) { + Start-Sleep -Seconds 5 + # We use the docker inspect command to check the status of the container. If the container is not running, we wait 5 seconds and try again. And if reaches 12 attempts, we fail the build. + $status = docker inspect -f '{{.State.Status}}' mssql + + if ($status -ne 'running') { + Write-Host "Waiting for SQL Server to be ready... Attempt $($attempt + 1)" + $attempt++ + } + } + + if ($status -eq 'running') { + Write-Host "SQL Server container is running" + docker ps -a + } else { + Write-Host "SQL Server did not become ready in time. Last known status: $status" + docker logs mssql + exit 1 + } + displayName: Wait for SQL Server to be ready (Linux) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + - pwsh: SqlLocalDB start MSSQLLocalDB displayName: Start SQL Server LocalDB (Windows) condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) @@ -447,7 +657,7 @@ stages: inputs: targetPath: $(Build.ArtifactStagingDirectory) artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)" - + # Publish test results - task: PublishTestResults@2 displayName: "Publish test results" From 275478066eb121b2e935adff70838b5081bf91e3 Mon Sep 17 00:00:00 2001 From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Date: Thu, 8 May 2025 11:12:37 +0200 Subject: [PATCH 15/17] V15 QA complex block grid test (#18347) --- .../BlockGrid/ComplexBlockGridTest.spec.ts | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ComplexBlockGridTest.spec.ts diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ComplexBlockGridTest.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ComplexBlockGridTest.spec.ts new file mode 100644 index 0000000000..78b1ea1213 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ComplexBlockGridTest.spec.ts @@ -0,0 +1,134 @@ +import {expect} from '@playwright/test'; +import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; + +// DocumentType +const documentTypeName = 'TestDocumentType'; +let documentTypeId = ''; +const groupName = 'TestGroup'; + +// Content +const contentName = 'TestContent'; +let contentId = ''; + +// Property Value +const wrongPropertyValue = 'This is a test with wrong value**'; +const correctPropertyValue = 'Test'; + +// ElementTypes +// TextString Element Type (for Block List) +const textStringElementGroupName = 'TextStringElementGroup'; +const textStringElementTypeName = 'TestElementWithTextString'; +const textStringElementRegex = '^[a-zA-Z0-9]*$'; +let textStringElementTypeId = ''; +// Area Element Type (for Block Grid) +const areaElementTypeName = 'TestElementArea'; +const areaAlias = 'testArea'; +let areaElementTypeId = ''; +// Rich Text Editor Element Type (for Block Grid) +const richTextEditorElementGroupName = 'RichTextEditorElementGroup'; +const richTextEditorElementTypeName = 'RichTextEditorTestElement'; +let richTextEditorElementTypeId = ''; +// Block List Element Type +const blockListElementTypeName = 'BlockListElement'; +const blockListGroupName = 'BlockListGroup'; +let blockListElementTypeId = ''; + +// DataTypes +const blockGridDataTypeName = 'TestBlockGridEditor'; +const blockListDataTypeName = 'TestBlockListEditor'; +const textStringElementDataTypeName = 'Textstring'; +const richTextDataTypeName = 'Rich Text Editor'; +let blockListDataTypeId = ''; +let blockGridDataTypeId = ''; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(areaElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(richTextEditorElementTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName); + await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName); + await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(areaElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(richTextEditorElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName); + await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName); + await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName); +}); + +test('can update property value nested in a block grid area with an RTE with a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + test.slow(); + // Arrange + // ElementType with Textstring And REGEX only accept letters and numbers + const textStringElementDataType = await umbracoApi.dataType.getByName(textStringElementDataTypeName); + textStringElementTypeId = await umbracoApi.documentType.createElementTypeWithRegexValidation(textStringElementTypeName, textStringElementGroupName, textStringElementDataTypeName, textStringElementDataType.id, textStringElementRegex); + // Block List Editor with Textstring + blockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListDataTypeName, textStringElementTypeId); + // ElementType with Block List Editor + blockListElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockListElementTypeName, blockListGroupName, blockListDataTypeName, blockListDataTypeId); + // Rich Text Editor in an ElementType, with a Block(Element Type), the block contains a Block List Editor + const richTextEditorId = await umbracoApi.dataType.createRichTextEditorWithABlock(richTextDataTypeName, blockListElementTypeId); + richTextEditorElementTypeId = await umbracoApi.documentType.createDefaultElementType(richTextEditorElementTypeName, richTextEditorElementGroupName, richTextDataTypeName, richTextEditorId); + // ElementType Area that is Empty + areaElementTypeId = await umbracoApi.documentType.createEmptyElementType(areaElementTypeName); + // Block Grid with 2 blocks, one with RTE and Inline, and one with areas + blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockWithInlineEditingModeAndABlockWithAnArea(blockGridDataTypeName, richTextEditorElementTypeId, true, areaElementTypeId, areaAlias); + // Document Type with the following + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridDataTypeName, blockGridDataTypeId, groupName); + // Creates Content + contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickAddBlockGridElementWithName(areaElementTypeName); + await umbracoUi.content.clickSelectBlockElementWithName(areaElementTypeName); + await umbracoUi.content.clickAddBlockGridElementWithName(richTextEditorElementTypeName); + await umbracoUi.content.clickExactLinkWithName(richTextEditorElementTypeName); + await umbracoUi.content.clickInsertBlockButton(); + await umbracoUi.content.clickExactLinkWithName(blockListElementTypeName); + await umbracoUi.content.clickAddBlockGridElementWithName(textStringElementTypeName); + await umbracoUi.content.clickExactLinkWithName(textStringElementTypeName); + // Enter text in the textstring block that won't match regex + await umbracoUi.content.enterPropertyValue(textStringElementDataTypeName, wrongPropertyValue); + await umbracoUi.content.clickCreateButtonForModalWithElementTypeNameAndGroupName(textStringElementTypeName, textStringElementGroupName); + await umbracoUi.content.clickCreateButtonForModalWithElementTypeNameAndGroupName(blockListElementTypeName, blockListGroupName); + await umbracoUi.content.clickCreateButtonForModalWithElementTypeNameAndGroupName(richTextEditorElementTypeName, richTextEditorElementGroupName); + await umbracoUi.content.clickSaveAndPublishButton(); + // Checks that the error notification is shown since the textstring block has the wrong value + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved, true, true); + await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished, true, true); + // Updates the textstring block with the correct value + await umbracoUi.waitForTimeout(1000); + await umbracoUi.content.clickBlockElementWithName(blockListElementTypeName); + await umbracoUi.content.clickEditBlockListEntryWithName(textStringElementTypeName); + await umbracoUi.content.enterPropertyValue(textStringElementDataTypeName, correctPropertyValue); + await umbracoUi.content.clickUpdateButtonForModalWithElementTypeNameAndGroupName(textStringElementTypeName, textStringElementGroupName); + await umbracoUi.content.clickUpdateButtonForModalWithElementTypeNameAndGroupName(blockListElementTypeName, blockListGroupName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved, true, true); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published, true, true); + // Checks if published + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe('Published'); + // Checks if the textstring block has the correct value after reloading the page + await umbracoUi.reloadPage(); + // Waits to make sure the page has loaded + await umbracoUi.waitForTimeout(2000); + await umbracoUi.content.clickBlockElementWithName(blockListElementTypeName); + // Needs to wait to make sure it has loaded + await umbracoUi.waitForTimeout(2000); + await umbracoUi.content.clickEditBlockListEntryWithName(textStringElementTypeName); + await umbracoUi.content.doesPropertyContainValue(textStringElementDataTypeName, correctPropertyValue); +}); From b13eb8aaf8901ece38d3c7d5df7a6ef1e9e5f4a5 Mon Sep 17 00:00:00 2001 From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Date: Mon, 12 May 2025 11:23:55 +0200 Subject: [PATCH 16/17] V15 Added second level block acceptance tests (#19264) --- .../package-lock.json | 9 +- .../Umbraco.Tests.AcceptanceTest/package.json | 2 +- .../SecondLevelBlockProperties.spec.ts | 161 +++++++++++++++++ .../SecondLevelBlockProperties.spec.ts | 161 +++++++++++++++++ .../SecondLevelBlockProperties.spec.ts | 165 ++++++++++++++++++ 5 files changed, 493 insertions(+), 5 deletions(-) create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/SecondLevelBlockProperties.spec.ts create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/SecondLevelBlockProperties.spec.ts create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/SecondLevelBlockProperties.spec.ts diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index f975ad0cbd..6a99fe040b 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -8,7 +8,7 @@ "hasInstallScript": true, "dependencies": { "@umbraco/json-models-builders": "^2.0.33", - "@umbraco/playwright-testhelpers": "^15.0.50", + "@umbraco/playwright-testhelpers": "^15.0.52", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" @@ -66,9 +66,10 @@ } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "15.0.50", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.50.tgz", - "integrity": "sha512-gi5bb4DShw3lmEdmQhKpPdkS6Uzg4CdNkrJDSkkUTE8CKY7T5goyE4QBTU8kj+LMHR2DnB7qyRUXrYWyS1ECiQ==", + "version": "15.0.52", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.52.tgz", + "integrity": "sha512-HJ+kiafLX0ck0/nYol0Kr7yks/hBbnCKotr1+TaxIBgowvbirIt0paY7wluQpHQi0e6nAmJZsjZZ08abh9ZKBA==", + "license": "MIT", "dependencies": { "@umbraco/json-models-builders": "2.0.33", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 0396992d5c..2c97cdd110 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^2.0.33", - "@umbraco/playwright-testhelpers": "^15.0.50", + "@umbraco/playwright-testhelpers": "^15.0.52", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/SecondLevelBlockProperties.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/SecondLevelBlockProperties.spec.ts new file mode 100644 index 0000000000..292d7c96d3 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/SecondLevelBlockProperties.spec.ts @@ -0,0 +1,161 @@ +import {AliasHelper, ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +// Content Name +const contentName = 'ContentName'; + +// Document Type +const documentTypeName = 'DocumentTypeName'; +let documentTypeId = null; +const documentTypeGroupName = 'DocumentGroup'; + +// Block Grid +const blockGridDataTypeName = 'BlockGridName'; +let blockGridDataTypeId = null; + +// Text String +const textStringElementTypeName = 'TextStringElementName'; +let textStringElementTypeId = null; +let textStringGroupName = 'TextGroup'; +const textStringDataTypeName = 'Textstring'; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName); +}); + +test('can publish a block grid editor with a rich text editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const richTextEditorValue = 'Hello World'; + const expectedRichTextEditorOutputValue = '

Hello World

'; + const richTextDataTypeName = 'RichTextDataTypeName'; + const richTextElementTypeName = 'RichTextElementName'; + const richTextElementGroupName = 'RichTextElementGroupName'; + await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(richTextElementTypeName); + + const richTextEditorDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(richTextDataTypeName); + const richTextElementTypeId = await umbracoApi.documentType.createDefaultElementType(richTextElementTypeName, richTextElementGroupName, richTextDataTypeName, richTextEditorDataTypeId); + blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridDataTypeName, richTextElementTypeId, true); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridDataTypeName, blockGridDataTypeId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockElementButton(); + await umbracoUi.content.clickBlockCardWithName(richTextElementTypeName, true); + await umbracoUi.content.enterRTETipTapEditor(richTextEditorValue); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + // Asserts that the value in the RTE is as expected + const documentData = await umbracoApi.document.getByName(contentName); + const documentValues = documentData.values.find(value => value.alias === AliasHelper.toAlias(blockGridDataTypeName)); + expect(documentValues.value.contentData[0].values[0].value.markup).toContain(expectedRichTextEditorOutputValue); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(richTextElementTypeName); +}); + +test('can publish a block grid editor with a block list editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringValue = 'Hello World'; + const blockListDataTypeName = 'BlockListName'; + const blockListElementTypeName = 'BlockListElementName'; + const blockListElementGroupName = 'BlockListElementGroupName'; + await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName); + + const textStringDataType = await umbracoApi.dataType.getByName(textStringDataTypeName); + textStringElementTypeId = await umbracoApi.documentType.createDefaultElementType(textStringElementTypeName, textStringGroupName, textStringDataTypeName, textStringDataType.id); + const blockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListDataTypeName, textStringElementTypeId); + const blockListElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockListElementTypeName, blockListElementGroupName, blockListDataTypeName, blockListDataTypeId); + blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridDataTypeName, blockListElementTypeId, true); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridDataTypeName, blockGridDataTypeId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockElementButton(); + await umbracoUi.content.clickBlockCardWithName(blockListElementTypeName, true); + await umbracoUi.content.clickAddBlockWithNameButton(textStringElementTypeName); + await umbracoUi.content.clickBlockCardWithName(textStringElementTypeName, true); + await umbracoUi.content.enterTextstring(textStringValue); + await umbracoUi.content.clickCreateForModalWithHeadline('Add ' + textStringElementTypeName); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + // Asserts that the value in the BlockList is as expected + const documentData = await umbracoApi.document.getByName(contentName); + const documentValues = documentData.values.find(value => value.alias === AliasHelper.toAlias(blockGridDataTypeName)); + expect(documentValues.value.contentData[0].values[0].value.contentData[0].values[0].value).toContain(textStringValue); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName); +}); + +test('can publish a block grid editor with a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringValue = 'Hello World'; + const secondBlockGridDataTypeName = 'SecondBlockGridDataTypeName'; + const blockGridElementTypeName = 'BlockGridElementTypeName'; + const blockGridElementGroupName = 'BlockGridElementGroupName'; + await umbracoApi.dataType.ensureNameNotExists(secondBlockGridDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockGridElementTypeName); + + const textStringDataType = await umbracoApi.dataType.getByName(textStringDataTypeName); + textStringElementTypeId = await umbracoApi.documentType.createDefaultElementType(textStringElementTypeName, textStringGroupName, textStringDataTypeName, textStringDataType.id); + const secondBlockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(secondBlockGridDataTypeName, textStringElementTypeId); + const blockGridElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockGridElementTypeName, blockGridElementGroupName, secondBlockGridDataTypeName, secondBlockGridDataTypeId); + blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridDataTypeName, blockGridElementTypeId, true); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridDataTypeName, blockGridDataTypeId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockElementButton(); + await umbracoUi.content.clickBlockCardWithName(blockGridElementTypeName, true); + await umbracoUi.content.clickAddBlockWithNameButton(textStringElementTypeName); + await umbracoUi.content.clickBlockCardWithName(textStringElementTypeName, true); + await umbracoUi.content.enterTextstring(textStringValue); + await umbracoUi.content.clickCreateForModalWithHeadline('Add ' + textStringElementTypeName); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + // Asserts that the value in the BlockGrid is as expected + const documentData = await umbracoApi.document.getByName(contentName); + const documentValues = documentData.values.find(value => value.alias === AliasHelper.toAlias(blockGridDataTypeName)); + expect(documentValues.value.contentData[0].values[0].value.contentData[0].values[0].value).toContain(textStringValue); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(secondBlockGridDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockGridElementTypeName); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/SecondLevelBlockProperties.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/SecondLevelBlockProperties.spec.ts new file mode 100644 index 0000000000..ac3dd0aefd --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/SecondLevelBlockProperties.spec.ts @@ -0,0 +1,161 @@ +import {AliasHelper, ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +// Content Name +const contentName = 'ContentName'; + +// Document Type +const documentTypeName = 'DocumentTypeName'; +let documentTypeId = null; +const documentTypeGroupName = 'DocumentGroup'; + +// Block List +const blockListDataTypeName = 'BlockListName'; +let blockListDataTypeId = null; + +// Text String +const textStringElementTypeName = 'TextStringElementName'; +let textStringElementTypeId = null; +let textStringGroupName = 'TextGroup'; +const textStringDataTypeName = 'Textstring'; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName); +}); + +test('can publish a block list editor with a rich text editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const richTextEditorValue = 'Hello World'; + const expectedRichTextEditorOutputValue = '

Hello World

'; + const richTextDataTypeName = 'RichTextName'; + const richTextElementTypeName = 'RichTextElementName'; + const richTextElementGroupName = 'RTEElementGroup'; + await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(richTextElementTypeName); + + const richTextEditorDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(richTextDataTypeName); + const richTextElementTypeId = await umbracoApi.documentType.createDefaultElementType(richTextElementTypeName, richTextElementGroupName, richTextDataTypeName, richTextEditorDataTypeId); + blockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListDataTypeName, richTextElementTypeId); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockListDataTypeName, blockListDataTypeId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockElementButton(); + await umbracoUi.content.clickBlockCardWithName(richTextElementTypeName, true); + await umbracoUi.content.enterRTETipTapEditor(richTextEditorValue); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + // Asserts that the value in the RTE is as expected + const documentData = await umbracoApi.document.getByName(contentName); + const documentRichTextValues = documentData.values[0].value.contentData[0].values.find(value => value.alias === AliasHelper.toAlias(richTextDataTypeName)); + expect(documentRichTextValues.value.markup).toContain(expectedRichTextEditorOutputValue); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(richTextElementTypeName); +}); + +test('can publish a block list editor with a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringValue = 'Hello World'; + const blockGridDataTypeName = 'BlockGridDataTypeName'; + const blockGridElementTypeName = 'BlockGridElementName'; + const blockGridElementGroupName = 'GridElementGroup'; + await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockGridElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + + const textStringDataType = await umbracoApi.dataType.getByName(textStringDataTypeName); + textStringElementTypeId = await umbracoApi.documentType.createDefaultElementType(textStringElementTypeName, textStringGroupName, textStringDataTypeName, textStringDataType.id); + const blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridDataTypeName, textStringElementTypeId); + const blockGridElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockGridElementTypeName, blockGridElementGroupName, blockGridDataTypeName, blockGridDataTypeId); + blockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListDataTypeName, blockGridElementTypeId); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockListDataTypeName, blockListDataTypeId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockElementButton(); + await umbracoUi.content.clickBlockCardWithName(blockGridElementTypeName, true); + await umbracoUi.content.clickAddBlockWithNameButton(textStringElementTypeName); + await umbracoUi.content.clickBlockCardWithName(textStringElementTypeName, true); + await umbracoUi.content.enterTextstring(textStringValue); + await umbracoUi.content.clickCreateForModalWithHeadline('Add ' + textStringElementTypeName); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + // Asserts that the value in the BlockGrid is as expected + const documentData = await umbracoApi.document.getByName(contentName); + const documentValues = documentData.values.find(value => value.alias === AliasHelper.toAlias(blockListDataTypeName)); + expect(documentValues.value.contentData[0].values[0].value.contentData[0].values[0].value).toContain(textStringValue); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockGridElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeId); +}); + +test('can publish a block list editor with a block list editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringValue = 'Hello World'; + const secondBlockListDataTypeName = 'SecondBlockListName'; + const blockListElementTypeName = 'BlockListElementName'; + const blockListElementGroupName = 'ListElementGroup'; + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + await umbracoApi.dataType.ensureNameNotExists(secondBlockListDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName); + + const textStringDataType = await umbracoApi.dataType.getByName(textStringDataTypeName); + textStringElementTypeId = await umbracoApi.documentType.createDefaultElementType(textStringElementTypeName, textStringGroupName, textStringDataTypeName, textStringDataType.id); + const secondBlockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(secondBlockListDataTypeName, textStringElementTypeId); + const blockListElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockListElementTypeName, blockListElementGroupName, secondBlockListDataTypeName, secondBlockListDataTypeId); + blockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListDataTypeName, blockListElementTypeId); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockListDataTypeName, blockListDataTypeId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockElementButton(); + await umbracoUi.content.clickBlockCardWithName(blockListElementTypeName, true); + await umbracoUi.content.clickAddBlockWithNameButton(textStringElementTypeName); + await umbracoUi.content.clickBlockCardWithName(textStringElementTypeName, true); + await umbracoUi.content.enterTextstring(textStringValue); + await umbracoUi.content.clickCreateForModalWithHeadline('Add ' + textStringElementTypeName); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + // Asserts that the value in the BlockList is as expected + const documentData = await umbracoApi.document.getByName(contentName); + const documentValues = documentData.values.find(value => value.alias === AliasHelper.toAlias(blockListDataTypeName)); + expect(documentValues.value.contentData[0].values[0].value.contentData[0].values[0].value).toContain(textStringValue); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(secondBlockListDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/SecondLevelBlockProperties.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/SecondLevelBlockProperties.spec.ts new file mode 100644 index 0000000000..63179d576a --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/SecondLevelBlockProperties.spec.ts @@ -0,0 +1,165 @@ +import {AliasHelper, ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +// Content Name +const contentName = 'ContentName'; + +// Document Type +const documentTypeName = 'DocumentTypeName'; +let documentTypeId = null; +const documentTypeGroupName = 'DocumentGroup'; + +// Rich Text Editor +const richTextDataTypeName = 'RichTextDataType'; +let richTextDataTypeId = null; + +// Text String +const textStringElementTypeName = 'TextStringElementName'; +let textStringElementTypeId = null; +let textStringGroupName = 'TextGroup'; +const textStringDataTypeName = 'Textstring'; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName); +}); + +test('can publish a rich text editor with a rich text editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const richTextEditorValue = 'Hello First World'; + const secondRichTextEditorValue = 'Hello Second World'; + const expectedRichTextEditorOutputValue = '

' + richTextEditorValue + '

'; + const secondExpectedRichTextEditorOutputValue = '

' + secondRichTextEditorValue + '

'; + const secondRichTextDataTypeName = 'SecondRichTextName'; + const richTextElementTypeName = 'RichTextElementName'; + const richTextElementGroupName = 'RichTextElementGroupName'; + await umbracoApi.dataType.ensureNameNotExists(secondRichTextDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(richTextElementGroupName); + + const secondRichTextEditorDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(secondRichTextDataTypeName); + const richTextElementTypeId = await umbracoApi.documentType.createDefaultElementType(richTextElementTypeName, richTextElementGroupName, secondRichTextDataTypeName, secondRichTextEditorDataTypeId); + richTextDataTypeId = await umbracoApi.dataType.createRichTextEditorWithABlock(richTextDataTypeName, richTextElementTypeId); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, richTextDataTypeName, richTextDataTypeId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.enterRTETipTapEditor(richTextEditorValue); + await umbracoUi.content.clickInsertBlockButton(); + await umbracoUi.content.clickBlockCardWithName(richTextElementTypeName, true); + await umbracoUi.content.enterRTETipTapEditorWithName(AliasHelper.toAlias(secondRichTextDataTypeName), secondRichTextEditorValue); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + // Asserts that the value in the RTE is as expected + const documentData = await umbracoApi.document.getByName(contentName); + const documentValues = documentData.values.find(value => value.alias === AliasHelper.toAlias(richTextDataTypeName)); + // Value in the first RTE + expect(documentValues.value.markup).toContain(expectedRichTextEditorOutputValue); + // Value in the second RTE + const secondRTEInBlock = documentValues.value.blocks.contentData[0].values.find(value => value.alias === AliasHelper.toAlias(secondRichTextDataTypeName)); + expect(secondRTEInBlock.value.markup).toContain(secondExpectedRichTextEditorOutputValue); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(richTextElementGroupName); +}); + +test('can publish a rich text editor with a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringValue = 'Hello World'; + const blockGridDataTypeName = 'BlockGridDataTypeName'; + const blockGridElementTypeName = 'BlockGridElementTypeName'; + const blockGridElementGroupName = 'BlockGridElementGroupName'; + await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockGridElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + + const textStringDataType = await umbracoApi.dataType.getByName(textStringDataTypeName); + textStringElementTypeId = await umbracoApi.documentType.createDefaultElementType(textStringElementTypeName, textStringGroupName, textStringDataTypeName, textStringDataType.id); + const blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockWithInlineEditingMode(blockGridDataTypeName, textStringElementTypeId, true); + const blockGridElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockGridElementTypeName, blockGridElementGroupName, blockGridDataTypeName, blockGridDataTypeId); + richTextDataTypeId = await umbracoApi.dataType.createRichTextEditorWithABlock(richTextDataTypeName, blockGridElementTypeId); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, richTextDataTypeName, richTextDataTypeId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickInsertBlockButton(); + await umbracoUi.content.clickBlockCardWithName(blockGridElementTypeName, true); + await umbracoUi.content.clickAddBlockWithNameButton(textStringElementTypeName); + await umbracoUi.content.clickBlockCardWithName(textStringElementTypeName, true); + await umbracoUi.content.enterTextstring(textStringValue); + await umbracoUi.content.clickCreateForModalWithHeadline('Add ' + textStringElementTypeName); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + // Asserts that the value in the BlockGrid is as expected + const documentData = await umbracoApi.document.getByName(contentName); + expect(documentData.values[0].value.blocks.contentData[0].values[0].value.contentData[0].values[0].value).toContain(textStringValue); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockGridElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); +}); + +test('can publish a rich text editor with a block list editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringValue = 'Hello World'; + const blockListDataTypeName = 'BlockListName'; + const blockListElementTypeName = 'BlockListElementName'; + const blockListElementGroupName = 'BlockListGroupName'; + await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + + const textStringDataType = await umbracoApi.dataType.getByName(textStringDataTypeName); + textStringElementTypeId = await umbracoApi.documentType.createDefaultElementType(textStringElementTypeName, textStringGroupName, textStringDataTypeName, textStringDataType.id); + const blockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListDataTypeName, textStringElementTypeId); + const blockListElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockListElementTypeName, blockListElementGroupName, blockListDataTypeName, blockListDataTypeId); + richTextDataTypeId = await umbracoApi.dataType.createRichTextEditorWithABlock(richTextDataTypeName, blockListElementTypeId); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, richTextDataTypeName, richTextDataTypeId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickInsertBlockButton(); + await umbracoUi.content.clickBlockCardWithName(blockListElementTypeName, true); + await umbracoUi.content.clickAddBlockWithNameButton(textStringElementTypeName); + await umbracoUi.content.clickBlockCardWithName(textStringElementTypeName, true); + await umbracoUi.content.enterTextstring(textStringValue); + await umbracoUi.content.clickCreateForModalWithHeadline('Add ' + textStringElementTypeName); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + // Asserts that the value in the BlockGrid is as expected + const documentData = await umbracoApi.document.getByName(contentName); + expect(documentData.values[0].value.blocks.contentData[0].values[0].value.contentData[0].values[0].value).toContain(textStringValue); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); +}); From 3f10bd8c21bdb16f2bb9e38a68d7b38c78ffac4f Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 19 May 2025 10:54:22 +0200 Subject: [PATCH 17/17] Added logging and try/catch around retrieval of references, so we don't block critical operations following an incompatible data type change (#18576) * Added logging and try/catch around retrieval of references, so we don't block critical operations following an incompatible data type change. * Added a little more detail to the log message. * Added a little more detail to the log message. * Fix unittest mock dependency --------- Co-authored-by: Migaroez --- .../DataValueReferenceFactoryCollection.cs | 65 +++++++++++++++++-- .../BlockValuePropertyValueEditorBase.cs | 6 +- .../Repositories/DocumentRepositoryTest.cs | 3 +- .../Repositories/MediaRepositoryTest.cs | 3 +- .../Repositories/MemberRepositoryTest.cs | 3 +- .../Repositories/TemplateRepositoryTest.cs | 3 +- ...BlockListEditorPropertyValueEditorTests.cs | 3 +- .../DataValueEditorReuseTests.cs | 5 +- ...ataValueReferenceFactoryCollectionTests.cs | 11 ++-- 9 files changed, 81 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs index c35ae6d2ae..5d137d04c6 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs @@ -1,4 +1,7 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; @@ -12,14 +15,27 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase _logger; + /// /// Initializes a new instance of the class. /// /// The items. + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")] public DataValueReferenceFactoryCollection(Func> items) - : base(items) + : this( + items, + StaticServiceProvider.Instance.GetRequiredService>()) { } + /// + /// Initializes a new instance of the class. + /// + /// The items. + /// The logger. + public DataValueReferenceFactoryCollection(Func> items, ILogger logger) + : base(items) => _logger = logger; + /// /// Gets all unique references from the specified properties. /// @@ -33,7 +49,7 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase(); // Group by property editor alias to avoid duplicate lookups and optimize value parsing - foreach (var propertyValuesByPropertyEditorAlias in properties.GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Values)) + foreach (IGrouping> propertyValuesByPropertyEditorAlias in properties.GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Values)) { if (!propertyEditors.TryGet(propertyValuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor)) { @@ -48,7 +64,7 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase public ISet GetReferences(IDataEditor dataEditor, IEnumerable values) => - GetReferencesEnumerable(dataEditor, values).ToHashSet(); - private IEnumerable GetReferencesEnumerable(IDataEditor dataEditor, IEnumerable values) + GetReferencesEnumerable(dataEditor, values, null).ToHashSet(); + + private ISet GetReferences(IDataEditor dataEditor, IEnumerable values, string propertyEditorAlias) => + GetReferencesEnumerable(dataEditor, values, propertyEditorAlias).ToHashSet(); + + private IEnumerable GetReferencesEnumerable(IDataEditor dataEditor, IEnumerable values, string? propertyEditorAlias) { // TODO: We will need to change this once we support tracking via variants/segments // for now, we are tracking values from ALL variants if (dataEditor.GetValueEditor() is IDataValueReference dataValueReference) { - foreach (UmbracoEntityReference reference in values.SelectMany(dataValueReference.GetReferences)) + foreach (UmbracoEntityReference reference in GetReferencesFromPropertyValues(values, dataValueReference, propertyEditorAlias)) { yield return reference; } @@ -107,6 +127,38 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase GetReferencesFromPropertyValues(IEnumerable values, IDataValueReference dataValueReference, string? propertyEditorAlias) + { + var result = new List(); + foreach (var value in values) + { + // When property editors on data types are changed, we could have values that are incompatible with the new editor. + // Leading to issues such as: + // - https://github.com/umbraco/Umbraco-CMS/issues/17628 + // - https://github.com/umbraco/Umbraco-CMS/issues/17725 + // Although some changes like this are not intended to be compatible, we should handle them gracefully and not + // error in retrieving references, which would prevent manipulating or deleting the content that uses the data type. + try + { + IEnumerable references = dataValueReference.GetReferences(value); + result.AddRange(references); + } + catch (Exception ex) + { + // Log the exception but don't throw, continue with the next value. + _logger.LogError( + ex, + "Error getting references from value {Value} with data editor {DataEditor} and property editor alias {PropertyEditorAlias}.", + value, + dataValueReference.GetType().FullName, + propertyEditorAlias ?? "n/a"); + throw; + } + } + + return result; + } + /// /// Gets all relation type aliases that are automatically tracked. /// @@ -117,6 +169,7 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase GetAutomaticRelationTypesAliases(PropertyEditorCollection propertyEditors) => GetAllAutomaticRelationTypesAliases(propertyEditors); + public ISet GetAllAutomaticRelationTypesAliases(PropertyEditorCollection propertyEditors) { // Always add default automatic relation types diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs index dfdb55fc63..fff521d13a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs @@ -99,17 +99,17 @@ public abstract class BlockValuePropertyValueEditorBase : DataV continue; } - var districtValues = valuesByPropertyEditorAlias.Distinct().ToArray(); + var distinctValues = valuesByPropertyEditorAlias.Distinct().ToArray(); if (dataEditor.GetValueEditor() is IDataValueReference reference) { - foreach (UmbracoEntityReference value in districtValues.SelectMany(reference.GetReferences)) + foreach (UmbracoEntityReference value in distinctValues.SelectMany(reference.GetReferences)) { result.Add(value); } } - IEnumerable references = _dataValueReferenceFactoryCollection.GetReferences(dataEditor, districtValues); + IEnumerable references = _dataValueReferenceFactoryCollection.GetReferences(dataEditor, distinctValues); foreach (UmbracoEntityReference value in references) { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs index e6ce8d467e..37ae7fb071 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; @@ -129,7 +130,7 @@ public class DocumentRepositoryTest : UmbracoIntegrationTest var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => Enumerable.Empty())); var dataValueReferences = - new DataValueReferenceFactoryCollection(() => Enumerable.Empty()); + new DataValueReferenceFactoryCollection(() => Enumerable.Empty(), new NullLogger()); var repository = new DocumentRepository( scopeAccessor, appCaches, diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs index 932be80a01..c01d84de7e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs @@ -3,6 +3,7 @@ using System.Linq; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; @@ -64,7 +65,7 @@ public class MediaRepositoryTest : UmbracoIntegrationTest new PropertyEditorCollection(new DataEditorCollection(() => Enumerable.Empty())); var mediaUrlGenerators = new MediaUrlGeneratorCollection(() => Enumerable.Empty()); var dataValueReferences = - new DataValueReferenceFactoryCollection(() => Enumerable.Empty()); + new DataValueReferenceFactoryCollection(() => Enumerable.Empty(), new NullLogger()); var repository = new MediaRepository( scopeAccessor, appCaches, diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberRepositoryTest.cs index 58dc4bb4b0..a935087701 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberRepositoryTest.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Linq; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using NPoco; @@ -53,7 +54,7 @@ public class MemberRepositoryTest : UmbracoIntegrationTest var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => Enumerable.Empty())); var dataValueReferences = - new DataValueReferenceFactoryCollection(() => Enumerable.Empty()); + new DataValueReferenceFactoryCollection(() => Enumerable.Empty(), new NullLogger()); return new MemberRepository( accessor, AppCaches.Disabled, diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs index 8e4a68af70..02e5bac7c5 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Text; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; @@ -272,7 +273,7 @@ public class TemplateRepositoryTest : UmbracoIntegrationTest var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => Enumerable.Empty())); var dataValueReferences = - new DataValueReferenceFactoryCollection(() => Enumerable.Empty()); + new DataValueReferenceFactoryCollection(() => Enumerable.Empty(), new NullLogger()); var contentRepo = new DocumentRepository( scopeAccessor, AppCaches.Disabled, diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListEditorPropertyValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListEditorPropertyValueEditorTests.cs index 879400f79a..fc56535200 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListEditorPropertyValueEditorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListEditorPropertyValueEditorTests.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.Text.Json.Nodes; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Moq; using NUnit.Framework; @@ -148,7 +149,7 @@ public class BlockListEditorPropertyValueEditorTests new DataEditorAttribute("alias"), new BlockListEditorDataConverter(jsonSerializer), new(new DataEditorCollection(() => [])), - new DataValueReferenceFactoryCollection(Enumerable.Empty), + new DataValueReferenceFactoryCollection(Enumerable.Empty, Mock.Of>()), Mock.Of(), Mock.Of(), localizedTextServiceMock.Object, diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs index 16ea5c11f9..f83b779c63 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Cache; @@ -35,7 +36,7 @@ public class DataValueEditorReuseTests Mock.Of())); _propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)); - _dataValueReferenceFactories = new DataValueReferenceFactoryCollection(Enumerable.Empty); + _dataValueReferenceFactories = new DataValueReferenceFactoryCollection(Enumerable.Empty, new NullLogger()); var blockVarianceHandler = new BlockEditorVarianceHandler(Mock.Of(), Mock.Of()); _dataValueEditorFactoryMock diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs index 33f4f307f8..43757f064c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using Microsoft.Extensions.Logging.Abstractions; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; @@ -49,7 +50,7 @@ public class DataValueReferenceFactoryCollectionTests [Test] public void GetAllReferences_All_Variants_With_IDataValueReferenceFactory() { - var collection = new DataValueReferenceFactoryCollection(() => new TestDataValueReferenceFactory().Yield()); + var collection = new DataValueReferenceFactoryCollection(() => new TestDataValueReferenceFactory().Yield(), new NullLogger()); // label does not implement IDataValueReference var labelEditor = new LabelPropertyEditor( @@ -93,7 +94,7 @@ public class DataValueReferenceFactoryCollectionTests [Test] public void GetAllReferences_All_Variants_With_IDataValueReference_Editor() { - var collection = new DataValueReferenceFactoryCollection(() => Enumerable.Empty()); + var collection = new DataValueReferenceFactoryCollection(() => Enumerable.Empty(), new NullLogger()); // mediaPicker does implement IDataValueReference var mediaPicker = new MediaPicker3PropertyEditor( @@ -137,7 +138,7 @@ public class DataValueReferenceFactoryCollectionTests [Test] public void GetAllReferences_Invariant_With_IDataValueReference_Editor() { - var collection = new DataValueReferenceFactoryCollection(() => Enumerable.Empty()); + var collection = new DataValueReferenceFactoryCollection(() => Enumerable.Empty(), new NullLogger()); // mediaPicker does implement IDataValueReference var mediaPicker = new MediaPicker3PropertyEditor( @@ -181,7 +182,7 @@ public class DataValueReferenceFactoryCollectionTests [Test] public void GetAutomaticRelationTypesAliases_ContainsDefault() { - var collection = new DataValueReferenceFactoryCollection(Enumerable.Empty); + var collection = new DataValueReferenceFactoryCollection(Enumerable.Empty, new NullLogger()); var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)); var result = collection.GetAllAutomaticRelationTypesAliases(propertyEditors).ToArray(); @@ -193,7 +194,7 @@ public class DataValueReferenceFactoryCollectionTests [Test] public void GetAutomaticRelationTypesAliases_ContainsCustom() { - var collection = new DataValueReferenceFactoryCollection(() => new TestDataValueReferenceFactory().Yield()); + var collection = new DataValueReferenceFactoryCollection(() => new TestDataValueReferenceFactory().Yield(), new NullLogger()); var labelPropertyEditor = new LabelPropertyEditor(DataValueEditorFactory, IOHelper); var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => labelPropertyEditor.Yield()));