diff --git a/Directory.Packages.props b/Directory.Packages.props index 9794b5a08a..e5305de259 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -44,14 +44,14 @@ - - - - + + + + - + - + @@ -61,20 +61,20 @@ - + - + - - - + + + diff --git a/global.json b/global.json index f1d8f700f5..5db4761d46 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,7 @@ { "sdk": { "version": "9.0.100-preview.5.24307.3", - "rollForward": "latestFeature" + "rollForward": "latestFeature", + "allowPrerelease": true } } diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/ProcessRequestContextHandler.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/ProcessRequestContextHandler.cs index c680ab6b34..21776b740f 100644 --- a/src/Umbraco.Cms.Api.Common/DependencyInjection/ProcessRequestContextHandler.cs +++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/ProcessRequestContextHandler.cs @@ -10,14 +10,15 @@ public class ProcessRequestContextHandler : IOpenIddictServerHandler, IOpenIddictValidationHandler { private readonly IHttpContextAccessor _httpContextAccessor; - private readonly string _backOfficePathSegment; + private readonly string[] _pathsToHandle; public ProcessRequestContextHandler(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; - _backOfficePathSegment = Constants.System.DefaultUmbracoPath.TrimStart(Constants.CharArrays.Tilde) + var backOfficePathSegment = Constants.System.DefaultUmbracoPath.TrimStart(Constants.CharArrays.Tilde) .EnsureStartsWith('/') .EnsureEndsWith('/'); + _pathsToHandle = [backOfficePathSegment, "/.well-known/openid-configuration"]; } public ValueTask HandleAsync(OpenIddictServerEvents.ProcessRequestContext context) @@ -48,6 +49,14 @@ public class ProcessRequestContextHandler return false; } - return requestPath.StartsWith(_backOfficePathSegment) is false; + foreach (var path in _pathsToHandle) + { + if (requestPath.StartsWith(path)) + { + return false; + } + } + + return true; } } diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Security/MemberController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Security/MemberController.cs index ed767947cd..f7296a950f 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/Security/MemberController.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Security/MemberController.cs @@ -218,11 +218,12 @@ public class MemberController : DeliveryApiControllerBase claim.SetDestinations(OpenIddictConstants.Destinations.AccessToken); } - if (request.GetScopes().Contains(OpenIddictConstants.Scopes.OfflineAccess)) - { - // "offline_access" scope is required to use refresh tokens - memberPrincipal.SetScopes(OpenIddictConstants.Scopes.OfflineAccess); - } + // "openid" and "offline_access" are the only scopes allowed for members; explicitly ensure we only add those + // NOTE: the "offline_access" scope is required to use refresh tokens + IEnumerable allowedScopes = request + .GetScopes() + .Intersect(new[] { OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess }); + memberPrincipal.SetScopes(allowedScopes); return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, memberPrincipal); } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/DataTypeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/DataTypeControllerBase.cs index 01e5d67dcc..442525b141 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DataType/DataTypeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/DataTypeControllerBase.cs @@ -11,7 +11,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType; [VersionedApiBackOfficeRoute(Constants.UdiEntityType.DataType)] [ApiExplorerSettings(GroupName = "Data Type")] -[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes)] +[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentOrMediaOrContentTypes)] public abstract class DataTypeControllerBase : ManagementApiControllerBase { protected IActionResult DataTypeOperationStatusResult(DataTypeOperationStatus status) => diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/GetAuditLogDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/GetAuditLogDocumentController.cs index a22841850e..eedf1a9bf2 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/GetAuditLogDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/GetAuditLogDocumentController.cs @@ -39,7 +39,7 @@ public class GetAuditLogDocumentController : DocumentControllerBase { AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( User, - ContentPermissionResource.WithKeys(ActionProtect.ActionLetter, id), + ContentPermissionResource.WithKeys(ActionBrowse.ActionLetter, id), AuthorizationPolicies.ContentPermissionByResource); if (!authorizationResult.Succeeded) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDomainsController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDomainsController.cs index a7c7256c6b..a44289b4c6 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDomainsController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDomainsController.cs @@ -66,6 +66,10 @@ public class UpdateDomainsController : DocumentControllerBase .WithDetail("One or more of the specified domain names were conflicting with domain assignments to other content items.") .WithExtension("conflictingDomainNames", _domainPresentationFactory.CreateDomainAssignmentModels(result.Result.ConflictingDomains.EmptyNull())) .Build()), + DomainOperationStatus.InvalidDomainName => BadRequest(problemDetailsBuilder + .WithTitle("Invalid domain name detected") + .WithDetail("One or more of the specified domain names were invalid.") + .Build()), _ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder .WithTitle("Unknown domain update operation status.") .Build()), diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs index c65ed1631d..da1eeddfac 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs @@ -98,6 +98,18 @@ public abstract class DocumentTypeControllerBase : ManagementApiControllerBase .WithTitle("Name was too long") .WithDetail("Name cannot be more than 255 characters in length.") .Build()), + ContentTypeOperationStatus.InvalidElementFlagDocumentHasContent => new BadRequestObjectResult(problemDetailsBuilder + .WithTitle("Invalid IsElement flag") + .WithDetail("Cannot change to element type because content has already been created with this document type.") + .Build()), + ContentTypeOperationStatus.InvalidElementFlagElementIsUsedInPropertyEditorConfiguration => new BadRequestObjectResult(problemDetailsBuilder + .WithTitle("Invalid IsElement flag") + .WithDetail("Cannot change to document type because this element type is used in the configuration of a data type.") + .Build()), + ContentTypeOperationStatus.InvalidElementFlagComparedToParent => new BadRequestObjectResult(problemDetailsBuilder + .WithTitle("Invalid IsElement flag") + .WithDetail("Can not create a documentType with inheritance composition where the parent and the new type's IsElement flag are different.") + .Build()), _ => new ObjectResult("Unknown content type operation status") { StatusCode = StatusCodes.Status500InternalServerError }, }); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs index 4c4d6211ff..d7a803b584 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs @@ -45,7 +45,8 @@ public abstract class UserStartNodeTreeControllerBase : EntityTreeControl IEntitySlim[] children = base.GetPagedChildEntities(parentKey, skip, take, out totalItems); return UserHasRootAccess() || IgnoreUserStartNodes() ? children - : CalculateAccessMap(() => _userStartNodeEntitiesService.ChildUserAccessEntities(children, UserStartNodePaths), out totalItems); + // Keeping the correct totalItems amount from GetPagedChildEntities + : CalculateAccessMap(() => _userStartNodeEntitiesService.ChildUserAccessEntities(children, UserStartNodePaths), out _); } protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/ApplicationBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/ApplicationBuilderExtensions.cs index 9018b97bee..870c4d3e1e 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/ApplicationBuilderExtensions.cs @@ -33,6 +33,7 @@ internal static class ApplicationBuilderExtensions { innerBuilder.UseExceptionHandler(exceptionBuilder => exceptionBuilder.Run(async context => { + var isDebug = context.RequestServices.GetRequiredService().IsDebugMode; Exception? exception = context.Features.Get()?.Error; if (exception is null) { @@ -42,16 +43,16 @@ internal static class ApplicationBuilderExtensions var response = new ProblemDetails { Title = exception.Message, - Detail = exception.StackTrace, + Detail = isDebug ? exception.StackTrace : null, Status = StatusCodes.Status500InternalServerError, - Instance = exception.GetType().Name, + Instance = isDebug ? exception.GetType().Name : null, Type = "Error" }; await context.Response.WriteAsJsonAsync(response); })); }); - internal static IApplicationBuilder UseEndpoints(this IApplicationBuilder applicationBuilder) +internal static IApplicationBuilder UseEndpoints(this IApplicationBuilder applicationBuilder) { IServiceProvider provider = applicationBuilder.ApplicationServices; diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs index 45eccad5ec..730da19a4c 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs @@ -29,6 +29,7 @@ internal static class BackOfficeAuthPolicyBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddAuthorization(CreatePolicies); return builder; @@ -46,7 +47,7 @@ internal static class BackOfficeAuthPolicyBuilderExtensions options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy => { policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); - policy.RequireAuthenticatedUser(); + policy.Requirements.Add(new BackOfficeRequirement()); }); options.AddPolicy(AuthorizationPolicies.RequireAdminAccess, policy => @@ -76,6 +77,7 @@ internal static class BackOfficeAuthPolicyBuilderExtensions AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDictionaryOrTemplates, Constants.Applications.Translation, Constants.Applications.Settings); AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocuments, Constants.Applications.Content); AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, Constants.Applications.Content, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentOrMediaOrContentTypes, Constants.Applications.Content, Constants.Applications.Settings, Constants.Applications.Media); AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, Constants.Applications.Settings); AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessLanguages, Constants.Applications.Settings); AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMediaTypes, Constants.Applications.Settings); diff --git a/src/Umbraco.Cms.Api.Management/Factories/UserGroupPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/UserGroupPresentationFactory.cs index 0119864f5f..794cfa9bd9 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/UserGroupPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/UserGroupPresentationFactory.cs @@ -1,11 +1,12 @@ -using Umbraco.Cms.Api.Management.Mapping; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Api.Management.Mapping; using Umbraco.Cms.Api.Management.ViewModels; using Umbraco.Cms.Api.Management.ViewModels.UserGroup; -using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; -using Umbraco.Cms.Core.Models.Membership.Permissions; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Core.Strings; @@ -20,17 +21,30 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory private readonly IShortStringHelper _shortStringHelper; private readonly ILanguageService _languageService; private readonly IPermissionPresentationFactory _permissionPresentationFactory; + private readonly ILogger _logger; + [Obsolete("Use the new constructor instead, will be removed in v16.")] public UserGroupPresentationFactory( IEntityService entityService, IShortStringHelper shortStringHelper, ILanguageService languageService, IPermissionPresentationFactory permissionPresentationFactory) + : this(entityService, shortStringHelper, languageService, permissionPresentationFactory, StaticServiceProvider.Instance.GetRequiredService>()) + { + } + + public UserGroupPresentationFactory( + IEntityService entityService, + IShortStringHelper shortStringHelper, + ILanguageService languageService, + IPermissionPresentationFactory permissionPresentationFactory, + ILogger logger) { _entityService = entityService; _shortStringHelper = shortStringHelper; _languageService = languageService; _permissionPresentationFactory = permissionPresentationFactory; + _logger = logger; } /// @@ -43,10 +57,9 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory Attempt, UserGroupOperationStatus> languageIsoCodesMappingAttempt = await MapLanguageIdsToIsoCodeAsync(userGroup.AllowedLanguages); - // We've gotten this data from the database, so the mapping should not fail if (languageIsoCodesMappingAttempt.Success is false) { - throw new InvalidOperationException($"Unknown language ID in User Group: {userGroup.Name}"); + _logger.LogDebug("Unknown language ID in User Group: {0}", userGroup.Name); } return new UserGroupResponseModel @@ -77,10 +90,9 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory Guid? mediaStartNodeKey = GetKeyFromId(userGroup.StartMediaId, UmbracoObjectTypes.Media); Attempt, UserGroupOperationStatus> languageIsoCodesMappingAttempt = await MapLanguageIdsToIsoCodeAsync(userGroup.AllowedLanguages); - // We've gotten this data from the database, so the mapping should not fail if (languageIsoCodesMappingAttempt.Success is false) { - throw new InvalidOperationException($"Unknown language ID in User Group: {userGroup.Name}"); + _logger.LogDebug("Unknown language ID in User Group: {0}", userGroup.Name); } return new UserGroupResponseModel @@ -217,9 +229,10 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory .Select(x => x.IsoCode) .ToArray(); - return isoCodes.Length == ids.Count() - ? Attempt.SucceedWithStatus, UserGroupOperationStatus>(UserGroupOperationStatus.Success, isoCodes) - : Attempt.FailWithStatus, UserGroupOperationStatus>(UserGroupOperationStatus.LanguageNotFound, isoCodes); + // if a language id does not exist, it simply not returned. + // We do this so we don't have to clean up user group data when deleting languages and to make it easier to restore accidentally removed languages + return Attempt.SucceedWithStatus, UserGroupOperationStatus>( + UserGroupOperationStatus.Success, isoCodes); } private async Task, UserGroupOperationStatus>> MapLanguageIsoCodesToIdsAsync(IEnumerable isoCodes) diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 06c4f45d24..9f56d0c8aa 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -45566,4 +45566,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/DenyLocalLogin/DenyLocalLoginHandler.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/DenyLocalLogin/DenyLocalLoginHandler.cs index e57eb6c742..cd9a675bfc 100644 --- a/src/Umbraco.Cms.Api.Management/Security/Authorization/DenyLocalLogin/DenyLocalLoginHandler.cs +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/DenyLocalLogin/DenyLocalLoginHandler.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization.Infrastructure; +using Umbraco.Cms.Api.Management.Security.Authorization.User; namespace Umbraco.Cms.Api.Management.Security.Authorization.DenyLocalLogin; @@ -24,12 +25,12 @@ public class DenyLocalLoginHandler : MustSatisfyRequirementAuthorizationHandler< if (isDenied is false) { - // AuthorizationPolicies.BackOfficeAccess policy adds this requirement by policy.RequireAuthenticatedUser() + // AuthorizationPolicies.BackOfficeAccess policy adds this requirement by policy.Requirements.Add(new BackOfficeRequirement()); // Since we want to "allow anonymous" for some endpoints (i.e. BackOfficeController.Login()), it is necessary to succeed this requirement - IEnumerable denyAnonymousUserRequirements = context.PendingRequirements.OfType(); - foreach (DenyAnonymousAuthorizationRequirement denyAnonymousUserRequirement in denyAnonymousUserRequirements) + IEnumerable backOfficeRequirements = context.PendingRequirements.OfType(); + foreach (BackOfficeRequirement backOfficeRequirement in backOfficeRequirements) { - context.Succeed(denyAnonymousUserRequirement); + context.Succeed(backOfficeRequirement); } } diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationHandler.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationHandler.cs index a36a592827..69246ee7fc 100644 --- a/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationHandler.cs +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationHandler.cs @@ -17,8 +17,8 @@ internal sealed class AllowedApplicationHandler : MustSatisfyRequirementAuthoriz protected override Task IsAuthorized(AuthorizationHandlerContext context, AllowedApplicationRequirement requirement) { - IUser user = _authorizationHelper.GetUmbracoUser(context.User); - var allowed = user.AllowedSections.ContainsAny(requirement.Applications); + var allowed = _authorizationHelper.TryGetUmbracoUser(context.User, out IUser? user) + && user.AllowedSections.ContainsAny(requirement.Applications); return Task.FromResult(allowed); } } diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/User/BackOfficeHandler.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/BackOfficeHandler.cs new file mode 100644 index 0000000000..ff79e344ae --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/BackOfficeHandler.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Authorization; +using Umbraco.Cms.Core.Security; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.User; + +/// +/// Ensures authorization is successful for a back office user. +/// +public class BackOfficeHandler : MustSatisfyRequirementAuthorizationHandler +{ + private readonly IBackOfficeSecurityAccessor _backOfficeSecurity; + + public BackOfficeHandler(IBackOfficeSecurityAccessor backOfficeSecurity) + { + _backOfficeSecurity = backOfficeSecurity; + } + + protected override Task IsAuthorized(AuthorizationHandlerContext context, BackOfficeRequirement requirement) + { + + if (context.HasFailed is false && context.HasSucceeded is true) + { + return Task.FromResult(true); + } + + if (!_backOfficeSecurity.BackOfficeSecurity?.IsAuthenticated() ?? false) + { + return Task.FromResult(false); + } + + var userApprovalSucceeded = !requirement.RequireApproval || + (_backOfficeSecurity.BackOfficeSecurity?.CurrentUser?.IsApproved ?? false); + return Task.FromResult(userApprovalSucceeded); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/User/BackOfficeRequirement.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/BackOfficeRequirement.cs new file mode 100644 index 0000000000..8c6f97b24f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/BackOfficeRequirement.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.User; + +/// +/// Authorization requirement for the . +/// +public class BackOfficeRequirement : IAuthorizationRequirement +{ + /// + /// Initializes a new instance of the class. + /// + /// Flag for whether back-office user approval is required. + public BackOfficeRequirement(bool requireApproval = true) => RequireApproval = requireApproval; + + /// + /// Gets a value indicating whether back-office user approval is required. + /// + public bool RequireApproval { get; } +} diff --git a/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj b/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj index 724c5b5e34..c513082675 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj +++ b/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/Umbraco.Core/Constants-HealthChecks.cs b/src/Umbraco.Core/Constants-HealthChecks.cs index 256efbcc54..bb18145401 100644 --- a/src/Umbraco.Core/Constants-HealthChecks.cs +++ b/src/Umbraco.Core/Constants-HealthChecks.cs @@ -22,6 +22,7 @@ public static partial class Constants public static class LiveEnvironment { public const string CompilationDebugCheck = "https://umbra.co/healthchecks-compilation-debug"; + public const string RuntimeModeCheck = "https://docs.umbraco.com/umbraco-cms/fundamentals/setup/server-setup/runtime-modes"; } public static class Configuration diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 11c9e91099..ffeafe17b7 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -398,11 +398,14 @@ namespace Umbraco.Cms.Core.DependencyInjection // Segments Services.AddUnique(); - + // definition Import/export Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); + + // add validation services + Services.AddUnique(); } } } diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index 599232b13a..893ab7c242 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -1,4 +1,4 @@ - + The Umbraco community @@ -428,6 +428,8 @@ Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. + Runtime mode is set to production. + Runtime mode is not set to Production. It is recommended to set the Runtime Mode to Production for live/production environments. diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 7517a43b12..69558f72da 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -419,6 +419,8 @@ Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. + Runtime mode is set to production. + Runtime mode is not set to Production. It is recommended to set the Runtime Mode to Production for live/production environments. diff --git a/src/Umbraco.Core/Extensions/ObjectExtensions.cs b/src/Umbraco.Core/Extensions/ObjectExtensions.cs index f0f10d8cc6..2ef1f9194f 100644 --- a/src/Umbraco.Core/Extensions/ObjectExtensions.cs +++ b/src/Umbraco.Core/Extensions/ObjectExtensions.cs @@ -219,6 +219,24 @@ public static class ObjectExtensions } } + if (target == typeof(DateTime) && input is DateTimeOffset dateTimeOffset) + { + // IMPORTANT: for compatability with various editors, we must discard any Offset information and assume UTC time here + return Attempt.Succeed((object?)new DateTime( + new DateOnly(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day), + new TimeOnly(dateTimeOffset.Hour, dateTimeOffset.Minute, dateTimeOffset.Second, dateTimeOffset.Millisecond, dateTimeOffset.Microsecond), + DateTimeKind.Utc)); + } + + if (target == typeof(DateTimeOffset) && input is DateTime dateTime) + { + // IMPORTANT: for compatability with various editors, we must discard any DateTimeKind information and assume UTC time here + return Attempt.Succeed((object?)new DateTimeOffset( + new DateOnly(dateTime.Year, dateTime.Month, dateTime.Day), + new TimeOnly(dateTime.Hour, dateTime.Minute, dateTime.Second, dateTime.Millisecond, dateTime.Microsecond), + TimeSpan.Zero)); + } + TypeConverter? inputConverter = GetCachedSourceTypeConverter(inputType, target); if (inputConverter != null) { diff --git a/src/Umbraco.Core/Handlers/WarnDocumentTypeElementSwitchNotificationHandler.cs b/src/Umbraco.Core/Handlers/WarnDocumentTypeElementSwitchNotificationHandler.cs new file mode 100644 index 0000000000..b4c13f8da3 --- /dev/null +++ b/src/Umbraco.Core/Handlers/WarnDocumentTypeElementSwitchNotificationHandler.cs @@ -0,0 +1,104 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Extensions; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.ContentTypeEditing; + +namespace Umbraco.Cms.Core.Handlers; + +public class WarnDocumentTypeElementSwitchNotificationHandler : + INotificationAsyncHandler, + INotificationAsyncHandler +{ + private const string NotificationStateKey = + "Umbraco.Cms.Core.Handlers.WarnDocumentTypeElementSwitchNotificationHandler"; + + private readonly IEventMessagesFactory _eventMessagesFactory; + private readonly IContentTypeService _contentTypeService; + private readonly IElementSwitchValidator _elementSwitchValidator; + + public WarnDocumentTypeElementSwitchNotificationHandler( + IEventMessagesFactory eventMessagesFactory, + IContentTypeService contentTypeService, + IElementSwitchValidator elementSwitchValidator) + { + _eventMessagesFactory = eventMessagesFactory; + _contentTypeService = contentTypeService; + _elementSwitchValidator = elementSwitchValidator; + } + + // To figure out whether a warning should be generated, we need both the state before and after saving + public async Task HandleAsync(ContentTypeSavingNotification notification, CancellationToken cancellationToken) + { + IEnumerable updatedKeys = notification.SavedEntities + .Where(e => e.HasIdentity) + .Select(e => e.Key); + + IEnumerable persistedItems = _contentTypeService.GetAll(updatedKeys); + + var stateInformation = persistedItems + .ToDictionary( + contentType => contentType.Key, + contentType => new DocumentTypeElementSwitchInformation { WasElement = contentType.IsElement }); + notification.State[NotificationStateKey] = stateInformation; + } + + public async Task HandleAsync(ContentTypeSavedNotification notification, CancellationToken cancellationToken) + { + if (notification.State[NotificationStateKey] is not Dictionary + stateInformation) + { + return; + } + + EventMessages eventMessages = _eventMessagesFactory.Get(); + + foreach (IContentType savedDocumentType in notification.SavedEntities) + { + if (stateInformation.ContainsKey(savedDocumentType.Key) is false) + { + continue; + } + + DocumentTypeElementSwitchInformation state = stateInformation[savedDocumentType.Key]; + if (state.WasElement == savedDocumentType.IsElement) + { + // no change + continue; + } + + await WarnIfAncestorsAreMisaligned(savedDocumentType, eventMessages); + await WarnIfDescendantsAreMisaligned(savedDocumentType, eventMessages); + } + } + + private async Task WarnIfAncestorsAreMisaligned(IContentType contentType, EventMessages eventMessages) + { + if (await _elementSwitchValidator.AncestorsAreAlignedAsync(contentType) == false) + { + // todo update this message when the format has been agreed upon on with the client + eventMessages.Add(new EventMessage( + "DocumentType saved", + "One or more ancestors have a mismatching element flag", + EventMessageType.Warning)); + } + } + + private async Task WarnIfDescendantsAreMisaligned(IContentType contentType, EventMessages eventMessages) + { + if (await _elementSwitchValidator.DescendantsAreAlignedAsync(contentType) == false) + { + // todo update this message when the format has been agreed upon on with the client + eventMessages.Add(new EventMessage( + "DocumentType saved", + "One or more descendants have a mismatching element flag", + EventMessageType.Warning)); + } + } + + private class DocumentTypeElementSwitchInformation + { + public bool WasElement { get; set; } + } +} diff --git a/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/RuntimeModeCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/RuntimeModeCheck.cs new file mode 100644 index 0000000000..777a8544ee --- /dev/null +++ b/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/RuntimeModeCheck.cs @@ -0,0 +1,53 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.HealthChecks.Checks.LiveEnvironment; + +/// +/// Health check for the recommended production configuration for the runtime mode. +/// +[HealthCheck( + "8E31E5C9-7A1D-4ACB-A3A8-6495F3EDB932", + "Runtime Mode", + Description = "The Production Runtime Mode disables development features and checks that settings are configured optimally for production.", + Group = "Live Environment")] +public class RuntimeModeCheck : AbstractSettingsCheck +{ + private readonly IOptionsMonitor _runtimeSettings; + + /// + /// Initializes a new instance of the class. + /// + public RuntimeModeCheck(ILocalizedTextService textService, IOptionsMonitor runtimeSettings) + : base(textService) => + _runtimeSettings = runtimeSettings; + + /// + public override string ItemPath => Constants.Configuration.ConfigRuntimeMode; + + /// + public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldEqual; + + /// + public override IEnumerable Values => new List + { + new() { IsRecommended = true, Value = RuntimeMode.Production.ToString() }, + }; + + /// + public override string CurrentValue => _runtimeSettings.CurrentValue.Mode.ToString(); + + /// + public override string CheckSuccessMessage => LocalizedTextService.Localize("healthcheck", "runtimeModeCheckSuccessMessage"); + + /// + public override string CheckErrorMessage => LocalizedTextService.Localize("healthcheck", "runtimeModeCheckErrorMessage"); + + /// + public override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.LiveEnvironment.RuntimeModeCheck; +} diff --git a/src/Umbraco.Core/Models/IDataValueEditor.cs b/src/Umbraco.Core/Models/IDataValueEditor.cs index d733488be8..691ee70bf1 100644 --- a/src/Umbraco.Core/Models/IDataValueEditor.cs +++ b/src/Umbraco.Core/Models/IDataValueEditor.cs @@ -27,7 +27,6 @@ public interface IDataValueEditor /// bool SupportsReadOnly => false; - /// /// Gets the validators to use to validate the edited value. /// @@ -75,4 +74,6 @@ public interface IDataValueEditor XNode ConvertDbToXml(IPropertyType propertyType, object value); string ConvertDbToString(IPropertyType propertyType, object? value); + + IEnumerable ConfiguredElementTypeKeys() => Enumerable.Empty(); } diff --git a/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs index f398d75eac..7ebf2fd112 100644 --- a/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs @@ -9,7 +9,7 @@ namespace Umbraco.Cms.Core.PropertyEditors; public class BlockListConfiguration { [ConfigurationField("blocks")] - public BlockConfiguration[] Blocks { get; set; } = null!; + public BlockConfiguration[] Blocks { get; set; } = Array.Empty(); [ConfigurationField("validationLimit")] public NumberRange ValidationLimit { get; set; } = new(); diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index f04e24c51b..3048162891 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -75,6 +75,10 @@ public class DataEditor : IDataEditor [DataMember(Name = "supportsReadOnly", IsRequired = true)] public bool SupportsReadOnly { get; set; } + // Adding a virtual method that wraps the default implementation allows derived classes + // to override the default implementation without having to explicitly inherit the interface. + public virtual bool SupportsConfigurableElements => false; + /// [IgnoreDataMember] public bool IsDeprecated { get; } diff --git a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs index 5faa60507a..e9d131c75a 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs @@ -356,6 +356,10 @@ public class DataValueEditor : IDataValueEditor } } + // Adding a virtual method that wraps the default implementation allows derived classes + // to override the default implementation without having to explicitly inherit the interface. + public virtual IEnumerable ConfiguredElementTypeKeys() => Enumerable.Empty(); + /// /// Used to try to convert the string value to the correct CLR type based on the specified for /// this value editor. diff --git a/src/Umbraco.Core/PropertyEditors/IDataEditor.cs b/src/Umbraco.Core/PropertyEditors/IDataEditor.cs index 6db8049d23..5a95445fce 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataEditor.cs @@ -16,6 +16,8 @@ public interface IDataEditor : IDiscoverable bool SupportsReadOnly => false; + bool SupportsConfigurableElements => false; + /// /// Gets a value indicating whether the editor is deprecated. /// diff --git a/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs b/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs index a0c76b1ffd..5ebcb13b5d 100644 --- a/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs @@ -6,7 +6,7 @@ namespace Umbraco.Cms.Core.PropertyEditors; public class RichTextConfiguration : IIgnoreUserStartNodesConfig { [ConfigurationField("blocks")] - public RichTextBlockConfiguration[]? Blocks { get; set; } = null!; + public RichTextBlockConfiguration[]? Blocks { get; set; } = Array.Empty(); [ConfigurationField("mediaParentId")] public Guid? MediaParentId { get; set; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs index 740e1b8f8e..6f9238f01f 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs @@ -20,23 +20,19 @@ public class DatePickerValueConverter : PropertyValueConverterBase internal static DateTime ParseDateTimeValue(object? source) { - if (source == null) + if (source is 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) + if (source is DateTime dateTimeValue) { - Attempt attempt = sourceString.TryConvertTo(); - return attempt.Success == false ? DateTime.MinValue : attempt.Result; + return dateTimeValue; } - // in the database a DateTime is: DateTime - // default value is: DateTime.MinValue - return source is DateTime dateTimeValue ? dateTimeValue : DateTime.MinValue; + Attempt attempt = source.TryConvertTo(); + return attempt.Success + ? attempt.Result + : DateTime.MinValue; } } diff --git a/src/Umbraco.Core/Security/Authorization/AuthorizationHelper.cs b/src/Umbraco.Core/Security/Authorization/AuthorizationHelper.cs index c4b6b092de..73fcc4a527 100644 --- a/src/Umbraco.Core/Security/Authorization/AuthorizationHelper.cs +++ b/src/Umbraco.Core/Security/Authorization/AuthorizationHelper.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Security.Claims; using System.Security.Principal; using Umbraco.Cms.Core.Models.Membership; @@ -20,8 +21,14 @@ internal sealed class AuthorizationHelper : IAuthorizationHelper /// public IUser GetUmbracoUser(IPrincipal currentUser) + => TryGetUmbracoUser(currentUser, out IUser? user) + ? user + : throw new InvalidOperationException($"Could not obtain an {nameof(IUser)} instance from {nameof(IPrincipal)}"); + + /// + public bool TryGetUmbracoUser(IPrincipal currentUser, [NotNullWhen(true)] out IUser? user) { - IUser? user = null; + user = null; ClaimsIdentity? umbIdentity = currentUser.GetUmbracoIdentity(); Guid? currentUserKey = umbIdentity?.GetUserKey(); @@ -38,12 +45,6 @@ internal sealed class AuthorizationHelper : IAuthorizationHelper user = _userService.GetAsync(currentUserKey.Value).GetAwaiter().GetResult(); } - if (user is null) - { - throw new InvalidOperationException( - $"Could not obtain an {nameof(IUser)} instance from {nameof(IPrincipal)}"); - } - - return user; + return user is not null; } } diff --git a/src/Umbraco.Core/Security/Authorization/IAuthorizationHelper.cs b/src/Umbraco.Core/Security/Authorization/IAuthorizationHelper.cs index 429d18c25f..d197770a00 100644 --- a/src/Umbraco.Core/Security/Authorization/IAuthorizationHelper.cs +++ b/src/Umbraco.Core/Security/Authorization/IAuthorizationHelper.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Security.Principal; using Umbraco.Cms.Core.Models.Membership; @@ -9,11 +10,31 @@ namespace Umbraco.Cms.Core.Security.Authorization; public interface IAuthorizationHelper { /// - /// Converts an into . + /// Converts an into an . /// /// The current user's principal. /// /// . /// IUser GetUmbracoUser(IPrincipal currentUser); + + /// + /// Attempts to convert an into an . + /// + /// The current user's principal. + /// The resulting , if the conversion is successful. + /// True if the conversion is successful, false otherwise + bool TryGetUmbracoUser(IPrincipal currentUser, [NotNullWhen(true)] out IUser? user) + { + try + { + user = GetUmbracoUser(currentUser); + return true; + } + catch + { + user = null; + return false; + } + } } diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingService.cs b/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingService.cs index 9c96fe64a4..e44b66b971 100644 --- a/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingService.cs +++ b/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingService.cs @@ -1,6 +1,9 @@ -using Umbraco.Cms.Core.Models; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; @@ -12,8 +15,24 @@ namespace Umbraco.Cms.Core.Services.ContentTypeEditing; internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase, IContentTypeEditingService { private readonly ITemplateService _templateService; + private readonly IElementSwitchValidator _elementSwitchValidator; private readonly IContentTypeService _contentTypeService; + public ContentTypeEditingService( + IContentTypeService contentTypeService, + ITemplateService templateService, + IDataTypeService dataTypeService, + IEntityService entityService, + IShortStringHelper shortStringHelper, + IElementSwitchValidator elementSwitchValidator) + : base(contentTypeService, contentTypeService, dataTypeService, entityService, shortStringHelper) + { + _contentTypeService = contentTypeService; + _templateService = templateService; + _elementSwitchValidator = elementSwitchValidator; + } + + [Obsolete("Use the constructor that is not marked obsolete, will be removed in v16")] public ContentTypeEditingService( IContentTypeService contentTypeService, ITemplateService templateService, @@ -24,6 +43,7 @@ internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase< { _contentTypeService = contentTypeService; _templateService = templateService; + _elementSwitchValidator = StaticServiceProvider.Instance.GetRequiredService(); } public async Task> CreateAsync(ContentTypeCreateModel model, Guid userKey) @@ -52,13 +72,20 @@ internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase< public async Task> UpdateAsync(IContentType contentType, ContentTypeUpdateModel model, Guid userKey) { - Attempt result = await ValidateAndMapForUpdateAsync(contentType, model); - if (result.Success is false) + // this needs to happen before the base call as that one is not a pure function + ContentTypeOperationStatus elementValidationStatus = await ValidateElementStatusForUpdateAsync(contentType, model); + if (elementValidationStatus is not ContentTypeOperationStatus.Success) { - return result; + return Attempt.Fail(elementValidationStatus); } - contentType = result.Result ?? throw new InvalidOperationException($"{nameof(ValidateAndMapForUpdateAsync)} succeeded but did not yield any result"); + Attempt baseValidationAttempt = await ValidateAndMapForUpdateAsync(contentType, model); + if (baseValidationAttempt.Success is false) + { + return baseValidationAttempt; + } + + contentType = baseValidationAttempt.Result ?? throw new InvalidOperationException($"{nameof(ValidateAndMapForUpdateAsync)} succeeded but did not yield any result"); UpdateHistoryCleanup(contentType, model); UpdateTemplates(contentType, model); @@ -77,6 +104,13 @@ internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase< bool isElement) => await FindAvailableCompositionsAsync(key, currentCompositeKeys, currentPropertyAliases, isElement); + protected override async Task AdditionalCreateValidationAsync( + ContentTypeEditingModelBase model) + { + // validate if the parent documentType (if set) has the same element status as the documentType being created + return await ValidateCreateParentElementStatusAsync(model); + } + // update content type history clean-up private void UpdateHistoryCleanup(IContentType contentType, ContentTypeModelBase model) { @@ -100,6 +134,48 @@ internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase< contentType.SetDefaultTemplate(allowedTemplates.FirstOrDefault(t => t.Key == model.DefaultTemplateKey)); } + private async Task ValidateElementStatusForUpdateAsync(IContentTypeBase contentType, ContentTypeModelBase model) + { + // no change, ignore rest of validation + if (contentType.IsElement == model.IsElement) + { + return ContentTypeOperationStatus.Success; + } + + // this method should only contain blocking validation, warnings are handled by WarnDocumentTypeElementSwitchNotificationHandler + + // => check whether the element was used in a block structure prior to updating + if (model.IsElement is false) + { + return await _elementSwitchValidator.ElementToDocumentNotUsedInBlockStructuresAsync(contentType) + ? ContentTypeOperationStatus.Success + : ContentTypeOperationStatus.InvalidElementFlagElementIsUsedInPropertyEditorConfiguration; + } + + return await _elementSwitchValidator.DocumentToElementHasNoContentAsync(contentType) + ? ContentTypeOperationStatus.Success + : ContentTypeOperationStatus.InvalidElementFlagDocumentHasContent; + } + + /// + /// Should be called after it has been established that the composition list is in a valid state and the (composition) parent exists + /// + private async Task ValidateCreateParentElementStatusAsync( + ContentTypeEditingModelBase model) + { + Guid? parentId = model.Compositions + .SingleOrDefault(composition => composition.CompositionType == CompositionType.Inheritance)?.Key; + if (parentId is null) + { + return ContentTypeOperationStatus.Success; + } + + IContentType? parent = await _contentTypeService.GetAsync(parentId.Value); + return parent!.IsElement == model.IsElement + ? ContentTypeOperationStatus.Success + : ContentTypeOperationStatus.InvalidElementFlagComparedToParent; + } + protected override IContentType CreateContentType(IShortStringHelper shortStringHelper, int parentId) => new ContentType(shortStringHelper, parentId); diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs b/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs index 46c4508095..8e602a01f1 100644 --- a/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs @@ -91,6 +91,8 @@ internal abstract class ContentTypeEditingServiceBase(operationStatus, null); } + await AdditionalCreateValidationAsync(model); + // get the ID of the parent to create the content type under (we already validated that it exists) var parentId = GetParentId(model, containerKey) ?? throw new ArgumentException("Parent ID could not be found", nameof(model)); TContentType contentType = CreateContentType(_shortStringHelper, parentId); @@ -137,6 +139,10 @@ internal abstract class ContentTypeEditingServiceBase(ContentTypeOperationStatus.Success, contentType); } + protected virtual async Task AdditionalCreateValidationAsync( + ContentTypeEditingModelBase model) + => await Task.FromResult(ContentTypeOperationStatus.Success); + #region Sanitization private void SanitizeModelAliases(ContentTypeEditingModelBase model) diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/ElementSwitchValidator.cs b/src/Umbraco.Core/Services/ContentTypeEditing/ElementSwitchValidator.cs new file mode 100644 index 0000000000..cc597b587f --- /dev/null +++ b/src/Umbraco.Core/Services/ContentTypeEditing/ElementSwitchValidator.cs @@ -0,0 +1,66 @@ +using Umbraco.Cms.Core.Extensions; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; + +namespace Umbraco.Cms.Core.Services.ContentTypeEditing; + +public class ElementSwitchValidator : IElementSwitchValidator +{ + private readonly IContentTypeService _contentTypeService; + private readonly PropertyEditorCollection _propertyEditorCollection; + private readonly IDataTypeService _dataTypeService; + + public ElementSwitchValidator( + IContentTypeService contentTypeService, + PropertyEditorCollection propertyEditorCollection, + IDataTypeService dataTypeService) + { + _contentTypeService = contentTypeService; + _propertyEditorCollection = propertyEditorCollection; + _dataTypeService = dataTypeService; + } + + public async Task AncestorsAreAlignedAsync(IContentType contentType) + { + // this call does not return the system roots + var ancestorIds = contentType.AncestorIds(); + if (ancestorIds.Length == 0) + { + // if there are no ancestors, validation passes + return true; + } + + // if there are any ancestors where IsElement is different from the contentType, the validation fails + return await Task.FromResult(_contentTypeService.GetAll(ancestorIds) + .Any(ancestor => ancestor.IsElement != contentType.IsElement) is false); + } + + public async Task DescendantsAreAlignedAsync(IContentType contentType) + { + IEnumerable descendants = _contentTypeService.GetDescendants(contentType.Id, false); + + // if there are any descendants where IsElement is different from the contentType, the validation fails + return await Task.FromResult(descendants.Any(descendant => descendant.IsElement != contentType.IsElement) is false); + } + + public async Task ElementToDocumentNotUsedInBlockStructuresAsync(IContentTypeBase contentType) + { + // get all propertyEditors that support block usage + IDataEditor[] editors = _propertyEditorCollection.Where(pe => pe.SupportsConfigurableElements).ToArray(); + var blockEditorAliases = editors.Select(pe => pe.Alias).ToArray(); + + // get all dataTypes that are based on those propertyEditors + IEnumerable dataTypes = await _dataTypeService.GetByEditorAliasAsync(blockEditorAliases); + + // if any dataType has a configuration where this element is selected as a possible block, the validation fails. + return dataTypes.Any(dataType => + editors.First(editor => editor.Alias == dataType.EditorAlias) + .GetValueEditor(dataType.ConfigurationObject) + .ConfiguredElementTypeKeys().Contains(contentType.Key)) is false; + } + + public async Task DocumentToElementHasNoContentAsync(IContentTypeBase contentType) => + + // if any content for the content type exists, the validation fails. + await Task.FromResult(_contentTypeService.HasContentNodes(contentType.Id) is false); +} diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/IElementSwitchValidator.cs b/src/Umbraco.Core/Services/ContentTypeEditing/IElementSwitchValidator.cs new file mode 100644 index 0000000000..8816f99ec8 --- /dev/null +++ b/src/Umbraco.Core/Services/ContentTypeEditing/IElementSwitchValidator.cs @@ -0,0 +1,14 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services.ContentTypeEditing; + +public interface IElementSwitchValidator +{ + Task AncestorsAreAlignedAsync(IContentType contentType); + + Task DescendantsAreAlignedAsync(IContentType contentType); + + Task ElementToDocumentNotUsedInBlockStructuresAsync(IContentTypeBase contentType); + + Task DocumentToElementHasNoContentAsync(IContentTypeBase contentType); +} diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index 5564802f3c..382238d583 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -330,6 +330,16 @@ namespace Umbraco.Cms.Core.Services.Implement return Task.FromResult(dataTypes); } + + /// + public async Task> GetByEditorAliasAsync(string[] propertyEditorAlias) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); + IQuery query = Query().Where(x => propertyEditorAlias.Contains(x.EditorAlias)); + IEnumerable dataTypes = _dataTypeRepository.Get(query).ToArray(); + ConvertMissingEditorsOfDataTypesToLabels(dataTypes); + return await Task.FromResult(dataTypes); + } /// public Task> GetByEditorUiAlias(string editorUiAlias) diff --git a/src/Umbraco.Core/Services/DomainService.cs b/src/Umbraco.Core/Services/DomainService.cs index b4e103f1a4..c527e40b82 100644 --- a/src/Umbraco.Core/Services/DomainService.cs +++ b/src/Umbraco.Core/Services/DomainService.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Extensions; @@ -201,6 +202,11 @@ public class DomainService : RepositoryService, IDomainService foreach (DomainModel domainModel in updateModel.Domains) { domainModel.DomainName = domainModel.DomainName.ToLowerInvariant(); + + if(Uri.IsWellFormedUriString(domainModel.DomainName, UriKind.RelativeOrAbsolute) is false) + { + return Attempt.FailWithStatus(DomainOperationStatus.InvalidDomainName, new DomainUpdateResult()); + } } // make sure we're not attempting to assign duplicate domains diff --git a/src/Umbraco.Core/Services/IDataTypeService.cs b/src/Umbraco.Core/Services/IDataTypeService.cs index 8aefd3b93f..8536c8a528 100644 --- a/src/Umbraco.Core/Services/IDataTypeService.cs +++ b/src/Umbraco.Core/Services/IDataTypeService.cs @@ -240,4 +240,11 @@ public interface IDataTypeService : IService /// The data type whose configuration to validate. /// One or more if the configuration data is invalid, an empty collection otherwise. IEnumerable ValidateConfigurationData(IDataType dataType); + + /// + /// Gets all for a set of property editors + /// + /// Aliases of the property editors + /// Collection of configured for the property editors + Task> GetByEditorAliasAsync(string[] propertyEditorAlias); } diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 755b36ae82..c754aa244a 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -1002,7 +1002,6 @@ namespace Umbraco.Cms.Core.Services MoveToRecycleBinEventInfo[] moveInfo = moves.Select(x => new MoveToRecycleBinEventInfo(x.Item1, x.Item2)).ToArray(); scope.Notifications.Publish(new MediaMovedToRecycleBinNotification(moveInfo, messages).WithStateFrom(movingToRecycleBinNotification)); Audit(AuditType.Move, userId, media.Id, "Move Media to recycle bin"); - scope.Complete(); } diff --git a/src/Umbraco.Core/Services/OperationStatus/ContentTypeOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/ContentTypeOperationStatus.cs index 8b52d921ee..32f28eaf16 100644 --- a/src/Umbraco.Core/Services/OperationStatus/ContentTypeOperationStatus.cs +++ b/src/Umbraco.Core/Services/OperationStatus/ContentTypeOperationStatus.cs @@ -21,4 +21,7 @@ public enum ContentTypeOperationStatus NotFound, NotAllowed, CancelledByNotification, + InvalidElementFlagDocumentHasContent, + InvalidElementFlagElementIsUsedInPropertyEditorConfiguration, + InvalidElementFlagComparedToParent, } diff --git a/src/Umbraco.Core/Services/OperationStatus/DomainOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/DomainOperationStatus.cs index a752684b2e..ba19e2bc3f 100644 --- a/src/Umbraco.Core/Services/OperationStatus/DomainOperationStatus.cs +++ b/src/Umbraco.Core/Services/OperationStatus/DomainOperationStatus.cs @@ -7,5 +7,6 @@ public enum DomainOperationStatus ContentNotFound, LanguageNotFound, DuplicateDomainName, - ConflictingDomainName + ConflictingDomainName, + InvalidDomainName } diff --git a/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs b/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs index 74105da511..c79506fb5f 100644 --- a/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs +++ b/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs @@ -35,11 +35,11 @@ public sealed class HtmlLocalLinkParser public IEnumerable FindUdisFromLocalLinks(string text) { - foreach ((var intId, GuidUdi? udi, var tagValue) in FindLocalLinkIds(text)) + foreach (LocalLinkTag tagData in FindLocalLinkIds(text)) { - if (udi is not null) + if (tagData.Udi is not null) { - yield return udi; // In v8, we only care abuot UDIs + yield return tagData.Udi; // In v8, we only care about UDIs } } } @@ -80,38 +80,41 @@ public sealed class HtmlLocalLinkParser throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); } - foreach ((var intId, GuidUdi? udi, var tagValue) in FindLocalLinkIds(text)) + foreach (LocalLinkTag tagData in FindLocalLinkIds(text)) { - if (udi is not null) + if (tagData.Udi is not null) { var newLink = "#"; - if (udi?.EntityType == Constants.UdiEntityType.Document) + if (tagData.Udi?.EntityType == Constants.UdiEntityType.Document) { - newLink = _publishedUrlProvider.GetUrl(udi.Guid); + newLink = _publishedUrlProvider.GetUrl(tagData.Udi.Guid); } - else if (udi?.EntityType == Constants.UdiEntityType.Media) + else if (tagData.Udi?.EntityType == Constants.UdiEntityType.Media) { - newLink = _publishedUrlProvider.GetMediaUrl(udi.Guid); + newLink = _publishedUrlProvider.GetMediaUrl(tagData.Udi.Guid); } - if (newLink == null) - { - newLink = "#"; - } - text = text.Replace(tagValue, "href=\"" + newLink); + text = StripTypeAttributeFromTag(text, tagData.Udi!.EntityType); + text = text.Replace(tagData.TagHref, "href=\"" + newLink); } - else if (intId.HasValue) + else if (tagData.IntId.HasValue) { - var newLink = _publishedUrlProvider.GetUrl(intId.Value); - text = text.Replace(tagValue, "href=\"" + newLink); + var newLink = _publishedUrlProvider.GetUrl(tagData.IntId.Value); + text = text.Replace(tagData.TagHref, "href=\"" + newLink); } } return text; } - private IEnumerable<(int? intId, GuidUdi? udi, string tagValue)> FindLocalLinkIds(string text) + // under normal circumstances, the type attribute is preceded by a space + // to cover the rare occasion where it isn't, we first replace with a a space and then without. + private string StripTypeAttributeFromTag(string tag, string type) => + tag.Replace($" type=\"{type}\"", string.Empty) + .Replace($"type=\"{type}\"", string.Empty); + + private IEnumerable FindLocalLinkIds(string text) { MatchCollection localLinkTagMatches = LocalLinkTagPattern.Matches(text); foreach (Match linkTag in localLinkTagMatches) @@ -126,18 +129,22 @@ public sealed class HtmlLocalLinkParser continue; } - yield return (null, new GuidUdi(linkTag.Groups["type"].Value, guid), linkTag.Groups["locallink"].Value); + yield return new LocalLinkTag( + null, + new GuidUdi(linkTag.Groups["type"].Value, guid), + linkTag.Groups["locallink"].Value, + linkTag.Value); } // also return legacy results for values that have not been migrated - foreach ((int? intId, GuidUdi? udi, string tagValue) legacyResult in FindLegacyLocalLinkIds(text)) + foreach (LocalLinkTag legacyResult in FindLegacyLocalLinkIds(text)) { yield return legacyResult; } } // todo remove at some point? - private IEnumerable<(int? intId, GuidUdi? udi, string tagValue)> FindLegacyLocalLinkIds(string text) + private IEnumerable FindLegacyLocalLinkIds(string text) { // Parse internal links MatchCollection tags = LocalLinkPattern.Matches(text); @@ -153,15 +160,41 @@ public sealed class HtmlLocalLinkParser var guidUdi = udi as GuidUdi; if (guidUdi is not null) { - yield return (null, guidUdi, tag.Value); + yield return new LocalLinkTag(null, guidUdi, tag.Value, null); } } if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intId)) { - yield return (intId, null, tag.Value); + yield return new LocalLinkTag (intId, null, tag.Value, null); } } } } + + private class LocalLinkTag + { + public LocalLinkTag(int? intId, GuidUdi? udi, string tagHref) + { + IntId = intId; + Udi = udi; + TagHref = tagHref; + } + + public LocalLinkTag(int? intId, GuidUdi? udi, string tagHref, string? fullTag) + { + IntId = intId; + Udi = udi; + TagHref = tagHref; + FullTag = fullTag; + } + + public int? IntId { get; } + + public GuidUdi? Udi { get; } + + public string TagHref { get; } + + public string? FullTag { get; } + } } diff --git a/src/Umbraco.Core/UdiRange.cs b/src/Umbraco.Core/UdiRange.cs index dd497e2f9e..b91b811b06 100644 --- a/src/Umbraco.Core/UdiRange.cs +++ b/src/Umbraco.Core/UdiRange.cs @@ -75,17 +75,25 @@ public class UdiRange public static bool operator !=(UdiRange range1, UdiRange range2) => !(range1 == range2); - public static UdiRange Parse(string s) + public static UdiRange Parse(string value) { - if (Uri.IsWellFormedUriString(s, UriKind.Absolute) == false - || Uri.TryCreate(s, UriKind.Absolute, out Uri? uri) == false) + if (Uri.TryCreate(value, UriKind.Absolute, out Uri? uri) is false || + uri.IsWellFormedOriginalString() is false) { - // if (tryParse) return false; - throw new FormatException(string.Format("String \"{0}\" is not a valid udi range.", s)); + throw new FormatException($"String \"{value}\" is not a valid UDI range."); } - Uri udiUri = uri.Query == string.Empty ? uri : new UriBuilder(uri) { Query = string.Empty }.Uri; - return new UdiRange(Udi.Create(udiUri), uri.Query.TrimStart(Constants.CharArrays.QuestionMark)); + // Remove selector from UDI + Uri udiUri = string.IsNullOrEmpty(uri.Query) + ? uri + : new UriBuilder(uri) { Query = string.Empty }.Uri; + + var udi = Udi.Create(udiUri); + + // Only specify selector if query string is not empty + return string.IsNullOrEmpty(uri.Query) + ? new UdiRange(udi) + : new UdiRange(udi, uri.Query.TrimStart(Constants.CharArrays.QuestionMark)); } public override string ToString() => _uriValue.ToString(); diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs index 338966dc39..c6a4a3c956 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs @@ -60,9 +60,18 @@ internal sealed class ApiRichTextMarkupParser : ApiRichTextParserBase, IApiRichT link.SetAttributeValue("href", route.Path); link.SetAttributeValue("data-start-item-path", route.StartItem.Path); link.SetAttributeValue("data-start-item-id", route.StartItem.Id.ToString("D")); + link.Attributes["type"]?.Remove(); }, - url => link.SetAttributeValue("href", url), - () => link.Attributes.Remove("href")); + url => + { + link.SetAttributeValue("href", url); + link.Attributes["type"]?.Remove(); + }, + () => + { + link.Attributes.Remove("href"); + link.Attributes["type"]?.Remove(); + }); } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index e12c96557c..6201691d9f 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -407,6 +407,11 @@ public static partial class UmbracoBuilderExtensions builder .AddNotificationHandler(); + // Handlers for save warnings + builder + .AddNotificationAsyncHandler() + .AddNotificationAsyncHandler(); + return builder; } diff --git a/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs b/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs index c2a434dd54..48113586b0 100644 --- a/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs @@ -2,12 +2,14 @@ // See LICENSE for more details. using System.Globalization; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; namespace Umbraco.Cms.Core.Events; @@ -21,21 +23,35 @@ public sealed class RelateOnTrashNotificationHandler : private readonly IAuditService _auditService; private readonly IEntityService _entityService; private readonly IRelationService _relationService; - private readonly IScopeProvider _scopeProvider; + private readonly ICoreScopeProvider _scopeProvider; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly ILocalizedTextService _textService; + [Obsolete("Use the new constructor instead, will be removed in V16")] public RelateOnTrashNotificationHandler( IRelationService relationService, IEntityService entityService, ILocalizedTextService textService, IAuditService auditService, IScopeProvider scopeProvider) + : this(relationService, entityService, textService, auditService, scopeProvider, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public RelateOnTrashNotificationHandler( + IRelationService relationService, + IEntityService entityService, + ILocalizedTextService textService, + IAuditService auditService, + IScopeProvider scopeProvider, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { _relationService = relationService; _entityService = entityService; _textService = textService; _auditService = auditService; _scopeProvider = scopeProvider; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } public void Handle(ContentMovedNotification notification) @@ -56,7 +72,7 @@ public sealed class RelateOnTrashNotificationHandler : public void Handle(ContentMovedToRecycleBinNotification notification) { - using (IScope scope = _scopeProvider.CreateScope()) + using (ICoreScope scope = _scopeProvider.CreateCoreScope()) { const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias; IRelationType? relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias); @@ -90,7 +106,7 @@ public sealed class RelateOnTrashNotificationHandler : _auditService.Add( AuditType.Delete, - item.Entity.WriterId, + _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? item.Entity.WriterId, item.Entity.Id, UmbracoObjectTypes.Document.GetName(), string.Format(_textService.Localize("recycleBin", "contentTrashed"), item.Entity.Id, originalParentId)); @@ -118,7 +134,7 @@ public sealed class RelateOnTrashNotificationHandler : public void Handle(MediaMovedToRecycleBinNotification notification) { - using (IScope scope = _scopeProvider.CreateScope()) + using (ICoreScope scope = _scopeProvider.CreateCoreScope()) { const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; IRelationType? relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias); @@ -150,7 +166,7 @@ public sealed class RelateOnTrashNotificationHandler : _relationService.Save(relation); _auditService.Add( AuditType.Delete, - item.Entity.CreatorId, + _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? item.Entity.WriterId, item.Entity.Id, UmbracoObjectTypes.Media.GetName(), string.Format(_textService.Localize("recycleBin", "mediaTrashed"), item.Entity.Id, originalParentId)); diff --git a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs index a35f7aa956..c6f21738c2 100644 --- a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs +++ b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs @@ -129,7 +129,7 @@ public abstract class RecurringHostedServiceBase : IHostedService, IDisposable /// Executes the task. /// /// The task state. - public async void ExecuteAsync(object? state) + public virtual async void ExecuteAsync(object? state) { try { diff --git a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs index ed0944036b..2455130e51 100644 --- a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs +++ b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs @@ -51,7 +51,6 @@ public class FilePermissionHelper : IFilePermissionHelper { hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Bin), hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Umbraco), - hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoPath), hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Packages), }; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index e8fdac3925..54a73f7839 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -89,6 +89,9 @@ public class UmbracoPlan : MigrationPlan To("{FEF2DAF4-5408-4636-BB0E-B8798DF8F095}"); To("{A385C5DF-48DC-46B4-A742-D5BB846483BC}"); + // To 14.2.0 + To("{20ED404C-6FF9-4F91-8AC9-2B298E0002EB}"); + // To 15.0.0 To("{7F4F31D8-DD71-4F0D-93FC-2690A924D84B}"); To("{1A8835EF-F8AB-4472-B4D8-D75B7C164022}"); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_2_0/AddMissingDateTimeConfiguration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_2_0/AddMissingDateTimeConfiguration.cs new file mode 100644 index 0000000000..6691ed7e5b --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_2_0/AddMissingDateTimeConfiguration.cs @@ -0,0 +1,50 @@ +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_14_2_0; + +public class AddMissingDateTimeConfiguration : MigrationBase +{ + private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; + + public AddMissingDateTimeConfiguration(IMigrationContext context, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + : base(context) + => _configurationEditorJsonSerializer = configurationEditorJsonSerializer; + + protected override void Migrate() + { + Sql sql = Sql() + .Select() + .From() + .Where(dto => + dto.NodeId == Constants.DataTypes.DateTime + && dto.EditorAlias.Equals(Constants.PropertyEditors.Aliases.DateTime)); + + DataTypeDto? dataTypeDto = Database.FirstOrDefault(sql); + if (dataTypeDto is null) + { + return; + } + + Dictionary configurationData = dataTypeDto.Configuration.IsNullOrWhiteSpace() + ? new Dictionary() + : _configurationEditorJsonSerializer + .Deserialize>(dataTypeDto.Configuration)? + .Where(item => item.Value is not null) + .ToDictionary(item => item.Key, item => item.Value!) + ?? new Dictionary(); + + // only proceed with the migration if the data-type has no format assigned + if (configurationData.TryAdd("format", "YYYY-MM-DD HH:mm:ss") is false) + { + return; + } + + dataTypeDto.Configuration = _configurationEditorJsonSerializer.Serialize(configurationData); + Database.Update(dataTypeDto); + } +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs index 043c4088e9..98c394d584 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs @@ -13,7 +13,7 @@ using Umbraco.Cms.Core.Strings; namespace Umbraco.Cms.Core.PropertyEditors; -internal abstract class BlockEditorPropertyValueEditor : BlockValuePropertyValueEditorBase +public abstract class BlockEditorPropertyValueEditor : BlockValuePropertyValueEditorBase where TValue : BlockValue, new() where TLayout : class, IBlockLayoutItem, new() { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs index 685dbae9aa..332c414628 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs @@ -7,7 +7,7 @@ using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.PropertyEditors; -internal class BlockEditorValidator : BlockEditorValidatorBase +public class BlockEditorValidator : BlockEditorValidatorBase where TValue : BlockValue, new() where TLayout : class, IBlockLayoutItem, new() { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs index 8452284cc9..cb8a69e446 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs @@ -6,7 +6,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; -internal abstract class BlockEditorValidatorBase : ComplexEditorValidator +public abstract class BlockEditorValidatorBase : ComplexEditorValidator where TValue : BlockValue, new() where TLayout : class, IBlockLayoutItem, new() { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs index bf8ccee15f..87c78fa119 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs @@ -10,9 +10,9 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; /// -/// Used to deserialize json values and clean up any values based on the existence of element types and layout structure +/// Used to deserialize json values and clean up any values based on the existence of element types and layout structure. /// -internal class BlockEditorValues +public class BlockEditorValues where TValue : BlockValue, new() where TLayout : class, IBlockLayoutItem, new() { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditor.cs index 8b6ef4862c..6fe0e3882d 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditor.cs @@ -22,6 +22,7 @@ public class BlockGridPropertyEditor : BlockGridPropertyEditorBase : base(dataValueEditorFactory, blockValuePropertyIndexValueFactory) => _ioHelper = ioHelper; + public override bool SupportsConfigurableElements => true; #region Pre Value Editor diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs index bfd0e61e0b..254933eb8f 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs @@ -111,6 +111,12 @@ public abstract class BlockGridPropertyEditorBase : DataEditor return validationResults; } } + + public override IEnumerable ConfiguredElementTypeKeys() + { + var configuration = ConfigurationObject as BlockGridConfiguration; + return configuration?.Blocks.SelectMany(ConfiguredElementTypeKeys) ?? Enumerable.Empty(); + } } #endregion diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs index ca10cc8f76..bbb368dec1 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs @@ -36,6 +36,8 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase { } + public override bool SupportsConfigurableElements => true; + #region Pre Value Editor protected override IConfigurationEditor CreateConfigurationEditor() => diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs index 8d84254f16..2e37056cd2 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs @@ -93,6 +93,12 @@ public abstract class BlockListPropertyEditorBase : DataEditor return ValidateNumberOfBlocks(blockEditorData, validationLimit.Min, validationLimit.Max); } } + + public override IEnumerable ConfiguredElementTypeKeys() + { + var configuration = ConfigurationObject as BlockListConfiguration; + return configuration?.Blocks.SelectMany(ConfiguredElementTypeKeys) ?? Enumerable.Empty(); + } } #endregion diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs index dd62a7d5b9..f2178fd71a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs @@ -10,7 +10,7 @@ using Umbraco.Cms.Core.Strings; namespace Umbraco.Cms.Core.PropertyEditors; -internal abstract class BlockValuePropertyValueEditorBase : DataValueEditor, IDataValueReference, IDataValueTags +public abstract class BlockValuePropertyValueEditorBase : DataValueEditor, IDataValueReference, IDataValueTags where TValue : BlockValue, new() where TLayout : class, IBlockLayoutItem, new() { @@ -114,6 +114,15 @@ internal abstract class BlockValuePropertyValueEditorBase : Dat MapBlockItemDataToEditor(property, blockValue.SettingsData); } + protected IEnumerable ConfiguredElementTypeKeys(IBlockConfiguration configuration) + { + yield return configuration.ContentElementTypeKey; + if (configuration.SettingsElementTypeKey is not null) + { + yield return configuration.SettingsElementTypeKey.Value; + } + } + private void MapBlockItemDataToEditor(IProperty property, List items) { var valEditors = new Dictionary(); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index 619e98743d..025bdd887c 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -47,6 +47,8 @@ public class RichTextPropertyEditor : DataEditor public override IPropertyIndexValueFactory PropertyIndexValueFactory => _richTextPropertyIndexValueFactory; + public override bool SupportsConfigurableElements => true; + /// /// Create a custom value editor /// @@ -238,6 +240,12 @@ public class RichTextPropertyEditor : DataEditor return RichTextPropertyEditorHelper.SerializeRichTextEditorValue(cleanedUpRichTextEditorValue, _jsonSerializer); } + public override IEnumerable ConfiguredElementTypeKeys() + { + var configuration = ConfigurationObject as RichTextConfiguration; + return configuration?.Blocks?.SelectMany(ConfiguredElementTypeKeys) ?? Enumerable.Empty(); + } + private bool TryParseEditorValue(object? value, [NotNullWhen(true)] out RichTextEditorValue? richTextEditorValue) => RichTextPropertyEditorHelper.TryParseRichTextEditorValue(value, _jsonSerializer, _logger, out richTextEditorValue); diff --git a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs index a321875571..6731ae1fce 100644 --- a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs @@ -113,7 +113,7 @@ public class IdentityMapDefinition : IMapDefinition target.PasswordConfig = source.PasswordConfiguration; target.IsApproved = source.IsApproved; target.SecurityStamp = source.SecurityStamp; - DateTime? lockedOutUntil = source.LastLockoutDate?.AddMinutes(_securitySettings.UserDefaultLockoutTimeInMinutes); + DateTime? lockedOutUntil = source.LastLockoutDate?.AddMinutes(_securitySettings.MemberDefaultLockoutTimeInMinutes); target.LockoutEnd = source.IsLockedOut ? (lockedOutUntil ?? DateTime.MaxValue).ToUniversalTime() : null; target.Comments = source.Comments; target.LastLockoutDateUtc = source.LastLockoutDate == DateTime.MinValue diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs index f76b176d5e..0d67d2a8e3 100644 --- a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs +++ b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs @@ -221,10 +221,17 @@ AND cmsContentNu.nodeId IS NULL .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)) .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + // Use a more efficient COUNT query + Sql? sqlCountQuery = SqlContentSourcesCount() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)); + + Sql? sqlCount = + SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); + IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); - IEnumerable dtos = GetContentNodeDtos(sql); + IEnumerable dtos = GetContentNodeDtos(sql, sqlCount); foreach (ContentSourceDto row in dtos) { @@ -239,10 +246,18 @@ AND cmsContentNu.nodeId IS NULL .Append(SqlWhereNodeIdX(SqlContext, id)) .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + // Use a more efficient COUNT query + Sql? sqlCountQuery = SqlContentSourcesCount(SqlContentSourcesSelectUmbracoNodeJoin) + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)) + .Append(SqlWhereNodeIdX(SqlContext, id)); + + Sql? sqlCount = + SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); + IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); - IEnumerable dtos = GetContentNodeDtos(sql); + IEnumerable dtos = GetContentNodeDtos(sql, sqlCount); foreach (ContentSourceDto row in dtos) { @@ -262,10 +277,18 @@ AND cmsContentNu.nodeId IS NULL .WhereIn(x => x.ContentTypeId, ids) .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + // Use a more efficient COUNT query + Sql sqlCountQuery = SqlContentSourcesCount() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)) + .WhereIn(x => x.ContentTypeId, ids); + + Sql? sqlCount = + SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); + IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); - IEnumerable dtos = GetContentNodeDtos(sql); + IEnumerable dtos = GetContentNodeDtos(sql, sqlCount); foreach (ContentSourceDto row in dtos) { @@ -1015,27 +1038,14 @@ WHERE cmsContentNu.nodeId IN ( return dtos; } - private IEnumerable GetContentNodeDtos(Sql sql) + private IEnumerable GetContentNodeDtos(Sql sql, Sql sqlCount) { // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. // QueryPaged is very slow on large sites however, so use fetch if UsePagedSqlQuery is disabled. - IEnumerable dtos; - if (_nucacheSettings.Value.UsePagedSqlQuery) - { - // Use a more efficient COUNT query - Sql? sqlCountQuery = SqlContentSourcesCount() - .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)); - - Sql? sqlCount = - SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); - - dtos = Database.QueryPaged(_nucacheSettings.Value.SqlPageSize, sql, sqlCount); - } - else - { - dtos = Database.Fetch(sql); - } + IEnumerable dtos = _nucacheSettings.Value.UsePagedSqlQuery ? + Database.QueryPaged(_nucacheSettings.Value.SqlPageSize, sql, sqlCount) : + Database.Fetch(sql); return dtos; } diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs index 8d471428de..324781b5a3 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs @@ -43,14 +43,14 @@ public class AspNetCoreHostingEnvironment : IHostingEnvironment _webHostEnvironment = webHostEnvironment ?? throw new ArgumentNullException(nameof(webHostEnvironment)); _urlProviderMode = _webRoutingSettings.CurrentValue.UrlProviderMode; - SetSiteName(hostingSettings.CurrentValue.SiteName); + SetSiteNameAndDebugMode(hostingSettings.CurrentValue); // We have to ensure that the OptionsMonitor is an actual options monitor since we have a hack // where we initially use an OptionsMonitorAdapter, which doesn't implement OnChange. // See summery of OptionsMonitorAdapter for more information. if (hostingSettings is OptionsMonitor) { - hostingSettings.OnChange(settings => SetSiteName(settings.SiteName)); + hostingSettings.OnChange(settings => SetSiteNameAndDebugMode(settings)); } ApplicationPhysicalPath = webHostEnvironment.ContentRootPath; @@ -95,7 +95,7 @@ public class AspNetCoreHostingEnvironment : IHostingEnvironment _hostingSettings.CurrentValue.ApplicationVirtualPath?.EnsureStartsWith('/') ?? "/"; /// - public bool IsDebugMode => _hostingSettings.CurrentValue.Debug; + public bool IsDebugMode { get; private set; } public string LocalTempPath { @@ -188,8 +188,12 @@ public class AspNetCoreHostingEnvironment : IHostingEnvironment } } - private void SetSiteName(string? siteName) => - SiteName = string.IsNullOrWhiteSpace(siteName) + private void SetSiteNameAndDebugMode(HostingSettings hostingSettings) + { + SiteName = string.IsNullOrWhiteSpace(hostingSettings.SiteName) ? _webHostEnvironment.ApplicationName - : siteName; + : hostingSettings.SiteName; + + IsDebugMode = hostingSettings.Debug; + } } diff --git a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs index 4256f29c25..9b54a3912a 100644 --- a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs +++ b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs @@ -52,6 +52,7 @@ public static class AuthorizationPolicies public const string TreeAccessDocumentsOrDocumentTypes = nameof(TreeAccessDocumentsOrDocumentTypes); public const string TreeAccessMediaOrMediaTypes = nameof(TreeAccessMediaOrMediaTypes); public const string TreeAccessDictionaryOrTemplates = nameof(TreeAccessDictionaryOrTemplates); + public const string TreeAccessDocumentOrMediaOrContentTypes = nameof(TreeAccessDocumentOrMediaOrContentTypes); // other public const string DictionaryPermissionByResource = nameof(DictionaryPermissionByResource); diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index 0c74f2a34c..a9d3a43969 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit 0c74f2a34c72de17bb84e0805fd70ba4a44eb4be +Subproject commit a9d3a4396968e4cc47c1d1cd290ca8b1cf764e12 diff --git a/src/Umbraco.Web.UI/Program.cs b/src/Umbraco.Web.UI/Program.cs index e91f6ba60d..8fca919b0a 100644 --- a/src/Umbraco.Web.UI/Program.cs +++ b/src/Umbraco.Web.UI/Program.cs @@ -3,7 +3,9 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.CreateUmbracoBuilder() .AddBackOffice() .AddWebsite() +#if UseDeliveryApi .AddDeliveryApi() +#endif .AddComposers() .Build(); @@ -23,6 +25,9 @@ app.UseUmbraco() }) .WithEndpoints(u => { + /*#if (UmbracoRelease = 'LTS') + u.UseInstallerEndpoints(); + #endif */ u.UseBackOfficeEndpoints(); u.UseWebsiteEndpoints(); }); diff --git a/src/Umbraco.Web.UI/appsettings.template.json b/src/Umbraco.Web.UI/appsettings.template.json index 698eb83955..35a41bcedb 100644 --- a/src/Umbraco.Web.UI/appsettings.template.json +++ b/src/Umbraco.Web.UI/appsettings.template.json @@ -41,7 +41,7 @@ "KeepUserLoggedIn": false, "UsernameIsEmail": true, "HideDisabledUsersInBackoffice": false, - "AllowedUserNameCharacters": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+\\", + "AllowedUserNameCharacters": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-'._@+\\", "UserPassword": { "RequiredLength": 10, "RequireNonLetterOrDigit": false, diff --git a/templates/Umbraco.Templates.csproj b/templates/Umbraco.Templates.csproj index 8301264ec8..b75df9f9a5 100644 --- a/templates/Umbraco.Templates.csproj +++ b/templates/Umbraco.Templates.csproj @@ -11,7 +11,6 @@ . NU5128 - UmbracoProject\Program.cs @@ -20,6 +19,7 @@ + UmbracoProject\Views\Partials\blocklist\%(RecursiveDir)%(Filename)%(Extension) UmbracoProject\Views\Partials\blocklist @@ -32,13 +32,7 @@ UmbracoProject\Views\_ViewImports.cshtml UmbracoProject\Views - - UmbracoProject\wwwroot\favicon.ico - UmbracoProject\wwwroot - - - @@ -54,7 +48,7 @@ - + <_PackageFiles Include="%(_TemplateJsonFiles.DestinationFile)"> %(_TemplateJsonFiles.RelativeDir) diff --git a/templates/UmbracoDockerCompose/.env b/templates/UmbracoDockerCompose/.env new file mode 100644 index 0000000000..38985be5c5 --- /dev/null +++ b/templates/UmbracoDockerCompose/.env @@ -0,0 +1 @@ +DB_PASSWORD=Password1234 diff --git a/templates/UmbracoDockerCompose/.template.config/dotnetcli.host.json b/templates/UmbracoDockerCompose/.template.config/dotnetcli.host.json new file mode 100644 index 0000000000..aaab590168 --- /dev/null +++ b/templates/UmbracoDockerCompose/.template.config/dotnetcli.host.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/dotnetcli.host.json", + "symbolInfo": { + "ProjectName": { + "longName": "ProjectName", + "shortName": "P" + }, + "DatabasePassword": { + "longName": "DatabasePassword", + "shortName": "dbpw" + }, + "Port": + { + "longName": "Port", + "shortName": "p" + } + }, + "usageExamples": [ + "dotnet new umbraco-compose -P MyProject", + "dotnet new umbraco-compose --ProjectName MyProject", + "dotnet new umbraco-compose -P -MyProject -dbpw MyStr0ngP@ssword", + "dotnet new umbraco-compose -P -MyProject --DatabasePassword MyStr0ngP@ssword" + ] +} diff --git a/templates/UmbracoDockerCompose/.template.config/ide.host.json b/templates/UmbracoDockerCompose/.template.config/ide.host.json new file mode 100644 index 0000000000..ae8c5fb05c --- /dev/null +++ b/templates/UmbracoDockerCompose/.template.config/ide.host.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/ide.host.json", + "order": 0, + "icon": "../../icon.png", + "description": { + "id": "UmbracoDockerCompose", + "text": "Umbraco Docker Compose - Docker compose for Umbraco CMS and associated database" + }, + "symbolInfo": [ + { + "id": "ProjectName", + "isVisible": true + }, + { + "id": "DatabasePassword", + "isVisible": true + }, + { + "id": "Port", + "isVisible": true + } + ] +} diff --git a/templates/UmbracoDockerCompose/.template.config/template.json b/templates/UmbracoDockerCompose/.template.config/template.json new file mode 100644 index 0000000000..6f6877b7e0 --- /dev/null +++ b/templates/UmbracoDockerCompose/.template.config/template.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Umbraco HQ", + "classifications": [ + "Web", + "CMS", + "Umbraco" + ], + "name": "Umbraco Docker Compose", + "description": "Creates the prerequisites for developing Umbraco in Docker containers", + "groupIdentity": "Umbraco.Templates.UmbracoDockerCompose", + "identity": "Umbraco.Templates.UmbracoDockerCompose", + "shortName": "umbraco-compose", + "tags": { + "type": "item" + }, + "symbols": { + "ProjectName": { + "type": "parameter", + "description": "The name of the project the Docker Compose file will be created for", + "datatype": "string", + "replaces": "UmbracoProject", + "isRequired": true + }, + "DatabasePassword": { + "type": "parameter", + "description": "The password to the database, will be stored in .env file", + "datatype": "string", + "replaces": "Password1234", + "defaultValue": "Password1234" + }, + "Port": { + "type": "parameter", + "description": "The port forward on the docker container, this is the port you use to access the site", + "datatype": "string", + "replaces": "TEMPLATE_PORT", + "defaultValue": "44372" + }, + "ImageName": { + "type": "generated", + "generator": "casing", + "parameters": { + "source": "ProjectName", + "toLower": true + }, + "replaces": "umbraco_image" + } + } +} diff --git a/templates/UmbracoDockerCompose/Database/Dockerfile b/templates/UmbracoDockerCompose/Database/Dockerfile new file mode 100644 index 0000000000..4e74b6435e --- /dev/null +++ b/templates/UmbracoDockerCompose/Database/Dockerfile @@ -0,0 +1,21 @@ +FROM mcr.microsoft.com/azure-sql-edge:latest + +ENV ACCEPT_EULA=Y + +USER root + +RUN mkdir /var/opt/sqlserver + +RUN chown mssql /var/opt/sqlserver + +ENV MSSQL_BACKUP_DIR="/var/opt/mssql" +ENV MSSQL_DATA_DIR="/var/opt/mssql/data" +ENV MSSQL_LOG_DIR="/var/opt/mssql/log" + +EXPOSE 1433/tcp +COPY setup.sql / +COPY startup.sh / +COPY healthcheck.sh / + +ENTRYPOINT [ "/bin/bash", "startup.sh" ] +CMD [ "/opt/mssql/bin/sqlservr" ] diff --git a/templates/UmbracoDockerCompose/Database/healthcheck.sh b/templates/UmbracoDockerCompose/Database/healthcheck.sh new file mode 100644 index 0000000000..2964e17dc4 --- /dev/null +++ b/templates/UmbracoDockerCompose/Database/healthcheck.sh @@ -0,0 +1,15 @@ +value="$(/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -d master -Q "SELECT state_desc FROM sys.databases WHERE name = 'umbracoDb'" | awk 'NR==3')" + +# This checks for any non-zero length string, and $value will be empty when the database does not exist. +if [ -n "$value" ] +then + echo "ONLINE" + return 0 # With docker 0 = success +else + echo "OFFLINE" + return 1 # And 1 = unhealthy +fi + +# This is useful for debugging +# echo "Value is:" +# echo "$value" diff --git a/templates/UmbracoDockerCompose/Database/setup.sql b/templates/UmbracoDockerCompose/Database/setup.sql new file mode 100644 index 0000000000..466030dd50 --- /dev/null +++ b/templates/UmbracoDockerCompose/Database/setup.sql @@ -0,0 +1,10 @@ +USE [master] +GO + +IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'UmbracoDb') + BEGIN + CREATE DATABASE [umbracoDb] + END; +GO + +USE UmbracoDb; \ No newline at end of file diff --git a/templates/UmbracoDockerCompose/Database/startup.sh b/templates/UmbracoDockerCompose/Database/startup.sh new file mode 100644 index 0000000000..c4fad8f0ae --- /dev/null +++ b/templates/UmbracoDockerCompose/Database/startup.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e + +# Taken from: https://github.com/CarlSargunar/Umbraco-Docker-Workshop +if [ "$1" = '/opt/mssql/bin/sqlservr' ]; then + # If this is the container's first run, initialize the application database + if [ ! -f /tmp/app-initialized ]; then + # Initialize the application database asynchronously in a background process. This allows a) the SQL Server process to be the main process in the container, which allows graceful shutdown and other goodies, and b) us to only start the SQL Server process once, as opposed to starting, stopping, then starting it again. + function initialize_app_database() { + # Wait a bit for SQL Server to start. SQL Server's process doesn't provide a clever way to check if it's up or not, and it needs to be up before we can import the application database + sleep 15s + + #run the setup script to create the DB and the schema in the DB + /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -d master -i setup.sql + + # Note that the container has been initialized so future starts won't wipe changes to the data + touch /tmp/app-initialized + } + initialize_app_database & + fi +fi + +exec "$@" diff --git a/templates/UmbracoDockerCompose/docker-compose.yml b/templates/UmbracoDockerCompose/docker-compose.yml new file mode 100644 index 0000000000..7acae5d8d6 --- /dev/null +++ b/templates/UmbracoDockerCompose/docker-compose.yml @@ -0,0 +1,102 @@ +services: + umb_database: + container_name: umbraco_image_database + build: + context: ./Database + environment: + SA_PASSWORD: ${DB_PASSWORD} + MSSQL_SA_PASSWORD: ${DB_PASSWORD} + ports: + - "1433:1433" + - "1434:1434" + volumes: + - umb_database:/var/opt/mssql + networks: + - umbnet + healthcheck: + # This healthcheck is to make sure that the database is up and running before the umbraco container starts. + # It works by querying the database for the state of the umbracoDb database, ensuring it exists. + test: ./healthcheck.sh + interval: 5m + timeout: 5s + retries: 3 + start_period: 15s # Bootstrap duration, for this duration failures does not count towards max retries. + start_interval: 5s # How long after the health check has started to run the healthcheck again. + + umbraco_image: + image: umbraco_image + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ConnectionStrings__umbracoDbDSN=Server=umb_database;Database=umbracoDb;User Id=sa;Password=${DB_PASSWORD};TrustServerCertificate=true; + - ConnectionStrings__umbracoDbDSN_ProviderName=Microsoft.Data.SqlClient + volumes: + - umb_media:/app/wwwroot/media + - umb_scripts:/app/wwwroot/scripts + - umb_styles:/app/wwwroot/css + - umb_logs:/app/umbraco/Logs + - umb_views:/app/Views + - umb_data:/app/umbraco + - umb_models:/app/umbraco/models + build: + context: . + dockerfile: UmbracoProject/Dockerfile + args: + - BUILD_CONFIGURATION=Debug + + depends_on: + umb_database: + condition: service_healthy + restart: always + ports: + - "TEMPLATE_PORT:8080" + networks: + - umbnet + develop: + # This allows you to run docker compose watch, after doing so the container will rebuild when the models are changed. + # Once a restart only feature is implemented (https://github.com/docker/compose/issues/11446) + # It would be really nice to add a restart only watch to \Views, since the file watchers for recompilation of Razor views does not work with docker. + watch: + - path: ./UmbracoProject/umbraco/models + action: rebuild + +# These volumes are all made as bind mounts, meaning that they are bound to the host machine's file system. +# This is to better facilitate local development in the IDE, so the views, models, etc... are available in the IDE. +# This can be changed by removing the driver and driver_opts from the volumes. +volumes: + umb_media: + driver: local + driver_opts: + type: none + device: ./UmbracoProject/wwwroot/media + o: bind + umb_scripts: + driver: local + driver_opts: + type: none + device: ./UmbracoProject/wwwroot/scripts + o: bind + umb_styles: + driver: local + driver_opts: + type: none + device: ./UmbracoProject/wwwroot/css + o: bind + umb_logs: + umb_views: + driver: local + driver_opts: + type: none + device: ./UmbracoProject/Views + o: bind + umb_data: + umb_models: + driver: local + driver_opts: + type: none + device: ./UmbracoProject/umbraco/models + o: bind + umb_database: + +networks: + umbnet: + driver: bridge diff --git a/templates/UmbracoProject/.dockerignore b/templates/UmbracoProject/.dockerignore new file mode 100644 index 0000000000..2f32bfe4fe --- /dev/null +++ b/templates/UmbracoProject/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/templates/UmbracoProject/.template.config/dotnetcli.host.json b/templates/UmbracoProject/.template.config/dotnetcli.host.json index dfd6f80184..b18020b6de 100644 --- a/templates/UmbracoProject/.template.config/dotnetcli.host.json +++ b/templates/UmbracoProject/.template.config/dotnetcli.host.json @@ -8,12 +8,25 @@ }, "UmbracoVersion": { "longName": "version", - "shortName": "v" + "shortName": "v", + "isHidden": true + }, + "UmbracoRelease": { + "longName": "release", + "shortName": "r" }, "UseHttpsRedirect": { "longName": "use-https-redirect", "shortName": "" }, + "UseDeliveryApi": { + "longName": "use-delivery-api", + "shortName": "da" + }, + "Docker": { + "longName": "add-docker", + "shortName": "" + }, "SkipRestore": { "longName": "no-restore", "shortName": "" @@ -58,6 +71,18 @@ "longName": "PackageTestSiteName", "shortName": "p", "isHidden": true + }, + "ModelsBuilderMode": { + "longName": "models-mode", + "shortName": "mm" + }, + "StarterKit": { + "longName": "starter-kit", + "shortName": "sk" + }, + "DevelopmentMode": { + "longName": "development-mode", + "shortName": "dm" } }, "usageExamples": [ diff --git a/templates/UmbracoProject/.template.config/ide.host.json b/templates/UmbracoProject/.template.config/ide.host.json index 1a302779cc..90de3b977a 100644 --- a/templates/UmbracoProject/.template.config/ide.host.json +++ b/templates/UmbracoProject/.template.config/ide.host.json @@ -9,11 +9,22 @@ "symbolInfo": [ { "id": "UmbracoVersion", - "isVisible": true + "isVisible": false }, { "id": "UseHttpsRedirect", "isVisible": true, + "persistenceScope": "templateGroup", + "defaultValue": "true" + }, + { + "id": "UseDeliveryApi", + "isVisible": true, + "persistenceScope": "templateGroup" + }, + { + "id": "ModelsBuilderMode", + "isVisible": true, "persistenceScope": "templateGroup" }, { @@ -54,6 +65,23 @@ { "id": "NoNodesViewPath", "isVisible": true + }, + { + "id": "Docker", + "isVisible": true + }, + { + "id": "StarterKit", + "isVisible": true + }, + { + "id": "UmbracoRelease", + "isVisible": true + }, + { + "id": "DevelopmentMode", + "isVisible": true, + "defaultValue": "IDEDevelopment" } ] } diff --git a/templates/UmbracoProject/.template.config/starterkits.template.json b/templates/UmbracoProject/.template.config/starterkits.template.json new file mode 100644 index 0000000000..5d2a5dabf9 --- /dev/null +++ b/templates/UmbracoProject/.template.config/starterkits.template.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "symbols": { + "StarterKit": { + "displayName": "Starter kit", + "type": "parameter", + "datatype": "choice", + "description": "Choose a starter kit to install.", + "defaultValue": "None", + "replaces": "STARTER_KIT_NAME", + // The choice here should be the name of the starter kit package, since it will be used directly for package reference. + "choices": [ + { + "choice": "None", + "description": "No starter kit." + }, + { + "choice": "Umbraco.TheStarterKit", + "description": "The Umbraco starter kit.", + "displayName": "The Starter Kit" + } + ] + }, + // Used to determine the version of the starter kit to install. + // there should be cases for Latest, LTS and Custom for every starterkit added above. + // This has the benefit that all maintenance of starter kits in template can be done from this file. + "StarterKitVersion": { + "type": "generated", + "generator": "switch", + "replaces": "STARTER_KIT_VERSION", + "parameters": { + "evaluator": "C++", + "datatype": "string", + "cases": [ + { + "condition": "(StarterKit == 'Umbraco.TheStarterKit' && (UmbracoRelease == 'Latest' || UmbracoRelease == 'Custom'))", + "value": "14.0.0" + }, + { + "condition": "(StarterKit == 'Umbraco.TheStarterKit' && UmbracoRelease == 'LTS')", + "value": "13.0.0" + } + ] + } + } + } +} diff --git a/templates/UmbracoProject/.template.config/template.json b/templates/UmbracoProject/.template.config/template.json index 13d4ec68f7..ad802476b3 100644 --- a/templates/UmbracoProject/.template.config/template.json +++ b/templates/UmbracoProject/.template.config/template.json @@ -18,6 +18,7 @@ "sourceName": "UmbracoProject", "defaultName": "UmbracoProject1", "preferNameDirectory": true, + "additionalConfigFiles": [ "starterkits.template.json"], "sources": [ { "modifiers": [ @@ -26,6 +27,13 @@ "exclude": [ ".gitignore" ] + }, + { + "condition": "(!Docker)", + "exclude": [ + "Dockerfile", + ".dockerignore" + ] } ] } @@ -46,13 +54,72 @@ "defaultValue": "net9.0", "replaces": "net9.0" }, + "UmbracoRelease": { + "displayName": "Umbraco Version", + "description": "The Umbraco release to use, either latest or latest long term supported", + "type": "parameter", + "datatype": "choice", + "defaultValue": "Latest", + "choices": [ + { + "choice": "Latest", + "description": "The latest umbraco release" + }, + { + "choice": "LTS", + "description": "The most recent long term supported version", + "displayName": "Long Term Supported" + } + ], + "isRequired": false + }, "UmbracoVersion": { - "displayName": "Umbraco version", - "description": "The version of Umbraco.Cms to add as PackageReference.", + "displayName": "Custom Version", + "description": "The selected custom version of Umbraco, this is obsoleted, and will be removed in a future version of the template.", "type": "parameter", "datatype": "string", - "defaultValue": "*", - "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" + "defaultValue": "null", + "replaces": "CUSTOM_VERSION", + "isRequired": false + }, + "FinalVersion" : { + "type": "generated", + "generator": "switch", + "datatype": "text", + "description": "The calculated version of Umbraco to use", + "replaces": "UMBRACO_VERSION_FROM_TEMPLATE", + "parameters": { + "evaluator": "C++", + "datatype": "text", + "cases": [ + { + "condition": "(UmbracoRelease == 'Latest')", + "value": "*" + }, + { + "condition": "(UmbracoRelease == 'LTS')", + "value": "13.4.1" + } + ] + } + }, + "DotnetVersion": + { + "type": "generated", + "generator": "switch", + "datatype": "text", + "description": "Not relevant at the moment, but if we need to change the dotnet version based on the Umbraco version, we can do it here", + "replaces": "DOTNET_VERSION_FROM_TEMPLATE", + "parameters": { + "evaluator": "C++", + "datatype": "text", + "cases": [ + { + "condition": "(true)", + "value": "net8.0" + } + ] + } }, "UseHttpsRedirect": { "displayName": "Use HTTPS redirect", @@ -61,6 +128,20 @@ "datatype": "bool", "defaultValue": "false" }, + "UseDeliveryApi": { + "displayName": "Use Delivery API", + "description": "Enables the Delivery API", + "type": "parameter", + "datatype": "bool", + "defaultValue": "false" + }, + "Docker": { + "displayName": "Add Docker file", + "description": "Adds a docker file to the project.", + "type": "parameter", + "datatype": "bool", + "defaultValue": "false" + }, "SkipRestore": { "displayName": "Skip restore", "description": "If specified, skips the automatic restore of the project on create.", @@ -244,6 +325,58 @@ "defaultValue": "", "replaces": "PACKAGE_PROJECT_NAME_FROM_TEMPLATE" }, + "DevelopmentMode": { + "type": "parameter", + "displayName": "Development mode", + "datatype": "choice", + "description": "Choose the development mode to use for the project.", + "defaultValue": "BackofficeDevelopment", + "choices": [ + { + "choice": "BackofficeDevelopment", + "description": "Enables backoffice development, allowing you to develop from within the backoffice, this is the default behaviour.", + "displayName": "Backoffice Development" + }, + { + "choice": "IDEDevelopment", + "description": "Configures appsettings.Development.json to Development runtime mode and SourceCodeAuto models builder mode, and configures appsettings.json to Production runtime mode, Nothing models builder mode, and enables UseHttps", + "displayName": "IDE Development" + } + ] + }, + "ModelsBuilderMode": { + "type": "parameter", + "displayName": "Models builder mode", + "datatype": "choice", + "description": "Choose the models builder mode to use for the project. When development mode is set to IDEDevelopment this only changes the models builder mode appsetttings.development.json", + "defaultValue": "Default", + "replaces": "MODELS_MODE", + "choices": [ + { + "choice": "Default", + "description": "Let DevelopmentMode determine the models builder mode." + }, + { + "choice": "InMemoryAuto", + "description": "Generate models in memory, automatically updating when a content type change, this means no need for app rebuild, however models are only available in views.", + "displayName": "In Memory Auto" + }, + { + "choice": "SourceCodeManual", + "description": "Generate models as source code, only updating when requested manually, this means a interaction and rebuild is required when content type(s) change, however models are available in code.", + "displayName": "Source Code Manual" + }, + { + "choice": "SourceCodeAuto", + "description": "Generate models as source code, automatically updating when a content type change, this means a rebuild is required when content type(s) change, however models are available in code.", + "displayName": "Source Code Auto" + }, + { + "choice": "Nothing", + "description": "No models are generated, this is recommended for production assuming generated models are used for development." + } + ] + }, "Namespace": { "type": "derived", "valueSource": "name", diff --git a/templates/UmbracoProject/Dockerfile b/templates/UmbracoProject/Dockerfile new file mode 100644 index 0000000000..e3eda648dd --- /dev/null +++ b/templates/UmbracoProject/Dockerfile @@ -0,0 +1,33 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["UmbracoProject/UmbracoProject.csproj", "UmbracoProject/"] +RUN dotnet restore "UmbracoProject/UmbracoProject.csproj" +COPY . . +WORKDIR "/src/UmbracoProject" +RUN dotnet build "UmbracoProject.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "UmbracoProject.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +# We need to make sure that the user running the app has write access to the umbraco folder, in order to write logs and other files. +# Since these are volumes they are created as root by the docker daemon. +USER root +RUN mkdir umbraco +RUN mkdir umbraco/Logs +RUN chown $APP_UID umbraco --recursive +#if (UmbracoRelease = 'LTS') +RUN chown $APP_UID wwwroot/umbraco --recursive +#endif +USER $APP_UID +ENTRYPOINT ["dotnet", "UmbracoProject.dll"] diff --git a/templates/UmbracoProject/UmbracoProject.csproj b/templates/UmbracoProject/UmbracoProject.csproj index 544456bcd6..d411fd4acc 100644 --- a/templates/UmbracoProject/UmbracoProject.csproj +++ b/templates/UmbracoProject/UmbracoProject.csproj @@ -1,13 +1,21 @@ net9.0 + DOTNET_VERSION_FROM_TEMPLATE enable enable Umbraco.Cms.Web.UI + + + + @@ -21,11 +29,13 @@ true + false false + diff --git a/templates/UmbracoProject/appsettings.Development.json b/templates/UmbracoProject/appsettings.Development.json index 17b9f86361..0521b835ed 100644 --- a/templates/UmbracoProject/appsettings.Development.json +++ b/templates/UmbracoProject/appsettings.Development.json @@ -25,6 +25,11 @@ //#endif "Umbraco": { "CMS": { + //#if (UseHttpsRedirect || DevelopmentMode == "IDEDevelopment") + "Global": { + "UseHttps": false + }, + //#endif //#if (UsingUnattenedInstall) "Unattended": { "InstallUnattended": true, @@ -36,12 +41,22 @@ "Content": { "MacroErrors": "Throw" }, + //#if (DevelopmentMode == "IDEDevelopment") + "Runtime": { + "Mode": "Development" + }, + //#if (ModelsBuilderMode == "Default") + "ModelsBuilder": { + "ModelsMode": "SourceCodeAuto" + }, + ////#else + //"ModelsBuilder": { + // "ModelsMode": "MODELS_MODE" + //}, + //#endif + //#endif "Hosting": { "Debug": true - }, - "RuntimeMinification": { - "UseInMemoryCache": true, - "CacheBuster": "Timestamp" } } } diff --git a/templates/UmbracoProject/appsettings.json b/templates/UmbracoProject/appsettings.json index 6678478951..23520bbe6b 100644 --- a/templates/UmbracoProject/appsettings.json +++ b/templates/UmbracoProject/appsettings.json @@ -20,7 +20,7 @@ "CMS": { "Global": { "Id": "TELEMETRYID_FROM_TEMPLATE", - //#if (UseHttpsRedirect) + //#if (UseHttpsRedirect || DevelopmentMode == "IDEDevelopment") "UseHttps": true, //#endif //#if (HasNoNodesViewPath) @@ -37,6 +37,24 @@ "Unattended": { "UpgradeUnattended": true }, + //#if (UseDeliveryApi) + "DeliveryApi": { + "Enabled": true + }, + //#endif + //#if (ModelsBuilderMode != "Default" && DevelopmentMode == "BackOfficeDevelopment") + "ModelsBuilder": { + "ModelsMode": "MODELS_MODE" + }, + //#endif + //#if (DevelopmentMode == "IDEDevelopment") + "Runtime": { + "Mode": "Production" + }, + "ModelsBuilder": { + "ModelsMode": "Nothing" + }, + //#endif "Security": { "AllowConcurrentLogins": false } diff --git a/tests/Directory.Packages.props b/tests/Directory.Packages.props index e56f69725b..6ec7277456 100644 --- a/tests/Directory.Packages.props +++ b/tests/Directory.Packages.props @@ -21,7 +21,7 @@ - + diff --git a/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/ArticleDOC.doc b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/ArticleDOC.doc new file mode 100644 index 0000000000..9232c5f1fd Binary files /dev/null and b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/ArticleDOC.doc differ diff --git a/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/ArticleDOCX.docx b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/ArticleDOCX.docx new file mode 100644 index 0000000000..afbc888bc8 Binary files /dev/null and b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/ArticleDOCX.docx differ diff --git a/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/AudioOGA.oga b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/AudioOGA.oga new file mode 100644 index 0000000000..ab0649d220 Binary files /dev/null and b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/AudioOGA.oga differ diff --git a/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/AudioOPUS.opus b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/AudioOPUS.opus new file mode 100644 index 0000000000..b5c7df1ea7 Binary files /dev/null and b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/AudioOPUS.opus differ diff --git a/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/AudioWEBA.weba b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/AudioWEBA.weba new file mode 100644 index 0000000000..1c18a1f2db Binary files /dev/null and b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/AudioWEBA.weba differ diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index f3fa4a9e36..f7103ba5a4 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.9", - "@umbraco/playwright-testhelpers": "^2.0.0-beta.65", + "@umbraco/json-models-builders": "^2.0.17", + "@umbraco/playwright-testhelpers": "^2.0.0-beta.78", "camelize": "^1.0.0", "dotenv": "^16.3.1", "faker": "^4.1.0", @@ -132,24 +132,20 @@ } }, "node_modules/@umbraco/json-models-builders": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.9.tgz", - "integrity": "sha512-p6LjcE38WsFCvLtRRRVOCuMvris3OXeoueFu0FZBOHk2r7PXiqYCBUls/KbKxqpixzVDAb48RBd1hV7sKPcm5A==", + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.17.tgz", + "integrity": "sha512-i7uuojDjWuXkch9XkEClGtlKJ0Lw3BTGpp4qKaUM+btb7g1sn1Gi50+f+478cJvLG6+q6rmQDZCIXqrTU6Ryhg==", "dependencies": { "camelize": "^1.0.1" } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "2.0.0-beta.65", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.65.tgz", - "integrity": "sha512-plSD/4hhVaMl2TItAaBOUQyuy0Qo5rW3EGIF0TvL3a01s6hNoW1DrOCZhWsOOsMTkgf+oScLEsVIBMk0uDLQrg==", + "version": "2.0.0-beta.78", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.78.tgz", + "integrity": "sha512-s9jLCKQRfXH2zAkT4iUzu/XsrrPQRFVWdj7Ps3uvBV8YzdM1EYMAaCKwgZ5OnCSCN87gysYTW++NZyKT2Fg6qQ==", "dependencies": { - "@umbraco/json-models-builders": "2.0.9", - "camelize": "^1.0.0", - "faker": "^4.1.0", - "form-data": "^4.0.0", - "node-fetch": "^2.6.7", - "xhr2": "^0.2.1" + "@umbraco/json-models-builders": "2.0.17", + "node-fetch": "^2.6.7" } }, "node_modules/aggregate-error": { @@ -419,20 +415,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index f0f2a60b94..d6877d5581 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -21,8 +21,8 @@ "wait-on": "^7.2.0" }, "dependencies": { - "@umbraco/json-models-builders": "^2.0.9", - "@umbraco/playwright-testhelpers": "^2.0.0-beta.65", + "@umbraco/json-models-builders": "^2.0.17", + "@umbraco/playwright-testhelpers": "^2.0.0-beta.78", "camelize": "^1.0.0", "dotenv": "^16.3.1", "faker": "^4.1.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ApiTesting/DataType/DataType.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/ApiTesting/DataType/DataType.spec.ts index e5bbf36b89..2024224119 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/ApiTesting/DataType/DataType.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ApiTesting/DataType/DataType.spec.ts @@ -9,6 +9,7 @@ test.describe('DataType tests', () => { const dataTypeName = 'TestDataType'; const folderName = 'TestDataTypeFolder'; const editorAlias = 'Umbraco.DateTime'; + const editorUiAlias = 'Umb.PropertyEditorUi.DatePicker'; const dataTypeData = [ { "alias": "tester", @@ -29,7 +30,7 @@ test.describe('DataType tests', () => { test('can create dataType', async ({umbracoApi}) => { // Act - dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, dataTypeData); + dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData); // Assert expect(umbracoApi.dataType.doesExist(dataTypeId)).toBeTruthy(); @@ -37,7 +38,7 @@ test.describe('DataType tests', () => { test('can update dataType', async ({umbracoApi}) => { // Arrange - dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, []); + dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, []); const dataType = await umbracoApi.dataType.get(dataTypeId); dataType.values = dataTypeData; @@ -52,7 +53,7 @@ test.describe('DataType tests', () => { test('can delete dataType', async ({umbracoApi}) => { // Arrange - dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, dataTypeData); + dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData); expect(await umbracoApi.dataType.doesExist(dataTypeId)).toBeTruthy(); // Act @@ -65,7 +66,7 @@ test.describe('DataType tests', () => { test('can move a dataType to a folder', async ({umbracoApi}) => { // Arrange await umbracoApi.dataType.ensureNameNotExists(folderName); - dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, dataTypeData); + dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData); expect(await umbracoApi.dataType.doesNameExist(dataTypeName)).toBeTruthy(); dataTypeFolderId = await umbracoApi.dataType.createFolder(folderName); expect(await umbracoApi.dataType.doesFolderExist(dataTypeFolderId)).toBeTruthy(); @@ -82,7 +83,7 @@ test.describe('DataType tests', () => { test('can copy a dataType to a folder', async ({umbracoApi}) => { // Arrange await umbracoApi.dataType.ensureNameNotExists(folderName); - dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, dataTypeData); + dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData); dataTypeFolderId = await umbracoApi.dataType.createFolder(folderName); const dataType = await umbracoApi.dataType.get(dataTypeId); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ChildrenContent.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ChildrenContent.spec.ts index bcbb9656dc..176f19c2b3 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ChildrenContent.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ChildrenContent.spec.ts @@ -7,7 +7,7 @@ let contentId = ''; const contentName = 'TestContent'; const childContentName = 'ChildContent'; const documentTypeName = 'DocumentTypeForContent'; -const childDocumentTypeName = 'ChildDocumentTypeForContent'; +const childDocumentTypeName = 'ChildDocumentType'; test.beforeEach(async ({umbracoApi}) => { await umbracoApi.documentType.ensureNameNotExists(documentTypeName); @@ -24,7 +24,7 @@ test.afterEach(async ({umbracoApi}) => { test('can create child node', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName); - documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId, true); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId); contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); @@ -32,10 +32,12 @@ test('can create child node', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) = // Act await umbracoUi.content.clickActionsMenuForContent(contentName); await umbracoUi.content.clickCreateButton(); - await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.chooseDocumentType(childDocumentTypeName); + // This wait is needed + await umbracoUi.waitForTimeout(500); await umbracoUi.content.enterContentName(childContentName); await umbracoUi.content.clickSaveButton(); - + // Assert await umbracoUi.content.isSuccessNotificationVisible(); expect(await umbracoApi.document.doesNameExist(childContentName)).toBeTruthy(); @@ -51,8 +53,7 @@ test('can create child node', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) = await umbracoApi.document.ensureNameNotExists(childContentName); }); -// TODO: Remove skip when the front-end is ready. -test.skip('can create child node in child node', async ({umbracoApi, umbracoUi}) => { +test('can create child node in child node', async ({umbracoApi, umbracoUi}) => { // Arrange const childOfChildContentName = 'ChildOfChildContent'; const childOfChildDocumentTypeName = 'ChildOfChildDocumentType'; @@ -60,9 +61,9 @@ test.skip('can create child node in child node', async ({umbracoApi, umbracoUi}) let childContentId: any; await umbracoApi.documentType.ensureNameNotExists(childOfChildDocumentTypeName); childOfChildDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childOfChildDocumentTypeName); - childDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(childDocumentTypeName, childOfChildDocumentTypeId, true); - documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId, true); - contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + childDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(childDocumentTypeName, childOfChildDocumentTypeId); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId); + contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); childContentId = await umbracoApi.document.createDefaultDocumentWithParent(childContentName, childDocumentTypeId, contentId); await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); @@ -71,8 +72,8 @@ test.skip('can create child node in child node', async ({umbracoApi, umbracoUi}) await umbracoUi.content.clickCaretButtonForContentName(contentName); await umbracoUi.content.clickActionsMenuForContent(childContentName); await umbracoUi.content.clickCreateButton(); - await umbracoUi.content.clickLabelWithName(childOfChildDocumentTypeName); - // This wait is needed + await umbracoUi.content.chooseDocumentType(childOfChildDocumentTypeName); + // This wait is needed await umbracoUi.waitForTimeout(500); await umbracoUi.content.enterContentName(childOfChildContentName); await umbracoUi.content.clickSaveButton(); @@ -81,9 +82,7 @@ test.skip('can create child node in child node', async ({umbracoApi, umbracoUi}) await umbracoUi.content.isSuccessNotificationVisible(); const childOfChildData = await umbracoApi.document.getChildren(childContentId); expect(childOfChildData[0].variants[0].name).toBe(childOfChildContentName); - // verify that the child content displays in the tree after reloading children - await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickReloadButton(); + // verify that the child content displays in the tree await umbracoUi.content.clickCaretButtonForContentName(childContentName); await umbracoUi.content.doesContentTreeHaveName(childOfChildContentName); @@ -95,7 +94,7 @@ test.skip('can create child node in child node', async ({umbracoApi, umbracoUi}) test('cannot publish child if the parent is not published', async ({umbracoApi, umbracoUi}) => { // Arrange childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName); - documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId, true); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId); contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoApi.document.createDefaultDocumentWithParent(childContentName, childDocumentTypeId, contentId); await umbracoUi.goToBackOffice(); @@ -115,7 +114,7 @@ test('cannot publish child if the parent is not published', async ({umbracoApi, test('can publish with descendants', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName); - documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId, true); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId); contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoApi.document.createDefaultDocumentWithParent(childContentName, childDocumentTypeId, contentId); await umbracoUi.goToBackOffice(); 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 98f0a6ec21..0c7a869eb0 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Content.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Content.spec.ts @@ -91,7 +91,7 @@ test('can rename content', async ({umbracoApi, umbracoUi}) => { await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act - await umbracoUi.content.openContent(wrongContentName); + await umbracoUi.content.goToContentWithName(wrongContentName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -111,7 +111,7 @@ test('can update content', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act - await umbracoUi.content.openContent(contentName); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.enterTextstring(contentText); await umbracoUi.content.clickSaveButton(); @@ -159,4 +159,4 @@ test('can unpublish content', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) = await umbracoUi.content.isSuccessNotificationVisible(); const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.variants[0].state).toBe('Draft'); -}); \ No newline at end of file +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts index 79cf1211e0..a680f656be 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts @@ -1,4 +1,4 @@ -import { expect } from '@playwright/test'; +import {expect} from '@playwright/test'; import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; let documentTypeId = ''; @@ -18,17 +18,20 @@ test.afterEach(async ({umbracoApi}) => { test('can see correct information when published', async ({umbracoApi, umbracoUi}) => { // Arrange const notPublishContentLink = 'This document is published but is not in the cache'; - documentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeName); - contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + const dataTypeName = 'Textstring'; + const contentText = 'This is test content text'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + contentId = await umbracoApi.document.createDocumentWithTextContent(contentName, documentTypeId, contentText, dataTypeName); await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act - await umbracoUi.content.openContent(contentName); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickInfoTab(); await umbracoUi.content.doesLinkHaveText(notPublishContentLink); await umbracoUi.content.clickSaveAndPublishButton(); - + // Assert const contentData = await umbracoApi.document.get(contentId); // TODO: Uncomment this when front-end is ready. Currently the link is not updated immediately after publishing @@ -47,8 +50,7 @@ test('can see correct information when published', async ({umbracoApi, umbracoUi await umbracoUi.content.doesCreatedDateHaveText(expectedCreatedDate); }); -// TODO: Remove skip when the frond-end is ready. Currently the document type is not opened after clicking to the button -test.skip('can open document type', async ({umbracoApi, umbracoUi}) => { +test('can open document type', async ({umbracoApi, umbracoUi}) => { // Arrange documentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeName); contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); @@ -56,8 +58,7 @@ test.skip('can open document type', async ({umbracoApi, umbracoUi}) => { await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act - await umbracoUi.content.openContent(contentName); - await umbracoUi.content.clickInfoTab(); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickDocumentTypeByName(documentTypeName); // Assert @@ -66,17 +67,16 @@ test.skip('can open document type', async ({umbracoApi, umbracoUi}) => { test('can open template', async ({umbracoApi, umbracoUi}) => { // Arrange - const templateName = "TestTemplateForContent"; + const templateName = 'TestTemplateForContent'; await umbracoApi.template.ensureNameNotExists(templateName); const templateId = await umbracoApi.template.createDefaultTemplate(templateName); documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedTemplate(documentTypeName, templateId, true); contentId = await umbracoApi.document.createDocumentWithTemplate(contentName, documentTypeId, templateId); await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); - + // Act - await umbracoUi.content.openContent(contentName); - await umbracoUi.content.clickInfoTab(); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickTemplateByName(templateName); // Assert @@ -88,8 +88,8 @@ test('can open template', async ({umbracoApi, umbracoUi}) => { test('can change template', async ({umbracoApi, umbracoUi}) => { // Arrange - const firstTemplateName = "TestTemplateOneForContent"; - const secondTemplateName = "TestTemplateTwoForContent"; + const firstTemplateName = 'TestTemplateOneForContent'; + const secondTemplateName = 'TestTemplateTwoForContent'; await umbracoApi.template.ensureNameNotExists(firstTemplateName); await umbracoApi.template.ensureNameNotExists(secondTemplateName); const firstTemplateId = await umbracoApi.template.createDefaultTemplate(firstTemplateName); @@ -100,8 +100,7 @@ test('can change template', async ({umbracoApi, umbracoUi}) => { await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act - await umbracoUi.content.openContent(contentName); - await umbracoUi.content.clickInfoTab(); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.changeTemplate(firstTemplateName, secondTemplateName); await umbracoUi.content.clickSaveButton(); @@ -116,8 +115,8 @@ test('can change template', async ({umbracoApi, umbracoUi}) => { test('cannot change to a template that is not allowed in the document type', async ({umbracoApi, umbracoUi}) => { // Arrange - const firstTemplateName = "TestTemplateOneForContent"; - const secondTemplateName = "TestTemplateTwoForContent"; + const firstTemplateName = 'TestTemplateOneForContent'; + const secondTemplateName = 'TestTemplateTwoForContent'; await umbracoApi.template.ensureNameNotExists(firstTemplateName); await umbracoApi.template.ensureNameNotExists(secondTemplateName); const firstTemplateId = await umbracoApi.template.createDefaultTemplate(firstTemplateName); @@ -128,8 +127,7 @@ test('cannot change to a template that is not allowed in the document type', asy await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act - await umbracoUi.content.openContent(contentName); - await umbracoUi.content.clickInfoTab(); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickEditTemplateByName(firstTemplateName); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCheckboxList.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCheckboxList.spec.ts index 71706203ea..1cfe2a419f 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCheckboxList.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCheckboxList.spec.ts @@ -1,4 +1,4 @@ -import { ConstantHelper, test, AliasHelper } from '@umbraco/playwright-testhelpers'; +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; import {expect} from "@playwright/test"; const contentName = 'TestContent'; @@ -18,6 +18,7 @@ test.afterEach(async ({umbracoApi}) => { test('can create content with the checkbox list data type', async ({umbracoApi, umbracoUi}) => { // Arrange + const expectedState = 'Draft'; const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); await umbracoUi.content.goToSection(ConstantHelper.sections.content); @@ -33,42 +34,41 @@ test('can create content with the checkbox list data type', async ({umbracoApi, await umbracoUi.content.isSuccessNotificationVisible(); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); expect(contentData.values).toEqual([]); }); test('can publish content with the checkbox list data type', async ({umbracoApi, umbracoUi}) => { // Arrange + const expectedState = 'Published'; const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act - await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); - await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickSaveAndPublishButton(); // Assert await umbracoUi.content.doesSuccessNotificationsHaveCount(2); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); expect(contentData.values).toEqual([]); }); -test('can create content with the custom approved color data type', async ({umbracoApi, umbracoUi}) => { +test('can create content with the custom checkbox list data type', async ({umbracoApi, umbracoUi}) => { // Arrange const customDataTypeName = 'CustomCheckboxList'; const optionValues = ['testOption1', 'testOption2']; const customDataTypeId = await umbracoApi.dataType.createCheckboxListDataType(customDataTypeName, optionValues); - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act - await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); - await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.chooseCheckboxListOption(optionValues[0]); await umbracoUi.content.clickSaveAndPublishButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithContentPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithContentPicker.spec.ts new file mode 100644 index 0000000000..5764b4b7fc --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithContentPicker.spec.ts @@ -0,0 +1,175 @@ +import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Content Picker'; +const contentPickerDocumentTypeName = 'DocumentTypeForContentPicker'; +const contentPickerName = 'TestContentPicker'; +let contentPickerDocumentTypeId = ''; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + contentPickerDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(contentPickerDocumentTypeName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentPickerName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.documentType.ensureNameNotExists(contentPickerDocumentTypeName); +}); + +test('can create content with the content picker datatype', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.addContentPicker(contentPickerName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].value).toEqual(contentPickerId); +}); + +test('can publish content with the content picker data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.addContentPicker(contentPickerName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].value).toEqual(contentPickerId); +}); + +test('can open content picker in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const customDataTypeName = 'CustomContentPicker'; + const customDataTypeId = await umbracoApi.dataType.createContentPickerDataTypeWithShowOpenButton(customDataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + // Create content to pick + await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.addContentPicker(contentPickerName); + + // Assert + await umbracoUi.content.isOpenButtonVisibleInContentPicker(contentPickerName); + await umbracoUi.content.clickContentPickerOpenButton(contentPickerName); + await umbracoUi.content.isNodeOpenForContentPicker(contentPickerName); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + +test('can choose start node for the content picker in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const customDataTypeName = 'CustomContentPicker'; + const childContentPickerDocumentTypeName = 'ChildDocumentTypeForContentPicker'; + const childContentPickerName = 'TestChildContentPicker'; + await umbracoApi.documentType.ensureNameNotExists(childContentPickerDocumentTypeName); + const childContentPickerDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childContentPickerDocumentTypeName); + contentPickerDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(contentPickerName, childContentPickerDocumentTypeId); + const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId); + await umbracoApi.document.createDefaultDocumentWithParent(childContentPickerName, childContentPickerDocumentTypeId, contentPickerId); + // Create a custom content picker with start node + const customDataTypeId = await umbracoApi.dataType.createContentPickerDataTypeWithStartNode(customDataTypeName, contentPickerId); + 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.clickChooseButton(); + + // Assert + await umbracoUi.content.isContentNameVisible(childContentPickerName); + await umbracoUi.content.isContentNameVisible(contentPickerName, false); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); + await umbracoApi.document.ensureNameNotExists(childContentPickerName); +}); + +test.skip('can ignore user start node for the content picker in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const customDataTypeName = 'CustomContentPicker'; + const childContentPickerDocumentTypeName = 'ChildDocumentTypeForContentPicker'; + const childContentPickerName = 'TestChildContentPicker'; + await umbracoApi.documentType.ensureNameNotExists(childContentPickerDocumentTypeName); + const childContentPickerDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childContentPickerDocumentTypeName); + contentPickerDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(contentPickerName, childContentPickerDocumentTypeId); + const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId); + await umbracoApi.document.createDefaultDocumentWithParent(childContentPickerName, childContentPickerDocumentTypeId, contentPickerId); + // Create a custom content picker with the setting "ignore user start node" is enable + const customDataTypeId = await umbracoApi.dataType.createContentPickerDataTypeWithIgnoreUserStartNodes(customDataTypeName, contentPickerId); + 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.clickChooseButton(); + + // Assert + await umbracoUi.content.isContentNameVisible(childContentPickerName); + await umbracoUi.content.isContentNameVisible(contentPickerName); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); + await umbracoApi.document.ensureNameNotExists(childContentPickerName); +}); + +test('can remove content picker in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId); + await umbracoApi.document.createDocumentWithContentPicker(contentName, documentTypeId, contentPickerId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.removeContentPicker(contentPickerName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values).toEqual([]); +}); + diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDropdown.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDropdown.spec.ts new file mode 100644 index 0000000000..7a4c246b01 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDropdown.spec.ts @@ -0,0 +1,90 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; + +const dataTypeNames = ['Dropdown', 'Dropdown multiple']; +for (const dataTypeName of dataTypeNames) { + test.describe(`${dataTypeName} tests`, () => { + test.beforeEach(async ({umbracoApi, umbracoUi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoUi.goToBackOffice(); + }); + + test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + }); + + test(`can create content with the ${dataTypeName} data type`, async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); + }); + + test(`can publish content with the ${dataTypeName} data type`, async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); + }); + + test(`can create content with the custom ${dataTypeName} data type`, async ({umbracoApi, umbracoUi}) => { + // Arrange + const customDataTypeName = 'CustomDropdown'; + const optionValues = ['testOption1', 'testOption2', 'testOption3']; + const selectedOptions = dataTypeName === 'Dropdown' ? [optionValues[0]] : optionValues; + const isMultiple = dataTypeName === 'Dropdown' ? false : true; + const customDataTypeId = await umbracoApi.dataType.createDropdownDataType(customDataTypeName, isMultiple, optionValues); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.chooseDropdownOption(selectedOptions); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName)); + expect(contentData.values[0].value).toEqual(selectedOptions); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); + }); + }); +} diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts new file mode 100644 index 0000000000..2b15dbbe14 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts @@ -0,0 +1,103 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Image Cropper'; +const imageFileName = 'Umbraco.png'; +const imageFilePath = './fixtures/mediaLibrary/' + imageFileName; +const defaultFocalPoint = { + left: 0.5, + top: 0.5, +}; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the image cropper data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.uploadFile(imageFilePath); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(imageFileName)); + expect(contentData.values[0].value.crops).toEqual([]); + expect(contentData.values[0].value.focalPoint).toEqual(defaultFocalPoint); +}); + +test('can publish content with the image cropper data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.uploadFile(imageFilePath); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(imageFileName)); + expect(contentData.values[0].value.crops).toEqual([]); + expect(contentData.values[0].value.focalPoint).toEqual(defaultFocalPoint); +}); + +test('can create content with the custom image cropper data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const customDataTypeName = 'CustomImageCropper'; + const cropValue = ['TestCropLabel', 100, 50]; + const customDataTypeId = await umbracoApi.dataType.createImageCropperDataTypeWithOneCrop(customDataTypeName, cropValue[0], cropValue[1], cropValue[2]); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.uploadFile(imageFilePath); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName)); + expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(imageFileName)); + expect(contentData.values[0].value.focalPoint).toEqual(defaultFocalPoint); + expect(contentData.values[0].value.crops[0].alias).toEqual(AliasHelper.toAlias(cropValue[0])); + expect(contentData.values[0].value.crops[0].width).toEqual(cropValue[1]); + expect(contentData.values[0].value.crops[0].height).toEqual(cropValue[2]); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageMediaPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageMediaPicker.spec.ts new file mode 100644 index 0000000000..5dfb9cafa0 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageMediaPicker.spec.ts @@ -0,0 +1,257 @@ +import {AliasHelper, ConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Image Media Picker'; +const customDataTypeName = 'Custom Image Media Picker'; +const groupName = 'TestGroup'; +const mediaName = 'TestImage'; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can save content with a image media picker', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataType = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataType.id, groupName); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); +}); + +test('can publish content with a image media picker', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataType = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataType.id, groupName); + 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.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); +}); + +test('can add an image to the image media picker', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataType = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.media.ensureNameNotExists(mediaName); + const imageId = await umbracoApi.media.createDefaultMediaWithImage(mediaName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataType.id, groupName); + 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.selectMediaByName(mediaName); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesImageMediaPickerContainImage(contentName, AliasHelper.toAlias(dataTypeName), imageId)).toBeTruthy(); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaName); +}); + +test('can remove an image from the image media picker', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataType = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.media.ensureNameNotExists(mediaName); + const imageId = await umbracoApi.media.createDefaultMediaWithImage(mediaName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataType.id, groupName); + await umbracoApi.document.createDocumentWithImageMediaPicker(contentName, documentTypeId, AliasHelper.toAlias(dataTypeName), imageId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickRemoveButtonForName(mediaName); + await umbracoUi.content.clickConfirmRemoveButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesImageMediaPickerContainImage(contentName, AliasHelper.toAlias(dataTypeName), imageId)).toBeFalsy(); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaName); +}); + +// TODO: Remove skip when the front-end is ready as there are currently no displayed error notification. +test.skip('image count can not be less that min amount set in image media picker', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); + const dataTypeId = await umbracoApi.dataType.createImageMediaPickerDataType(customDataTypeName, 1); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId, groupName); + 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.clickSaveButton(); + + // Assert + await umbracoUi.content.isErrorNotificationVisible(); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + +// TODO: Remove skip when the front-end is ready as there are currently no displayed error notification. +test.skip('image count can not be more that max amount set in image media picker', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); + const dataTypeId = await umbracoApi.dataType.createImageMediaPickerDataType(customDataTypeName, 0, 0); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId, groupName); + 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.selectMediaByName(mediaName); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isErrorNotificationVisible(); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + +test('can add an image from the image media picker with a start node', async ({umbracoApi, umbracoUi}) => { + const mediaFolderName = 'TestFolder'; + await umbracoApi.media.ensureNameNotExists(mediaFolderName); + await umbracoApi.media.ensureNameNotExists(mediaName); + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); + const imageFolderId = await umbracoApi.media.createDefaultMediaFolder(mediaFolderName); + const imageId = await umbracoApi.media.createDefaultMediaWithImageAndParentId(mediaName, imageFolderId); + const dataTypeId = await umbracoApi.dataType.createImageMediaPickerDataTypeWithStartNodeId(customDataTypeName, imageFolderId); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId, groupName); + 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.selectMediaByName(mediaName); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesImageMediaPickerContainImage(contentName, AliasHelper.toAlias(customDataTypeName), imageId)).toBeTruthy(); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaFolderName); + await umbracoApi.media.ensureNameNotExists(mediaName); + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + +test('can add an image from the image media picker with focal point enabled', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.media.ensureNameNotExists(mediaName); + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); + const imageId = await umbracoApi.media.createDefaultMediaWithImage(mediaName); + const dataTypeId = await umbracoApi.dataType.createImageMediaPickerDataType(customDataTypeName, 0, 1, true); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId, groupName); + await umbracoApi.document.createDocumentWithImageMediaPicker(contentName, documentTypeId, AliasHelper.toAlias(customDataTypeName), imageId); + const widthPercentage = 40; + const heightPercentage = 20; + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickExactLinkWithName(mediaName); + await umbracoUi.content.setFocalPoint(widthPercentage, heightPercentage); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesImageMediaPickerContainImageWithFocalPoint(contentName, AliasHelper.toAlias(customDataTypeName), imageId, {left: 0.4, top: 0.2})).toBeTruthy(); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaName); + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + +test('can reset focal point in a image from the image media picker', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.media.ensureNameNotExists(mediaName); + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); + const imageId = await umbracoApi.media.createDefaultMediaWithImage(mediaName); + const dataTypeId = await umbracoApi.dataType.createImageMediaPickerDataType(customDataTypeName, 0, 1, true); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId, groupName); + await umbracoApi.document.createDocumentWithImageMediaPicker(contentName, documentTypeId, AliasHelper.toAlias(customDataTypeName), imageId, {left: 0.4, top: 0.2}); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickExactLinkWithName(mediaName); + await umbracoUi.content.clickResetFocalPointButton(); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesImageMediaPickerContainImageWithFocalPoint(contentName, AliasHelper.toAlias(customDataTypeName), imageId, {left: 0, top: 0})).toBeTruthy(); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaName); + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + +// TODO: Remove skip when the front-end is ready as currently the crop is not being selected. +test.skip('can add an image from the image media picker with a image crop', async ({umbracoApi, umbracoUi}) => { + // Arrange + const cropLabel = 'TestCrop'; + const cropWidth = 100; + const cropHeight = 100; + await umbracoApi.media.ensureNameNotExists(mediaName); + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); + const imageId = await umbracoApi.media.createDefaultMediaWithImage(mediaName); + const dataTypeId = await umbracoApi.dataType.createImageMediaPickerDataTypeWithCrop(customDataTypeName, cropLabel, cropWidth, cropHeight); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId, groupName); + await umbracoApi.document.createDocumentWithImageMediaPicker(contentName, documentTypeId, AliasHelper.toAlias(customDataTypeName), imageId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts new file mode 100644 index 0000000000..f855841738 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts @@ -0,0 +1,130 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const dataTypeName = 'Media Picker'; +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const mediaFileName = 'TestMediaFileForContent'; +const mediaTypeName = 'File'; +let mediaFileId = ''; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.media.ensureNameNotExists(mediaFileName); + mediaFileId = await umbracoApi.media.createDefaultMediaFile(mediaFileName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.media.ensureNameNotExists(mediaFileName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the media picker data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.selectMediaByName(mediaFileName); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value[0].mediaKey).toEqual(mediaFileId); + expect(contentData.values[0].value[0].mediaTypeAlias).toEqual(mediaTypeName); + expect(contentData.values[0].value[0].focalPoint).toBeNull(); + expect(contentData.values[0].value[0].crops).toEqual([]); +}); + +test('can publish content with the media picker data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.selectMediaByName(mediaFileName); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value[0].mediaKey).toEqual(mediaFileId); + expect(contentData.values[0].value[0].mediaTypeAlias).toEqual(mediaTypeName); + expect(contentData.values[0].value[0].focalPoint).toBeNull(); + expect(contentData.values[0].value[0].crops).toEqual([]); +}); + +test('can remove a media picker in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithOneMediaPicker(contentName, documentTypeId, mediaFileId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.removeMediaPickerByName(mediaFileName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values).toEqual([]); +}); + +test('can limit the media picker in the content by setting the start node', async ({umbracoApi, umbracoUi}) => { + // Arrange + const customDataTypeName = 'CustomMediaPicker'; + const mediaFolderName = 'TestMediaFolder'; + const childMediaName = 'ChildMedia'; + await umbracoApi.media.ensureNameNotExists(mediaFolderName); + const mediaFolderId = await umbracoApi.media.createDefaultMediaFolder(mediaFolderName); + await umbracoApi.media.ensureNameNotExists(childMediaName); + await umbracoApi.media.createDefaultMediaFileAndParentId(childMediaName, mediaFolderId); + const customDataTypeId = await umbracoApi.dataType.createMediaPickerDataTypeWithStartNodeId(customDataTypeName, mediaFolderId); + 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.clickChooseMediaPickerButton(); + + // Assert + await umbracoUi.content.isMediaNameVisible(mediaFolderName, false); + await umbracoUi.content.isMediaNameVisible(childMediaName, true); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaFolderName); + await umbracoApi.media.ensureNameNotExists(childMediaName); + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMemberPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMemberPicker.spec.ts new file mode 100644 index 0000000000..b049976a1c --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMemberPicker.spec.ts @@ -0,0 +1,97 @@ +import { ConstantHelper, test, AliasHelper } from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const dataTypeName = 'Member Picker'; +const contentName = 'TestContent'; +const memberName = 'TestMemberForContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const memberTypeName = 'Test Member Type'; +const memberInfo = ['testmember@acceptance.test', 'testmember', '0123456789']; +let memberId = ''; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.memberType.ensureNameNotExists(memberTypeName); + await umbracoApi.member.ensureNameNotExists(memberName); + const memberTypeId = await umbracoApi.memberType.createDefaultMemberType(memberTypeName); + memberId = await umbracoApi.member.createDefaultMember(memberName, memberTypeId, memberInfo[0], memberInfo[1], memberInfo[2]); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.memberType.ensureNameNotExists(memberTypeName); + await umbracoApi.member.ensureNameNotExists(memberName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the member picker data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickChooseMemberPickerButton(); + await umbracoUi.content.selectMemberByName(memberName); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value).toEqual(memberId); +}); + +test('can publish content with the member picker data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickChooseMemberPickerButton(); + await umbracoUi.content.selectMemberByName(memberName); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value).toEqual(memberId); +}); + +test('can remove a member picker in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithMemberPicker(contentName, documentTypeId, memberId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.removeMemberPickerByName(memberName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values).toEqual([]); +}); + diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultiURLPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultiURLPicker.spec.ts new file mode 100644 index 0000000000..6c6135b3e3 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultiURLPicker.spec.ts @@ -0,0 +1,258 @@ +import { ConstantHelper, test, AliasHelper } from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const dataTypeName = 'Multi URL Picker'; +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const link = 'https://docs.umbraco.com'; +const linkTitle = 'Umbraco Documentation'; + +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); +}); + +test('can create content with the document link', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + // Create a document to link + const documentTypeForLinkedDocumentName = 'TestDocumentType'; + const documentTypeForLinkedDocumentId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeForLinkedDocumentName); + const linkedDocumentName = 'LinkedDocument'; + const linkedDocumentId = await umbracoApi.document.createDefaultDocument(linkedDocumentName, documentTypeForLinkedDocumentId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickAddMultiURLPickerButton(); + await umbracoUi.content.selectLinkByName(linkedDocumentName); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.length).toBe(1); + expect(contentData.values[0].value[0].type).toEqual('document'); + expect(contentData.values[0].value[0].icon).toEqual('icon-document'); + expect(contentData.values[0].value[0].target).toBeNull(); + expect(contentData.values[0].value[0].unique).toEqual(linkedDocumentId); + // Uncomment this when the front-end is ready. Currently the link title is not auto filled after choosing document to link + //expect(contentData.values[0].value[0].name).toEqual(linkedDocumentId); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(documentTypeForLinkedDocumentName); + await umbracoApi.document.ensureNameNotExists(linkedDocumentName); +}); + +test('can publish content with the document link', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + // Create a document to link + const documentTypeForLinkedDocumentName = 'TestDocumentType'; + const documentTypeForLinkedDocumentId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeForLinkedDocumentName); + const linkedDocumentName = 'ContentToPick'; + const linkedDocumentId = 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.clickAddMultiURLPickerButton(); + await umbracoUi.content.selectLinkByName(linkedDocumentName); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.length).toBe(1); + expect(contentData.values[0].value[0].type).toEqual('document'); + expect(contentData.values[0].value[0].icon).toEqual('icon-document'); + expect(contentData.values[0].value[0].target).toBeNull(); + expect(contentData.values[0].value[0].unique).toEqual(linkedDocumentId); + // Uncomment this when the front-end is ready. Currently the link title is not auto filled after choosing document to link + //expect(contentData.values[0].value[0].name).toEqual(linkedDocumentId); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(documentTypeForLinkedDocumentName); + await umbracoApi.document.ensureNameNotExists(linkedDocumentName); +}); + +test('can create content with the external link', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + 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.clickAddMultiURLPickerButton(); + await umbracoUi.content.enterLink(link); + await umbracoUi.content.enterLinkTitle(linkTitle); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.length).toBe(1); + expect(contentData.values[0].value[0].type).toEqual('external'); + expect(contentData.values[0].value[0].icon).toEqual('icon-link'); + expect(contentData.values[0].value[0].name).toEqual(linkTitle); + expect(contentData.values[0].value[0].url).toEqual(link); +}); + +test('can create content with the media link', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + // Create a media to pick + const mediaFileName = 'TestMediaFileForContent'; + await umbracoApi.media.ensureNameNotExists(mediaFileName); + const mediaFileId = await umbracoApi.media.createDefaultMediaWithImage(mediaFileName); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickAddMultiURLPickerButton(); + await umbracoUi.content.selectLinkByName(mediaFileName); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.length).toBe(1); + expect(contentData.values[0].value[0].type).toEqual('media'); + expect(contentData.values[0].value[0].icon).toEqual('icon-picture'); + expect(contentData.values[0].value[0].unique).toEqual(mediaFileId); + // Uncomment this when the front-end is ready. Currently the link title is not auto filled after choosing media to link + //expect(contentData.values[0].value[0].name).toEqual(mediaFileName); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaFileName); +}); + +test('can add multiple links in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + // Create a media to pick + const mediaFileName = 'TestMediaFileForContent'; + await umbracoApi.media.ensureNameNotExists(mediaFileName); + const mediaFileId = await umbracoApi.media.createDefaultMediaWithImage(mediaFileName); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + // Add media link + await umbracoUi.content.clickAddMultiURLPickerButton(); + await umbracoUi.content.selectLinkByName(mediaFileName); + await umbracoUi.content.clickSubmitButton(); + // Add external link + await umbracoUi.content.clickAddMultiURLPickerButton(); + await umbracoUi.content.enterLink(link); + await umbracoUi.content.enterLinkTitle(linkTitle); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.length).toBe(2); + // Verify the information of the first URL picker + expect(contentData.values[0].value[0].type).toEqual('media'); + expect(contentData.values[0].value[0].icon).toEqual('icon-picture'); + expect(contentData.values[0].value[0].unique).toEqual(mediaFileId); + // Uncomment this when the front-end is ready. Currently the link title is not auto filled after choosing media to link + //expect(contentData.values[0].value[0].name).toEqual(mediaFileName); + // Verify the information of the second URL picker + expect(contentData.values[0].value[1].type).toEqual('external'); + expect(contentData.values[0].value[1].icon).toEqual('icon-link'); + expect(contentData.values[0].value[1].name).toEqual(linkTitle); + expect(contentData.values[0].value[1].url).toEqual(link); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaFileName); +}); + +test('can remove the URL picker in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithExternalLinkURLPicker(contentName, documentTypeId, link, linkTitle); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.removeUrlPickerByName(linkTitle); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values).toEqual([]); +}); + +test('can edit the URL picker in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const updatedLinkTitle = 'Updated Umbraco Documentation'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithExternalLinkURLPicker(contentName, documentTypeId, link, linkTitle); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickEditUrlPickerButtonByName(linkTitle); + await umbracoUi.content.enterLinkTitle(updatedLinkTitle); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.length).toBe(1); + expect(contentData.values[0].value[0].type).toEqual('external'); + expect(contentData.values[0].value[0].icon).toEqual('icon-link'); + expect(contentData.values[0].value[0].name).toEqual(updatedLinkTitle); + expect(contentData.values[0].value[0].url).toEqual(link); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleImageMediaPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleImageMediaPicker.spec.ts new file mode 100644 index 0000000000..907889247c --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleImageMediaPicker.spec.ts @@ -0,0 +1,120 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const dataTypeName = 'Multiple Image Media Picker'; +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const firstMediaFileName = 'TestFirstMedia'; +const secondMediaFileName = 'TestSecondMedia'; +const mediaTypeName = 'Image'; +let firstMediaFileId = ''; +let secondMediaFileId = ''; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.media.ensureNameNotExists(firstMediaFileName); + firstMediaFileId = await umbracoApi.media.createDefaultMediaWithImage(firstMediaFileName); + await umbracoApi.media.ensureNameNotExists(secondMediaFileName); + secondMediaFileId = await umbracoApi.media.createDefaultMediaWithImage(secondMediaFileName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.media.ensureNameNotExists(firstMediaFileName); + await umbracoApi.media.ensureNameNotExists(secondMediaFileName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with multiple image media picker data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can publish content with multiple image media picker data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can add multiple images to the multiple image media picker', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickChooseMediaPickerButton(); + await umbracoUi.content.clickMediaByNameInMediaPicker(firstMediaFileName); + await umbracoUi.content.clickMediaByNameInMediaPicker(secondMediaFileName); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.length).toBe(2); + expect(contentData.values[0].value[0].mediaKey).toEqual(firstMediaFileId); + expect(contentData.values[0].value[0].mediaTypeAlias).toEqual(mediaTypeName); + expect(contentData.values[0].value[1].mediaKey).toEqual(secondMediaFileId); + expect(contentData.values[0].value[1].mediaTypeAlias).toEqual(mediaTypeName); +}); + +test('can remove a media picker in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithTwoMediaPicker(contentName, documentTypeId, firstMediaFileId, secondMediaFileId, AliasHelper.toAlias(dataTypeName)); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.removeMediaPickerByName(firstMediaFileName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.length).toBe(1); + expect(contentData.values[0].value[0].mediaKey).toEqual(secondMediaFileId); + expect(contentData.values[0].value[0].mediaTypeAlias).toEqual(mediaTypeName); +}); + diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleMediaPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleMediaPicker.spec.ts new file mode 100644 index 0000000000..2cf94e8a89 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleMediaPicker.spec.ts @@ -0,0 +1,122 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const dataTypeName = 'Multiple Media Picker'; +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const firstMediaFileName = 'TestFirstMedia'; +const secondMediaFileName = 'TestSecondMedia'; +const firstMediaTypeName = 'File'; +const secondMediaTypeName = 'Image'; +let firstMediaFileId = ''; +let secondMediaFileId = ''; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.media.ensureNameNotExists(firstMediaFileName); + firstMediaFileId = await umbracoApi.media.createDefaultMediaFile(firstMediaFileName); + await umbracoApi.media.ensureNameNotExists(secondMediaFileName); + secondMediaFileId = await umbracoApi.media.createDefaultMediaWithImage(secondMediaFileName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.media.ensureNameNotExists(firstMediaFileName); + await umbracoApi.media.ensureNameNotExists(secondMediaFileName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with multiple media picker data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can publish content with multiple media picker data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can add multiple media files to the multiple media picker', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickChooseMediaPickerButton(); + await umbracoUi.content.clickMediaByNameInMediaPicker(firstMediaFileName); + await umbracoUi.content.clickMediaByNameInMediaPicker(secondMediaFileName); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.length).toBe(2); + expect(contentData.values[0].value[0].mediaKey).toEqual(firstMediaFileId); + expect(contentData.values[0].value[0].mediaTypeAlias).toEqual(firstMediaTypeName); + expect(contentData.values[0].value[1].mediaKey).toEqual(secondMediaFileId); + expect(contentData.values[0].value[1].mediaTypeAlias).toEqual(secondMediaTypeName); +}); + +test('can remove a media picker in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithTwoMediaPicker(contentName, documentTypeId, firstMediaFileId, secondMediaFileId, AliasHelper.toAlias(dataTypeName)); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.removeMediaPickerByName(firstMediaFileName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.length).toBe(1); + expect(contentData.values[0].value[0].mediaKey).toEqual(secondMediaFileId); + expect(contentData.values[0].value[0].mediaTypeAlias).toEqual(secondMediaTypeName); +}); + + diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithNumeric.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithNumeric.spec.ts new file mode 100644 index 0000000000..946c16b312 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithNumeric.spec.ts @@ -0,0 +1,65 @@ +import { ConstantHelper, test, AliasHelper } from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Numeric'; +const number = 10; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the numeric data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.enterNumeric(number); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value).toEqual(number); +}); + +test('can publish content with the numeric data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.enterNumeric(number); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value).toEqual(number); +}); + diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithPropertyEditors.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithPropertyEditors.spec.ts index a6f998e80a..074a4840f0 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithPropertyEditors.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithPropertyEditors.spec.ts @@ -47,30 +47,6 @@ test('can create content with the Rich Text Editor datatype', {tag: '@smoke'}, a expect(contentData.values[0].value).toEqual(expectedContentValue); }); -test('can create content with the text area datatype', async ({umbracoApi, umbracoUi}) => { - // Arrange - const dataTypeName = 'Textarea'; - const contentText = 'This is Textarea content!'; - const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); - await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - - // Act - await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); - await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.enterContentName(contentName); - await umbracoUi.content.enterTextArea(contentText); - await umbracoUi.content.clickSaveAndPublishButton(); - - // Assert - await umbracoUi.content.doesSuccessNotificationsHaveCount(2); - expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); - const contentData = await umbracoApi.document.getByName(contentName); - expect(contentData.values[0].value).toEqual(contentText); -}); - // TODO: Remove skip when the front-end is ready. Currently it returns error when publishing a content test.skip('can create content with the upload file datatype', async ({umbracoApi, umbracoUi}) => { // Arrange @@ -86,7 +62,7 @@ test.skip('can create content with the upload file datatype', async ({umbracoApi await umbracoUi.content.clickCreateButton(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); - await umbracoUi.content.changeFileTypeWithFileChooser('./fixtures/mediaLibrary/' + uploadFilePath); + await umbracoUi.content.uploadFile('./fixtures/mediaLibrary/' + uploadFilePath); await umbracoUi.content.clickSaveAndPublishButton(); // Assert @@ -96,58 +72,6 @@ test.skip('can create content with the upload file datatype', async ({umbracoApi expect(contentData.values[0].value.src).toContainEqual(uploadFilePath); }); -test('can create content with the tags datatype', async ({umbracoApi, umbracoUi}) => { - // Arrange - const dataTypeName = 'Tags'; - const tagName = 'test'; - const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); - await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - - // Act - await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); - await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.enterContentName(contentName); - await umbracoUi.content.addTags(tagName); - await umbracoUi.content.clickSaveAndPublishButton(); - - // Assert - await umbracoUi.content.doesSuccessNotificationsHaveCount(2); - expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); - const contentData = await umbracoApi.document.getByName(contentName); - expect(contentData.values[0].value).toEqual([tagName]); -}); - -test('can create content with the content picker datatype', async ({umbracoApi, umbracoUi}) => { - // Arrange - const dataTypeName = 'Content Picker'; - const contentPickerName = 'TestContentPicker'; - const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); - const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); - const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, documentTypeId); - await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - - // Act - await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); - await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.enterContentName(contentName); - await umbracoUi.content.addContentPicker(contentPickerName); - await umbracoUi.content.clickSaveAndPublishButton(); - - // Assert - await umbracoUi.content.doesSuccessNotificationsHaveCount(2); - expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); - const contentData = await umbracoApi.document.getByName(contentName); - expect(contentData.values[0].value).toEqual(contentPickerId); - - // Clean - await umbracoApi.document.ensureNameNotExists(contentPickerName); -}); - // TODO: Remove skip and update the test when the front-end is ready. Currently the list of content is not displayed. test.skip('can create content with the list view - content datatype', async ({umbracoApi, umbracoUi}) => { // Arrange diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithRadiobox.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithRadiobox.spec.ts new file mode 100644 index 0000000000..195483125b --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithRadiobox.spec.ts @@ -0,0 +1,87 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Radiobox'; + +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); +}); + +test('can create content with the radiobox data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can publish content with the radiobox data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + 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.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can create content with the custom radiobox data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const customDataTypeName = 'CustomRadiobox'; + const optionValues = ['testOption1', 'testOption2']; + const customDataTypeId = await umbracoApi.dataType.createRadioboxDataType(customDataTypeName, optionValues); + 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.chooseRadioboxOption(optionValues[0]); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName)); + expect(contentData.values[0].value).toEqual(optionValues[0]); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTags.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTags.spec.ts new file mode 100644 index 0000000000..33856a5b35 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTags.spec.ts @@ -0,0 +1,85 @@ +import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Tags'; +const tagsName = ['testTag1', 'testTag2']; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with one tag', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickPlusIconButton(); + await umbracoUi.content.enterTag(tagsName[0]); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].value).toEqual([tagsName[0]]); +}); + +test('can publish content with multiple tags', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickPlusIconButton(); + await umbracoUi.content.enterTag(tagsName[0]); + await umbracoUi.content.enterTag(tagsName[1]); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].value).toEqual(tagsName); +}); + +test('can remove a tag in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithTags(contentName, documentTypeId, [tagsName[0]]); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.removeTagByName(tagsName[0]); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values).toEqual([]); +}); + diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextarea.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextarea.spec.ts new file mode 100644 index 0000000000..243713d8df --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextarea.spec.ts @@ -0,0 +1,104 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Textarea'; +const text = 'This is the content with textarea'; +const customDataTypeName = 'Custom Textarea'; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the textarea data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can publish content with the textarea data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can input text into the textarea', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.enterTextArea(text); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value).toEqual(text); +}); + +test('cannot input the text that exceeds the allowed amount of characters', async ({umbracoApi, umbracoUi}) => { + // Arrange + const maxChars = 100; + const textExceedMaxChars = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam mattis porttitor orci id cursus. Nulla'; + const warningMessage = 'This field exceeds the allowed amount of characters'; + const dataTypeId = await umbracoApi.dataType.createTextareaDataType(customDataTypeName, maxChars); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.enterTextArea(textExceedMaxChars); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isTextWithExactNameVisible(warningMessage); + await umbracoUi.content.isSuccessNotificationVisible(); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextstring.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextstring.spec.ts new file mode 100644 index 0000000000..f834ae35de --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextstring.spec.ts @@ -0,0 +1,104 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Textstring'; +const text = 'This is the content with textstring'; +const customDataTypeName = 'Custom Textstring'; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the textstring data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can publish content with the textstring data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can input text into the textstring', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.enterTextstring(text); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value).toEqual(text); +}); + +test('cannot input the text that exceeds the allowed amount of characters', async ({umbracoApi, umbracoUi}) => { + // Arrange + const maxChars = 20; + const textExceedMaxChars = 'Lorem ipsum dolor sit'; + const warningMessage = 'This field exceeds the allowed amount of characters'; + const dataTypeId = await umbracoApi.dataType.createTextstringDataType(customDataTypeName, maxChars); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.enterTextstring(textExceedMaxChars); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isTextWithExactNameVisible(warningMessage); + await umbracoUi.content.isSuccessNotificationVisible(); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTrueFalse.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTrueFalse.spec.ts new file mode 100644 index 0000000000..479176b401 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTrueFalse.spec.ts @@ -0,0 +1,142 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'True/false'; +const customDataTypeName = 'Custom Truefalse'; + +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); +}); + +test('can create content with the true/false data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can publish content with the true/false data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + 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.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can toggle the true/false value in the content ', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + 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.clickToggleButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual('truefalse'); + expect(contentData.values[0].value).toEqual(true); +}); + +test('can toggle the true/false value with the initial state enabled', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeId = await umbracoApi.dataType.createTrueFalseDataTypeWithInitialState(customDataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId); + 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.clickToggleButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName)); + expect(contentData.values[0].value).toEqual(false); +}); + +test('can show the label on for the true/false in the content ', async ({umbracoApi, umbracoUi}) => { + // Arrange + const labelOn = 'Test Label On'; + const dataTypeId = await umbracoApi.dataType.createTrueFalseDataTypeWithLabelOn(customDataTypeName, labelOn); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId); + 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.clickToggleButton(); + + // Assert + await umbracoUi.content.doesToggleHaveLabel(labelOn); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + +test('can show the label off for the true/false in the content ', async ({umbracoApi, umbracoUi}) => { + // Arrange + const labelOff = 'Test Label Off'; + const dataTypeId = await umbracoApi.dataType.createTrueFalseDataTypeWithLabelOff(customDataTypeName, labelOff); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + + // Assert + await umbracoUi.content.doesToggleHaveLabel(labelOff); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadArticle.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadArticle.spec.ts new file mode 100644 index 0000000000..33c25bcd53 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadArticle.spec.ts @@ -0,0 +1,112 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Upload Article'; +const uploadFilePath = './fixtures/mediaLibrary/'; + +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); +}); + +test('can create content with the upload article data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can publish content with the upload article data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + 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.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +const uploadFiles = [ + {fileExtension: 'pdf', fileName: 'Article.pdf'}, + {fileExtension: 'docx', fileName: 'ArticleDOCX.docx'}, + {fileExtension: 'doc', fileName: 'ArticleDOC.doc'} +]; +for (const uploadFile of uploadFiles) { + test(`can upload an article with the ${uploadFile.fileExtension} extension in the content`, async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + 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.uploadFile(uploadFilePath + uploadFile.fileName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(uploadFile.fileName)); + }); +} + +// TODO: Remove skip when the front-end is ready. Currently the uploaded file still displays after removing. +test.skip('can remove an article file in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const uploadFileName = 'Article.pdf'; + const mimeType = 'application/pdf'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithUploadFile(contentName, documentTypeId, dataTypeName, uploadFileName, mimeType); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickRemoveFilesButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values).toEqual([]); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadAudio.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadAudio.spec.ts new file mode 100644 index 0000000000..21114c52b5 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadAudio.spec.ts @@ -0,0 +1,113 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Upload Audio'; +const uploadFilePath = './fixtures/mediaLibrary/'; + +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); +}); + +test('can create content with the upload audio data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can publish content with the upload audio data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + 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.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +const uploadFiles = [ + {fileExtension: 'mp3', fileName: 'Audio.mp3'}, + {fileExtension: 'weba', fileName: 'AudioWEBA.weba'}, + {fileExtension: 'oga', fileName: 'AudioOGA.oga'}, + {fileExtension: 'opus', fileName: 'AudioOPUS.opus'} +]; +for (const uploadFile of uploadFiles) { + test(`can upload an audio with the ${uploadFile.fileExtension} extension in the content`, async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + 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.uploadFile(uploadFilePath + uploadFile.fileName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(uploadFile.fileName)); + }); +} + +// TODO: Remove skip when the front-end is ready. Currently the uploaded file still displays after removing. +test.skip('can remove an audio file in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const uploadFileName = 'Audio.mp3'; + const mineType = 'audio/mpeg'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithUploadFile(contentName, documentTypeId, dataTypeName, uploadFileName, mineType); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickRemoveFilesButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values).toEqual([]); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadFile.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadFile.spec.ts new file mode 100644 index 0000000000..eec9b3febe --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadFile.spec.ts @@ -0,0 +1,111 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Upload File'; +const uploadFilePath = './fixtures/mediaLibrary/'; + +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); +}); + +test('can create content with the upload file data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can publish content with the upload file data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + 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.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +const uploadFiles = [ + {fileExtension: 'txt', fileName: 'File.txt'}, + {fileExtension: 'png', fileName: 'Umbraco.png'} +]; +for (const uploadFile of uploadFiles) { + test(`can upload a file with the ${uploadFile.fileExtension} extension in the content`, async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + 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.uploadFile(uploadFilePath + uploadFile.fileName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(uploadFile.fileName)); + }); +} + +// TODO: Remove skip when the front-end is ready. Currently the uploaded file still displays after removing. +test.skip('can remove a text file in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const uploadFileName = 'File.txt'; + const mineType = 'text/plain'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithUploadFile(contentName, documentTypeId, dataTypeName, uploadFileName, mineType); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickRemoveFilesButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values).toEqual([]); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RedirectManagement.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RedirectManagement.spec.ts index 0cfe5e2c18..62d92f7b55 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RedirectManagement.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RedirectManagement.spec.ts @@ -13,7 +13,7 @@ test.beforeEach(async ({umbracoApi, umbracoUi}) => { await umbracoApi.redirectManagement.setStatus(enableStatus); await umbracoUi.goToBackOffice(); // Create a content - await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); documentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeName); contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); // Publish the content @@ -36,10 +36,10 @@ test('can disable URL tracker', async ({umbracoApi, umbracoUi}) => { await umbracoUi.redirectManagement.clickDisableButton(); // Assert - // Verfiy that if renaming a published page, there are no redirects have been made - // rename the published content + // Verify that if renaming a published page, there are no redirects have been made + // rename the published content await umbracoUi.content.goToSection(ConstantHelper.sections.content); - await umbracoUi.content.openContent(contentName); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.enterContentName(updatedContentName); await umbracoUi.content.clickSaveAndPublishButton(); // verify that there is no redirects have been made @@ -63,10 +63,10 @@ test.skip('can re-enable URL tracker', async ({umbracoApi, umbracoUi}) => { await umbracoUi.redirectManagement.clickEnableURLTrackerButton(); // Assert - // Verfiy that if renaming a published page, there are one redirects have been made - // rename the published content + // Verify that if renaming a published page, there are one redirects have been made + // rename the published content await umbracoUi.content.goToSection(ConstantHelper.sections.content); - await umbracoUi.content.openContent(contentName); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.enterContentName(updatedContentName); await umbracoUi.content.clickSaveAndPublishButton(); // verify that there is one redirects have been made @@ -90,7 +90,7 @@ test.skip('can search for original URL', async ({umbracoUi}) => { await umbracoUi.redirectManagement.clickRedirectManagementTab(); await umbracoUi.redirectManagement.enterOriginalUrl(searchKeyword); await umbracoUi.redirectManagement.clickSearchButton(); - + // Assert // TODO: verify the search result }); @@ -98,9 +98,9 @@ test.skip('can search for original URL', async ({umbracoUi}) => { // TODO: Remove skip when the frond-end is ready. Currently there is no redirect have been made after renaming a published page test.skip('can delete a redirect', async ({umbracoApi, umbracoUi}) => { // Arrange - // Rename the published content + // Rename the published content await umbracoUi.content.goToSection(ConstantHelper.sections.content); - await umbracoUi.content.openContent(contentName); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.enterContentName(updatedContentName); await umbracoUi.content.clickSaveAndPublishButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/Block/BlockGridBlockAdvanced.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/Block/BlockGridBlockAdvanced.spec.ts new file mode 100644 index 0000000000..3f6c56c50c --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/Block/BlockGridBlockAdvanced.spec.ts @@ -0,0 +1,277 @@ +import {test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const blockGridEditorName = 'TestBlockGridEditor'; +const elementTypeName = 'BlockGridElement'; +const dataTypeName = 'Textstring'; +const groupName = 'testGroup'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockGridEditorName); + await umbracoUi.goToBackOffice(); + await umbracoUi.dataType.goToSettingsTreeItem('Data Types'); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockGridEditorName); +}); + +//TODO: It is not possible to add a view to a block +test.skip('can add a custom view to a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); +}); + +//TODO: It is not possible to add a view to a block +test.skip('can remove a custom view from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); +}); + +// TODO: Stylesheets are currently saved as arrays +test.skip('can remove a custom stylesheet from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const stylesheetName = 'TestStylesheet.css' + const stylesheetPath = '/wwwroot/css/' + stylesheetName; + await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName); + await umbracoApi.stylesheet.createDefaultStylesheet(stylesheetName); + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAdvancedSettingsInBlock(blockGridEditorName, contentElementTypeId, undefined, stylesheetPath, undefined, undefined, undefined); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainStylesheet(blockGridEditorName, contentElementTypeId, stylesheetPath)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); +}); + +test('can update overlay size in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const overlaySize = 'medium'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.updateBlockOverlaySize(overlaySize); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainOverlaySize(blockGridEditorName, contentElementTypeId, overlaySize)).toBeTruthy(); +}); + +test('can enable inline editing mode in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.clickInlineEditingMode(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainInlineEditing(blockGridEditorName, contentElementTypeId, true)).toBeTruthy(); +}); + +test('can disable inline editing mode in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAdvancedSettingsInBlock(blockGridEditorName, contentElementTypeId, undefined, undefined, 'small', true); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainInlineEditing(blockGridEditorName, contentElementTypeId, true)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.clickInlineEditingMode(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainInlineEditing(blockGridEditorName, contentElementTypeId, false)).toBeTruthy(); +}); + +test('can enable hide content editor in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.clickBlockGridHideContentEditorButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainHideContentEditor(blockGridEditorName, contentElementTypeId, true)).toBeTruthy(); +}); + +test('can disable hide content editor in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAdvancedSettingsInBlock(blockGridEditorName, contentElementTypeId, undefined, undefined, 'small', false, true); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainHideContentEditor(blockGridEditorName, contentElementTypeId, true)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.clickBlockGridHideContentEditorButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainHideContentEditor(blockGridEditorName, contentElementTypeId, false)).toBeTruthy(); +}); + +test('can add a background color to a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const backGroundColor = '#000000'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.selectBlockBackgroundColor(backGroundColor); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainBackgroundColor(blockGridEditorName, contentElementTypeId, backGroundColor)).toBeTruthy(); +}); + +test('can remove a background color to a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const backGroundColor = '#000000'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithCatalogueAppearanceInBlock(blockGridEditorName, contentElementTypeId, backGroundColor); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainBackgroundColor(blockGridEditorName, contentElementTypeId, backGroundColor)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.selectBlockBackgroundColor(''); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainBackgroundColor(blockGridEditorName, contentElementTypeId, '')).toBeTruthy(); +}); + +test('can add a icon color to a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const iconColor = '#000000'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.selectBlockIconColor(iconColor); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainIconColor(blockGridEditorName, contentElementTypeId, iconColor)).toBeTruthy(); +}); + +test('can remove a icon color from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const iconColor = '#000000'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithCatalogueAppearanceInBlock(blockGridEditorName, contentElementTypeId, '', iconColor); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainIconColor(blockGridEditorName, contentElementTypeId, iconColor)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.selectBlockIconColor(''); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainIconColor(blockGridEditorName, contentElementTypeId, '')).toBeTruthy(); +}); + +// TODO: Thumbnails are not showing correctly +test.skip('can add a thumbnail to a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const mediaName = 'TestMedia'; + await umbracoApi.media.ensureNameNotExists(mediaName); + await umbracoApi.media.createDefaultMediaWithImage(mediaName); + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + const mediaUrl = await umbracoApi.media.getMediaPathByName(mediaName); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.chooseBlockThumbnailWithPath(mediaUrl.fileName, mediaUrl.mediaPath); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); +}); + +// TODO: Thumbnails are not showing correctly +test.skip('can remove a thumbnail from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/Block/BlockGridBlockAreas.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/Block/BlockGridBlockAreas.spec.ts new file mode 100644 index 0000000000..d33141220a --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/Block/BlockGridBlockAreas.spec.ts @@ -0,0 +1,356 @@ +import {test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const blockGridEditorName = 'TestBlockGridEditor'; +const elementTypeName = 'BlockGridElement'; +const dataTypeName = 'Textstring'; +const groupName = 'testGroup'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockGridEditorName); + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); + await umbracoUi.goToBackOffice(); + await umbracoUi.dataType.goToSettingsTreeItem('Data Types'); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockGridEditorName); + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); +}); + +test('can update grid columns for areas for a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const gridColumns = 6; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.enterGridColumnsForArea(gridColumns); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaGridColumns(blockGridEditorName, contentElementTypeId, gridColumns)).toBeTruthy(); +}); + +test('can add an area for a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.addAreaButton(); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithAlias(blockGridEditorName, contentElementTypeId)).toBeTruthy(); +}); + +// TODO: There are currently issues when trying to select the locator. +test.skip('can resize an area for a block', async ({umbracoApi, umbracoUi}) => { +// Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); +}); + +test('can update alias an area for a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const newAlias = 'NewAlias'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.goToAreaByAlias(areaAlias); + await umbracoUi.dataType.enterAreaAlias(newAlias); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithAlias(blockGridEditorName, contentElementTypeId, newAlias)).toBeTruthy(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaCount(blockGridEditorName, contentElementTypeId, 1)).toBeTruthy(); +}); + +test('can remove an area for a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithAlias(blockGridEditorName, contentElementTypeId, areaAlias)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.clickRemoveAreaByAlias(areaAlias); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithAlias(blockGridEditorName, contentElementTypeId, areaAlias)).toBeFalsy(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaCount(blockGridEditorName, contentElementTypeId, 0)).toBeTruthy(); +}); + +test('can add multiple areas for a block', async ({umbracoApi, umbracoUi}) => { +// Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaCount(blockGridEditorName, contentElementTypeId, 1)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.addAreaButton(); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithAlias(blockGridEditorName, contentElementTypeId)).toBeTruthy(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithAlias(blockGridEditorName, contentElementTypeId, areaAlias)).toBeTruthy(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaCount(blockGridEditorName, contentElementTypeId, 2)).toBeTruthy(); +}); + +test('can add create button label for an area in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const createButtonLabel = 'CreateButtonLabel'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.goToAreaByAlias(areaAlias); + await umbracoUi.dataType.enterCreateButtonLabelInArea(createButtonLabel); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithCreateButtonLabel(blockGridEditorName, contentElementTypeId, areaAlias, createButtonLabel)).toBeTruthy(); +}); + +test('can remove create button label for an area in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const createButtonLabel = 'CreateButtonLabel'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias, createButtonLabel); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.goToAreaByAlias(areaAlias); + await umbracoUi.dataType.enterCreateButtonLabelInArea(''); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithCreateButtonLabel(blockGridEditorName, contentElementTypeId, areaAlias, '')).toBeTruthy(); +}); + +//TODO: Frontend issue. when value is inserted to the min or max, it is set as a string instead of number +test.skip('can add min allowed for an area in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const minAllowed = 3; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.goToAreaByAlias(areaAlias); + await umbracoUi.dataType.enterMinAllowedInArea(minAllowed); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithMinAllowed(blockGridEditorName, contentElementTypeId, areaAlias, minAllowed)).toBeTruthy(); +}); + +test('can remove min allowed for an area in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const minAllowed = 6; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias, null, undefined, undefined, minAllowed); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithMinAllowed(blockGridEditorName, contentElementTypeId, areaAlias, minAllowed)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.goToAreaByAlias(areaAlias); + await umbracoUi.dataType.enterMinAllowedInArea(undefined); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithMinAllowed(blockGridEditorName, contentElementTypeId, areaAlias, minAllowed)).toBeFalsy(); +}); + +//TODO: Frontend issue. when value is inserted to the min or max, it is set as a string instead of number +test.skip('can add add max allowed for an area in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const maxAllowed = 7; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.goToAreaByAlias(areaAlias); + await umbracoUi.dataType.enterMaxAllowedInArea(maxAllowed); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithMaxAllowed(blockGridEditorName, contentElementTypeId, areaAlias, maxAllowed)).toBeTruthy(); +}); + +test('can remove max allowed for an area in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const maxAllowed = 7; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias, null, undefined, undefined, undefined, maxAllowed); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithMaxAllowed(blockGridEditorName, contentElementTypeId, areaAlias, maxAllowed)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.goToAreaByAlias(areaAlias); + await umbracoUi.dataType.enterMaxAllowedInArea(undefined); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithMaxAllowed(blockGridEditorName, contentElementTypeId, areaAlias, maxAllowed)).toBeFalsy(); +}); + +// TODO: There is no frontend validation for min and max values +test.skip('min can not be more than max an area in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const areaAlias = 'TestArea'; + const minAllowed = 6; + const maxAllowed = 7; + const newMinAllowed = 8; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias, null, undefined, undefined, minAllowed, maxAllowed); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.goToAreaByAlias(areaAlias); + await umbracoUi.dataType.enterMinAllowedInArea(newMinAllowed); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); +}); + +// TODO: It is currently not possible to add a specified allowance +test.skip('can add specified allowance for an area in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const areaAlias = 'TestArea'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.goToAreaByAlias(areaAlias); + await umbracoUi.dataType.clickAddSpecifiedAllowanceButton(); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); +}); + +// TODO: It is currently not possible to add a specified allowance +test.skip('can update specified allowance for an area in a block', async ({umbracoApi, umbracoUi}) => { + +}); + +// TODO: It is currently not possible to add a specified allowance +test.skip('can remove specified allowance for an area in a block', async ({umbracoApi, umbracoUi}) => { + +}); + +// TODO: It is currently not possible to add a specified allowance +test.skip('can add multiple specified allowances for an area in a block', async ({umbracoApi, umbracoUi}) => { + +}); + +// TODO: It is currently not possible to add a specified allowance +test.skip('can add specified allowance with min and max for an area in a block', async ({umbracoApi, umbracoUi}) => { +}); + +// TODO: It is currently not possible to add a specified allowance +test.skip('can remove min and max from specified allowance for an area in a block', async ({umbracoApi, umbracoUi}) => { + +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/Block/BlockGridBlockSettings.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/Block/BlockGridBlockSettings.spec.ts new file mode 100644 index 0000000000..bf0ca4905e --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/Block/BlockGridBlockSettings.spec.ts @@ -0,0 +1,253 @@ +import {test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const blockGridEditorName = 'TestBlockGridEditor'; +const elementTypeName = 'BlockGridElement'; +const dataTypeName = 'Textstring'; +const groupName = 'testGroup'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockGridEditorName); + await umbracoUi.goToBackOffice(); + await umbracoUi.dataType.goToSettingsTreeItem('Data Types'); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockGridEditorName); +}); + +test('can add a label to a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const labelText = 'TestLabel'; + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.enterBlockLabelText(labelText); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockGridEditorName, elementTypeId, labelText)).toBeTruthy(); +}); + +test('can remove a label from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const labelText = 'TestLabel'; + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithEditorAppearance(blockGridEditorName, elementTypeId, labelText); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockGridEditorName, elementTypeId, labelText)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.enterBlockLabelText(''); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockGridEditorName, elementTypeId, labelText)).toBeFalsy(); +}); + +test('can open content model in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.openBlockContentModel(); + + // Assert + await umbracoUi.dataType.isElementWorkspaceOpenInBlock(elementTypeName); +}); + +test('can add a settings model to a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + const secondElementName = 'SecondElementTest'; + const settingsElementTypeId = await umbracoApi.documentType.createDefaultElementType(secondElementName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.addBlockSettingsModel(secondElementName); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithSettingsTypeIds(blockGridEditorName, [settingsElementTypeId])).toBeTruthy(); +}); + +test('can remove a settings model from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + const secondElementName = 'SecondElementTest'; + const settingsElementTypeId = await umbracoApi.documentType.createDefaultElementType(secondElementName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithContentAndSettingsElementType(blockGridEditorName, contentElementTypeId, settingsElementTypeId); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithSettingsTypeIds(blockGridEditorName, [settingsElementTypeId])).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.removeBlockSettingsModel(); + await umbracoUi.dataType.clickConfirmRemoveButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithSettingsTypeIds(blockGridEditorName, [settingsElementTypeId])).toBeFalsy(); +}); + +test('can enable allow in root from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickAllowInRootForBlock(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockHaveAllowInRootEnabled(blockGridEditorName, contentElementTypeId)).toBeTruthy(); +}); + +test('can enable allow in areas from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickAllowInAreasForBlock(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockHaveAllowInAreasEnabled(blockGridEditorName, contentElementTypeId)).toBeTruthy(); +}); + +test('can add a column span to a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const columnSpan = [1]; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickShowResizeOptions(); + await umbracoUi.dataType.clickAvailableColumnSpans(columnSpan); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainColumnSpanOptions(blockGridEditorName, contentElementTypeId, columnSpan)).toBeTruthy(); +}); + +test('can add multiple column spans to a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const columnSpan = [1, 3, 6, 8]; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickShowResizeOptions(); + await umbracoUi.dataType.clickAvailableColumnSpans(columnSpan); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainColumnSpanOptions(blockGridEditorName, contentElementTypeId, columnSpan)).toBeTruthy(); +}); + +test('can remove a column span from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const columnSpan = [4]; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithSizeOptions(blockGridEditorName, contentElementTypeId, columnSpan[0]); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainColumnSpanOptions(blockGridEditorName, contentElementTypeId, columnSpan)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickAvailableColumnSpans(columnSpan); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainColumnSpanOptions(blockGridEditorName, contentElementTypeId, [])).toBeTruthy(); +}); + +test('can add min and max row span to a block', async ({umbracoApi, umbracoUi}) => { +// Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const rowSpanMin = 2; + const rowSpanMax = 6; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickShowResizeOptions(); + await umbracoUi.dataType.enterMinRowSpan(rowSpanMin); + await umbracoUi.dataType.enterMaxRowSpan(rowSpanMax); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainRowSpanOptions(blockGridEditorName, contentElementTypeId, rowSpanMin, rowSpanMax)).toBeTruthy(); +}); + +test('can remove min and max row spans from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const rowSpanMin = undefined; + const rowSpanMax = undefined; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithSizeOptions(blockGridEditorName, contentElementTypeId, undefined, 2, 6); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainRowSpanOptions(blockGridEditorName, contentElementTypeId, 2, 6)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.enterMinRowSpan(rowSpanMin); + await umbracoUi.dataType.enterMaxRowSpan(rowSpanMax); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainRowSpanOptions(blockGridEditorName, contentElementTypeId, rowSpanMin, rowSpanMax)).toBeTruthy(); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/BlockGridEditor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/BlockGridEditor.spec.ts new file mode 100644 index 0000000000..fa717aa183 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/BlockGridEditor.spec.ts @@ -0,0 +1,399 @@ +import {test} from "@umbraco/playwright-testhelpers"; +import {expect} from "@playwright/test"; + +const blockGridEditorName = 'TestBlockGridEditor'; +const elementTypeName = 'BlockGridElement'; +const dataTypeName = 'Textstring'; +const groupName = 'testGroup'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockGridEditorName); + await umbracoUi.goToBackOffice(); + await umbracoUi.dataType.goToSettingsTreeItem('Data Types'); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockGridEditorName); +}); + +test('can create a block grid editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const blockGridLocatorName = 'Block Grid'; + const blockGridEditorAlias = 'Umbraco.BlockGrid'; + const blockGridEditorUiAlias = 'Umb.PropertyEditorUi.BlockGrid'; + + // Act + await umbracoUi.dataType.clickActionsMenuAtRoot(); + await umbracoUi.dataType.clickCreateButton(); + await umbracoUi.dataType.clickNewDataTypeThreeDotsButton(); + await umbracoUi.dataType.enterDataTypeName(blockGridEditorName); + await umbracoUi.dataType.clickSelectAPropertyEditorButton(); + await umbracoUi.dataType.selectAPropertyEditor(blockGridLocatorName); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesNameExist(blockGridEditorName)).toBeTruthy(); + const dataTypeData = await umbracoApi.dataType.getByName(blockGridEditorName); + expect(dataTypeData.editorAlias).toBe(blockGridEditorAlias); + expect(dataTypeData.editorUiAlias).toBe(blockGridEditorUiAlias); +}); + +test('can rename a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const wrongName = 'BlockListEditorTest'; + await umbracoApi.dataType.createEmptyBlockGrid(wrongName); + + // Act + await umbracoUi.dataType.goToDataType(wrongName); + await umbracoUi.dataType.enterDataTypeName(blockGridEditorName); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesNameExist(blockGridEditorName)).toBeTruthy(); + expect(await umbracoApi.dataType.doesNameExist(wrongName)).toBeFalsy(); +}); + +test('can delete a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const blockGridId = await umbracoApi.dataType.createEmptyBlockGrid(blockGridEditorName); + + // Act + await umbracoUi.dataType.clickRootFolderCaretButton(); + await umbracoUi.dataType.clickActionsMenuForDataType(blockGridEditorName); + await umbracoUi.dataType.clickDeleteExactButton(); + await umbracoUi.dataType.clickConfirmToDeleteButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesExist(blockGridId)).toBeFalsy(); + await umbracoUi.dataType.isTreeItemVisible(blockGridEditorName, false); +}); + +test('can add a block to a block grid editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createEmptyBlockGrid(blockGridEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.clickAddBlockButton(); + await umbracoUi.dataType.clickLabelWithName(elementTypeName); + await umbracoUi.dataType.clickChooseModalButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockGridEditorName, [elementTypeId])).toBeTruthy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); +}); + +test('can add multiple blocks to a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const secondElementName = 'SecondBlockGridElement'; + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + const secondElementTypeId = await umbracoApi.documentType.createDefaultElementType(secondElementName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.clickAddBlockButton(); + await umbracoUi.dataType.clickLabelWithName(secondElementName); + await umbracoUi.dataType.clickChooseModalButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockGridEditorName, [elementTypeId, secondElementTypeId])).toBeTruthy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); + await umbracoApi.documentType.ensureNameNotExists(secondElementTypeId); +}); + +test('can remove a block from a block grid editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.clickRemoveBlockWithName(elementTypeName); + await umbracoUi.dataType.clickConfirmRemoveButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockGridEditorName, [elementTypeId])).toBeFalsy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); +}); + +test('can add a block to a group in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createEmptyBlockGrid(blockGridEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.clickAddGroupButton(); + await umbracoUi.dataType.enterGroupName(groupName); + await umbracoUi.dataType.clickAddBlockButton(1); + await umbracoUi.dataType.clickLabelWithName(elementTypeName); + await umbracoUi.dataType.clickChooseModalButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockGridGroupContainCorrectBlocks(blockGridEditorName, groupName, [elementTypeId])).toBeTruthy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); +}); + +test('can add multiple blocks to a group in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const secondElementName = 'SecondBlockGridElement'; + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + const secondElementTypeId = await umbracoApi.documentType.createDefaultElementType(secondElementName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlockInAGroup(blockGridEditorName, elementTypeId, groupName); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.clickAddBlockButton(1); + await umbracoUi.dataType.clickLabelWithName(secondElementName); + await umbracoUi.dataType.clickChooseModalButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockGridGroupContainCorrectBlocks(blockGridEditorName, groupName, [elementTypeId, secondElementTypeId])).toBeTruthy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); + await umbracoApi.documentType.ensureNameNotExists(secondElementName); +}); + +test('can delete a block in a group from a block grid editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlockInAGroup(blockGridEditorName, elementTypeId, groupName); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockGridEditorName, [elementTypeId])).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.clickRemoveBlockWithName(elementTypeName); + await umbracoUi.dataType.clickConfirmRemoveButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockGridEditorName, [elementTypeId])).toBeFalsy(); +}); + +test('can move a block from a group to another group in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const secondGroupName = 'MoveToHereGroup'; + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlockInAGroup(blockGridEditorName, elementTypeId, groupName); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockGridEditorName, [elementTypeId])).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.clickAddGroupButton(); + await umbracoUi.dataType.enterGroupName(secondGroupName, 1); + // Drag and Drop + const dragFromLocator = await umbracoUi.dataType.getLinkWithName(elementTypeName); + const dragToLocator = await umbracoUi.dataType.getAddButtonInGroupWithName(secondGroupName); + await umbracoUi.dataType.dragAndDrop(dragFromLocator, dragToLocator, -10, 0, 10); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockGridGroupContainCorrectBlocks(blockGridEditorName, secondGroupName, [elementTypeId])).toBeTruthy(); + expect(await umbracoApi.dataType.doesBlockGridGroupContainCorrectBlocks(blockGridEditorName, groupName, [elementTypeId])).toBeFalsy(); +}); + +// TODO: When deleting a group should there not be a confirmation button? and should the block be moved another group when the group it was in is deleted? +test.skip('can delete a group in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlockInAGroup(blockGridEditorName, elementTypeId, groupName); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockGridEditorName, [elementTypeId])).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); +}); + +test('can add a min and max amount to a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const minAmount = 1; + const maxAmount = 2; + await umbracoApi.dataType.createEmptyBlockGrid(blockGridEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.enterMinAmount(minAmount.toString()); + await umbracoUi.dataType.enterMaxAmount(maxAmount.toString()); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const dataTypeData = await umbracoApi.dataType.getByName(blockGridEditorName); + expect(dataTypeData.values[0].value.min).toBe(minAmount); + expect(dataTypeData.values[0].value.max).toBe(maxAmount); +}); + +test('max can not be less than min in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const minAmount = 2; + const oldMaxAmount = 2; + const newMaxAmount = 1; + await umbracoApi.dataType.createBlockGridWithMinAndMaxAmount(blockGridEditorName, minAmount, oldMaxAmount); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.enterMaxAmount(newMaxAmount.toString()); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(false); + await umbracoUi.dataType.doesAmountContainErrorMessageWithText('The low value must not be exceed the high value'); + const dataTypeData = await umbracoApi.dataType.getByName(blockGridEditorName); + expect(dataTypeData.values[0].value.min).toBe(minAmount); + // The max value should not be updated + expect(dataTypeData.values[0].value.max).toBe(oldMaxAmount); +}); + +test('can enable live editing mode in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.createEmptyBlockGrid(blockGridEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + // This wait is currently necessary, sometimes there are issues when clicking the liveEdtingMode button + await umbracoUi.waitForTimeout(2000); + await umbracoUi.dataType.clickLiveEditingMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isLiveEditingModeEnabledForBlockEditor(blockGridEditorName, true)).toBeTruthy(); +}); + +test('can disable live editing mode in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.createBlockGridWithLiveEditingMode(blockGridEditorName, true); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + // This wait is currently necessary, sometimes there are issues when clicking the liveEditingMode button + await umbracoUi.waitForTimeout(2000); + await umbracoUi.dataType.clickLiveEditingMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isLiveEditingModeEnabledForBlockEditor(blockGridEditorName, false)).toBeTruthy(); +}); + +test('can add editor width in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const propertyEditorWidth = '100%'; + await umbracoApi.dataType.createEmptyBlockGrid(blockGridEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.enterEditorWidth(propertyEditorWidth); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockGridEditorName, propertyEditorWidth)).toBeTruthy(); +}); + +test('can remove editor width in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const propertyEditorWidth = '100%'; + await umbracoApi.dataType.createBlockGridWithPropertyEditorWidth(blockGridEditorName, propertyEditorWidth); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.enterEditorWidth(''); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockGridEditorName, propertyEditorWidth)).toBeFalsy(); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockGridEditorName, '')).toBeTruthy(); +}); + +test('can add a create button label in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const createButtonLabel = 'Create Block'; + await umbracoApi.dataType.createEmptyBlockGrid(blockGridEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.enterCreateButtonLabel(createButtonLabel); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockGridContainCreateButtonLabel(blockGridEditorName, createButtonLabel)).toBeTruthy(); +}); + +test('can remove a create button label in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const createButtonLabel = 'Create Block'; + await umbracoApi.dataType.createBlockGridWithCreateButtonLabel(blockGridEditorName, createButtonLabel); + expect(await umbracoApi.dataType.doesBlockGridContainCreateButtonLabel(blockGridEditorName, createButtonLabel)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.enterCreateButtonLabel(''); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockGridContainCreateButtonLabel(blockGridEditorName, createButtonLabel)).toBeFalsy(); + expect(await umbracoApi.dataType.doesBlockGridContainCreateButtonLabel(blockGridEditorName, '')).toBeTruthy(); +}); + +test('can update grid columns in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const gridColumns = 3; + await umbracoApi.dataType.createEmptyBlockGrid(blockGridEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.enterGridColumns(gridColumns); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockGridContainGridColumns(blockGridEditorName, gridColumns)).toBeTruthy(); +}); + +// TODO: wait until fixed by frontend, currently you are able to insert multiple stylesheets +test.skip('can add a stylesheet a block grid editor', async ({umbracoApi, umbracoUi}) => { +}); + +test.skip('can remove a stylesheet in a block grid editor', async ({umbracoApi, umbracoUi}) => { +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataType.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataType.spec.ts index 5fa452f211..cf3b771973 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataType.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataType.spec.ts @@ -2,8 +2,6 @@ import {expect} from "@playwright/test"; const dataTypeName = 'TestDataType'; -const editorAlias = 'Umbraco.ColorPicker'; -const propertyEditorName = 'Color Picker'; test.beforeEach(async ({umbracoApi, umbracoUi}) => { await umbracoApi.dataType.ensureNameNotExists(dataTypeName); @@ -22,7 +20,7 @@ test('can create a data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) await umbracoUi.dataType.clickNewDataTypeThreeDotsButton(); await umbracoUi.dataType.enterDataTypeName(dataTypeName); await umbracoUi.dataType.clickSelectAPropertyEditorButton(); - await umbracoUi.dataType.selectAPropertyEditor(propertyEditorName); + await umbracoUi.dataType.selectAPropertyEditor('Text Box'); await umbracoUi.dataType.clickSaveButton(); // Assert @@ -34,7 +32,7 @@ test('can rename a data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) // Arrange const wrongDataTypeName = 'Wrong Data Type'; await umbracoApi.dataType.ensureNameNotExists(wrongDataTypeName); - await umbracoApi.dataType.create(wrongDataTypeName, editorAlias, []); + await umbracoApi.dataType.createTextstringDataType(wrongDataTypeName); expect(await umbracoApi.dataType.doesNameExist(wrongDataTypeName)).toBeTruthy(); // Act @@ -49,7 +47,7 @@ test('can rename a data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) test('can delete a data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange - await umbracoApi.dataType.create(dataTypeName, editorAlias, []); + await umbracoApi.dataType.createTextstringDataType(dataTypeName); expect(await umbracoApi.dataType.doesNameExist(dataTypeName)).toBeTruthy(); // Act @@ -67,7 +65,7 @@ test('can change property editor in a data type', {tag: '@smoke'}, async ({umbra const updatedEditorAlias = 'Umbraco.TextArea'; const updatedEditorUiAlias = 'Umb.PropertyEditorUi.TextArea'; - await umbracoApi.dataType.create(dataTypeName, editorAlias, []); + await umbracoApi.dataType.createTextstringDataType(dataTypeName); expect(await umbracoApi.dataType.doesNameExist(dataTypeName)).toBeTruthy(); // Act @@ -98,16 +96,17 @@ test('cannot create a data type without selecting the property editor', {tag: '@ test('can change settings', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange + const maxCharsValue = 126; const expectedDataTypeValues = { - alias: "useLabel", - value: true + "alias": "maxChars", + "value": maxCharsValue }; - await umbracoApi.dataType.create(dataTypeName, editorAlias, []); + await umbracoApi.dataType.createTextstringDataType(dataTypeName); expect(await umbracoApi.dataType.doesNameExist(dataTypeName)).toBeTruthy(); // Act await umbracoUi.dataType.goToDataType(dataTypeName); - await umbracoUi.dataType.clickIncludeLabelsSlider(); + await umbracoUi.dataType.enterMaximumAllowedCharactersValue(maxCharsValue.toString()); await umbracoUi.dataType.clickSaveButton(); // Assert 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 128a86fcfc..d116a46e8f 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts @@ -69,7 +69,7 @@ for (const mediaFileType of mediaFileTypes) { await umbracoUi.media.clickCreateMediaItemButton(); await umbracoUi.media.clickMediaTypeWithNameButton(mediaFileType.fileName); await umbracoUi.media.enterMediaItemName(mediaFileType.fileName); - await umbracoUi.media.changeFileTypeWithFileChooser('./fixtures/mediaLibrary/' + mediaFileType.filePath); + await umbracoUi.media.uploadFile('./fixtures/mediaLibrary/' + mediaFileType.filePath); await umbracoUi.media.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/Members.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/Members.spec.ts index 9070c5b659..f44a164a20 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/Members.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/Members.spec.ts @@ -107,7 +107,7 @@ test('can edit password', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { await umbracoUi.member.clickMemberLinkByName(memberName); await umbracoUi.member.clickChangePasswordButton(); await umbracoUi.member.enterNewPassword(updatedPassword); - await umbracoUi.member.enterConfirmPassword(updatedPassword); + await umbracoUi.member.enterConfirmNewPassword(updatedPassword); await umbracoUi.member.clickSaveButton(); // Assert @@ -173,9 +173,16 @@ test('can view member info', async ({umbracoApi, umbracoUi}) => { // Assert const memberData = await umbracoApi.member.get(memberId); await umbracoUi.member.doesMemberInfoHaveValue('Failed login attempts', memberData.failedPasswordAttempts.toString()); - await umbracoUi.member.doesMemberInfoHaveValue('Last lockout date', memberData.lastLoginDate == null ? 'never' : memberData.lastLoginDate); - await umbracoUi.member.doesMemberInfoHaveValue('Last login', memberData.lastLoginDate == null ? 'never' : memberData.lastLoginDate); - await umbracoUi.member.doesMemberInfoHaveValue('Password changed', new Date(memberData.lastPasswordChangeDate).toLocaleString()); + await umbracoUi.member.doesMemberInfoHaveValue('Last lockout date', memberData.lastLoginDate == null ? 'Never' : memberData.lastLoginDate); + await umbracoUi.member.doesMemberInfoHaveValue('Last login', memberData.lastLoginDate == null ? 'Never' : memberData.lastLoginDate); + await umbracoUi.member.doesMemberInfoHaveValue('Password changed', new Date(memberData.lastPasswordChangeDate).toLocaleString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "numeric", + hour12: true, + })); }); test('can enable approved', async ({umbracoApi, umbracoUi}) => { diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeDesignTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeDesignTab.spec.ts index a92ed3ce23..eeb2e13200 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeDesignTab.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeDesignTab.spec.ts @@ -339,7 +339,7 @@ test('can add a description to a property in a document type', async ({umbracoAp await umbracoUi.documentType.goToDocumentType(documentTypeName); await umbracoUi.documentType.clickEditorSettingsButton(); await umbracoUi.documentType.enterPropertyEditorDescription(descriptionText); - await umbracoUi.documentType.clickUpdateButton(); + await umbracoUi.documentType.clickSubmitButton(); await umbracoUi.documentType.clickSaveButton(); // Assert @@ -360,7 +360,7 @@ test('can set is mandatory for a property in a document type', {tag: '@smoke'}, await umbracoUi.documentType.goToDocumentType(documentTypeName); await umbracoUi.documentType.clickEditorSettingsButton(); await umbracoUi.documentType.clickMandatorySlider(); - await umbracoUi.documentType.clickUpdateButton(); + await umbracoUi.documentType.clickSubmitButton(); await umbracoUi.documentType.clickSaveButton(); // Assert @@ -383,7 +383,7 @@ test('can enable validation for a property in a document type', async ({umbracoA await umbracoUi.documentType.selectValidationOption(''); await umbracoUi.documentType.enterRegEx(regex); await umbracoUi.documentType.enterRegExMessage(regexMessage); - await umbracoUi.documentType.clickUpdateButton(); + await umbracoUi.documentType.clickSubmitButton(); await umbracoUi.documentType.clickSaveButton(); // Assert @@ -403,7 +403,7 @@ test('can allow vary by culture for a property in a document type', {tag: '@smok await umbracoUi.documentType.goToDocumentType(documentTypeName); await umbracoUi.documentType.clickEditorSettingsButton(); await umbracoUi.documentType.clickVaryByCultureSlider(); - await umbracoUi.documentType.clickUpdateButton(); + await umbracoUi.documentType.clickSubmitButton(); await umbracoUi.documentType.clickSaveButton(); // Assert @@ -422,7 +422,7 @@ test('can set appearance to label on top for a property in a document type', asy await umbracoUi.documentType.goToDocumentType(documentTypeName); await umbracoUi.documentType.clickEditorSettingsButton(); await umbracoUi.documentType.clickLabelAboveButton(); - await umbracoUi.documentType.clickUpdateButton(); + await umbracoUi.documentType.clickSubmitButton(); await umbracoUi.documentType.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Language/Language.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Language/Language.spec.ts index 4586122d50..9d756daab3 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Language/Language.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Language/Language.spec.ts @@ -15,9 +15,9 @@ test.afterEach(async ({umbracoApi}) => { await umbracoApi.language.ensureNameNotExists(languageName); }); -test.skip('can add language', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { +test('can add language', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange - await umbracoUi.language.goToSettingsTreeItem('Language'); + await umbracoUi.language.goToLanguages(); // Act await umbracoUi.language.clickCreateLink(); @@ -25,9 +25,10 @@ test.skip('can add language', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) = await umbracoUi.language.clickSaveButton(); // Assert + await umbracoUi.language.isSuccessNotificationVisible(); expect(await umbracoApi.language.doesExist(isoCode)).toBeTruthy(); // Verify the created language displays in the list - await umbracoUi.language.clickLanguageRoot(); + await umbracoUi.language.clickLanguagesMenu(); await umbracoUi.language.isLanguageNameVisible(languageName, true); }); @@ -35,7 +36,7 @@ test('can update default language option', {tag: '@smoke'}, async ({umbracoApi, // Arrange await umbracoApi.language.create(languageName, false, false, isoCode); expect(await umbracoApi.language.doesExist(isoCode)).toBeTruthy(); - await umbracoUi.language.goToSettingsTreeItem('Language'); + await umbracoUi.language.goToLanguages(); // Act await umbracoUi.language.clickLanguageByName(languageName); @@ -43,6 +44,7 @@ test('can update default language option', {tag: '@smoke'}, async ({umbracoApi, await umbracoUi.language.clickSaveButton(); // Assert + await umbracoUi.language.isSuccessNotificationVisible(); const languageData = await umbracoApi.language.get(isoCode); expect(languageData.isDefault).toBe(true); @@ -57,7 +59,7 @@ test('can update mandatory language option', async ({umbracoApi, umbracoUi}) => // Arrange await umbracoApi.language.create(languageName, false, false, isoCode); expect(await umbracoApi.language.doesExist(isoCode)).toBeTruthy(); - await umbracoUi.language.goToSettingsTreeItem('Language'); + await umbracoUi.language.goToLanguages(); // Act await umbracoUi.language.clickLanguageByName(languageName); @@ -65,6 +67,7 @@ test('can update mandatory language option', async ({umbracoApi, umbracoUi}) => await umbracoUi.language.clickSaveButton(); // Assert + await umbracoUi.language.isSuccessNotificationVisible(); const languageData = await umbracoApi.language.get(isoCode); expect(languageData.isMandatory).toBe(true); }); @@ -73,7 +76,7 @@ test('can delete language', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => // Arrange await umbracoApi.language.create(languageName, false, false, isoCode); expect(await umbracoApi.language.doesExist(isoCode)).toBeTruthy(); - await umbracoUi.language.goToSettingsTreeItem('Language'); + await umbracoUi.language.goToLanguages(); // Act await umbracoUi.language.removeLanguageByName(languageName); @@ -81,16 +84,14 @@ test('can delete language', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => // Assert await umbracoUi.language.isSuccessNotificationVisible(); expect(await umbracoApi.language.doesExist(isoCode)).toBeFalsy(); - // TODO: uncomment this when the front-end is ready. Currently the deleted language is not disappeared after deleting. - //await umbracoUi.language.isLanguageNameVisible(languageName, false); + await umbracoUi.language.isLanguageNameVisible(languageName, false); }); -// TODO: Remove skip when the add fallback language function works -test.skip('can remove fallback language', async ({umbracoApi, umbracoUi}) => { +test('can remove fallback language', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.language.create(languageName, false, false, isoCode); expect(await umbracoApi.language.doesExist(isoCode)).toBeTruthy(); - await umbracoUi.language.goToSettingsTreeItem('Language'); + await umbracoUi.language.goToLanguages(); // Act await umbracoUi.language.clickLanguageByName(languageName); @@ -98,24 +99,40 @@ test.skip('can remove fallback language', async ({umbracoApi, umbracoUi}) => { await umbracoUi.language.clickSaveButton(); // Act + await umbracoUi.language.isSuccessNotificationVisible(); const languageData = await umbracoApi.language.get(isoCode); expect(languageData.fallbackIsoCode).toBeFalsy(); }); -// TODO: Remove skip when the add fallback language function works -test.skip('can add fallback language', async ({umbracoApi, umbracoUi}) => { +test('can add fallback language', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.language.create(languageName, false, false, isoCode, null); expect(await umbracoApi.language.doesExist(isoCode)).toBeTruthy(); - await umbracoUi.language.goToSettingsTreeItem('Language'); + await umbracoUi.language.goToLanguages(); // Act await umbracoUi.language.clickLanguageByName(languageName); - await umbracoUi.language.clickAddFallbackLanguageButton(); + await umbracoUi.language.clickChooseButton(); await umbracoUi.language.selectFallbackLanguageByName(defaultLanguageName); await umbracoUi.language.clickSaveButton(); // Act + await umbracoUi.language.isSuccessNotificationVisible(); const languageData = await umbracoApi.language.get(isoCode); expect(languageData.fallbackIsoCode).toBe(defaultLanguageIsoCode); }); + +test('cannot add a language with duplicate ISO code', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.language.create(languageName, false, false, isoCode); + expect(await umbracoApi.language.doesExist(isoCode)).toBeTruthy(); + await umbracoUi.language.goToLanguages(); + + // Act + await umbracoUi.language.clickCreateLink(); + await umbracoUi.language.chooseLanguageByName(languageName); + await umbracoUi.language.clickSaveButton(); + + // Assert + await umbracoUi.language.isErrorNotificationVisible(); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeDesignTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeDesignTab.spec.ts index 725db20a43..6a6499a551 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeDesignTab.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeDesignTab.spec.ts @@ -98,7 +98,7 @@ test('can add a description to property in a media type', {tag: '@smoke'}, async await umbracoUi.mediaType.goToMediaType(mediaTypeName); await umbracoUi.mediaType.clickEditorSettingsButton(); await umbracoUi.mediaType.enterPropertyEditorDescription(descriptionText); - await umbracoUi.mediaType.clickUpdateButton(); + await umbracoUi.mediaType.clickSubmitButton(); await umbracoUi.mediaType.clickSaveButton(); // Assert @@ -118,7 +118,7 @@ test('can set a property as mandatory in a media type', {tag: '@smoke'}, async ( await umbracoUi.mediaType.goToMediaType(mediaTypeName); await umbracoUi.mediaType.clickEditorSettingsButton(); await umbracoUi.mediaType.clickMandatorySlider(); - await umbracoUi.mediaType.clickUpdateButton(); + await umbracoUi.mediaType.clickSubmitButton(); await umbracoUi.mediaType.clickSaveButton(); // Assert @@ -140,7 +140,7 @@ test('can set up validation for a property in a media type', async ({umbracoApi, await umbracoUi.mediaType.selectValidationOption(''); await umbracoUi.mediaType.enterRegEx(regex); await umbracoUi.mediaType.enterRegExMessage(regexMessage); - await umbracoUi.mediaType.clickUpdateButton(); + await umbracoUi.mediaType.clickSubmitButton(); await umbracoUi.mediaType.clickSaveButton(); // Assert @@ -159,7 +159,7 @@ test('can set appearance as label on top for property in a media type', async ({ await umbracoUi.mediaType.goToMediaType(mediaTypeName); await umbracoUi.mediaType.clickEditorSettingsButton(); await umbracoUi.mediaType.clickLabelAboveButton(); - await umbracoUi.mediaType.clickUpdateButton(); + await umbracoUi.mediaType.clickSubmitButton(); await umbracoUi.mediaType.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts index d6bd65429b..21e81f80aa 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts @@ -30,7 +30,7 @@ test('can create an empty partial view', {tag: '@smoke'}, async ({umbracoApi, um expect(await umbracoApi.partialView.doesNameExist(partialViewFileName)).toBeTruthy(); // Verify the new partial view is displayed under the Partial Views section await umbracoUi.partialView.isPartialViewRootTreeItemVisibile(partialViewFileName); -}) +}); test('can create a partial view from snippet', async ({umbracoApi, umbracoUi}) => { // Arrange @@ -279,3 +279,16 @@ test.skip('can show returned items in query builder ', async ({umbracoApi, umbra // Clean await umbracoApi.documentType.ensureNameNotExists(documentTypeName); }); + +test('cannot create a partial view with an empty name', async ({umbracoApi, umbracoUi}) => { + // Act + await umbracoUi.partialView.clickActionsMenuAtRoot(); + await umbracoUi.partialView.clickCreateButton(); + await umbracoUi.partialView.clickNewEmptyPartialViewButton(); + await umbracoUi.partialView.clickSaveButton(); + + // Assert + // TODO: Uncomment this when the front-end is ready. Currently there is no error displays. + //await umbracoUi.partialView.isErrorNotificationVisible(); + expect(await umbracoApi.partialView.doesNameExist(partialViewFileName)).toBeFalsy(); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialViewFolder.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialViewFolder.spec.ts index 6c157646af..add9815b5e 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialViewFolder.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialViewFolder.spec.ts @@ -15,8 +15,10 @@ test.afterEach(async ({umbracoApi}) => { }); test('can create a folder', async ({umbracoApi, umbracoUi}) => { - // Act + // Arrange await umbracoUi.partialView.goToSection(ConstantHelper.sections.settings); + + // Act await umbracoUi.partialView.clickActionsMenuAtRoot(); await umbracoUi.partialView.createFolder(folderName); @@ -25,16 +27,16 @@ test('can create a folder', async ({umbracoApi, umbracoUi}) => { expect(await umbracoApi.partialView.doesFolderExist(folderName)).toBeTruthy(); // Verify the partial view folder is displayed under the Partial Views section await umbracoUi.partialView.clickRootFolderCaretButton(); - await umbracoUi.partialView.isPartialViewRootTreeItemVisibile(folderName); + await umbracoUi.partialView.isPartialViewRootTreeItemVisibile(folderName, true, false); }); test('can delete a folder', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { - //Arrange + // Arrange await umbracoApi.partialView.createFolder(folderName); expect(await umbracoApi.partialView.doesFolderExist(folderName)).toBeTruthy(); + await umbracoUi.partialView.goToSection(ConstantHelper.sections.settings); // Act - await umbracoUi.partialView.goToSection(ConstantHelper.sections.settings); await umbracoUi.partialView.reloadPartialViewTree(); await umbracoUi.partialView.clickActionsMenuForPartialView(folderName); await umbracoUi.partialView.deleteFolder(); @@ -47,14 +49,14 @@ test('can delete a folder', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => await umbracoUi.partialView.isPartialViewRootTreeItemVisibile(folderName, false, false); }); -test('can place a partial view into folder', async ({umbracoApi, umbracoUi}) => { - //Arrange +test('can create a partial view in a folder', async ({umbracoApi, umbracoUi}) => { + // Arrange await umbracoApi.partialView.ensureNameNotExists(partialViewFileName); const folderPath = await umbracoApi.partialView.createFolder(folderName); expect(await umbracoApi.partialView.doesFolderExist(folderName)).toBeTruthy(); + await umbracoUi.partialView.goToSection(ConstantHelper.sections.settings); // Act - await umbracoUi.partialView.goToSection(ConstantHelper.sections.settings); await umbracoUi.partialView.reloadPartialViewTree(); await umbracoUi.partialView.clickActionsMenuForPartialView(folderName); await umbracoUi.partialView.clickCreateButton(); @@ -70,15 +72,43 @@ test('can place a partial view into folder', async ({umbracoApi, umbracoUi}) => await umbracoUi.partialView.isPartialViewRootTreeItemVisibile(partialViewFileName, false, false); await umbracoUi.partialView.clickCaretButtonForName(folderName); await umbracoUi.partialView.isPartialViewRootTreeItemVisibile(partialViewFileName, true, false); + + // Clean + await umbracoApi.partialView.ensureNameNotExists(partialViewFileName); +}); + +test('can create a partial view in a folder in a folder', async ({umbracoApi, umbracoUi}) => { + // Arrange + const childFolderName = 'ChildFolderName'; + await umbracoApi.partialView.createFolder(folderName); + const childFolderPath = await umbracoApi.partialView.createFolder(childFolderName, folderName); + await umbracoUi.partialView.goToSection(ConstantHelper.sections.settings); + + //Act + await umbracoUi.partialView.reloadPartialViewTree(); + await umbracoUi.partialView.clickCaretButtonForName(folderName); + await umbracoUi.partialView.clickActionsMenuForPartialView(childFolderName); + await umbracoUi.partialView.clickCreateButton(); + await umbracoUi.partialView.clickNewEmptyPartialViewButton(); + await umbracoUi.partialView.enterPartialViewName(partialViewName); + await umbracoUi.partialView.clickSaveButton(); + + // Assert + await umbracoUi.partialView.isSuccessNotificationVisible(); + const childFolderChildrenData = await umbracoApi.partialView.getChildren(childFolderPath); + expect(childFolderChildrenData[0].name).toEqual(partialViewFileName); + + // Clean + await umbracoApi.partialView.ensureNameNotExists(partialViewFileName); }); test('can create a folder in a folder', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.partialView.createFolder(folderName); const childFolderName = 'childFolderName'; + await umbracoUi.partialView.goToSection(ConstantHelper.sections.settings); // Act - await umbracoUi.partialView.goToSection(ConstantHelper.sections.settings); await umbracoUi.partialView.reloadPartialViewTree(); await umbracoUi.partialView.clickActionsMenuForPartialView(folderName); await umbracoUi.partialView.createFolder(childFolderName); @@ -98,9 +128,9 @@ test('can create a folder in a folder in a folder', {tag: '@smoke'}, async ({umb const childOfChildFolderName = 'ChildOfChildFolderName'; await umbracoApi.partialView.createFolder(folderName); await umbracoApi.partialView.createFolder(childFolderName, folderName); + await umbracoUi.partialView.goToSection(ConstantHelper.sections.settings); // Act - await umbracoUi.partialView.goToSection(ConstantHelper.sections.settings); await umbracoUi.partialView.reloadPartialViewTree(); await umbracoUi.partialView.clickCaretButtonForName(folderName); await umbracoUi.partialView.clickActionsMenuForPartialView(childFolderName); @@ -114,3 +144,19 @@ test('can create a folder in a folder in a folder', {tag: '@smoke'}, async ({umb await umbracoUi.partialView.clickCaretButtonForName(childFolderName); await umbracoUi.partialView.isPartialViewRootTreeItemVisibile(childOfChildFolderName, true, false); }); + +test('cannot delete non-empty folder', async ({umbracoApi, umbracoUi}) => { + // Arrange + const childFolderName = 'ChildFolderName'; + await umbracoApi.partialView.createFolder(folderName); + await umbracoApi.partialView.createFolder(childFolderName, folderName); + await umbracoUi.partialView.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.partialView.clickRootFolderCaretButton(); + await umbracoUi.partialView.clickActionsMenuForPartialView(folderName); + await umbracoUi.partialView.deleteFolder(); + + // Assert + await umbracoUi.script.isErrorNotificationVisible(); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Script/Script.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Script/Script.spec.ts index 62bfac67ac..282a503021 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Script/Script.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Script/Script.spec.ts @@ -14,8 +14,10 @@ test.afterEach(async ({umbracoApi}) => { }); test('can create a empty script', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { - // Act + // Arrange await umbracoUi.script.goToSection(ConstantHelper.sections.settings); + + // Act await umbracoUi.script.clickActionsMenuAtRoot(); await umbracoUi.script.clickCreateButton(); await umbracoUi.script.clickNewJavascriptFileButton(); @@ -31,9 +33,9 @@ test('can create a empty script', {tag: '@smoke'}, async ({umbracoApi, umbracoUi test('can create a script with content', async ({umbracoApi, umbracoUi}) => { // Arrange const scriptContent = 'TestContent'; + await umbracoUi.script.goToSection(ConstantHelper.sections.settings); // Act - await umbracoUi.script.goToSection(ConstantHelper.sections.settings); await umbracoUi.script.clickActionsMenuAtRoot(); await umbracoUi.script.clickCreateButton(); await umbracoUi.script.clickNewJavascriptFileButton(); @@ -51,11 +53,11 @@ test('can create a script with content', async ({umbracoApi, umbracoUi}) => { test('can update a script', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange - await umbracoApi.script.create(scriptName, 'test'); + await umbracoApi.script.create(scriptName, ''); const updatedScriptContent = 'const test = {\r\n script = \u0022Test\u0022,\r\n extension = \u0022.js\u0022,\r\n scriptPath: function() {\r\n return this.script \u002B this.extension;\r\n }\r\n};\r\n'; + await umbracoUi.script.goToSection(ConstantHelper.sections.settings); // Act - await umbracoUi.script.goToSection(ConstantHelper.sections.settings); await umbracoUi.script.openScriptAtRoot(scriptName); await umbracoUi.script.enterScriptContent(updatedScriptContent); await umbracoUi.script.clickSaveButton(); @@ -69,9 +71,9 @@ test('can update a script', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => test('can delete a script', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.script.create(scriptName, ''); + await umbracoUi.script.goToSection(ConstantHelper.sections.settings); // Act - await umbracoUi.script.goToSection(ConstantHelper.sections.settings); await umbracoUi.script.reloadScriptTree(); await umbracoUi.script.clickActionsMenuForScript(scriptName); await umbracoUi.script.clickDeleteAndConfirmButton(); @@ -87,9 +89,9 @@ test('can rename a script', async ({umbracoApi, umbracoUi}) => { const wrongScriptName = 'WrongTestScript.js'; await umbracoApi.script.ensureNameNotExists(wrongScriptName); await umbracoApi.script.create(wrongScriptName, ''); + await umbracoUi.script.goToScript(wrongScriptName); // Act - await umbracoUi.script.goToScript(wrongScriptName); await umbracoUi.script.clickActionsMenuForScript(wrongScriptName); await umbracoUi.script.rename(scriptName); @@ -98,3 +100,19 @@ test('can rename a script', async ({umbracoApi, umbracoUi}) => { expect(await umbracoApi.script.doesNameExist(scriptName)).toBeTruthy(); expect(await umbracoApi.script.doesNameExist(wrongScriptName)).toBeFalsy(); }); + +test('cannot create a script with an empty name', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.script.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.script.clickActionsMenuAtRoot(); + await umbracoUi.script.clickCreateButton(); + await umbracoUi.script.clickNewJavascriptFileButton(); + await umbracoUi.script.clickSaveButton(); + + // Assert + // TODO: Uncomment this when the front-end is ready. Currently there is no error displays. + //await umbracoUi.script.isErrorNotificationVisible(); + expect(await umbracoApi.script.doesNameExist(scriptName)).toBeFalsy(); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Script/ScriptFolder.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Script/ScriptFolder.spec.ts index 8ea9158657..d8f105867d 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Script/ScriptFolder.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Script/ScriptFolder.spec.ts @@ -14,8 +14,10 @@ test.afterEach(async ({umbracoApi}) => { }); test('can create a folder', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { - // Act + // Arrange await umbracoUi.script.goToSection(ConstantHelper.sections.settings); + + // Act await umbracoUi.script.clickActionsMenuAtRoot(); await umbracoUi.script.createFolder(scriptFolderName); // TODO: remove it later @@ -30,9 +32,9 @@ test('can create a folder', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => test('can delete a folder', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.script.createFolder(scriptFolderName); + await umbracoUi.script.goToSection(ConstantHelper.sections.settings); // Act - await umbracoUi.script.goToSection(ConstantHelper.sections.settings); await umbracoUi.script.reloadScriptTree(); await umbracoUi.script.clickActionsMenuForScript(scriptFolderName); await umbracoUi.script.deleteFolder(); @@ -47,9 +49,9 @@ test('can create a script in a folder', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.script.createFolder(scriptFolderName); const scriptContent = 'const test = {\r\n script = \u0022Test\u0022,\r\n extension = \u0022.js\u0022,\r\n scriptPath: function() {\r\n return this.script \u002B this.extension;\r\n }\r\n};\r\n'; + await umbracoUi.script.goToSection(ConstantHelper.sections.settings); // Act - await umbracoUi.script.goToSection(ConstantHelper.sections.settings); await umbracoUi.script.reloadScriptTree(); await umbracoUi.script.clickActionsMenuForScript(scriptFolderName); await umbracoUi.script.clickCreateButton(); @@ -73,9 +75,9 @@ test('can create a folder in a folder', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.script.createFolder(scriptFolderName); const childFolderName = 'childFolderName'; + await umbracoUi.script.goToSection(ConstantHelper.sections.settings); // Act - await umbracoUi.script.goToSection(ConstantHelper.sections.settings); await umbracoUi.script.reloadScriptTree(); await umbracoUi.script.clickActionsMenuForScript(scriptFolderName); await umbracoUi.script.createFolder(childFolderName); @@ -95,9 +97,9 @@ test('can create a folder in a folder in a folder', {tag: '@smoke'}, async ({umb const childOfChildFolderName = 'ChildOfChildFolderName'; await umbracoApi.script.createFolder(scriptFolderName); await umbracoApi.script.createFolder(childFolderName, scriptFolderName); + await umbracoUi.script.goToSection(ConstantHelper.sections.settings); // Act - await umbracoUi.script.goToSection(ConstantHelper.sections.settings); await umbracoUi.script.reloadScriptTree(); await umbracoUi.script.clickCaretButtonForName(scriptFolderName); await umbracoUi.script.clickActionsMenuForScript(childFolderName); @@ -117,9 +119,9 @@ test('can create a script in a folder in a folder', async ({umbracoApi, umbracoU const childFolderName = 'ChildFolderName'; await umbracoApi.script.createFolder(scriptFolderName); await umbracoApi.script.createFolder(childFolderName, scriptFolderName); + await umbracoUi.script.goToSection(ConstantHelper.sections.settings); // Act - await umbracoUi.script.goToSection(ConstantHelper.sections.settings); await umbracoUi.script.reloadScriptTree(); await umbracoUi.script.clickCaretButtonForName(scriptFolderName); await umbracoUi.script.clickActionsMenuForScript(childFolderName); @@ -136,3 +138,19 @@ test('can create a script in a folder in a folder', async ({umbracoApi, umbracoU await umbracoUi.stylesheet.clickCaretButtonForName(childFolderName); await umbracoUi.script.isScriptRootTreeItemVisible(scriptName, true, false); }); + +test('cannot delete non-empty folder', async ({umbracoApi, umbracoUi}) => { + // Arrange + const childFolderName = 'ChildFolderName'; + await umbracoApi.script.createFolder(scriptFolderName); + await umbracoApi.script.createFolder(childFolderName, scriptFolderName); + await umbracoUi.script.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.script.clickRootFolderCaretButton(); + await umbracoUi.script.clickActionsMenuForScript(scriptFolderName); + await umbracoUi.script.deleteFolder(); + + // Assert + await umbracoUi.script.isErrorNotificationVisible(); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Stylesheet/Stylesheet.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Stylesheet/Stylesheet.spec.ts index d20eea701f..43ee3af2d7 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Stylesheet/Stylesheet.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Stylesheet/Stylesheet.spec.ts @@ -1,164 +1,183 @@ import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; import {expect} from '@playwright/test'; - const stylesheetName = 'TestStyleSheetFile.css'; - const styleName = 'TestStyleName'; - const styleSelector = 'h1'; - const styleStyles = 'color:red'; +const stylesheetName = 'TestStyleSheetFile.css'; +const styleName = 'TestStyleName'; +const styleSelector = 'h1'; +const styleStyles = 'color:red'; - test.beforeEach(async ({umbracoUi,umbracoApi}) => { - await umbracoUi.goToBackOffice(); - await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName); - }); +test.beforeEach(async ({umbracoUi,umbracoApi}) => { + await umbracoUi.goToBackOffice(); + await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName); +}); - test.afterEach(async ({umbracoApi}) => { - await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName); - }); +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName); +}); - test('can create a empty stylesheet', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { - // Act - await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); - await umbracoUi.stylesheet.clickActionsMenuAtRoot(); - await umbracoUi.stylesheet.clickCreateButton(); - await umbracoUi.stylesheet.clickNewStylesheetButton(); - await umbracoUi.stylesheet.enterStylesheetName(stylesheetName); - await umbracoUi.stylesheet.clickSaveButton(); +test('can create a empty stylesheet', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); - // Assert - await umbracoUi.stylesheet.isSuccessNotificationVisible(); - expect(await umbracoApi.stylesheet.doesNameExist(stylesheetName)).toBeTruthy(); - await umbracoUi.stylesheet.isStylesheetRootTreeItemVisible(stylesheetName); - }); + // Act + await umbracoUi.stylesheet.clickActionsMenuAtRoot(); + await umbracoUi.stylesheet.clickCreateButton(); + await umbracoUi.stylesheet.clickNewStylesheetButton(); + await umbracoUi.stylesheet.enterStylesheetName(stylesheetName); + await umbracoUi.stylesheet.clickSaveButton(); - test('can create a stylesheet with content', async ({umbracoApi, umbracoUi}) => { - // Arrange - const stylesheetContent = 'TestContent'; + // Assert + await umbracoUi.stylesheet.isSuccessNotificationVisible(); + expect(await umbracoApi.stylesheet.doesNameExist(stylesheetName)).toBeTruthy(); + await umbracoUi.stylesheet.isStylesheetRootTreeItemVisible(stylesheetName); +}); - //Act - await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); - await umbracoUi.stylesheet.clickActionsMenuAtRoot(); - await umbracoUi.stylesheet.clickCreateButton(); - await umbracoUi.stylesheet.clickNewStylesheetButton(); - await umbracoUi.stylesheet.enterStylesheetName(stylesheetName); - await umbracoUi.stylesheet.enterStylesheetContent(stylesheetContent); - await umbracoUi.stylesheet.clickSaveButton(); +test('can create a stylesheet with content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const stylesheetContent = 'TestContent'; + await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); - // Assert - await umbracoUi.stylesheet.isSuccessNotificationVisible(); - expect(await umbracoApi.stylesheet.doesNameExist(stylesheetName)).toBeTruthy(); - const stylesheetData = await umbracoApi.stylesheet.getByName(stylesheetName); - expect(stylesheetData.content).toEqual(stylesheetContent); - await umbracoUi.stylesheet.isStylesheetRootTreeItemVisible(stylesheetName); - }); + //Act + await umbracoUi.stylesheet.clickActionsMenuAtRoot(); + await umbracoUi.stylesheet.clickCreateButton(); + await umbracoUi.stylesheet.clickNewStylesheetButton(); + await umbracoUi.stylesheet.enterStylesheetName(stylesheetName); + await umbracoUi.stylesheet.enterStylesheetContent(stylesheetContent); + await umbracoUi.stylesheet.clickSaveButton(); - test.skip('can create a new Rich Text Editor stylesheet file', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { - // Arrange - const stylesheetContent = '/**umb_name:' + styleName + '*/\n' + styleSelector + ' {\n\t' + styleStyles + '\n}'; + // Assert + await umbracoUi.stylesheet.isSuccessNotificationVisible(); + expect(await umbracoApi.stylesheet.doesNameExist(stylesheetName)).toBeTruthy(); + const stylesheetData = await umbracoApi.stylesheet.getByName(stylesheetName); + expect(stylesheetData.content).toEqual(stylesheetContent); + await umbracoUi.stylesheet.isStylesheetRootTreeItemVisible(stylesheetName); +}); - //Act - await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); - await umbracoUi.stylesheet.clickActionsMenuAtRoot(); - await umbracoUi.stylesheet.clickCreateButton(); - await umbracoUi.stylesheet.clickNewRichTextEditorStylesheetButton(); - await umbracoUi.stylesheet.enterStylesheetName(stylesheetName); - await umbracoUi.stylesheet.addRTEStyle(styleName, styleSelector, styleStyles); - await umbracoUi.stylesheet.clickSaveButton(); +test.skip('can create a new Rich Text Editor stylesheet file', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const stylesheetContent = '/**umb_name:' + styleName + '*/\n' + styleSelector + ' {\n\t' + styleStyles + '\n}'; + await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); - // Assert - await umbracoUi.stylesheet.isSuccessNotificationVisible(); - expect(await umbracoApi.stylesheet.doesExist(stylesheetName)).toBeTruthy(); - const stylesheetData = await umbracoApi.stylesheet.getByName(stylesheetName); - expect(stylesheetData.content).toEqual(stylesheetContent); - await umbracoUi.stylesheet.isStylesheetRootTreeItemVisible(stylesheetName); - }); + //Act + await umbracoUi.stylesheet.clickActionsMenuAtRoot(); + await umbracoUi.stylesheet.clickCreateButton(); + await umbracoUi.stylesheet.clickNewRichTextEditorStylesheetButton(); + await umbracoUi.stylesheet.enterStylesheetName(stylesheetName); + await umbracoUi.stylesheet.addRTEStyle(styleName, styleSelector, styleStyles); + await umbracoUi.stylesheet.clickSaveButton(); - test.skip('can update a stylesheet', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { - // Arrange - const stylesheetContent = '/**umb_name:' + styleName + '*/\n' + styleSelector + ' {\n\t' + styleStyles + '\n}'; - await umbracoApi.stylesheet.create(stylesheetName, '', '/'); - expect(await umbracoApi.stylesheet.doesExist(stylesheetName)).toBeTruthy(); + // Assert + await umbracoUi.stylesheet.isSuccessNotificationVisible(); + expect(await umbracoApi.stylesheet.doesExist(stylesheetName)).toBeTruthy(); + const stylesheetData = await umbracoApi.stylesheet.getByName(stylesheetName); + expect(stylesheetData.content).toEqual(stylesheetContent); + await umbracoUi.stylesheet.isStylesheetRootTreeItemVisible(stylesheetName); +}); - //Act - await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); - await umbracoUi.stylesheet.openStylesheetByNameAtRoot(stylesheetName); - await umbracoUi.stylesheet.addRTEStyle(styleName, styleSelector, styleStyles); - await umbracoUi.stylesheet.clickSaveButton(); +test.skip('can update a stylesheet', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const stylesheetContent = '/**umb_name:' + styleName + '*/\n' + styleSelector + ' {\n\t' + styleStyles + '\n}'; + await umbracoApi.stylesheet.create(stylesheetName, '', '/'); + expect(await umbracoApi.stylesheet.doesExist(stylesheetName)).toBeTruthy(); + await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); - // Assert - await umbracoUi.stylesheet.isSuccessNotificationVisible(); - const stylesheetData = await umbracoApi.stylesheet.getByName(stylesheetName); - expect(stylesheetData.content).toEqual(stylesheetContent); - }); + //Act + await umbracoUi.stylesheet.openStylesheetByNameAtRoot(stylesheetName); + await umbracoUi.stylesheet.addRTEStyle(styleName, styleSelector, styleStyles); + await umbracoUi.stylesheet.clickSaveButton(); - test('can delete a stylesheet', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { - // Arrange - await umbracoApi.stylesheet.create(stylesheetName, '', '/'); + // Assert + await umbracoUi.stylesheet.isSuccessNotificationVisible(); + const stylesheetData = await umbracoApi.stylesheet.getByName(stylesheetName); + expect(stylesheetData.content).toEqual(stylesheetContent); +}); - //Act - await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); - await umbracoUi.stylesheet.reloadStylesheetTree(); - await umbracoUi.stylesheet.clickActionsMenuForStylesheet(stylesheetName); - await umbracoUi.stylesheet.clickDeleteAndConfirmButton(); +test('can delete a stylesheet', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.stylesheet.create(stylesheetName, '', '/'); + await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); - // Assert - await umbracoUi.stylesheet.isSuccessNotificationVisible(); - expect(await umbracoApi.stylesheet.doesNameExist(stylesheetName)).toBeFalsy(); - await umbracoUi.stylesheet.isStylesheetRootTreeItemVisible(stylesheetName, false, false); - }); + //Act + await umbracoUi.stylesheet.reloadStylesheetTree(); + await umbracoUi.stylesheet.clickActionsMenuForStylesheet(stylesheetName); + await umbracoUi.stylesheet.clickDeleteAndConfirmButton(); - test('can rename a stylesheet', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { - // Arrange - const wrongStylesheetName = 'WrongStylesheetName.css'; - await umbracoApi.stylesheet.ensureNameNotExists(wrongStylesheetName); - await umbracoApi.stylesheet.create(wrongStylesheetName, '', '/'); + // Assert + await umbracoUi.stylesheet.isSuccessNotificationVisible(); + expect(await umbracoApi.stylesheet.doesNameExist(stylesheetName)).toBeFalsy(); + await umbracoUi.stylesheet.isStylesheetRootTreeItemVisible(stylesheetName, false, false); +}); - //Act - await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); - await umbracoUi.stylesheet.reloadStylesheetTree(); - await umbracoUi.stylesheet.clickActionsMenuForStylesheet(wrongStylesheetName); - await umbracoUi.stylesheet.rename(stylesheetName); +test('can rename a stylesheet', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const wrongStylesheetName = 'WrongStylesheetName.css'; + await umbracoApi.stylesheet.ensureNameNotExists(wrongStylesheetName); + await umbracoApi.stylesheet.create(wrongStylesheetName, '', '/'); + await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); - // Assert - await umbracoUi.stylesheet.isSuccessNotificationVisible(); - expect(await umbracoApi.stylesheet.doesNameExist(stylesheetName)).toBeTruthy(); - expect(await umbracoApi.stylesheet.doesNameExist(wrongStylesheetName)).toBeFalsy(); - }); + //Act + await umbracoUi.stylesheet.reloadStylesheetTree(); + await umbracoUi.stylesheet.clickActionsMenuForStylesheet(wrongStylesheetName); + await umbracoUi.stylesheet.rename(stylesheetName); - test('can edit rich text editor styles', async ({umbracoApi, umbracoUi}) => { - // Arrange - const newStyleName = 'TestNewStyleName'; - const newStyleSelector = 'h2'; - const newStyleStyles = 'color: white'; - const newStylesheetContent = '/**umb_name:' + newStyleName + '*/\n' + newStyleSelector + ' {\n\t' + newStyleStyles + '\n}'; - const stylesheetContent = '/**umb_name:' + styleName + '*/\n' + styleSelector + ' {\n\t' + styleStyles + '\n}'; - await umbracoApi.stylesheet.create(stylesheetName, stylesheetContent, '/'); - expect(await umbracoApi.stylesheet.doesExist(stylesheetName)).toBeTruthy(); + // Assert + await umbracoUi.stylesheet.isSuccessNotificationVisible(); + expect(await umbracoApi.stylesheet.doesNameExist(stylesheetName)).toBeTruthy(); + expect(await umbracoApi.stylesheet.doesNameExist(wrongStylesheetName)).toBeFalsy(); +}); - //Act - await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); - await umbracoUi.stylesheet.openStylesheetByNameAtRoot(stylesheetName); - await umbracoUi.stylesheet.editRTEStyle(styleName, newStyleName, newStyleSelector, newStyleStyles); - await umbracoUi.stylesheet.clickSaveButton(); +test('can edit rich text editor styles', async ({umbracoApi, umbracoUi}) => { + // Arrange + const newStyleName = 'TestNewStyleName'; + const newStyleSelector = 'h2'; + const newStyleStyles = 'color: white'; + const newStylesheetContent = '/**umb_name:' + newStyleName + '*/\n' + newStyleSelector + ' {\n\t' + newStyleStyles + '\n}'; + const stylesheetContent = '/**umb_name:' + styleName + '*/\n' + styleSelector + ' {\n\t' + styleStyles + '\n}'; + await umbracoApi.stylesheet.create(stylesheetName, stylesheetContent, '/'); + expect(await umbracoApi.stylesheet.doesExist(stylesheetName)).toBeTruthy(); + await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); - // Assert - await umbracoUi.stylesheet.isSuccessNotificationVisible(); - const stylesheetData = await umbracoApi.stylesheet.getByName(stylesheetName); - expect(stylesheetData.content).toEqual(newStylesheetContent); - }); + //Act + await umbracoUi.stylesheet.openStylesheetByNameAtRoot(stylesheetName); + await umbracoUi.stylesheet.editRTEStyle(styleName, newStyleName, newStyleSelector, newStyleStyles); + await umbracoUi.stylesheet.clickSaveButton(); - test.skip('can remove rich text editor styles', async ({umbracoApi, umbracoUi}) => { - // Arrange - const stylesheetContent = '/**umb_name:' + styleName + '*/\n' + styleSelector + ' {\n\t' + styleStyles + '\n}'; - await umbracoApi.stylesheet.create(stylesheetName, stylesheetContent, '/'); - expect(await umbracoApi.stylesheet.doesExist(stylesheetName)).toBeTruthy(); + // Assert + await umbracoUi.stylesheet.isSuccessNotificationVisible(); + const stylesheetData = await umbracoApi.stylesheet.getByName(stylesheetName); + expect(stylesheetData.content).toEqual(newStylesheetContent); +}); - //Act - await umbracoUi.stylesheet.openStylesheetByNameAtRoot(stylesheetName); - await umbracoUi.stylesheet.removeRTEStyle(styleName); - await umbracoUi.stylesheet.clickSaveButton(); +test('can remove rich text editor styles', async ({umbracoApi, umbracoUi}) => { + // Arrange + const stylesheetContent = '/**umb_name:' + styleName + '*/\n' + styleSelector + ' {\n\t' + styleStyles + '\n}'; + await umbracoApi.stylesheet.create(stylesheetName, stylesheetContent, '/'); + expect(await umbracoApi.stylesheet.doesExist(stylesheetName)).toBeTruthy(); + await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); - // Assert - await umbracoUi.stylesheet.isSuccessNotificationVisible(); - const stylesheetData = await umbracoApi.stylesheet.getByName(stylesheetName); - expect(stylesheetData.content).toEqual(''); - }); + //Act + await umbracoUi.stylesheet.openStylesheetByNameAtRoot(stylesheetName); + await umbracoUi.stylesheet.removeRTEStyle(styleName); + await umbracoUi.stylesheet.clickSaveButton(); + + // Assert + await umbracoUi.stylesheet.isSuccessNotificationVisible(); + const stylesheetData = await umbracoApi.stylesheet.getByName(stylesheetName); + expect(stylesheetData.content).toEqual(''); +}); + +test('cannot create a stylesheet with an empty name', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.stylesheet.clickActionsMenuAtRoot(); + await umbracoUi.stylesheet.clickCreateButton(); + await umbracoUi.stylesheet.clickNewStylesheetButton(); + await umbracoUi.stylesheet.clickSaveButton(); + + // Assert + // TODO: Uncomment this when the front-end is ready. Currently there is no error displays. + //await umbracoUi.stylesheet.isErrorNotificationVisible(); + expect(await umbracoApi.stylesheet.doesNameExist(stylesheetName)).toBeFalsy(); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Stylesheet/StylesheetFolder.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Stylesheet/StylesheetFolder.spec.ts index 3ce08955ff..edc2552fba 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Stylesheet/StylesheetFolder.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Stylesheet/StylesheetFolder.spec.ts @@ -14,8 +14,10 @@ test.afterEach(async ({umbracoApi}) => { }); test('can create a folder', async ({umbracoApi, umbracoUi}) => { - // Act + // Arrange await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); + + // Act await umbracoUi.stylesheet.clickActionsMenuAtRoot(); await umbracoUi.stylesheet.createFolder(stylesheetFolderName); // TODO: remove it later @@ -30,9 +32,9 @@ test('can create a folder', async ({umbracoApi, umbracoUi}) => { test('can delete a folder', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.stylesheet.createFolder(stylesheetFolderName, ''); + await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); // Act - await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); await umbracoUi.stylesheet.reloadStylesheetTree(); await umbracoUi.stylesheet.clickActionsMenuForStylesheet(stylesheetFolderName); await umbracoUi.stylesheet.deleteFolder(); @@ -47,9 +49,9 @@ test('can create a folder in a folder', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.stylesheet.createFolder(stylesheetFolderName); const childFolderName = 'ChildFolderName'; + await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); // Act - await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); await umbracoUi.stylesheet.reloadStylesheetTree(); await umbracoUi.stylesheet.clickActionsMenuForStylesheet(stylesheetFolderName); await umbracoUi.stylesheet.createFolder(childFolderName); @@ -69,9 +71,9 @@ test('can create a folder in a folder in a folder', {tag: '@smoke'}, async ({umb const childOfChildFolderName = 'ChildOfChildFolderName'; await umbracoApi.stylesheet.createFolder(stylesheetFolderName); await umbracoApi.stylesheet.createFolder(childFolderName, stylesheetFolderName); + await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); // Act - await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); await umbracoUi.stylesheet.reloadStylesheetTree(); await umbracoUi.stylesheet.clickCaretButtonForName(stylesheetFolderName); await umbracoUi.stylesheet.clickActionsMenuForStylesheet(childFolderName); @@ -90,9 +92,9 @@ test('can create a stylesheet in a folder', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.stylesheet.createFolder(stylesheetFolderName); const stylesheetContent = 'TestContent'; + await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); //Act - await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); await umbracoUi.stylesheet.reloadStylesheetTree(); await umbracoUi.stylesheet.clickActionsMenuForStylesheet(stylesheetFolderName); await umbracoUi.stylesheet.clickCreateButton(); @@ -118,9 +120,9 @@ test('can create a stylesheet in a folder in a folder', async ({umbracoApi, umbr await umbracoApi.stylesheet.createFolder(stylesheetFolderName); await umbracoApi.stylesheet.createFolder(childFolderName, stylesheetFolderName); const stylesheetContent = 'TestContent'; + await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); //Act - await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); await umbracoUi.stylesheet.reloadStylesheetTree(); await umbracoUi.stylesheet.clickCaretButtonForName(stylesheetFolderName); await umbracoUi.stylesheet.clickActionsMenuForStylesheet(childFolderName); @@ -140,3 +142,19 @@ test('can create a stylesheet in a folder in a folder', async ({umbracoApi, umbr await umbracoUi.stylesheet.clickCaretButtonForName(childFolderName); await umbracoUi.stylesheet.isStylesheetRootTreeItemVisible(stylesheetName, true, false); }); + +test('cannot delete non-empty folder', async ({umbracoApi, umbracoUi}) => { + // Arrange + const childFolderName = 'ChildFolderName'; + await umbracoApi.stylesheet.createFolder(stylesheetFolderName); + await umbracoApi.stylesheet.createFolder(childFolderName, stylesheetFolderName); + await umbracoUi.stylesheet.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.stylesheet.clickRootFolderCaretButton(); + await umbracoUi.stylesheet.clickActionsMenuForStylesheet(stylesheetFolderName); + await umbracoUi.stylesheet.deleteFolder(); + + //Assert + await umbracoUi.stylesheet.isErrorNotificationVisible(); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Template/Templates.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Template/Templates.spec.ts index 58c74904b3..f1304ed4d6 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Template/Templates.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Template/Templates.spec.ts @@ -371,3 +371,17 @@ test.skip('can show returned items in query builder ', async ({umbracoApi, umbra // Clean await umbracoApi.documentType.ensureNameNotExists(documentTypeName); }); + +test('cannot create a template with an empty name', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.template.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.template.clickActionsMenuAtRoot(); + await umbracoUi.template.clickCreateButton(); + await umbracoUi.template.clickSaveButton(); + + // Assert + await umbracoUi.template.isErrorNotificationVisible(); + expect(await umbracoApi.template.doesNameExist(templateName)).toBeFalsy(); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts index 789bf96939..87c69bb6a9 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts @@ -224,12 +224,11 @@ test('can remove a content start node from a user', {tag: '@smoke'}, async ({umb test('can add media start nodes for a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange - const mediaTypeName = 'File'; const mediaName = 'TestMediaFile'; const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); await umbracoApi.media.ensureNameNotExists(mediaName); - const mediaId = await umbracoApi.media.createDefaultMedia(mediaName, mediaTypeName); + const mediaId = await umbracoApi.media.createDefaultMediaFile(mediaName); await umbracoUi.user.goToSection(ConstantHelper.sections.users); // Act @@ -249,15 +248,14 @@ test('can add media start nodes for a user', {tag: '@smoke'}, async ({umbracoApi test('can add multiple media start nodes for a user', async ({umbracoApi, umbracoUi}) => { // Arrange - const mediaTypeName = 'File'; const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); const mediaName = 'TestMediaFile'; const secondMediaName = 'SecondMediaFile'; await umbracoApi.media.ensureNameNotExists(mediaName); await umbracoApi.media.ensureNameNotExists(secondMediaName); - const firstMediaId = await umbracoApi.media.createDefaultMedia(mediaName, mediaTypeName); - const secondMediaId = await umbracoApi.media.createDefaultMedia(secondMediaName, mediaTypeName); + const firstMediaId = await umbracoApi.media.createDefaultMediaFile(mediaName); + const secondMediaId = await umbracoApi.media.createDefaultMediaFile(secondMediaName); // Adds the media start node to the user const userData = await umbracoApi.user.getByName(nameOfTheUser); userData.mediaStartNodeIds.push({id: firstMediaId}); @@ -283,12 +281,11 @@ test('can add multiple media start nodes for a user', async ({umbracoApi, umbrac test('can remove a media start node from a user', async ({umbracoApi, umbracoUi}) => { // Arrange - const mediaTypeName = 'File'; const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); const mediaName = 'TestMediaFile'; await umbracoApi.media.ensureNameNotExists(mediaName); - const mediaId = await umbracoApi.media.createDefaultMedia(mediaName, mediaTypeName); + const mediaId = await umbracoApi.media.createDefaultMediaFile(mediaName); // Adds the media start node to the user const userData = await umbracoApi.user.getByName(nameOfTheUser); userData.mediaStartNodeIds.push({id: mediaId}); @@ -374,12 +371,11 @@ test('can see if the user has the correct access based on content start nodes', test('can see if the user has the correct access based on media start nodes', async ({umbracoApi, umbracoUi}) => { // Arrange - const mediaTypeName = 'File'; const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); const mediaName = 'TestMediaFile'; await umbracoApi.media.ensureNameNotExists(mediaName); - const mediaId = await umbracoApi.media.createDefaultMedia(mediaName, mediaTypeName); + const mediaId = await umbracoApi.media.createDefaultMediaFile(mediaName); // Adds the media start node to the user const userData = await umbracoApi.user.getByName(nameOfTheUser); userData.mediaStartNodeIds.push({id: mediaId}); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ElementSwitchValidatorTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ElementSwitchValidatorTests.cs new file mode 100644 index 0000000000..0b7ebd84c9 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ElementSwitchValidatorTests.cs @@ -0,0 +1,358 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Extensions; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.ContentTypeEditing; +using Umbraco.Cms.Tests.Common.Attributes; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public class ElementSwitchValidatorTests : UmbracoIntegrationTest +{ + private IElementSwitchValidator ElementSwitchValidator => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private IContentService ContentService => GetRequiredService(); + + private IDataTypeService DataTypeService => GetRequiredService(); + + [TestCase(new[] { true }, 0, true, true, TestName = "E=>E No Ancestor or children")] + [TestCase(new[] { false }, 0, false, true, TestName = "D=>D No Ancestor or children")] + [TestCase(new[] { true }, 0, false, true, TestName = "E=>D No Ancestor or children")] + [TestCase(new[] { false }, 0, true, true, TestName = "D=>E No Ancestor or children")] + [TestCase(new[] { true, true }, 1, true, true, TestName = "E Valid Parent")] + [TestCase(new[] { true, true }, 0, true, true, TestName = "E Valid Child")] + [TestCase(new[] { false, false }, 1, false, true, TestName = "D Valid Parent")] + [TestCase(new[] { false, false }, 0, false, true, TestName = "D Valid Child")] + [TestCase(new[] { false, false }, 1, true, false, TestName = "E InValid Parent")] + [TestCase(new[] { false, false }, 0, true, true, TestName = "E InValid Child")] + [TestCase(new[] { true, true }, 1, false, false, TestName = "D InValid Parent")] + [TestCase(new[] { true, true }, 0, false, true, TestName = "D InValid Child")] + [TestCase(new[] { true, false, false, true, false }, 2, true, false, + TestName = "D=>E InValid Child, Invalid Parent")] + [TestCase(new[] { false, true, false, true, false }, 2, true, false, + TestName = "D=>E InValid Child, Invalid Ancestor")] + [TestCase(new[] { true, false, false, true, true }, 2, true, false, + TestName = "D=>E Valid Children, Invalid Parent")] + [TestCase(new[] { false, true, false, true, true }, 2, true, false, + TestName = "D=>E Valid Children, Invalid Ancestor")] + [TestCase(new[] { false, false, false, false, false }, 2, true, false, TestName = "D=>E mismatch")] + [TestCase(new[] { false, false, true, false, false }, 2, false, true, TestName = "D=>E correction")] + [TestCase(new[] { true, true, true, true, true }, 2, false, false, TestName = "E=>D mismatch")] + [TestCase(new[] { true, true, false, true, true }, 2, true, true, TestName = "E=>D correction")] + [LongRunning] + public async Task AncestorsAreAligned( + bool[] isElementDoctypeChain, + int itemToTestIndex, + bool itemToTestNewIsElementValue, + bool validationShouldPass) + { + // Arrange + IContentType? parentItem = null; + IContentType? itemToTest = null; + for (var index = 0; index < isElementDoctypeChain.Length; index++) + { + var itemIsElement = isElementDoctypeChain[index]; + var builder = new ContentTypeBuilder() + .WithIsElement(itemIsElement); + if (parentItem is not null) + { + builder.WithParentContentType(parentItem); + } + + var contentType = builder.Build(); + await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); + parentItem = contentType; + if (index == itemToTestIndex) + { + itemToTest = contentType; + } + } + + // Act + itemToTest!.IsElement = itemToTestNewIsElementValue; + var result = await ElementSwitchValidator.AncestorsAreAlignedAsync(itemToTest); + + // Assert + Assert.AreEqual(result, validationShouldPass); + } + + [TestCase(new[] { true }, 0, true, true, TestName = "E=>E No Ancestor or children")] + [TestCase(new[] { false }, 0, false, true, TestName = "D=>D No Ancestor or children")] + [TestCase(new[] { true }, 0, false, true, TestName = "E=>D No Ancestor or children")] + [TestCase(new[] { false }, 0, true, true, TestName = "D=>E No Ancestor or children")] + [TestCase(new[] { true, true }, 1, true, true, TestName = "E Valid Parent")] + [TestCase(new[] { true, true }, 0, true, true, TestName = "E Valid Child")] + [TestCase(new[] { false, false }, 1, false, true, TestName = "D Valid Parent")] + [TestCase(new[] { false, false }, 0, false, true, TestName = "D Valid Child")] + [TestCase(new[] { false, false }, 1, true, true, TestName = "E InValid Parent")] + [TestCase(new[] { false, false }, 0, true, false, TestName = "E InValid Child")] + [TestCase(new[] { true, true }, 1, false, true, TestName = "D InValid Parent")] + [TestCase(new[] { true, true }, 0, false, false, TestName = "D InValid Child")] + [TestCase(new[] { true, false, false, true, false }, 2, true, false, + TestName = "D=>E InValid Child, Invalid Parent")] + [TestCase(new[] { false, true, false, true, false }, 2, true, false, + TestName = "D=>E InValid Child, Invalid Ancestor")] + [TestCase(new[] { true, false, false, true, true }, 2, true, true, + TestName = "D=>E Valid Children, Invalid Parent")] + [TestCase(new[] { false, true, false, true, true }, 2, true, true, + TestName = "D=>E Valid Children, Invalid Ancestor")] + [TestCase(new[] { false, false, false, false, false }, 2, true, false, TestName = "D=>E mismatch")] + [TestCase(new[] { false, false, true, false, false }, 2, false, true, TestName = "D=>E correction")] + [TestCase(new[] { true, true, true, true, true }, 2, false, false, TestName = "E=>D mismatch")] + [TestCase(new[] { true, true, false, true, true }, 2, true, true, TestName = "E=>D correction")] + [LongRunning] + public async Task DescendantsAreAligned( + bool[] isElementDoctypeChain, + int itemToTestIndex, + bool itemToTestNewIsElementValue, + bool validationShouldPass) + { + // Arrange + IContentType? parentItem = null; + IContentType? itemToTest = null; + for (var index = 0; index < isElementDoctypeChain.Length; index++) + { + var itemIsElement = isElementDoctypeChain[index]; + var builder = new ContentTypeBuilder() + .WithIsElement(itemIsElement); + if (parentItem is not null) + { + builder.WithParentContentType(parentItem); + } + + var contentType = builder.Build(); + await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); + parentItem = contentType; + if (index == itemToTestIndex) + { + itemToTest = contentType; + } + } + + // Act + itemToTest!.IsElement = itemToTestNewIsElementValue; + var result = await ElementSwitchValidator.DescendantsAreAlignedAsync(itemToTest); + + // Assert + Assert.AreEqual(result, validationShouldPass); + } + + [TestCase(0, true, TestName = "No Content")] + [TestCase(1, false, TestName = "One Content Item")] + [TestCase(5, false, TestName = "Many Content Items")] + public async Task DocumentToElementHasNoContent(int amountOfDocumentsCreated, bool validationShouldPass) + { + // Arrange + var contentType = await SetupContentType(false); + + for (int i = 0; i < amountOfDocumentsCreated; i++) + { + var contentBuilder = new ContentBuilder().WithContentType(contentType); + var content = contentBuilder.Build(); + ContentService.Save(content); + } + + // Act + contentType.IsElement = true; + var result = await ElementSwitchValidator.DocumentToElementHasNoContentAsync(contentType); + + // Assert + Assert.AreEqual(result, validationShouldPass); + } + + // Since the full permutation table would result in 64 tests and more block editors might be added later, + // we will at least test each single failure and a few combinations + // used in none + [TestCase(false, false, false, false, false, false, true)] + // used in one + [TestCase(true, false, false, false, false, false, false)] + [TestCase(false, true, false, false, false, false, false)] + [TestCase(false, false, true, false, false, false, false)] + [TestCase(false, false, false, true, false, false, false)] + [TestCase(false, false, false, false, true, false, false)] + [TestCase(false, false, false, false, false, true, false)] + // used in selection and setting + [TestCase(true, true, false, false, false, false, false)] + // used in 2 selections + [TestCase(true, false, true, false, false, false, false)] + // used in 2 settings + [TestCase(false, true, false, false, false, true, false)] + // used in all + [TestCase(true, true, true, true, true, true, false)] + public async Task ElementToDocumentNotUsedInBlockStructures( + bool isUsedInBlockList, + bool isUsedInBlockListBlockSetting, + bool isUsedInBlockGrid, + bool isUsedInBlockGridBlockSetting, + bool isUsedInRte, + bool isUsedInRteBlockSetting, + bool validationShouldPass) + { + // Arrange + var elementType = await SetupContentType(true); + + var otherElementType = await SetupContentType(true); + + if (isUsedInBlockList) + { + await SetupDataType(Constants.PropertyEditors.Aliases.BlockList, elementType.Key, null); + } + + if (isUsedInBlockListBlockSetting) + { + await SetupDataType(Constants.PropertyEditors.Aliases.BlockList, otherElementType.Key, elementType.Key); + } + + if (isUsedInBlockGrid) + { + await SetupDataType(Constants.PropertyEditors.Aliases.BlockGrid, elementType.Key, null); + } + + if (isUsedInBlockGridBlockSetting) + { + await SetupDataType(Constants.PropertyEditors.Aliases.BlockGrid, otherElementType.Key, elementType.Key); + } + + if (isUsedInRte) + { + await SetupDataType(Constants.PropertyEditors.Aliases.RichText, elementType.Key, null); + } + + if (isUsedInRteBlockSetting) + { + await SetupDataType(Constants.PropertyEditors.Aliases.RichText, otherElementType.Key, elementType.Key); + } + + // Act + var result = await ElementSwitchValidator.ElementToDocumentNotUsedInBlockStructuresAsync(elementType); + + // Assert + Assert.AreEqual(result, validationShouldPass); + } + + private async Task SetupContentType(bool isElement) + { + var typeBuilder = new ContentTypeBuilder() + .WithIsElement(isElement); + var contentType = typeBuilder.Build(); + await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); + return contentType; + } + + // Constants.PropertyEditors.Aliases.BlockGrid + private async Task SetupDataType( + string editorAlias, + Guid elementKey, + Guid? elementSettingKey) + { + Dictionary configuration; + switch (editorAlias) + { + case Constants.PropertyEditors.Aliases.BlockGrid: + configuration = GetBlockGridBaseConfiguration(); + break; + case Constants.PropertyEditors.Aliases.RichText: + configuration = GetRteBaseConfiguration(); + break; + default: + configuration = new Dictionary(); + break; + } + + SetBlockConfiguration( + configuration, + elementKey, + elementSettingKey, + editorAlias == Constants.PropertyEditors.Aliases.BlockGrid ? true : null); + + + var dataTypeBuilder = new DataTypeBuilder() + .WithId(0) + .WithDatabaseType(ValueStorageType.Nvarchar) + .AddEditor() + .WithAlias(editorAlias); + + switch (editorAlias) + { + case Constants.PropertyEditors.Aliases.BlockGrid: + dataTypeBuilder.WithConfigurationEditor( + new BlockGridConfigurationEditor(IOHelper) { DefaultConfiguration = configuration }); + break; + case Constants.PropertyEditors.Aliases.BlockList: + dataTypeBuilder.WithConfigurationEditor( + new BlockListConfigurationEditor(IOHelper) { DefaultConfiguration = configuration }); + break; + case Constants.PropertyEditors.Aliases.RichText: + dataTypeBuilder.WithConfigurationEditor( + new RichTextConfigurationEditor(IOHelper) { DefaultConfiguration = configuration }); + break; + } + + var dataType = dataTypeBuilder.Done() + .Build(); + + await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey); + } + + private void SetBlockConfiguration( + Dictionary dictionary, + Guid? elementKey, + Guid? elementSettingKey, + bool? allowAtRoot) + { + if (elementKey is null) + { + return; + } + + dictionary["blocks"] = new[] { BuildBlockConfiguration(elementKey.Value, elementSettingKey, allowAtRoot) }; + } + + private Dictionary GetBlockGridBaseConfiguration() + => new Dictionary { ["gridColumns"] = 12 }; + + private Dictionary GetRteBaseConfiguration() + { + var dictionary = new Dictionary + { + ["maxImageSize"] = 500, + ["mode"] = "Classic", + ["toolbar"] = new[] + { + "styles", "bold", "italic", "alignleft", "aligncenter", "alignright", "bullist", "numlist", + "outdent", "indent", "sourcecode", "link", "umbmediapicker", "umbembeddialog" + }, + }; + return dictionary; + } + + private Dictionary BuildBlockConfiguration( + Guid? elementKey, + Guid? elementSettingKey, + bool? allowAtRoot) + { + var dictionary = new Dictionary(); + if (allowAtRoot is not null) + { + dictionary.Add("allowAtRoot", allowAtRoot.Value); + } + + dictionary.Add("contentElementTypeKey", elementKey.ToString()); + if (elementSettingKey is not null) + { + dictionary.Add("settingsElementTypeKey", elementSettingKey.ToString()); + } + + return dictionary; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs index abc4e3894c..462205b231 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs @@ -332,6 +332,22 @@ public class DomainAndUrlsTests : UmbracoIntegrationTest Assert.AreEqual(DomainOperationStatus.DuplicateDomainName, result.Status); } + [TestCase("https://*.umbraco.com")] + [TestCase("&#€%#€")] + [TestCase("¢”$¢”¢$≈{")] + public async Task Cannot_Assign_Invalid_Domains(string domainName) + { + var domainService = GetRequiredService(); + var updateModel = new DomainsUpdateModel + { + Domains = new DomainModel { DomainName = domainName, IsoCode = Cultures.First() }.Yield() + }; + + var result = await domainService.UpdateDomainsAsync(Root.Key, updateModel); + Assert.IsFalse(result.Success); + Assert.AreEqual(DomainOperationStatus.InvalidDomainName, result.Status); + } + [Test] public async Task Cannot_Assign_Already_Used_Domains() { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/ObjectExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/ObjectExtensionsTests.cs index bfbe581f2e..7aae5e54ee 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/ObjectExtensionsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/ObjectExtensionsTests.cs @@ -11,6 +11,7 @@ using NUnit.Framework; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Tests.Common.TestHelpers; using Umbraco.Extensions; +using DateTimeOffset = System.DateTimeOffset; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.CoreThings; @@ -332,6 +333,50 @@ public class ObjectExtensionsTests Assert.AreEqual("This is a string", conv.Result); } + [Test] + public void CanConvertDateTimeOffsetToDateTime() + { + var dateTimeOffset = new DateTimeOffset(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03), TimeSpan.Zero); + var result = dateTimeOffset.TryConvertTo(); + Assert.IsTrue(result.Success); + Assert.Multiple(() => + { + Assert.AreEqual(new DateTime(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03)), result.Result); + Assert.AreEqual(DateTimeKind.Utc, result.Result.Kind); + }); + } + + [Test] + public void CanConvertDateTimeToDateTimeOffset() + { + var dateTime = new DateTime(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03), DateTimeKind.Utc); + var result = dateTime.TryConvertTo(); + Assert.IsTrue(result.Success); + Assert.AreEqual(new DateTimeOffset(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03), TimeSpan.Zero), result.Result); + } + + [Test] + public void DiscardsOffsetWhenConvertingDateTimeOffsetToDateTime() + { + var dateTimeOffset = new DateTimeOffset(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03), TimeSpan.FromHours(2)); + var result = dateTimeOffset.TryConvertTo(); + Assert.IsTrue(result.Success); + Assert.Multiple(() => + { + Assert.AreEqual(new DateTime(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03)), result.Result); + Assert.AreEqual(DateTimeKind.Utc, result.Result.Kind); + }); + } + + [Test] + public void DiscardsDateTimeKindWhenConvertingDateTimeToDateTimeOffset() + { + var dateTime = new DateTime(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03), DateTimeKind.Local); + var result = dateTime.TryConvertTo(); + Assert.IsTrue(result.Success); + Assert.AreEqual(new DateTimeOffset(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03), TimeSpan.Zero), result.Result); + } + [Test] public void Value_Editor_Can_Convert_Decimal_To_Decimal_Clr_Type() { @@ -342,6 +387,23 @@ public class ObjectExtensionsTests Assert.AreEqual(12.34d, result.Result); } + [Test] + public void Value_Editor_Can_Convert_DateTimeOffset_To_DateTime_Clr_Type() + { + var valueEditor = MockedValueEditors.CreateDataValueEditor(ValueTypes.Date); + + var result = valueEditor.TryConvertValueToCrlType(new DateTimeOffset(new DateOnly(2024, 07, 05), new TimeOnly(12, 30), TimeSpan.Zero)); + Assert.IsTrue(result.Success); + Assert.IsTrue(result.Result is DateTime); + + var dateTime = (DateTime)result.Result; + Assert.Multiple(() => + { + Assert.AreEqual(new DateTime(new DateOnly(2024, 07, 05), new TimeOnly(12, 30)), dateTime); + Assert.AreEqual(DateTimeKind.Utc, dateTime.Kind); + }); + } + private class MyTestObject { public override string ToString() => "Hello world"; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/UdiTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/UdiTests.cs index cf89b6d13b..b7f9e4c4cd 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/UdiTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/UdiTests.cs @@ -201,6 +201,21 @@ public class UdiTests Assert.Throws(() => new UdiRange(guidUdi, "x")); } + [Test] + [TestCase(Constants.DeploySelector.This)] + [TestCase(Constants.DeploySelector.ThisAndChildren)] + [TestCase(Constants.DeploySelector.ThisAndDescendants)] + [TestCase(Constants.DeploySelector.ChildrenOfThis)] + [TestCase(Constants.DeploySelector.DescendantsOfThis)] + [TestCase(Constants.DeploySelector.EntitiesOfType)] + public void RangeParseTest(string selector) + { + var expected = new UdiRange(Udi.Create(Constants.UdiEntityType.AnyGuid, Guid.NewGuid()), selector); + var actual = UdiRange.Parse(expected.ToString()); + + Assert.AreEqual(expected, actual); + } + [Test] public void TryParseTest() { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs index 93b0d5bba3..6ef0f74e2f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Linq; using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; @@ -13,7 +12,6 @@ using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Tests.Common; using Umbraco.Cms.Tests.UnitTests.TestHelpers.Objects; -using Umbraco.Extensions; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Templates; @@ -111,10 +109,16 @@ public class HtmlLocalLinkParserTests // current [TestCase( "world", - "world")] + "world")] [TestCase( "world", - "world")] + "world")] + [TestCase( + "world", + "world")] + [TestCase( + "world", + "world")] // legacy [TestCase( "hello href=\"{localLink:1234}\" world ", diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParserTests.cs index 43ec2136f7..850aabbe04 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParserTests.cs @@ -72,8 +72,8 @@ public class ApiRichTextMarkupParserTests

and to the other page

"; var expectedOutput = - @"

Rich text outside of the blocks with a link to itself

-

and to the other page

"; + @"

Rich text outside of the blocks with a link to itself

+

and to the other page

"; var parsedHtml = parser.Parse(html); @@ -81,7 +81,7 @@ public class ApiRichTextMarkupParserTests } [Test] - public void Can_Parse_Legacy_LocalImages() + public void Can_Parse_Inline_LocalImages() { var key1 = Guid.Parse("395bdc0e8f4d4ad4af7f3a3f6265651e"); var data1 = new MockData() diff --git a/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj b/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj index 431674852e..ce5001608f 100644 --- a/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj +++ b/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj @@ -7,7 +7,7 @@ - +