diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b2e6ac484f..c14b41f13f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -61,7 +61,7 @@ This guide describes each step to make your first contribution: ## Further contribution guides - [Before you start](contributing-before-you-start.md) -- [Finding your first issue: Up for grabs](contributing-before-you-start.md) +- [Finding your first issue: Up for grabs](contributing-first-issue.md) - [Contributing to the new backoffice](https://docs.umbraco.com/umbraco-backoffice/) - [Unwanted changes](contributing-unwanted-changes.md) - [Other ways to contribute](contributing-other-ways-to-contribute.md) diff --git a/NOTICES.txt b/NOTICES.txt new file mode 100644 index 0000000000..1ea114b715 --- /dev/null +++ b/NOTICES.txt @@ -0,0 +1,571 @@ +Third-Party Notices +=================== + +This file contains notices and attributions for third-party software used in the Umbraco CMS project. + +It is not a license and does not grant any rights to use the third-party software. + +Umbraco CMS is licensed under the MIT License, which can be found in the LICENSE file. + +--- + +@openid/AppAuth-JS: An OpenID Connect and OAuth 2.0 client library for JavaScript + +URL: https://github.com/openid/AppAuth-JS +License: Apache License, Version 2.0 +Copyright: 2017 Google Inc. + +--- + +AutoFixture: Write maintainable unit tests, faster + +URL: https://github.com/AutoFixture/AutoFixture +License: MIT License +Copyright: 2013 Mark Seemann + +--- + +Asp.Versioning.Mvc: A library for ASP.NET Core versioning + +URL: https://github.com/dotnet/aspnet-api-versioning +License: MIT License +Copyright: .NET Foundation and contributors + +--- + +Babel: A JavaScript compiler + +URL: https://babeljs.io/ +License: MIT License +Copyright: 2014-present Sebastian McKenzie and other contributors + +--- + +BenchmarkDotNet: Powerful .NET library for benchmarking + +URL: https://github.com/dotnet/BenchmarkDotNet +License: MIT License +Copyright: .NET Foundation and Contributors + +--- + +Bogus: A simple and sane data generator for populating objects that supports different locales. + +URL: https://github.com/bchavez/Bogus +License: MIT License +Copyright: 2015 Brian Chavez + +--- + +CommandLineParser: Terse syntax C# command line parser for .NET + +URL: https://github.com/commandlineparser/commandline +License: MIT License +Copyright: 2005-2015 Giacomo Stelluti Scala & Contributors + +--- + +cross-env: A CLI tool to set environment variables across platforms + +URL: https://github.com/kentcdodds/cross-env +License: MIT License +Copyright: 2017 Kent C. Dodds + +--- + +Dazinator.Extensions.FileProviders: A library for file provider extensions + +URL: https://github.com/dazinator/Dazinator.Extensions.FileProviders +License: MIT License +Copyright: 2016 Darrell + +--- + +DOMPurify: A DOM-only XSS sanitizer for HTML, MathML and SVG + +URL: https://github.com/cure53/DOMPurify +License: Apache License, Version 2.0 +Copyright: 2025 Dr.-Ing. Mario Heiderich, Cure53 + +--- + +Element Internals Polyfill: A polyfill for the Element Internals API + +URL: https://github.com/calebdwilliams/element-internals-polyfill +License: MIT License +Copyright: 2021 Caleb Williams + +--- + +Eslint: A tool for identifying and reporting on patterns in JavaScript + +URL: https://eslint.org/ +License: MIT License +Copyright: OpenJS Foundation and other contributors + +--- + +Examine: A search and indexing library for .NET + +URL: https://github.com/Shazwazza/Examine +License: Microsoft Public License (Ms-PL) +Copyright: 2023 Shannon Deminick + +--- + +Glob: A library for matching file paths using glob patterns + +URL: https://github.com/isaacs/node-glob +License: ISC License +Copyright: 2009-2023 Isaac Z. Schlueter and Contributors + +--- + +Globals: A library for managing global variables in JavaScript + +URL: https://github.com/sindresorhus/globals +License: MIT License +Copyright: Sindre Sorhus + +--- + +Html Agility Pack: An HTML parser for .NET + +URL: https://html-agility-pack.net/ +License: MIT License +Copyright: ZZZ Projects Inc. + +--- + +ImageSharp: A cross-platform library for processing images in .NET + +URL: https://github.com/SixLabors/ImageSharp +License: Apache License, Version 2.0 under the Six Labors Split License +Copyright: Six Labors + +--- + +jsdiff: A JavaScript text differencing implementation + +URL: https://github.com/kpdecker/jsdiff +License: BSD 3-Clause License +Copyright: 2009-2015 Kevin Decker + +--- + +JsonPatch.Net: A library for JSON Patch (RFC 6902) in .NET + +URL: https://github.com/json-everything/json-everything +License: MIT License +Copyright: .NET Foundation and Contributors + +--- + +K4os.Compression.LZ4: A fast LZ4 compression library for .NET + +URL: https://github.com/MiloszKrajewski/K4os.Compression.LZ4 +License: MIT License +Copyright: 2017 Milosz Krajewski + +--- + +Lit: A simple library for building fast, lightweight web components + +URL: https://lit.dev +License: BSD 3-Clause License +Copyright: 2020 Google LLC. All rights reserved. + +--- + +Lucide: Beautiful & consistent icons for the web + +URL: https://lucide.dev/ +License: ISC License +Copyright: 2013-2022 Cole Bemis +Copyright: 2022 Lucide Contributors + +--- + +Madge: A dependency graph generator for JavaScript + +URL: https://github.com/pahen/madge +License: MIT License +Copyright: 2017 Patrik Henningsson + +--- + +MailKit: A library for sending email in .NET + +URL: https://github.com/jstedfast/MailKit +License: MIT License +Copyright: 2013-2024 .NET Foundation and Contributors + +--- + +Markdown: A library for parsing and compiling Markdown + +URL: https://github.com/hey-red/Markdown +License: MIT License +Copyright: 2018 red + +--- + +marked: A markdown parser and compiler + +URL: https://marked.js.org/ +License: MIT License +Copyright: 2011-2018, Christopher Jeffrey (https://github.com/chjj/) +Copyright: 2018+, MarkedJS (https://github.com/markedjs/) + +--- + +Message Pack: The extremely fast MessagePack serializer for C# + +URL: https://github.com/MessagePack-CSharp/MessagePack-CSharp +License: MIT License +Copyright: 2017 Yoshifumi Kawai and contributors + +--- + +Miniprofiler: A mini profiler for .NET + +URL: https://github.com/MiniProfiler/dotnet +License: MIT License +Copyright: .NET MiniProfiler Contributors + +--- + +Monaco Editor: A browser-based code editor + +URL: https://microsoft.github.io/monaco-editor/ +License: MIT License +Copyright: 2016-present Microsoft Corporation + +--- + +Moq: A mocking library for .NET + +URL: https://github.com/moq/moq +License: BSD 3-Clause License +Copyright: 2007 Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors. + +--- + +Mock Service Worker (MSW): A library for mocking API requests in JavaScript + +URL: https://mswjs.io/ +License: MIT License +Copyright: 2018–present Artem Zakharchenko + +--- + +NCrontab: A cron schedule parser for .NET + +URL: https://github.com/atifaziz/NCrontab +License: Apache License, Version 2.0 +Copyright: 2001 The OpenSymphony Group +Copyright: 2008 Atif Aziz + +--- + +Nerdbank.GitVersioning: A library for versioning .NET projects + +URL: https://github.com/dotnet/Nerdbank.GitVersioning +License: MIT License +Copyright: .NET Foundation and Contributors + +--- + +NJsonSchema: A JSON schema validator for .NET + +URL: https://github.com/RicoSuter/NJsonSchema +License: MIT License +Copyright: 2022 Rico Suter + +--- + +NPoco: A micro ORM for .NET + +URL: https://github.com/schotime/NPoco +License: Apache License, Version 2.0 +Copyright: Schotime + +--- + +NUnit: A unit testing framework for .NET + +URL: https://github.com/nunit/nunit +License: MIT License +Copyright: Charlie Poole, Rob Prouse and Contributors + +--- + +Open Web Components: A set of standards and libraries for building web components + +URL: https://open-wc.org/ +License: MIT License +Copyright: 2018 open-wc + +--- + +Openapi-ts: The OpenAPI to TypeScript codegen + +URL: https://github.com/hey-api/openapi-ts +License: MIT License +Copyright: Hey API + +--- + +OpenIddict: A simple and flexible OpenID Connect server for ASP.NET Core + +URL: https://github.com/openiddict/openiddict-core +License: Apache License, Version 2.0 +Copyright: Kévin Chalet + +--- + +Playwright: A Node.js library to automate browser testing + +URL: https://playwright.dev/ +License: Apache License, Version 2.0 +Copyright: 2025 Microsoft Corporation + +--- + +Playwright-msw: A library to wrap Mock Service Worker with Playwright + +URL: https://github.com/valendres/playwright-msw +License: MIT License +Copyright: 2022 Peter Weller + +--- + +Prettier: An opinionated code formatter + +URL: https://prettier.io/ +License: MIT License +Copyright: James Long and contributors + +--- + +Remark-gfm: A GitHub Flavored Markdown plugin for Remark + +URL: https://github.com/remarkjs/remark-gfm +License: MIT License +Copyright: Titus Wormer + +--- + +Rollup: A module bundler for JavaScript + +URL: https://rollupjs.org/ +License: MIT License +Copyright: 2015-present Rollup contributors + +--- + +Rollup Plugins: A collection of Rollup plugins + +URL: https://github.com/rollup/plugins +License: MIT License +Copyright: 2019-present Rollup Plugins contributors + +--- + +Rollup-plugin-esbuild: A Rollup plugin for using esbuild + +URL: https://github.com/egoist/rollup-plugin-esbuild +License: MIT License +Copyright: 2020 EGOIST + +--- + +Rollup-plugin-import-css: A Rollup plugin for importing CSS files + +URL: https://github.com/jleeson/rollup-plugin-import-css +License: MIT License +Copyright: 2020 Jacob Leeson + +--- + +rxjs: Reactive Extensions for JavaScript + +URL: https://rxjs.dev/ +License: Apache License, Version 2.0 +Copyright: 2015-present Ben Lesh , Google, Inc., Netflix, Inc., Microsoft Corp., and contributors + +--- + +Serilog: A diagnostic logging library for .NET + +URL: https://github.com/serilog/serilog +License: Apache License, Version 2.0 +Copyright: Serilog Contributors + +--- + +Simple Icons: A set of SVG icons for popular brands + +URL: https://simpleicons.org/ +License: CC0 1.0 Universal License +Copyright: Simple Icons Contributors + +--- + +Storybook: A UI component explorer for Web Components + +URL: https://storybook.js.org/ +License: MIT License +Copyright: 2024 Storybook + +--- + +StyleCop.Analyzers: Analyzers for StyleCop + +URL: https://github.com/DotNetAnalyzers/StyleCopAnalyzers +License: MIT License +Copyright: Tunnel Vision Laboratories, LLC + +--- + +SVGO: A tool for optimizing SVG files + +URL: https://svgo.dev/ +License: MIT License +Copyright: Kir Belevich + +--- + +Swashbuckle.AspNetCore: A library for generating Swagger documentation for ASP.NET Core APIs + +URL: https://github.com/domaindrivendev/Swashbuckle.AspNetCore +License: MIT License +Copyright: 2016 Richard Morris + +--- + +Tiny Glob: A tiny globbing library for Node.js + +URL: https://github.com/terkelg/tiny-glob +License: MIT License +Copyright: 2018 Terkel + +--- + +Tiptap: A renderless rich-text editor for the web + +URL: https://tiptap.dev/ +License: MIT License +Copyright: 2025 Tiptap GmbH + +--- + +Tsc-alias: A TypeScript compiler plugin for aliasing module paths + +URL: https://github.com/justkey007/tsc-alias +License: MIT License +Copyright: 2018 Justkey + +--- + +Typedoc: A documentation generator for TypeScript projects + +URL: https://typedoc.org/ +License: Apache License, Version 2.0 +Copyright: Gerrit Birkeland and Contributors + +--- + +Typescript: A typed superset of JavaScript that compiles to plain JavaScript + +URL: https://www.typescriptlang.org/ +License: Apache License, Version 2.0 +Copyright: 2012-present Microsoft Corporation + +--- + +Typescript-eslint: A set of tools for linting TypeScript code + +URL: https://github.com/typescript-eslint/typescript-eslint +License: MIT License +Copyright: 2019 typescript-eslint and other contributors + +--- + +Typescript-json-schema: A library for generating JSON schema from TypeScript types + +URL: https://github.com/YousefED/typescript-json-schema +License: BSD 3-Clause License +Copyright: 2016 typescript-json-schema contributors + +--- + +Umbraco.Code: Provides code-level tools for Umbraco + +URL: https://github.com/umbraco/Umbraco-Code +License: MIT License +Copyright: 2005-present Umbraco A/S + +--- + +Umbraco.GitVersioning.Extensions: Utilities for Nerdbank.GitVersioning + +URL: https://github.com/umbraco/Umbraco.GitVersioning.Extensions +License: MIT License +Copyright: 2005-present Umbraco A/S + +--- + +Umbraco.JsonSchema.Extensions: Utilities for JSON schema generation + +URL: https://github.com/umbraco/Umbraco.JsonSchema.Extensions +License: MIT License +Copyright: 2005-present Umbraco A/S + +--- + +Umbraco UI Library: A set of UI components for building web applications + +URL: https://uui.umbraco.com/ +License: MIT License +Copyright: 2005-present Umbraco A/S + +--- + +uuid: A library for generating unique identifiers + +URL: https://github.com/uuidjs/uuid +License: MIT License +Copyright: 2010-2020 Robert Kieffer and other contributors + +--- + +Vite: A fast build tool and development server for modern web projects + +URL: https://vite.dev/ +License: MIT License +Copyright: 2019-present VoidZero Inc. and Vite contributors + +--- + +Vite-plugin-static-copy: A Vite plugin for copying static files + +URL: https://github.com/sapphi-red/vite-plugin-static-copy +License: MIT License +Copyright: 2021 sapphi-red + +--- + +Vite-tsconfig-paths: A Vite plugin for resolving TypeScript paths + +URL: https://github.com/aleclarson/vite-tsconfig-paths +License: MIT License +Copyright: Alec Larson + +--- + +Web Component Analyzer: A tool for analyzing web components + +URL: https://github.com/runem/web-component-analyzer +License: MIT License +Copyright: 2019 Rune Mehlsen diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentVersionPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentVersionPresentationFactory.cs index 21d87f53f8..5006ccbaa7 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentVersionPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentVersionPresentationFactory.cs @@ -26,7 +26,7 @@ internal sealed class DocumentVersionPresentationFactory : IDocumentVersionPrese new ReferenceByIdModel(_entityService.GetKey(contentVersion.ContentTypeId, UmbracoObjectTypes.DocumentType) .Result), new ReferenceByIdModel(await _userIdKeyResolver.GetAsync(contentVersion.UserId)), - new DateTimeOffset(contentVersion.VersionDate, TimeSpan.Zero), // todo align with datetime offset rework + new DateTimeOffset(contentVersion.VersionDate), contentVersion.CurrentPublishedVersion, contentVersion.CurrentDraftVersion, contentVersion.PreventCleanup); diff --git a/src/Umbraco.Cms.Api.Management/Serialization/ConfigureUmbracoBackofficeJsonOptions.cs b/src/Umbraco.Cms.Api.Management/Serialization/ConfigureUmbracoBackofficeJsonOptions.cs index 8f668b268d..d66d47cf16 100644 --- a/src/Umbraco.Cms.Api.Management/Serialization/ConfigureUmbracoBackofficeJsonOptions.cs +++ b/src/Umbraco.Cms.Api.Management/Serialization/ConfigureUmbracoBackofficeJsonOptions.cs @@ -36,5 +36,7 @@ public class ConfigureUmbracoBackofficeJsonOptions : IConfigureNamedOptionsValue cannot be empty Value is invalid, it does not match the correct pattern %1% more.]]> - %1% too many.]]> + %1% too many.]]> The string length exceeds the maximum length of %0% characters, %1% too many. The content amount requirements are not met for one or more areas. Invalid member group name diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index cd65b07490..d7a56e1d1d 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -394,7 +394,7 @@ The value %0% is not expected to contain a range The value %0% is not expected to have a to value less than the from value %1% more.]]> - %1% too many.]]> + %1% too many.]]> The string length exceeds the maximum length of %0% characters, %1% too many. The content amount requirements are not met for one or more areas. The chosen media type is invalid. diff --git a/src/Umbraco.Core/Models/ContentVersionMeta.cs b/src/Umbraco.Core/Models/ContentVersionMeta.cs index cf95257716..b9b1be6080 100644 --- a/src/Umbraco.Core/Models/ContentVersionMeta.cs +++ b/src/Umbraco.Core/Models/ContentVersionMeta.cs @@ -37,7 +37,7 @@ public class ContentVersionMeta public int UserId { get; } - public DateTime VersionDate { get; } + public DateTime VersionDate { get; private set; } public bool CurrentPublishedVersion { get; } @@ -47,5 +47,7 @@ public class ContentVersionMeta public string? Username { get; } + public void SpecifyVersionDateKind(DateTimeKind kind) => VersionDate = DateTime.SpecifyKind(VersionDate, kind); + public override string ToString() => $"ContentVersionMeta(versionId: {VersionId}, versionDate: {VersionDate:s}"; } diff --git a/src/Umbraco.Core/Services/PublicAccessService.cs b/src/Umbraco.Core/Services/PublicAccessService.cs index d5ad2069c3..465b37b89c 100644 --- a/src/Umbraco.Core/Services/PublicAccessService.cs +++ b/src/Umbraco.Core/Services/PublicAccessService.cs @@ -1,5 +1,3 @@ -using System.Globalization; -using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; @@ -67,19 +65,14 @@ internal sealed class PublicAccessService : RepositoryService, IPublicAccessServ { // Get all ids in the path for the content item and ensure they all // parse to ints that are not -1. - var ids = contentPath.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) - .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val) ? val : -1) - .Where(x => x != -1) - .ToList(); - - // start with the deepest id - ids.Reverse(); + // Start with the deepest id. + IEnumerable ids = contentPath.GetIdsFromPathReversed().Where(x => x != -1); using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { // This will retrieve from cache! var entries = _publicAccessRepository.GetMany().ToList(); - foreach (var id in CollectionsMarshal.AsSpan(ids)) + foreach (var id in ids) { PublicAccessEntry? found = entries.Find(x => x.ProtectedNodeId == id); if (found != null) @@ -286,7 +279,7 @@ internal sealed class PublicAccessService : RepositoryService, IPublicAccessServ return Attempt.FailWithStatus(PublicAccessOperationStatus.NoAllowedEntities, result); } - if(entry.MemberUserNames.Any() && entry.MemberGroupNames.Any()) + if (entry.MemberUserNames.Any() && entry.MemberGroupNames.Any()) { return Attempt.FailWithStatus(PublicAccessOperationStatus.AmbiguousRule, result); } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs index fed80e753f..ffcc6447eb 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs @@ -39,8 +39,11 @@ internal class ContentBaseFactory content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId; content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId; - content.CreateDate = nodeDto.CreateDate; - content.UpdateDate = contentVersionDto.VersionDate; + + // Dates stored in the database are local server time, but for SQL Server, will be considered + // as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset. + content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local); + content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local); content.Published = dto.Published; content.Edited = dto.Edited; @@ -52,7 +55,7 @@ internal class ContentBaseFactory content.PublishedVersionId = publishedVersionDto.Id; if (dto.Published) { - content.PublishDate = publishedVersionDto.ContentVersionDto.VersionDate; + content.PublishDate = DateTime.SpecifyKind(publishedVersionDto.ContentVersionDto.VersionDate, DateTimeKind.Local); content.PublishName = publishedVersionDto.ContentVersionDto.Text; content.PublisherId = publishedVersionDto.ContentVersionDto.UserId; } @@ -71,7 +74,7 @@ internal class ContentBaseFactory } /// - /// Builds an IMedia item from a dto and content type. + /// Builds a Media item from a dto and content type. /// public static Core.Models.Media BuildEntity(ContentDto dto, IMediaType? contentType) { @@ -97,8 +100,8 @@ internal class ContentBaseFactory content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId; content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId; - content.CreateDate = nodeDto.CreateDate; - content.UpdateDate = contentVersionDto.VersionDate; + content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local); + content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local); // reset dirty initial properties (U4-1946) content.ResetDirtyProperties(false); @@ -111,7 +114,7 @@ internal class ContentBaseFactory } /// - /// Builds an IMedia item from a dto and content type. + /// Builds a Member item from a dto and member type. /// public static Member BuildEntity(MemberDto dto, IMemberType? contentType) { @@ -126,7 +129,9 @@ internal class ContentBaseFactory content.Id = dto.NodeId; content.SecurityStamp = dto.SecurityStampToken; - content.EmailConfirmedDate = dto.EmailConfirmedDate; + content.EmailConfirmedDate = dto.EmailConfirmedDate.HasValue + ? DateTime.SpecifyKind(dto.EmailConfirmedDate.Value, DateTimeKind.Local) + : null; content.PasswordConfiguration = dto.PasswordConfig; content.Key = nodeDto.UniqueId; content.VersionId = contentVersionDto.Id; @@ -140,14 +145,20 @@ internal class ContentBaseFactory content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId; content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId; - content.CreateDate = nodeDto.CreateDate; - content.UpdateDate = contentVersionDto.VersionDate; + content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local); + content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local); content.FailedPasswordAttempts = dto.FailedPasswordAttempts ?? default; content.IsLockedOut = dto.IsLockedOut; content.IsApproved = dto.IsApproved; - content.LastLoginDate = dto.LastLoginDate; - content.LastLockoutDate = dto.LastLockoutDate; - content.LastPasswordChangeDate = dto.LastPasswordChangeDate; + content.LastLockoutDate = dto.LastLockoutDate.HasValue + ? DateTime.SpecifyKind(dto.LastLockoutDate.Value, DateTimeKind.Local) + : null; + content.LastLoginDate = dto.LastLoginDate.HasValue + ? DateTime.SpecifyKind(dto.LastLoginDate.Value, DateTimeKind.Local) + : null; + content.LastPasswordChangeDate = dto.LastPasswordChangeDate.HasValue + ? DateTime.SpecifyKind(dto.LastPasswordChangeDate.Value, DateTimeKind.Local) + : null; // reset dirty initial properties (U4-1946) content.ResetDirtyProperties(false); @@ -186,7 +197,7 @@ internal class ContentBaseFactory new ContentScheduleDto { Action = x.Action.ToString(), - Date = x.Date, + Date = DateTime.SpecifyKind(x.Date, DateTimeKind.Local), NodeId = entity.Id, LanguageId = languageRepository.GetIdByIsoCode(x.Culture, false), Id = x.Id, @@ -261,7 +272,7 @@ internal class ContentBaseFactory UserId = entity.CreatorId, Text = entity.Name, NodeObjectType = objectType, - CreateDate = entity.CreateDate, + CreateDate = DateTime.SpecifyKind(entity.CreateDate, DateTimeKind.Local), }; return dto; @@ -275,7 +286,7 @@ internal class ContentBaseFactory { Id = entity.VersionId, NodeId = entity.Id, - VersionDate = entity.UpdateDate, + VersionDate = DateTime.SpecifyKind(entity.UpdateDate, DateTimeKind.Local), UserId = entity.WriterId, Current = true, // always building the current one Text = entity.Name, diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs index 9a8ae11386..60ec173ca5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs @@ -39,16 +39,25 @@ internal static class UserFactory user.Language = dto.UserLanguage; user.SecurityStamp = dto.SecurityStampToken; user.FailedPasswordAttempts = dto.FailedLoginAttempts ?? 0; - user.LastLockoutDate = dto.LastLockoutDate; - user.LastLoginDate = dto.LastLoginDate; - user.LastPasswordChangeDate = dto.LastPasswordChangeDate; - user.CreateDate = dto.CreateDate; - user.UpdateDate = dto.UpdateDate; user.Avatar = dto.Avatar; user.EmailConfirmedDate = dto.EmailConfirmedDate; user.InvitedDate = dto.InvitedDate; user.Kind = (UserKind)dto.Kind; + // Dates stored in the database are local server time, but for SQL Server, will be considered + // as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset. + user.LastLockoutDate = dto.LastLockoutDate.HasValue + ? DateTime.SpecifyKind(dto.LastLockoutDate.Value, DateTimeKind.Local) + : null; + user.LastLoginDate = dto.LastLoginDate.HasValue + ? DateTime.SpecifyKind(dto.LastLoginDate.Value, DateTimeKind.Local) + : null; + user.LastPasswordChangeDate = dto.LastPasswordChangeDate.HasValue + ? DateTime.SpecifyKind(dto.LastPasswordChangeDate.Value, DateTimeKind.Local) + : null; + user.CreateDate = DateTime.SpecifyKind(dto.CreateDate, DateTimeKind.Local); + user.UpdateDate = DateTime.SpecifyKind(dto.UpdateDate, DateTimeKind.Local); + // reset dirty initial properties (U4-1946) user.ResetDirtyProperties(false); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs index d7dc4f8161..e112e360d0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs @@ -29,7 +29,7 @@ internal class AuditRepository : EntityRepositoryBase, IAuditRe List? dtos = Database.Fetch(sql); - return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, x.Datestamp)).ToList(); + return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, DateTime.SpecifyKind(x.Datestamp, DateTimeKind.Local))).ToList(); } public void CleanLogs(int maximumAgeOfLogsInMinutes) @@ -104,12 +104,12 @@ internal class AuditRepository : EntityRepositoryBase, IAuditRe totalRecords = page.TotalItems; var items = page.Items.Select( - dto => new AuditItem(dto.NodeId, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, dto.Datestamp)).ToList(); + dto => new AuditItem(dto.NodeId, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, DateTime.SpecifyKind(dto.Datestamp, DateTimeKind.Local))).ToList(); // map the DateStamp for (var i = 0; i < items.Count; i++) { - items[i].CreateDate = page.Items[i].Datestamp; + items[i].CreateDate = DateTime.SpecifyKind(page.Items[i].Datestamp, DateTimeKind.Local); } return items; @@ -149,7 +149,7 @@ internal class AuditRepository : EntityRepositoryBase, IAuditRe LogDto? dto = Database.First(sql); return dto == null ? null - : new AuditItem(dto.NodeId, Enum.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, dto.Datestamp); + : new AuditItem(dto.NodeId, Enum.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, DateTime.SpecifyKind(dto.Datestamp, DateTimeKind.Local)); } protected override IEnumerable PerformGetAll(params int[]? ids) => throw new NotImplementedException(); @@ -162,7 +162,7 @@ internal class AuditRepository : EntityRepositoryBase, IAuditRe List? dtos = Database.Fetch(sql); - return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, x.Datestamp)).ToList(); + return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, DateTime.SpecifyKind(x.Datestamp, DateTimeKind.Local))).ToList(); } protected override Sql GetBaseQuery(bool isCount) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 72459bd755..1b4f2b1efd 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -400,15 +400,17 @@ public class DocumentRepository : ContentRepositoryBase 0 && contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation)) { foreach (ContentVariation v in contentVariation) { - content.SetPublishInfo(v.Culture, v.Name, v.Date); + content.SetPublishInfo(v.Culture, v.Name, DateTime.SpecifyKind(v.Date, DateTimeKind.Local)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs index e922ed3cdb..ef9dd67520 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs @@ -1,3 +1,4 @@ +using System.Data; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -98,6 +99,16 @@ internal class DocumentVersionRepository : IDocumentVersionRepository Page? page = _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, query); + // Dates stored in the database are local server time, but for SQL Server, will be considered + // as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset. + if (page is not null) + { + foreach (ContentVersionMeta item in page.Items) + { + item.SpecifyVersionDateKind(DateTimeKind.Local); + } + } + totalRecords = page?.TotalItems ?? 0; return page?.Items; diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs index 21758a1fcd..f78863e64f 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -115,12 +115,10 @@ internal sealed class DocumentCacheService : IDocumentCacheService // When unpublishing a node, a payload with RefreshBranch is published, so we don't have to worry about this. // Similarly, when a branch is published, next time the content is requested, the parent will be published, // this works because we don't cache null values. - if (preview is false && contentCacheNode is not null) + if (preview is false && contentCacheNode is not null && HasPublishedAncestorPath(contentCacheNode.Key) is false) { - if (HasPublishedAncestorPath(contentCacheNode.Key) is false) - { - return null; - } + // Careful not to early return here. We need to complete the scope even if returning null. + contentCacheNode = null; } scope.Complete(); diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index d4febe34b1..b3793c8d7f 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -88,7 +88,7 @@ "typescript": "^5.7.3", "typescript-eslint": "^8.24.1", "typescript-json-schema": "^0.65.1", - "vite": "^6.2.5", + "vite": "^6.2.6", "vite-plugin-static-copy": "^2.2.0", "vite-tsconfig-paths": "^5.1.4", "web-component-analyzer": "^2.0.0" @@ -16880,9 +16880,9 @@ } }, "node_modules/vite": { - "version": "6.2.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz", - "integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==", + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz", + "integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index ae56d11963..1918fc0145 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -275,7 +275,7 @@ "typescript": "^5.7.3", "typescript-eslint": "^8.24.1", "typescript-json-schema": "^0.65.1", - "vite": "^6.2.5", + "vite": "^6.2.6", "vite-plugin-static-copy": "^2.2.0", "vite-tsconfig-paths": "^5.1.4", "web-component-analyzer": "^2.0.0" diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index ddcd6c5051..5247685107 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -2057,7 +2057,7 @@ export default { duplicateUsername: "Username '%0%' is already taken", customValidation: 'Custom validation', entriesShort: 'Minimum %0% entries, requires %1% more.', - entriesExceed: 'Maximum %0% entries, %1% too many.', + entriesExceed: 'Maximum %0% entries, you have entered %1% too many.', entriesAreasMismatch: 'The content amount requirements are not met for one or more areas.', }, healthcheck: { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 97a51c381f..08ebf8cccd 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2170,7 +2170,7 @@ export default { invalidPattern: 'Value is invalid, it does not match the correct pattern', customValidation: 'Custom validation', entriesShort: 'Minimum %0% entries, requires %1% more.', - entriesExceed: 'Maximum %0% entries, %1% too many.', + entriesExceed: 'Maximum %0% entries, you have entered %1% too many.', entriesAreasMismatch: 'The content amount requirements are not met for one or more areas.', invalidMemberGroupName: 'Invalid member group name', invalidUserGroupName: 'Invalid user group name', diff --git a/src/Umbraco.Web.UI.Client/src/external/openid/base64-js/index.ts b/src/Umbraco.Web.UI.Client/src/external/openid/base64-js/index.ts index 8dcb906f50..1c1cb0f715 100644 --- a/src/Umbraco.Web.UI.Client/src/external/openid/base64-js/index.ts +++ b/src/Umbraco.Web.UI.Client/src/external/openid/base64-js/index.ts @@ -1,9 +1,9 @@ /* - * This Source Code has been derived from Jameson Little's base64-js. + * This Source Code has been derived from base64-js. * https://github.com/beatgammit/base64-js - * SPDX-License-Identifier: MIT - * Copyright © 2014 Jameson Little. - * Modifications are licensed under the MIT License. + * Copyright © 2014 Jameson Little + * Copyright © 2024 Umbraco A/S + * Licensed under the MIT License. */ const lookup: string[] = []; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/LICENSE b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/LICENSE deleted file mode 100644 index 66b4696927..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/LICENSE +++ /dev/null @@ -1,16 +0,0 @@ -Lucide License -ISC License - -Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022. - -Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - ---- - -Simple Icons -CC0 1.0 Universal license - -The person who associated a work with this deed has dedicated the work to the public domain by waiving all of his or her rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law. -You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission. diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/router/router-slot/LICENSE.md b/src/Umbraco.Web.UI.Client/src/packages/core/router/router-slot/LICENSE similarity index 87% rename from src/Umbraco.Web.UI.Client/src/packages/core/router/router-slot/LICENSE.md rename to src/Umbraco.Web.UI.Client/src/packages/core/router/router-slot/LICENSE index ba42248464..d13da40025 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/router/router-slot/LICENSE.md +++ b/src/Umbraco.Web.UI.Client/src/packages/core/router/router-slot/LICENSE @@ -1,9 +1,12 @@ +This Source Code has been derived from router-slot. + The MIT License (MIT) -Copyright © 2018 Andreas Mehlsen andmehlsen@gmail.com +Copyright © 2018 Andreas Mehlsen +Copyright © 2023 Umbraco A/S Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts index 3664bcafeb..d4c8fa2429 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts @@ -1,8 +1,10 @@ -/* This Source Code has been derived from Lee Kelleher's Contentment. +/* + * This Source Code has been derived from Contentment. * https://github.com/leekelleher/umbraco-contentment/blob/develop/src/Umbraco.Community.Contentment/DataEditors/Bytes/bytes.js - * SPDX-License-Identifier: MPL-2.0 - * Copyright © 2019 Lee Kelleher. - * Modifications are licensed under the MIT License. + * Copyright © 2016-2023 Lee Kelleher + * Copyright © 2024 Umbraco A/S + * Originally licensed under the Mozilla Public License, v. 2.0 + * Relicensed under the MIT License with permission from the copyright holder. */ export interface IFormatBytesOptions { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/utils.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/utils.ts index 8f65e11141..c566013167 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/utils.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/utils.ts @@ -461,6 +461,7 @@ export function getMimeTypeFromExtension(extension: string): string | null { '.onetoc2': 'application/onenote', '.opf': 'application/oebps-package+xml', '.oprc': 'application/vnd.palm', + '.opus': 'audio/ogg', '.org': 'application/vnd.lotus-organizer', '.osf': 'application/vnd.yamaha.openscoreformat', '.osfpvg': 'application/vnd.yamaha.openscoreformat.osfpvg+xml', @@ -744,6 +745,8 @@ export function getMimeTypeFromExtension(extension: string): string | null { '.wbxml': 'application/vnd.wap.wbxml', '.wcm': 'application/vnd.ms-works', '.wdb': 'application/vnd.ms-works', + '.weba': 'audio/webm', + '.webm': 'video/webm', '.webp': 'image/webp', '.wiz': 'application/msword', '.wks': 'application/vnd.ms-works', diff --git a/src/Umbraco.Web.UI.Login/package-lock.json b/src/Umbraco.Web.UI.Login/package-lock.json index 22143e31df..04c971df6d 100644 --- a/src/Umbraco.Web.UI.Login/package-lock.json +++ b/src/Umbraco.Web.UI.Login/package-lock.json @@ -11,7 +11,7 @@ "@umbraco-cms/backoffice": "15.3.0", "msw": "^2.7.0", "typescript": "^5.7.3", - "vite": "^6.2.5", + "vite": "^6.2.6", "vite-tsconfig-paths": "^5.1.4" }, "engines": { @@ -4292,9 +4292,9 @@ } }, "node_modules/vite": { - "version": "6.2.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz", - "integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==", + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz", + "integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/Umbraco.Web.UI.Login/package.json b/src/Umbraco.Web.UI.Login/package.json index 4add649e72..766f83b654 100644 --- a/src/Umbraco.Web.UI.Login/package.json +++ b/src/Umbraco.Web.UI.Login/package.json @@ -19,7 +19,7 @@ "@umbraco-cms/backoffice": "15.3.0", "msw": "^2.7.0", "typescript": "^5.7.3", - "vite": "^6.2.5", + "vite": "^6.2.6", "vite-tsconfig-paths": "^5.1.4" }, "msw": { diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/BlockGridWithPropertyEditor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/BlockGridWithPropertyEditor.spec.ts new file mode 100644 index 0000000000..84d82d419e --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/BlockGridWithPropertyEditor.spec.ts @@ -0,0 +1,117 @@ +import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; + +// Content Name +const contentName = 'ContentName'; + +// Document Type +const documentTypeName = 'DocumentTypeName'; +let documentTypeId = null; +const documentTypeGroupName = 'DocumentGroup'; + +// Block Grid +const blockGridName = 'BlockGridName'; +let blockGridId = null; + +// Element Type +const blockName = 'BlockName'; +let elementTypeId = null; +const elementGroupName = 'ElementGroup'; + +// Property Editor +const propertyEditorName = 'ProperyEditorInBlockName'; +let propertyEditorId = null; +const optionValues = ['testOption1', 'testOption2']; + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockName); + await umbracoApi.dataType.ensureNameNotExists(blockGridName); +}); + +test('can not publish a block grid with a mandatory radiobox without a value', async ({umbracoApi, umbracoUi}) => { + // Arrange + propertyEditorId = await umbracoApi.dataType.createRadioboxDataType(propertyEditorName, optionValues); + elementTypeId = await umbracoApi.documentType.createDefaultElementType(blockName, elementGroupName, propertyEditorName, propertyEditorId, true); + blockGridId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridName, elementTypeId, true); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridName, blockGridId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockElementButton(); + await umbracoUi.content.clickBlockElementWithName(blockName); + // Do not select any radiobox values and the validation error appears + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue); + // Select a radiobox value and the validation error disappears + await umbracoUi.content.chooseRadioboxOption(optionValues[0]); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); +}); + +test('can not publish a block grid with a mandatory checkbox list without a value', async ({umbracoApi, umbracoUi}) => { + // Arrange + propertyEditorId = await umbracoApi.dataType.createCheckboxListDataType(propertyEditorName, optionValues); + elementTypeId = await umbracoApi.documentType.createDefaultElementType(blockName, elementGroupName, propertyEditorName, propertyEditorId, true); + blockGridId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridName, elementTypeId, true); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridName, blockGridId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockElementButton(); + await umbracoUi.content.clickBlockElementWithName(blockName); + // Do not select any checkbox list values and the validation error appears + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue); + // Select a checkbox list value and the validation error disappears + await umbracoUi.content.chooseCheckboxListOption(optionValues[0]); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); +}); + +test('can not publish a block grid with a mandatory dropdown without a value', async ({umbracoApi, umbracoUi}) => { + // Arrange + propertyEditorId = await umbracoApi.dataType.createDropdownDataType(propertyEditorName, false, optionValues); + elementTypeId = await umbracoApi.documentType.createDefaultElementType(blockName, elementGroupName, propertyEditorName, propertyEditorId, true); + blockGridId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridName, elementTypeId, true); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridName, blockGridId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockElementButton(); + await umbracoUi.content.clickBlockElementWithName(blockName); + // Do not select any dropdown values and the validation error appears + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue); + // Select a dropdown value and the validation error disappears + await umbracoUi.content.chooseDropdownOption([optionValues[0]]); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/VariantBlockGrid.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/VariantBlockGrid.spec.ts index 33a2afc94d..0ad59c90ad 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/VariantBlockGrid.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/VariantBlockGrid.spec.ts @@ -32,9 +32,10 @@ test.beforeEach(async ({umbracoApi}) => { test.afterEach(async ({umbracoApi}) => { await umbracoApi.language.ensureIsoCodeNotExists('da'); - await umbracoApi.documentType.ensureNameNotExists(documentTypeName); await umbracoApi.documentType.ensureNameNotExists(blockName); await umbracoApi.dataType.ensureNameNotExists(blockGridName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); }); test('invariant document type with invariant block grid with invariant block with an invariant textString', async ({umbracoApi, umbracoUi}) => { @@ -63,34 +64,25 @@ test('invariant document type with invariant block grid with invariant block wit await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText); }); -test('invariant document type with invariant block grid with variant block with an invariant textString', async ({umbracoApi, umbracoUi}) => { +test('can not create unsupported invariant document type with invariant block grid with variant block with an invariant textString', async ({umbracoApi, umbracoUi}) => { // Arrange elementTypeId = await umbracoApi.documentType.createDefaultElementTypeWithVaryByCulture(blockName, elementGroupName, textStringName, textStringDataTypeId, true, false); blockGridId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridName, elementTypeId, true); documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridName, blockGridId, documentTypeGroupName); await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - await umbracoUi.content.goToContentWithName(contentName); // Act - await umbracoUi.content.clickAddBlockElementButton(); - await umbracoUi.content.clickBlockElementWithName(blockName); - await umbracoUi.content.enterTextstring(textStringText); - await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.unsupportInvariantContentItemWithVariantBlocks); await umbracoUi.content.clickSaveAndPublishButton(); // Assert await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); - await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); - - await umbracoUi.reloadPage(); - await umbracoUi.content.goToBlockGridBlockWithName(documentTypeGroupName, blockGridName, blockName); - await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText); + await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished); }); -// Remove fixme when this test works. Currently, the textstring value is not saved when saving / publishing the document -test.fixme('invariant document type with invariant block grid with variant block with an variant textString', async ({umbracoApi, umbracoUi}) => { +test('can not create unsupported invariant document type with invariant block grid with variant block with an variant textString', async ({umbracoApi, umbracoUi}) => { // Arrange elementTypeId = await umbracoApi.documentType.createDefaultElementTypeWithVaryByCulture(blockName, elementGroupName, textStringName, textStringDataTypeId, true, true); blockGridId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridName, elementTypeId, true); @@ -98,22 +90,15 @@ test.fixme('invariant document type with invariant block grid with variant block await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); - await umbracoUi.content.goToContentWithName(contentName); // Act - await umbracoUi.content.clickAddBlockElementButton(); - await umbracoUi.content.clickBlockElementWithName(blockName) - await umbracoUi.content.enterTextstring(textStringText); - await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.unsupportInvariantContentItemWithVariantBlocks); await umbracoUi.content.clickSaveAndPublishButton(); // Assert await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); - await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); - - await umbracoUi.reloadPage(); - await umbracoUi.content.goToBlockGridBlockWithName(documentTypeGroupName, blockGridName, blockName); - await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText); + await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished); }); test('variant document type with variant block grid with variant block with an variant textString', async ({umbracoApi, umbracoUi}) => { @@ -195,4 +180,4 @@ test('variant document type with invariant block grid with variant block with an await umbracoUi.reloadPage(); await umbracoUi.content.goToBlockGridBlockWithName(documentTypeGroupName, blockGridName, blockName); await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText); -}); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/BlockListWithPropertyEditor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/BlockListWithPropertyEditor.spec.ts new file mode 100644 index 0000000000..5821c00c2d --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/BlockListWithPropertyEditor.spec.ts @@ -0,0 +1,117 @@ +import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; + +// Content Name +const contentName = 'ContentName'; + +// Document Type +const documentTypeName = 'DocumentTypeName'; +let documentTypeId = null; +const documentTypeGroupName = 'DocumentGroup'; + +// Block List +const blockListName = 'BlockListName'; +let blockListId = null; + +// Element Type +const blockName = 'BlockName'; +let elementTypeId = null; +const elementGroupName = 'ElementGroup'; + +// Property Editor +const propertyEditorName = 'ProperyEditorInBlockName'; +let propertyEditorId = null; +const optionValues = ['testOption1', 'testOption2']; + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockName); + await umbracoApi.dataType.ensureNameNotExists(blockListName); +}); + +test('can not publish a block list with a mandatory radiobox without a value', async ({umbracoApi, umbracoUi}) => { + // Arrange + propertyEditorId = await umbracoApi.dataType.createRadioboxDataType(propertyEditorName, optionValues); + elementTypeId = await umbracoApi.documentType.createDefaultElementType(blockName, elementGroupName, propertyEditorName, propertyEditorId, true); + blockListId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListName, elementTypeId); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockListName, blockListId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockElementButton(); + await umbracoUi.content.clickBlockElementWithName(blockName); + // Do not select any radiobox values and the validation error appears + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue); + // Select a radiobox value and the validation error disappears + await umbracoUi.content.chooseRadioboxOption(optionValues[0]); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); +}); + +test('can not publish a block list with a mandatory checkbox list without a value', async ({umbracoApi, umbracoUi}) => { + // Arrange + propertyEditorId = await umbracoApi.dataType.createCheckboxListDataType(propertyEditorName, optionValues); + elementTypeId = await umbracoApi.documentType.createDefaultElementType(blockName, elementGroupName, propertyEditorName, propertyEditorId, true); + blockListId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListName, elementTypeId); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockListName, blockListId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockElementButton(); + await umbracoUi.content.clickBlockElementWithName(blockName); + // Do not select any checkbox list values and the validation error appears + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue); + // Select a checkbox list value and the validation error disappears + await umbracoUi.content.chooseCheckboxListOption(optionValues[0]); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); +}); + +test('can not publish a block list with a mandatory dropdown without a value', async ({umbracoApi, umbracoUi}) => { + // Arrange + propertyEditorId = await umbracoApi.dataType.createDropdownDataType(propertyEditorName, false, optionValues); + elementTypeId = await umbracoApi.documentType.createDefaultElementType(blockName, elementGroupName, propertyEditorName, propertyEditorId, true); + blockListId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListName, elementTypeId); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockListName, blockListId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockElementButton(); + await umbracoUi.content.clickBlockElementWithName(blockName); + // Do not select any dropdown values and the validation error appears + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue); + // Select a dropdown value and the validation error disappears + await umbracoUi.content.chooseDropdownOption([optionValues[0]]); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/VariantBlockList.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/VariantBlockList.spec.ts index 1ad6625c86..222deef581 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/VariantBlockList.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/VariantBlockList.spec.ts @@ -32,9 +32,10 @@ test.beforeEach(async ({umbracoApi}) => { test.afterEach(async ({umbracoApi}) => { await umbracoApi.language.ensureIsoCodeNotExists('da'); - await umbracoApi.documentType.ensureNameNotExists(documentTypeName); await umbracoApi.documentType.ensureNameNotExists(blockName); await umbracoApi.dataType.ensureNameNotExists(blockListName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); }); test('invariant document type with invariant block list with invariant block with an invariant textString', async ({umbracoApi, umbracoUi}) => { @@ -63,57 +64,40 @@ test('invariant document type with invariant block list with invariant block wit await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText); }); -test('invariant document type with invariant block list with variant block with an invariant textString', async ({umbracoApi, umbracoUi}) => { +test('can not create unsupported invariant document type with invariant block list with variant block with an invariant textString', async ({umbracoApi, umbracoUi}) => { // Arrange elementTypeId = await umbracoApi.documentType.createDefaultElementTypeWithVaryByCulture(blockName, elementGroupName, textStringName, textStringDataTypeId, true, false); blockListId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListName, elementTypeId); documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockListName, blockListId, documentTypeGroupName); await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - await umbracoUi.content.goToContentWithName(contentName); // Act - await umbracoUi.content.clickAddBlockElementButton(); - await umbracoUi.content.clickBlockElementWithName(blockName); - await umbracoUi.content.enterTextstring(textStringText); - await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.unsupportInvariantContentItemWithVariantBlocks); await umbracoUi.content.clickSaveAndPublishButton(); // Assert await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); - await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); - - await umbracoUi.reloadPage(); - await umbracoUi.content.goToBlockListBlockWithName(documentTypeGroupName, blockListName, blockName); - await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText); + await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished); }); -// Remove fixme when this test works. Currently the textstring value is is not saved when saving / publishing the document -test.fixme('invariant document type with invariant block list with variant block with an variant textString', async ({umbracoApi, umbracoUi}) => { +test('can not create unsupported invariant document type with invariant block list with variant block with an variant textString', async ({umbracoApi, umbracoUi}) => { // Arrange elementTypeId = await umbracoApi.documentType.createDefaultElementTypeWithVaryByCulture(blockName, elementGroupName, textStringName, textStringDataTypeId, true, true); blockListId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListName, elementTypeId); documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockListName, blockListId, documentTypeGroupName); await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - await umbracoUi.content.goToContentWithName(contentName); // Act - await umbracoUi.content.clickAddBlockElementButton(); - await umbracoUi.content.clickBlockElementWithName(blockName) - await umbracoUi.content.enterTextstring(textStringText); - await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.unsupportInvariantContentItemWithVariantBlocks); await umbracoUi.content.clickSaveAndPublishButton(); // Assert await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); - await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); - - await umbracoUi.reloadPage(); - await umbracoUi.content.goToBlockListBlockWithName(documentTypeGroupName, blockListName, blockName); - await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText); + await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished); }); test('variant document type with variant block list with variant block with an variant textString', async ({umbracoApi, umbracoUi}) => { 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 1cfe2a419f..b4760a8c77 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCheckboxList.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCheckboxList.spec.ts @@ -1,19 +1,22 @@ -import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {ConstantHelper, test, AliasHelper, NotificationConstantHelper} from '@umbraco/playwright-testhelpers'; import {expect} from "@playwright/test"; const contentName = 'TestContent'; const documentTypeName = 'TestDocumentTypeForContent'; const dataTypeName = 'Checkbox list'; +const customDataTypeName = 'CustomCheckboxList'; test.beforeEach(async ({umbracoApi, umbracoUi}) => { await umbracoApi.documentType.ensureNameNotExists(documentTypeName); await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); await umbracoUi.goToBackOffice(); }); test.afterEach(async ({umbracoApi}) => { await umbracoApi.document.ensureNameNotExists(contentName); await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); }); test('can create content with the checkbox list data type', async ({umbracoApi, umbracoUi}) => { @@ -31,7 +34,7 @@ test('can create content with the checkbox list data type', async ({umbracoApi, await umbracoUi.content.clickSaveButton(); // Assert - await umbracoUi.content.isSuccessNotificationVisible(); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.created); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.variants[0].state).toBe(expectedState); @@ -51,8 +54,8 @@ test('can publish content with the checkbox list data type', async ({umbracoApi, await umbracoUi.content.clickSaveAndPublishButton(); // Assert - await umbracoUi.content.doesSuccessNotificationsHaveCount(2); - expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.variants[0].state).toBe(expectedState); expect(contentData.values).toEqual([]); @@ -60,7 +63,6 @@ test('can publish content with the checkbox list data type', async ({umbracoApi, 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); const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); @@ -73,13 +75,38 @@ test('can create content with the custom checkbox list data type', async ({umbra await umbracoUi.content.clickSaveAndPublishButton(); // Assert - await umbracoUi.content.doesSuccessNotificationsHaveCount(2); - expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); 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); }); +test('can not publish a mandatory checkbox list with an empty value', async ({umbracoApi, umbracoUi}) => { + // Arrange + const optionValues = ['testOption1', 'testOption2']; + const customDataTypeId = await umbracoApi.dataType.createCheckboxListDataType(customDataTypeName, optionValues); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId, 'Test Group', false, false, true); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + // Do not select any checkbox list values and the validation error appears + await umbracoUi.content.clickSaveAndPublishButton(); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished); + // Select a checkbox list value and the validation error disappears + await umbracoUi.content.chooseCheckboxListOption(optionValues[0]); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName)); + expect(contentData.values[0].value).toEqual([optionValues[0]]); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDropdown.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDropdown.spec.ts index 7a4c246b01..98743009d4 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDropdown.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDropdown.spec.ts @@ -1,90 +1,116 @@ -import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {ConstantHelper, test, AliasHelper, NotificationConstantHelper} from '@umbraco/playwright-testhelpers'; import {expect} from "@playwright/test"; const contentName = 'TestContent'; const documentTypeName = 'TestDocumentTypeForContent'; - const dataTypeNames = ['Dropdown', 'Dropdown multiple']; +const customDataTypeName = 'CustomDropdown'; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + 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); - }); + 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.doesSuccessNotificationHaveText(NotificationConstantHelper.success.created); + 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.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + 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 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.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName)); + expect(contentData.values[0].value).toEqual(selectedOptions); }); } + +test('can not publish a mandatory dropdown with an empty value', async ({umbracoApi, umbracoUi}) => { + // Arrange + const optionValues = ['testOption1', 'testOption2', 'testOption3']; + const customDataTypeId = await umbracoApi.dataType.createDropdownDataType(customDataTypeName, false, optionValues); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId, 'Test Group', false, false, true); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + // Do not select any dropdown values and the validation error appears + await umbracoUi.content.clickSaveAndPublishButton(); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished); + // Select a dropdown value and the validation error disappears + await umbracoUi.content.chooseDropdownOption([optionValues[0]]); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName)); + expect(contentData.values[0].value).toEqual([optionValues[0]]); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts index e060a48257..939b94c2f0 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts @@ -1,4 +1,4 @@ -import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {ConstantHelper, test, AliasHelper, NotificationConstantHelper} from '@umbraco/playwright-testhelpers'; import {expect} from "@playwright/test"; const dataTypeName = 'Media Picker'; @@ -8,7 +8,7 @@ const mediaFileName = 'TestMediaFileForContent'; const mediaTypeName = 'File'; let mediaFileId = ''; -test.beforeEach(async ({umbracoApi, umbracoUi}) => { +test.beforeEach(async ({umbracoApi}) => { await umbracoApi.documentType.ensureNameNotExists(documentTypeName); await umbracoApi.document.ensureNameNotExists(contentName); await umbracoApi.media.ensureNameNotExists(mediaFileName); @@ -39,7 +39,7 @@ test('can create content with the media picker data type', {tag: '@smoke'}, asyn await umbracoUi.content.clickSaveButton(); // Assert - await umbracoUi.content.isSuccessNotificationVisible(); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.created); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.variants[0].state).toBe(expectedState); @@ -68,7 +68,8 @@ test('can publish content with the media picker data type', async ({umbracoApi, await umbracoUi.content.clickSaveAndPublishButton(); // Assert - await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.variants[0].state).toBe(expectedState); @@ -93,7 +94,7 @@ test('can remove a media picker in the content', async ({umbracoApi, umbracoUi}) await umbracoUi.content.clickSaveButton(); // Assert - await umbracoUi.content.isSuccessNotificationVisible(); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.values).toEqual([]); @@ -128,3 +129,34 @@ test('can limit the media picker in the content by setting the start node', asyn await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); }); +test('can not publish a mandatory media picker with an empty value', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id, 'Test Group', false, false, true); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + // Do not pick any media and the validation error appears + await umbracoUi.content.clickSaveAndPublishButton(); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished); + // Pick a media value and the validation error disappears + await umbracoUi.content.clickChooseButtonAndSelectMediaWithName(mediaFileName); + await umbracoUi.content.clickChooseModalButton(); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + const contentData = await umbracoApi.document.getByName(contentName); + 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([]); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithRadiobox.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithRadiobox.spec.ts index 195483125b..19dba0f78f 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithRadiobox.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithRadiobox.spec.ts @@ -1,18 +1,22 @@ -import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {ConstantHelper, test, AliasHelper, NotificationConstantHelper} from '@umbraco/playwright-testhelpers'; import {expect} from "@playwright/test"; const contentName = 'TestContent'; const documentTypeName = 'TestDocumentTypeForContent'; const dataTypeName = 'Radiobox'; +const customDataTypeName = 'CustomRadiobox'; +const optionValues = ['testOption1', 'testOption2']; test.beforeEach(async ({umbracoApi}) => { await umbracoApi.documentType.ensureNameNotExists(documentTypeName); await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); }); test.afterEach(async ({umbracoApi}) => { await umbracoApi.document.ensureNameNotExists(contentName); await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); }); test('can create content with the radiobox data type', async ({umbracoApi, umbracoUi}) => { @@ -31,7 +35,7 @@ test('can create content with the radiobox data type', async ({umbracoApi, umbra await umbracoUi.content.clickSaveButton(); // Assert - await umbracoUi.content.isSuccessNotificationVisible(); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.created); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.variants[0].state).toBe(expectedState); @@ -52,7 +56,8 @@ test('can publish content with the radiobox data type', async ({umbracoApi, umbr await umbracoUi.content.clickSaveAndPublishButton(); // Assert - await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.variants[0].state).toBe(expectedState); @@ -61,8 +66,6 @@ test('can publish content with the radiobox data type', async ({umbracoApi, umbr 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); @@ -75,13 +78,37 @@ test('can create content with the custom radiobox data type', async ({umbracoApi await umbracoUi.content.clickSaveButton(); // Assert - await umbracoUi.content.isSuccessNotificationVisible(); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName)); expect(contentData.values[0].value).toEqual(optionValues[0]); - - // Clean - await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); }); +test('can not publish mandatory radiobox with an empty value', async ({umbracoApi, umbracoUi}) => { + // Arrange + const customDataTypeId = await umbracoApi.dataType.createRadioboxDataType(customDataTypeName, optionValues); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId, 'Test Group', false, false, true); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + // Do not select any radiobox values and the validation error appears + await umbracoUi.content.clickSaveAndPublishButton(); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished); + // Select a radiobox value and the validation error disappears + await umbracoUi.content.chooseRadioboxOption(optionValues[0]); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName)); + expect(contentData.values[0].value).toEqual(optionValues[0]); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTags.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTags.spec.ts index 33856a5b35..551830489f 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTags.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTags.spec.ts @@ -81,5 +81,4 @@ test('can remove a tag in the content', async ({umbracoApi, umbracoUi}) => { 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/RichTextEditor/VariantTipTapBlocks.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/VariantTipTapBlocks.spec.ts index 21f8a99295..cf9a8a3088 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/VariantTipTapBlocks.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/VariantTipTapBlocks.spec.ts @@ -32,9 +32,10 @@ test.beforeEach(async ({umbracoApi}) => { test.afterEach(async ({umbracoApi}) => { await umbracoApi.language.ensureIsoCodeNotExists('da'); - await umbracoApi.documentType.ensureNameNotExists(documentTypeName); await umbracoApi.documentType.ensureNameNotExists(blockName); await umbracoApi.dataType.ensureNameNotExists(tipTapName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); }); test('invariant document type with invariant tiptap RTE with invariant block with an invariant textString', async ({umbracoApi, umbracoUi}) => { @@ -64,57 +65,41 @@ test('invariant document type with invariant tiptap RTE with invariant block wit await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText); }); -test('invariant document type with invariant tiptap RTE with variant block with an invariant textString', async ({umbracoApi, umbracoUi}) => { +test('can not create unsupported invariant document type with invariant tiptap RTE with variant block with an invariant textString', async ({umbracoApi, umbracoUi}) => { // Arrange elementTypeId = await umbracoApi.documentType.createDefaultElementTypeWithVaryByCulture(blockName, elementGroupName, textStringName, textStringDataTypeId, true, false); tipTapId = await umbracoApi.dataType.createTipTapDataTypeWithABlock(tipTapName, elementTypeId); documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, tipTapName, tipTapId, documentTypeGroupName); await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - await umbracoUi.content.goToContentWithName(contentName); // Act - await umbracoUi.content.clickInsertBlockButton(); - await umbracoUi.content.clickBlockElementWithName(blockName); - await umbracoUi.content.enterTextstring(textStringText); - await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.unsupportInvariantContentItemWithVariantBlocks); await umbracoUi.content.clickSaveAndPublishButton(); // Assert await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); - await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); - - await umbracoUi.reloadPage(); - await umbracoUi.content.clickBlockElementWithName(blockName); - await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText); + await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished); }); -// Remove fixme when this test works. Currently the textstring value is is not saved when saving / publishing the document -test.fixme('invariant document type with invariant tiptap RTE with variant block with an variant textString', async ({umbracoApi, umbracoUi}) => { +test('can not create unsupported invariant document type with invariant tiptap RTE with variant block with an variant textString', async ({umbracoApi, umbracoUi}) => { // Arrange elementTypeId = await umbracoApi.documentType.createDefaultElementTypeWithVaryByCulture(blockName, elementGroupName, textStringName, textStringDataTypeId, true, true); tipTapId = await umbracoApi.dataType.createTipTapDataTypeWithABlock(tipTapName, elementTypeId); documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, tipTapName, tipTapId, documentTypeGroupName); await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); await umbracoUi.content.goToContentWithName(contentName); // Act - await umbracoUi.content.clickInsertBlockButton(); - await umbracoUi.content.clickBlockElementWithName(blockName); - await umbracoUi.content.enterTextstring(textStringText); - await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.unsupportInvariantContentItemWithVariantBlocks); await umbracoUi.content.clickSaveAndPublishButton(); // Assert await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); - await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); - - await umbracoUi.reloadPage(); - await umbracoUi.content.clickBlockElementWithName(blockName); - await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText); + await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished); }); test('variant document type with variant tiptap RTE with variant block with an variant textString', async ({umbracoApi, umbracoUi}) => { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Serialization/BackOfficeSerializationTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Serialization/BackOfficeSerializationTests.cs new file mode 100644 index 0000000000..aa82a37c6d --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Serialization/BackOfficeSerializationTests.cs @@ -0,0 +1,80 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using NUnit.Framework; +using Umbraco.Cms.Api.Common.Serialization; +using Umbraco.Cms.Api.Management.Serialization; +using Umbraco.Cms.Tests.UnitTests.TestHelpers; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Management.Serialization; + +[TestFixture] +public class BackOfficeSerializationTests +{ + private JsonOptions jsonOptions; + + [SetUp] + public void SetupOptions() + { + var typeInfoResolver = new UmbracoJsonTypeInfoResolver(TestHelper.GetTypeFinder()); + var configurationOptions = new ConfigureUmbracoBackofficeJsonOptions(typeInfoResolver); + var options = new JsonOptions(); + configurationOptions.Configure(global::Umbraco.Cms.Core.Constants.JsonOptionsNames.BackOffice, options); + jsonOptions = options; + } + + [Test] + public void Will_Serialize_To_Camel_Case() + { + var objectToSerialize = new UnNestedJsonTestValue(); + + var json = JsonSerializer.Serialize(objectToSerialize, jsonOptions.JsonSerializerOptions); + + Assert.AreEqual("{\"stringValue\":\"theValue\"}", json); + } + + // the limit is 64, but it seems like the functional limit is that minus 1 + [TestCase(1, true, TestName = "Can_Serialize_At_Min_Depth(1)")] + [TestCase(48, true, TestName = "Can_Serialize_At_High_Depth(33)")] + [TestCase(63, true, TestName = "Can_Serialize_To_Max_Depth(63)")] + [TestCase(64, false, TestName = "Can_NOT_Serialize_Beyond_Max_Depth(64)")] + public void Can_Serialize_To_Max_Depth(int depth, bool shouldPass) + { + var objectToSerialize = CreateNestedObject(depth); + + if (shouldPass) + { + var json = JsonSerializer.Serialize(objectToSerialize, jsonOptions.JsonSerializerOptions); + Assert.IsNotEmpty(json); + } + else + { + Assert.Throws(() => JsonSerializer.Serialize(objectToSerialize, jsonOptions.JsonSerializerOptions)); + } + } + + private static NestedJsonTestValue CreateNestedObject(int levels) + { + var root = new NestedJsonTestValue { Level = 1 }; + var outer = root; + for (var i = 2; i <= levels; i++) + { + var inner = new NestedJsonTestValue { Level = i }; + outer.Inner = inner; + outer = inner; + } + + return root; + } + + public class UnNestedJsonTestValue + { + public string StringValue { get; set; } = "theValue"; + } + + public class NestedJsonTestValue + { + public int Level { get; set; } + + public NestedJsonTestValue? Inner { get; set; } + } +}