From cd71e5666a8ef51ed351adac29f9b0fce8038ac3 Mon Sep 17 00:00:00 2001 From: Lotte Pitcher Date: Tue, 22 Apr 2025 05:51:08 +0100 Subject: [PATCH 01/11] Incorrect forum and security urls when raising issue (#19080) --- .github/ISSUE_TEMPLATE/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index ecf10b8854..0681900d88 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -4,11 +4,11 @@ contact_links: url: https://github.com/umbraco/Umbraco-CMS/discussions/new?category=features-and-ideas about: Start a new discussion when you have ideas or feature requests, eventually discussions can turn into plans - name: ⁉️ Support Question - url: https://our.umbraco.com + url: https://forum.umbraco.com about: This issue tracker is NOT meant for support questions. If you have a question, please join us on the forum. - name: 📖 Documentation Issue url: https://github.com/umbraco/UmbracoDocs/issues about: Documentation issues should be reported on the Umbraco documentation repository. - name: 🔐 Security Issue - url: https://umbraco.com/about-us/trust-center/security-and-umbraco/how-to-report-a-vulnerability-in-umbraco/ + url: https://umbraco.com/trust-center/security-and-umbraco/how-to-report-a-vulnerability-in-umbraco/ about: Discovered a Security Issue in Umbraco? From 15643135bace98282a25aa6fcff339406032fb4a Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Thu, 24 Apr 2025 11:25:58 +0100 Subject: [PATCH 02/11] Add 'ManifestWithDynamicConditions' to ManifestHeaderApp so Header Apps can be conditionally shown/loaded (#19124) --- .../extension-registry/extensions/header-app.extension.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extensions/header-app.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extensions/header-app.extension.ts index 58ac1f5d7d..b76d659278 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extensions/header-app.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extensions/header-app.extension.ts @@ -1,10 +1,10 @@ -import type { ManifestElement } from '@umbraco-cms/backoffice/extension-api'; +import type { ManifestElement, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; /** * Header apps are displayed in the top right corner of the backoffice * The two provided header apps are the search and the user menu */ -export interface ManifestHeaderApp extends ManifestElement { +export interface ManifestHeaderApp extends ManifestElement, ManifestWithDynamicConditions { type: 'headerApp'; //meta: MetaHeaderApp; } From 31c3f5ae0c627bd0b4d13377a74ef0e6051e43b4 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Wed, 23 Apr 2025 07:09:00 +0200 Subject: [PATCH 03/11] Remove fake null checks as they are no longer needed after merge into v16 (#19109) --- .../BlockListElementLevelVariationTests.Editing.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Editing.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Editing.cs index d7e8ffc5cb..c6f2dff383 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Editing.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Editing.cs @@ -555,8 +555,7 @@ internal partial class BlockListElementLevelVariationTests content = ContentService.GetById(content.Key); var savedBlocksValue = content?.Properties["blocks"]?.GetValue()?.ToString(); - Assert.NotNull(savedBlocksValue); - blockListValue = JsonSerializer.Deserialize(savedBlocksValue); + blockListValue = savedBlocksValue is null ? null : JsonSerializer.Deserialize(savedBlocksValue); // limited user access means invariant data is inaccessible since AllowEditInvariantFromNonDefault is disabled if (updateWithLimitedUserAccess) @@ -973,7 +972,7 @@ internal partial class BlockListElementLevelVariationTests } else { - Assert.AreEqual("null", savedBlocksValue); + Assert.IsNull(savedBlocksValue); } } From cf0f3f138004632ea0f0bae4d42f070b52f1da61 Mon Sep 17 00:00:00 2001 From: Mole Date: Thu, 24 Apr 2025 13:15:30 +0200 Subject: [PATCH 04/11] V15: Ensure elements cache is cleared on subscribers in load balanced scenarios (#19128) * Clear elementscache from cache refreshers * Add very simple test ensuring the elements cache is cleared --------- Co-authored-by: Kenn Jacobsen --- .../Implement/ContentCacheRefresher.cs | 49 ++++++++++++++++++ .../Implement/MediaCacheRefresher.cs | 39 ++++++++++++++ .../CacheRefreshingNotificationHandler.cs | 29 +---------- .../Cache/DistributedCacheRefresherTests.cs | 51 +++++++++++++++++++ 4 files changed, 141 insertions(+), 27 deletions(-) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/DistributedCacheRefresherTests.cs diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs index b286eccdb7..4741eedcc4 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -21,9 +23,11 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase()) + { + } + + public ContentCacheRefresher( + AppCaches appCaches, + IJsonSerializer serializer, + IIdKeyMap idKeyMap, + IDomainService domainService, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory, + IDocumentUrlService documentUrlService, + IDomainCacheService domainCacheService, + IDocumentNavigationQueryService documentNavigationQueryService, + IDocumentNavigationManagementService documentNavigationManagementService, + IContentService contentService, + IPublishStatusManagementService publishStatusManagementService, + IDocumentCacheService documentCacheService, + ICacheManager cacheManager) : base(appCaches, serializer, eventAggregator, factory) { _idKeyMap = idKeyMap; @@ -49,6 +86,11 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase(); AppCaches.RuntimeCache.ClearByKey(CacheKeys.ContentRecycleBinCacheKey); + // Ideally, we'd like to not have to clear the entire cache here. However, this was the existing behavior in NuCache. + // The reason for this is that we have no way to know which elements are affected by the changes or what their keys are. + // This is because currently published elements live exclusively in a JSON blob in the umbracoPropertyData table. + // This means that the only way to resolve these keys is to actually parse this data with a specific value converter, and for all cultures, which is not possible. + // If published elements become their own entities with relations, instead of just property data, we can revisit this. + _cacheManager.ElementsCache.Clear(); + var idsRemoved = new HashSet(); IAppPolicyCache isolatedCache = AppCaches.IsolatedCaches.GetOrCreate(); diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs index 284b248aba..7beeda73b6 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -18,7 +20,9 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase()) + { + } + + public MediaCacheRefresher( + AppCaches appCaches, + IJsonSerializer serializer, + IIdKeyMap idKeyMap, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory, + IMediaNavigationQueryService mediaNavigationQueryService, + IMediaNavigationManagementService mediaNavigationManagementService, + IMediaService mediaService, + IMediaCacheService mediaCacheService, + ICacheManager cacheManager) : base(appCaches, serializer, eventAggregator, factory) { _idKeyMap = idKeyMap; @@ -36,6 +65,9 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase mediaCache = AppCaches.IsolatedCaches.Get(); + // Ideally, we'd like to not have to clear the entire cache here. However, this was the existing behavior in NuCache. + // The reason for this is that we have no way to know which elements are affected by the changes or what their keys are. + // This is because currently published elements live exclusively in a JSON blob in the umbracoPropertyData table. + // This means that the only way to resolve these keys is to actually parse this data with a specific value converter, and for all cultures, which is not possible. + // If published elements become their own entities with relations, instead of just property data, we can revisit this. + _cacheManager.ElementsCache.Clear(); + foreach (JsonPayload payload in payloads) { if (payload.ChangeTypes == TreeChangeTypes.Remove) diff --git a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs index 6ff5a5fe1e..1283ce90ec 100644 --- a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs +++ b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs @@ -25,65 +25,40 @@ internal sealed class CacheRefreshingNotificationHandler : { private readonly IDocumentCacheService _documentCacheService; private readonly IMediaCacheService _mediaCacheService; - private readonly IElementsCache _elementsCache; - private readonly IRelationService _relationService; private readonly IPublishedContentTypeCache _publishedContentTypeCache; public CacheRefreshingNotificationHandler( IDocumentCacheService documentCacheService, IMediaCacheService mediaCacheService, - IElementsCache elementsCache, - IRelationService relationService, IPublishedContentTypeCache publishedContentTypeCache) { _documentCacheService = documentCacheService; _mediaCacheService = mediaCacheService; - _elementsCache = elementsCache; - _relationService = relationService; _publishedContentTypeCache = publishedContentTypeCache; } public async Task HandleAsync(ContentRefreshNotification notification, CancellationToken cancellationToken) - { - ClearElementsCache(); - - await _documentCacheService.RefreshContentAsync(notification.Entity); - } + => await _documentCacheService.RefreshContentAsync(notification.Entity); public async Task HandleAsync(ContentDeletedNotification notification, CancellationToken cancellationToken) { foreach (IContent deletedEntity in notification.DeletedEntities) { - ClearElementsCache(); await _documentCacheService.DeleteItemAsync(deletedEntity); } } public async Task HandleAsync(MediaRefreshNotification notification, CancellationToken cancellationToken) - { - ClearElementsCache(); - await _mediaCacheService.RefreshMediaAsync(notification.Entity); - } + => await _mediaCacheService.RefreshMediaAsync(notification.Entity); public async Task HandleAsync(MediaDeletedNotification notification, CancellationToken cancellationToken) { foreach (IMedia deletedEntity in notification.DeletedEntities) { - ClearElementsCache(); await _mediaCacheService.DeleteItemAsync(deletedEntity); } } - private void ClearElementsCache() - { - // Ideally we'd like to not have to clear the entire cache here. However, this was the existing behavior in NuCache. - // The reason for this is that we have no way to know which elements are affected by the changes. or what their keys are. - // This is because currently published elements lives exclusively in a JSON blob in the umbracoPropertyData table. - // This means that the only way to resolve these keys are to actually parse this data with a specific value converter, and for all cultures, which is not feasible. - // If published elements become their own entities with relations, instead of just property data, we can revisit this, - _elementsCache.Clear(); - } - public Task HandleAsync(ContentTypeRefreshedNotification notification, CancellationToken cancellationToken) { const ContentTypeChangeTypes types // only for those that have been refreshed diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/DistributedCacheRefresherTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/DistributedCacheRefresherTests.cs new file mode 100644 index 0000000000..0b9fd4755d --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/DistributedCacheRefresherTests.cs @@ -0,0 +1,51 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Infrastructure.HybridCache; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Cache; + +// We need to make sure that it's the distributed cache refreshers that refresh the elements cache +// see: https://github.com/umbraco/Umbraco-CMS/issues/18467 +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] +internal sealed class DistributedCacheRefresherTests : UmbracoIntegrationTest +{ + private IElementsCache ElementsCache => GetRequiredService(); + + private ContentCacheRefresher ContentCacheRefresher => GetRequiredService(); + + private MediaCacheRefresher MediaCacheRefresher => GetRequiredService(); + + [Test] + public void DistributedContentCacheRefresherClearsElementsCache() + { + var cacheKey = "test"; + PopulateCache("test"); + + ContentCacheRefresher.Refresh([new ContentCacheRefresher.JsonPayload()]); + + Assert.IsNull(ElementsCache.Get(cacheKey)); + } + + [Test] + public void DistributedMediaCacheRefresherClearsElementsCache() + { + var cacheKey = "test"; + PopulateCache("test"); + + MediaCacheRefresher.Refresh([new MediaCacheRefresher.JsonPayload(1, Guid.NewGuid(), TreeChangeTypes.RefreshAll)]); + + Assert.IsNull(ElementsCache.Get(cacheKey)); + } + + private void PopulateCache(string key) + { + ElementsCache.Get(key, () => new object()); + + // Just making sure something is in the cache now. + Assert.IsNotNull(ElementsCache.Get(key)); + } +} From 31ee35e7210148565ab75e227c7b5e79fc6deb71 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 24 Apr 2025 13:18:29 +0200 Subject: [PATCH 05/11] Fixed error with reflection on integration test configure builder attributes, so integration tests can be created outside of the Umbraco integration test project (#19077) * Fixed error with reflection on integration test configure builder attributes, so integration tests can be created outside of the Umbraco integration test project. * Fix nullability --------- Co-authored-by: mole --- .../Testing/UmbracoIntegrationTest.cs | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 585f6b268c..ca5654eadd 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -189,25 +189,48 @@ public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase private void ExecuteBuilderAttributes(IUmbracoBuilder builder) { - // todo better errors + Type? testClassType = GetTestClassType() + ?? throw new Exception($"Could not find test class for {TestContext.CurrentContext.Test.FullName} in order to execute builder attributes."); - // execute builder attributes defined on method - foreach (ConfigureBuilderAttribute builderAttribute in Type.GetType(TestContext.CurrentContext.Test.ClassName) - .GetMethods().First(m => m.Name == TestContext.CurrentContext.Test.MethodName) - .GetCustomAttributes(typeof(ConfigureBuilderAttribute), true)) + // Execute builder attributes defined on method. + foreach (ConfigureBuilderAttribute builderAttribute in GetConfigureBuilderAttributes(testClassType)) { builderAttribute.Execute(builder); } - // execute builder attributes defined on method with param value passtrough from testcase - foreach (ConfigureBuilderTestCaseAttribute builderAttribute in Type.GetType(TestContext.CurrentContext.Test.ClassName) - .GetMethods().First(m => m.Name == TestContext.CurrentContext.Test.MethodName) - .GetCustomAttributes(typeof(ConfigureBuilderTestCaseAttribute), true)) + // Execute builder attributes defined on method with param value pass through from test case. + foreach (ConfigureBuilderTestCaseAttribute builderAttribute in GetConfigureBuilderAttributes(testClassType)) { builderAttribute.Execute(builder); } } + private static Type? GetTestClassType() + { + string testClassName = TestContext.CurrentContext.Test.ClassName; + + // Try resolving the type name directly (which will work for tests in this assembly). + Type testClass = Type.GetType(testClassName); + if (testClass is not null) + { + return testClass; + } + + // Try scanning the loaded assemblies to see if we can find the class by full name. This will be necessary + // for integration test projects using the base classess provided by Umbraco. + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + return assemblies + .SelectMany(a => a.GetTypes().Where(t => t.FullName == testClassName)) + .FirstOrDefault(); + } + + private static IEnumerable GetConfigureBuilderAttributes(Type testClassType) + where TAttribute : Attribute => + testClassType + .GetMethods().First(m => m.Name == TestContext.CurrentContext.Test.MethodName) + .GetCustomAttributes(typeof(TAttribute), true) + .Cast(); + /// /// Hook for altering UmbracoBuilder setup /// From 83e580c3a719d3ae25ae59c56bf13868df5c72c6 Mon Sep 17 00:00:00 2001 From: mole Date: Fri, 25 Apr 2025 13:31:34 +0200 Subject: [PATCH 06/11] Fix nuget publish stage --- build/azure-pipelines.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 767c0848cf..a33ee744f7 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -829,6 +829,8 @@ stages: condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.myGetDeploy}})) jobs: - job: + pool: + vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available. displayName: Push to pre-release feed steps: - checkout: none @@ -890,6 +892,8 @@ stages: condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.nuGetDeploy}})) jobs: - job: + pool: + vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available. displayName: Push to NuGet steps: - checkout: none From 3b6e4a96f141b6fa3d053ece92543cd892d6b3b9 Mon Sep 17 00:00:00 2001 From: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> Date: Sat, 26 Apr 2025 09:59:03 +0700 Subject: [PATCH 07/11] V15 QA Added acceptance tests for bulk trash dialog (#19125) * Added tests for bulk trash content dialog * Updated tests for trash content dialog * Added tests for trash and bulk trash media dialog * Moved trash content tests into a folder * Bumped version * Make trash tests run in the pipeline * Make trash tests run in the pipeline * Fixed comments * Reverted npm command --- .../package-lock.json | 20 ++-- .../Umbraco.Tests.AcceptanceTest/package.json | 4 +- .../TrashContent/BulkTrashContent.spec.ts | 93 +++++++++++++++++++ .../{ => TrashContent}/TrashContent.spec.ts | 2 +- .../tests/DefaultConfig/Media/Media.spec.ts | 78 ++++++++++++++++ 5 files changed, 183 insertions(+), 14 deletions(-) create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent/BulkTrashContent.spec.ts rename tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/{ => TrashContent}/TrashContent.spec.ts (99%) diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index b6ee78da3a..635fb500f2 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -7,8 +7,8 @@ "name": "acceptancetest", "hasInstallScript": true, "dependencies": { - "@umbraco/json-models-builders": "^2.0.31", - "@umbraco/playwright-testhelpers": "^15.0.44", + "@umbraco/json-models-builders": "^2.0.33", + "@umbraco/playwright-testhelpers": "^15.0.47", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" @@ -58,21 +58,19 @@ } }, "node_modules/@umbraco/json-models-builders": { - "version": "2.0.32", - "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.32.tgz", - "integrity": "sha512-Aw7yBu8ePNxdjS7Q61j5KPFsiOS+IGCYxBX0H4KWbjXTdvL/PsB98KiqbDHHKFnp0fF1b2ffwJAI6jmvnxPBzg==", - "license": "MIT", + "version": "2.0.33", + "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.33.tgz", + "integrity": "sha512-FAQxQIHoY6PGxWuodp4LSQxNufnOiqnaRNmtG8Ejn01r9lJJdA27CJKJ0bCs8U0W3cdN+Z+j7jBLd8H025/THw==", "dependencies": { "camelize": "^1.0.1" } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "15.0.44", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.44.tgz", - "integrity": "sha512-rbStBJG0bdVfs9pzEjAhArhcPoQbuvrUpvDJLbEMBcg54lRFH0I5nzcAimdZaY4UWh2r0i9ANfjv9E25NJxRUg==", - "license": "MIT", + "version": "15.0.47", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.47.tgz", + "integrity": "sha512-rYr3IU1O/mcOw74zHxXyqa7eRkxci2G6yMgWmTDKdYrB1jPFrElWvn5GQVVP6r9znH4z1VkzPT0rWmuPI8rP/w==", "dependencies": { - "@umbraco/json-models-builders": "2.0.32", + "@umbraco/json-models-builders": "2.0.33", "node-fetch": "^2.6.7" } }, diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 524acec6eb..beffdfba62 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -20,8 +20,8 @@ "typescript": "^4.8.3" }, "dependencies": { - "@umbraco/json-models-builders": "^2.0.31", - "@umbraco/playwright-testhelpers": "^15.0.44", + "@umbraco/json-models-builders": "^2.0.33", + "@umbraco/playwright-testhelpers": "^15.0.47", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent/BulkTrashContent.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent/BulkTrashContent.spec.ts new file mode 100644 index 0000000000..957c330e65 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent/BulkTrashContent.spec.ts @@ -0,0 +1,93 @@ +import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +let collectionId = ''; +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const childDocumentTypeName = 'TestChildDocumentType'; +const firstChildContentName = 'First Child Content'; +const secondChildContentName = 'Second Child Content'; +const collectionDataTypeName = 'List View - Content'; +const referenceHeadline = ConstantHelper.trashDeleteDialogMessage.bulkReferenceHeadline; +const documentPickerName = ['TestPicker', 'DocumentTypeForPicker']; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + const collectionDataTypeData = await umbracoApi.dataType.getByName(collectionDataTypeName); + collectionId = collectionDataTypeData.id; +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.documentType.ensureNameNotExists(childDocumentTypeName); + await umbracoApi.document.emptyRecycleBin(); +}); + +test('can bulk trash content nodes without a relation', async ({umbracoApi, umbracoUi}) => { + // Arrange + const childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNodeAndCollectionId(documentTypeName, childDocumentTypeId, collectionId); + const contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoApi.document.createDefaultDocumentWithParent(firstChildContentName, childDocumentTypeId, contentId); + await umbracoApi.document.createDefaultDocumentWithParent(secondChildContentName, childDocumentTypeId, contentId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.selectContentWithNameInListView(firstChildContentName); + await umbracoUi.content.selectContentWithNameInListView(secondChildContentName); + await umbracoUi.content.clickTrashSelectedListItems(); + // Verify the references list not displayed + await umbracoUi.content.isReferenceHeadlineVisible(false); + await umbracoUi.content.clickConfirmTrashButton(); + + // // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(firstChildContentName)).toBeFalsy(); + expect(await umbracoApi.document.doesNameExist(secondChildContentName)).toBeFalsy(); + await umbracoUi.content.isItemVisibleInRecycleBin(firstChildContentName); + await umbracoUi.content.isItemVisibleInRecycleBin(secondChildContentName); + expect(await umbracoApi.document.doesItemExistInRecycleBin(firstChildContentName)).toBeTruthy(); + expect(await umbracoApi.document.doesItemExistInRecycleBin(secondChildContentName)).toBeTruthy(); +}); + +test('can bulk trash content nodes with a relation', async ({umbracoApi, umbracoUi}) => { + // Arrange + const childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNodeAndCollectionId(documentTypeName, childDocumentTypeId, collectionId); + const contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoApi.document.publish(contentId); + const firstChildContentId = await umbracoApi.document.createDefaultDocumentWithParent(firstChildContentName, childDocumentTypeId, contentId); + await umbracoApi.document.publish(firstChildContentId); + await umbracoApi.document.createDefaultDocumentWithParent(secondChildContentName, childDocumentTypeId, contentId); + // Create a document that has a document picker with firstChildContentName + await umbracoApi.document.createDefaultDocumentWithOneDocumentLink(documentPickerName[0], firstChildContentName, firstChildContentId, documentPickerName[1]); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.selectContentWithNameInListView(firstChildContentName); + await umbracoUi.content.selectContentWithNameInListView(secondChildContentName); + await umbracoUi.content.clickTrashSelectedListItems(); + // Verify the references list + await umbracoUi.content.doesReferenceHeadlineHaveText(referenceHeadline); + await umbracoUi.content.doesReferenceItemsHaveCount(1); + await umbracoUi.content.isReferenceItemNameVisible(firstChildContentName); + await umbracoUi.content.clickConfirmTrashButton(); + + // // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(firstChildContentName)).toBeFalsy(); + expect(await umbracoApi.document.doesNameExist(secondChildContentName)).toBeFalsy(); + await umbracoUi.content.isItemVisibleInRecycleBin(firstChildContentName); + await umbracoUi.content.isItemVisibleInRecycleBin(secondChildContentName); + expect(await umbracoApi.document.doesItemExistInRecycleBin(firstChildContentName)).toBeTruthy(); + expect(await umbracoApi.document.doesItemExistInRecycleBin(secondChildContentName)).toBeTruthy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(documentPickerName[1]); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent/TrashContent.spec.ts similarity index 99% rename from tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent.spec.ts rename to tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent/TrashContent.spec.ts index c008ea38ba..4214ff691a 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent/TrashContent.spec.ts @@ -6,7 +6,7 @@ const contentName = 'TestContent'; const documentTypeName = 'TestDocumentTypeForContent'; const dataTypeName = 'Textstring'; const contentText = 'This is test content text'; -const referenceHeadline = 'The following items depend on this'; +const referenceHeadline = ConstantHelper.trashDeleteDialogMessage.referenceHeadline; const documentPickerName = ['TestPicker', 'DocumentTypeForPicker']; test.beforeEach(async ({umbracoApi}) => { diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts index 8e7f878166..8eaedb8313 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts @@ -174,6 +174,8 @@ test('can trash a media item', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.media.clickActionsMenuForName(mediaFileName); await umbracoUi.media.clickTrashButton(); + // Verify the references list not displayed + await umbracoUi.content.isReferenceHeadlineVisible(false); await umbracoUi.media.clickConfirmTrashButton(); // Assert @@ -245,3 +247,79 @@ test('can empty the recycle bin', async ({umbracoApi, umbracoUi}) => { expect(await umbracoApi.media.doesNameExist(mediaFileName)).toBeFalsy(); expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(mediaFileName)).toBeFalsy(); }); + +test('can trash a media node with a relation', async ({umbracoApi, umbracoUi}) => { + // Arrange + const documentPickerName = ['TestPicker', 'DocumentTypeForPicker']; + await umbracoApi.media.emptyRecycleBin(); + await umbracoApi.media.createDefaultMediaFile(mediaFileName); + await umbracoApi.media.doesNameExist(mediaFileName); + // Create a document that have media picker is firstMediaFileName + await umbracoApi.document.createDefaultDocumentWithOneMediaLink(documentPickerName[0], mediaFileName, documentPickerName[1]); + await umbracoUi.media.goToSection(ConstantHelper.sections.media); + + // Act + await umbracoUi.media.clickActionsMenuForName(mediaFileName); + await umbracoUi.media.clickTrashButton(); + // Verify the references list + await umbracoUi.media.doesReferenceHeadlineHaveText(ConstantHelper.trashDeleteDialogMessage.referenceHeadline); + await umbracoUi.media.doesReferenceItemsHaveCount(1); + await umbracoUi.media.isReferenceItemNameVisible(documentPickerName[0]); + await umbracoUi.media.clickConfirmTrashButton(); + + // Assert + await umbracoUi.media.doesSuccessNotificationHaveText(NotificationConstantHelper.success.movedToRecycleBin); + await umbracoUi.media.isItemVisibleInRecycleBin(mediaFileName); + expect(await umbracoApi.media.doesNameExist(mediaFileName)).toBeFalsy(); + expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(mediaFileName)).toBeTruthy(); + + // Clean + await umbracoApi.media.emptyRecycleBin(); + await umbracoApi.document.ensureNameNotExists(documentPickerName[0]); + await umbracoApi.documentType.ensureNameNotExists(documentPickerName[1]); +}); + +test('can bulk trash media nodes with a relation', async ({umbracoApi, umbracoUi}) => { + // Arrange + const firstMediaFileName = 'FirstMediaFile'; + const secondMediaFileName = 'SecondMediaFile'; + const documentPickerName1 = ['TestPicker1', 'DocumentTypeForPicker1']; + const documentPickerName2 = ['TestPicker2', 'DocumentTypeForPicker2']; + await umbracoApi.media.emptyRecycleBin(); + await umbracoApi.media.createDefaultMediaFile(firstMediaFileName); + await umbracoApi.media.createDefaultMediaFile(secondMediaFileName); + // Create a document that has a media picker with firstMediaFileName + await umbracoApi.document.createDefaultDocumentWithOneMediaLink(documentPickerName1[0], firstMediaFileName, documentPickerName1[1]); + // Create a document that has a media picker with secondMediaFileName + await umbracoApi.document.createDefaultDocumentWithOneMediaLink(documentPickerName2[0], secondMediaFileName, documentPickerName2[1]); + + // Act + await umbracoUi.media.goToSection(ConstantHelper.sections.media); + await umbracoUi.media.selectMediaWithName(firstMediaFileName); + await umbracoUi.media.selectMediaWithName(secondMediaFileName); + await umbracoUi.media.clickBulkTrashButton(); + // Verify the references list + await umbracoUi.media.doesReferenceHeadlineHaveText(ConstantHelper.trashDeleteDialogMessage.bulkReferenceHeadline); + await umbracoUi.media.doesReferenceItemsHaveCount(2); + await umbracoUi.media.isReferenceItemNameVisible(firstMediaFileName); + await umbracoUi.media.isReferenceItemNameVisible(secondMediaFileName); + await umbracoUi.media.clickConfirmTrashButton(); + + // Assert + await umbracoUi.media.isSuccessNotificationVisible(); + expect(await umbracoApi.media.doesNameExist(firstMediaFileName)).toBeFalsy(); + expect(await umbracoApi.media.doesNameExist(secondMediaFileName)).toBeFalsy(); + expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(firstMediaFileName)).toBeTruthy(); + expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(secondMediaFileName)).toBeTruthy(); + await umbracoUi.media.isItemVisibleInRecycleBin(firstMediaFileName); + await umbracoUi.media.isItemVisibleInRecycleBin(secondMediaFileName, true, false); + + // Clean + await umbracoApi.media.ensureNameNotExists(firstMediaFileName); + await umbracoApi.media.ensureNameNotExists(secondMediaFileName); + await umbracoApi.document.ensureNameNotExists(documentPickerName1[0]); + await umbracoApi.documentType.ensureNameNotExists(documentPickerName1[1]); + await umbracoApi.document.ensureNameNotExists(documentPickerName2[0]); + await umbracoApi.documentType.ensureNameNotExists(documentPickerName2[1]); + await umbracoApi.media.emptyRecycleBin(); +}); \ No newline at end of file From 3579c4ac9e4e618e07393cbc1a79924242cf5e36 Mon Sep 17 00:00:00 2001 From: Lotte Pitcher Date: Sat, 19 Apr 2025 22:20:45 +0100 Subject: [PATCH 08/11] readme shield for forum --- .github/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/README.md b/.github/README.md index f4e3f76009..63b908daed 100644 --- a/.github/README.md +++ b/.github/README.md @@ -4,8 +4,8 @@ [![NuGet Version](https://img.shields.io/nuget/v/Umbraco.Cms)](https://www.nuget.org/packages/Umbraco.Cms) [![Build status](https://img.shields.io/azure-devops/build/umbraco/Umbraco%2520Cms/301?logo=azurepipelines&label=Azure%20Pipelines)](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=301) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) +[![Forum](https://img.shields.io/badge/help-forum-blue)](https://forum.umbraco.com) [![Chat about Umbraco on Discord](https://img.shields.io/discord/869656431308189746?logo=discord&logoColor=fff)](https://discord.gg/umbraco) -[![Read what's going on in the Umbraco Discord chat now](https://img.shields.io/badge/read-discord-blue)](https://discord-chats.umbraco.com) ![Mastodon Follow](https://img.shields.io/mastodon/follow/110661369750014952?domain=https%3A%2F%2Fumbracocommunity.social) From e932fa5404f81fab812c4177d8a07b836d1a1380 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 28 Apr 2025 14:10:43 +0200 Subject: [PATCH 09/11] Webhook log authorization and file system path checks (#19177) * Add authorization for webhooks to item and log endpoints. * Remove full path details from exception when requesting a path outside of the physical file system's root. * Added missing usings. * Revert changes to the webhook items API --------- Co-authored-by: kjac --- .../Controllers/Webhook/Logs/WebhookLogControllerBase.cs | 3 +++ src/Umbraco.Core/IO/PhysicalFileSystem.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Webhook/Logs/WebhookLogControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Webhook/Logs/WebhookLogControllerBase.cs index 6581335165..020aa6ef24 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Webhook/Logs/WebhookLogControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Webhook/Logs/WebhookLogControllerBase.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Factories; @@ -5,11 +6,13 @@ using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Api.Management.ViewModels.Webhook.Logs; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.Webhook.Logs; [VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Webhook}")] [ApiExplorerSettings(GroupName = "Webhook")] +[Authorize(Policy = AuthorizationPolicies.TreeAccessWebhooks)] public class WebhookLogControllerBase : ManagementApiControllerBase { protected PagedViewModel CreatePagedWebhookLogResponseModel(PagedModel logs, IWebhookPresentationFactory webhookPresentationFactory) diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs index f2ddb74f0b..32f0d0fdab 100644 --- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs +++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs @@ -358,7 +358,7 @@ namespace Umbraco.Cms.Core.IO // nothing prevents us to reach the file, security-wise, yet it is outside // this filesystem's root - throw - throw new UnauthorizedAccessException($"File original: [{originalPath}] full: [{path}] is outside this filesystem's root."); + throw new UnauthorizedAccessException($"Requested path {originalPath} is outside this filesystem's root."); } /// From dfc6ead6d5b1463165a9334ee22c7b3839b477b0 Mon Sep 17 00:00:00 2001 From: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:39:56 +0700 Subject: [PATCH 10/11] V15 Added acceptance tests for tiptap statusbar (#19131) * Updated tests for tiptap RTE * Moved tests for titptap toolbar to another class * Added tests for titptap toolbar * Added tests for tiptap statusbar * Bumped version * Make tiptap tests run in the pipeline * Bumped version * Reverted npm command --- .../package-lock.json | 8 +- .../Umbraco.Tests.AcceptanceTest/package.json | 2 +- .../RichTextEditor/ContentWithTiptap.spec.ts | 132 ------------ .../RichTextEditor/TiptapToolbar.spec.ts | 190 ++++++++++++++++++ .../DataType/RichTextEditor.spec.ts | 4 +- .../DefaultConfig/DataType/Tiptap.spec.ts | 38 ++++ 6 files changed, 235 insertions(+), 139 deletions(-) create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/TiptapToolbar.spec.ts diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 635fb500f2..1353eb665a 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -8,7 +8,7 @@ "hasInstallScript": true, "dependencies": { "@umbraco/json-models-builders": "^2.0.33", - "@umbraco/playwright-testhelpers": "^15.0.47", + "@umbraco/playwright-testhelpers": "^15.0.49", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" @@ -66,9 +66,9 @@ } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "15.0.47", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.47.tgz", - "integrity": "sha512-rYr3IU1O/mcOw74zHxXyqa7eRkxci2G6yMgWmTDKdYrB1jPFrElWvn5GQVVP6r9znH4z1VkzPT0rWmuPI8rP/w==", + "version": "15.0.49", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.49.tgz", + "integrity": "sha512-1At/e057u6rB3T3iH8tR6SLXnYRZJsCVjmm8jm+6sftJDvgB0Q5kXKaSDyLTU6wVuLALiDNUuNuJ86FgOOdUJw==", "dependencies": { "@umbraco/json-models-builders": "2.0.33", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index beffdfba62..98d612ca81 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^2.0.33", - "@umbraco/playwright-testhelpers": "^15.0.47", + "@umbraco/playwright-testhelpers": "^15.0.49", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/ContentWithTiptap.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/ContentWithTiptap.spec.ts index cbac5c752e..8652b59137 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/ContentWithTiptap.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/ContentWithTiptap.spec.ts @@ -84,136 +84,4 @@ test('can publish content with RTE Tiptap property editor', async ({umbracoApi, const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.variants[0].state).toBe(expectedState); expect(contentData.values[0].value.markup).toEqual('

' + inputText + '

'); -}); - -test.fixme('can add a media in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { - // Arrange - const iconTitle = 'Media Picker'; - const imageName = 'Test Image For Content'; - await umbracoApi.media.ensureNameNotExists(imageName); - await umbracoApi.media.createDefaultMediaWithImage(imageName); - const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); - await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); - await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - - // Act - await umbracoUi.content.goToContentWithName(contentName); - await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); - // fix this - await umbracoUi.content.selectMediaWithName(imageName); - await umbracoUi.content.clickChooseModalButton(); - await umbracoUi.content.clickMediaCaptionAltTextModalSubmitButton(); - await umbracoUi.content.clickSaveButton(); - - // Assert - await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); - expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); - const contentData = await umbracoApi.document.getByName(contentName); - expect(contentData.values[0].value.markup).toContain(' { - // Arrange - const iconTitle = 'Embed'; - const videoURL = 'https://www.youtube.com/watch?v=Yu29dE-0OoI'; - const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); - await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); - await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - - // Act - await umbracoUi.content.goToContentWithName(contentName); - await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); - await umbracoUi.content.enterEmbeddedURL(videoURL); - await umbracoUi.content.clickEmbeddedRetrieveButton(); - await umbracoUi.content.waitForEmbeddedPreviewVisible(); - await umbracoUi.content.clickEmbeddedMediaModalConfirmButton(); - await umbracoUi.content.clickSaveButton(); - - // Assert - await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); - expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); - const contentData = await umbracoApi.document.getByName(contentName); - expect(contentData.values[0].value.markup).toContain('data-embed-url'); - expect(contentData.values[0].value.markup).toContain(videoURL); -}); - -test('cannot submit an empty link in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { - // Arrange - const iconTitle = 'Link'; - const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); - await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); - await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - - // Act - await umbracoUi.content.goToContentWithName(contentName); - await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); - await umbracoUi.content.clickManualLinkButton(); - await umbracoUi.content.enterLink(''); - await umbracoUi.content.enterAnchorOrQuerystring(''); - await umbracoUi.content.enterLinkTitle(''); - await umbracoUi.content.clickAddButton(); - - // Assert - await umbracoUi.content.isTextWithMessageVisible(ConstantHelper.validationMessages.emptyLinkPicker); -}); - -// TODO: Remove skip when the front-end ready. Currently it still accept the empty link with an anchor or querystring -// Issue link: https://github.com/umbraco/Umbraco-CMS/issues/17411 -test.skip('cannot submit an empty URL with an anchor or querystring in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { - // Arrange - const iconTitle = 'Link'; - const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); - await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); - await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - - // Act - await umbracoUi.content.goToContentWithName(contentName); - await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); - await umbracoUi.content.clickManualLinkButton(); - await umbracoUi.content.enterLink(''); - await umbracoUi.content.enterAnchorOrQuerystring('#value'); - await umbracoUi.content.clickAddButton(); - - // Assert - await umbracoUi.content.isTextWithMessageVisible(ConstantHelper.validationMessages.emptyLinkPicker); -}); - -// TODO: Remove skip when the front-end ready. Currently it is impossible to link to unpublished document -// Issue link: https://github.com/umbraco/Umbraco-CMS/issues/17974 -test.skip('can insert a link to an unpublished document in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { - // Arrange - const iconTitle = 'Link'; - const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); - await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); - // Create a document to link - const documentTypeForLinkedDocumentName = 'TestDocumentType'; - const documentTypeForLinkedDocumentId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeForLinkedDocumentName); - const linkedDocumentName = 'LinkedDocument'; - await umbracoApi.document.createDefaultDocument(linkedDocumentName, documentTypeForLinkedDocumentId); - await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - - // Act - await umbracoUi.content.goToContentWithName(contentName); - await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); - await umbracoUi.content.clickDocumentLinkButton(); - await umbracoUi.content.selectLinkByName(linkedDocumentName); - await umbracoUi.content.clickButtonWithName('Choose'); - await umbracoUi.content.clickAddButton(); - await umbracoUi.content.clickSaveButton(); - - // Assert - await umbracoUi.content.isSuccessNotificationVisible(); - - // Clean - await umbracoApi.documentType.ensureNameNotExists(documentTypeForLinkedDocumentName); - await umbracoApi.document.ensureNameNotExists(linkedDocumentName); }); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/TiptapToolbar.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/TiptapToolbar.spec.ts new file mode 100644 index 0000000000..3f4cf6d572 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/TiptapToolbar.spec.ts @@ -0,0 +1,190 @@ +import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const customDataTypeName = 'Test RTE Tiptap'; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + +test('can add a media in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const iconTitle = 'Media Picker'; + const imageName = 'Test Image For Content'; + await umbracoApi.media.ensureNameNotExists(imageName); + await umbracoApi.media.createDefaultMediaWithImage(imageName); + const customDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customDataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); + await umbracoUi.content.selectMediaWithName(imageName); + await umbracoUi.content.clickChooseModalButton(); + await umbracoUi.content.clickMediaCaptionAltTextModalSubmitButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].value.markup).toContain(' { + // Arrange + const iconTitle = 'Embed'; + const videoURL = 'https://www.youtube.com/watch?v=Yu29dE-0OoI'; + const customDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customDataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); + await umbracoUi.content.enterEmbeddedURL(videoURL); + await umbracoUi.content.clickEmbeddedRetrieveButton(); + await umbracoUi.content.waitForEmbeddedPreviewVisible(); + await umbracoUi.content.clickEmbeddedMediaModalConfirmButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].value.markup).toContain('data-embed-url'); + expect(contentData.values[0].value.markup).toContain(videoURL); +}); + +test('cannot submit an empty link in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const iconTitle = 'Link'; + const customDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customDataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); + await umbracoUi.content.clickManualLinkButton(); + await umbracoUi.content.enterLink(''); + await umbracoUi.content.enterAnchorOrQuerystring(''); + await umbracoUi.content.enterLinkTitle(''); + await umbracoUi.content.clickAddButton(); + + // Assert + await umbracoUi.content.isTextWithMessageVisible(ConstantHelper.validationMessages.emptyLinkPicker); +}); + +// TODO: Remove skip when the front-end ready. Currently it still accept the empty link with an anchor or querystring +// Issue link: https://github.com/umbraco/Umbraco-CMS/issues/17411 +test.skip('cannot submit an empty URL with an anchor or querystring in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const iconTitle = 'Link'; + const customDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customDataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); + await umbracoUi.content.clickManualLinkButton(); + await umbracoUi.content.enterLink(''); + await umbracoUi.content.enterAnchorOrQuerystring('#value'); + await umbracoUi.content.clickAddButton(); + + // Assert + await umbracoUi.content.isTextWithMessageVisible(ConstantHelper.validationMessages.emptyLinkPicker); +}); + +// TODO: Remove skip when the front-end ready. Currently it is impossible to link to unpublished document +// Issue link: https://github.com/umbraco/Umbraco-CMS/issues/17974 +test.skip('can insert a link to an unpublished document in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const iconTitle = 'Link'; + const customDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customDataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + // Create a document to link + const documentTypeForLinkedDocumentName = 'TestDocumentType'; + const documentTypeForLinkedDocumentId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeForLinkedDocumentName); + const linkedDocumentName = 'LinkedDocument'; + await umbracoApi.document.createDefaultDocument(linkedDocumentName, documentTypeForLinkedDocumentId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); + await umbracoUi.content.clickDocumentLinkButton(); + await umbracoUi.content.selectLinkByName(linkedDocumentName); + await umbracoUi.content.clickButtonWithName('Choose'); + await umbracoUi.content.clickAddButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(documentTypeForLinkedDocumentName); + await umbracoApi.document.ensureNameNotExists(linkedDocumentName); +}); + +test('can view word count', async ({umbracoApi, umbracoUi}) => { + // Arrange + const inputText = 'Test Tiptap here!!!'; + const expectedWordCount = 3; + const customDataTypeId = await umbracoApi.dataType.createTiptapDataTypeWithWordCountStatusbar(customDataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.enterRTETipTapEditor(inputText); + + // Assert + await umbracoUi.content.doesTiptapHaveWordCount(expectedWordCount); +}); + +test('can view element path', async ({umbracoApi, umbracoUi}) => { + // Arrange + const inputText = 'This is Tiptap test'; + const expectedElementPath = 'p'; + const customDataTypeId = await umbracoApi.dataType.createTiptapDataTypeWithElementPathStatusbar(customDataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.enterRTETipTapEditor(inputText); + + // Assert + await umbracoUi.content.doesElementPathHaveText(expectedElementPath); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/RichTextEditor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/RichTextEditor.spec.ts index 62b8dc3928..2660e106d0 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/RichTextEditor.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/RichTextEditor.spec.ts @@ -61,8 +61,8 @@ test('tiptap is the default property editor in rich text editor', async ({umbrac await umbracoUi.dataType.goToDataType(dataTypeName); // Assert - //await umbracoUi.dataType.doesSettingHaveValue(ConstantHelper.tipTapSettings); - //await umbracoUi.dataType.doesSettingItemsHaveCount(ConstantHelper.tipTapSettings); + await umbracoUi.dataType.doesSettingHaveValue(ConstantHelper.tipTapSettings); + await umbracoUi.dataType.doesSettingItemsHaveCount(ConstantHelper.tipTapSettings); await umbracoUi.dataType.doesPropertyEditorHaveName(tipTapPropertyEditorName); await umbracoUi.dataType.doesPropertyEditorHaveAlias(tipTapAlias); await umbracoUi.dataType.doesPropertyEditorHaveUiAlias(tipTapUiAlias); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts index 650859289e..e52f09b5ec 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts @@ -121,12 +121,15 @@ test('can add an available block', async ({umbracoApi, umbracoUi}) => { await umbracoUi.dataType.goToDataType(tipTapName); // Act + await umbracoUi.dataType.isExtensionItemChecked('Block', false); await umbracoUi.dataType.addAvailableBlocks(elementTypeName); await umbracoUi.dataType.clickSaveButton(); // Assert await umbracoUi.dataType.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); expect(await umbracoApi.dataType.doesRTEContainBlocks(tipTapName, [elementTypeId])).toBeTruthy(); + // Verify that "Block" extension is enable + await umbracoUi.dataType.isExtensionItemChecked('Block'); // Clean await umbracoApi.documentType.ensureNameNotExists(elementTypeName); @@ -226,3 +229,38 @@ test('can disable extensions item', async ({umbracoApi, umbracoUi}) => { expect(extensionsValue.value.length).toBe(extensionsCount - 1); expect(extensionsValue.value).not.toContain(extensionItemName); }); + +test('can add a statusbar', async ({umbracoApi, umbracoUi}) => { + // Arrange + const statusbarName = 'Word Count'; + const statusbarApiValue = 'Umb.Tiptap.Statusbar.WordCount'; + await umbracoApi.dataType.createDefaultTiptapDataType(tipTapName); + await umbracoUi.dataType.goToDataType(tipTapName); + + // Act + await umbracoUi.dataType.clickStatusbarItemInToolboxWithName(statusbarName); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + const tipTapData = await umbracoApi.dataType.getByName(tipTapName); + const statusbarValue = tipTapData.values.find(value => value.alias === 'statusbar'); + expect(statusbarValue.value).toEqual([[statusbarApiValue]]); +}); + +test('can remove a statusbar', async ({umbracoApi, umbracoUi}) => { + // Arrange + const statusbarName = 'Word Count'; + await umbracoApi.dataType.createTiptapDataTypeWithWordCountStatusbar(tipTapName); + await umbracoUi.dataType.goToDataType(tipTapName); + + // Act + await umbracoUi.dataType.clickStatusbarItemWithName(statusbarName); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + const tipTapData = await umbracoApi.dataType.getByName(tipTapName); + const statusbarValue = tipTapData.values.find(value => value.alias === 'statusbar'); + expect(statusbarValue).toBeFalsy(); +}); \ No newline at end of file From 38e9781fc52a9a607cc3cd53dfa8caef64b41a59 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 29 Apr 2025 13:04:24 +0200 Subject: [PATCH 11/11] build: restores some of the behavior from V13 in relation to StaticAssets (#19189) In v13, the StaticAssets build was only triggered based on the existence of either the output folder or a preserve.* marker file. Here, we also additionally check for the node_modules/.package-lock.json file before reinstalling npm dependencies. We also now only run `npm install` rather than `npm ci` to optimise the build. --- .../Umbraco.Cms.StaticAssets.csproj | 77 +++++++++++++++---- 1 file changed, 62 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj b/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj index d6fbf7ab37..3193533266 100644 --- a/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj +++ b/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj @@ -26,23 +26,36 @@ - + + + + + + + + ..\Umbraco.Web.UI.Client\ - wwwroot\umbraco\backoffice + $(ProjectDir)wwwroot\umbraco\backoffice - - - - + + + + + - + + + + + + @@ -61,27 +74,46 @@ - + + + + + + + + + + + + + + + + + + ..\Umbraco.Web.UI.Login\ - wwwroot\umbraco\login + $(ProjectDir)wwwroot\umbraco\login - - - - + + + + + + - + - + @@ -99,4 +131,19 @@ + + + + + + + + + + + + + + +