From 158cef16929de4991a3917636e0ec41d3db76d7d Mon Sep 17 00:00:00 2001 From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Date: Wed, 20 Sep 2023 12:15:24 +0200 Subject: [PATCH 1/7] V10 fixed up failing acceptance test (#14829) * Updated playwright, our builders, and our helpers * Updated the use of our testhelper methods, we removed the need for sending umbracoApi through the methods as a parameter since it was unnecessary * Added a timeout * Updated successNotifications and added a locator * Removed timeout from successNotification * Added additional timeouts and updated old locators which were failing * Updated locator so it only finds one element * Removed timeouts and added a waitForTimeout * Removed timeouts * Removed timeouts * Added timeouts * Updated locator * Added a timeout * Updated locators for clicking button * Changed locator for adding the image, sometimes the old locator would find 2 locators --- .../package-lock.json | 114 ++++++++++-------- .../Umbraco.Tests.AcceptanceTest/package.json | 6 +- .../Content/blockGridEditorAdvanced.spec.ts | 24 ++-- .../blockGridEditorAreasContent.spec.ts | 96 +++++++-------- .../Content/blockGridEditorContent.spec.ts | 56 ++++----- .../Content/blockGridEditorRendering.spec.ts | 60 ++++----- .../Content/blockGridEditorSettings.spec.ts | 38 +++--- .../BlockGridEditorDataTypeBlocks.spec.ts | 23 ++-- .../blockListEditorContent.spec.ts | 7 +- .../blockListEditorDocument.spec.ts | 17 ++- .../DefaultConfig/Content/content.spec.ts | 12 +- .../DefaultConfig/Content/routing.spec.ts | 14 +-- .../DefaultConfig/DataTypes/dataTypes.spec.ts | 15 ++- .../DataTypes/textBoxVariation.spec.ts | 6 +- .../DefaultConfig/Media/mediaSection.spec.ts | 2 +- .../ModelsBuilder/modelsbuilder.spec.ts | 22 ++-- .../Settings/partialViewMacroFiles.spec.ts | 24 ++-- .../Settings/partialViews.spec.ts | 8 +- 18 files changed, 281 insertions(+), 263 deletions(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index e0c761a08b..cb06c7964d 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": "^1.0.5", - "@umbraco/playwright-testhelpers": "^1.0.22", + "@umbraco/json-models-builders": "^1.0.6", + "@umbraco/playwright-testhelpers": "^1.0.25", "camelize": "^1.0.0", "dotenv": "^16.0.2", "faker": "^4.1.0", @@ -17,7 +17,7 @@ "xhr2": "^0.2.1" }, "devDependencies": { - "@playwright/test": "^1.32", + "@playwright/test": "^1.38", "del": "^6.0.0", "ncp": "^2.0.0", "prompt": "^1.2.0", @@ -70,45 +70,35 @@ } }, "node_modules/@playwright/test": { - "version": "1.32.3", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.3.tgz", - "integrity": "sha512-BvWNvK0RfBriindxhLVabi8BRe3X0J9EVjKlcmhxjg4giWBD/xleLcg2dz7Tx0agu28rczjNIPQWznwzDwVsZQ==", + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.0.tgz", + "integrity": "sha512-xis/RXXsLxwThKnlIXouxmIvvT3zvQj1JE39GsNieMUrMpb3/GySHDh2j8itCG22qKVD4MYLBp7xB73cUW/UUw==", "dev": true, "dependencies": { - "@types/node": "*", - "playwright-core": "1.32.3" + "playwright": "1.38.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "fsevents": "2.3.2" + "node": ">=16" } }, - "node_modules/@types/node": { - "version": "14.17.33", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.33.tgz", - "integrity": "sha512-noEeJ06zbn3lOh4gqe2v7NMGS33jrulfNqYFDjjEbhpDEHR5VTxgYNQSBqBlJIsBJW3uEYDgD6kvMnrrhGzq8g==", - "dev": true - }, "node_modules/@umbraco/json-models-builders": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-1.0.5.tgz", - "integrity": "sha512-14xowT8oiW9+DQVOoundRPvcvnNrU0Ey+06G/q/iZyUnqaNRu/i5nUqcbUZGdv6VBCdxaxq2H3WwtSET3gtneA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-1.0.6.tgz", + "integrity": "sha512-bXwfXcpuqG1Ye714L9KJEGXuSzJfckysE/6CuPjdG8FqHWTE1brv28teR2oMw+ih8ca2u2zUboRgdzLEU/1D3Q==", "dependencies": { "camelize": "^1.0.0", "faker": "^4.1.0" } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "1.0.22", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.22.tgz", - "integrity": "sha512-hFqqQvEIylagfqFyhQ2rSyYlUP+xpWA5lkhJjkpb2qpxkIISxjwC/FYJTJGvcoBHuUaZrjsSv4lM2aJy2ZWHMA==", + "version": "1.0.25", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.25.tgz", + "integrity": "sha512-6H452J6LhP0EHjF4jR7V7i0U8WPTiAbSyhN1J459BbbYEJ4QX1A2ZlCdA6VSBAsK1xYdMXD+yxsVJq7AAwiy9A==", "dependencies": { - "@umbraco/json-models-builders": "^1.0.5", + "@umbraco/json-models-builders": "^1.0.6", "camelize": "^1.0.0", "faker": "^4.1.0", "form-data": "^4.0.0", @@ -647,16 +637,34 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/playwright-core": { - "version": "1.32.3", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.32.3.tgz", - "integrity": "sha512-SB+cdrnu74ZIn5Ogh/8278ngEh9NEEV0vR4sJFmK04h2iZpybfbqBY0bX6+BLYWVdV12JLLI+JEFtSnYgR+mWg==", + "node_modules/playwright": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.0.tgz", + "integrity": "sha512-fJGw+HO0YY+fU/F1N57DMO+TmXHTrmr905J05zwAQE9xkuwP/QLDk63rVhmyxh03dYnEhnRbsdbH9B0UVVRB3A==", "dev": true, + "dependencies": { + "playwright-core": "1.38.0" + }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=14" + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.0.tgz", + "integrity": "sha512-f8z1y8J9zvmHoEhKgspmCvOExF2XdcxMW8jNRuX4vkQFrzV4MlZ55iwb5QeyiFQgOFCUolXiRHgpjSEnqvO48g==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" } }, "node_modules/prompt": { @@ -898,37 +906,29 @@ } }, "@playwright/test": { - "version": "1.32.3", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.3.tgz", - "integrity": "sha512-BvWNvK0RfBriindxhLVabi8BRe3X0J9EVjKlcmhxjg4giWBD/xleLcg2dz7Tx0agu28rczjNIPQWznwzDwVsZQ==", + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.0.tgz", + "integrity": "sha512-xis/RXXsLxwThKnlIXouxmIvvT3zvQj1JE39GsNieMUrMpb3/GySHDh2j8itCG22qKVD4MYLBp7xB73cUW/UUw==", "dev": true, "requires": { - "@types/node": "*", - "fsevents": "2.3.2", - "playwright-core": "1.32.3" + "playwright": "1.38.0" } }, - "@types/node": { - "version": "14.17.33", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.33.tgz", - "integrity": "sha512-noEeJ06zbn3lOh4gqe2v7NMGS33jrulfNqYFDjjEbhpDEHR5VTxgYNQSBqBlJIsBJW3uEYDgD6kvMnrrhGzq8g==", - "dev": true - }, "@umbraco/json-models-builders": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-1.0.5.tgz", - "integrity": "sha512-14xowT8oiW9+DQVOoundRPvcvnNrU0Ey+06G/q/iZyUnqaNRu/i5nUqcbUZGdv6VBCdxaxq2H3WwtSET3gtneA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-1.0.6.tgz", + "integrity": "sha512-bXwfXcpuqG1Ye714L9KJEGXuSzJfckysE/6CuPjdG8FqHWTE1brv28teR2oMw+ih8ca2u2zUboRgdzLEU/1D3Q==", "requires": { "camelize": "^1.0.0", "faker": "^4.1.0" } }, "@umbraco/playwright-testhelpers": { - "version": "1.0.22", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.22.tgz", - "integrity": "sha512-hFqqQvEIylagfqFyhQ2rSyYlUP+xpWA5lkhJjkpb2qpxkIISxjwC/FYJTJGvcoBHuUaZrjsSv4lM2aJy2ZWHMA==", + "version": "1.0.25", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.25.tgz", + "integrity": "sha512-6H452J6LhP0EHjF4jR7V7i0U8WPTiAbSyhN1J459BbbYEJ4QX1A2ZlCdA6VSBAsK1xYdMXD+yxsVJq7AAwiy9A==", "requires": { - "@umbraco/json-models-builders": "^1.0.5", + "@umbraco/json-models-builders": "^1.0.6", "camelize": "^1.0.0", "faker": "^4.1.0", "form-data": "^4.0.0", @@ -1329,10 +1329,20 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "playwright": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.0.tgz", + "integrity": "sha512-fJGw+HO0YY+fU/F1N57DMO+TmXHTrmr905J05zwAQE9xkuwP/QLDk63rVhmyxh03dYnEhnRbsdbH9B0UVVRB3A==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.38.0" + } + }, "playwright-core": { - "version": "1.32.3", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.32.3.tgz", - "integrity": "sha512-SB+cdrnu74ZIn5Ogh/8278ngEh9NEEV0vR4sJFmK04h2iZpybfbqBY0bX6+BLYWVdV12JLLI+JEFtSnYgR+mWg==", + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.0.tgz", + "integrity": "sha512-f8z1y8J9zvmHoEhKgspmCvOExF2XdcxMW8jNRuX4vkQFrzV4MlZ55iwb5QeyiFQgOFCUolXiRHgpjSEnqvO48g==", "dev": true }, "prompt": { diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 6262fe6ced..5cabb6b091 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -10,7 +10,7 @@ "createTest": "node createTest.js" }, "devDependencies": { - "@playwright/test": "^1.32", + "@playwright/test": "^1.38", "typescript": "^4.8.3", "tslib": "^2.4.0", "del": "^6.0.0", @@ -18,8 +18,8 @@ "prompt": "^1.2.0" }, "dependencies": { - "@umbraco/json-models-builders": "^1.0.5", - "@umbraco/playwright-testhelpers": "^1.0.22", + "@umbraco/json-models-builders": "^1.0.6", + "@umbraco/playwright-testhelpers": "^1.0.25", "camelize": "^1.0.0", "faker": "^4.1.0", "form-data": "^4.0.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorAdvanced.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorAdvanced.spec.ts index 87b19b8776..79a5cacf71 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorAdvanced.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorAdvanced.spec.ts @@ -44,7 +44,7 @@ test.describe('BlockGridEditorAdvancedContent', () => { await umbracoApi.media.ensureNameNotExists(customViewItemName); const imageData = await umbracoApi.media.createImageWithFile(imageName, umbracoFileValue, imageFileName, imagePath, imageMimeType); - + const customViewData = await umbracoApi.media.createFileWithFile(customViewItemName, customViewFileName, customViewPath, customViewMimeType); const customViewMediaPath = customViewData.mediaLink; @@ -72,7 +72,7 @@ test.describe('BlockGridEditorAdvancedContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(umbracoApi, element, dataType); + await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(element, dataType); const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) @@ -101,7 +101,7 @@ test.describe('BlockGridEditorAdvancedContent', () => { await umbracoUi.navigateToContent(blockGridName); // Assert - // Checks if the block has the correct CustomView + // Checks if the block has the correct CustomView await expect(page.locator('[data-content-element-type-key="' + element['key'] + '"]').locator('[view="' + customViewMediaPath + '"]')).toBeVisible(); // Checks if the custom view updated the block by locating a name in the customView await expect(page.locator('[data-content-element-type-key="' + element['key'] + '"]').locator('[view="' + customViewMediaPath + '"]').locator('[name="BlockGridCustomView"]')).toBeVisible(); @@ -134,10 +134,10 @@ test.describe('BlockGridEditorAdvancedContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, null); + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, false); await umbracoUi.navigateToContent(blockGridName); - + // Assert // Checks if the block has the correct template await expect(page.locator('umb-block-grid-entry', {hasText: elementName}).locator('umb-block-grid-block')).toHaveAttribute('stylesheet', stylesheetDataPath); @@ -160,7 +160,7 @@ test.describe('BlockGridEditorAdvancedContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, null); + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, false); await umbracoUi.navigateToContent(blockGridName); @@ -186,7 +186,7 @@ test.describe('BlockGridEditorAdvancedContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, null); + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, false); await umbracoUi.navigateToContent(blockGridName); @@ -213,7 +213,7 @@ test.describe('BlockGridEditorAdvancedContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, null); + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, false); await umbracoUi.navigateToContent(blockGridName); @@ -250,7 +250,7 @@ test.describe('BlockGridEditorAdvancedContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, null); + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, false); await umbracoUi.navigateToContent(blockGridName); @@ -288,7 +288,7 @@ test.describe('BlockGridEditorAdvancedContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, null); + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, false); await umbracoUi.navigateToContent(blockGridName); @@ -336,7 +336,7 @@ test.describe('BlockGridEditorAdvancedContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, null); + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, false); await umbracoUi.navigateToContent(blockGridName); @@ -352,4 +352,4 @@ test.describe('BlockGridEditorAdvancedContent', () => { await umbracoApi.media.ensureNameNotExists(imageName); }); }); -}); \ No newline at end of file +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorAreasContent.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorAreasContent.spec.ts index a55c12a7d9..5419c74adc 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorAreasContent.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorAreasContent.spec.ts @@ -6,26 +6,26 @@ import {expect} from "@playwright/test"; test.describe('BlockGridEditorAreasContent', () => { const documentName = 'DocumentTest'; const blockGridName = 'BlockGridTest'; - const elementTitleName = 'ElementTitle'; + const elementTitleName = 'ElementTitle'; const titleText = 'ElementTitle'; const titleArea = 'AreaTitle'; const elementBodyName = 'ElementBody'; const bodyText = 'Lorem ipsum dolor sit amet'; - + const elementBodyAlias = AliasHelper.toAlias(elementBodyName); const documentAlias = AliasHelper.toAlias(documentName); const blockGridAlias = AliasHelper.toAlias(blockGridName); const elementTitleAlias = AliasHelper.toAlias(elementTitleName); - + async function createContentWithABlockInAnotherBlock(umbracoApi,elementParent, elementChild, dataType?, document?) { if (dataType == null) { - dataType = await umbracoApi.dataTypes.createBlockGridDataTypeWithArea(umbracoApi, elementParent, elementChild, blockGridName, titleArea); + dataType = await umbracoApi.dataTypes.createBlockGridDataTypeWithArea(elementParent, elementChild, blockGridName, titleArea); } if (document == null) { - await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(umbracoApi, elementParent, dataType); + await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(elementParent, dataType); } - + const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) .withAction(ConstantHelper.actions.save) @@ -58,7 +58,7 @@ test.describe('BlockGridEditorAreasContent', () => { .build(); return await umbracoApi.content.save(rootContentNode); } - + test.beforeEach(async ({page, umbracoApi, umbracoUi}, testInfo) => { await umbracoApi.report.report(testInfo); await umbracoApi.login(); @@ -80,12 +80,12 @@ test.describe('BlockGridEditorAreasContent', () => { const element = await umbracoApi.documentTypes.createDefaultElementType(elementTitleName, elementTitleAlias); const elementBody = await umbracoApi.documentTypes.createDefaultElementType(elementBodyName, elementBodyAlias); - const dataType = await umbracoApi.dataTypes.createBlockGridDataTypeWithArea(umbracoApi, element, elementBody, blockGridName, titleArea); - - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, null); + const dataType = await umbracoApi.dataTypes.createBlockGridDataTypeWithArea(element, elementBody, blockGridName, titleArea); + + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, false); await umbracoUi.navigateToContent(blockGridName); - + // Adds a body to the area await page.locator('[data-content-element-type-key="' + element['key'] + '"] >> [data-area-alias="' + titleArea + '"]').click(); await page.locator('[name="infiniteEditorForm"]').locator('[data-element="editor-container"]').getByRole('button', {name: elementBodyName}).click(); @@ -166,9 +166,9 @@ test.describe('BlockGridEditorAreasContent', () => { .done() .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - + await createContentWithABlockInAnotherBlock(umbracoApi, element, elementBody, dataType); - + await umbracoUi.navigateToContent(blockGridName); // Adds another body to the area @@ -204,7 +204,7 @@ test.describe('BlockGridEditorAreasContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, null); + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, false); await umbracoUi.navigateToContent(blockGridName); @@ -212,12 +212,12 @@ test.describe('BlockGridEditorAreasContent', () => { // Checks if the area in content has the value that is defined in the block grid editor await expect(page.locator('[data-content-element-type-key="' + element['key'] + '"]').locator('[data-area-alias="' + titleArea + '"]')).toHaveAttribute('data-area-col-span', '6'); }); - + test('can add two different blocks in a area with different grid columns', async ({page, umbracoApi, umbracoUi}) => { const secondArea = 'AreaFour'; const columnSpanEight = "8"; const columnSpanFour = "4"; - + const element = await umbracoApi.documentTypes.createDefaultElementType(elementTitleName, elementTitleAlias); const elementBody = await umbracoApi.documentTypes.createDefaultElementType(elementBodyName, elementBodyAlias); @@ -239,8 +239,8 @@ test.describe('BlockGridEditorAreasContent', () => { .done() .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - - await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(umbracoApi, element, dataType); + + await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(element, dataType); const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) @@ -275,14 +275,14 @@ test.describe('BlockGridEditorAreasContent', () => { await umbracoApi.content.save(rootContentNode); await umbracoUi.navigateToContent(blockGridName); - + // Adds a block to the area with a different column span await page.locator('[data-content-element-type-key="' + element['key'] + '"]').locator('[data-area-alias="' + secondArea + '"]').getByRole('button', {name: 'Add content'}).click(); await page.locator('[name="infiniteEditorForm"]').locator('[data-element="editor-container"]').getByRole('button', {name: elementBodyName}).click(); await page.locator('[id="sub-view-0"]').locator('[id="title"]').fill('BodyTwoText'); await page.locator('[label="Create"]').click(); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)); - + // Assert await umbracoUi.isSuccessNotificationVisible(); // Checks if there are two block in the ElementTitle Area @@ -311,14 +311,14 @@ test.describe('BlockGridEditorAreasContent', () => { .done() .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, false); + + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, false); await umbracoUi.navigateToContent(blockGridName); // Checks if the button has the correct row span await expect(page.locator('[data-content-element-type-key="' + element['key'] + '"]').locator('[data-area-row-span="3"]').getByRole('button', {name: 'Add content'})).toBeVisible(); - + // Adds a block to the area await page.locator('[data-content-element-type-key="' + element['key'] + '"]').locator('[data-area-alias="' + titleArea + '"]').getByRole('button', {name: 'Add content'}).click(); await page.locator('[name="infiniteEditorForm"]').locator('[data-element="editor-container"]').getByRole('button', {name: elementBodyName}).click(); @@ -336,7 +336,7 @@ test.describe('BlockGridEditorAreasContent', () => { await expect(page.locator('[data-content-element-type-key="' + element['key'] + '"]').locator('[data-content-element-type-alias="' + elementBodyAlias + '"]')).toHaveAttribute('data-row-span', '3'); }); }); - + test.describe('Create Button Label', () => { test('can add a create button label for an area in block grid editor', async ({page, umbracoApi, umbracoUi}) => { const newButtonLabel = 'NewAreaBlock'; @@ -358,9 +358,9 @@ test.describe('BlockGridEditorAreasContent', () => { .done() .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, false); - + + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, false); + await umbracoUi.navigateToContent(blockGridName); // Assert @@ -371,7 +371,7 @@ test.describe('BlockGridEditorAreasContent', () => { await createButtonLocator.click(); }); }); - + test.describe('Number of blocks', () => { test('can add a minimum number of blocks for a block grid editor', async ({page, umbracoApi, umbracoUi}) => { const element = await umbracoApi.documentTypes.createDefaultElementType(elementTitleName, elementTitleAlias); @@ -418,7 +418,7 @@ test.describe('BlockGridEditorAreasContent', () => { test('can add a maximum number of blocks for a block grid editor', async ({page, umbracoApi, umbracoUi}) => { const bodyTextTwo = 'AnotherBody'; - + const element = await umbracoApi.documentTypes.createDefaultElementType(elementTitleName, elementTitleAlias); const elementBody = await umbracoApi.documentTypes.createDefaultElementType(elementBodyName, elementBodyAlias); @@ -437,7 +437,7 @@ test.describe('BlockGridEditorAreasContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(umbracoApi, element, dataType); + await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(element, dataType); const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) @@ -522,7 +522,7 @@ test.describe('BlockGridEditorAreasContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(umbracoApi, element, dataType); + await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(element, dataType); const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) @@ -551,7 +551,7 @@ test.describe('BlockGridEditorAreasContent', () => { await umbracoApi.content.save(rootContentNode); await umbracoUi.navigateToContent(blockGridName); - + // Adds a ElementBody await page.locator('[data-content-element-type-key="' + element['key'] + '"]').locator('[data-area-alias="' + titleArea + '"]').click(); // Since the ElementBody is added as the only Specified Allowance for the area of the Element, then we should be instantly directed to it, instead of having to pick it. @@ -586,7 +586,7 @@ test.describe('BlockGridEditorAreasContent', () => { const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); await createContentWithABlockInAnotherBlock(umbracoApi, element, elementBody, dataType, null); - + await umbracoUi.navigateToContent(blockGridName); // Checks if a validation error is visible @@ -630,7 +630,7 @@ test.describe('BlockGridEditorAreasContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(umbracoApi, element, dataType); + await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(element, dataType); const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) @@ -736,7 +736,7 @@ test.describe('BlockGridEditorAreasContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(umbracoApi, element, dataType); + await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(element, dataType); const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) @@ -778,7 +778,7 @@ test.describe('BlockGridEditorAreasContent', () => { await umbracoApi.content.save(rootContentNode); await umbracoUi.navigateToContent(blockGridName); - + // Checks if there is validation error for both blocks await expect(page.locator('[key="blockEditor_areaValidationEntriesExceed"]', {hasText: elementBodyName})).toBeVisible(); await expect(page.locator('[key="blockEditor_areaValidationEntriesShort"]', {hasText: elementFooterName})).toBeVisible(); @@ -808,7 +808,7 @@ test.describe('BlockGridEditorAreasContent', () => { await expect(page.locator('[data-content-element-type-key="' + element['key'] + '"]').locator('[data-content-element-type-alias="' + elementBodyAlias + '"]')).toHaveCount(1); await expect(page.locator('[data-content-element-type-key="' + element['key'] + '"]').locator('[data-content-element-type-alias="' + elementFooterAlias + '"]')).toHaveCount(1); - // Clean + // Clean await umbracoApi.documentTypes.ensureNameNotExists(elementFooterName); }); }); @@ -818,9 +818,9 @@ test.describe('BlockGridEditorAreasContent', () => { const element = await umbracoApi.documentTypes.createDefaultElementType(elementTitleName, elementTitleAlias); const elementBody = await umbracoApi.documentTypes.createDefaultElementType(elementBodyName, elementBodyAlias); - const dataType = await umbracoApi.dataTypes.createBlockGridDataTypeWithArea(umbracoApi, element, elementBody, blockGridName, titleArea); + const dataType = await umbracoApi.dataTypes.createBlockGridDataTypeWithArea(element, elementBody, blockGridName, titleArea); - await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(umbracoApi, element, dataType); + await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(element, dataType); const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) @@ -869,9 +869,9 @@ test.describe('BlockGridEditorAreasContent', () => { const element = await umbracoApi.documentTypes.createDefaultElementType(elementTitleName, elementTitleAlias); const elementBody = await umbracoApi.documentTypes.createDefaultElementType(elementBodyName, elementBodyAlias); - const dataType = await umbracoApi.dataTypes.createBlockGridDataTypeWithArea(umbracoApi, element, elementBody, blockGridName, titleArea); + const dataType = await umbracoApi.dataTypes.createBlockGridDataTypeWithArea(element, elementBody, blockGridName, titleArea); - await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(umbracoApi, element, dataType); + await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(element, dataType); const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) @@ -935,9 +935,9 @@ test.describe('BlockGridEditorAreasContent', () => { const element = await umbracoApi.documentTypes.createDefaultElementType(elementTitleName, elementTitleAlias); const elementBody = await umbracoApi.documentTypes.createDefaultElementType(elementBodyName, elementBodyAlias); - const dataType = await umbracoApi.dataTypes.createBlockGridDataTypeWithArea(umbracoApi, element, elementBody, blockGridName, titleArea); + const dataType = await umbracoApi.dataTypes.createBlockGridDataTypeWithArea(element, elementBody, blockGridName, titleArea); - await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(umbracoApi, element, dataType); + await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(element, dataType); const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) @@ -1002,9 +1002,9 @@ test.describe('BlockGridEditorAreasContent', () => { const element = await umbracoApi.documentTypes.createDefaultElementType(elementTitleName, elementTitleAlias); const elementBody = await umbracoApi.documentTypes.createDefaultElementType(elementBodyName, elementBodyAlias); - const dataType = await umbracoApi.dataTypes.createBlockGridDataTypeWithArea(umbracoApi, element, elementBody, blockGridName, titleArea); + const dataType = await umbracoApi.dataTypes.createBlockGridDataTypeWithArea(element, elementBody, blockGridName, titleArea); - await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(umbracoApi, element, dataType); + await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(element, dataType); const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) @@ -1041,14 +1041,14 @@ test.describe('BlockGridEditorAreasContent', () => { const blockParentUdi = rootContentNode.variants[0].properties[0].value.contentData[1].udi; await umbracoUi.navigateToContent(blockGridName); - + // Copies and pastes the block into another block await page.locator('[data-element-udi="' + blockParentUdi + '"]').getByRole('button', {name: 'Copy'}).nth(1).click(); await page.locator('[data-element="property-' + blockGridAlias + '"]').getByRole('button', {name: 'Clipboard'}).click(); await page.locator('[data-element="editor-container"]').locator('umb-block-card', {hasText: elementTitleName}).click(); - + // Checks if the blocks were copied await expect(page.locator('[data-content-element-type-alias="' + elementTitleAlias + '"]')).toHaveCount(2); }); }); -}); \ No newline at end of file +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorContent.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorContent.spec.ts index 9eb4d75e92..8b2f86db74 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorContent.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorContent.spec.ts @@ -27,8 +27,8 @@ test.describe('BlockGridEditorContent', () => { }); test('can create content with a block grid editor', async ({page, umbracoApi, umbracoUi}) => { - await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(umbracoApi, null, null); - + await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(null, null); + const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) .withAction(ConstantHelper.actions.save) @@ -58,11 +58,11 @@ test.describe('BlockGridEditorContent', () => { const newContentValue = 'UpdatedTitle'; const newSettingValue = 'UpdatedSetting'; - const element = await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, null, null, null); + const element = await umbracoApi.content.createDefaultContentWithABlockGridEditor(null, null, null); await umbracoUi.navigateToContent(blockGridName); - // Updates the already created content text + // Updates the already created content text await page.locator('[data-content-element-type-key="' + element['key'] + '"]', {hasText: elementName}).click(); await page.locator('[id="sub-view-0"]').locator('[id="title"]').fill(newContentValue); await umbracoUi.clickDataElementByElementName('sub-view-settings'); @@ -71,7 +71,7 @@ test.describe('BlockGridEditorContent', () => { await page.locator('[label="Submit"]').click(); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)); - // Assert + // Assert await umbracoUi.isSuccessNotificationVisible(); // Checks if the Content and Setting were updated after it was saved await page.locator('[data-content-element-type-key="' + element['key'] + '"]', {hasText: elementName}).click(); @@ -81,13 +81,13 @@ test.describe('BlockGridEditorContent', () => { }); test('can delete a block grid editor in content', async ({page, umbracoApi, umbracoUi}) => { - const element = await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, null, null, null); + const element = await umbracoApi.content.createDefaultContentWithABlockGridEditor(null, null, null); await umbracoUi.navigateToContent(blockGridName); // Deletes the block grid editor inside of the content await page.getByTitle("Delete").click(); - + // Can't use our constant helper because the action for delete does not contain an s. The correct way is 'action-delete' await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey('actions_delete')); @@ -101,7 +101,7 @@ test.describe('BlockGridEditorContent', () => { }); test('can copy block grid content and paste it', async ({page, umbracoApi, umbracoUi}) => { - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, null, null, null); + await umbracoApi.content.createDefaultContentWithABlockGridEditor(null, null, null); await umbracoUi.navigateToContent(blockGridName); // Checks to make sure that there is only one item @@ -134,7 +134,7 @@ test.describe('BlockGridEditorContent', () => { const element = await umbracoApi.documentTypes.createDefaultElementType(elementName, elementAlias); - const dataType = await umbracoApi.dataTypes.createDefaultBlockGrid(umbracoApi, blockGridName, element); + const dataType = await umbracoApi.dataTypes.createDefaultBlockGrid(blockGridName, element); const docType = new DocumentTypeBuilder() .withName(documentName) @@ -157,7 +157,7 @@ test.describe('BlockGridEditorContent', () => { .build(); await umbracoApi.documentTypes.save(docType); - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, true); + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, true); await umbracoUi.navigateToContent(blockGridName); @@ -192,9 +192,9 @@ test.describe('BlockGridEditorContent', () => { const element = await umbracoApi.documentTypes.createDefaultElementType(elementName, elementAlias); // We give the dataType a label so we can differentiate between the top and bottom block in the content editor. - const dataType = await umbracoApi.dataTypes.createDefaultBlockGrid(umbracoApi, blockGridName, element, '{{' + element.groups[0].properties[0].alias + '}}'); + const dataType = await umbracoApi.dataTypes.createDefaultBlockGrid(blockGridName, element, '{{' + element.groups[0].properties[0].alias + '}}'); - await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(umbracoApi, element, dataType); + await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(element, dataType); const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) @@ -226,7 +226,7 @@ test.describe('BlockGridEditorContent', () => { await umbracoApi.content.save(rootContentNode); await umbracoUi.navigateToContent(blockGridName); - + // Drag and Drop const dragFromLocator = await page.locator('[data-content-element-type-key="' + element['key'] + '"]', {hasText: bottomBlock}); const dragToLocator = await page.locator('[data-content-element-type-key="' + element['key'] + '"]', {hasText: topBlock}); @@ -253,7 +253,7 @@ test.describe('BlockGridEditorContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(umbracoApi, element, dataType); + await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(element, dataType); const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) @@ -319,7 +319,7 @@ test.describe('BlockGridEditorContent', () => { await umbracoApi.media.createDefaultImage(imageName); - await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(umbracoApi, element, null); + await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(element, null); const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) @@ -346,7 +346,7 @@ test.describe('BlockGridEditorContent', () => { // Selects the created image for the block await page.locator('[data-content-element-type-key="' + element['key'] + '"]').click(); - await page.locator('[data-element="property-image"]').locator('[key="' + ConstantHelper.buttons.add + '"]').click(); + await page.getByRole('button', { name: 'Add', exact: true }).click(); await page.locator('[data-element="media-grid"] >> [title="' + imageName + '"]').click(); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.select)); await page.locator('[label="Submit"]').click(); @@ -374,7 +374,7 @@ test.describe('BlockGridEditorContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, false); + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, false); await umbracoUi.navigateToContent(blockGridName); // Checks if there is validation for needing 2 entries or more @@ -408,7 +408,7 @@ test.describe('BlockGridEditorContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(umbracoApi, element, dataType); + await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(element, dataType); const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) @@ -470,7 +470,7 @@ test.describe('BlockGridEditorContent', () => { test.describe('Live editing mode', () => { test('can use live editing mode in content with a block grid editor', async ({page, umbracoApi, umbracoUi}) => { const newText = 'LiveUpdatedContent'; - + const element = await umbracoApi.documentTypes.createDefaultElementType(elementName, elementAlias); const dataTypeBlockGrid = new BlockGridDataTypeBuilder() @@ -485,17 +485,17 @@ test.describe('BlockGridEditorContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, false); + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, false); await umbracoUi.navigateToContent(blockGridName); // Checks if the block contains the correct text before being edited await expect(page.locator('[data-content-element-type-alias="'+elementAlias +'"]')).toContainText('aliasTest'); - + // Updates the text without saving the changes. await page.locator('[data-content-element-type-key="' + element['key'] + '"]').click(); await page.locator('[id="sub-view-0"]').locator('[id="title"]').fill(newText); - + // Checks if the block is being live updated as the content is being updated. await expect(page.locator('[data-content-element-type-alias="'+elementAlias +'"]')).toContainText(newText); }); @@ -515,7 +515,7 @@ test.describe('BlockGridEditorContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, false); + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, false); await umbracoUi.navigateToContent(blockGridName); @@ -540,7 +540,7 @@ test.describe('BlockGridEditorContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, false); + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, false); await umbracoUi.navigateToContent(blockGridName); @@ -571,8 +571,8 @@ test.describe('BlockGridEditorContent', () => { .withLayoutStylesheet(stylesheetDataPath) .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, null); + + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, null); await umbracoUi.navigateToContent(blockGridName); @@ -600,7 +600,7 @@ test.describe('BlockGridEditorContent', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, false); + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, false); // Goes to the created Content await umbracoUi.goToSection(ConstantHelper.sections.content); @@ -614,4 +614,4 @@ test.describe('BlockGridEditorContent', () => { await page.locator('[data-element="property-' + blockGridAlias + '"] >> .umb-block-grid__actions', {hasText: newButtonLabel}).click(); }); }); -}); \ No newline at end of file +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorRendering.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorRendering.spec.ts index d95d39ccaa..0d8bdd58ac 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorRendering.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorRendering.spec.ts @@ -9,14 +9,14 @@ test.describe('BlockGridEditorRending', () => { const blockGridName = 'BlockGridTest'; const elementTitleName = 'ElementTitle'; const contentText = "ContentTest"; - + const documentAlias = 'documentTest'; const blockGridAlias = 'blockGridTest'; const elementTitleAlias = "elementTitle"; const elementTitleLabel = 'Title'; const elementTitleLabelAlias = AliasHelper.toAlias(elementTitleLabel); - + test.beforeEach(async ({page, umbracoApi, umbracoUi}, testInfo) => { await umbracoApi.report.report(testInfo); await umbracoApi.login(); @@ -51,7 +51,7 @@ test.describe('BlockGridEditorRending', () => { .build(); return await umbracoApi.documentTypes.save(element); } - + async function createDocumentWithTemplateAndDataType(umbracoApi, dataType){ const docType = new DocumentTypeBuilder() .withName(documentName) @@ -69,14 +69,14 @@ test.describe('BlockGridEditorRending', () => { .build(); return await umbracoApi.documentTypes.save(docType); } - + async function editDefaultTemplate(umbracoApi){ await umbracoApi.templates.edit(documentName, '@using Umbraco.Cms.Web.Common.PublishedModels;\n' + '@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\n' + '@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;' + '\n' + '@await Html.GetBlockGridHtmlAsync(Model.' + blockGridName + ')'); } - + async function createPartialViewWithArea(umbracoApi, elementAlias, elementLabelAlias){ const partialViewElementTitle = new PartialViewBuilder() .withName(elementAlias) @@ -91,7 +91,7 @@ test.describe('BlockGridEditorRending', () => { partialViewElementTitle.virtualPath = "/Views/Partials/blockgrid/Components/"; return await umbracoApi.partialViews.save(partialViewElementTitle); } - + async function createPartialViewWithBlock(umbracoApi,elementAlias, elementLabelAlias){ const partialViewElementBody = new PartialViewBuilder() .withName(elementAlias) @@ -105,11 +105,11 @@ test.describe('BlockGridEditorRending', () => { partialViewElementBody.virtualPath = "/Views/Partials/blockgrid/Components/"; await umbracoApi.partialViews.save(partialViewElementBody); } - + test('can render content with a block grid editor', async ({page, umbracoApi, umbracoUi}) => { const element = await createElementWithRTE(umbracoApi, elementTitleName, elementTitleAlias, elementTitleLabel, elementTitleLabelAlias); - const dataType = await umbracoApi.dataTypes.createDefaultBlockGrid(umbracoApi, blockGridName, element); + const dataType = await umbracoApi.dataTypes.createDefaultBlockGrid(blockGridName, element); await createDocumentWithTemplateAndDataType(umbracoApi, dataType); @@ -140,7 +140,7 @@ test.describe('BlockGridEditorRending', () => { .withTemplateAlias(documentName) .build(); await umbracoApi.content.save(rootContentNode); - + // Assert await page.goto(umbracoConfig.environment.baseUrl); await expect(page).toHaveScreenshot('Block-grid-editor.png'); @@ -158,7 +158,7 @@ test.describe('BlockGridEditorRending', () => { const element = await createElementWithRTE(umbracoApi, elementTitleName, elementTitleAlias, elementTitleLabel, elementTitleLabelAlias); const elementBody = await createElementWithRTE(umbracoApi, elementBodyName, elementBodyAlias, elementBodyLabel, elementBodyLabelAlias); - + const dataTypeBlockGrid = new BlockGridDataTypeBuilder() .withName(blockGridName) .addBlock() @@ -179,7 +179,7 @@ test.describe('BlockGridEditorRending', () => { // Creates partial view for the ElementBody await createPartialViewWithBlock(umbracoApi, elementBodyAlias, elementBodyLabelAlias); - + const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) .withAction(ConstantHelper.actions.publish) @@ -210,12 +210,12 @@ test.describe('BlockGridEditorRending', () => { .withTemplateAlias(documentName) .build(); await umbracoApi.content.save(rootContentNode); - + // Assert await page.goto(umbracoConfig.environment.baseUrl); await expect(page).toHaveScreenshot('Block-grid-editor-with-two-elements.png'); - // Clean + // Clean await umbracoApi.documentTypes.ensureNameNotExists(elementBodyName); await umbracoApi.partialViews.ensureNameNotExists('blockgrid/Components', elementBodyAlias + '.cshtml'); }); @@ -257,7 +257,7 @@ test.describe('BlockGridEditorRending', () => { // Creates partial view for the ElementBody await createPartialViewWithBlock(umbracoApi, elementBodyAlias, elementBodyLabelAlias); - + const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) .withAction(ConstantHelper.actions.publish) @@ -291,12 +291,12 @@ test.describe('BlockGridEditorRending', () => { .withTemplateAlias(documentName) .build(); await umbracoApi.content.save(rootContentNode); - + // Assert await page.goto(umbracoConfig.environment.baseUrl); await expect(page).toHaveScreenshot('Block-grid-editor-with-area.png'); - - // Clean + + // Clean await umbracoApi.documentTypes.ensureNameNotExists(elementBodyName); await umbracoApi.partialViews.ensureNameNotExists('blockgrid/Components', elementBodyAlias + '.cshtml'); }) @@ -392,7 +392,7 @@ test.describe('BlockGridEditorRending', () => { .withTemplateAlias(documentName) .build(); await umbracoApi.content.save(rootContentNode); - + // Assert await page.goto(umbracoConfig.environment.baseUrl); await expect(page).toHaveScreenshot('Block-grid-editor-with-multiple-areas.png'); @@ -433,7 +433,7 @@ test.describe('BlockGridEditorRending', () => { .done() .done() .addBlock() - .withContentElementTypeKey(elementBody['key']) + .withContentElementTypeKey(elementBody['key']) .addArea() .withAlias('bodyArea') .done() @@ -551,7 +551,7 @@ test.describe('BlockGridEditorRending', () => { .build(); partialViewElement.virtualPath = "/Views/Partials/blockgrid/Components/"; await umbracoApi.partialViews.save(partialViewElement); - + const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) .withAction(ConstantHelper.actions.publish) @@ -595,7 +595,7 @@ test.describe('BlockGridEditorRending', () => { // ElementBody const elementBodyName = "ElementBody"; const elementBodyAlias = AliasHelper.toAlias(elementBodyName); - + await umbracoApi.documentTypes.ensureNameNotExists(elementBodyName); await umbracoApi.media.ensureNameNotExists(stylesheetTitleName); await umbracoApi.media.ensureNameNotExists(stylesheetBodyName); @@ -695,7 +695,7 @@ test.describe('BlockGridEditorRending', () => { // Assert await page.goto(umbracoConfig.environment.baseUrl); await expect(page).toHaveScreenshot('Block-grid-editor-with-two-custom-stylesheets.png'); - + // Clean await umbracoApi.documentTypes.ensureNameNotExists(elementBodyName); await umbracoApi.media.ensureNameNotExists(stylesheetTitleName); @@ -703,7 +703,7 @@ test.describe('BlockGridEditorRending', () => { await umbracoApi.partialViews.ensureNameNotExists('blockgrid/Components', elementBodyAlias + '.cshtml'); }); }); - + test.describe('Image', () => { test('can render a block grid with an image', async ({page, umbracoApi, umbracoUi}) => { // Image @@ -714,9 +714,9 @@ test.describe('BlockGridEditorRending', () => { const imageMimeType = "image/png"; await umbracoApi.media.ensureNameNotExists(imageName); - + const imageData = await umbracoApi.media.createImageWithFile(imageName, umbracoFileValue, imageFileName, imagePath, imageMimeType); - + const element = new DocumentTypeBuilder() .withName(elementTitleName) .withAlias(elementTitleAlias) @@ -756,7 +756,7 @@ test.describe('BlockGridEditorRending', () => { .build(); partialViewImage.virtualPath = "/Views/Partials/blockgrid/Components/"; await umbracoApi.partialViews.save(partialViewImage); - + const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) .withAction(ConstantHelper.actions.publish) @@ -771,7 +771,7 @@ test.describe('BlockGridEditorRending', () => { .addImage() .withMediaKey(imageData.key) .done() - .done() + .done() .addLayout() .withContentUdi(element['key']) .done() @@ -780,12 +780,12 @@ test.describe('BlockGridEditorRending', () => { .done() .build(); await umbracoApi.content.save(rootContentNode); - + // Assert await page.goto(umbracoConfig.environment.baseUrl); await expect(page).toHaveScreenshot('Block-grid-editor-with-image.png'); - - // Clean + + // Clean await umbracoApi.media.ensureNameNotExists(imageName); }); }); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorSettings.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorSettings.spec.ts index 358a979a43..5f6fd950e1 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorSettings.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorSettings.spec.ts @@ -29,7 +29,7 @@ test.describe('BlockGridEditorSettings', () => { test.describe('General', () => { test('can see label in content for a block grid editor', async ({page, umbracoApi, umbracoUi}) => { const newLabel = "New Label"; - + const element = await umbracoApi.documentTypes.createDefaultElementType(elementName, elementAlias); const dataTypeBlockGrid = new BlockGridDataTypeBuilder() @@ -41,14 +41,14 @@ test.describe('BlockGridEditorSettings', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, null); + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, false); await umbracoUi.navigateToContent(blockGridName); - + // Assert // Checks if the element contains the correct label await expect(page.locator('[data-content-element-type-alias="' + elementAlias + '"]')).toContainText(newLabel); - // Checks if the element is clickable + // Checks if the element is clickable await page.locator('[data-content-element-type-alias="' + elementAlias + '"]').click(); }); }); @@ -65,9 +65,9 @@ test.describe('BlockGridEditorSettings', () => { .done() .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - - await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(umbracoApi, element, dataType); - + + await umbracoApi.documentTypes.createDefaultDocumentWithBlockGridEditor(element, dataType); + const rootContentNode = new ContentBuilder() .withContentTypeAlias(documentAlias) .withAction(ConstantHelper.actions.save) @@ -77,21 +77,21 @@ test.describe('BlockGridEditorSettings', () => { .done() .build(); await umbracoApi.content.save(rootContentNode); - + await umbracoUi.navigateToContent(blockGridName); - + // Checks if adding a block is disabled await expect(page.locator('[data-element="property-'+blockGridAlias+'"]').locator('umb-block-grid-root').locator('[disabled="disabled"]')).toBeDisabled(); // Checks if the button is not clickable await expect(page.locator('[data-element="property-' + blockGridAlias + '"]').locator('[key="blockEditor_addBlock"]')).not.toBeEnabled(); }); - + test('can set allow in areas to false for an element in a block grid editor', async ({page, umbracoApi, umbracoUi}) => { const elementBodyName = 'BodyElement'; const elementBodyAlias = AliasHelper.toAlias(elementBodyName); - + await umbracoApi.documentTypes.ensureNameNotExists(elementBodyName); - + const element = await umbracoApi.documentTypes.createDefaultElementType(elementName, elementAlias); const elementBody = await umbracoApi.documentTypes.createDefaultElementType(elementBodyName, elementBodyAlias); @@ -110,14 +110,14 @@ test.describe('BlockGridEditorSettings', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi,element,dataType,false); + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element,dataType, false); await umbracoUi.navigateToContent(blockGridName); - + // Assert // Checks if the elementTitle is the only element selectable to be in a area await expect(page.locator('[data-area-alias="titleArea"]').locator('[key="blockEditor_addThis"]')).toContainText('Add ' + elementName); - + // Clean await umbracoApi.documentTypes.ensureNameNotExists(elementBodyName); }); @@ -136,20 +136,20 @@ test.describe('BlockGridEditorSettings', () => { .build(); const dataType = await umbracoApi.dataTypes.save(dataTypeBlockGrid); - await umbracoApi.content.createDefaultContentWithABlockGridEditor(umbracoApi, element, dataType, null); + await umbracoApi.content.createDefaultContentWithABlockGridEditor(element, dataType, false); await umbracoUi.navigateToContent(blockGridName); - + // Drags the blocks from a columnSpan of 12 to 6 const dragFrom = await page.locator('[data-content-element-type-key="' + element['key'] + '"]', {hasText: elementName}).locator('[title="Drag to scale"]'); const dragTo = await page.locator('[data-content-element-type-key="' + element['key'] + '"]', {hasText: elementName}); await umbracoUi.dragAndDrop(dragFrom,dragTo, 0, 0 ,10); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)); - // Assert + // Assert await umbracoUi.isSuccessNotificationVisible(); // Checks if the block is resized to a column span of 6 await expect(page.locator('[data-content-element-type-key="' + element['key'] + '"], [data-col-span="6"]', {hasText: elementName})).toBeVisible(); }); }); -}); \ No newline at end of file +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Datatype/BlockGridEditorDataTypeBlocks.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Datatype/BlockGridEditorDataTypeBlocks.spec.ts index 2acd818661..c88fbd43e5 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Datatype/BlockGridEditorDataTypeBlocks.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Datatype/BlockGridEditorDataTypeBlocks.spec.ts @@ -43,7 +43,7 @@ test.describe('BlockGridEditorDataTypeBlock', () => { return blockGridType; } - + test('can create empty block grid editor', async ({page, umbracoApi, umbracoUi}) => { await umbracoUi.goToSection(ConstantHelper.sections.settings); @@ -261,7 +261,7 @@ test.describe('BlockGridEditorDataTypeBlock', () => { test('can create a block grid datatype with multiple groups and multiple element in each group', async ({page, umbracoApi, umbracoUi},testInfo) => { await testInfo.slow(); - + const GroupOne = 'GroupOne'; const elementNameFourth = 'FourthElement'; const elementFourthAlias = AliasHelper.toAlias(elementNameFourth); @@ -362,7 +362,7 @@ test.describe('BlockGridEditorDataTypeBlock', () => { await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true); await umbracoUi.doesDataTypeExist(blockGridName); - // Clean + // Clean await umbracoApi.documentTypes.ensureNameNotExists(elementName); await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo); await umbracoApi.documentTypes.ensureNameNotExists(elementNameThree); @@ -552,6 +552,9 @@ test.describe('BlockGridEditorDataTypeBlock', () => { await umbracoUi.clickDataElementByElementName(ConstantHelper.actions.delete); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.delete)); + // We need a wait to make sure the block grid editor is deleted + await page.waitForTimeout(1000); + // Assert // Checks if the block grid editor still exists await umbracoUi.goToSection(ConstantHelper.sections.settings); @@ -586,7 +589,9 @@ test.describe('BlockGridEditorDataTypeBlock', () => { const dragFrom = await page.locator('.umb-block-card-group').nth(0).locator('[data-content-element-type-key="' + element['key'] + '"]'); const dragTo = await page.locator('[key="blockEditor_addBlockType"]').nth(1); await umbracoUi.dragAndDrop(dragFrom, dragTo, 0, 0, 15); - + + // We need a wait to make sure the element is moved + await page.waitForTimeout(2000); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); // Assert @@ -626,7 +631,7 @@ test.describe('BlockGridEditorDataTypeBlock', () => { const dragFrom = await page.locator('.umb-block-card-group >> [icon="icon-navigation"]').nth(0); const dragTo = await page.locator('[key="blockEditor_addBlockType"]').nth(2); await umbracoUi.dragAndDrop(dragFrom, dragTo, 0, 0, 15); - + await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); // Assert @@ -642,7 +647,7 @@ test.describe('BlockGridEditorDataTypeBlock', () => { test('can move a group with elements in a block grid editor', async ({page, umbracoApi, umbracoUi}, testInfo) => { await testInfo.slow(); - + const GroupMove = 'GroupMove'; const GroupNotMoving = 'GroupNotMoving'; @@ -685,9 +690,9 @@ test.describe('BlockGridEditorDataTypeBlock', () => { const dragFrom = await page.locator('.umb-block-card-group >> [icon="icon-navigation"]').nth(0); const dragTo = await page.locator('[key="blockEditor_addBlockType"]').nth(2); await umbracoUi.dragAndDrop(dragFrom, dragTo, 20, 0, 15); - + await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); - + // Assert await umbracoUi.isSuccessNotificationVisible(); // Checks if the elements were moved with their group @@ -716,4 +721,4 @@ test.describe('BlockGridEditorDataTypeBlock', () => { await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo); await umbracoApi.documentTypes.ensureNameNotExists(elementNameThree); }); -}); \ No newline at end of file +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockListEditor/blockListEditorContent.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockListEditor/blockListEditorContent.spec.ts index 0c99d46ab5..ca67427968 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockListEditor/blockListEditorContent.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockListEditor/blockListEditorContent.spec.ts @@ -312,7 +312,7 @@ test.describe('BlockListEditorContent', () => { await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)); // Assert - await umbracoUi.getSuccessNotification(); + await umbracoUi.isSuccessNotificationVisible(); }); test('can set a maximum of required blocks in content with a block list editor', async ({page, umbracoApi, umbracoUi}) => { @@ -374,8 +374,11 @@ test.describe('BlockListEditorContent', () => { // Can't use our constant helper because the action for delete does not contain an s. The correct way is 'action-delete' await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey('actions_delete')); + // Now that we deleted the block, we should be able to save our content + await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)); + // Assert - await umbracoUi.getSuccessNotification(); + await umbracoUi.isSuccessNotificationVisible(); }); test('can use inline editing mode in content with a block list editor', async ({page, umbracoApi, umbracoUi}) => { diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockListEditor/blockListEditorDocument.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockListEditor/blockListEditorDocument.spec.ts index 55588a2848..51e7e98934 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockListEditor/blockListEditorDocument.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockListEditor/blockListEditorDocument.spec.ts @@ -17,12 +17,12 @@ test.describe('BlockListEditorDocument', () => { await umbracoApi.documentTypes.ensureNameNotExists(documentName); await umbracoApi.dataTypes.ensureNameNotExists(blockListName); }); - + test.afterEach(async ({page, umbracoApi, umbracoUi}) => { await umbracoApi.documentTypes.ensureNameNotExists(documentName); await umbracoApi.dataTypes.ensureNameNotExists(blockListName); }); - + test('can create empty block list in a document', async ({page, umbracoApi, umbracoUi}) => { const groupName = 'blockListGroup'; @@ -38,8 +38,7 @@ test.describe('BlockListEditorDocument', () => { await umbracoUi.goToAddEditor(groupName, blockListName); // Waits until the selector is visible await expect(page.locator('[data-element="datatype-Block List"]')).toBeVisible(); - await umbracoUi.clickDataElementByElementName('datatype-Block List'); - + await page.getByRole('button', { name: 'Block List' }).click(); // Creates new BlockList editor await page.locator('[title="Create a new configuration of Block List"]').click(); await page.locator('[id="dataTypeName"]').fill(blockListName); @@ -49,14 +48,14 @@ test.describe('BlockListEditorDocument', () => { // Checks to ensure that the button is visible await expect(page.locator('[name="propertySettingsForm"]').locator('[label-key=' + ConstantHelper.buttons.submit + ']')).toBeVisible(); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.submit)); - + await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); // Assert await expect(page.locator('.umb-notifications__notifications > .alert-success', {hasText: "Datatype saved"})).toBeVisible(); await expect(page.locator('.umb-notifications__notifications > .alert-success', {hasText: "Document Type saved"})).toBeVisible(); }); - + test('can create multiple block list editors in a document', async ({page, umbracoApi, umbracoUi}) => { const elementNameSecond = 'TestElementTwo'; const blockListNameSecond = 'BlockListTestNumbaTwo'; @@ -127,7 +126,7 @@ test.describe('BlockListEditorDocument', () => { await umbracoApi.documentTypes.save(rootDocType); await umbracoUi.navigateToDocumentType(documentName); - + // Adds another block list editor to the first group await page.locator('[data-element="group-TestName"]').locator('[data-element="property-add"]').click(); await page.locator('[data-element="property-name"]').fill('TheBlock'); @@ -136,7 +135,7 @@ test.describe('BlockListEditorDocument', () => { await page.locator('[title="Select ' + blockListNameSecond + '"]').click(); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.submit)); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); - + // Assert await expect(page.locator('.umb-notifications__notifications > .alert-success', {hasText: "Document Type saved"})).toBeVisible(); // Checks if the new block list is in the group @@ -147,4 +146,4 @@ test.describe('BlockListEditorDocument', () => { await umbracoApi.documentTypes.ensureNameNotExists(elementNameSecond); await umbracoApi.dataTypes.ensureNameNotExists(blockListNameSecond); }); -}); \ No newline at end of file +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/content.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/content.spec.ts index e402f751e6..4793d98614 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/content.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/content.spec.ts @@ -13,7 +13,7 @@ test.describe('Content tests', () => { test.beforeEach(async ({page, umbracoApi}) => { await umbracoApi.login(); }); - + async function createSimpleMacro(name, umbracoApi: ApiHelpers){ const insertMacro = new PartialViewMacroBuilder() .withName(name) @@ -280,7 +280,7 @@ test.describe('Content tests', () => { await umbracoUi.clickElement(umbracoUi.getTreeItem("content", [initialNodeName])); const header = await page.locator('#headerName') - // Sadly playwright doesn't have a clear method for inputs :( + // Sadly playwright doesn't have a clear method for inputs :( // so we have to triple click to select all, and then hit backspace... await header.click({ clickCount: 3 }) await page.keyboard.press('Backspace'); @@ -388,7 +388,7 @@ test.describe('Content tests', () => { // Clean up (content is automatically deleted when document types are gone) await umbracoApi.documentTypes.ensureNameNotExists(rootDocTypeName); - }); + }); test('Preview draft', async ({ page, umbracoApi, umbracoUi }) => { const rootDocTypeName = "Test document type"; @@ -428,7 +428,7 @@ test.describe('Content tests', () => { // Clean up (content is automatically deleted when document types are gone) await umbracoApi.documentTypes.ensureNameNotExists(rootDocTypeName); - }); + }); test('Publish draft', async ({ page, umbracoApi, umbracoUi }) => { const rootDocTypeName = "Test document type"; @@ -554,8 +554,8 @@ test.describe('Content tests', () => { // Save and publish await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)); // Added additional time because it could fail on pipeline because it's not saving fast enough - await umbracoUi.isSuccessNotificationVisible({timeout:20000}); - + await umbracoUi.isSuccessNotificationVisible(); + // Assert const expectedContent = '

Acceptance test

' await expect(await umbracoApi.content.verifyRenderedContent('/contentpickercontent', expectedContent, true)).toBeTruthy(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/routing.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/routing.spec.ts index 14cfd99185..b2c6893226 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/routing.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/routing.spec.ts @@ -105,7 +105,7 @@ test.describe('Routing', () => { await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)); // Pop-up with what cultures you want to publish shows, click it - await page.locator('.btn-success').last().click() + await umbracoUi.clickDataElementByElementName('button-overlaySubmit'); // Assert await umbracoUi.isSuccessNotificationVisible(); @@ -161,13 +161,13 @@ test.describe('Routing', () => { await expect(await page.locator('.umb-list')).toBeVisible(); await page.locator('.checkbox').last().click(); // Pop-up with what cultures you want to publish shows, click it - await page.locator('.btn-success').last().click() + await umbracoUi.clickDataElementByElementName('button-overlaySubmit'); // Assert - await expect(await umbracoUi.getSuccessNotification()).toHaveCount(2); + await expect(await umbracoUi.getSuccessNotification()).toHaveCount(2, {timeout: 20000}); await expect(await page.locator('.alert-warning')).toBeVisible(); }); - + test('Root node published in language A, Child node published in language A + B, Grandchild published in A + B', async ({page, umbracoApi, umbracoUi}) => { const rootDocType = new DocumentTypeBuilder() .withName(rootDocTypeName) @@ -239,7 +239,7 @@ test.describe('Routing', () => { await expect(await page.locator('.umb-list')).toBeVisible(); await page.locator('.checkbox').last().click(); - await page.locator('.btn-success').last().click() + await umbracoUi.clickDataElementByElementName('button-overlaySubmit'); await umbracoUi.clickMultiple(page.locator('.alert-success > .close')); await umbracoUi.clickElement(umbracoUi.getTreeItem("content", [nodeName, childNodeName, grandChildNodeName])); @@ -247,8 +247,8 @@ test.describe('Routing', () => { await expect(await page.locator('.umb-list')).toBeVisible(); await page.locator('.checkbox').last().click(); - await page.locator('.btn-success').last().click() + await umbracoUi.clickDataElementByElementName('button-overlaySubmit'); // Assert - await expect(await umbracoUi.getSuccessNotification()).toHaveCount(2); + await expect(await umbracoUi.getSuccessNotification()).toHaveCount(2, {timeout: 20000}); }) }); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataTypes/dataTypes.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataTypes/dataTypes.spec.ts index 808858ab01..cada9e7503 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataTypes/dataTypes.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataTypes/dataTypes.spec.ts @@ -103,7 +103,7 @@ test.describe('DataTypes', () => { // Add char and assert helptext appears - no publish to save time & has been asserted above & below await page.locator('input[name="textbox"]').type('9'); await expect(page.locator('localize[key="textbox_characters_left"]', {hasText: "characters left"}).first()).toBeVisible(); - await expect(await umbracoUi.getErrorNotification()).not.toBeVisible(); + await expect(await umbracoUi.getErrorNotification()).not.toBeVisible({timeout: 20000}); // Add char and assert errortext appears and can't save await page.locator('input[name="textbox"]').type('10'); // 1 char over max @@ -119,7 +119,6 @@ test.describe('DataTypes', () => { }); test('Test Url Picker', async ({page, umbracoApi, umbracoUi}) => { - const urlPickerDocTypeName = 'Url Picker Test'; const pickerDocTypeAlias = AliasHelper.toAlias(urlPickerDocTypeName); @@ -162,17 +161,17 @@ test.describe('DataTypes', () => { await page.locator('span:has-text("×")').click(); await page.locator('.umb-node-preview-add').click(); - // Should really try and find a better way to do this, but umbracoTreeItem tries to click the content pane in the background - await page.locator('#treePicker >> [data-element="tree-item-UrlPickerContent"]').click(); - await page.locator('.umb-editor-footer-content__right-side > [button-style="success"] > .umb-button > .btn > .umb-button__content').click(); - await expect(await page.locator('.umb-node-preview__name').first()).toBeVisible(); + await page.locator('[data-element="editor-container"]').locator('[data-element="tree-item-UrlPickerContent"]').click(); + await expect(page.locator('[alias="urlLinkPicker"]').locator('input[id="urlLinkPicker"]')).toHaveValue('/'); + await page.locator('.umb-editor-footer-content__right-side').locator('[label-key="' + ConstantHelper.buttons.submit + '"]').click(); + await expect(page.locator('.umb-node-preview__name').first()).toBeVisible(); // Save and publish await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)); await umbracoUi.isSuccessNotificationVisible(); // Assert - await expect(await umbracoUi.getErrorNotification()).not.toBeVisible(); + await expect(await umbracoUi.getErrorNotification()).not.toBeVisible({timeout: 20000}); // Testing if the edits match the expected results const expected = 'UrlPickerContent'; @@ -184,4 +183,4 @@ test.describe('DataTypes', () => { await umbracoApi.content.deleteAllContent(); await umbracoApi.templates.ensureNameNotExists(urlPickerDocTypeName); }); -}); \ No newline at end of file +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataTypes/textBoxVariation.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataTypes/textBoxVariation.spec.ts index 1cd66d12d2..d637b736d7 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataTypes/textBoxVariation.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataTypes/textBoxVariation.spec.ts @@ -7,7 +7,7 @@ test.describe('Vary by culture for TextBox', () => { test.beforeEach(async ({page, umbracoApi, umbracoUi}) => { await umbracoApi.login(); }); - + test('create documentType with vary by culture with UI with a textbox property which also has vary by culture', async ({page, umbracoApi, umbracoUi}) => { const documentTypeName = 'Test Document'; const textBoxPropertyName = 'TestBox'; @@ -36,7 +36,7 @@ test.describe('Vary by culture for TextBox', () => { await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); // Assert - await expect(page.locator('.umb-notifications__notifications > .alert-success')).toBeVisible(); + await umbracoUi.isSuccessNotificationVisible(); // Clean await umbracoApi.documentTypes.ensureNameNotExists(documentTypeName); @@ -279,7 +279,7 @@ test.describe('Vary by culture for TextBox', () => { await expect(await umbracoApi.content.verifyRenderedContent(daEndpoint, daValue, true)).toBeTruthy(); await expect(await umbracoApi.content.verifyRenderedContent(enEndpoint, enValue, true)).toBeTruthy(); - // Clean + // Clean await umbracoApi.content.deleteAllContent(); await umbracoApi.documentTypes.ensureNameNotExists(documentName); await umbracoApi.languages.ensureCultureNotExists(languageDa); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/mediaSection.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/mediaSection.spec.ts index 19c69d6b9d..60891d4b47 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/mediaSection.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/mediaSection.spec.ts @@ -51,7 +51,7 @@ test.describe('Media', () => { } }); await page.locator('[label-key="actions_move"]').click(); - await page.locator('[data-element="editor-container"] >> "' + folderToMoveTooName + '"').click(); + await page.locator('[data-element="editor-container"]').locator('[data-element="tree-item-MoveHere"]').click(); await page.locator('[label-key="general_submit"]').click(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/ModelsBuilder/modelsbuilder.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/ModelsBuilder/modelsbuilder.spec.ts index 1ac6efc02e..c919912c55 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/ModelsBuilder/modelsbuilder.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/ModelsBuilder/modelsbuilder.spec.ts @@ -32,7 +32,7 @@ test.describe('Modelsbuilder tests', () => { .done() .build(); await umbracoApi.documentTypes.save(docType); - + await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels; @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @using ContentModels = Umbraco.Cms.Web.Common.PublishedModels; @@ -48,7 +48,7 @@ test.describe('Modelsbuilder tests', () => { // Fortunately for us the input field of a text box has the alias of the property as an id :) await page.locator("#title").type("Hello world!"); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)); - await umbracoUi.isSuccessNotificationVisible({timeout:10000}); + await umbracoUi.isSuccessNotificationVisible(); // Ensure that we can render it on the frontend = we can compile the models and views await umbracoApi.content.verifyRenderedContent("/", "

Hello world!

", true); @@ -80,7 +80,7 @@ test.describe('Modelsbuilder tests', () => { .done() .build(); const savedDocType = await umbracoApi.documentTypes.save(docType); - + await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels; @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @using ContentModels = Umbraco.Cms.Web.Common.PublishedModels; @@ -116,7 +116,7 @@ test.describe('Modelsbuilder tests', () => { await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.submit)); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); // Has a long timeout because it can sometimes take longer than 5 sec to save on the pipeline - await umbracoUi.isSuccessNotificationVisible({timeout:10000}); + await umbracoUi.isSuccessNotificationVisible(); // Now that the content is updated and the models are rebuilt, ensure that we can still render the frontend. await umbracoApi.content.verifyRenderedContent("/", "

" + propertyValue + "

", true) @@ -149,7 +149,7 @@ test.describe('Modelsbuilder tests', () => { .done() .build(); const savedDocType = await umbracoApi.documentTypes.save(docType); - + await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels; @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @using ContentModels = Umbraco.Cms.Web.Common.PublishedModels; @@ -182,8 +182,8 @@ test.describe('Modelsbuilder tests', () => { // We only have to type out the opening tag, the editor adds the closing tag automatically. await editor.type("

Edited"); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); - - await umbracoUi.isSuccessNotificationVisible({timeout:10000}); + + await umbracoUi.isSuccessNotificationVisible(); await umbracoApi.content.verifyRenderedContent("/", "

" + propertyValue + "

Edited

", true); @@ -194,7 +194,7 @@ test.describe('Modelsbuilder tests', () => { test('Can update view and document type', async ({page, umbracoApi, umbracoUi}, testInfo) => { await testInfo.slow(); - + const docTypeName = "TestDocument"; const docTypeAlias = AliasHelper.toAlias(docTypeName); const propertyAlias = "title"; @@ -218,7 +218,7 @@ test.describe('Modelsbuilder tests', () => { .done() .build(); const savedDocType = await umbracoApi.documentTypes.save(docType); - + await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels; @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @using ContentModels = Umbraco.Cms.Web.Common.PublishedModels; @@ -264,7 +264,7 @@ test.describe('Modelsbuilder tests', () => { // We only have to type out the opening tag, the editor adds the closing tag automatically. await editor.type("

@Model.Bod"); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); - await umbracoUi.isSuccessNotificationVisible({timeout: 10000}); + await umbracoUi.isSuccessNotificationVisible(); await page.locator('span:has-text("×")').click(); // Navigate to the content section and update the content @@ -274,6 +274,8 @@ test.describe('Modelsbuilder tests', () => { await page.locator("#bod").type("Fancy body text"); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)); + await page.waitForTimeout(2000); + await umbracoApi.content.verifyRenderedContent("/", "

" + propertyValue + "

Fancy body text

", true); await umbracoApi.content.deleteAllContent(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/partialViewMacroFiles.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/partialViewMacroFiles.spec.ts index 94903f0a54..bcefbb60c5 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/partialViewMacroFiles.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/partialViewMacroFiles.spec.ts @@ -30,18 +30,18 @@ test.describe('Partial View Macro Files', () => { await page.locator('.menu-label localize[key="create_newPartialViewMacro"]').click(); - //Type name + // Type name await umbracoUi.setEditorHeaderName(name); - //Save + // Save await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); //Assert - await umbracoUi.isSuccessNotificationVisible({timeout:10000}); + await umbracoUi.isSuccessNotificationVisible(); //Clean up await cleanup(umbracoApi, name); - }); + }); test('Create new partial view macro without macro', async ({page, umbracoApi, umbracoUi}) => { const name = "TestPartialMacrolessMacro"; @@ -54,16 +54,16 @@ test.describe('Partial View Macro Files', () => { // Type name await umbracoUi.setEditorHeaderName(name); - + // Save await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); // Assert - await umbracoUi.isSuccessNotificationVisible({timeout:10000}); + await umbracoUi.isSuccessNotificationVisible(); // Clean await cleanup(umbracoApi, name); - }); + }); test('Create new partial view macro from snippet', async ({page, umbracoApi, umbracoUi}) => { const name = "TestPartialFromSnippet"; @@ -84,11 +84,11 @@ test.describe('Partial View Macro Files', () => { await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); // Assert - await umbracoUi.isSuccessNotificationVisible({timeout:10000}); + await umbracoUi.isSuccessNotificationVisible(); // Clean await cleanup(umbracoApi, name); - }); + }); test('Delete partial view macro', async ({page, umbracoApi, umbracoUi}) => { const name = "TestDeletePartialViewMacro"; @@ -117,7 +117,7 @@ test.describe('Partial View Macro Files', () => { // Clean await cleanup(umbracoApi, name); - }); + }); test('Edit partial view macro', async ({page, umbracoApi, umbracoUi}) => { const name = "TestPartialViewMacroEditable"; @@ -139,12 +139,12 @@ test.describe('Partial View Macro Files', () => { // Type an edit await page.locator('.ace_text-input').type(" // test" ); - + // Save await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); // Assert - await umbracoUi.isSuccessNotificationVisible({timeout:10000}); + await umbracoUi.isSuccessNotificationVisible(); await cleanup(umbracoApi, name); }); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/partialViews.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/partialViews.spec.ts index 64be19cd82..13ebb4c97d 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/partialViews.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/partialViews.spec.ts @@ -37,7 +37,7 @@ test.describe('Partial Views', () => { await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); //Assert - await umbracoUi.isSuccessNotificationVisible({timeout: 30000}); + await umbracoUi.isSuccessNotificationVisible(); //Clean up await umbracoApi.partialViews.ensureNameNotExists('', fileName); @@ -63,7 +63,7 @@ test.describe('Partial Views', () => { await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); // Assert - await umbracoUi.isSuccessNotificationVisible({timeout:20000}); + await umbracoUi.isSuccessNotificationVisible(); // Clean up await umbracoApi.partialViews.ensureNameNotExists('', fileName); @@ -135,9 +135,9 @@ test.describe('Partial Views', () => { await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); // Assert - await umbracoUi.isSuccessNotificationVisible({timeout:20000}); + await umbracoUi.isSuccessNotificationVisible(); // Clean await umbracoApi.partialViews.ensureNameNotExists('', fileName); }); -}); \ No newline at end of file +}); From ccd54bfb0e4b059dd2be6a8b4f6538a0b41280e7 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 25 Sep 2023 13:45:28 +0200 Subject: [PATCH 2/7] Fix property variance cache issue (#14824) * Property variance should be the union of variance between content type and property type * Fix a few comments in the tests * Review changes --- .../Property.cs | 4 +- .../Published/PropertyCacheVarianceTests.cs | 381 ++++++++++++++++++ 2 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PropertyCacheVarianceTests.cs diff --git a/src/Umbraco.PublishedCache.NuCache/Property.cs b/src/Umbraco.PublishedCache.NuCache/Property.cs index e86f6f4ad9..d921eb3f7c 100644 --- a/src/Umbraco.PublishedCache.NuCache/Property.cs +++ b/src/Umbraco.PublishedCache.NuCache/Property.cs @@ -86,7 +86,9 @@ internal class Property : PublishedPropertyBase _isPreviewing = content.IsPreviewing; _isMember = content.ContentType.ItemType == PublishedItemType.Member; _publishedSnapshotAccessor = publishedSnapshotAccessor; - _variations = propertyType.Variations; + // this variable is used for contextualizing the variation level when calculating property values. + // it must be set to the union of variance (the combination of content type and property type variance). + _variations = propertyType.Variations | content.ContentType.Variations; } // clone for previewing as draft a published content that is published and has no draft diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PropertyCacheVarianceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PropertyCacheVarianceTests.cs new file mode 100644 index 0000000000..72a68b443c --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/PropertyCacheVarianceTests.cs @@ -0,0 +1,381 @@ +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Infrastructure.PublishedCache; +using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; +using Property = Umbraco.Cms.Infrastructure.PublishedCache.Property; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Published; + +[TestFixture] +public class PropertyCacheVarianceTests +{ + // This class tests various permutations of property value calculation across variance types and cache levels. + // + // Properties contain different "value levels", all of which are cached: + // 1. The source value => the "raw" value from the client side editor (it can be different, but it's easiest to think of it like that). + // 2. The intermediate value => a "temporary" value that is used to calculate the various "final" values. + // 3. The object value => the "final" object value that is exposed in an IPublishedElement output. + // 4. The XPath value => a legacy "final" value, don't think too hard on it. + // 3. The delivery API object value => the "final" object value that is exposed in the Delivery API. + // + // Property values are cached based on a few rules: + // 1. The property type variation and the parent content type variation determines how the intermediate value is cached. + // The effective property variation is a product of both variations, meaning the property type and the content type + // variations are combined in an OR. + // The rules are as follows: + // - ContentVariation.Nothing => the intermediate value is calculated once and reused across all variants (cultures and segments). + // - ContentVariation.Culture => the intermediate value is calculated per culture and reused across all segments. + // - ContentVariation.Segment => the intermediate value is calculated per segment and reused across all cultures. + // - ContentVariation.CultureAndSegment => the intermediate value is calculated for all invoked culture and segment combinations. + // 2. The property type cache level (which is usually derived from the property value converter). + // - PropertyCacheLevel.Element => the final values are cached until the parent content item is updated. + // - PropertyCacheLevel.Elements => the final values are cached until the _any_ content item is updated. + // - PropertyCacheLevel.Snapshot => the final values are cached for the duration of the active cache snapshot (i.e. until the end of the current request). + // - PropertyCacheLevel.None => the final values are never cached and will be re-calculated each time they're requested. + + // ### Invariant content type + invariant property type ### + [TestCase( + ContentVariation.Nothing, + ContentVariation.Nothing, + PropertyCacheLevel.Element, + // no variation => the intermediate value is calculated only once + // cache level => the final value is calculated only once + "da-DK:segment1 (da-DK:segment1)", + "da-DK:segment1 (da-DK:segment1)", + "da-DK:segment1 (da-DK:segment1)", + "da-DK:segment1 (da-DK:segment1)")] + [TestCase( + ContentVariation.Nothing, + ContentVariation.Nothing, + PropertyCacheLevel.Elements, + "da-DK:segment1 (da-DK:segment1)", + "da-DK:segment1 (da-DK:segment1)", + "da-DK:segment1 (da-DK:segment1)", + "da-DK:segment1 (da-DK:segment1)")] + [TestCase( + ContentVariation.Nothing, + ContentVariation.Nothing, + PropertyCacheLevel.Snapshot, + "da-DK:segment1 (da-DK:segment1)", + "da-DK:segment1 (da-DK:segment1)", + "da-DK:segment1 (da-DK:segment1)", + "da-DK:segment1 (da-DK:segment1)")] + [TestCase( + ContentVariation.Nothing, + ContentVariation.Nothing, + PropertyCacheLevel.None, + // no variation => the intermediate value is calculated once + // no cache => the final value is calculated for each request (reflects both changes in culture and segments) + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment1 (da-DK:segment1)", + "en-US:segment2 (da-DK:segment1)", + "da-DK:segment2 (da-DK:segment1)")] + // ### Culture variant content type + invariant property type ### + [TestCase( + ContentVariation.Culture, + ContentVariation.Nothing, + PropertyCacheLevel.Element, + // culture variation => the intermediate value is calculated per culture (ignores segment changes until a culture changes) + // cache level => the final value is calculated only once per culture (ignores segment changes until a culture changes) + // NOTE: in this test, culture changes before segment, so the updated segment is never reflected here + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment1 (en-US:segment1)", + "en-US:segment1 (en-US:segment1)", + "da-DK:segment1 (da-DK:segment1)")] + [TestCase( + ContentVariation.Culture, + ContentVariation.Nothing, + PropertyCacheLevel.Elements, + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment1 (en-US:segment1)", + "en-US:segment1 (en-US:segment1)", + "da-DK:segment1 (da-DK:segment1)")] + [TestCase( + ContentVariation.Culture, + ContentVariation.Nothing, + PropertyCacheLevel.Snapshot, + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment1 (en-US:segment1)", + "en-US:segment1 (en-US:segment1)", + "da-DK:segment1 (da-DK:segment1)")] + [TestCase( + ContentVariation.Culture, + ContentVariation.Nothing, + PropertyCacheLevel.None, + // culture variation => the intermediate value is calculated per culture (ignores segment changes until a culture changes) + // no cache => the final value is calculated for each request (reflects both changes in culture and segments) + // NOTE: in this test, culture changes before segment, so the updated segment is never reflected in the intermediate value here + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment1 (en-US:segment1)", + "en-US:segment2 (en-US:segment1)", + "da-DK:segment2 (da-DK:segment1)")] + // NOTE: As the tests above show, cache levels Element, Elements and Snapshot all yield the same values in this + // test, because we are efficiently executing the test in a snapshot. From here on out we're only building + // test cases for Element and None. + // ### Segment variant content type + invariant property type ### + [TestCase( + ContentVariation.Segment, + ContentVariation.Nothing, + PropertyCacheLevel.Element, + // segment variation => the intermediate value is calculated per segment (ignores culture changes until a segment changes) + // cache level => the final value is calculated only once per segment (ignores culture changes until a segment changes) + "da-DK:segment1 (da-DK:segment1)", + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment2 (en-US:segment2)", + "en-US:segment2 (en-US:segment2)")] + [TestCase( + ContentVariation.Segment, + ContentVariation.Nothing, + PropertyCacheLevel.None, + // segment variation => the intermediate value is calculated per segment (ignores culture changes until a segment changes) + // no cache => the final value is calculated for each request (reflects both changes in culture and segments) + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment1 (da-DK:segment1)", + "en-US:segment2 (en-US:segment2)", + "da-DK:segment2 (en-US:segment2)")] + // ### Culture and segment variant content type + invariant property type ### + [TestCase( + ContentVariation.CultureAndSegment, + ContentVariation.Nothing, + PropertyCacheLevel.Element, + // culture and segment variation => the intermediate value is calculated per culture and segment + // cache level => the final value is calculated only once per culture and segment (efficiently on every request in this test) + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment1 (en-US:segment1)", + "en-US:segment2 (en-US:segment2)", + "da-DK:segment2 (da-DK:segment2)")] + [TestCase( + ContentVariation.CultureAndSegment, + ContentVariation.Nothing, + PropertyCacheLevel.None, + // culture and segment variation => the intermediate value is calculated per culture and segment + // no cache => the final value is calculated for each request + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment1 (en-US:segment1)", + "en-US:segment2 (en-US:segment2)", + "da-DK:segment2 (da-DK:segment2)")] + // ### Invariant content type + culture variant property type ### + [TestCase( + ContentVariation.Nothing, + ContentVariation.Culture, + PropertyCacheLevel.Element, + // same behaviour as culture variation on content type + no variation on property type, see comments above + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment1 (en-US:segment1)", + "en-US:segment1 (en-US:segment1)", + "da-DK:segment1 (da-DK:segment1)")] + [TestCase( + ContentVariation.Nothing, + ContentVariation.Culture, + PropertyCacheLevel.None, + // same behaviour as culture variation on content type + no variation on property type, see comments above + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment1 (en-US:segment1)", + "en-US:segment2 (en-US:segment1)", + "da-DK:segment2 (da-DK:segment1)")] + // ### Invariant content type + segment variant property type ### + [TestCase( + ContentVariation.Nothing, + ContentVariation.Segment, + PropertyCacheLevel.Element, + // same behaviour as segment variation on content type + no variation on property type, see comments above + "da-DK:segment1 (da-DK:segment1)", + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment2 (en-US:segment2)", + "en-US:segment2 (en-US:segment2)")] + [TestCase( + ContentVariation.Nothing, + ContentVariation.Segment, + PropertyCacheLevel.None, + // same behaviour as segment variation on content type + no variation on property type, see comments above + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment1 (da-DK:segment1)", + "en-US:segment2 (en-US:segment2)", + "da-DK:segment2 (en-US:segment2)")] + // ### Invariant content type + culture and segment variant property type ### + [TestCase( + ContentVariation.Nothing, + ContentVariation.CultureAndSegment, + PropertyCacheLevel.Element, + // same behaviour as culture and segment variation on content type + no variation on property type, see comments above + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment1 (en-US:segment1)", + "en-US:segment2 (en-US:segment2)", + "da-DK:segment2 (da-DK:segment2)")] + [TestCase( + ContentVariation.Nothing, + ContentVariation.CultureAndSegment, + PropertyCacheLevel.None, + // same behaviour as culture and segment variation on content type + no variation on property type, see comments above + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment1 (en-US:segment1)", + "en-US:segment2 (en-US:segment2)", + "da-DK:segment2 (da-DK:segment2)")] + // ### Culture variant content type + segment variant property type ### + [TestCase( + ContentVariation.Culture, + ContentVariation.Segment, + PropertyCacheLevel.Element, + // same behaviour as culture and segment variation on content type + no variation on property type, see comments above + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment1 (en-US:segment1)", + "en-US:segment2 (en-US:segment2)", + "da-DK:segment2 (da-DK:segment2)")] + [TestCase( + ContentVariation.Culture, + ContentVariation.Segment, + PropertyCacheLevel.None, + // same behaviour as culture and segment variation on content type + no variation on property type, see comments above + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment1 (en-US:segment1)", + "en-US:segment2 (en-US:segment2)", + "da-DK:segment2 (da-DK:segment2)")] + public void ContentType_PropertyType_Variation_Cache_Values( + ContentVariation contentTypeVariation, + ContentVariation propertyTypeVariation, + PropertyCacheLevel propertyCacheLevel, + string expectedValue1DaDkSegment1, + string expectedValue2EnUsSegment1, + string expectedValue3EnUsSegment2, + string expectedValue4DaDkSegment2) + { + var variationContextCulture = "da-DK"; + var variationContextSegment = "segment1"; + var property = CreateProperty( + contentTypeVariation, + propertyTypeVariation, + propertyCacheLevel, + () => variationContextCulture, + () => variationContextSegment); + + Assert.AreEqual(expectedValue1DaDkSegment1, property.GetValue()); + + variationContextCulture = "en-US"; + Assert.AreEqual(expectedValue2EnUsSegment1, property.GetValue()); + + variationContextSegment = "segment2"; + Assert.AreEqual(expectedValue3EnUsSegment2, property.GetValue()); + + variationContextCulture = "da-DK"; + Assert.AreEqual(expectedValue4DaDkSegment2, property.GetValue()); + } + + [TestCase( + ContentVariation.Culture, + ContentVariation.Nothing, + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment1 (en-US:segment1)", + "en-US:segment1 (en-US:segment1)", + "da-DK:segment1 (da-DK:segment1)")] + [TestCase( + ContentVariation.Segment, + ContentVariation.Nothing, + "da-DK:segment1 (da-DK:segment1)", + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment2 (en-US:segment2)", + "en-US:segment2 (en-US:segment2)")] + [TestCase( + ContentVariation.Culture, + ContentVariation.Segment, + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment1 (en-US:segment1)", + "en-US:segment2 (en-US:segment2)", + "da-DK:segment2 (da-DK:segment2)")] + [TestCase( + ContentVariation.CultureAndSegment, + ContentVariation.Nothing, + "da-DK:segment1 (da-DK:segment1)", + "en-US:segment1 (en-US:segment1)", + "en-US:segment2 (en-US:segment2)", + "da-DK:segment2 (da-DK:segment2)")] + public void ContentType_PropertyType_Variation_Are_Interchangeable( + ContentVariation variation1, + ContentVariation variation2, + string expectedValue1DaDkSegment1, + string expectedValue2EnUsSegment1, + string expectedValue3EnUsSegment2, + string expectedValue4DaDkSegment2) + { + var scenarios = new[] + { + new { ContentTypeVariation = variation1, PropertyTypeVariation = variation2 }, + new { ContentTypeVariation = variation2, PropertyTypeVariation = variation1 } + }; + + foreach (var scenario in scenarios) + { + var variationContextCulture = "da-DK"; + var variationContextSegment = "segment1"; + var property = CreateProperty( + scenario.ContentTypeVariation, + scenario.PropertyTypeVariation, + PropertyCacheLevel.Element, + () => variationContextCulture, + () => variationContextSegment); + + Assert.AreEqual(expectedValue1DaDkSegment1, property.GetValue()); + + variationContextCulture = "en-US"; + Assert.AreEqual(expectedValue2EnUsSegment1, property.GetValue()); + + variationContextSegment = "segment2"; + Assert.AreEqual(expectedValue3EnUsSegment2, property.GetValue()); + + variationContextCulture = "da-DK"; + Assert.AreEqual(expectedValue4DaDkSegment2, property.GetValue()); + } + } + + /// + /// Creates a new property with a mocked publishedSnapshotAccessor that uses a VariationContext that reads culture and segment information from the passed in functions. + /// + private Property CreateProperty(ContentVariation contentTypeVariation, ContentVariation propertyTypeVariation, PropertyCacheLevel propertyTypeCacheLevel, Func getCulture, Func getSegment) + { + var contentType = new Mock(); + contentType.SetupGet(c => c.PropertyTypes).Returns(Array.Empty()); + contentType.SetupGet(c => c.Variations).Returns(contentTypeVariation); + + var contentNode = new ContentNode(123, Guid.NewGuid(), contentType.Object, 1, string.Empty, 1, 1, DateTime.Now, 1); + var contentData = new ContentData("bla", "bla", 1, DateTime.Now, 1, 1, true, new Dictionary(), null); + + var elementCache = new FastDictionaryAppCache(); + var snapshotCache = new FastDictionaryAppCache(); + var publishedSnapshotMock = new Mock(); + publishedSnapshotMock.SetupGet(p => p.ElementsCache).Returns(elementCache); + publishedSnapshotMock.SetupGet(p => p.SnapshotCache).Returns(snapshotCache); + + var publishedSnapshot = publishedSnapshotMock.Object; + var publishedSnapshotAccessor = new Mock(); + publishedSnapshotAccessor.Setup(p => p.TryGetPublishedSnapshot(out publishedSnapshot)).Returns(true); + + var variationContextAccessorMock = new Mock(); + variationContextAccessorMock + .SetupGet(mock => mock.VariationContext) + .Returns(() => new VariationContext(getCulture(), getSegment())); + + var content = new PublishedContent( + contentNode, + contentData, + publishedSnapshotAccessor.Object, + variationContextAccessorMock.Object, + Mock.Of()); + + var propertyType = new Mock(); + propertyType.SetupGet(p => p.CacheLevel).Returns(propertyTypeCacheLevel); + propertyType.SetupGet(p => p.DeliveryApiCacheLevel).Returns(propertyTypeCacheLevel); + propertyType.SetupGet(p => p.Variations).Returns(propertyTypeVariation); + propertyType + .Setup(p => p.ConvertSourceToInter(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(() => $"{getCulture()}:{getSegment()}"); + propertyType + .Setup(p => p.ConvertInterToObject(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((IPublishedElement _, PropertyCacheLevel _, object? inter, bool _) => $"{getCulture()}:{getSegment()} ({inter})" ); + + return new Property(propertyType.Object, content, publishedSnapshotAccessor.Object); + } +} From ce86abe8ac05315c1b7d93e4b40dc0aa5cd73dad Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 28 Sep 2023 13:20:03 +0200 Subject: [PATCH 3/7] Add default property value converters for all value types (#14869) * Add default property value converters for all value types * Clean up some left-over stuff --- ...alueTypePropertyValueConverterAttribute.cs | 10 + .../PropertyValueConverterCollection.cs | 12 +- .../BigintValueTypeConverter.cs | 24 ++ .../DatePickerValueConverter.cs | 43 ++-- .../DateTimeValueTypeConverter.cs | 23 ++ .../ValueConverters/DecimalValueConverter.cs | 7 +- .../DecimalValueTypeConverter.cs | 23 ++ .../IntegerValueTypeConverter.cs | 24 ++ .../TextStringValueTypeConverter.cs | 23 ++ .../ValueConverters/TimeValueTypeConverter.cs | 23 ++ .../ValueTypePropertyValueConverterBase.cs | 18 ++ .../ValueConverters/XmlValueTypeConverter.cs | 51 ++++ .../PropertyEditorValueConverterTests.cs | 35 ++- .../PropertyEditorValueTypeConverterTests.cs | 221 ++++++++++++++++++ 14 files changed, 492 insertions(+), 45 deletions(-) create mode 100644 src/Umbraco.Core/PropertyEditors/DefaultValueTypePropertyValueConverterAttribute.cs create mode 100644 src/Umbraco.Core/PropertyEditors/ValueConverters/BigintValueTypeConverter.cs create mode 100644 src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeValueTypeConverter.cs create mode 100644 src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueTypeConverter.cs create mode 100644 src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueTypeConverter.cs create mode 100644 src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueTypeConverter.cs create mode 100644 src/Umbraco.Core/PropertyEditors/ValueConverters/TimeValueTypeConverter.cs create mode 100644 src/Umbraco.Core/PropertyEditors/ValueConverters/ValueTypePropertyValueConverterBase.cs create mode 100644 src/Umbraco.Core/PropertyEditors/ValueConverters/XmlValueTypeConverter.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueTypeConverterTests.cs diff --git a/src/Umbraco.Core/PropertyEditors/DefaultValueTypePropertyValueConverterAttribute.cs b/src/Umbraco.Core/PropertyEditors/DefaultValueTypePropertyValueConverterAttribute.cs new file mode 100644 index 0000000000..22a0532b5a --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/DefaultValueTypePropertyValueConverterAttribute.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Indicates that this is a default value type property value converter (shipped with Umbraco). +/// This attribute is for internal use only. It should never be applied to custom value converters. +/// +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public class DefaultValueTypePropertyValueConverterAttribute : DefaultPropertyValueConverterAttribute +{ +} diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs index 20eb9ae4c4..9833a31ed7 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs @@ -44,5 +44,15 @@ public class PropertyValueConverterCollection : BuilderCollectionBase DefaultConverters.ContainsKey(converter); internal bool Shadows(IPropertyValueConverter shadowing, IPropertyValueConverter shadowed) - => DefaultConverters.TryGetValue(shadowing, out Type[]? types) && types.Contains(shadowed.GetType()); + { + Type shadowedType = shadowed.GetType(); + + // any value converter built specifically to convert purely value type bound properties can always be shadowed + if (shadowedType.GetCustomAttribute(false) is not null) + { + return true; + } + + return DefaultConverters.TryGetValue(shadowing, out Type[]? types) && types.Contains(shadowedType); + } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/BigintValueTypeConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/BigintValueTypeConverter.cs new file mode 100644 index 0000000000..102236350a --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/BigintValueTypeConverter.cs @@ -0,0 +1,24 @@ +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultValueTypePropertyValueConverter] +public class BigintValueTypeConverter : ValueTypePropertyValueConverterBase +{ + protected override string[] SupportedValueTypes => new[] { ValueTypes.Bigint }; + + public BigintValueTypeConverter(PropertyEditorCollection propertyEditors) + : base(propertyEditors) + { + } + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(long); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => source.TryConvertTo().Result; +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs index 73ef424ba4..60cbc28b6c 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs @@ -17,26 +17,7 @@ public class DatePickerValueConverter : PropertyValueConverterBase => PropertyCacheLevel.Element; public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - if (source == null) - { - return DateTime.MinValue; - } - - // in XML a DateTime is: string - format "yyyy-MM-ddTHH:mm:ss" - // Actually, not always sometimes it is formatted in UTC style with 'Z' suffixed on the end but that is due to this bug: - // http://issues.umbraco.org/issue/U4-4145, http://issues.umbraco.org/issue/U4-3894 - // We should just be using TryConvertTo instead. - if (source is string sourceString) - { - Attempt attempt = sourceString.TryConvertTo(); - return attempt.Success == false ? DateTime.MinValue : attempt.Result; - } - - // in the database a DateTime is: DateTime - // default value is: DateTime.MinValue - return source is DateTime ? source : DateTime.MinValue; - } + => ParseDateTimeValue(source); // default ConvertSourceToObject just returns source ie a DateTime value [Obsolete("The current implementation of XPath is suboptimal and will be removed entirely in a future version. Scheduled for removal in v14")] @@ -55,4 +36,26 @@ public class DatePickerValueConverter : PropertyValueConverterBase return XmlConvert.ToString((DateTime)inter, XmlDateTimeSerializationMode.Unspecified); } + + internal static DateTime ParseDateTimeValue(object? source) + { + if (source == null) + { + return DateTime.MinValue; + } + + // in XML a DateTime is: string - format "yyyy-MM-ddTHH:mm:ss" + // Actually, not always sometimes it is formatted in UTC style with 'Z' suffixed on the end but that is due to this bug: + // http://issues.umbraco.org/issue/U4-4145, http://issues.umbraco.org/issue/U4-3894 + // We should just be using TryConvertTo instead. + if (source is string sourceString) + { + Attempt attempt = sourceString.TryConvertTo(); + return attempt.Success == false ? DateTime.MinValue : attempt.Result; + } + + // in the database a DateTime is: DateTime + // default value is: DateTime.MinValue + return source is DateTime dateTimeValue ? dateTimeValue : DateTime.MinValue; + } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeValueTypeConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeValueTypeConverter.cs new file mode 100644 index 0000000000..f9c8caa50c --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeValueTypeConverter.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultValueTypePropertyValueConverter] +public class DateTimeValueTypeConverter : ValueTypePropertyValueConverterBase +{ + protected override string[] SupportedValueTypes => new[] { ValueTypes.Date, ValueTypes.DateTime }; + + public DateTimeValueTypeConverter(PropertyEditorCollection propertyEditors) + : base(propertyEditors) + { + } + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(DateTime); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => DatePickerValueConverter.ParseDateTimeValue(source); // reuse the value conversion from the default "Umbraco.DateTime" value converter +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs index 5a7f0a4adc..ba3a1e552b 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs @@ -16,6 +16,9 @@ public class DecimalValueConverter : PropertyValueConverterBase => PropertyCacheLevel.Element; public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => ParseDecimalValue(source); + + internal static decimal ParseDecimalValue(object? source) { if (source == null) { @@ -23,9 +26,9 @@ public class DecimalValueConverter : PropertyValueConverterBase } // is it already a decimal? - if (source is decimal) + if (source is decimal sourceDecimal) { - return source; + return sourceDecimal; } // is it a double? diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueTypeConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueTypeConverter.cs new file mode 100644 index 0000000000..37ab859994 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueTypeConverter.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultValueTypePropertyValueConverter] +public class DecimalValueTypeConverter : ValueTypePropertyValueConverterBase +{ + protected override string[] SupportedValueTypes => new[] { ValueTypes.Decimal }; + + public DecimalValueTypeConverter(PropertyEditorCollection propertyEditors) + : base(propertyEditors) + { + } + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(decimal); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => DecimalValueConverter.ParseDecimalValue(source); // reuse the value conversion from the default "Umbraco.Decimal" value converter +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueTypeConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueTypeConverter.cs new file mode 100644 index 0000000000..78789b3953 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueTypeConverter.cs @@ -0,0 +1,24 @@ +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultValueTypePropertyValueConverter] +public class IntegerValueTypeConverter : ValueTypePropertyValueConverterBase +{ + protected override string[] SupportedValueTypes => new[] { ValueTypes.Integer }; + + public IntegerValueTypeConverter(PropertyEditorCollection propertyEditors) + : base(propertyEditors) + { + } + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(int); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => source.TryConvertTo().Result; +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueTypeConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueTypeConverter.cs new file mode 100644 index 0000000000..9a57aaf52c --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueTypeConverter.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultValueTypePropertyValueConverter] +public class TextStringValueTypeConverter : ValueTypePropertyValueConverterBase +{ + protected override string[] SupportedValueTypes => new[] { ValueTypes.Text, ValueTypes.String }; + + public TextStringValueTypeConverter(PropertyEditorCollection propertyEditors) + : base(propertyEditors) + { + } + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => source as string; +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TimeValueTypeConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TimeValueTypeConverter.cs new file mode 100644 index 0000000000..c18890e22a --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TimeValueTypeConverter.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultValueTypePropertyValueConverter] +public class TimeValueTypeConverter : ValueTypePropertyValueConverterBase +{ + protected override string[] SupportedValueTypes => new[] { ValueTypes.Time }; + + public TimeValueTypeConverter(PropertyEditorCollection propertyEditors) + : base(propertyEditors) + { + } + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(TimeSpan); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => source is DateTime dateTimeValue ? dateTimeValue.ToUniversalTime().TimeOfDay : null; +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ValueTypePropertyValueConverterBase.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ValueTypePropertyValueConverterBase.cs new file mode 100644 index 0000000000..461de31812 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ValueTypePropertyValueConverterBase.cs @@ -0,0 +1,18 @@ +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +public abstract class ValueTypePropertyValueConverterBase : PropertyValueConverterBase +{ + private readonly PropertyEditorCollection _propertyEditors; + + protected abstract string[] SupportedValueTypes { get; } + + protected ValueTypePropertyValueConverterBase(PropertyEditorCollection propertyEditors) + => _propertyEditors = propertyEditors; + + public override bool IsConverter(IPublishedPropertyType propertyType) + => _propertyEditors.TryGet(propertyType.EditorAlias, out IDataEditor? editor) + && SupportedValueTypes.InvariantContains(editor.GetValueEditor().ValueType); +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/XmlValueTypeConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/XmlValueTypeConverter.cs new file mode 100644 index 0000000000..bab85a25d9 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/XmlValueTypeConverter.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors.DeliveryApi; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultValueTypePropertyValueConverter] +public class XmlValueTypeConverter : ValueTypePropertyValueConverterBase, IDeliveryApiPropertyValueConverter +{ + protected override string[] SupportedValueTypes => new[] { ValueTypes.Xml }; + + public XmlValueTypeConverter(PropertyEditorCollection propertyEditors) + : base(propertyEditors) + { + } + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(XDocument); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + if (source is not string stringValue) + { + return null; + } + + try + { + return XDocument.Parse(stringValue); + } + catch + { + return null; + } + } + + public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) + => GetPropertyCacheLevel(propertyType); + + public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); + + // System.Text.Json does not appreciate serializing XDocument because of parent/child node references. Let's settle for outputting the raw XML as a string, then. + public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) + => inter is XDocument xDocumentValue + ? xDocumentValue.ToString(SaveOptions.DisableFormatting) + : null; +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueConverterTests.cs index d046064af2..f84f08d8aa 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueConverterTests.cs @@ -1,11 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Logging; using Moq; -using Newtonsoft.Json.Linq; using NUnit.Framework; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -116,18 +112,12 @@ public class PropertyEditorValueConverterTests } [TestCase("1", 1)] - [TestCase("1", 1)] - [TestCase("0", 0)] [TestCase("0", 0)] [TestCase(null, 0)] - [TestCase(null, 0)] - [TestCase("-1", -1)] [TestCase("-1", -1)] [TestCase("1.65", 1.65)] - [TestCase("1.65", 1.65)] [TestCase("-1.65", -1.65)] - [TestCase("-1.65", -1.65)] - public void CanConvertDecimalAliasPropertyEditor(object value, double expected) + public void CanConvertDecimalAliasPropertyEditor(object value, decimal expected) { var converter = new DecimalValueConverter(); var inter = converter.ConvertSourceToIntermediate(null, null, value, false); @@ -136,18 +126,19 @@ public class PropertyEditorValueConverterTests Assert.AreEqual(expected, result); } - [Test] - public void CanConvertManifestBasedPropertyWithValueTypeJson() + [TestCase("100", 100)] + [TestCase("0", 0)] + [TestCase(null, 0)] + [TestCase("-100", -100)] + [TestCase("1.65", 2)] + [TestCase("-1.65", -2)] + [TestCase("something something", 0)] + public void CanConvertIntegerAliasPropertyEditor(object value, int expected) { - var valueEditor = Mock.Of(x => x.ValueType == ValueTypes.Json); - var dataEditor = Mock.Of(x => x.GetValueEditor() == valueEditor); - var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => new[] { dataEditor })); - var propertyType = Mock.Of(x => x.EditorAlias == "My.Custom.Json"); + var converter = new IntegerValueConverter(); + var inter = converter.ConvertSourceToIntermediate(null, null, value, false); + var result = converter.ConvertIntermediateToObject(null, null, PropertyCacheLevel.Unknown, inter, false); - var valueConverter = new JsonValueConverter(propertyEditors, Mock.Of>()); - var inter = valueConverter.ConvertSourceToIntermediate(Mock.Of(), propertyType, "{\"message\": \"Hello, JSON\"}", false); - var result = valueConverter.ConvertIntermediateToObject(Mock.Of(), propertyType, PropertyCacheLevel.Element, inter, false) as JObject; - Assert.IsNotNull(result); - Assert.AreEqual("Hello, JSON", result["message"]!.Value()); + Assert.AreEqual(expected, result); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueTypeConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueTypeConverterTests.cs new file mode 100644 index 0000000000..fb4bc40ea0 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueTypeConverterTests.cs @@ -0,0 +1,221 @@ +using System.Xml.Linq; +using Microsoft.Extensions.Logging; +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; + +[TestFixture] +public class PropertyEditorValueTypeConverterTests +{ + [TestCase("2023-09-26 13:14:15", true)] + [TestCase("2023-09-26T13:14:15", true)] + [TestCase("2023-09-26T00:00:00", true)] + [TestCase("2023-09-26", true)] + [TestCase("", false)] + [TestCase("Hello, world!", false)] + [TestCase(123456, false)] + [TestCase(null, false)] + public void CanConvertDateValueTypePropertyEditor(object? date, bool expectedSuccess) + { + var expectedResult = expectedSuccess ? DateTime.Parse((date as string)!) : DateTime.MinValue; + var supportedValueTypes = new[] { ValueTypes.DateTime, ValueTypes.Date }; + foreach (var valueType in supportedValueTypes) + { + var converter = new DateTimeValueTypeConverter(ValueTypePropertyEditorCollection(valueType)); + var propertyType = PropertyType(); + + Assert.IsTrue(converter.IsConverter(propertyType)); + + var result = converter.ConvertSourceToIntermediate(null, propertyType, date, false); + + Assert.AreEqual(expectedResult, result); + } + } + + [TestCase("1", 1)] + [TestCase("0", 0)] + [TestCase(null, 0)] + [TestCase("", 0)] + [TestCase("Hello, world!", 0)] + [TestCase("-1", -1)] + [TestCase("1.65", 1.65)] + [TestCase("-1.65", -1.65)] + public void CanConvertDecimalValueTypePropertyEditor(object value, decimal expected) + { + var propertyEditors = ValueTypePropertyEditorCollection(ValueTypes.Decimal); + var propertyType = PropertyType(); + + var converter = new DecimalValueTypeConverter(propertyEditors); + var inter = converter.ConvertSourceToIntermediate(Mock.Of(), propertyType, value, false); + + Assert.IsTrue(converter.IsConverter(propertyType)); + + var result = converter.ConvertIntermediateToObject(Mock.Of(), propertyType, PropertyCacheLevel.Element, inter, false); + Assert.IsTrue(result is decimal); + Assert.AreEqual(expected, result); + } + + [TestCase("100", 100)] + [TestCase("0", 0)] + [TestCase(null, 0)] + [TestCase("", 0)] + [TestCase("Hello, world!", 0)] + [TestCase("-100", -100)] + [TestCase("1.65", 2)] + [TestCase("-1.65", -2)] + public void CanConvertIntegerValueTypePropertyEditor(object value, int expected) + { + var propertyEditors = ValueTypePropertyEditorCollection(ValueTypes.Integer); + var propertyType = PropertyType(); + + var converter = new IntegerValueTypeConverter(propertyEditors); + var inter = converter.ConvertSourceToIntermediate(Mock.Of(), propertyType, value, false); + + Assert.IsTrue(converter.IsConverter(propertyType)); + + var result = converter.ConvertIntermediateToObject(Mock.Of(), propertyType, PropertyCacheLevel.Element, inter, false); + Assert.IsTrue(result is int); + Assert.AreEqual(expected, result); + } + + [TestCase("100", 100)] + [TestCase("0", 0)] + [TestCase(null, 0)] + [TestCase("", 0)] + [TestCase("Hello, world!", 0)] + [TestCase("-100", -100)] + [TestCase("1.65", 2)] + [TestCase("-1.65", -2)] + public void CanConvertBigintValueTypePropertyEditor(object value, long expected) + { + var propertyEditors = ValueTypePropertyEditorCollection(ValueTypes.Bigint); + var propertyType = PropertyType(); + + var converter = new BigintValueTypeConverter(propertyEditors); + var inter = converter.ConvertSourceToIntermediate(Mock.Of(), propertyType, value, false); + + Assert.IsTrue(converter.IsConverter(propertyType)); + + var result = converter.ConvertIntermediateToObject(Mock.Of(), propertyType, PropertyCacheLevel.Element, inter, false); + Assert.IsTrue(result is long); + Assert.AreEqual(expected, result); + } + + [TestCase("100", "100")] + [TestCase("0", "0")] + [TestCase(null, null)] + [TestCase("", "")] + [TestCase("Hello, world!", "Hello, world!")] + [TestCase(-100, null)] + [TestCase(1.65, null)] + public void CanConvertTextAndStringValueTypePropertyEditor(object? value, string? expected) + { + var scenarios = new[] { ValueTypes.Text, ValueTypes.String }; + foreach (var scenario in scenarios) + { + var propertyEditors = ValueTypePropertyEditorCollection(scenario); + var propertyType = PropertyType(); + + var converter = new TextStringValueTypeConverter(propertyEditors); + var inter = converter.ConvertSourceToIntermediate(Mock.Of(), propertyType, value, false); + + Assert.IsTrue(converter.IsConverter(propertyType)); + + var result = converter.ConvertIntermediateToObject(Mock.Of(), propertyType, PropertyCacheLevel.Element, inter, false); + Assert.AreEqual(expected, result); + } + } + + [TestCase("2023-01-01T03:04:00Z","03:04:00")] + [TestCase("2023-01-01T13:14:00Z", "13:14:00")] + [TestCase("2023-01-01T13:14:15Z", "13:14:15")] + [TestCase("2023-01-01T13:14:15.678Z", "13:14:15.678")] + [TestCase("", null)] + [TestCase("Hello, world!", null)] + [TestCase(123456, null)] + [TestCase(null, null)] + public void CanConvertTimeValueTypePropertyEditor(object? value, object? expectedTime) + { + var sourceValue = expectedTime is not null ? DateTime.Parse((value as string)!) : value; + TimeSpan? expectedResult = expectedTime is not null ? TimeSpan.Parse((expectedTime as string)!) : null; + var propertyEditors = ValueTypePropertyEditorCollection(ValueTypes.Time); + var converter = new TimeValueTypeConverter(propertyEditors); + var propertyType = PropertyType(); + + Assert.IsTrue(converter.IsConverter(propertyType)); + + var result = converter.ConvertSourceToIntermediate(null, propertyType, sourceValue, false); + Assert.AreEqual(expectedResult, result); + } + + [TestCase("test", true)] + [TestCase("child 1child 2", true)] + [TestCase("malformed XML", false)] + [TestCase("", false)] + [TestCase("Hello, world!", false)] + [TestCase(123456, false)] + [TestCase(null, false)] + public void CanConvertXmlValueTypePropertyEditor(object? value, bool expectsSuccess) + { + var propertyEditors = ValueTypePropertyEditorCollection(ValueTypes.Xml); + var converter = new XmlValueTypeConverter(propertyEditors); + var propertyType = PropertyType(); + + Assert.IsTrue(converter.IsConverter(propertyType)); + + var result = converter.ConvertSourceToIntermediate(null, propertyType, value, false) as XDocument; + if (expectsSuccess) + { + Assert.IsNotNull(result); + Assert.AreEqual(value, result.ToString(SaveOptions.DisableFormatting)); + } + else + { + Assert.IsNull(result); + } + } + + [TestCase("{\"message\":\"Hello, JSON\"}", true)] + [TestCase("{\"nested\":{\"message\":\"Hello, Nested\"}}", true)] + [TestCase("{\"nested\":{\"invalid JSON", false)] + [TestCase("", false)] + [TestCase("Hello, world!", false)] + [TestCase(123456, false)] + [TestCase(null, false)] + public void CanConvertJsonValueTypePropertyEditor(object? source, bool expectsSuccess) + { + var propertyEditors = ValueTypePropertyEditorCollection(ValueTypes.Json); + var converter = new JsonValueConverter(propertyEditors, Mock.Of>()); + var propertyType = PropertyType(); + + Assert.IsTrue(converter.IsConverter(propertyType)); + + var result = converter.ConvertSourceToIntermediate(null, propertyType, source, false) as JToken; + if (expectsSuccess) + { + Assert.IsNotNull(result); + Assert.AreEqual(source, result.ToString(Formatting.None)); + } + else + { + Assert.IsNull(result); + } + } + + private static PropertyEditorCollection ValueTypePropertyEditorCollection(string valueType) + { + var valueEditor = Mock.Of(x => x.ValueType == valueType); + var dataEditor = Mock.Of(x => x.GetValueEditor() == valueEditor && x.Alias == "My.Custom.Alias" && x.Type == EditorType.PropertyValue); + var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => new[] { dataEditor })); + return propertyEditors; + } + + private static IPublishedPropertyType PropertyType() => Mock.Of(x => x.EditorAlias == "My.Custom.Alias"); +} From 7be18528271dd05355079fba5cad156353bb9deb Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 28 Sep 2023 13:25:11 +0200 Subject: [PATCH 4/7] Make sure to sign out external users if an error occurs during sign-in (#14867) --- .../Controllers/BackOfficeController.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index 43e99622c1..911028912a 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -585,6 +585,11 @@ public class BackOfficeController : UmbracoController if (errors.Count > 0) { + // the external user might actually be signed in at this point, but certain errors (i.e. missing claims) + // prevents us from applying said user to a back-office session. make sure the sign-in manager does not + // report the user as being signed in for subsequent requests. + await _signInManager.SignOutAsync(); + ViewData.SetExternalSignInProviderErrors( new BackOfficeExternalLoginProviderErrors( loginInfo.LoginProvider, From 04b05714e572e226e1473e8946e776d3c3ab3d34 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 5 Oct 2023 14:39:00 +0200 Subject: [PATCH 5/7] Add notification state to "final" published notification when publishing with descendants (#14922) * Revert "V10/bugfix/14543 publish descendants (#14763)" This reverts commit f750bca453c12d4cc4321868eb1197c9916f8243. * Add notification state to "final" published notification when publishing with descendants --- src/Umbraco.Core/Services/ContentService.cs | 43 ++++++------------- .../ContentServicePublishBranchTests.cs | 12 +++--- 2 files changed, 19 insertions(+), 36 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index e9d0f8f4fd..262598451a 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1950,7 +1950,7 @@ public class ContentService : RepositoryService, IContentService cultures = new HashSet(); // empty means 'already published' } - if (isRoot || edited) + if (edited) { cultures.Add(c); // means 'republish this culture' } @@ -2105,13 +2105,11 @@ public class ContentService : RepositoryService, IContentService } // deal with the branch root - if it fails, abort - var rootPublishNotificationState = new Dictionary(); - PublishResult? rootResult = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true, - publishedDocuments, eventMessages, userId, allLangs, rootPublishNotificationState); - if (rootResult != null) + PublishResult? result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, eventMessages, userId, allLangs, out IDictionary notificationState); + if (result != null) { - results.Add(rootResult); - if (!rootResult.Success) + results.Add(result); + if (!result.Success) { return results; } @@ -2124,7 +2122,6 @@ public class ContentService : RepositoryService, IContentService int count; var page = 0; const int pageSize = 100; - PublishResult? result = null; do { count = 0; @@ -2143,8 +2140,7 @@ public class ContentService : RepositoryService, IContentService } // no need to check path here, parent has to be published here - result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false, - publishedDocuments, eventMessages, userId, allLangs,null); + result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, eventMessages, userId, allLangs, out _); if (result != null) { results.Add(result); @@ -2168,12 +2164,7 @@ public class ContentService : RepositoryService, IContentService // (SaveAndPublishBranchOne does *not* do it) scope.Notifications.Publish( new ContentTreeChangeNotification(document, TreeChangeTypes.RefreshBranch, eventMessages)); - if (rootResult?.Success is true) - { - scope.Notifications.Publish( - new ContentPublishedNotification(rootResult!.Content!, eventMessages) - .WithState(rootPublishNotificationState)); - } + scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages).WithState(notificationState)); scope.Complete(); } @@ -2184,9 +2175,6 @@ public class ContentService : RepositoryService, IContentService // shouldPublish: a function determining whether the document has changes that need to be published // note - 'force' is handled by 'editing' // publishValues: a function publishing values (using the appropriate PublishCulture calls) - /// Only set this when processing a the root of the branch - /// Published notification will not be send when this property is set - /// private PublishResult? SaveAndPublishBranchItem( ICoreScope scope, IContent document, @@ -2198,8 +2186,9 @@ public class ContentService : RepositoryService, IContentService EventMessages evtMsgs, int userId, IReadOnlyCollection allLangs, - IDictionary? rootPublishingNotificationState) + out IDictionary notificationState) { + notificationState = new Dictionary(); HashSet? culturesToPublish = shouldPublish(document); // null = do not include @@ -2227,17 +2216,11 @@ public class ContentService : RepositoryService, IContentService return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document); } - var notificationState = rootPublishingNotificationState ?? new Dictionary(); - PublishResult result = CommitDocumentChangesInternal(scope, document, evtMsgs, allLangs, notificationState, userId, true, isRoot); - if (!result.Success) + PublishResult result = CommitDocumentChangesInternal(scope, document, evtMsgs, allLangs, savingNotification.State, userId, true, isRoot); + if (result.Success) { - return result; - } - - publishedDocuments.Add(document); - if (rootPublishingNotificationState == null) - { - scope.Notifications.Publish(new ContentPublishedNotification(result.Content!, evtMsgs).WithState(notificationState)); + publishedDocuments.Add(document); + notificationState = savingNotification.State; } return result; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePublishBranchTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePublishBranchTests.cs index bee4e7d045..83d51dbe8c 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePublishBranchTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePublishBranchTests.cs @@ -93,7 +93,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest AssertPublishResults( r, x => x.Result, - PublishResultType.SuccessPublish, // During branch publishing, the change detection of the root branch runs AFTER the check to process the branchItem => root is always saved&Published as the intent requires it. + PublishResultType.SuccessPublishAlready, PublishResultType.SuccessPublishAlready, PublishResultType.SuccessPublishAlready); @@ -140,7 +140,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest AssertPublishResults( r, x => x.Result, - PublishResultType.SuccessPublish, // During branch publishing, the change detection of the root branch runs AFTER the check to process the branchItem => root is always saved&Published as the intent requires it. + PublishResultType.SuccessPublishAlready, PublishResultType.SuccessPublishAlready, PublishResultType.SuccessPublishAlready, PublishResultType.SuccessPublish, @@ -185,7 +185,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest var r = ContentService.SaveAndPublishBranch(vRoot, false) .ToArray(); // no culture specified so "*" is used, so all cultures - Assert.AreEqual(PublishResultType.SuccessPublishCulture, r[0].Result); // During branch publishing, the change detection of the root branch runs AFTER the check to process the branchItem => root is always saved&Published as the intent requires it. + Assert.AreEqual(PublishResultType.SuccessPublishAlready, r[0].Result); Assert.AreEqual(PublishResultType.SuccessPublishCulture, r[1].Result); } @@ -221,7 +221,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest var saveResult = ContentService.Save(iv1); var r = ContentService.SaveAndPublishBranch(vRoot, false, "de").ToArray(); - Assert.AreEqual(PublishResultType.SuccessPublishCulture, r[0].Result); // During branch publishing, the change detection of the root branch runs AFTER the check to process the branchItem => root is always saved&Published as the intent requires it. + Assert.AreEqual(PublishResultType.SuccessPublishAlready, r[0].Result); Assert.AreEqual(PublishResultType.SuccessPublishCulture, r[1].Result); } @@ -381,7 +381,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest AssertPublishResults( r, x => x.Result, - PublishResultType.SuccessPublish, // During branch publishing, the change detection of the root branch runs AFTER the check to process the branchItem => root is always saved&Published as the intent requires it. + PublishResultType.SuccessPublishAlready, PublishResultType.SuccessPublish, PublishResultType.SuccessPublishCulture); @@ -407,7 +407,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest AssertPublishResults( r, x => x.Result, - PublishResultType.SuccessPublish, // During branch publishing, the change detection of the root branch runs AFTER the check to process the branchItem => root is always saved&Published as the intent requires it. + PublishResultType.SuccessPublishAlready, PublishResultType.SuccessPublish, PublishResultType.SuccessPublishCulture); From 132935c2d76a2e457113db6b4721a77f8c986035 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Mon, 9 Oct 2023 10:19:36 +0200 Subject: [PATCH 6/7] Handle corrupted index (#14889) --- .../ExamineManagementController.cs | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs b/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs index 4ab0ccd072..e57f790fb0 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs @@ -187,15 +187,41 @@ public class ExamineManagementController : UmbracoAuthorizedJsonController private ExamineIndexModel CreateModel(IIndex index) { var indexName = index.Name; - IIndexDiagnostics indexDiag = _indexDiagnosticsFactory.Create(index); - Attempt isHealth = indexDiag.IsHealthy(); + var healthResult = isHealth.Result; + + long documentCount; + int fieldCount; + + try + { + // This will throw if the index is corrupted - i.e. a file in the index folder cannot be found + // Which will break the UI and not give the possibility to rebuild the index + documentCount = indexDiag.GetDocumentCount(); + fieldCount = indexDiag.GetFieldNames().Count(); + } + catch (FileNotFoundException ex) + { + // Safe catch that will allow to rebuild a corrupted index + documentCount = 0; + fieldCount = 0; + + _logger.LogWarning(ex, "{name} is corrupted.", indexName); + + if (!string.IsNullOrWhiteSpace(healthResult)) + { + healthResult += " "; + } + + // Provide a useful message in the Examine dashboard + healthResult += $"It may not be possible to rebuild the index. Please try deleting the entire {indexName} folder and then attempt to rebuild it again."; + } var properties = new Dictionary { - ["DocumentCount"] = indexDiag.GetDocumentCount(), - ["FieldCount"] = indexDiag.GetFieldNames().Count() + ["DocumentCount"] = documentCount, + ["FieldCount"] = fieldCount }; foreach (KeyValuePair p in indexDiag.Metadata) @@ -206,7 +232,7 @@ public class ExamineManagementController : UmbracoAuthorizedJsonController var indexerModel = new ExamineIndexModel { Name = indexName, - HealthStatus = isHealth.Success ? isHealth.Result ?? "Healthy" : isHealth.Result ?? "Unhealthy", + HealthStatus = isHealth.Success ? healthResult ?? "Healthy" : healthResult ?? "Unhealthy", ProviderProperties = properties, CanRebuild = _indexRebuilder.CanRebuild(index.Name) }; From 41f8f03c2c4eb4d45a512c21af457923db900866 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 10 Oct 2023 11:51:47 +0200 Subject: [PATCH 7/7] Fixed a couple of occurrences where scopes was auto-completed while modified db state (#14947) * Fixed a couple of occurrences where scopes was auto-complated while actually modified the state of the database. * Added temp ctor to fix boot issue --- src/Umbraco.Core/Services/ContentService.cs | 14 +++-- ...peServiceBaseOfTRepositoryTItemTService.cs | 1 - .../Services/ContentVersionService.cs | 15 +++-- src/Umbraco.Core/Services/DataTypeService.cs | 2 +- .../DefaultContentVersionCleanupPolicy.cs | 4 +- src/Umbraco.Core/Services/MediaService.cs | 3 +- .../Services/TwoFactorLoginService.cs | 15 +++-- .../HostedServices/ScheduledPublishing.cs | 4 +- .../Logging/Viewer/LogViewerConfig.cs | 28 +++++++-- .../Security/MemberUserStore.cs | 6 +- .../Controllers/UmbProfileController.cs | 3 +- .../Controllers/UmbRegisterController.cs | 4 +- tests/Umbraco.Tests.Common/TestHelperBase.cs | 60 +++++++++++++++++++ .../TestHelpers/TestHelper.cs | 4 ++ .../Logging/LogviewerTests.cs | 2 +- .../Security/MemberManagerTests.cs | 3 +- .../Security/MemberUserStoreTests.cs | 18 ++---- 17 files changed, 145 insertions(+), 41 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 160804c85b..c1b81505a1 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -372,7 +372,7 @@ public class ContentService : RepositoryService, IContentService public IContent CreateAndSave(string name, int parentId, string contentTypeAlias, int userId = Constants.Security.SuperUserId) { // TODO: what about culture? - using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { // locking the content tree secures content types too scope.WriteLock(Constants.Locks.ContentTree); @@ -395,6 +395,8 @@ public class ContentService : RepositoryService, IContentService Save(content, userId); + scope.Complete(); + return content; } } @@ -416,7 +418,7 @@ public class ContentService : RepositoryService, IContentService throw new ArgumentNullException(nameof(parent)); } - using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { // locking the content tree secures content types too scope.WriteLock(Constants.Locks.ContentTree); @@ -431,6 +433,7 @@ public class ContentService : RepositoryService, IContentService Save(content, userId); + scope.Complete(); return content; } } @@ -508,10 +511,11 @@ public class ContentService : RepositoryService, IContentService /// public void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule) { - using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { scope.WriteLock(Constants.Locks.ContentTree); _documentRepository.PersistContentSchedule(content, contentSchedule); + scope.Complete(); } } @@ -2978,7 +2982,7 @@ public class ContentService : RepositoryService, IContentService public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options) { - using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { scope.WriteLock(Constants.Locks.ContentTree); @@ -2991,6 +2995,8 @@ public class ContentService : RepositoryService, IContentService scope.Notifications.Publish(new ContentTreeChangeNotification(root, TreeChangeTypes.RefreshAll, EventMessagesFactory.Get())); } + scope.Complete(); + return report; } } diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index 7cf63445a9..512fad0674 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -322,7 +322,6 @@ public abstract class ContentTypeServiceBase : ContentTypeSe } using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { scope.ReadLock(ReadLockIds); return Repository.GetMany(ids.ToArray()); diff --git a/src/Umbraco.Core/Services/ContentVersionService.cs b/src/Umbraco.Core/Services/ContentVersionService.cs index 6f1c28deb8..1c190311da 100644 --- a/src/Umbraco.Core/Services/ContentVersionService.cs +++ b/src/Umbraco.Core/Services/ContentVersionService.cs @@ -68,7 +68,7 @@ internal class ContentVersionService : IContentVersionService /// public void SetPreventCleanup(int versionId, bool preventCleanup, int userId = Constants.Security.SuperUserId) { - using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + using (ICoreScope scope = _scopeProvider.CreateCoreScope()) { scope.WriteLock(Constants.Locks.ContentTree); _documentVersionRepository.SetPreventCleanup(versionId, preventCleanup); @@ -87,6 +87,7 @@ internal class ContentVersionService : IContentVersionService var message = $"set preventCleanup = '{preventCleanup}' for version '{versionId}'"; Audit(auditType, userId, version.ContentId, message, $"{version.VersionDate}"); + scope.Complete(); } } @@ -120,7 +121,7 @@ internal class ContentVersionService : IContentVersionService * * tl;dr lots of scopes to enable other connections to use the DB whilst we work. */ - using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + using (ICoreScope scope = _scopeProvider.CreateCoreScope()) { IReadOnlyCollection? allHistoricVersions = _documentVersionRepository.GetDocumentVersionsEligibleForCleanup(); @@ -154,6 +155,8 @@ internal class ContentVersionService : IContentVersionService versionsToDelete.Add(version); } + + scope.Complete(); } if (!versionsToDelete.Any()) @@ -169,7 +172,7 @@ internal class ContentVersionService : IContentVersionService foreach (IEnumerable group in versionsToDelete.InGroupsOf(Constants.Sql.MaxParameterCount)) { - using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + using (ICoreScope scope = _scopeProvider.CreateCoreScope()) { scope.WriteLock(Constants.Locks.ContentTree); var groupEnumerated = group.ToList(); @@ -182,12 +185,16 @@ internal class ContentVersionService : IContentVersionService scope.Notifications.Publish( new ContentDeletedVersionsNotification(version.ContentId, messages, version.VersionId)); } + + scope.Complete(); } } - using (_scopeProvider.CreateCoreScope(autoComplete: true)) + using (ICoreScope scope = _scopeProvider.CreateCoreScope()) { Audit(AuditType.Delete, Constants.Security.SuperUserId, -1, $"Removed {versionsToDelete.Count} ContentVersion(s) according to cleanup policy"); + + scope.Complete(); } return versionsToDelete; diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index 08cfd602f5..857ecdc347 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -608,7 +608,7 @@ namespace Umbraco.Cms.Core.Services.Implement public IReadOnlyDictionary> GetReferences(int id) { - using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete:true); + using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); return _dataTypeRepository.FindUsages(id); } diff --git a/src/Umbraco.Core/Services/DefaultContentVersionCleanupPolicy.cs b/src/Umbraco.Core/Services/DefaultContentVersionCleanupPolicy.cs index f51858fa5b..234dae683f 100644 --- a/src/Umbraco.Core/Services/DefaultContentVersionCleanupPolicy.cs +++ b/src/Umbraco.Core/Services/DefaultContentVersionCleanupPolicy.cs @@ -33,7 +33,7 @@ public class DefaultContentVersionCleanupPolicy : IContentVersionCleanupPolicy var theRest = new List(); - using (_scopeProvider.CreateCoreScope(autoComplete: true)) + using (ICoreScope scope = _scopeProvider.CreateCoreScope()) { var policyOverrides = _documentVersionRepository.GetCleanupPolicies()? .ToDictionary(x => x.ContentTypeId); @@ -77,6 +77,8 @@ public class DefaultContentVersionCleanupPolicy : IContentVersionCleanupPolicy yield return version; } } + + scope.Complete(); } } diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index fadc682d16..8738c6c67e 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -1197,7 +1197,7 @@ namespace Umbraco.Cms.Core.Services public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options) { - using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { scope.WriteLock(Constants.Locks.MediaTree); @@ -1210,6 +1210,7 @@ namespace Umbraco.Cms.Core.Services scope.Notifications.Publish(new MediaTreeChangeNotification(root, TreeChangeTypes.RefreshAll, EventMessagesFactory.Get())); } + scope.Complete(); return report; } } diff --git a/src/Umbraco.Core/Services/TwoFactorLoginService.cs b/src/Umbraco.Core/Services/TwoFactorLoginService.cs index 64da30b991..bcfa102d1f 100644 --- a/src/Umbraco.Core/Services/TwoFactorLoginService.cs +++ b/src/Umbraco.Core/Services/TwoFactorLoginService.cs @@ -42,8 +42,10 @@ public class TwoFactorLoginService : ITwoFactorLoginService /// public async Task DeleteUserLoginsAsync(Guid userOrMemberKey) { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + using ICoreScope scope = _scopeProvider.CreateCoreScope(); await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey); + + scope.Complete(); } /// @@ -138,8 +140,12 @@ public class TwoFactorLoginService : ITwoFactorLoginService /// public async Task DisableAsync(Guid userOrMemberKey, string providerName) { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - return await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey, providerName); + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + var result = await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey, providerName); + + scope.Complete(); + + return result; } /// @@ -156,9 +162,10 @@ public class TwoFactorLoginService : ITwoFactorLoginService /// public Task SaveAsync(TwoFactorLogin twoFactorLogin) { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + using ICoreScope scope = _scopeProvider.CreateCoreScope(); _twoFactorLoginRepository.Save(twoFactorLogin); + scope.Complete(); return Task.CompletedTask; } diff --git a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs index 328cf5ee5c..da1fbaf157 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs @@ -106,7 +106,7 @@ public class ScheduledPublishing : RecurringHostedServiceBase // but then what should be its "scope"? could we attach it to scopes? // - and we should definitively *not* have to flush it here (should be auto) using UmbracoContextReference contextReference = _umbracoContextFactory.EnsureUmbracoContext(); - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + using ICoreScope scope = _scopeProvider.CreateCoreScope(); /* We used to assume that there will never be two instances running concurrently where (IsMainDom && ServerRole == SchedulingPublisher) * However this is possible during an azure deployment slot swap for the SchedulingPublisher instance when trying to achieve zero downtime deployments. @@ -125,6 +125,8 @@ public class ScheduledPublishing : RecurringHostedServiceBase grouped.Count(), grouped.Key); } + + scope.Complete(); } finally { diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs index ecd9aaf329..ca8a4e0353 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs @@ -1,6 +1,8 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Scoping; using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; namespace Umbraco.Cms.Core.Logging.Viewer; @@ -10,6 +12,21 @@ public class LogViewerConfig : ILogViewerConfig private readonly ILogViewerQueryRepository _logViewerQueryRepository; private readonly IScopeProvider _scopeProvider; + [Obsolete("Use non-obsolete ctor. This will be removed in Umbraco 14.")] + public LogViewerConfig(ILogViewerQueryRepository logViewerQueryRepository, Umbraco.Cms.Core.Scoping.IScopeProvider scopeProvider) + : this(logViewerQueryRepository, StaticServiceProvider.Instance.GetRequiredService()) + { + + } + + //Temp ctor used by MSDI (Greedy) + [Obsolete("Use non-obsolete ctor. This will be removed in Umbraco 14.")] + public LogViewerConfig(ILogViewerQueryRepository logViewerQueryRepository, Umbraco.Cms.Core.Scoping.IScopeProvider coreScopeProvider, IScopeProvider scopeProvider) + : this(logViewerQueryRepository, scopeProvider) + { + + } + public LogViewerConfig(ILogViewerQueryRepository logViewerQueryRepository, IScopeProvider scopeProvider) { _logViewerQueryRepository = logViewerQueryRepository; @@ -26,9 +43,10 @@ public class LogViewerConfig : ILogViewerConfig public IReadOnlyList AddSavedSearch(string name, string query) { - using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + using IScope scope = _scopeProvider.CreateScope(); _logViewerQueryRepository.Save(new LogViewerQuery(name, query)); + scope.Complete(); return GetSavedSearches(); } @@ -37,7 +55,7 @@ public class LogViewerConfig : ILogViewerConfig public IReadOnlyList DeleteSavedSearch(string name) { - using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + using IScope scope = _scopeProvider.CreateScope(); ILogViewerQuery? item = _logViewerQueryRepository.GetByName(name); if (item is not null) @@ -46,6 +64,8 @@ public class LogViewerConfig : ILogViewerConfig } // Return the updated object - so we can instantly reset the entire array from the API response - return GetSavedSearches(); + IReadOnlyList result = GetSavedSearches(); + scope.Complete(); + return result; } } diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 934cefb0b8..4e4b43f509 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -96,7 +96,7 @@ public class MemberUserStore : UmbracoUserStore UpdateMemberAsync(ProfileModel model, MemberIdentityUser currentMember) { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + using ICoreScope scope = _scopeProvider.CreateCoreScope(); currentMember.Email = model.Email; currentMember.Name = model.Name; @@ -140,6 +140,7 @@ public class UmbProfileController : SurfaceController _memberService.Save(member); + scope.Complete(); return saveResult; } } diff --git a/src/Umbraco.Web.Website/Controllers/UmbRegisterController.cs b/src/Umbraco.Web.Website/Controllers/UmbRegisterController.cs index 493dd624d1..3fb2a966c4 100644 --- a/src/Umbraco.Web.Website/Controllers/UmbRegisterController.cs +++ b/src/Umbraco.Web.Website/Controllers/UmbRegisterController.cs @@ -118,7 +118,7 @@ public class UmbRegisterController : SurfaceController /// Result of registration operation. private async Task RegisterMemberAsync(RegisterModel model) { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + using ICoreScope scope = _scopeProvider.CreateCoreScope(); // U4-10762 Server error with "Register Member" snippet (Cannot save member with empty name) // If name field is empty, add the email address instead. @@ -160,6 +160,8 @@ public class UmbRegisterController : SurfaceController } } + scope.Complete(); + return identityResult; } } diff --git a/tests/Umbraco.Tests.Common/TestHelperBase.cs b/tests/Umbraco.Tests.Common/TestHelperBase.cs index ba7d29cd69..21c3c65b7d 100644 --- a/tests/Umbraco.Tests.Common/TestHelperBase.cs +++ b/tests/Umbraco.Tests.Common/TestHelperBase.cs @@ -7,6 +7,7 @@ using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using Moq; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; @@ -14,6 +15,8 @@ using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Diagnostics; +using Umbraco.Cms.Core.DistributedLocking; +using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Logging; @@ -24,6 +27,8 @@ using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Infrastructure.Serialization; using Umbraco.Cms.Tests.Common.TestHelpers; using Umbraco.Extensions; @@ -76,6 +81,61 @@ public abstract class TestHelperBase public IShortStringHelper ShortStringHelper { get; } = new DefaultShortStringHelper(new DefaultShortStringHelperConfig()); + public IScopeProvider ScopeProvider + { + get + { + var loggerFactory = NullLoggerFactory.Instance; + var fileSystems = new FileSystems( + loggerFactory, + Mock.Of(), + Mock.Of>(), + Mock.Of()); + var mediaFileManager = new MediaFileManager( + Mock.Of(), + Mock.Of(), + loggerFactory.CreateLogger(), + Mock.Of(), + Mock.Of(), + Options.Create(new ContentSettings())); + var databaseFactory = new Mock(); + var database = new Mock(); + var sqlContext = new Mock(); + + var lockingMechanism = new Mock(); + lockingMechanism.Setup(x => x.ReadLock(It.IsAny(), It.IsAny())) + .Returns(Mock.Of()); + lockingMechanism.Setup(x => x.WriteLock(It.IsAny(), It.IsAny())) + .Returns(Mock.Of()); + + var lockingMechanismFactory = new Mock(); + lockingMechanismFactory.Setup(x => x.DistributedLockingMechanism) + .Returns(lockingMechanism.Object); + + // Setup mock of database factory to return mock of database. + databaseFactory.Setup(x => x.CreateDatabase()).Returns(database.Object); + databaseFactory.Setup(x => x.SqlContext).Returns(sqlContext.Object); + + // Setup mock of database to return mock of sql SqlContext + database.Setup(x => x.SqlContext).Returns(sqlContext.Object); + + var syntaxProviderMock = new Mock(); + + // Setup mock of ISqlContext to return syntaxProviderMock + sqlContext.Setup(x => x.SqlSyntax).Returns(syntaxProviderMock.Object); + + return new ScopeProvider( + new AmbientScopeStack(), + new AmbientScopeContextStack(), + lockingMechanismFactory.Object, + databaseFactory.Object, + fileSystems, + new TestOptionsMonitor(new CoreDebugSettings()), + mediaFileManager, + loggerFactory, + Mock.Of()); + } + } public IJsonSerializer JsonSerializer { get; } = new JsonNetSerializer(); diff --git a/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs b/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs index 33b89f1738..24b15b5723 100644 --- a/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs +++ b/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs @@ -31,6 +31,7 @@ using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Mail; @@ -42,6 +43,7 @@ using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Extensions; using File = System.IO.File; using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; +using IScopeProvider = Umbraco.Cms.Infrastructure.Scoping.IScopeProvider; namespace Umbraco.Cms.Tests.UnitTests.TestHelpers; @@ -58,6 +60,8 @@ public static class TestHelper /// The assembly directory. public static string WorkingDirectory => s_testHelperInternal.WorkingDirectory; + public static IScopeProvider ScopeProvider => s_testHelperInternal.ScopeProvider; + public static ICoreScopeProvider CoreScopeProvider => s_testHelperInternal.ScopeProvider; public static IShortStringHelper ShortStringHelper => s_testHelperInternal.ShortStringHelper; public static IJsonSerializer JsonSerializer => s_testHelperInternal.JsonSerializer; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Logging/LogviewerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Logging/LogviewerTests.cs index 65307cd143..a27ca71058 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Logging/LogviewerTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Logging/LogviewerTests.cs @@ -48,7 +48,7 @@ public class LogviewerTests File.Copy(exampleLogfilePath, _newLogfilePath, true); var logger = Mock.Of>(); - var logViewerConfig = new LogViewerConfig(LogViewerQueryRepository, Mock.Of()); + var logViewerConfig = new LogViewerConfig(LogViewerQueryRepository, TestHelper.ScopeProvider); var logLevelLoader = Mock.Of(); _logViewer = new SerilogJsonLogViewer(logger, logViewerConfig, loggingConfiguration, logLevelLoader, Log.Logger); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs index f745c06ceb..10d4510902 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs @@ -17,6 +17,7 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.UnitTests.TestHelpers; using Umbraco.Cms.Web.Common.Security; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security; @@ -33,7 +34,7 @@ public class MemberManagerTests public MemberManager CreateSut() { - var scopeProvider = new Mock().Object; + var scopeProvider = TestHelper.ScopeProvider; _mockMemberService = new Mock(); var mapDefinitions = new List diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs index b631e2140f..a9a4d7a91c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs @@ -14,6 +14,7 @@ using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.UnitTests.TestHelpers; using Umbraco.Cms.Tests.UnitTests.Umbraco.Core.ShortStringHelper; using IScopeProvider = Umbraco.Cms.Infrastructure.Scoping.IScopeProvider; @@ -27,23 +28,12 @@ public class MemberUserStoreTests public MemberUserStore CreateSut() { _mockMemberService = new Mock(); - var mockScope = new Mock(); - var mockScopeProvider = new Mock(); - mockScopeProvider - .Setup(x => x.CreateScope( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(mockScope.Object); + var mockScopeProvider = TestHelper.ScopeProvider; return new MemberUserStore( _mockMemberService.Object, - new UmbracoMapper(new MapDefinitionCollection(() => new List()), mockScopeProvider.Object, NullLogger.Instance), - mockScopeProvider.Object, + new UmbracoMapper(new MapDefinitionCollection(() => new List()), mockScopeProvider, NullLogger.Instance), + mockScopeProvider, new IdentityErrorDescriber(), Mock.Of(), Mock.Of(),