Compare commits

..
Author SHA1 Message Date
Nhu DinhandGitHub 59f9bf290d Build: V15 Disable schedule nightly E2E trigger (#22203)
Disable schedule nightly E2E trigger
2026-03-20 17:00:08 +07:00
b6b8f54c65 V15 QA Added the authorization integration tests (#18419)
* Added authorization integration tests

* Removed unnecessary tests and update tests for preview controller

* Updated to use the newest changes from v15/dev and added an override for the AuthenticateClientAsync to use the userGroupKey

* Updated CompatibilitySuppressions to include changes from integration tests

* Updated pipelines

* Skips managementApi tests

* Only run necessary tests

* Added new schema per fixture to reduce test setup time

* Fixed failing tests

* Updated test setup

* Updated test

* Added suppression

* Fixed failing tests

* Updated addOnTeardown methods to protected

* Added method for clearing the host

* Added teardown

* Updated model usage

* Added a lot of cleanup for memory leak issues when running tests

* Added CompatibilitySuppressions.xml

* Updated tests

* Cleaned up

* Adjusted base classes

* Updated pipeline

* Updated CompatibilitySuppressions.xml

* Updated test logging

* Fixed reponse

* Updated condition to skip tests

* Updated tests, not done

* Reworked test to expect correct responses with correct setup

* Updated tests

* More updates to tests

* Updated tests

* Cleaned up tests

* Updated setup

* Cleaned up tests to match setup

* Cleaned up setup

* Removed suppression

* Fixed tests

* Move order of checks

* Fix naming

* Formatting

* Dispose of host

* Keep track of if we're disposed

* Compat suppression

* Dont dispose

* Fix failing tests

* removed unused virtual

* Updated CompatibilitySuppressions.xml

---------

Co-authored-by: Andreas Zerbst <andr317c@live.dk>
Co-authored-by: Zeegaan <skrivdetud@gmail.com>
Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
2025-10-06 20:49:09 +02:00
Andreas ZerbstandGitHub 50139ee506 E2E Pipeline: QA Remove continue on error condition (#20356)
* Removed continue on error

* Planted flaky test for testing on pipeline

* Fixed test
2025-10-03 09:39:05 +07:00
Andreas ZerbstandGitHub 62947b2862 V15 QA Added external login provider tests and split pipeline into templates (#20049)
* Added setup for external login

* Started on yaml

* Added test file

* Updated pipeline

* Use env vars

* Added env variables and commented out test we don't need to run

* Removed list from matrix

* Updated condition

* Updated package path

* Updated testFolder

* double slash

* Updated condition

* Updated condition again

* Added port

* Removed redundant values

* Set as env vars

* Added env vars beneath matrix

* Get env

* Updated naming

* Updated usage of values

* Added a check for client id, to see if value set

* Moved env out of pool

* Tried moving env

* Trying to fix the env being empty

* Removed env

* Updated name of variable

* Fixed A cyclical reference

* Updated typo

* More logging

* Reverted change

* Added env

* Added env to tests

* Cleaned up

* Added yaml template files

* Updated nighly pipepline to use templates

* Updated sln

* Split yaml into templates for e2e setup

* Updated pipeline

* Updated solution file

* Set value

* Added if statement

* Added variables

* Set default values

* Updated values

* Updated condition

* Run multiple tests

* Added env

* Updated from parameter to variable

* Fixed condition

* Fixed condition to use actual value

* Updated npx wait on command

* Updated pwsh

* Updated port again

* Updated port value

* Updated wait on

* Updated condition

* Restructured

* Updated var

* Updated run application steps

* Added echo

* Updated to boolean

* Updated conditions

* Updated test template usage

* Added databaseType

* Added another databaseType

* Split up templates

* Fixed indentation

* Updated condition

* updated path

* removed build from path

* Updated conditions for azureAd

* Fixed indentation

* Updated to single qoutes

* Cleaned up

* Removed unused file

* Clarified namin

* Moved

* Updated pipeline, not done

* Updated locator

* Updated pipelines

* Updated test helpers package

* Skipped build stage for default app settings tests

* Updated password var

* Updated locators

* Updated defaultconfig build setup

* Split E2E stage in two

* Added parameter for skipping integration tests

* Cleaned up

* Added ASPNETCORE_URLS
2025-09-05 08:42:58 +02:00
Andreas ZerbstandGitHub 1748a08f7b V15 QA updated failing acceptance tests on the nightly pipeline (#19882)
* Updated tests

* Bumped test helpers
2025-08-11 09:05:17 +02:00
Andreas ZerbstandGitHub 3264d58f13 V15 QA acceptance tests with appsettings (#19550)
* Start of appsetting

* Updated setup of playwright

* Adjusted the pipeline

* Updated appsetting

* Added install test

* Added comments

* Updated pipeline

* Updated development app settings

* Commented tests out

* comment

* Added if statement

* Updated pipeline

* Fixed condition

* Changed to production

* Added a log

* Updated copy item

* Added

* Updated app settings

* Updated pipeline

* Moved playwright login

* Updated pipeline

* Updated app setting

* Updated nightly

* Updated appsettings

* Updated get

* Updated wait on

* Updated appsettings

* Updated connection string

* Updates

* Skips code

* Updated variable

* Updated pipeline

* We want to always retain the trace, to see if the test runs as expected on the pipeline

* Added a temporary wait till port is open

* Fixed condition

* Added missing tcp for wait on

* Updated URL env

* Updated setup

* Fixed string

* Updated locator

* Split tests into SQLite and SQLServer

* Updated pipeline to run all tests

* Retain trace on failure

* Added testFolder var

* Added appsettings and program for delivery api tests

* Updated playwright config

* Split test runners into defaultconfig and different app settings

* Added delivery api tests

* Cleaned up tests

* Bumped version

* Updated pipeline

* Small fixes

* Added password

* Updated connection string

* Fixed

* Removed quotes

* Removed unnecessary connection string

* Added missing password

* Cleaned up

* Cleaned up

* Cleaned up

* Updated to use helpers

* Bumped version

* Updated helper usage

* Added password to variables and a condition

* Added check

* Indented value

* Fixed condition

* More updates

* Updated variable
2025-08-05 11:22:03 +02:00
Andy Butland b6be468b3a Merge branch 'v15/dev' of https://github.com/umbraco/Umbraco-CMS into v15/dev 2025-07-29 07:18:37 +02:00
Andy Butland 3ec88aab65 Merge branch 'release/15.4.4' into v15/dev
# Conflicts:
#	src/Umbraco.Web.UI.Client/package.json
#	version.json
2025-07-29 07:18:24 +02:00
da43086017 Merge commit from fork
Co-authored-by: kjac <kja@umbraco.dk>
2025-07-29 05:10:52 +02:00
d62fe5e315 Fix issue with use of EF Core scopes within notification handlers (take 2 - handling scopes with a base parent) (#19797)
* Add integration tests that shows the problem

* Fix the problem and add explenation

* Improved comments slightly to help when we come back here!
Moved tests alongside existing ones related to scopes.
Removed long running attribute from tests (they are quite fast).

* Fixed casing in comment.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-07-25 13:10:44 +02:00
Andy Butland 852192a5d2 Bumped package.json version to 15.4.4. 2025-07-24 09:27:21 +02:00
Andy Butland f4498c3d05 Bumped version to 15.4.4. 2025-07-21 11:56:22 +02:00
Nhu DinhandGitHub b045b33049 V15 QA Removed unused API acceptance tests (#19680)
Removed unused API acceptance tests
2025-07-09 10:11:37 +07:00
LiliandGitHub 32a4fbebd5 V15 Bugfix: Horizontal scrollbar in Media section #19479 (#19541)
fix to stop the scroll bar appearing in the media section.
2025-06-30 14:52:35 +02:00
Andy Butland cc2f1df78e Merge branch 'release/15.4.3' into v15/dev
# Conflicts:
#	version.json
2025-06-10 11:43:50 +02:00
Andy ButlandandCopilot 9b2fd1253b Optimize initialization of document URLs on start-up (#19498)
* Optimize initialization of document URLs on startup.

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-10 10:31:26 +02:00
Andy Butland 024697f3bf Bumped version to 15.4.3 2025-06-10 10:28:55 +02:00
Andy Butland d8b76575ca Merge branch 'v15/dev' of https://github.com/umbraco/Umbraco-CMS into v15/dev 2025-06-03 07:04:56 +02:00
Andy Butland 5b00312158 Merge branch 'release/15.4.2' into v15/dev
# Conflicts:
#	version.json
2025-06-03 07:04:43 +02:00
Andy ButlandandGitHub d920e93d1e Merge commit from fork 2025-06-03 05:21:11 +02:00
MoleandGitHub feba51a49d Use correct service name in obsoletion method (#19398)
Use correct service in obsoletion method
2025-05-23 09:46:16 +00:00
Andy Butland 853c1acf7e Bumped version to 15.4.2 2025-05-20 13:10:02 +01:00
3f10bd8c21 Added logging and try/catch around retrieval of references, so we don't block critical operations following an incompatible data type change (#18576)
* Added logging and try/catch around retrieval of references, so we don't block critical operations following an incompatible data type change.

* Added a little more detail to the log message.

* Added a little more detail to the log message.

* Fix unittest mock dependency

---------

Co-authored-by: Migaroez <geusens@gmail.com>
2025-05-19 10:54:22 +02:00
Andreas ZerbstandGitHub b13eb8aaf8 V15 Added second level block acceptance tests (#19264) 2025-05-12 09:23:55 +00:00
Andreas ZerbstandGitHub 275478066e V15 QA complex block grid test (#18347) 2025-05-08 11:12:37 +02:00
Andreas ZerbstandGitHub b01def0872 V15 QA added a fix for flaky integration tests run on SQL Server Linux (#18965) 2025-05-08 08:53:29 +02:00
Nhu DinhandGitHub b106305167 V15 QA Added acceptance tests for tiptap style select (#19234) 2025-05-06 07:15:30 +00:00
Nhu DinhandGitHub 103631de15 V15 QA Updated the build stage to align with the azure pipelines yml (#19235) 2025-05-06 06:59:32 +00:00
Andy Butland 1b19ca7cdb Merge branch 'release/15.4.1' into v15/dev
# Conflicts:
#	version.json
2025-05-05 10:31:48 +02:00
Andy ButlandandGitHub 0ad020f0ce Restored interface methods on obsolete IBackgroundTaskQueue (#19223)
* Restore interface methods on obsolete IBackgroundTaskQueue.

* Fixed typos in comment.
2025-05-05 07:38:31 +02:00
Andy Butland fc1455e0d8 Bumped version to 15.4.1. 2025-05-03 17:28:44 +02:00
Andy Butland 793534c627 Merge branch 'release/15.4' into v15/dev
# Conflicts:
#	version.json
2025-05-01 07:38:58 +02:00
Jacob Overgaard 10518ed454 Merge branch 'release/15.4' into v15/dev 2025-04-29 15:51:45 +02:00
Jacob Overgaard a1379f13a0 Merge branch 'contrib' into v15/dev 2025-04-29 13:05:06 +02:00
Jacob OvergaardandGitHub 38e9781fc5 build: restores some of the behavior from V13 in relation to StaticAssets (#19189)
In v13, the StaticAssets build was only triggered based on the existence of either the output folder or a preserve.* marker file. Here, we also additionally check for the node_modules/.package-lock.json file before reinstalling npm dependencies. We also now only run `npm install` rather than `npm ci` to optimise the build.
2025-04-29 13:04:24 +02:00
Nhu DinhandGitHub dfc6ead6d5 V15 Added acceptance tests for tiptap statusbar (#19131)
* Updated tests for tiptap RTE

* Moved tests for titptap toolbar to another class

* Added tests for titptap toolbar

* Added tests for tiptap statusbar

* Bumped version

* Make tiptap tests run in the pipeline

* Bumped version

* Reverted npm command
2025-04-29 07:39:56 +00:00
Andy Butland 30a431f870 Merge branch 'release/15.4' into v15/dev 2025-04-28 14:37:26 +02:00
Lotte PitcherandSebastiaan Janssen 3579c4ac9e readme shield for forum 2025-04-28 12:57:51 +02:00
Sebastiaan Janssen 671751b618 Merge branch 'v15/dev' into contrib 2025-04-28 12:55:17 +02:00
Nhu DinhandGitHub 3b6e4a96f1 V15 QA Added acceptance tests for bulk trash dialog (#19125)
* Added tests for bulk trash content dialog

* Updated tests for trash content dialog

* Added tests for trash and bulk trash media dialog

* Moved trash content tests into a folder

* Bumped version

* Make trash tests run in the pipeline

* Make trash tests run in the pipeline

* Fixed comments

* Reverted npm command
2025-04-26 09:59:03 +07:00
b4528cf963 Fixed error with reflection on integration test configure builder attributes, so integration tests can be created outside of the Umbraco integration test project (#19077)
* Fixed error with reflection on integration test configure builder attributes, so integration tests can be created outside of the Umbraco integration test project.

* Fix nullability

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2025-04-24 13:18:29 +02:00
cb1ec988ce V15: Ensure elements cache is cleared on subscribers in load balanced scenarios (#19128)
* Clear elementscache from cache refreshers

* Add very simple test ensuring the elements cache is cleared

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2025-04-24 13:15:30 +02:00
Warren BuckleyandGitHub 15643135ba Add 'ManifestWithDynamicConditions' to ManifestHeaderApp so Header Apps can be conditionally shown/loaded (#19124) 2025-04-24 12:25:58 +02:00
Sven Geusensandkjac 297ceaf0aa Remove fake null checks as they are no longer needed after merge into v16 (#19109) 2025-04-24 11:46:41 +02:00
Andy Butland 8e87f2260c Bumped version in dev branch to 15.5.0-rc. 2025-04-22 09:52:10 +02:00
Andy Butland 927e4d8cad Merge branch 'release/15.4' into v15/dev 2025-04-22 09:45:15 +02:00
Lotte PitcherandGitHub cd71e5666a Incorrect forum and security urls when raising issue (#19080) 2025-04-22 06:51:08 +02:00
Sven GeusensandGitHub c86a6fa8e5 V15/fix/sub variant block deletion (#18802)
* Fix

* Editors with access should be able to clear a blocklist value

* Writeup around block element level variation

* Dissallow values to be removed a limited language user does not have permissions to

* Remove commented out code

* improved comments

* Improve expose list for limited language access sub variant block lists
2025-04-21 10:30:12 +02:00
Andy ButlandandGitHub 1ccb5cc09e Removes tree actions from tree selection in duplicate document dialog. (#19053) 2025-04-16 18:21:39 +01:00
Jacob Overgaard 12ca71558a Merge branch 'release/15.4' into v15/dev 2025-04-16 14:48:09 +02:00
Jacob Overgaard 25fffd4173 Merge remote-tracking branch 'origin/release/15.4' into v15/dev 2025-04-16 14:47:02 +02:00
Migaroez beb60065bb Merge branch 'release/15.4' into v15/dev 2025-04-15 14:40:19 +02:00
Jacob Overgaard 7e57396960 Merge branch 'contrib' into v15/dev 2025-04-15 09:21:11 +02:00
Andy ButlandandGitHub 83fe87ca88 Clarified "too many" entries validation message. (#19040) 2025-04-15 06:34:58 +00:00
Andy ButlandandGitHub ae078bf96b Adds some missing mime types to ensure uploaded audio and video displays with preview. (#19039) 2025-04-15 08:03:10 +02:00
6455f96e51 CONTRIBUTING.md - fix incorrect link to contributing-first-issue.md (#19038)
Co-authored-by: Andrew Lacey <AndrewL@if.co.nz>
2025-04-15 06:35:10 +02:00
Andy ButlandandGitHub 76e2103551 Make sure not to early return when verifying ancestor path is published without completing scope (#19029)
* Make sure not to early return when verifying ancestor path is published without completing scope.

* Added comment.
2025-04-14 17:45:57 +02:00
340ae6c474 Document 3rd Party Licenses (#19027)
* specifies the license for base64-js

* specifies the license for router-slot

* specifies the license for contentment/bytes

* adds a general third-party notices file for all direct dependency libraries

* merge separate licenses for icon packs into NOTICES.txt

* updates lee kelleher permission

* updates the name of DOMPurify

* adds Element Internals Polyfill

* capitalize names

* Update src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts

Co-authored-by: Lee Kelleher <leekelleher@users.noreply.github.com>

* adds babel

* adds eslint

* adds hey-api/openapi-ts

* adds open web components

* adds playwright

* adds rollup

* adds storybook

* adds cross-env

* adds glob

* adds globals

* adds madge

* adds msw

* adds playwright-msw

* adds prettier

* adds remark-gfm

* adds svgo

* adds tiny-glob

* adds tsc-alias

* adds typedoc

* adds typescript

* adds typescript-eslint

* adds typescript-json-schema

* adds vite-plugin-static-copy

* adds vite-tsconfig-paths

* adds web component analyzer

* adds rollup-plugin-esbuild

* adds rollup-plugin-import-css

* adds rollup plugins as general license

---------

Co-authored-by: Lee Kelleher <leekelleher@users.noreply.github.com>
2025-04-14 16:01:14 +02:00
Sebastiaan Janssen 449c94f08d Merge branch 'v15/dev' into contrib 2025-04-14 14:22:36 +02:00
83b92666ac Use maximum available value for JSON serialization depth (#19001)
* Use maximum available value for JSON serialization depth.

* Updated unittests with best practices

---------

Co-authored-by: Migaroez <geusens@gmail.com>
2025-04-14 13:50:19 +02:00
Henrik GedionsenandSebastiaan Janssen b7bf30744a Use GetIdsFromPathReversed to avoid allocating the string values of the integers parsed 2025-04-14 11:19:08 +02:00
dependabot[bot]andJacob Overgaard 9e97f6028b Bump vite from 6.2.5 to 6.2.6 in /src/Umbraco.Web.UI.Client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.5 to 6.2.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.2.6
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 11:01:59 +02:00
dependabot[bot]andJacob Overgaard 8ba3a4d926 Bump vite from 6.2.5 to 6.2.6 in /src/Umbraco.Web.UI.Login
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.5 to 6.2.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.2.6
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 10:22:22 +02:00
Nhu DinhandGitHub 7fe6971a61 V15 Updated acceptance tests for unsupported invariant content item with variant blocks (#18919)
* Updated tests for invariant block grid with variant blocks

* Updated tests for invariant block list with variant blocks

* Updated tests for invariant document with variant blocks

* Bumped version

* Changed npm command

* Updated isValidationMessageVisible

* Fixed name

* Fixed comments

* Reverted npm command
2025-04-12 08:59:31 +00:00
Andy ButlandandGitHub 9ef2b37be4 Ensure dates read from the database are treated as local when constructing entities (#18989)
* Ensure dates read from the database are treated as local when constructing entities.

* Fixed typos in comments.
2025-04-11 09:32:05 +02:00
Nhu DinhandGitHub 516d62a348 V15 Added acceptance tests for the mandatory property editor in a content or a block (#18920)
* Added tests for mandatory checkboxlist in a content

* Added tests for mandatory dropdown in a content

* Added tests for mandatory media picker in a content

* Added tests for mandatory radiobox in a content

* Added tests for block grid with mandatory property editors

* Added tests for block list with mandatory property editors

* Bumped version

* Cleaned up

* Changed npm command

* Fixed comments

* Fixed comments

* Reverted
2025-04-11 03:13:36 +00:00
Niels Lyngsø ff30b4f686 Merge branch 'contrib' into v15/dev 2025-04-09 16:56:48 +02:00
dependabot[bot]andJacob Overgaard 6bca91a52f Bump koa from 2.15.3 to 2.16.1 in /src/Umbraco.Web.UI.Client
Bumps [koa](https://github.com/koajs/koa) from 2.15.3 to 2.16.1.
- [Release notes](https://github.com/koajs/koa/releases)
- [Changelog](https://github.com/koajs/koa/blob/master/History.md)
- [Commits](https://github.com/koajs/koa/compare/2.15.3...v2.16.1)

---
updated-dependencies:
- dependency-name: koa
  dependency-version: 2.16.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-09 15:57:36 +02:00
164b80af0e Display content type names on dynamic node query steps (#18742)
* Display content type names on dynamic node query steps.

* Refactored to use `UmbRepositoryItemsManager` observable

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2025-04-09 11:47:25 +00:00
dependabot[bot]andJacob Overgaard 10ecc99d74 Bump vite from 6.2.4 to 6.2.5 in /src/Umbraco.Web.UI.Login
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.4 to 6.2.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.5/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.2.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-08 09:23:23 +02:00
360 changed files with 18550 additions and 2938 deletions
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -4,11 +4,11 @@ contact_links:
url: https://github.com/umbraco/Umbraco-CMS/discussions/new?category=features-and-ideas
about: Start a new discussion when you have ideas or feature requests, eventually discussions can turn into plans
- name: ⁉️ Support Question
url: https://our.umbraco.com
url: https://forum.umbraco.com
about: This issue tracker is NOT meant for support questions. If you have a question, please join us on the forum.
- name: 📖 Documentation Issue
url: https://github.com/umbraco/UmbracoDocs/issues
about: Documentation issues should be reported on the Umbraco documentation repository.
- name: 🔐 Security Issue
url: https://umbraco.com/about-us/trust-center/security-and-umbraco/how-to-report-a-vulnerability-in-umbraco/
url: https://umbraco.com/trust-center/security-and-umbraco/how-to-report-a-vulnerability-in-umbraco/
about: Discovered a Security Issue in Umbraco?
+1 -1
View File
@@ -4,8 +4,8 @@
[![NuGet Version](https://img.shields.io/nuget/v/Umbraco.Cms)](https://www.nuget.org/packages/Umbraco.Cms)
[![Build status](https://img.shields.io/azure-devops/build/umbraco/Umbraco%2520Cms/301?logo=azurepipelines&label=Azure%20Pipelines)](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=301)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)
[![Forum](https://img.shields.io/badge/help-forum-blue)](https://forum.umbraco.com)
[![Chat about Umbraco on Discord](https://img.shields.io/discord/869656431308189746?logo=discord&logoColor=fff)](https://discord.gg/umbraco)
[![Read what's going on in the Umbraco Discord chat now](https://img.shields.io/badge/read-discord-blue)](https://discord-chats.umbraco.com)
![Mastodon Follow](https://img.shields.io/mastodon/follow/110661369750014952?domain=https%3A%2F%2Fumbracocommunity.social)
+579
View File
@@ -0,0 +1,579 @@
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 <kpdecker@gmail.com>
---
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: 2018present 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 <ben@benlesh.com>, 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
---
TinyMCE, version 6.x: A rich text editor for the web
URL: https://www.tiny.cloud/
License: MIT License
Copyright: 2022 Ephox Corporation DBA Tiny Technologies, Inc.
---
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
+101 -254
View File
@@ -313,8 +313,8 @@ stages:
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
LinuxPart3Of3:
vmImage: "ubuntu-latest"
# Filter tests that are not part of the Umbraco.Infrastructure namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
macOSPart1Of3:
vmImage: "macOS-latest"
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
@@ -325,8 +325,8 @@ stages:
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
macOSPart3Of3:
vmImage: "macOS-latest"
# Filter tests that are not part of the Umbraco.Infrastructure namespace.
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
pool:
vmImage: $(vmImage)
variables:
@@ -389,8 +389,8 @@ stages:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are not part of the Umbraco.Infrastructure namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
LinuxPart1Of3:
vmImage: "ubuntu-latest"
SA_PASSWORD: UmbracoIntegration123!
@@ -410,8 +410,8 @@ stages:
SA_PASSWORD: UmbracoIntegration123!
Tests__Database__DatabaseType: SqlServer
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
# Filter tests that are not part of the Umbraco.Infrastructure namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
pool:
vmImage: $(vmImage)
steps:
@@ -432,6 +432,33 @@ stages:
displayName: Start SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- powershell: |
$maxAttempts = 12
$attempt = 0
$status = ""
while (($status -ne 'running') -and ($attempt -lt $maxAttempts)) {
Start-Sleep -Seconds 5
# We use the docker inspect command to check the status of the container. If the container is not running, we wait 5 seconds and try again. And if reaches 12 attempts, we fail the build.
$status = docker inspect -f '{{.State.Status}}' mssql
if ($status -ne 'running') {
Write-Host "Waiting for SQL Server to be ready... Attempt $($attempt + 1)"
$attempt++
}
}
if ($status -eq 'running') {
Write-Host "SQL Server container is running"
docker ps -a
} else {
Write-Host "SQL Server did not become ready in time. Last known status: $status"
docker logs mssql
exit 1
}
displayName: Wait for SQL Server to be ready (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- pwsh: SqlLocalDB start MSSQLLocalDB
displayName: Start SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
@@ -495,151 +522,70 @@ stages:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=Umbraco;Mode=Memory;Cache=Shared;Foreign Keys=True;Pooling=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.Sqlite
DatabaseType: SQLite
strategy:
matrix:
LinuxPart1Of3:
vmImage: "ubuntu-latest"
testFolder: "DefaultConfig"
testCommand: "npm run smokeTestSqlite -- --shard=1/3"
LinuxPart2Of3:
vmImage: "ubuntu-latest"
testFolder: "DefaultConfig"
testCommand: "npm run smokeTestSqlite -- --shard=2/3"
LinuxPart3Of3:
vmImage: "ubuntu-latest"
testFolder: "DefaultConfig"
testCommand: "npm run smokeTestSqlite -- --shard=3/3"
WindowsPart1Of3:
vmImage: "windows-latest"
testFolder: "DefaultConfig"
testCommand: "npm run smokeTestSqlite -- --shard=1/3"
WindowsPart2Of3:
vmImage: "windows-latest"
testFolder: "DefaultConfig"
testCommand: "npm run smokeTestSqlite -- --shard=2/3"
WindowsPart3Of3:
vmImage: "windows-latest"
testFolder: "DefaultConfig"
testCommand: "npm run smokeTestSqlite -- --shard=3/3"
pool:
vmImage: $(vmImage)
steps:
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download NuGet artifacts
inputs:
artifact: nupkg
path: $(Agent.BuildDirectory)/app/nupkg
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
retryCountOnTaskFailure: 3
inputs:
versionSpec: $(nodeVersion)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
# Setup test environment Template
- template: nightly-E2E-setup-template.yml
parameters:
nodeVersion: ${{ variables.nodeVersion }}
PlaywrightUserEmail: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL }}
PlaywrightPassword: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD }}
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
npm_config_cache: ${{ variables.npm_config_cache }}
- pwsh: |
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
URL=$(ASPNETCORE_URLS)
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# Cache and restore NPM packages
- task: Cache@2
displayName: Cache NPM packages
inputs:
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
restoreKeys: |
npm_e2e | "$(Agent.OS)"
npm_e2e
path: $(npm_config_cache)
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
# Build application
- pwsh: |
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
dotnet new nugetconfig
dotnet nuget add source ./nupkg --name Local
dotnet new install Umbraco.Templates::$cmsVersion
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
dotnet restore UmbracoProject
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
dotnet build UmbracoProject --configuration $(buildConfiguration) --no-restore
displayName: Restore project
workingDirectory: $(Agent.BuildDirectory)/app
- pwsh: |
dotnet build UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-restore
dotnet dev-certs https
displayName: Build application
workingDirectory: $(Agent.BuildDirectory)/app
condition: succeeded()
# Run application
- bash: |
nohup dotnet run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
displayName: Run application (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
workingDirectory: $(Agent.BuildDirectory)/app
# Run application Template
- template: nightly-E2E-run-application-template.yml
parameters:
DatabaseType: ${{ variables.DatabaseType }}
buildConfiguration: ${{ variables.buildConfiguration }}
- pwsh: |
$process = Start-Process dotnet "run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
displayName: Run application (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
workingDirectory: $(Agent.BuildDirectory)/app
# Wait for application to start responding to requests
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
displayName: Wait for application
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install chromium
displayName: Install Playwright only with Chromium browser
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: $(testCommand)
displayName: Run Playwright tests
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: true
CommitId: $(Build.SourceVersion)
AgentOs: $(Agent.OS)
# Stop application
- bash: kill -15 $(AcceptanceTestProcessId)
displayName: Stop application (Linux)
condition: and(ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
displayName: Stop application (Windows)
condition: and(ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
# Publish test artifacts
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
# Publish test results
- task: PublishTestResults@2
displayName: "Publish test results"
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
# Run tests Template
- template: nightly-E2E-run-tests-template.yml
parameters:
testCommand: $(testCommand)
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
DatabaseType: ${{ variables.DatabaseType }}
- job:
displayName: E2E Tests (SQL Server)
@@ -647,175 +593,76 @@ stages:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
DatabaseType: SQLServer
strategy:
matrix:
${{ if eq(parameters.sqlServerLinuxAcceptanceTests, True) }}:
LinuxPart1Of3:
testCommand: "npm run smokeTest -- --shard=1/3"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
testFolder: "DefaultConfig"
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
LinuxPart2Of3:
testCommand: "npm run smokeTest -- --shard=2/3"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
testFolder: "DefaultConfig"
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
LinuxPart3Of3:
testCommand: "npm run smokeTest -- --shard=3/3"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
testFolder: "DefaultConfig"
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
WindowsPart1Of3:
vmImage: "windows-latest"
testFolder: "DefaultConfig"
testCommand: "npm run smokeTest -- --shard=1/3"
WindowsPart2Of3:
vmImage: "windows-latest"
testFolder: "DefaultConfig"
testCommand: "npm run smokeTest -- --shard=2/3"
WindowsPart3Of3:
vmImage: "windows-latest"
testFolder: "DefaultConfig"
testCommand: "npm run smokeTest -- --shard=3/3"
pool:
vmImage: $(vmImage)
steps:
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download NuGet artifacts
inputs:
artifact: nupkg
path: $(Agent.BuildDirectory)/app/nupkg
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
inputs:
versionSpec: $(nodeVersion)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
# Setup test environment Template
- template: nightly-E2E-setup-template.yml
parameters:
nodeVersion: ${{ variables.nodeVersion }}
PlaywrightUserEmail: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL }}
PlaywrightPassword: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD }}
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
npm_config_cache: ${{ variables.npm_config_cache }}
- pwsh: |
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
URL=$(ASPNETCORE_URLS)
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# Cache and restore NPM packages
- task: Cache@2
displayName: Cache NPM packages
inputs:
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
restoreKeys: |
npm_e2e | "$(Agent.OS)"
npm_e2e
path: $(npm_config_cache)
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
# Build application
- pwsh: |
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
dotnet new nugetconfig
dotnet nuget add source ./nupkg --name Local
dotnet new install Umbraco.Templates::$cmsVersion
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
dotnet restore UmbracoProject
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
dotnet build UmbracoProject --configuration $(buildConfiguration) --no-restore
displayName: Restore project
workingDirectory: $(Agent.BuildDirectory)/app
- pwsh: |
dotnet build UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-restore
dotnet dev-certs https
displayName: Build application
workingDirectory: $(Agent.BuildDirectory)/app
condition: succeeded()
# Start SQL Server
- powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$(SA_PASSWORD)" mcr.microsoft.com/mssql/server:2022-latest
displayName: Start SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
# Run application Template
- template: nightly-E2E-run-application-template.yml
parameters:
SA_PASSWORD: ${{ variables.SA_PASSWORD }}
buildConfiguration: ${{ variables.buildConfiguration }}
DatabaseType: ${{ variables.DatabaseType }}
- pwsh: SqlLocalDB start MSSQLLocalDB
displayName: Start SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
# Run application
- bash: |
nohup dotnet run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
displayName: Run application (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
workingDirectory: $(Agent.BuildDirectory)/app
- pwsh: |
$process = Start-Process dotnet "run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
displayName: Run application (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
workingDirectory: $(Agent.BuildDirectory)/app
# Wait for application to start responding to requests
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
displayName: Wait for application
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install chromium
displayName: Install Playwright only with Chromium browser
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: $(testCommand)
displayName: Run Playwright tests
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: true
CommitId: $(Build.SourceVersion)
AgentOs: $(Agent.OS)
# Stop application
- bash: kill -15 $(AcceptanceTestProcessId)
displayName: Stop application (Linux)
condition: and(ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
displayName: Stop application (Windows)
condition: and(ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
# Stop SQL Server
- pwsh: docker stop mssql
displayName: Stop SQL Server Docker image (Linux)
condition: eq(variables['Agent.OS'], 'Linux')
- pwsh: SqlLocalDB stop MSSQLLocalDB
displayName: Stop SQL Server LocalDB (Windows)
condition: eq(variables['Agent.OS'], 'Windows_NT')
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
# Publish test artifacts
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
# Publish test results
- task: PublishTestResults@2
displayName: "Publish test results"
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
# Run tests Template
- template: nightly-E2E-run-tests-template.yml
parameters:
testCommand: $(testCommand)
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
DatabaseType: ${{ variables.DatabaseType }}
###############################################
## Release
+75
View File
@@ -0,0 +1,75 @@
parameters:
- name: testFolder
type: string
default: ''
- name: buildConfiguration
type: string
default: ''
- name: additionalEnvironmentVariables
type: boolean
default: False
steps:
- pwsh: |
dotnet restore UmbracoProject
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
displayName: Restore project
workingDirectory: $(Agent.BuildDirectory)/app
# Update application to use necessary app settings
- pwsh: |
$sourcePath = "$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/tests/${{ parameters.testFolder }}/AdditionalSetup"
$destinationPath = "UmbracoProject"
$jsonFiles = Get-ChildItem -Path $sourcePath -Filter "*.json"
if ($jsonFiles) {
$jsonFiles | ForEach-Object {
Write-Host "Copying: $($_.FullName)"
Copy-Item -Path $_.FullName -Destination $destinationPath -Force
}
} else {
Write-Host "No JSON files found."
}
displayName: Update application to use necessary app settings
workingDirectory: $(Agent.BuildDirectory)/app
# Update application to use necessary App_Plugins
- pwsh: |
$sourcePath = "$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/tests/${{ parameters.testFolder }}/AdditionalSetup"
$destinationPath = "UmbracoProject"
$appPluginsFolders = Get-ChildItem -Path $sourcePath -Directory -Filter "App_Plugins"
if ($appPluginsFolders) {
foreach ($folder in $appPluginsFolders) {
Write-Host "Copying folder: $($folder.FullName)"
Copy-Item -Path $folder.FullName -Destination $destinationPath -Recurse -Force
}
} else {
Write-Host "No App_Plugins found."
}
displayName: Update application to use necessary app plugins
workingDirectory: $(Agent.BuildDirectory)/app
# Update application to use necessary classes
- pwsh: |
$sourcePath = "$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/tests/${{ parameters.testFolder }}/AdditionalSetup"
$destinationPath = "UmbracoProject"
$csharpFiles = Get-ChildItem -Path $sourcePath -Filter "*.cs"
if ($csharpFiles) {
$csharpFiles | ForEach-Object {
Write-Host "Copying: $($_.FullName)"
Copy-Item -Path $_.FullName -Destination $destinationPath -Force
}
} else {
Write-Host "No C# files found."
}
displayName: Update application to use necessary classes
workingDirectory: $(Agent.BuildDirectory)/app
- ${{ if eq(parameters.additionalEnvironmentVariables, False) }}:
- pwsh: |
dotnet build UmbracoProject --configuration ${{ parameters.buildConfiguration }} --no-restore
dotnet dev-certs https
displayName: Build application
workingDirectory: $(Agent.BuildDirectory)/app
condition: succeeded()
@@ -0,0 +1,47 @@
parameters:
- name: SA_PASSWORD
type: string
default: ''
- name: buildConfiguration
type: string
default: ''
- name: additionalEnvironmentVariables
type: boolean
default: False
- name: DatabaseType
type: string
default: ''
steps:
# Skips the SQLServer setup if the databaseType does not match
- ${{ if eq(parameters.DatabaseType, 'SQLServer') }}:
# Start SQL Server Linux
- powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=${{ parameters.SA_PASSWORD }}" mcr.microsoft.com/mssql/server:2022-latest
displayName: Start SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
# Start SQL Server LocalDB Windows
- pwsh: SqlLocalDB start MSSQLLocalDB
displayName: Start SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
# If we want to add additional environment variables to the run step, then we will skip these
- ${{ if eq(parameters.additionalEnvironmentVariables, False) }}:
# Run application for Linux
- bash: |
nohup dotnet run --project UmbracoProject --configuration ${{ parameters.buildConfiguration }} --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
displayName: Run application (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
workingDirectory: $(Agent.BuildDirectory)/app
# Run application for Windows
- pwsh: |
$process = Start-Process dotnet "run --project UmbracoProject --configuration ${{ parameters.buildConfiguration }} --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
displayName: Run application (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
workingDirectory: $(Agent.BuildDirectory)/app
+105
View File
@@ -0,0 +1,105 @@
parameters:
- name: ASPNETCORE_URLS
type: string
default: ''
- name: testCommand
type: string
default: ''
- name: port
type: string
default: ''
- name: AZUREB2CTESTUSEREMAIL
type: string
default: ''
- name: AZUREB2CTESTUSERPASSWORD
type: string
default: ''
- name: DatabaseType
type: string
default: ''
steps:
# Ensures we have the package wait-on installed
- pwsh: npm install wait-on
displayName: Install wait-on package
# Wait for either the port of the aspnetcore url
- pwsh: |
$Port = "${{ parameters.port }}"
$Url = "${{ parameters.ASPNETCORE_URLS }}"
if ($Port -ne "") {
Write-Host "Waiting on TCP port $Port"
npx wait-on -v --interval 1000 --timeout 120000 "tcp:$Port"
} else {
Write-Host "Waiting on URL $Url"
npx wait-on -v --interval 1000 --timeout 120000 "$Url"
}
displayName: Wait for application
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install chromium
displayName: Install Playwright only with Chromium browser
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: ${{ parameters.testCommand }}
displayName: Run Playwright tests
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: true
CommitId: $(Build.SourceVersion)
AgentOs: $(Agent.OS)
AZUREADB2CTESTUSEREMAIL: ${{ parameters.AZUREB2CTESTUSEREMAIL }}
AZUREADB2CTESTUSERPASSWORD: ${{ parameters.AZUREB2CTESTUSERPASSWORD }}
# Stop application
- bash: kill -15 $(AcceptanceTestProcessId)
displayName: Stop application (Linux)
condition: and(succeededOrFailed(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
displayName: Stop application (Windows)
condition: and(succeededOrFailed(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
- ${{ if eq(parameters.DatabaseType, 'SQLServer') }}:
# Stop SQL Server
- pwsh: docker stop mssql
displayName: Stop SQL Server Docker image (Linux)
condition: and(succeededOrFailed(), eq(variables['Agent.OS'], 'Linux'))
- pwsh: SqlLocalDB stop MSSQLLocalDB
displayName: Stop SQL Server LocalDB (Windows)
condition: and(succeededOrFailed(), eq(variables['Agent.OS'], 'Windows_NT'))
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
# Publish
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
# Publish test results
- task: PublishTestResults@2
displayName: "Publish test results"
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
+69
View File
@@ -0,0 +1,69 @@
parameters:
- name: nodeVersion
type: string
default: ''
- name: PlaywrightUserEmail
type: string
default: ''
- name: PlaywrightPassword
type: string
default: ''
- name: ASPNETCORE_URLS
type: string
default: ''
- name: npm_config_cache
type: string
default: ''
steps:
- task: DownloadPipelineArtifact@2
displayName: Download NuGet artifacts
inputs:
artifact: nupkg
path: $(Agent.BuildDirectory)/app/nupkg
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
inputs:
versionSpec: $(nodeVersion)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
- pwsh: |
"UMBRACO_USER_LOGIN=${{ parameters.PlaywrightUserEmail }}
UMBRACO_USER_PASSWORD=${{ parameters.PlaywrightPassword }}
URL=${{ parameters.ASPNETCORE_URLS }}
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# Cache and restore NPM packages
- task: Cache@2
displayName: Cache NPM packages
inputs:
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
restoreKeys: |
npm_e2e | "$(Agent.OS)"
npm_e2e
path: ${{ parameters.npm_config_cache }}
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
# Install Template
- pwsh: |
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
dotnet new nugetconfig
dotnet nuget add source ./nupkg --name Local
dotnet new install Umbraco.Templates::$cmsVersion
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
displayName: Install Template
workingDirectory: $(Agent.BuildDirectory)/app
+471 -282
View File
@@ -3,13 +3,29 @@ name: Nightly_E2E_Test_$(TeamProject)_$(Build.DefinitionName)_$(SourceBranchName
pr: none
trigger: none
schedules:
- cron: '0 0 * * *'
displayName: Daily midnight build
branches:
include:
- v14/dev
- v15/dev
# schedules:
# - cron: '0 0 * * *'
# displayName: Daily midnight build
# branches:
# include:
# - v14/dev
# - v15/dev
parameters:
- name: differentAppSettingsAcceptanceTests
displayName: Run acceptance tests with different app settings
type: boolean
default: false
- name: skipDefaultConfigAcceptanceTests
displayName: Skip tests with DefaultConfig
type: boolean
default: false
- name: skipIntegrationTests
displayName: Skip integration tests
type: boolean
default: false
variables:
nodeVersion: 20
@@ -32,25 +48,17 @@ stages:
- job: A
displayName: Build Umbraco CMS
pool:
vmImage: 'ubuntu-latest'
vmImage: "windows-latest"
steps:
- checkout: self
fetchDepth: 0
submodules: true
submodules: false
lfs: false,
fetchDepth: 500
- template: templates/backoffice-install.yml
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
- template: templates/backoffice-install.yml
- script: npm run build:for:cms
displayName: Run build (Bellissima)
workingDirectory: src/Umbraco.Web.UI.Client
- script: npm ci --no-fund --no-audit --prefer-offline
displayName: Run npm ci (Login)
workingDirectory: src/Umbraco.Web.UI.Login
- script: npm run build
displayName: Run npm build (Login)
workingDirectory: src/Umbraco.Web.UI.Login
- task: DotNetCoreCLI@2
displayName: Run dotnet restore
inputs:
@@ -62,7 +70,7 @@ stages:
inputs:
command: build
projects: $(solution)
arguments: '--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg'
arguments: "--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg"
- task: PublishPipelineArtifact@1
displayName: Publish nupkg
inputs:
@@ -74,8 +82,237 @@ stages:
targetPath: $(Build.SourcesDirectory)
artifactName: build_output
- stage: E2E
displayName: E2E Tests
- job: B
displayName: Build Bellissima Package
pool:
vmImage: "ubuntu-latest"
steps:
- checkout: self
submodules: false
lfs: false,
fetchDepth: 500
- template: templates/backoffice-install.yml
- script: npm run build:for:npm
displayName: Run build:for:npm
workingDirectory: src/Umbraco.Web.UI.Client
- bash: |
echo "##[command]Running npm pack"
echo "##[debug]Output directory: $(Build.ArtifactStagingDirectory)"
mkdir $(Build.ArtifactStagingDirectory)/npm
npm pack --pack-destination $(Build.ArtifactStagingDirectory)/npm
mv .npmrc $(Build.ArtifactStagingDirectory)/npm/
displayName: Run npm pack
workingDirectory: src/Umbraco.Web.UI.Client
- task: PublishPipelineArtifact@1
displayName: Publish Bellissima npm artifact
inputs:
targetPath: $(Build.ArtifactStagingDirectory)/npm
artifactName: npm
- stage: Integration
displayName: Integration Tests
dependsOn: Build
condition: ${{ eq(parameters.skipIntegrationTests, false) }}
jobs:
# Integration Tests (SQLite)
- job:
timeoutInMinutes: 180
displayName: Integration Tests (SQLite)
strategy:
matrix:
# Windows:
# vmImage: 'windows-latest'
# We split the tests into 4 parts for each OS to reduce the time it takes to run them on the pipeline
LinuxPart1Of4:
vmImage: "ubuntu-latest"
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
LinuxPart2Of4:
vmImage: "ubuntu-latest"
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
LinuxPart3Of4:
vmImage: "ubuntu-latest"
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
LinuxPart4Of4:
vmImage: "ubuntu-latest"
# Filter tests that are part of the ManagementApi namespace
testFilter: "(FullyQualifiedName~ManagementApi)"
macOSPart1Of4:
vmImage: "macOS-latest"
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
macOSPart2Of4:
vmImage: "macOS-latest"
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
macOSPart3Of4:
vmImage: "macOS-latest"
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace.
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
macOSPart4Of4:
vmImage: "macOS-latest"
# Filter tests that are part of the ManagementApi namespace.
testFilter: "(FullyQualifiedName~ManagementApi)"
pool:
vmImage: $(vmImage)
variables:
Tests__Database__DatabaseType: "Sqlite"
steps:
- checkout: self
submodules: false
lfs: false,
fetchDepth: 1
fetchFilter: tree:0
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download build artifacts
inputs:
artifact: build_output
path: $(Build.SourcesDirectory)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
# Test
- task: DotNetCoreCLI@2
displayName: Run dotnet test
inputs:
command: test
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
testRunTitle: Integration Tests SQLite - $(Agent.OS)
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
# Integration Tests (SQL Server)
- job:
timeoutInMinutes: 180
displayName: Integration Tests (SQL Server)
variables:
SA_PASSWORD: UmbracoAcceptance123!
strategy:
matrix:
# We split the tests into 4 parts for each OS to reduce the time it takes to run them on the pipeline
WindowsPart1Of4:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
WindowsPart2Of4:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
WindowsPart3Of4:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace.
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
WindowsPart4Of4:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are part of the ManagementApi namespace.
testFilter: "(FullyQualifiedName~ManagementApi)"
LinuxPart1Of4:
vmImage: "ubuntu-latest"
Tests__Database__DatabaseType: SqlServer
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
LinuxPart2Of4:
vmImage: "ubuntu-latest"
Tests__Database__DatabaseType: SqlServer
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
LinuxPart3Of4:
vmImage: "ubuntu-latest"
Tests__Database__DatabaseType: SqlServer
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace.
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
LinuxPart4Of4:
vmImage: "ubuntu-latest"
Tests__Database__DatabaseType: SqlServer
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
# Filter tests that are part of the ManagementApi namespace.
testFilter: "(FullyQualifiedName~ManagementApi)"
pool:
vmImage: $(vmImage)
steps:
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download build artifacts
inputs:
artifact: build_output
path: $(Build.SourcesDirectory)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
# Start SQL Server
- powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$(SA_PASSWORD)" mcr.microsoft.com/mssql/server:2022-latest
displayName: Start SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- powershell: |
$maxAttempts = 12
$attempt = 0
$status = ""
while (($status -ne 'running') -and ($attempt -lt $maxAttempts)) {
Start-Sleep -Seconds 5
# We use the docker inspect command to check the status of the container. If the container is not running, we wait 5 seconds and try again. And if reaches 12 attempts, we fail the build.
$status = docker inspect -f '{{.State.Status}}' mssql
if ($status -ne 'running') {
Write-Host "Waiting for SQL Server to be ready... Attempt $($attempt + 1)"
$attempt++
}
}
if ($status -eq 'running') {
Write-Host "SQL Server container is running"
docker ps -a
} else {
Write-Host "SQL Server did not become ready in time. Last known status: $status"
docker logs mssql
exit 1
}
displayName: Wait for SQL Server to be ready (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- pwsh: SqlLocalDB start MSSQLLocalDB
displayName: Start SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
# Test
- task: DotNetCoreCLI@2
displayName: Run dotnet test
inputs:
command: test
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
testRunTitle: Integration Tests SQL Server - $(Agent.OS)
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
# Stop SQL Server
- pwsh: docker stop mssql
displayName: Stop SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- pwsh: SqlLocalDB stop MSSQLLocalDB
displayName: Stop SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
- stage: DefaultConfigE2E
displayName: Default Config E2E Tests
dependsOn: Build
variables:
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
@@ -103,338 +340,290 @@ stages:
- job:
displayName: E2E Tests (SQLite)
timeoutInMinutes: 180
condition: ${{ eq(parameters.skipDefaultConfigAcceptanceTests, false) }}
variables:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=Umbraco;Mode=Memory;Cache=Shared;Foreign Keys=True;Pooling=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.Sqlite
DatabaseType: SQLite
strategy:
matrix:
LinuxPart1Of3:
vmImage: "ubuntu-latest"
testCommand: "npm run test -- --shard=1/3"
testFolder: "DefaultConfig"
testCommand: "npm run testSqlite -- --shard=1/3"
LinuxPart2Of3:
vmImage: "ubuntu-latest"
testCommand: "npm run test -- --shard=2/3"
testFolder: "DefaultConfig"
testCommand: "npm run testSqlite -- --shard=2/3"
LinuxPart3Of3:
vmImage: "ubuntu-latest"
testCommand: "npm run test -- --shard=3/3"
testFolder: "DefaultConfig"
testCommand: "npm run testSqlite -- --shard=3/3"
WindowsPart1Of3:
vmImage: "windows-latest"
testCommand: "npm run test -- --shard=1/3"
testFolder: "DefaultConfig"
testCommand: "npm run testSqlite -- --shard=1/3"
WindowsPart2Of3:
vmImage: "windows-latest"
testCommand: "npm run test -- --shard=2/3"
testFolder: "DefaultConfig"
testCommand: "npm run testSqlite -- --shard=2/3"
WindowsPart3Of3:
vmImage: "windows-latest"
testCommand: "npm run test -- --shard=3/3"
testFolder: "DefaultConfig"
testCommand: "npm run testSqlite -- --shard=3/3"
pool:
vmImage: $(vmImage)
steps:
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download NuGet artifacts
inputs:
artifact: nupkg
path: $(Agent.BuildDirectory)/app/nupkg
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
retryCountOnTaskFailure: 3
inputs:
versionSpec: $(nodeVersion)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
# Setup test environment Template
- template: nightly-E2E-setup-template.yml
parameters:
nodeVersion: ${{ variables.nodeVersion }}
PlaywrightUserEmail: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL }}
PlaywrightPassword: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD }}
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
npm_config_cache: ${{ variables.npm_config_cache }}
- pwsh: |
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
URL=$(ASPNETCORE_URLS)
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# Cache and restore NPM packages
- task: Cache@2
displayName: Cache NPM packages
inputs:
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
restoreKeys: |
npm_e2e | "$(Agent.OS)"
npm_e2e
path: $(npm_config_cache)
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
# Build application
- pwsh: |
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
dotnet new nugetconfig
dotnet nuget add source ./nupkg --name Local
dotnet new install Umbraco.Templates::$cmsVersion
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
dotnet restore UmbracoProject
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
dotnet build UmbracoProject --configuration $(buildConfiguration) --no-restore
displayName: Restore project
workingDirectory: $(Agent.BuildDirectory)/app
- pwsh: |
dotnet build UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-restore
dotnet dev-certs https
displayName: Build application
workingDirectory: $(Agent.BuildDirectory)/app
condition: succeeded()
# Run application
- bash: |
nohup dotnet run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
displayName: Run application (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
workingDirectory: $(Agent.BuildDirectory)/app
# Run application Template
- template: nightly-E2E-run-application-template.yml
parameters:
DatabaseType: ${{ variables.DatabaseType }}
buildConfiguration: ${{ variables.buildConfiguration }}
- pwsh: |
$process = Start-Process dotnet "run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
displayName: Run application (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
workingDirectory: $(Agent.BuildDirectory)/app
# Ensures we have the package wait-on installed
- pwsh: npm install wait-on
displayName: Install wait-on package
# Wait for application to start responding to requests
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
displayName: Wait for application
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install --with-deps
displayName: Install Playwright
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: $(testCommand)
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: true
CommitId: $(Build.SourceVersion)
AgentOs: $(Agent.OS)
# Stop application
- bash: kill -15 $(AcceptanceTestProcessId)
displayName: Stop application (Linux)
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
displayName: Stop application (Windows)
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
# Publish
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
# Publish test results
- task: PublishTestResults@2
displayName: "Publish test results"
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
# Run tests Template
- template: nightly-E2E-run-tests-template.yml
parameters:
testCommand: $(testCommand)
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
DatabaseType: ${{ variables.DatabaseType }}
- job:
displayName: E2E Tests (SQL Server)
timeoutInMinutes: 180
condition: ${{ eq(parameters.skipDefaultConfigAcceptanceTests, false) }}
variables:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
DatabaseType: SQLServer
SA_PASSWORD: UmbracoAcceptance123!
strategy:
matrix:
LinuxPart1Of3:
testCommand: "npm run testSqlite -- --shard=1/3"
testCommand: "npm run test -- --shard=1/3"
testFolder: "DefaultConfig"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
LinuxPart2Of3:
testCommand: "npm run testSqlite -- --shard=2/3"
testCommand: "npm run test -- --shard=2/3"
testFolder: "DefaultConfig"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
LinuxPart3Of3:
testCommand: "npm run testSqlite -- --shard=3/3"
testCommand: "npm run test -- --shard=3/3"
testFolder: "DefaultConfig"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
WindowsPart1Of3:
testCommand: "npm run test -- --shard=1/3"
testFolder: "DefaultConfig"
vmImage: "windows-latest"
testCommand: "npm run testSqlite -- --shard=1/3"
WindowsPart2Of3:
testCommand: "npm run test -- --shard=2/3"
testFolder: "DefaultConfig"
vmImage: "windows-latest"
testCommand: "npm run testSqlite -- --shard=2/3"
WindowsPart3Of3:
testCommand: "npm run test -- --shard=3/3"
testFolder: "DefaultConfig"
vmImage: "windows-latest"
testCommand: "npm run testSqlite -- --shard=3/3"
pool:
vmImage: $(vmImage)
steps:
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download NuGet artifacts
inputs:
artifact: nupkg
path: $(Agent.BuildDirectory)/app/nupkg
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
inputs:
versionSpec: $(nodeVersion)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
# Setup test environment Template
- template: nightly-E2E-setup-template.yml
parameters:
nodeVersion: ${{ variables.nodeVersion }}
PlaywrightUserEmail: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL }}
PlaywrightPassword: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD }}
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
npm_config_cache: ${{ variables.npm_config_cache }}
- pwsh: |
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
URL=$(ASPNETCORE_URLS)
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# Cache and restore NPM packages
- task: Cache@2
displayName: Cache NPM packages
inputs:
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
restoreKeys: |
npm_e2e | "$(Agent.OS)"
npm_e2e
path: $(npm_config_cache)
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
# Build application
- pwsh: |
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
dotnet new nugetconfig
dotnet nuget add source ./nupkg --name Local
dotnet new install Umbraco.Templates::$cmsVersion
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
dotnet restore UmbracoProject
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
dotnet build UmbracoProject --configuration $(buildConfiguration) --no-restore
displayName: Restore project
workingDirectory: $(Agent.BuildDirectory)/app
- pwsh: |
dotnet build UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-restore
dotnet dev-certs https
displayName: Build application
workingDirectory: $(Agent.BuildDirectory)/app
condition: succeeded()
# Start SQL Server
- powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$(SA_PASSWORD)" mcr.microsoft.com/mssql/server:2022-latest
displayName: Start SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
# Run application Template
- template: nightly-E2E-run-application-template.yml
parameters:
SA_PASSWORD: ${{ variables.SA_PASSWORD }}
buildConfiguration: ${{ variables.buildConfiguration }}
DatabaseType: ${{ variables.DatabaseType }}
- pwsh: SqlLocalDB start MSSQLLocalDB
displayName: Start SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
# Run tests Template
- template: nightly-E2E-run-tests-template.yml
parameters:
testCommand: $(testCommand)
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
DatabaseType: ${{ variables.DatabaseType }}
# Run application
- stage: AdditionalConfigE2E
displayName: Additional Config E2E Tests
dependsOn: Build
variables:
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
ASPNETCORE_URLS: https://localhost:44331
PlaywrightPassword: UmbracoAcceptance123!
PlaywrightUserEmail: playwright@umbraco.com
jobs:
- job:
displayName: E2E Tests with Different App settings (SQL Server)
condition: ${{ eq(parameters.differentAppSettingsAcceptanceTests, true) }}
timeoutInMinutes: 180
variables:
SA_PASSWORD: UmbracoAcceptance123!
DatabaseType: SQLServer
strategy:
matrix:
# UnattendedInstallConfig
WindowsUnattendedInstallConfig:
vmImage: "windows-latest"
testFolder: "UnattendedInstallConfig"
testCommand: "npx playwright test --project=unattendedInstallConfig --grep=InstallSQLServer"
port: 44331
additionalEnvironmentVariables: false
# DeliveryApiConfig
WindowsDeliveryApiConfig:
vmImage: "windows-latest"
testFolder: "DeliveryApi"
port: ''
testCommand: "npx playwright test --project=deliveryApi"
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
additionalEnvironmentVariables: false
LinuxDeliveryApiConfig:
vmImage: "ubuntu-latest"
testFolder: "DeliveryApi"
port: ''
testCommand: "npx playwright test --project=deliveryApi"
CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
additionalEnvironmentVariables: false
# ExternalLogin AzureADB2C
WindowsExternalLoginAzureADB2C:
vmImage: "windows-latest"
testFolder: "ExternalLogin\\AzureADB2C"
testCommand: "npx playwright test --project=externalLoginAzureADB2C"
port: 44331
packageName: "Microsoft.AspNetCore.Authentication.OpenIdConnect"
packageVersion: "9.0.8"
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
additionalEnvironmentVariables: true
pool:
vmImage: $(vmImage)
steps:
# Setup test environment Template
- template: nightly-E2E-setup-template.yml
parameters:
nodeVersion: ${{ variables.nodeVersion }}
PlaywrightUserEmail: ${{ variables.PlaywrightUserEmail }}
PlaywrightPassword: ${{ variables.PlaywrightPassword }}
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
npm_config_cache: ${{ variables.npm_config_cache }}
# Install NuGet package if specified in the matrix
- pwsh: |
Write-Host "Installing package $(packageName) version $(packageVersion)"
dotnet add package $(packageName) --version $(packageVersion)
displayName: "Install NuGet package: $(packageName)"
workingDirectory: $(Agent.BuildDirectory)/app/UmbracoProject
condition: and(succeeded(), ne(variables['packageName'], ''), ne(variables['packageVersion'], ''))
# Build application Template
- template: nightly-E2E-build-template.yml
parameters:
testFolder: $(testFolder)
buildConfiguration: ${{ variables.buildConfiguration }}
additionalEnvironmentVariables: ${{ eq(variables['additionalEnvironmentVariables'], true) }}
# Build application for AzureADB2C
- pwsh: |
dotnet build UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-restore
dotnet dev-certs https
displayName: Build application for AzureADB2C
workingDirectory: $(Agent.BuildDirectory)/app
env:
AZUREADB2CDOMAIN: $(AZUREB2CDOMAIN)
AZUREADB2CTENANT: $(AZUREB2CTENANT)
AZUREADB2CPOLICY: $(AZUREB2CPOLICY)
AZUREADB2CCLIENTID: $(AZUREB2CCLIENTID)
AZUREADB2CCLIENTSECRET: $(AZUREB2CCLIENTSECRET)
condition: and(succeeded(), eq(variables['testFolder'], 'ExternalLogin\AzureADB2C'))
# Run application Template
- template: nightly-E2E-run-application-template.yml
parameters:
SA_PASSWORD: ${{ variables.SA_PASSWORD }}
additionalEnvironmentVariables: ${{ eq(variables['additionalEnvironmentVariables'], true ) }}
buildConfiguration: ${{ variables.buildConfiguration }}
DatabaseType: ${{ variables.DatabaseType }}
# Run application for Linux with additional Environment Variables for Azure AD
- bash: |
nohup dotnet run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
nohup dotnet run --project UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
displayName: Run application (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'), eq(variables['testFolder'], 'ExternalLogin\AzureADB2C'))
workingDirectory: $(Agent.BuildDirectory)/app
env:
AZUREADB2CDOMAIN: $(AZUREB2CDOMAIN)
AZUREADB2CTENANT: $(AZUREB2CTENANT)
AZUREADB2CPOLICY: $(AZUREB2CPOLICY)
AZUREADB2CCLIENTID: $(AZUREB2CCLIENTID)
AZUREADB2CCLIENTSECRET: $(AZUREB2CCLIENTSECRET)
# Run application for Windows with additional Environment Variables for Azure AD
- pwsh: |
$process = Start-Process dotnet "run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
$process = Start-Process dotnet "run --project UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
displayName: Run application (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['testFolder'], 'ExternalLogin\AzureADB2C'))
workingDirectory: $(Agent.BuildDirectory)/app
# Ensures we have the package wait-on installed
- pwsh: npm install wait-on
displayName: Install wait-on package
# Wait for application to start responding to requests
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
displayName: Wait for application
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install --with-deps
displayName: Install Playwright
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: $(testCommand)
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: true
CommitId: $(Build.SourceVersion)
AgentOs: $(Agent.OS)
AZUREADB2CDOMAIN: $(AZUREB2CDOMAIN)
AZUREADB2CTENANT: $(AZUREB2CTENANT)
AZUREADB2CPOLICY: $(AZUREB2CPOLICY)
AZUREADB2CCLIENTID: $(AZUREB2CCLIENTID)
AZUREADB2CCLIENTSECRET: $(AZUREB2CCLIENTSECRET)
# Stop application
- bash: kill -15 $(AcceptanceTestProcessId)
displayName: Stop application (Linux)
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
displayName: Stop application (Windows)
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
# Stop SQL Server
- pwsh: docker stop mssql
displayName: Stop SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- pwsh: SqlLocalDB stop MSSQLLocalDB
displayName: Stop SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
# Publish
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
# Publish test results
- task: PublishTestResults@2
displayName: "Publish test results"
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
# Run tests Template
- template: nightly-E2E-run-tests-template.yml
parameters:
testCommand: $(testCommand)
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
port: $(port)
AZUREB2CTESTUSEREMAIL: $(AZUREB2CTESTUSEREMAIL)
AZUREB2CTESTUSERPASSWORD: $(AZUREB2CTESTUSERPASSWORD)
DatabaseType: ${{ variables.DatabaseType }}
@@ -18,7 +18,12 @@ internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
.RequestServices
.GetRequiredService<IRequestPreviewService>();
context.EnableOutputCaching = requestPreviewService.IsPreview() is false;
IApiAccessService apiAccessService = context
.HttpContext
.RequestServices
.GetRequiredService<IApiAccessService>();
context.EnableOutputCaching = requestPreviewService.IsPreview() is false && apiAccessService.HasPublicAccess();
context.ResponseExpirationTimeSpan = _duration;
return ValueTask.CompletedTask;
@@ -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);
@@ -36,5 +36,7 @@ public class ConfigureUmbracoBackofficeJsonOptions : IConfigureNamedOptions<Json
options.JsonSerializerOptions.Converters.Add(new JsonObjectConverter());
options.JsonSerializerOptions.TypeInfoResolver = _umbracoJsonTypeInfoResolver;
options.JsonSerializerOptions.MaxDepth = 64; // Ensures the maximum possible value is used, in particular to support handling as best we can levels of nested blocks.
}
}
@@ -127,10 +127,16 @@ internal class EFCoreScope<TDbContext> : CoreScope, IEfCoreScope<TDbContext>
Locks.ClearLocks(InstanceId);
if (ParentScope is null)
// Since we can nest EFCoreScopes in other scopes derived from CoreScope, we should check whether our ParentScope OR the base ParentScope exists.
// Only if neither do do we take responsibility for ensuring the locks are cleared.
// Eventually the highest parent will clear the locks.
// Further, these locks are a reference to the locks of the highest parent anyway (see the constructor of CoreScope).
#pragma warning disable SA1100 // Do not prefix calls with base unless local implementation exists (justification: provides additional clarify here that this is defined on the base class).
if (ParentScope is null && base.HasParentScope is false)
{
Locks.EnsureLocksCleared(InstanceId);
}
#pragma warning restore SA1100 // Do not prefix calls with base unless local implementation exists
_efCoreScopeProvider.PopAmbientScope();
@@ -5,14 +5,17 @@ namespace Umbraco.Extensions;
public static class ContentSettingsExtensions
{
/// <summary>
/// Determines if file extension is allowed for upload based on (optional) white list and black list
/// held in settings.
/// Allow upload if extension is whitelisted OR if there is no whitelist and extension is NOT blacklisted.
/// Determines if file extension is allowed for upload based on (optional) allow list and deny list held in settings.
/// Disallowed file extensions are only considered if there are no allowed file extensions.
/// </summary>
public static bool IsFileAllowedForUpload(this ContentSettings contentSettings, string extension) =>
contentSettings.AllowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension)) ||
(contentSettings.AllowedUploadedFileExtensions.Any() == false &&
contentSettings.DisallowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension)) == false);
/// <param name="contentSettings">The content settings.</param>
/// <param name="extension">The file extension.</param>
/// <returns>
/// <c>true</c> if the file extension is allowed for upload; otherwise, <c>false</c>.
/// </returns>
public static bool IsFileAllowedForUpload(this ContentSettings contentSettings, string extension)
=> contentSettings.AllowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension.Trim())) ||
(contentSettings.AllowedUploadedFileExtensions.Any() == false && contentSettings.DisallowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension.Trim())) == false);
/// <summary>
/// Gets the auto-fill configuration for a specified property alias.
@@ -9,7 +9,7 @@ namespace Umbraco.Cms.Core.HostedServices;
public interface IBackgroundTaskQueue
{
/// <summary>
/// Enqueue a work item to be executed on in the background.
/// Enqueue a work item to be executed in the background.
/// </summary>
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
@@ -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}";
}
@@ -1,4 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
@@ -12,14 +15,27 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase<IDataVa
// TODO: We could further reduce circular dependencies with PropertyEditorCollection by not having IDataValueReference implemented
// by property editors and instead just use the already built in IDataValueReferenceFactory and/or refactor that into a more normal collection
private readonly ILogger<DataValueReferenceFactoryCollection> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="DataValueReferenceFactoryCollection" /> class.
/// </summary>
/// <param name="items">The items.</param>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
public DataValueReferenceFactoryCollection(Func<IEnumerable<IDataValueReferenceFactory>> items)
: base(items)
: this(
items,
StaticServiceProvider.Instance.GetRequiredService<ILogger<DataValueReferenceFactoryCollection>>())
{ }
/// <summary>
/// Initializes a new instance of the <see cref="DataValueReferenceFactoryCollection" /> class.
/// </summary>
/// <param name="items">The items.</param>
/// <param name="logger">The logger.</param>
public DataValueReferenceFactoryCollection(Func<IEnumerable<IDataValueReferenceFactory>> items, ILogger<DataValueReferenceFactoryCollection> logger)
: base(items) => _logger = logger;
/// <summary>
/// Gets all unique references from the specified properties.
/// </summary>
@@ -33,7 +49,7 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase<IDataVa
var references = new HashSet<UmbracoEntityReference>();
// Group by property editor alias to avoid duplicate lookups and optimize value parsing
foreach (var propertyValuesByPropertyEditorAlias in properties.GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Values))
foreach (IGrouping<string, IReadOnlyCollection<IPropertyValue>> propertyValuesByPropertyEditorAlias in properties.GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Values))
{
if (!propertyEditors.TryGet(propertyValuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor))
{
@@ -48,7 +64,7 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase<IDataVa
values.Add(propertyValue.PublishedValue);
}
references.UnionWith(GetReferences(dataEditor, values));
references.UnionWith(GetReferences(dataEditor, values, propertyValuesByPropertyEditorAlias.Key));
}
return references;
@@ -74,14 +90,18 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase<IDataVa
/// The references.
/// </returns>
public ISet<UmbracoEntityReference> GetReferences(IDataEditor dataEditor, IEnumerable<object?> values) =>
GetReferencesEnumerable(dataEditor, values).ToHashSet();
private IEnumerable<UmbracoEntityReference> GetReferencesEnumerable(IDataEditor dataEditor, IEnumerable<object?> values)
GetReferencesEnumerable(dataEditor, values, null).ToHashSet();
private ISet<UmbracoEntityReference> GetReferences(IDataEditor dataEditor, IEnumerable<object?> values, string propertyEditorAlias) =>
GetReferencesEnumerable(dataEditor, values, propertyEditorAlias).ToHashSet();
private IEnumerable<UmbracoEntityReference> GetReferencesEnumerable(IDataEditor dataEditor, IEnumerable<object?> values, string? propertyEditorAlias)
{
// TODO: We will need to change this once we support tracking via variants/segments
// for now, we are tracking values from ALL variants
if (dataEditor.GetValueEditor() is IDataValueReference dataValueReference)
{
foreach (UmbracoEntityReference reference in values.SelectMany(dataValueReference.GetReferences))
foreach (UmbracoEntityReference reference in GetReferencesFromPropertyValues(values, dataValueReference, propertyEditorAlias))
{
yield return reference;
}
@@ -107,6 +127,38 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase<IDataVa
}
}
private IEnumerable<UmbracoEntityReference> GetReferencesFromPropertyValues(IEnumerable<object?> values, IDataValueReference dataValueReference, string? propertyEditorAlias)
{
var result = new List<UmbracoEntityReference>();
foreach (var value in values)
{
// When property editors on data types are changed, we could have values that are incompatible with the new editor.
// Leading to issues such as:
// - https://github.com/umbraco/Umbraco-CMS/issues/17628
// - https://github.com/umbraco/Umbraco-CMS/issues/17725
// Although some changes like this are not intended to be compatible, we should handle them gracefully and not
// error in retrieving references, which would prevent manipulating or deleting the content that uses the data type.
try
{
IEnumerable<UmbracoEntityReference> references = dataValueReference.GetReferences(value);
result.AddRange(references);
}
catch (Exception ex)
{
// Log the exception but don't throw, continue with the next value.
_logger.LogError(
ex,
"Error getting references from value {Value} with data editor {DataEditor} and property editor alias {PropertyEditorAlias}.",
value,
dataValueReference.GetType().FullName,
propertyEditorAlias ?? "n/a");
throw;
}
}
return result;
}
/// <summary>
/// Gets all relation type aliases that are automatically tracked.
/// </summary>
@@ -117,6 +169,7 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase<IDataVa
[Obsolete("Use GetAllAutomaticRelationTypesAliases. This will be removed in Umbraco 15.")]
public ISet<string> GetAutomaticRelationTypesAliases(PropertyEditorCollection propertyEditors) =>
GetAllAutomaticRelationTypesAliases(propertyEditors);
public ISet<string> GetAllAutomaticRelationTypesAliases(PropertyEditorCollection propertyEditors)
{
// Always add default automatic relation types
@@ -101,7 +101,7 @@ public interface IPublishedCache
/// </summary>
/// <param name="id">The content type unique identifier.</param>
/// <returns>The content type, or null.</returns>
[Obsolete("Please use the IContentTypeCacheService instead, scheduled for removal in V16")]
[Obsolete("Please use the IPublishedContentTypeCache instead, scheduled for removal in V16")]
IPublishedContentType? GetContentType(int id);
/// <summary>
@@ -110,7 +110,7 @@ public interface IPublishedCache
/// <param name="alias">The content type alias.</param>
/// <returns>The content type, or null.</returns>
/// <remarks>The alias is case-insensitive.</remarks>
[Obsolete("Please use the IContentTypeCacheService instead, scheduled for removal in V16")]
[Obsolete("Please use the IPublishedContentTypeCache instead, scheduled for removal in V16")]
IPublishedContentType? GetContentType(string alias);
/// <summary>
@@ -126,6 +126,6 @@ public interface IPublishedCache
/// </summary>
/// <param name="key">The content type key.</param>
/// <returns>The content type, or null.</returns>
[Obsolete("Please use the IContentTypeCacheService instead, scheduled for removal in V16")]
[Obsolete("Please use the IPublishedContentTypeCache instead, scheduled for removal in V16")]
IPublishedContentType? GetContentType(Guid key);
}
+2
View File
@@ -250,6 +250,8 @@ public class CoreScope : ICoreScope
_parentScope = coreScope;
}
protected bool HasParentScope => _parentScope is not null;
protected void HandleScopedNotifications() => _notificationPublisher?.ScopeExit(Completed.HasValue && Completed.Value);
private void EnsureNotDisposed()
+45 -25
View File
@@ -44,26 +44,51 @@ public class DocumentUrlService : IDocumentUrlService
/// <summary>
/// Model used to cache a single published document along with all it's URL segments.
/// </summary>
private class PublishedDocumentUrlSegments
/// <remarks>Internal for the purpose of unit and benchmark testing.</remarks>
internal class PublishedDocumentUrlSegments
{
/// <summary>
/// Gets or sets the document key.
/// </summary>
public required Guid DocumentKey { get; set; }
/// <summary>
/// Gets or sets the language Id.
/// </summary>
public required int LanguageId { get; set; }
/// <summary>
/// Gets or sets the collection of <see cref="UrlSegment"/> for the document, language and state.
/// </summary>
public required IList<UrlSegment> UrlSegments { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the document is a draft version or not.
/// </summary>
public required bool IsDraft { get; set; }
/// <summary>
/// Model used to represent a URL segment for a document in the cache.
/// </summary>
public class UrlSegment
{
/// <summary>
/// Initializes a new instance of the <see cref="UrlSegment"/> class.
/// </summary>
public UrlSegment(string segment, bool isPrimary)
{
Segment = segment;
IsPrimary = isPrimary;
}
/// <summary>
/// Gets the URL segment string.
/// </summary>
public string Segment { get; }
/// <summary>
/// Gets a value indicating whether this URL segment is the primary one for the document, language and state.
/// </summary>
public bool IsPrimary { get; }
}
}
@@ -168,45 +193,40 @@ public class DocumentUrlService : IDocumentUrlService
scope.Complete();
}
private static IEnumerable<PublishedDocumentUrlSegments> ConvertToCacheModel(IEnumerable<PublishedDocumentUrlSegment> publishedDocumentUrlSegments)
/// <summary>
/// Converts a collection of <see cref="PublishedDocumentUrlSegment"/> to a collection of <see cref="PublishedDocumentUrlSegments"/> for caching purposes.
/// </summary>
/// <param name="publishedDocumentUrlSegments">The collection of <see cref="PublishedDocumentUrlSegment"/> retrieved from the database on startup.</param>
/// <returns>The collection of cache models.</returns>
/// <remarks>Internal for the purpose of unit and benchmark testing.</remarks>
internal static IEnumerable<PublishedDocumentUrlSegments> ConvertToCacheModel(IEnumerable<PublishedDocumentUrlSegment> publishedDocumentUrlSegments)
{
var cacheModels = new List<PublishedDocumentUrlSegments>();
var cacheModels = new Dictionary<(Guid DocumentKey, int LanguageId, bool IsDraft), PublishedDocumentUrlSegments>();
foreach (PublishedDocumentUrlSegment model in publishedDocumentUrlSegments)
{
PublishedDocumentUrlSegments? existingCacheModel = GetModelFromCache(cacheModels, model);
if (existingCacheModel is null)
(Guid DocumentKey, int LanguageId, bool IsDraft) key = (model.DocumentKey, model.LanguageId, model.IsDraft);
if (!cacheModels.TryGetValue(key, out PublishedDocumentUrlSegments? existingCacheModel))
{
cacheModels.Add(new PublishedDocumentUrlSegments
cacheModels[key] = new PublishedDocumentUrlSegments
{
DocumentKey = model.DocumentKey,
LanguageId = model.LanguageId,
UrlSegments = [new PublishedDocumentUrlSegments.UrlSegment(model.UrlSegment, model.IsPrimary)],
IsDraft = model.IsDraft,
});
};
}
else
{
existingCacheModel.UrlSegments = GetUpdatedUrlSegments(existingCacheModel.UrlSegments, model.UrlSegment, model.IsPrimary);
if (existingCacheModel.UrlSegments.Any(x => x.Segment == model.UrlSegment) is false)
{
existingCacheModel.UrlSegments.Add(new PublishedDocumentUrlSegments.UrlSegment(model.UrlSegment, model.IsPrimary));
}
}
}
return cacheModels;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static PublishedDocumentUrlSegments? GetModelFromCache(List<PublishedDocumentUrlSegments> cacheModels, PublishedDocumentUrlSegment model)
=> cacheModels
.SingleOrDefault(x => x.DocumentKey == model.DocumentKey && x.LanguageId == model.LanguageId && x.IsDraft == model.IsDraft);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static IList<PublishedDocumentUrlSegments.UrlSegment> GetUpdatedUrlSegments(IList<PublishedDocumentUrlSegments.UrlSegment> urlSegments, string segment, bool isPrimary)
{
if (urlSegments.FirstOrDefault(x => x.Segment == segment) is null)
{
urlSegments.Add(new PublishedDocumentUrlSegments.UrlSegment(segment, isPrimary));
}
return urlSegments;
return cacheModels.Values;
}
private void RemoveFromCache(IScopeContext scopeContext, Guid documentKey, string isoCode, bool isDraft)
@@ -1,5 +1,3 @@
using System.Globalization;
using System.Runtime.InteropServices;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.DependencyInjection;
@@ -88,19 +86,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<int> 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)
@@ -9,4 +9,13 @@ namespace Umbraco.Cms.Infrastructure.HostedServices;
[Obsolete("This has been relocated into Umbraco.Cms.Core. This definition in Umbraco.Cms.Infrastructure is scheduled for removal in Umbraco 17.")]
public interface IBackgroundTaskQueue : Core.HostedServices.IBackgroundTaskQueue
{
/// <summary>
/// Enqueue a work item to be executed in the background.
/// </summary>
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
/// <summary>
/// Dequeue the first item on the queue.
/// </summary>
Task<Func<CancellationToken, Task>?> DequeueAsync(CancellationToken cancellationToken);
}
@@ -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
}
/// <summary>
/// Builds an IMedia item from a dto and content type.
/// Builds a Media item from a dto and content type.
/// </summary>
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
}
/// <summary>
/// Builds an IMedia item from a dto and content type.
/// Builds a Member item from a dto and member type.
/// </summary>
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,
@@ -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);
@@ -29,7 +29,7 @@ internal class AuditRepository : EntityRepositoryBase<int, IAuditItem>, IAuditRe
List<LogDto>? dtos = Database.Fetch<LogDto>(sql);
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.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<AuditType>.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<int, IAuditItem>, IAuditRe
totalRecords = page.TotalItems;
var items = page.Items.Select(
dto => new AuditItem(dto.NodeId, Enum<AuditType>.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<AuditType>.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<int, IAuditItem>, IAuditRe
LogDto? dto = Database.First<LogDto>(sql);
return dto == null
? null
: new AuditItem(dto.NodeId, Enum<AuditType>.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, dto.Datestamp);
: new AuditItem(dto.NodeId, Enum<AuditType>.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, DateTime.SpecifyKind(dto.Datestamp, DateTimeKind.Local));
}
protected override IEnumerable<IAuditItem> PerformGetAll(params int[]? ids) => throw new NotImplementedException();
@@ -162,7 +162,7 @@ internal class AuditRepository : EntityRepositoryBase<int, IAuditItem>, IAuditRe
List<LogDto>? dtos = Database.Fetch<LogDto>(sql);
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.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<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, DateTime.SpecifyKind(x.Datestamp, DateTimeKind.Local))).ToList();
}
protected override Sql<ISqlContext> GetBaseQuery(bool isCount)
@@ -400,15 +400,17 @@ public class DocumentRepository : ContentRepositoryBase<int, IContent, DocumentR
{
foreach (ContentVariation v in contentVariation)
{
content.SetCultureInfo(v.Culture, v.Name, v.Date);
content.SetCultureInfo(v.Culture, v.Name, DateTime.SpecifyKind(v.Date, DateTimeKind.Local));
}
}
// 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 (content.PublishedState is PublishedState.Published && content.PublishedVersionId > 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));
}
}
@@ -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<ContentVersionMeta>? page =
_scopeAccessor.AmbientScope?.Database.Page<ContentVersionMeta>(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;
@@ -99,17 +99,17 @@ public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataV
continue;
}
var districtValues = valuesByPropertyEditorAlias.Distinct().ToArray();
var distinctValues = valuesByPropertyEditorAlias.Distinct().ToArray();
if (dataEditor.GetValueEditor() is IDataValueReference reference)
{
foreach (UmbracoEntityReference value in districtValues.SelectMany(reference.GetReferences))
foreach (UmbracoEntityReference value in distinctValues.SelectMany(reference.GetReferences))
{
result.Add(value);
}
}
IEnumerable<UmbracoEntityReference> references = _dataValueReferenceFactoryCollection.GetReferences(dataEditor, districtValues);
IEnumerable<UmbracoEntityReference> references = _dataValueReferenceFactoryCollection.GetReferences(dataEditor, distinctValues);
foreach (UmbracoEntityReference value in references)
{
+9 -9
View File
@@ -1,12 +1,12 @@
{
"name": "@umbraco-cms/backoffice",
"version": "15.4.0-rc3",
"version": "15.5.0-rc",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@umbraco-cms/backoffice",
"version": "15.4.0-rc3",
"version": "15.5.0-rc",
"license": "MIT",
"workspaces": [
"./src/packages/*"
@@ -94,7 +94,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"
@@ -10881,9 +10881,9 @@
}
},
"node_modules/koa": {
"version": "2.15.3",
"resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz",
"integrity": "sha512-j/8tY9j5t+GVMLeioLaxweJiKUayFhlGqNTzf2ZGwL0ZCQijd2RLHK0SLW5Tsko8YyyqCZC2cojIb0/s62qTAg==",
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/koa/-/koa-2.16.1.tgz",
"integrity": "sha512-umfX9d3iuSxTQP4pnzLOz0HKnPg0FaUUIKcye2lOiz3KPu1Y3M3xlz76dISdFPQs37P9eJz1wUpcTS6KDPn9fA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -16885,9 +16885,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": {
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@umbraco-cms/backoffice",
"license": "MIT",
"version": "15.4.0-rc3",
"version": "15.5.0-rc",
"type": "module",
"exports": {
".": null,
@@ -284,7 +284,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"
+4 -4
View File
@@ -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[] = [];
@@ -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 <andmehlsen@gmail.com>
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
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
@@ -1,10 +1,10 @@
import type { ManifestElement } from '@umbraco-cms/backoffice/extension-api';
import type { ManifestElement, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api';
/**
* Header apps are displayed in the top right corner of the backoffice
* The two provided header apps are the search and the user menu
*/
export interface ManifestHeaderApp extends ManifestElement {
export interface ManifestHeaderApp extends ManifestElement, ManifestWithDynamicConditions<UmbExtensionConditionConfig> {
type: 'headerApp';
//meta: MetaHeaderApp;
}
@@ -1,16 +0,0 @@
Lucide License
ISC License <https://lucide.dev/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 <https://creativecommons.org/publicdomain/zero/1.0/>
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.
@@ -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 {
@@ -126,6 +126,7 @@ export class UmbDropzoneMediaElement extends UmbInputDropzoneElement {
z-index: 100;
border-radius: var(--uui-border-radius);
border: 1px solid var(--uui-color-focus);
box-sizing:border-box;
}
`,
];
@@ -4,15 +4,20 @@ import {
UMB_CONTENT_PICKER_DOCUMENT_ROOT_QUERY_STEP_PICKER_MODAL,
} from '../modals/index.js';
import type { ManifestDynamicRootOrigin, ManifestDynamicRootQueryStep } from '../dynamic-root.extension.js';
import { html, css, customElement, property, ifDefined, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { css, customElement, html, ifDefined, property, repeat, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
import { UmbId } from '@umbraco-cms/backoffice/id';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbRepositoryItemsManager } from '@umbraco-cms/backoffice/repository';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS } from '@umbraco-cms/backoffice/document';
import { UMB_DOCUMENT_TYPE_ITEM_REPOSITORY_ALIAS } from '@umbraco-cms/backoffice/document-type';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import type { UmbDocumentItemModel } from '@umbraco-cms/backoffice/document';
import type { UmbDocumentTypeItemModel } from '@umbraco-cms/backoffice/document-type';
import type { UmbModalContext } from '@umbraco-cms/backoffice/modal';
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
const elementName = 'umb-input-content-picker-document-root';
@customElement(elementName)
@@ -20,6 +25,16 @@ export class UmbInputContentPickerDocumentRootElement extends UmbFormControlMixi
string | undefined,
typeof UmbLitElement
>(UmbLitElement) {
readonly #documentItemManager = new UmbRepositoryItemsManager<UmbDocumentItemModel>(
this,
UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS,
);
readonly #documentTypeItemManager = new UmbRepositoryItemsManager<UmbDocumentTypeItemModel>(
this,
UMB_DOCUMENT_TYPE_ITEM_REPOSITORY_ALIAS,
);
protected override getFormElement() {
return undefined;
}
@@ -35,6 +50,10 @@ export class UmbInputContentPickerDocumentRootElement extends UmbFormControlMixi
#dynamicRootOrigin?: { label: string; icon: string; description?: string };
#documentLookup: Record<string, string> = {};
#documentTypeLookup: Record<string, string> = {};
#modalContext?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE;
#openModal?: UmbModalContext;
@@ -59,9 +78,29 @@ export class UmbInputContentPickerDocumentRootElement extends UmbFormControlMixi
this._queryStepManifests = queryStepManifests;
},
);
this.observe(this.#documentItemManager.items, (documents) => {
if (!documents?.length) return;
documents.forEach((document) => {
this.#documentLookup[document.unique] = document.name;
});
this.requestUpdate();
});
this.observe(this.#documentTypeItemManager.items, (documentTypes) => {
if (!documentTypes?.length) return;
documentTypes.forEach((documentType) => {
this.#documentTypeLookup[documentType.unique] = documentType.name;
});
this.requestUpdate();
});
}
override connectedCallback(): void {
override connectedCallback() {
super.connectedCallback();
this.#updateDynamicRootOrigin(this.data);
@@ -116,6 +155,11 @@ export class UmbInputContentPickerDocumentRootElement extends UmbFormControlMixi
#updateDynamicRootOrigin(data?: UmbContentPickerDynamicRoot) {
if (!data) return;
const origin = this._originManifests.find((item) => item.meta.originAlias === data.originAlias)?.meta;
if (data.originKey) {
this.#documentItemManager.setUniques([data.originKey]);
}
this.#dynamicRootOrigin = {
label: origin?.label ?? data.originAlias,
icon: origin?.icon ?? 'icon-wand',
@@ -131,7 +175,10 @@ export class UmbInputContentPickerDocumentRootElement extends UmbFormControlMixi
querySteps = querySteps.map((item) => (item.unique ? item : { ...item, unique: UmbId.new() }));
}
this.#documentTypeItemManager.setUniques((querySteps ?? []).map((x) => x.anyOfDocTypeKeys ?? []).flat());
this.#sorter?.setModel(querySteps ?? []);
this.data = { ...this.data, ...{ querySteps } };
}
@@ -142,8 +189,16 @@ export class UmbInputContentPickerDocumentRootElement extends UmbFormControlMixi
description?: string;
} {
const step = this._queryStepManifests.find((step) => step.meta.queryStepAlias === item.alias)?.meta;
const docTypes = item.anyOfDocTypeKeys?.join(', ');
const description = docTypes ? this.localize.term('dynamicRoot_queryStepTypes') + docTypes : undefined;
const docTypeNames =
item.anyOfDocTypeKeys
?.map((docTypeKey) => this.#documentTypeLookup[docTypeKey] ?? docTypeKey)
.sort()
.join(', ') ?? '';
const description = item.anyOfDocTypeKeys
? this.localize.term('dynamicRoot_queryStepTypes') + docTypeNames
: undefined;
return {
unique: item.unique,
@@ -193,11 +248,11 @@ export class UmbInputContentPickerDocumentRootElement extends UmbFormControlMixi
#renderOrigin() {
if (!this.#dynamicRootOrigin) return;
const description = this.#dynamicRootOrigin.description
? this.#documentLookup[this.#dynamicRootOrigin.description]
: '';
return html`
<uui-ref-node
standalone
name=${this.#dynamicRootOrigin.label}
detail=${ifDefined(this.#dynamicRootOrigin.description)}>
<uui-ref-node standalone name=${this.#dynamicRootOrigin.label} detail=${ifDefined(description)}>
<umb-icon slot="icon" name=${ifDefined(this.#dynamicRootOrigin.icon)}></umb-icon>
<uui-action-bar slot="actions">
<uui-button
+4 -4
View File
@@ -9,7 +9,7 @@
"@umbraco-cms/backoffice": "15.2.1",
"msw": "^2.7.0",
"typescript": "^5.7.3",
"vite": "^6.2.4",
"vite": "^6.2.6",
"vite-tsconfig-paths": "^5.1.4"
},
"engines": {
@@ -3772,9 +3772,9 @@
}
},
"node_modules/vite": {
"version": "6.2.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz",
"integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==",
"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": {
+1 -1
View File
@@ -16,7 +16,7 @@
"@umbraco-cms/backoffice": "15.2.1",
"msw": "^2.7.0",
"typescript": "^5.7.3",
"vite": "^6.2.4",
"vite": "^6.2.6",
"vite-tsconfig-paths": "^5.1.4"
},
"msw": {
+9 -10
View File
@@ -7,8 +7,8 @@
"name": "acceptancetest",
"hasInstallScript": true,
"dependencies": {
"@umbraco/json-models-builders": "^2.0.31",
"@umbraco/playwright-testhelpers": "^15.0.44",
"@umbraco/json-models-builders": "^2.0.33",
"@umbraco/playwright-testhelpers": "^15.0.56",
"camelize": "^1.0.0",
"dotenv": "^16.3.1",
"node-fetch": "^2.6.7"
@@ -58,21 +58,20 @@
}
},
"node_modules/@umbraco/json-models-builders": {
"version": "2.0.32",
"resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.32.tgz",
"integrity": "sha512-Aw7yBu8ePNxdjS7Q61j5KPFsiOS+IGCYxBX0H4KWbjXTdvL/PsB98KiqbDHHKFnp0fF1b2ffwJAI6jmvnxPBzg==",
"license": "MIT",
"version": "2.0.33",
"resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.33.tgz",
"integrity": "sha512-FAQxQIHoY6PGxWuodp4LSQxNufnOiqnaRNmtG8Ejn01r9lJJdA27CJKJ0bCs8U0W3cdN+Z+j7jBLd8H025/THw==",
"dependencies": {
"camelize": "^1.0.1"
}
},
"node_modules/@umbraco/playwright-testhelpers": {
"version": "15.0.44",
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.44.tgz",
"integrity": "sha512-rbStBJG0bdVfs9pzEjAhArhcPoQbuvrUpvDJLbEMBcg54lRFH0I5nzcAimdZaY4UWh2r0i9ANfjv9E25NJxRUg==",
"version": "15.0.56",
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.56.tgz",
"integrity": "sha512-QPLNLnS96QvkCPpumpOwn9ufgIWgsv4W0UQENTU63pMniHPYfaflFMafI4qjM/euwMyXHH7k9+c4k/tGew2EQA==",
"license": "MIT",
"dependencies": {
"@umbraco/json-models-builders": "2.0.32",
"@umbraco/json-models-builders": "2.0.33",
"node-fetch": "^2.6.7"
}
},
@@ -20,8 +20,8 @@
"typescript": "^4.8.3"
},
"dependencies": {
"@umbraco/json-models-builders": "^2.0.31",
"@umbraco/playwright-testhelpers": "^15.0.44",
"@umbraco/json-models-builders": "^2.0.33",
"@umbraco/playwright-testhelpers": "^15.0.56",
"camelize": "^1.0.0",
"dotenv": "^16.3.1",
"node-fetch": "^2.6.7"
@@ -43,14 +43,43 @@ export default defineConfig({
testMatch: '**/*.setup.ts',
},
{
name: 'chromium',
name: 'defaultConfig',
testMatch: 'DefaultConfig/**',
dependencies: ['setup'],
use: {
...devices['Desktop Chrome'],
// Use prepared auth state.
ignoreHTTPSErrors: true,
storageState: STORAGE_STATE,
storageState: STORAGE_STATE
}
},
{
name: 'deliveryApi',
testMatch: 'DeliveryApi/**',
dependencies: ['setup'],
use: {
...devices['Desktop Chrome'],
// Use prepared auth state.
ignoreHTTPSErrors: true,
storageState: STORAGE_STATE
},
},
{
name: 'externalLoginAzureADB2C',
testMatch: 'ExternalLogin/AzureADB2C/**',
use: {
...devices['Desktop Chrome'],
// Use prepared auth state.
ignoreHTTPSErrors: true,
}
},
// This project is used to test the install steps, for that we do not need to authenticate.
{
name: 'unattendedInstallConfig',
testMatch: 'UnattendedInstallConfig/**',
use: {
...devices['Desktop Chrome']
}
}
],
});
@@ -1,105 +0,0 @@
import {test} from "@umbraco/playwright-testhelpers";
import {expect} from "@playwright/test";
test.describe('DataType tests', () => {
let dataTypeId = '';
let dataTypeFolderId = '';
let copiedDataTypeId = '';
const dataTypeName = 'TestDataType';
const folderName = 'TestDataTypeFolder';
const editorAlias = 'Umbraco.DateTime';
const editorUiAlias = 'Umb.PropertyEditorUi.DatePicker';
const dataTypeData = [
{
"alias": "tester",
"value": "Howdy"
}
];
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.dataType.ensureNameNotExists(dataTypeName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.dataType.ensureNameNotExists(dataTypeName);
if (dataTypeFolderId != '') {
await umbracoApi.dataType.ensureNameNotExists(folderName);
}
});
test('can create dataType', async ({umbracoApi}) => {
// Act
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData);
// Assert
expect(umbracoApi.dataType.doesExist(dataTypeId)).toBeTruthy();
});
test('can update dataType', async ({umbracoApi}) => {
// Arrange
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, []);
const dataType = await umbracoApi.dataType.get(dataTypeId);
dataType.values = dataTypeData;
// Act
await umbracoApi.dataType.update(dataTypeId, dataType);
// Assert
// Checks if the data type was updated
const updatedDataType = await umbracoApi.dataType.get(dataTypeId);
expect(updatedDataType.values).toEqual(dataTypeData);
});
test('can delete dataType', async ({umbracoApi}) => {
// Arrange
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData);
expect(await umbracoApi.dataType.doesExist(dataTypeId)).toBeTruthy();
// Act
await umbracoApi.dataType.delete(dataTypeId);
// Assert
expect(await umbracoApi.dataType.doesExist(dataTypeId)).toBeFalsy();
});
test('can move a dataType to a folder', async ({umbracoApi}) => {
// Arrange
await umbracoApi.dataType.ensureNameNotExists(folderName);
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData);
expect(await umbracoApi.dataType.doesNameExist(dataTypeName)).toBeTruthy();
dataTypeFolderId = await umbracoApi.dataType.createFolder(folderName);
expect(await umbracoApi.dataType.doesFolderExist(dataTypeFolderId)).toBeTruthy();
// Act
await umbracoApi.dataType.moveToFolder(dataTypeId, dataTypeFolderId);
// Assert
// Checks if the datatype has the parentId of the folder
const dataTypeInFolder = await umbracoApi.dataType.getChildren(dataTypeFolderId);
expect(dataTypeInFolder[0].parent.id).toEqual(dataTypeFolderId);
});
test('can copy a dataType to a folder', async ({umbracoApi}) => {
// Arrange
await umbracoApi.dataType.ensureNameNotExists(folderName);
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData);
dataTypeFolderId = await umbracoApi.dataType.createFolder(folderName);
const dataType = await umbracoApi.dataType.get(dataTypeId);
const dataTypeFolder = await umbracoApi.dataType.getFolder(dataTypeFolderId);
// Act
copiedDataTypeId = await umbracoApi.dataType.copyToFolder(dataType.id, dataTypeFolder.id);
// Assert
const copiedDataType = await umbracoApi.dataType.get(copiedDataTypeId);
expect(copiedDataType.name).toEqual(dataTypeName + ' (copy)');
// Checks if both dataTypes exists
expect(await umbracoApi.dataType.doesExist(dataTypeId)).toBeTruthy();
expect(await umbracoApi.dataType.doesExist(copiedDataTypeId)).toBeTruthy();
// Clean
await umbracoApi.dataType.delete(copiedDataTypeId);
});
});
@@ -1,50 +0,0 @@
import {test} from "@umbraco/playwright-testhelpers";
import {expect} from "@playwright/test";
test.describe('DataTypeFolder tests', () => {
let dataTypeFolderId = '';
const dataTypeFolderName = 'TestDataTypeFolder';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.dataType.ensureNameNotExists(dataTypeFolderName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.dataType.ensureNameNotExists(dataTypeFolderName);
});
test('can create a dataType folder', async ({umbracoApi}) => {
// Act
dataTypeFolderId = await umbracoApi.dataType.createFolder(dataTypeFolderName);
// Assert
expect(umbracoApi.dataType.doesFolderExist(dataTypeFolderId)).toBeTruthy();
});
test('can rename a dataType folder', async ({umbracoApi}) => {
// Arrange
const wrongDataTypeFolderName = 'WrongFolderName';
dataTypeFolderId = await umbracoApi.dataType.createFolder(wrongDataTypeFolderName);
// Act
await umbracoApi.dataType.renameFolder(dataTypeFolderId, dataTypeFolderName);
// Assert
expect(umbracoApi.dataType.doesFolderExist(dataTypeFolderId)).toBeTruthy();
// Checks if the dataType folder was updated
const newDataTypeFolderName = await umbracoApi.dataType.getFolder(dataTypeFolderId);
expect(newDataTypeFolderName.name).toEqual(dataTypeFolderName);
});
test('can delete a dataType folder', async ({umbracoApi}) => {
// Arrange
await umbracoApi.dataType.createFolder(dataTypeFolderName);
expect(umbracoApi.dataType.doesFolderExist(dataTypeFolderId)).toBeTruthy();
// Act
await umbracoApi.dataType.delete(dataTypeFolderId);
// Assert
expect(await umbracoApi.dataType.doesFolderExist(dataTypeFolderId)).toBeFalsy();
});
});
@@ -1,106 +0,0 @@
import {test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
import * as crypto from 'crypto';
test.describe('Dictionary tests', () => {
let dictionaryId = '';
const dictionaryName = 'TestDictionary';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.dictionary.ensureNameNotExists(dictionaryName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.dictionary.ensureNameNotExists(dictionaryName);
});
test('can create a dictionary', async ({umbracoApi}) => {
// Act
dictionaryId = await umbracoApi.dictionary.createDefaultDictionary(dictionaryName);
// Assert
expect(await umbracoApi.dictionary.doesExist(dictionaryId)).toBeTruthy();
});
test('can update a dictionary', async ({umbracoApi}) => {
// Arrange
const wrongDictionaryName = 'WrongTestDictionary';
dictionaryId = await umbracoApi.dictionary.createDefaultDictionary(wrongDictionaryName);
expect(await umbracoApi.dictionary.doesNameExist(wrongDictionaryName)).toBeTruthy();
const dictionary = await umbracoApi.dictionary.get(dictionaryId);
// Act
dictionary.name = dictionaryName;
await umbracoApi.dictionary.update(dictionaryId, dictionary);
// Assert
// Checks if the dictionary was updated
const newDictionary = await umbracoApi.dictionary.get(dictionaryId);
expect(newDictionary.name).toEqual(dictionaryName);
expect(await umbracoApi.dictionary.doesExist(dictionaryId)).toBeTruthy();
});
test('can delete a dictionary', async ({umbracoApi}) => {
// Arrange
dictionaryId = await umbracoApi.dictionary.create(dictionaryName);
expect(await umbracoApi.dictionary.doesExist(dictionaryId)).toBeTruthy();
// Act
await umbracoApi.dictionary.delete(dictionaryId);
// Assert
expect(await umbracoApi.dictionary.doesExist(dictionaryId)).toBeFalsy();
});
test('can create a dictionary item in a dictionary', async ({umbracoApi}) => {
// Arrange
const childDictionaryName = 'ChildDictionary';
await umbracoApi.dictionary.ensureNameNotExists(dictionaryName);
dictionaryId = await umbracoApi.dictionary.create(dictionaryName);
// Act
await umbracoApi.dictionary.create(childDictionaryName, [], dictionaryId);
// Assert
// Checks if the parent dictionary contains the child dictionary
const dictionaryChildren = await umbracoApi.dictionary.getChildren(dictionaryId);
expect(dictionaryChildren[0].name).toEqual(childDictionaryName);
});
test('can export a dictionary', async ({umbracoApi}) => {
// Arrange
dictionaryId = await umbracoApi.dictionary.create(dictionaryName);
expect(await umbracoApi.dictionary.doesExist(dictionaryId)).toBeTruthy();
// Act
const exportResponse = await umbracoApi.dictionary.export(dictionaryId, false);
// Assert
// Checks if the .udt file is exported
expect(exportResponse.headers()['content-disposition']).toContain(".udt");
});
test('can import a dictionary', async ({umbracoApi}) => {
// Arrange
const temporaryFileId = crypto.randomUUID();
const fileName = 'TestSingleDictionary.udt';
const mimeType = 'application/octet-stream';
const filePath = './fixtures/dictionary/TestSingleDictionary.udt';
// This variable must not be changed as it is declared in the file TestDictionary.udt
const importDictionaryName = 'TestImportDictionary';
// Create temporary file
await umbracoApi.temporaryFile.create(temporaryFileId, fileName, mimeType, filePath);
expect(await umbracoApi.temporaryFile.doesExist(temporaryFileId)).toBeTruthy();
// Act
const importResponse = await umbracoApi.dictionary.import(temporaryFileId, null);
// Assert
expect(importResponse.status).toBeTruthy();
expect(await umbracoApi.dictionary.doesNameExist(importDictionaryName)).toBeTruthy();
// Clean
await umbracoApi.temporaryFile.delete(temporaryFileId);
});
});
@@ -1,55 +0,0 @@
import {test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
test.describe('Documents tests', () => {
let documentTypeId = '';
let documentId = '';
const documentName = 'TestDocument';
const documentTypeName = 'TestDocumentType';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
documentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(documentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
test('can create a document', async ({umbracoApi}) => {
// Act
await umbracoApi.document.createDefaultDocument(documentName, documentTypeId);
// Assert
expect(await umbracoApi.document.doesNameExist(documentName)).toBeTruthy();
});
test('can delete a document', async ({umbracoApi}) => {
// Arrange
documentId = await umbracoApi.document.createDefaultDocument(documentName, documentTypeId);
expect(umbracoApi.document.doesExist(documentId)).toBeTruthy();
// Act
await umbracoApi.document.delete(documentId);
// Assert
expect(await umbracoApi.document.doesNameExist(documentName)).toBeFalsy();
});
test('can update a document', async ({umbracoApi}) => {
// Arrange
const wrongName = 'WrongDocument';
documentId = await umbracoApi.document.createDefaultDocument(wrongName, documentTypeId);
expect(await umbracoApi.document.doesNameExist(wrongName)).toBeTruthy();
let documentData = await umbracoApi.document.get(documentId);
documentData.variants[0].name = documentName;
// Act
await umbracoApi.document.update(documentData.id, documentData);
// Assert
const updatedDocumentData = await umbracoApi.document.get(documentId);
expect(updatedDocumentData.variants[0].name).toEqual(documentName);
});
});
@@ -1,52 +0,0 @@
import {test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
test.describe('Language tests', () => {
const languageNameDanish = 'Dansk';
const isoCodeDanish = 'da-DK';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.language.ensureNameNotExists(languageNameDanish);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.language.ensureNameNotExists(languageNameDanish);
});
test('can create a language', async ({umbracoApi}) => {
// Act
await umbracoApi.language.create(languageNameDanish, false, false, isoCodeDanish);
// Assert
expect(await umbracoApi.language.doesExist(isoCodeDanish)).toBeTruthy();
});
test('can update a language', async ({umbracoApi}) => {
// Arrange
const wrongLanguageName = 'densk';
await umbracoApi.language.create(wrongLanguageName, false, false, isoCodeDanish);
const language = await umbracoApi.language.get(isoCodeDanish);
// Act
language.name = languageNameDanish;
await umbracoApi.language.update(isoCodeDanish, language);
// Assert
expect(await umbracoApi.language.doesExist(isoCodeDanish)).toBeTruthy();
// Checks if the language name was updated
const updatedLanguage = await umbracoApi.language.get(isoCodeDanish);
expect(updatedLanguage.name).toEqual(languageNameDanish);
});
test('can delete a language', async ({umbracoApi}) => {
// Arrange
await umbracoApi.language.create(languageNameDanish, false, false, isoCodeDanish);
expect(await umbracoApi.language.doesExist(isoCodeDanish)).toBeTruthy();
//Act
await umbracoApi.language.delete(isoCodeDanish);
// Assert
expect(await umbracoApi.language.doesExist(isoCodeDanish)).toBeFalsy();
});
});
@@ -1,45 +0,0 @@
import {test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
test.describe('Member tests', () => {
let memberId = '';
let memberTypeId = '';
const memberName = 'TestMember';
const memberTypeName = 'TestMemberType';
const username = 'testmember';
const email = 'testmember@acceptance.test';
const password = '0123456789';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.memberType.ensureNameNotExists(memberTypeName);
await umbracoApi.member.ensureNameNotExists(memberName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.memberType.ensureNameNotExists(memberTypeName);
await umbracoApi.member.ensureNameNotExists(memberName);
});
test('can create a member', async ({umbracoApi}) => {
// Arrange
memberTypeId = await umbracoApi.memberType.createDefaultMemberType(memberTypeName);
// Act
memberId = await umbracoApi.member.createDefaultMember(memberName, memberTypeId, email, username, password);
// Assert
expect(await umbracoApi.member.doesExist(memberId)).toBeTruthy();
});
test('can delete a member', async ({umbracoApi}) => {
// Arrange
memberTypeId = await umbracoApi.memberType.createDefaultMemberType(memberTypeName);
memberId = await umbracoApi.member.createDefaultMember(memberName, memberTypeId, email, username, password);
// Act
await umbracoApi.member.delete(memberId);
// Assert
expect(await umbracoApi.member.doesExist(memberId)).toBeFalsy();
});
});
@@ -1,50 +0,0 @@
import {test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
test.describe('Member Group tests', () => {
let memberGroupId = '';
const memberGroupName = 'TestMemberGroup';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.memberGroup.ensureNameNotExists(memberGroupName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.memberGroup.ensureNameNotExists(memberGroupName);
});
test('can create a member group', async ({umbracoApi}) => {
// Act
memberGroupId = await umbracoApi.memberGroup.create(memberGroupName);
// Assert
expect(await umbracoApi.memberGroup.doesExist(memberGroupId)).toBeTruthy();
});
test('can rename a member group', async ({umbracoApi}) => {
// Arrange
const wrongMemberGroupName = 'Wrong Name';
await umbracoApi.memberGroup.ensureNameNotExists(wrongMemberGroupName);
memberGroupId = await umbracoApi.memberGroup.create(wrongMemberGroupName);
expect(await umbracoApi.memberGroup.doesExist(memberGroupId)).toBeTruthy();
// Act
await umbracoApi.memberGroup.rename(memberGroupId, memberGroupName);
// Assert
expect(await umbracoApi.memberGroup.doesNameExist(memberGroupName)).toBeTruthy();
expect(await umbracoApi.memberGroup.doesNameExist(wrongMemberGroupName)).toBeFalsy();
});
test('can delete a member group', async ({umbracoApi}) => {
// Arrange
memberGroupId = await umbracoApi.memberGroup.create(memberGroupName);
expect(await umbracoApi.memberGroup.doesExist(memberGroupId)).toBeTruthy();
// Act
await umbracoApi.memberGroup.delete(memberGroupId);
// Assert
expect(await umbracoApi.memberGroup.doesExist(memberGroupId)).toBeFalsy();
});
});
@@ -1,50 +0,0 @@
import {test} from '@umbraco/playwright-testhelpers';
import {expect} from '@playwright/test';
test.describe('Package tests', () => {
let packageId = '';
const packageName = 'TestName';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.package.ensureNameNotExists(packageName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.package.delete(packageId);
});
test('can create a package', async ({umbracoApi}) => {
// Act
packageId = await umbracoApi.package.create(packageName);
// Assert
expect(await umbracoApi.package.doesExist(packageId)).toBeTruthy();
});
test('can update a package', async ({umbracoApi}) => {
// Arrange
const wrongPackageName = 'WrongName';
await umbracoApi.package.ensureNameNotExists(wrongPackageName);
// Act
packageId = await umbracoApi.package.create(wrongPackageName);
const packageData = await umbracoApi.package.get(packageId);
packageData.name = packageName;
await umbracoApi.package.update(packageId, packageData);
// Assert
const updatedPackageData = await umbracoApi.package.get(packageId);
expect(updatedPackageData.name).toEqual(packageName);
});
test('can delete a package', async ({umbracoApi}) => {
// Arrange
packageId = await umbracoApi.package.create(packageName);
// Act
await umbracoApi.package.delete(packageId);
// Assert
expect(await umbracoApi.package.doesExist(packageId)).toBeFalsy();
});
});
@@ -1,61 +0,0 @@
import {test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
test.describe('Partial View tests', () => {
let partialViewPath = '';
const partialViewName = 'partialViewName.cshtml';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.partialView.ensureNameNotExists(partialViewName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.partialView.ensureNameNotExists(partialViewName);
});
test('can create a partial view', async ({umbracoApi}) => {
// Act
partialViewPath = await umbracoApi.partialView.create(partialViewName, 'test');
// Assert
expect(await umbracoApi.partialView.doesExist(partialViewPath)).toBeTruthy();
});
test('can update a partial view', async ({umbracoApi}) => {
// Arrange
const newContent = 'Howdy';
partialViewPath = await umbracoApi.partialView.create(partialViewName, 'test');
// Act
await umbracoApi.partialView.updateContent(partialViewPath, newContent);
// Assert
const updatedPartialView = await umbracoApi.partialView.get(partialViewPath);
expect(updatedPartialView.content).toEqual(newContent);
});
test('can rename a partial view', async ({umbracoApi}) => {
// Arrange
const wrongPartialViewName = 'wrongPartialViewName.cshtml';
const wrongPartialViewPath = await umbracoApi.partialView.create(wrongPartialViewName, 'test');
// Act
const updatedPartialViewPath = await umbracoApi.partialView.updateName(wrongPartialViewPath, partialViewName);
// Assert
const updatedPartialView = await umbracoApi.partialView.get(updatedPartialViewPath);
expect(updatedPartialView.name).toEqual(partialViewName);
});
test('can delete a partial view', async ({umbracoApi}) => {
// Arrange
partialViewPath = await umbracoApi.partialView.create(partialViewName, 'test');
expect(await umbracoApi.partialView.doesExist(partialViewPath)).toBeTruthy();
// Act
await umbracoApi.partialView.delete(partialViewPath);
// Assert
expect(await umbracoApi.partialView.doesExist(partialViewPath)).toBeFalsy();
});
});
@@ -1,71 +0,0 @@
import {test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
test.describe('Partial View Folder tests', () => {
let partialViewFolderPath = '';
const partialViewFolderName = 'partialViewFolder';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.partialView.ensureNameNotExists(partialViewFolderName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.partialView.ensureNameNotExists(partialViewFolderName);
});
test('can create a partial view folder', async ({umbracoApi}) => {
// Act
partialViewFolderPath = await umbracoApi.partialView.createFolder(partialViewFolderName);
// Assert
expect(await umbracoApi.partialView.doesFolderExist(partialViewFolderPath)).toBeTruthy();
});
test('can delete a partial view folder', async ({umbracoApi}) => {
// Arrange
partialViewFolderPath = await umbracoApi.partialView.createFolder(partialViewFolderName);
expect(await umbracoApi.partialView.doesFolderExist(partialViewFolderPath)).toBeTruthy();
// Act
await umbracoApi.partialView.deleteFolder(partialViewFolderPath);
// Assert
expect(await umbracoApi.partialView.doesFolderExist(partialViewFolderPath)).toBeFalsy();
});
test('can add a partial view folder in another', async ({umbracoApi}) => {
// Arrange
const childFolderName = 'childFolder';
partialViewFolderPath = await umbracoApi.partialView.createFolder(partialViewFolderName);
// Act
await umbracoApi.partialView.createFolder(childFolderName, partialViewFolderPath);
// Assert
const childrenData = await umbracoApi.partialView.getChildren(partialViewFolderPath);
expect(childrenData[0].name).toEqual(childFolderName);
});
test('can add a partial view folder in a folder in another folder', async ({umbracoApi}) => {
// Arrange
const childFolderName = 'childFolder';
const childOfChildFolderName = 'childOfChildFolder';
// Creates parent folder
partialViewFolderPath = await umbracoApi.partialView.createFolder(partialViewFolderName);
// Creates child folder in parent folder
const childFolderPath = await umbracoApi.partialView.createFolder(childFolderName, partialViewFolderPath);
// Act
// Creates childOfChild folder in child folder
await umbracoApi.partialView.createFolder(childOfChildFolderName, childFolderPath);
// Assert
// Checks if the partial views folder are in the correct folders
const childOfParentData = await umbracoApi.partialView.getChildren(partialViewFolderPath);
expect(childOfParentData[0].name).toEqual(childFolderName);
const childOfChildData = await umbracoApi.partialView.getChildren(childFolderPath);
expect(childOfChildData[0].name).toEqual(childOfChildFolderName);
});
});
@@ -1,58 +0,0 @@
import {test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
test.describe('Relation type tests', () => {
const relationTypeName = 'Test Relation Type';
const objectTypeName = 'Document';
let relationTypeId = '';
let objectTypeId = '';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.relationType.ensureNameNotExists(relationTypeName);
objectTypeId = await umbracoApi.objectTypes.getIdByName(objectTypeName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.relationType.ensureNameNotExists(relationTypeName);
});
// Skip this test because there is currently no endpoint available for creating a relation type anymore.
test.skip('can create a relation type', async ({umbracoApi}) => {
// Act
relationTypeId = await umbracoApi.relationType.create(relationTypeName, false, false, objectTypeId, objectTypeId);
// Assert
expect(await umbracoApi.relationType.doesExist(relationTypeId)).toBeTruthy();
});
// Skip this test because there is currently no endpoint available for updating a relation type anymore.
test.skip('can update a relation type', async ({umbracoApi}) => {
// Arrange
const wrongRelationTypeName = 'Updated Relation Type';
relationTypeId = await umbracoApi.relationType.create(wrongRelationTypeName, false, false, objectTypeId, objectTypeId);
const relationTypeData = await umbracoApi.relationType.get(relationTypeId);
// Act
relationTypeData.name = relationTypeName;
await umbracoApi.relationType.update(relationTypeId, relationTypeData);
// Assert
expect(await umbracoApi.relationType.doesExist(relationTypeId)).toBeTruthy();
// Checks if the relation type name was updated
const updatedRelationType = await umbracoApi.relationType.get(relationTypeId);
expect(updatedRelationType.name).toEqual(relationTypeName);
});
// Skip this test because there is currently no endpoint available for deleting a relation type anymore.
test.skip('can delete a relation type', async ({umbracoApi}) => {
// Arrange
relationTypeId = await umbracoApi.relationType.create(relationTypeName, false, false, objectTypeId, objectTypeId);
expect(await umbracoApi.relationType.doesExist(relationTypeId)).toBeTruthy();
//Act
await umbracoApi.relationType.delete(relationTypeId);
// Assert
expect(await umbracoApi.relationType.doesExist(relationTypeId)).toBeFalsy();
});
});
@@ -1,66 +0,0 @@
import {test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
test.describe('Script tests', () => {
let scriptPath = '';
const scriptName = 'scriptName.js';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.script.ensureNameNotExists(scriptName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.script.ensureNameNotExists(scriptName);
});
test('can create a script', async ({umbracoApi}) => {
// Act
scriptPath = await umbracoApi.script.create(scriptName, 'test');
await umbracoApi.script.get(scriptPath);
// Assert
expect(await umbracoApi.script.doesExist(scriptPath)).toBeTruthy();
});
test('can update script name', async ({umbracoApi}) => {
// Arrange
const oldName = 'RandomScriptName.js';
await umbracoApi.script.ensureNameNotExists(oldName);
const oldScriptPath = await umbracoApi.script.create(oldName, 'test');
// Act
scriptPath = await umbracoApi.script.updateName(oldScriptPath, scriptName);
// Assert
// Checks if the content was updated for the script
const updatedScript = await umbracoApi.script.get(scriptPath);
expect(updatedScript.name).toEqual(scriptName);
});
test('can update script content', async ({umbracoApi}) => {
// Arrange
const newContent = 'BetterContent';
scriptPath = await umbracoApi.script.create(scriptName, 'test');
await umbracoApi.script.get(scriptPath);
// Act
await umbracoApi.script.updateContent(scriptPath, newContent);
// Assert
// Checks if the content was updated for the script
const updatedScript = await umbracoApi.script.get(scriptPath);
expect(updatedScript.content).toEqual(newContent);
});
test('can delete a script', async ({umbracoApi}) => {
// Arrange
scriptPath = await umbracoApi.script.create(scriptName, 'test');
expect(await umbracoApi.script.doesExist(scriptPath)).toBeTruthy();
// Act
await umbracoApi.script.delete(scriptPath);
// Assert
expect(await umbracoApi.script.doesExist(scriptPath)).toBeFalsy();
});
});
@@ -1,70 +0,0 @@
import {test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
test.describe('Script Folder tests', () => {
const scriptFolderName = 'ScriptFolder';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.script.ensureNameNotExists(scriptFolderName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.script.ensureNameNotExists(scriptFolderName);
});
test('can create a script folder', async ({umbracoApi}) => {
// Act
await umbracoApi.script.createFolder(scriptFolderName);
// Assert
expect(await umbracoApi.script.doesFolderExist(scriptFolderName)).toBeTruthy();
});
test('can delete a script folder', async ({umbracoApi}) => {
// Arrange
await umbracoApi.script.createFolder(scriptFolderName);
expect(await umbracoApi.script.doesFolderExist(scriptFolderName)).toBeTruthy();
// Act
await umbracoApi.script.deleteFolder(scriptFolderName);
// Assert
expect(await umbracoApi.script.doesFolderExist(scriptFolderName)).toBeFalsy();
});
test('can add a script folder in another folder', async ({umbracoApi}) => {
// Arrange
const childFolderName = 'childFolder';
await umbracoApi.script.createFolder(scriptFolderName);
await umbracoApi.script.createFolder(childFolderName, scriptFolderName);
// Act
const childFolder = await umbracoApi.script.getChildren(scriptFolderName);
// Assert
expect(childFolder[0].name).toEqual(childFolderName);
});
test('can add a script folder in a folder in another folder', async ({umbracoApi}) => {
// Arrange
const childFolderName = 'childFolder';
const childOfChildFolderName = 'childOfChildFolder';
const childFolderPath = scriptFolderName + '/' + childFolderName;
// Creates parent folder
await umbracoApi.script.createFolder(scriptFolderName);
// Creates child folder in parent folder
await umbracoApi.script.createFolder(childFolderName, scriptFolderName);
// Act
// Creates childOfChild folder in child folder
await umbracoApi.script.createFolder(childOfChildFolderName, childFolderPath);
// Assert
// Checks if the script folder are in the correct folders
const childOfParent = await umbracoApi.script.getChildren(scriptFolderName);
const childOfChild = await umbracoApi.script.getChildren(childFolderPath);
expect(childOfParent[0].name).toEqual(childFolderName);
expect(childOfChild[0].name).toEqual(childOfChildFolderName);
});
});
@@ -1,98 +0,0 @@
import {test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
test.describe('Stylesheet tests', () => {
const stylesheetName = 'Stylesheet.css';
const folderName = 'StyledFolder';
let stylesheetPath = '';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName);
await umbracoApi.stylesheet.ensureNameNotExists(folderName);
});
test('can create a stylesheet', async ({umbracoApi}) => {
// Act
stylesheetPath = await umbracoApi.stylesheet.create(stylesheetName, 'content');
// Assert
expect(await umbracoApi.stylesheet.doesExist(stylesheetPath)).toBeTruthy();
});
test('can update stylesheet name', async ({umbracoApi}) => {
// Arrange
const oldStylesheetName = 'RandomStylesheetName.css';
await umbracoApi.script.ensureNameNotExists(oldStylesheetName);
const oldScriptPath = await umbracoApi.stylesheet.create(oldStylesheetName, 'content');
// Act
stylesheetPath = await umbracoApi.stylesheet.updateName(oldScriptPath, stylesheetName);
// Assert
// Checks if the name was updated for the stylesheet
const updatedStylesheet = await umbracoApi.stylesheet.get(stylesheetPath);
expect(updatedStylesheet.name).toEqual(stylesheetName);
});
test('can update stylesheet content', async ({umbracoApi}) => {
// Arrange
const newContent = 'BetterContent';
stylesheetPath = await umbracoApi.stylesheet.create(stylesheetName, 'content');
// Act
await umbracoApi.stylesheet.updateContent(stylesheetPath, newContent);
// Assert
// Checks if the content was updated for the stylesheet
const updatedStylesheet = await umbracoApi.stylesheet.get(stylesheetPath);
expect(updatedStylesheet.content).toEqual(newContent);
});
test('can delete a stylesheet', async ({umbracoApi}) => {
// Arrange
stylesheetPath = await umbracoApi.stylesheet.create(stylesheetName, 'content');
expect(await umbracoApi.stylesheet.doesExist(stylesheetPath)).toBeTruthy();
// Act
await umbracoApi.stylesheet.delete(stylesheetName);
// Assert
expect(await umbracoApi.stylesheet.doesExist(stylesheetPath)).toBeFalsy();
});
test('can create a stylesheet in a folder', async ({umbracoApi}) => {
// Arrange
await umbracoApi.stylesheet.ensureNameNotExists(folderName);
const stylesheetFolderPath = await umbracoApi.stylesheet.createFolder(folderName);
// Act
stylesheetPath = await umbracoApi.stylesheet.create(stylesheetName, 'content', folderName);
// Assert
const child = await umbracoApi.stylesheet.getChildren(stylesheetFolderPath);
expect(child[0].name).toEqual(stylesheetName);
});
test('can delete a stylesheet from a folder', async ({umbracoApi}) => {
// Arrange
await umbracoApi.stylesheet.ensureNameNotExists(folderName);
const stylesheetFolderPath = await umbracoApi.stylesheet.createFolder(folderName);
stylesheetPath = await umbracoApi.stylesheet.create(stylesheetName, 'deleteMe', folderName);
// Checks if the stylesheet was created
const child = await umbracoApi.stylesheet.getChildren(stylesheetFolderPath);
// Checks if the child is in the folder
expect(child[0].name).toEqual(stylesheetName);
// Act
await umbracoApi.stylesheet.delete(stylesheetPath);
// Assert
const noChild = await umbracoApi.stylesheet.getChildren(stylesheetFolderPath);
// Checks if the children is empty
expect(noChild[0]).toEqual(undefined);
});
});
@@ -1,88 +0,0 @@
import {test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
test.describe('Stylesheet Folder tests', () => {
const stylesheetFolderName = 'StylesheetFolder';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.stylesheet.ensureNameNotExists(stylesheetFolderName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.stylesheet.ensureNameNotExists(stylesheetFolderName);
});
test('can create a stylesheet folder', async ({umbracoApi}) => {
// Act
await umbracoApi.stylesheet.createFolder(stylesheetFolderName);
// Assert
expect(await umbracoApi.stylesheet.doesFolderExist(stylesheetFolderName)).toBeTruthy();
});
test('can delete a stylesheet folder', async ({umbracoApi}) => {
// Arrange
await umbracoApi.stylesheet.createFolder(stylesheetFolderName);
expect(await umbracoApi.stylesheet.doesFolderExist(stylesheetFolderName)).toBeTruthy();
// Act
await umbracoApi.stylesheet.deleteFolder(stylesheetFolderName);
// Assert
expect(await umbracoApi.stylesheet.doesFolderExist(stylesheetFolderName)).toBeFalsy();
});
test('can add a stylesheet folder in another folder', async ({umbracoApi}) => {
// Arrange
const childFolderName = 'childFolder';
await umbracoApi.stylesheet.createFolder(stylesheetFolderName);
// Act
await umbracoApi.stylesheet.createFolder(childFolderName, stylesheetFolderName);
// Assert
const children = await umbracoApi.stylesheet.getChildren(stylesheetFolderName);
expect(children[0].name).toEqual(childFolderName);
});
test('can add a stylesheet folder in a folder in another folder', async ({umbracoApi}) => {
// Arrange
const childFolderName = 'childFolder';
const childOfChildFolderName = 'childOfChildFolder';
const childFolderPath = stylesheetFolderName + '/' + childFolderName;
// Creates parent folder
await umbracoApi.stylesheet.createFolder(stylesheetFolderName);
// Creates child folder in parent folder
await umbracoApi.stylesheet.createFolder(childFolderName, stylesheetFolderName);
// Act
// Creates childOfChild folder in child folder
await umbracoApi.stylesheet.createFolder(childOfChildFolderName, childFolderPath);
// Assert
// Checks if the stylesheet folder are in the correct folders
const childOfParent = await umbracoApi.stylesheet.getChildren(stylesheetFolderName);
const childOfChild = await umbracoApi.stylesheet.getChildren(childFolderPath);
expect(childOfParent[0].name).toEqual(childFolderName);
expect(childOfChild[0].name).toEqual(childOfChildFolderName);
});
test('can delete a stylesheet folder from another folder', async ({umbracoApi}) => {
// Arrange
const childFolderName = 'childFolder';
const stylesheetParentFolderPath = await umbracoApi.stylesheet.createFolder(stylesheetFolderName);
const stylesheetChildFolderPath = await umbracoApi.stylesheet.createFolder(childFolderName, stylesheetFolderName);
// Checks if a child exists in the parent folder with the name
const child = await umbracoApi.stylesheet.getChildren(stylesheetParentFolderPath);
expect(child[0].name).toEqual(childFolderName);
expect(child[0].isFolder).toBe(true);
expect(await umbracoApi.stylesheet.doesFolderExist(stylesheetChildFolderPath)).toBeTruthy();
// Act
await umbracoApi.stylesheet.deleteFolder(stylesheetChildFolderPath);
// Assert
const noChild = await umbracoApi.stylesheet.getChildren(stylesheetParentFolderPath);
expect(noChild[0]).toBe(undefined);
});
});
@@ -1,54 +0,0 @@
import {AliasHelper, test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
test.describe('Template tests', () => {
let templateId = '';
const templateName = 'TemplateTester';
const templateAlias = AliasHelper.toAlias(templateName);
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.template.ensureNameNotExists(templateName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.template.ensureNameNotExists(templateName);
})
test('can create a template', async ({umbracoApi}) => {
// Act
templateId = await umbracoApi.template.create(templateName, templateAlias, 'Template Stuff');
// Assert
expect(await umbracoApi.template.doesExist(templateId)).toBeTruthy();
});
test('can update a template', async ({umbracoApi}) => {
// Arrange
const newTemplateAlias = 'betterAlias';
templateId = await umbracoApi.template.create(templateName, templateAlias, 'Template Stuff');
const templateData = await umbracoApi.template.get(templateId);
// Act
// Updates the template
templateData.alias = newTemplateAlias;
await umbracoApi.template.update(templateId, templateData);
// Assert
expect(await umbracoApi.template.doesExist(templateId)).toBeTruthy();
// Checks if the template alias was updated
const updatedTemplate = await umbracoApi.template.get(templateId);
expect(updatedTemplate.alias).toEqual(newTemplateAlias);
});
test('can delete template', async ({umbracoApi}) => {
// Arrange
templateId = await umbracoApi.template.create(templateName, templateAlias, 'More Template Stuff');
expect(await umbracoApi.template.doesExist(templateId)).toBeTruthy();
// Act
await umbracoApi.template.delete(templateId);
// Assert
expect(await umbracoApi.template.doesExist(templateId)).toBeFalsy();
});
});
@@ -1,38 +0,0 @@
import {test} from "@umbraco/playwright-testhelpers";
import {expect} from "@playwright/test";
import * as crypto from 'crypto';
test.describe('Temporary File tests', () => {
const temporaryFileId = crypto.randomUUID();
const fileName = 'Umbraco.png';
const mimeType = 'image/png';
const filePath = './fixtures/mediaLibrary/Umbraco.png';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.temporaryFile.delete(temporaryFileId);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.temporaryFile.delete(temporaryFileId);
});
test('can create temporary file', async ({umbracoApi}) => {
// Act
await umbracoApi.temporaryFile.create(temporaryFileId, fileName, mimeType, filePath);
// Assert
expect(await umbracoApi.temporaryFile.doesExist(temporaryFileId)).toBeTruthy();
});
test('can delete temporary file', async ({umbracoApi}) => {
// Arrange
await umbracoApi.temporaryFile.create(temporaryFileId, fileName, mimeType, filePath);
expect(await umbracoApi.temporaryFile.get(temporaryFileId)).toBeTruthy();
// Act
await umbracoApi.temporaryFile.delete(temporaryFileId);
// Assert
expect(await umbracoApi.temporaryFile.doesExist(temporaryFileId)).toBeFalsy();
});
});
@@ -1,60 +0,0 @@
import {test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
test.describe('User Tests', () => {
let userId = '';
let userGroupId = '';
const userEmail = 'user@acceptance.test';
const userName = 'UserTests';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.user.ensureNameNotExists(userName);
const userGroupData = await umbracoApi.userGroup.getByName('Writers');
userGroupId = userGroupData.id;
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.user.ensureNameNotExists(userName);
});
test('can create a user', async ({umbracoApi}) => {
// Act
userId = await umbracoApi.user.createDefaultUser(userName, userEmail, [userGroupId]);
// Assert
expect(await umbracoApi.user.doesExist(userId)).toBeTruthy();
});
test('can update a user', async ({umbracoApi}) => {
// Arrange
const anotherUserGroup = await umbracoApi.userGroup.getByName("Translators");
userId = await umbracoApi.user.createDefaultUser(userName, userEmail, [userGroupId]);
const userData = await umbracoApi.user.get(userId);
const newUserGroupData = [
userGroupId,
anotherUserGroup.id
];
userData.userGroupIds = newUserGroupData;
// Act
await umbracoApi.user.update(userId, userData);
// Assert
await umbracoApi.user.doesExist(userId);
// Checks if the user was updated with another userGroupID
const updatedUser = await umbracoApi.user.get(userId);
expect(updatedUser.userGroupIds.toString()).toEqual(newUserGroupData.toString());
});
test('can delete a user', async ({umbracoApi}) => {
// Arrange
userId = await umbracoApi.user.createDefaultUser(userName, userEmail, [userGroupId]);
expect(await umbracoApi.user.doesExist(userId)).toBeTruthy();
// Act
await umbracoApi.user.delete(userId);
// Assert
expect(await umbracoApi.user.doesExist(userId)).toBeFalsy();
});
});
@@ -1,61 +0,0 @@
import {test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
import * as crypto from 'crypto';
test.describe('User Avatar Tests', () => {
// User
let userId = '';
let userGroupId = '';
const userEmail = 'userAvatar@acceptance.test';
const userName = 'UserAvatarTests';
// Avatar
const avatarFileId = crypto.randomUUID();
const avatarName = 'Umbraco.png';
const mimeType = 'image/png';
const avatarFilePath = './fixtures/mediaLibrary/Umbraco.png';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.user.ensureNameNotExists(userName);
await umbracoApi.temporaryFile.delete(avatarFileId);
const userGroupData = await umbracoApi.userGroup.getByName('Writers');
userGroupId = userGroupData.id;
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.user.ensureNameNotExists(userName);
await umbracoApi.temporaryFile.delete(avatarFileId);
});
test('can add an avatar to a user', async ({umbracoApi}) => {
// Arrange
userId = await umbracoApi.user.createDefaultUser(userName, userEmail, userGroupId);
await umbracoApi.temporaryFile.create(avatarFileId, avatarName, mimeType, avatarFilePath);
// Act
await umbracoApi.user.addAvatar(userId, avatarFileId);
// Assert
// Checks if the avatar was added to the user
const userDataWithAvatar = await umbracoApi.user.get(userId);
expect(userDataWithAvatar.avatarUrls.length !== 0).toBeTruthy();
});
test('can remove an avatar from a user', async ({umbracoApi}) => {
// Arrange
userId = await umbracoApi.user.createDefaultUser(userName, userEmail, userGroupId);
await umbracoApi.temporaryFile.create(avatarFileId, avatarName, mimeType, avatarFilePath);
await umbracoApi.user.addAvatar(userId, avatarFileId);
// Checks if the avatar was added to the user
const userDataWithAvatar = await umbracoApi.user.get(userId);
expect(userDataWithAvatar.avatarUrls.length !== 0).toBeTruthy();
// Act
await umbracoApi.user.removeAvatar(userId);
// Assert
// Checks if the avatar was removed from the user
const userDataWithoutAvatar = await umbracoApi.user.get(userId);
expect(userDataWithoutAvatar.avatarUrls.length).toEqual(0);
});
});
@@ -1,48 +0,0 @@
import {test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
test.describe('User Group Tests', () => {
let userGroupId = '';
const userGroupName = 'UserGroupTest';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.userGroup.ensureNameNotExists(userGroupName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.userGroup.delete(userGroupId);
});
test('can create a user group', async ({umbracoApi}) => {
// Act
userGroupId = await umbracoApi.userGroup.createEmptyUserGroup(userGroupName);
// Assert
expect(await umbracoApi.userGroup.doesExist(userGroupId)).toBeTruthy();
});
test('can update a user group', async ({umbracoApi}) => {
// Arrange
userGroupId = await umbracoApi.userGroup.createEmptyUserGroup('WrongUserGroupName');
const userGroupData = await umbracoApi.userGroup.get(userGroupId);
userGroupData.name = userGroupName;
// Act
await umbracoApi.userGroup.update(userGroupId, userGroupData);
// Assert
const updatedUserGroupData = await umbracoApi.userGroup.get(userGroupId);
expect(updatedUserGroupData.name).toEqual(userGroupName);
});
test('can delete a user group', async ({umbracoApi}) => {
// Arrange
userGroupId = await umbracoApi.userGroup.createEmptyUserGroup(userGroupName);
// Act
await umbracoApi.userGroup.delete(userGroupId);
// Assert
expect(await umbracoApi.userGroup.doesExist(userGroupId)).toBeFalsy();
});
});
@@ -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);
});
@@ -0,0 +1,134 @@
import {expect} from '@playwright/test';
import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers';
// DocumentType
const documentTypeName = 'TestDocumentType';
let documentTypeId = '';
const groupName = 'TestGroup';
// Content
const contentName = 'TestContent';
let contentId = '';
// Property Value
const wrongPropertyValue = 'This is a test with wrong value**';
const correctPropertyValue = 'Test';
// ElementTypes
// TextString Element Type (for Block List)
const textStringElementGroupName = 'TextStringElementGroup';
const textStringElementTypeName = 'TestElementWithTextString';
const textStringElementRegex = '^[a-zA-Z0-9]*$';
let textStringElementTypeId = '';
// Area Element Type (for Block Grid)
const areaElementTypeName = 'TestElementArea';
const areaAlias = 'testArea';
let areaElementTypeId = '';
// Rich Text Editor Element Type (for Block Grid)
const richTextEditorElementGroupName = 'RichTextEditorElementGroup';
const richTextEditorElementTypeName = 'RichTextEditorTestElement';
let richTextEditorElementTypeId = '';
// Block List Element Type
const blockListElementTypeName = 'BlockListElement';
const blockListGroupName = 'BlockListGroup';
let blockListElementTypeId = '';
// DataTypes
const blockGridDataTypeName = 'TestBlockGridEditor';
const blockListDataTypeName = 'TestBlockListEditor';
const textStringElementDataTypeName = 'Textstring';
const richTextDataTypeName = 'Rich Text Editor';
let blockListDataTypeId = '';
let blockGridDataTypeId = '';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName);
await umbracoApi.documentType.ensureNameNotExists(areaElementTypeName);
await umbracoApi.documentType.ensureNameNotExists(richTextEditorElementTypeName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName);
await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName);
await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName);
await umbracoApi.documentType.ensureNameNotExists(areaElementTypeName);
await umbracoApi.documentType.ensureNameNotExists(richTextEditorElementTypeName);
await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName);
await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName);
await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName);
});
test('can update property value nested in a block grid area with an RTE with a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => {
test.slow();
// Arrange
// ElementType with Textstring And REGEX only accept letters and numbers
const textStringElementDataType = await umbracoApi.dataType.getByName(textStringElementDataTypeName);
textStringElementTypeId = await umbracoApi.documentType.createElementTypeWithRegexValidation(textStringElementTypeName, textStringElementGroupName, textStringElementDataTypeName, textStringElementDataType.id, textStringElementRegex);
// Block List Editor with Textstring
blockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListDataTypeName, textStringElementTypeId);
// ElementType with Block List Editor
blockListElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockListElementTypeName, blockListGroupName, blockListDataTypeName, blockListDataTypeId);
// Rich Text Editor in an ElementType, with a Block(Element Type), the block contains a Block List Editor
const richTextEditorId = await umbracoApi.dataType.createRichTextEditorWithABlock(richTextDataTypeName, blockListElementTypeId);
richTextEditorElementTypeId = await umbracoApi.documentType.createDefaultElementType(richTextEditorElementTypeName, richTextEditorElementGroupName, richTextDataTypeName, richTextEditorId);
// ElementType Area that is Empty
areaElementTypeId = await umbracoApi.documentType.createEmptyElementType(areaElementTypeName);
// Block Grid with 2 blocks, one with RTE and Inline, and one with areas
blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockWithInlineEditingModeAndABlockWithAnArea(blockGridDataTypeName, richTextEditorElementTypeId, true, areaElementTypeId, areaAlias);
// Document Type with the following
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridDataTypeName, blockGridDataTypeId, groupName);
// Creates Content
contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickAddBlockGridElementWithName(areaElementTypeName);
await umbracoUi.content.clickSelectBlockElementWithName(areaElementTypeName);
await umbracoUi.content.clickAddBlockGridElementWithName(richTextEditorElementTypeName);
await umbracoUi.content.clickExactLinkWithName(richTextEditorElementTypeName);
await umbracoUi.content.clickInsertBlockButton();
await umbracoUi.content.clickExactLinkWithName(blockListElementTypeName);
await umbracoUi.content.clickAddBlockGridElementWithName(textStringElementTypeName);
await umbracoUi.content.clickExactLinkWithName(textStringElementTypeName);
// Enter text in the textstring block that won't match regex
await umbracoUi.content.enterPropertyValue(textStringElementDataTypeName, wrongPropertyValue);
await umbracoUi.content.clickCreateButtonForModalWithElementTypeNameAndGroupName(textStringElementTypeName, textStringElementGroupName);
await umbracoUi.content.clickCreateButtonForModalWithElementTypeNameAndGroupName(blockListElementTypeName, blockListGroupName);
await umbracoUi.content.clickCreateButtonForModalWithElementTypeNameAndGroupName(richTextEditorElementTypeName, richTextEditorElementGroupName);
await umbracoUi.content.clickSaveAndPublishButton();
// Checks that the error notification is shown since the textstring block has the wrong value
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved, true, true);
await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished, true, true);
// Updates the textstring block with the correct value
await umbracoUi.waitForTimeout(1000);
await umbracoUi.content.clickBlockElementWithName(blockListElementTypeName);
await umbracoUi.content.clickEditBlockListEntryWithName(textStringElementTypeName);
await umbracoUi.content.enterPropertyValue(textStringElementDataTypeName, correctPropertyValue);
await umbracoUi.content.clickUpdateButtonForModalWithElementTypeNameAndGroupName(textStringElementTypeName, textStringElementGroupName);
await umbracoUi.content.clickUpdateButtonForModalWithElementTypeNameAndGroupName(blockListElementTypeName, blockListGroupName);
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved, true, true);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published, true, true);
// Checks if published
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe('Published');
// Checks if the textstring block has the correct value after reloading the page
await umbracoUi.reloadPage();
// Waits to make sure the page has loaded
await umbracoUi.waitForTimeout(2000);
await umbracoUi.content.clickBlockElementWithName(blockListElementTypeName);
// Needs to wait to make sure it has loaded
await umbracoUi.waitForTimeout(2000);
await umbracoUi.content.clickEditBlockListEntryWithName(textStringElementTypeName);
await umbracoUi.content.doesPropertyContainValue(textStringElementDataTypeName, correctPropertyValue);
});
@@ -0,0 +1,161 @@
import {AliasHelper, ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
// Content Name
const contentName = 'ContentName';
// Document Type
const documentTypeName = 'DocumentTypeName';
let documentTypeId = null;
const documentTypeGroupName = 'DocumentGroup';
// Block Grid
const blockGridDataTypeName = 'BlockGridName';
let blockGridDataTypeId = null;
// Text String
const textStringElementTypeName = 'TextStringElementName';
let textStringElementTypeId = null;
let textStringGroupName = 'TextGroup';
const textStringDataTypeName = 'Textstring';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName);
});
test('can publish a block grid editor with a rich text editor', async ({umbracoApi, umbracoUi}) => {
// Arrange
const richTextEditorValue = 'Hello World';
const expectedRichTextEditorOutputValue = '<p>Hello World</p>';
const richTextDataTypeName = 'RichTextDataTypeName';
const richTextElementTypeName = 'RichTextElementName';
const richTextElementGroupName = 'RichTextElementGroupName';
await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName);
await umbracoApi.documentType.ensureNameNotExists(richTextElementTypeName);
const richTextEditorDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(richTextDataTypeName);
const richTextElementTypeId = await umbracoApi.documentType.createDefaultElementType(richTextElementTypeName, richTextElementGroupName, richTextDataTypeName, richTextEditorDataTypeId);
blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridDataTypeName, richTextElementTypeId, true);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridDataTypeName, blockGridDataTypeId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickAddBlockElementButton();
await umbracoUi.content.clickBlockCardWithName(richTextElementTypeName, true);
await umbracoUi.content.enterRTETipTapEditor(richTextEditorValue);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
// Asserts that the value in the RTE is as expected
const documentData = await umbracoApi.document.getByName(contentName);
const documentValues = documentData.values.find(value => value.alias === AliasHelper.toAlias(blockGridDataTypeName));
expect(documentValues.value.contentData[0].values[0].value.markup).toContain(expectedRichTextEditorOutputValue);
// Clean
await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName);
await umbracoApi.documentType.ensureNameNotExists(richTextElementTypeName);
});
test('can publish a block grid editor with a block list editor', async ({umbracoApi, umbracoUi}) => {
// Arrange
const textStringValue = 'Hello World';
const blockListDataTypeName = 'BlockListName';
const blockListElementTypeName = 'BlockListElementName';
const blockListElementGroupName = 'BlockListElementGroupName';
await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName);
await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName);
await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName);
const textStringDataType = await umbracoApi.dataType.getByName(textStringDataTypeName);
textStringElementTypeId = await umbracoApi.documentType.createDefaultElementType(textStringElementTypeName, textStringGroupName, textStringDataTypeName, textStringDataType.id);
const blockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListDataTypeName, textStringElementTypeId);
const blockListElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockListElementTypeName, blockListElementGroupName, blockListDataTypeName, blockListDataTypeId);
blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridDataTypeName, blockListElementTypeId, true);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridDataTypeName, blockGridDataTypeId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickAddBlockElementButton();
await umbracoUi.content.clickBlockCardWithName(blockListElementTypeName, true);
await umbracoUi.content.clickAddBlockWithNameButton(textStringElementTypeName);
await umbracoUi.content.clickBlockCardWithName(textStringElementTypeName, true);
await umbracoUi.content.enterTextstring(textStringValue);
await umbracoUi.content.clickCreateForModalWithHeadline('Add ' + textStringElementTypeName);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
// Asserts that the value in the BlockList is as expected
const documentData = await umbracoApi.document.getByName(contentName);
const documentValues = documentData.values.find(value => value.alias === AliasHelper.toAlias(blockGridDataTypeName));
expect(documentValues.value.contentData[0].values[0].value.contentData[0].values[0].value).toContain(textStringValue);
// Clean
await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName);
await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName);
await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName);
});
test('can publish a block grid editor with a block grid editor', async ({umbracoApi, umbracoUi}) => {
// Arrange
const textStringValue = 'Hello World';
const secondBlockGridDataTypeName = 'SecondBlockGridDataTypeName';
const blockGridElementTypeName = 'BlockGridElementTypeName';
const blockGridElementGroupName = 'BlockGridElementGroupName';
await umbracoApi.dataType.ensureNameNotExists(secondBlockGridDataTypeName);
await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName);
await umbracoApi.documentType.ensureNameNotExists(blockGridElementTypeName);
const textStringDataType = await umbracoApi.dataType.getByName(textStringDataTypeName);
textStringElementTypeId = await umbracoApi.documentType.createDefaultElementType(textStringElementTypeName, textStringGroupName, textStringDataTypeName, textStringDataType.id);
const secondBlockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(secondBlockGridDataTypeName, textStringElementTypeId);
const blockGridElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockGridElementTypeName, blockGridElementGroupName, secondBlockGridDataTypeName, secondBlockGridDataTypeId);
blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridDataTypeName, blockGridElementTypeId, true);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridDataTypeName, blockGridDataTypeId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickAddBlockElementButton();
await umbracoUi.content.clickBlockCardWithName(blockGridElementTypeName, true);
await umbracoUi.content.clickAddBlockWithNameButton(textStringElementTypeName);
await umbracoUi.content.clickBlockCardWithName(textStringElementTypeName, true);
await umbracoUi.content.enterTextstring(textStringValue);
await umbracoUi.content.clickCreateForModalWithHeadline('Add ' + textStringElementTypeName);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
// Asserts that the value in the BlockGrid is as expected
const documentData = await umbracoApi.document.getByName(contentName);
const documentValues = documentData.values.find(value => value.alias === AliasHelper.toAlias(blockGridDataTypeName));
expect(documentValues.value.contentData[0].values[0].value.contentData[0].values[0].value).toContain(textStringValue);
// Clean
await umbracoApi.dataType.ensureNameNotExists(secondBlockGridDataTypeName);
await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName);
await umbracoApi.documentType.ensureNameNotExists(blockGridElementTypeName);
});
@@ -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);
});
});
@@ -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);
});
@@ -0,0 +1,161 @@
import {AliasHelper, ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
// Content Name
const contentName = 'ContentName';
// Document Type
const documentTypeName = 'DocumentTypeName';
let documentTypeId = null;
const documentTypeGroupName = 'DocumentGroup';
// Block List
const blockListDataTypeName = 'BlockListName';
let blockListDataTypeId = null;
// Text String
const textStringElementTypeName = 'TextStringElementName';
let textStringElementTypeId = null;
let textStringGroupName = 'TextGroup';
const textStringDataTypeName = 'Textstring';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName);
});
test('can publish a block list editor with a rich text editor', async ({umbracoApi, umbracoUi}) => {
// Arrange
const richTextEditorValue = 'Hello World';
const expectedRichTextEditorOutputValue = '<p>Hello World</p>';
const richTextDataTypeName = 'RichTextName';
const richTextElementTypeName = 'RichTextElementName';
const richTextElementGroupName = 'RTEElementGroup';
await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName);
await umbracoApi.documentType.ensureNameNotExists(richTextElementTypeName);
const richTextEditorDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(richTextDataTypeName);
const richTextElementTypeId = await umbracoApi.documentType.createDefaultElementType(richTextElementTypeName, richTextElementGroupName, richTextDataTypeName, richTextEditorDataTypeId);
blockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListDataTypeName, richTextElementTypeId);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockListDataTypeName, blockListDataTypeId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickAddBlockElementButton();
await umbracoUi.content.clickBlockCardWithName(richTextElementTypeName, true);
await umbracoUi.content.enterRTETipTapEditor(richTextEditorValue);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
// Asserts that the value in the RTE is as expected
const documentData = await umbracoApi.document.getByName(contentName);
const documentRichTextValues = documentData.values[0].value.contentData[0].values.find(value => value.alias === AliasHelper.toAlias(richTextDataTypeName));
expect(documentRichTextValues.value.markup).toContain(expectedRichTextEditorOutputValue);
// Clean
await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName);
await umbracoApi.documentType.ensureNameNotExists(richTextElementTypeName);
});
test('can publish a block list editor with a block grid editor', async ({umbracoApi, umbracoUi}) => {
// Arrange
const textStringValue = 'Hello World';
const blockGridDataTypeName = 'BlockGridDataTypeName';
const blockGridElementTypeName = 'BlockGridElementName';
const blockGridElementGroupName = 'GridElementGroup';
await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName);
await umbracoApi.documentType.ensureNameNotExists(blockGridElementTypeName);
await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName);
const textStringDataType = await umbracoApi.dataType.getByName(textStringDataTypeName);
textStringElementTypeId = await umbracoApi.documentType.createDefaultElementType(textStringElementTypeName, textStringGroupName, textStringDataTypeName, textStringDataType.id);
const blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridDataTypeName, textStringElementTypeId);
const blockGridElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockGridElementTypeName, blockGridElementGroupName, blockGridDataTypeName, blockGridDataTypeId);
blockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListDataTypeName, blockGridElementTypeId);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockListDataTypeName, blockListDataTypeId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickAddBlockElementButton();
await umbracoUi.content.clickBlockCardWithName(blockGridElementTypeName, true);
await umbracoUi.content.clickAddBlockWithNameButton(textStringElementTypeName);
await umbracoUi.content.clickBlockCardWithName(textStringElementTypeName, true);
await umbracoUi.content.enterTextstring(textStringValue);
await umbracoUi.content.clickCreateForModalWithHeadline('Add ' + textStringElementTypeName);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
// Asserts that the value in the BlockGrid is as expected
const documentData = await umbracoApi.document.getByName(contentName);
const documentValues = documentData.values.find(value => value.alias === AliasHelper.toAlias(blockListDataTypeName));
expect(documentValues.value.contentData[0].values[0].value.contentData[0].values[0].value).toContain(textStringValue);
// Clean
await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName);
await umbracoApi.documentType.ensureNameNotExists(blockGridElementTypeName);
await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeId);
});
test('can publish a block list editor with a block list editor', async ({umbracoApi, umbracoUi}) => {
// Arrange
const textStringValue = 'Hello World';
const secondBlockListDataTypeName = 'SecondBlockListName';
const blockListElementTypeName = 'BlockListElementName';
const blockListElementGroupName = 'ListElementGroup';
await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName);
await umbracoApi.dataType.ensureNameNotExists(secondBlockListDataTypeName);
await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName);
const textStringDataType = await umbracoApi.dataType.getByName(textStringDataTypeName);
textStringElementTypeId = await umbracoApi.documentType.createDefaultElementType(textStringElementTypeName, textStringGroupName, textStringDataTypeName, textStringDataType.id);
const secondBlockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(secondBlockListDataTypeName, textStringElementTypeId);
const blockListElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockListElementTypeName, blockListElementGroupName, secondBlockListDataTypeName, secondBlockListDataTypeId);
blockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListDataTypeName, blockListElementTypeId);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockListDataTypeName, blockListDataTypeId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickAddBlockElementButton();
await umbracoUi.content.clickBlockCardWithName(blockListElementTypeName, true);
await umbracoUi.content.clickAddBlockWithNameButton(textStringElementTypeName);
await umbracoUi.content.clickBlockCardWithName(textStringElementTypeName, true);
await umbracoUi.content.enterTextstring(textStringValue);
await umbracoUi.content.clickCreateForModalWithHeadline('Add ' + textStringElementTypeName);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
// Asserts that the value in the BlockList is as expected
const documentData = await umbracoApi.document.getByName(contentName);
const documentValues = documentData.values.find(value => value.alias === AliasHelper.toAlias(blockListDataTypeName));
expect(documentValues.value.contentData[0].values[0].value.contentData[0].values[0].value).toContain(textStringValue);
// Clean
await umbracoApi.dataType.ensureNameNotExists(secondBlockListDataTypeName);
await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName);
await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName);
});
@@ -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}) => {
@@ -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]]);
});
@@ -55,6 +55,7 @@ test('can create child content in a collection', async ({umbracoApi, umbracoUi})
await umbracoUi.content.chooseDocumentType(childDocumentTypeName);
await umbracoUi.content.enterContentName(firstChildContentName);
await umbracoUi.content.clickSaveButton();
await umbracoUi.content.isSuccessNotificationVisible();
// Assert
const childData = await umbracoApi.document.getChildren(contentId);
@@ -87,6 +88,7 @@ test('can create multiple child nodes in a collection', async ({umbracoApi, umbr
await umbracoUi.content.chooseDocumentType(childDocumentTypeName);
await umbracoUi.content.enterContentName(secondChildContentName);
await umbracoUi.content.clickSaveButton();
await umbracoUi.content.isSuccessNotificationVisible();
// Assert
const childData = await umbracoApi.document.getChildren(contentId);
@@ -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]]);
});
@@ -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.created);
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([]);
});
@@ -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]);
});
@@ -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([]);
});
});
@@ -84,7 +84,7 @@ test('cannot input the text that exceeds the allowed amount of characters', asyn
// Arrange
const maxChars = 100;
const textExceedMaxChars = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam mattis porttitor orci id cursus. Nulla';
const warningMessage = 'This field exceeds the allowed amount of characters';
const warningMessage = 'The string length exceeds the maximum length of ' + maxChars + ' characters, 1 too many.';
const dataTypeId = await umbracoApi.dataType.createTextareaDataType(customDataTypeName, maxChars);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
@@ -14,7 +14,7 @@ test.beforeEach(async ({umbracoApi, umbracoUi}) => {
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
@@ -84,7 +84,7 @@ test('cannot input the text that exceeds the allowed amount of characters', asyn
// Arrange
const maxChars = 20;
const textExceedMaxChars = 'Lorem ipsum dolor sit';
const warningMessage = 'This field exceeds the allowed amount of characters';
const warningMessage = 'The string length exceeds the maximum length of ' + maxChars + ' characters, 1 too many.';
const dataTypeId = await umbracoApi.dataType.createTextstringDataType(customDataTypeName, maxChars);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
@@ -84,136 +84,4 @@ test('can publish content with RTE Tiptap property editor', async ({umbracoApi,
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe(expectedState);
expect(contentData.values[0].value.markup).toEqual('<p>' + inputText + '</p>');
});
test.fixme('can add a media in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => {
// Arrange
const iconTitle = 'Media Picker';
const imageName = 'Test Image For Content';
await umbracoApi.media.ensureNameNotExists(imageName);
await umbracoApi.media.createDefaultMediaWithImage(imageName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle);
// fix this
await umbracoUi.content.selectMediaWithName(imageName);
await umbracoUi.content.clickChooseModalButton();
await umbracoUi.content.clickMediaCaptionAltTextModalSubmitButton();
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].value.markup).toContain('<img');
expect(contentData.values[0].value.markup).toContain(imageName);
// Clean
await umbracoApi.media.ensureNameNotExists(imageName);
});
test('can add a video in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => {
// Arrange
const iconTitle = 'Embed';
const videoURL = 'https://www.youtube.com/watch?v=Yu29dE-0OoI';
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle);
await umbracoUi.content.enterEmbeddedURL(videoURL);
await umbracoUi.content.clickEmbeddedRetrieveButton();
await umbracoUi.content.waitForEmbeddedPreviewVisible();
await umbracoUi.content.clickEmbeddedMediaModalConfirmButton();
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].value.markup).toContain('data-embed-url');
expect(contentData.values[0].value.markup).toContain(videoURL);
});
test('cannot submit an empty link in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => {
// Arrange
const iconTitle = 'Link';
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle);
await umbracoUi.content.clickManualLinkButton();
await umbracoUi.content.enterLink('');
await umbracoUi.content.enterAnchorOrQuerystring('');
await umbracoUi.content.enterLinkTitle('');
await umbracoUi.content.clickAddButton();
// Assert
await umbracoUi.content.isTextWithMessageVisible(ConstantHelper.validationMessages.emptyLinkPicker);
});
// TODO: Remove skip when the front-end ready. Currently it still accept the empty link with an anchor or querystring
// Issue link: https://github.com/umbraco/Umbraco-CMS/issues/17411
test.skip('cannot submit an empty URL with an anchor or querystring in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => {
// Arrange
const iconTitle = 'Link';
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle);
await umbracoUi.content.clickManualLinkButton();
await umbracoUi.content.enterLink('');
await umbracoUi.content.enterAnchorOrQuerystring('#value');
await umbracoUi.content.clickAddButton();
// Assert
await umbracoUi.content.isTextWithMessageVisible(ConstantHelper.validationMessages.emptyLinkPicker);
});
// TODO: Remove skip when the front-end ready. Currently it is impossible to link to unpublished document
// Issue link: https://github.com/umbraco/Umbraco-CMS/issues/17974
test.skip('can insert a link to an unpublished document in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => {
// Arrange
const iconTitle = 'Link';
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
// Create a document to link
const documentTypeForLinkedDocumentName = 'TestDocumentType';
const documentTypeForLinkedDocumentId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeForLinkedDocumentName);
const linkedDocumentName = 'LinkedDocument';
await umbracoApi.document.createDefaultDocument(linkedDocumentName, documentTypeForLinkedDocumentId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle);
await umbracoUi.content.clickDocumentLinkButton();
await umbracoUi.content.selectLinkByName(linkedDocumentName);
await umbracoUi.content.clickButtonWithName('Choose');
await umbracoUi.content.clickAddButton();
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.isSuccessNotificationVisible();
// Clean
await umbracoApi.documentType.ensureNameNotExists(documentTypeForLinkedDocumentName);
await umbracoApi.document.ensureNameNotExists(linkedDocumentName);
});
@@ -0,0 +1,165 @@
import {AliasHelper, ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
// Content Name
const contentName = 'ContentName';
// Document Type
const documentTypeName = 'DocumentTypeName';
let documentTypeId = null;
const documentTypeGroupName = 'DocumentGroup';
// Rich Text Editor
const richTextDataTypeName = 'RichTextDataType';
let richTextDataTypeId = null;
// Text String
const textStringElementTypeName = 'TextStringElementName';
let textStringElementTypeId = null;
let textStringGroupName = 'TextGroup';
const textStringDataTypeName = 'Textstring';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName);
});
test('can publish a rich text editor with a rich text editor', async ({umbracoApi, umbracoUi}) => {
// Arrange
const richTextEditorValue = 'Hello First World';
const secondRichTextEditorValue = 'Hello Second World';
const expectedRichTextEditorOutputValue = '<p>' + richTextEditorValue + '</p>';
const secondExpectedRichTextEditorOutputValue = '<p>' + secondRichTextEditorValue + '</p>';
const secondRichTextDataTypeName = 'SecondRichTextName';
const richTextElementTypeName = 'RichTextElementName';
const richTextElementGroupName = 'RichTextElementGroupName';
await umbracoApi.dataType.ensureNameNotExists(secondRichTextDataTypeName);
await umbracoApi.documentType.ensureNameNotExists(richTextElementGroupName);
const secondRichTextEditorDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(secondRichTextDataTypeName);
const richTextElementTypeId = await umbracoApi.documentType.createDefaultElementType(richTextElementTypeName, richTextElementGroupName, secondRichTextDataTypeName, secondRichTextEditorDataTypeId);
richTextDataTypeId = await umbracoApi.dataType.createRichTextEditorWithABlock(richTextDataTypeName, richTextElementTypeId);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, richTextDataTypeName, richTextDataTypeId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.enterRTETipTapEditor(richTextEditorValue);
await umbracoUi.content.clickInsertBlockButton();
await umbracoUi.content.clickBlockCardWithName(richTextElementTypeName, true);
await umbracoUi.content.enterRTETipTapEditorWithName(AliasHelper.toAlias(secondRichTextDataTypeName), secondRichTextEditorValue);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
// Asserts that the value in the RTE is as expected
const documentData = await umbracoApi.document.getByName(contentName);
const documentValues = documentData.values.find(value => value.alias === AliasHelper.toAlias(richTextDataTypeName));
// Value in the first RTE
expect(documentValues.value.markup).toContain(expectedRichTextEditorOutputValue);
// Value in the second RTE
const secondRTEInBlock = documentValues.value.blocks.contentData[0].values.find(value => value.alias === AliasHelper.toAlias(secondRichTextDataTypeName));
expect(secondRTEInBlock.value.markup).toContain(secondExpectedRichTextEditorOutputValue);
// Clean
await umbracoApi.documentType.ensureNameNotExists(richTextElementGroupName);
});
test('can publish a rich text editor with a block grid editor', async ({umbracoApi, umbracoUi}) => {
// Arrange
const textStringValue = 'Hello World';
const blockGridDataTypeName = 'BlockGridDataTypeName';
const blockGridElementTypeName = 'BlockGridElementTypeName';
const blockGridElementGroupName = 'BlockGridElementGroupName';
await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName);
await umbracoApi.documentType.ensureNameNotExists(blockGridElementTypeName);
await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName);
const textStringDataType = await umbracoApi.dataType.getByName(textStringDataTypeName);
textStringElementTypeId = await umbracoApi.documentType.createDefaultElementType(textStringElementTypeName, textStringGroupName, textStringDataTypeName, textStringDataType.id);
const blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockWithInlineEditingMode(blockGridDataTypeName, textStringElementTypeId, true);
const blockGridElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockGridElementTypeName, blockGridElementGroupName, blockGridDataTypeName, blockGridDataTypeId);
richTextDataTypeId = await umbracoApi.dataType.createRichTextEditorWithABlock(richTextDataTypeName, blockGridElementTypeId);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, richTextDataTypeName, richTextDataTypeId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickInsertBlockButton();
await umbracoUi.content.clickBlockCardWithName(blockGridElementTypeName, true);
await umbracoUi.content.clickAddBlockWithNameButton(textStringElementTypeName);
await umbracoUi.content.clickBlockCardWithName(textStringElementTypeName, true);
await umbracoUi.content.enterTextstring(textStringValue);
await umbracoUi.content.clickCreateForModalWithHeadline('Add ' + textStringElementTypeName);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
// Asserts that the value in the BlockGrid is as expected
const documentData = await umbracoApi.document.getByName(contentName);
expect(documentData.values[0].value.blocks.contentData[0].values[0].value.contentData[0].values[0].value).toContain(textStringValue);
// Clean
await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName);
await umbracoApi.documentType.ensureNameNotExists(blockGridElementTypeName);
await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName);
});
test('can publish a rich text editor with a block list editor', async ({umbracoApi, umbracoUi}) => {
// Arrange
const textStringValue = 'Hello World';
const blockListDataTypeName = 'BlockListName';
const blockListElementTypeName = 'BlockListElementName';
const blockListElementGroupName = 'BlockListGroupName';
await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName);
await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName);
await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName);
const textStringDataType = await umbracoApi.dataType.getByName(textStringDataTypeName);
textStringElementTypeId = await umbracoApi.documentType.createDefaultElementType(textStringElementTypeName, textStringGroupName, textStringDataTypeName, textStringDataType.id);
const blockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListDataTypeName, textStringElementTypeId);
const blockListElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockListElementTypeName, blockListElementGroupName, blockListDataTypeName, blockListDataTypeId);
richTextDataTypeId = await umbracoApi.dataType.createRichTextEditorWithABlock(richTextDataTypeName, blockListElementTypeId);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, richTextDataTypeName, richTextDataTypeId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickInsertBlockButton();
await umbracoUi.content.clickBlockCardWithName(blockListElementTypeName, true);
await umbracoUi.content.clickAddBlockWithNameButton(textStringElementTypeName);
await umbracoUi.content.clickBlockCardWithName(textStringElementTypeName, true);
await umbracoUi.content.enterTextstring(textStringValue);
await umbracoUi.content.clickCreateForModalWithHeadline('Add ' + textStringElementTypeName);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
// Asserts that the value in the BlockGrid is as expected
const documentData = await umbracoApi.document.getByName(contentName);
expect(documentData.values[0].value.blocks.contentData[0].values[0].value.contentData[0].values[0].value).toContain(textStringValue);
// Clean
await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName);
await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName);
await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName);
});
@@ -0,0 +1,114 @@
import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
const contentName = 'TestContent';
const documentTypeName = 'TestDocumentTypeForContent';
const customDataTypeName = 'Test RTE Tiptap Style Select';
const inputText = 'This is Tiptap test';
test.beforeEach(async ({umbracoApi, umbracoUi}) => {
const customDataTypeId = await umbracoApi.dataType.createTiptapDataTypeWithStyleSelect(customDataTypeName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.enterRTETipTapEditor(inputText);
await umbracoUi.content.selectAllRTETipTapEditorText();
})
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
});
test('can apply page header format', async ({umbracoApi, umbracoUi}) => {
// Arrange
await umbracoUi.content.clickStyleSelectButton();
// Act
await umbracoUi.content.hoverCascadingMenuItemWithName('Headers');
await umbracoUi.content.clickCascadingMenuItemWithName('Page header');
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].value.markup).toEqual('<h2>' + inputText + '</h2><p></p>');
});
test('can apply section header format', async ({umbracoApi, umbracoUi}) => {
// Arrange
await umbracoUi.content.clickStyleSelectButton();
// Act
await umbracoUi.content.hoverCascadingMenuItemWithName('Headers');
await umbracoUi.content.clickCascadingMenuItemWithName('Section header');
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].value.markup).toEqual('<h3>' + inputText + '</h3><p></p>');
});
test('can apply paragraph header format', async ({umbracoApi, umbracoUi}) => {
// Arrange
await umbracoUi.content.clickStyleSelectButton();
// Act
await umbracoUi.content.hoverCascadingMenuItemWithName('Headers');
await umbracoUi.content.clickCascadingMenuItemWithName('Paragraph header');
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].value.markup).toEqual('<h4>' + inputText + '</h4><p></p>');
});
test('can apply paragraph blocks format', async ({umbracoApi, umbracoUi}) => {
// Arrange
await umbracoUi.content.clickStyleSelectButton();
// Act
await umbracoUi.content.hoverCascadingMenuItemWithName('Blocks');
await umbracoUi.content.clickCascadingMenuItemWithName('Paragraph');
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].value.markup).toEqual('<p>' + inputText + '</p>');
});
test('can apply block quote format', async ({umbracoApi, umbracoUi}) => {
// Arrange
await umbracoUi.content.clickStyleSelectButton();
// Act
await umbracoUi.content.hoverCascadingMenuItemWithName('Containers');
await umbracoUi.content.clickCascadingMenuItemWithName('Block quote');
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].value.markup).toEqual('<blockquote><p>' + inputText + '</p></blockquote><p></p>');
});
test('can apply code block format', async ({umbracoApi, umbracoUi}) => {
// Arrange
await umbracoUi.content.clickStyleSelectButton();
// Act
await umbracoUi.content.hoverCascadingMenuItemWithName('Containers');
await umbracoUi.content.clickCascadingMenuItemWithName('Code block');
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].value.markup).toEqual('<pre><code>' + inputText + '</code></pre><p></p>');
});
@@ -0,0 +1,190 @@
import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
const contentName = 'TestContent';
const documentTypeName = 'TestDocumentTypeForContent';
const customDataTypeName = 'Test RTE Tiptap';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.document.ensureNameNotExists(contentName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
});
test('can add a media in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => {
// Arrange
const iconTitle = 'Media Picker';
const imageName = 'Test Image For Content';
await umbracoApi.media.ensureNameNotExists(imageName);
await umbracoApi.media.createDefaultMediaWithImage(imageName);
const customDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customDataTypeName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle);
await umbracoUi.content.selectMediaWithName(imageName);
await umbracoUi.content.clickChooseModalButton();
await umbracoUi.content.clickMediaCaptionAltTextModalSubmitButton();
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].value.markup).toContain('<img');
expect(contentData.values[0].value.markup).toContain(imageName);
// Clean
await umbracoApi.media.ensureNameNotExists(imageName);
});
test('can embed a video into RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => {
// Arrange
const iconTitle = 'Embed';
const videoURL = 'https://www.youtube.com/watch?v=Yu29dE-0OoI';
const customDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customDataTypeName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle);
await umbracoUi.content.enterEmbeddedURL(videoURL);
await umbracoUi.content.clickEmbeddedRetrieveButton();
await umbracoUi.content.waitForEmbeddedPreviewVisible();
await umbracoUi.content.clickEmbeddedMediaModalConfirmButton();
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].value.markup).toContain('data-embed-url');
expect(contentData.values[0].value.markup).toContain(videoURL);
});
test('cannot submit an empty link in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => {
// Arrange
const iconTitle = 'Link';
const customDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customDataTypeName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle);
await umbracoUi.content.clickManualLinkButton();
await umbracoUi.content.enterLink('');
await umbracoUi.content.enterAnchorOrQuerystring('');
await umbracoUi.content.enterLinkTitle('');
await umbracoUi.content.clickAddButton();
// Assert
await umbracoUi.content.isTextWithMessageVisible(ConstantHelper.validationMessages.emptyLinkPicker);
});
// TODO: Remove skip when the front-end ready. Currently it still accept the empty link with an anchor or querystring
// Issue link: https://github.com/umbraco/Umbraco-CMS/issues/17411
test.skip('cannot submit an empty URL with an anchor or querystring in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => {
// Arrange
const iconTitle = 'Link';
const customDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customDataTypeName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle);
await umbracoUi.content.clickManualLinkButton();
await umbracoUi.content.enterLink('');
await umbracoUi.content.enterAnchorOrQuerystring('#value');
await umbracoUi.content.clickAddButton();
// Assert
await umbracoUi.content.isTextWithMessageVisible(ConstantHelper.validationMessages.emptyLinkPicker);
});
// TODO: Remove skip when the front-end ready. Currently it is impossible to link to unpublished document
// Issue link: https://github.com/umbraco/Umbraco-CMS/issues/17974
test.skip('can insert a link to an unpublished document in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => {
// Arrange
const iconTitle = 'Link';
const customDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customDataTypeName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
// Create a document to link
const documentTypeForLinkedDocumentName = 'TestDocumentType';
const documentTypeForLinkedDocumentId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeForLinkedDocumentName);
const linkedDocumentName = 'LinkedDocument';
await umbracoApi.document.createDefaultDocument(linkedDocumentName, documentTypeForLinkedDocumentId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle);
await umbracoUi.content.clickDocumentLinkButton();
await umbracoUi.content.selectLinkByName(linkedDocumentName);
await umbracoUi.content.clickButtonWithName('Choose');
await umbracoUi.content.clickAddButton();
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.isSuccessNotificationVisible();
// Clean
await umbracoApi.documentType.ensureNameNotExists(documentTypeForLinkedDocumentName);
await umbracoApi.document.ensureNameNotExists(linkedDocumentName);
});
test('can view word count', async ({umbracoApi, umbracoUi}) => {
// Arrange
const inputText = 'Test Tiptap <b>here</b>!!!';
const expectedWordCount = 3;
const customDataTypeId = await umbracoApi.dataType.createTiptapDataTypeWithWordCountStatusbar(customDataTypeName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.enterRTETipTapEditor(inputText);
// Assert
await umbracoUi.content.doesTiptapHaveWordCount(expectedWordCount);
});
test('can view element path', async ({umbracoApi, umbracoUi}) => {
// Arrange
const inputText = 'This is Tiptap test';
const expectedElementPath = 'p';
const customDataTypeId = await umbracoApi.dataType.createTiptapDataTypeWithElementPathStatusbar(customDataTypeName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.enterRTETipTapEditor(inputText);
// Assert
await umbracoUi.content.doesElementPathHaveText(expectedElementPath);
});
@@ -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}) => {
@@ -0,0 +1,93 @@
import {ConstantHelper, test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
let collectionId = '';
const contentName = 'TestContent';
const documentTypeName = 'TestDocumentTypeForContent';
const childDocumentTypeName = 'TestChildDocumentType';
const firstChildContentName = 'First Child Content';
const secondChildContentName = 'Second Child Content';
const collectionDataTypeName = 'List View - Content';
const referenceHeadline = ConstantHelper.trashDeleteDialogMessage.bulkReferenceHeadline;
const documentPickerName = ['TestPicker', 'DocumentTypeForPicker'];
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.document.ensureNameNotExists(contentName);
const collectionDataTypeData = await umbracoApi.dataType.getByName(collectionDataTypeName);
collectionId = collectionDataTypeData.id;
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.documentType.ensureNameNotExists(childDocumentTypeName);
await umbracoApi.document.emptyRecycleBin();
});
test('can bulk trash content nodes without a relation', async ({umbracoApi, umbracoUi}) => {
// Arrange
const childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNodeAndCollectionId(documentTypeName, childDocumentTypeId, collectionId);
const contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoApi.document.createDefaultDocumentWithParent(firstChildContentName, childDocumentTypeId, contentId);
await umbracoApi.document.createDefaultDocumentWithParent(secondChildContentName, childDocumentTypeId, contentId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.selectContentWithNameInListView(firstChildContentName);
await umbracoUi.content.selectContentWithNameInListView(secondChildContentName);
await umbracoUi.content.clickTrashSelectedListItems();
// Verify the references list not displayed
await umbracoUi.content.isReferenceHeadlineVisible(false);
await umbracoUi.content.clickConfirmTrashButton();
// // Assert
await umbracoUi.content.isSuccessNotificationVisible();
expect(await umbracoApi.document.doesNameExist(firstChildContentName)).toBeFalsy();
expect(await umbracoApi.document.doesNameExist(secondChildContentName)).toBeFalsy();
await umbracoUi.content.isItemVisibleInRecycleBin(firstChildContentName);
await umbracoUi.content.isItemVisibleInRecycleBin(secondChildContentName);
expect(await umbracoApi.document.doesItemExistInRecycleBin(firstChildContentName)).toBeTruthy();
expect(await umbracoApi.document.doesItemExistInRecycleBin(secondChildContentName)).toBeTruthy();
});
test('can bulk trash content nodes with a relation', async ({umbracoApi, umbracoUi}) => {
// Arrange
const childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNodeAndCollectionId(documentTypeName, childDocumentTypeId, collectionId);
const contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoApi.document.publish(contentId);
const firstChildContentId = await umbracoApi.document.createDefaultDocumentWithParent(firstChildContentName, childDocumentTypeId, contentId);
await umbracoApi.document.publish(firstChildContentId);
await umbracoApi.document.createDefaultDocumentWithParent(secondChildContentName, childDocumentTypeId, contentId);
// Create a document that has a document picker with firstChildContentName
await umbracoApi.document.createDefaultDocumentWithOneDocumentLink(documentPickerName[0], firstChildContentName, firstChildContentId, documentPickerName[1]);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.selectContentWithNameInListView(firstChildContentName);
await umbracoUi.content.selectContentWithNameInListView(secondChildContentName);
await umbracoUi.content.clickTrashSelectedListItems();
// Verify the references list
await umbracoUi.content.doesReferenceHeadlineHaveText(referenceHeadline);
await umbracoUi.content.doesReferenceItemsHaveCount(1);
await umbracoUi.content.isReferenceItemNameVisible(firstChildContentName);
await umbracoUi.content.clickConfirmTrashButton();
// // Assert
await umbracoUi.content.isSuccessNotificationVisible();
expect(await umbracoApi.document.doesNameExist(firstChildContentName)).toBeFalsy();
expect(await umbracoApi.document.doesNameExist(secondChildContentName)).toBeFalsy();
await umbracoUi.content.isItemVisibleInRecycleBin(firstChildContentName);
await umbracoUi.content.isItemVisibleInRecycleBin(secondChildContentName);
expect(await umbracoApi.document.doesItemExistInRecycleBin(firstChildContentName)).toBeTruthy();
expect(await umbracoApi.document.doesItemExistInRecycleBin(secondChildContentName)).toBeTruthy();
// Clean
await umbracoApi.documentType.ensureNameNotExists(documentPickerName[1]);
});
@@ -6,7 +6,7 @@ const contentName = 'TestContent';
const documentTypeName = 'TestDocumentTypeForContent';
const dataTypeName = 'Textstring';
const contentText = 'This is test content text';
const referenceHeadline = 'The following items depend on this';
const referenceHeadline = ConstantHelper.trashDeleteDialogMessage.referenceHeadline;
const documentPickerName = ['TestPicker', 'DocumentTypeForPicker'];
test.beforeEach(async ({umbracoApi}) => {
@@ -61,8 +61,8 @@ test('tiptap is the default property editor in rich text editor', async ({umbrac
await umbracoUi.dataType.goToDataType(dataTypeName);
// Assert
//await umbracoUi.dataType.doesSettingHaveValue(ConstantHelper.tipTapSettings);
//await umbracoUi.dataType.doesSettingItemsHaveCount(ConstantHelper.tipTapSettings);
await umbracoUi.dataType.doesSettingHaveValue(ConstantHelper.tipTapSettings);
await umbracoUi.dataType.doesSettingItemsHaveCount(ConstantHelper.tipTapSettings);
await umbracoUi.dataType.doesPropertyEditorHaveName(tipTapPropertyEditorName);
await umbracoUi.dataType.doesPropertyEditorHaveAlias(tipTapAlias);
await umbracoUi.dataType.doesPropertyEditorHaveUiAlias(tipTapUiAlias);
@@ -121,12 +121,15 @@ test('can add an available block', async ({umbracoApi, umbracoUi}) => {
await umbracoUi.dataType.goToDataType(tipTapName);
// Act
await umbracoUi.dataType.isExtensionItemChecked('Block', false);
await umbracoUi.dataType.addAvailableBlocks(elementTypeName);
await umbracoUi.dataType.clickSaveButton();
// Assert
await umbracoUi.dataType.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
expect(await umbracoApi.dataType.doesRTEContainBlocks(tipTapName, [elementTypeId])).toBeTruthy();
// Verify that "Block" extension is enable
await umbracoUi.dataType.isExtensionItemChecked('Block');
// Clean
await umbracoApi.documentType.ensureNameNotExists(elementTypeName);
@@ -226,3 +229,38 @@ test('can disable extensions item', async ({umbracoApi, umbracoUi}) => {
expect(extensionsValue.value.length).toBe(extensionsCount - 1);
expect(extensionsValue.value).not.toContain(extensionItemName);
});
test('can add a statusbar', async ({umbracoApi, umbracoUi}) => {
// Arrange
const statusbarName = 'Word Count';
const statusbarApiValue = 'Umb.Tiptap.Statusbar.WordCount';
await umbracoApi.dataType.createDefaultTiptapDataType(tipTapName);
await umbracoUi.dataType.goToDataType(tipTapName);
// Act
await umbracoUi.dataType.clickStatusbarItemInToolboxWithName(statusbarName);
await umbracoUi.dataType.clickSaveButton();
// Assert
await umbracoUi.dataType.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
const tipTapData = await umbracoApi.dataType.getByName(tipTapName);
const statusbarValue = tipTapData.values.find(value => value.alias === 'statusbar');
expect(statusbarValue.value).toEqual([[statusbarApiValue]]);
});
test('can remove a statusbar', async ({umbracoApi, umbracoUi}) => {
// Arrange
const statusbarName = 'Word Count';
await umbracoApi.dataType.createTiptapDataTypeWithWordCountStatusbar(tipTapName);
await umbracoUi.dataType.goToDataType(tipTapName);
// Act
await umbracoUi.dataType.clickStatusbarItemWithName(statusbarName);
await umbracoUi.dataType.clickSaveButton();
// Assert
await umbracoUi.dataType.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
const tipTapData = await umbracoApi.dataType.getByName(tipTapName);
const statusbarValue = tipTapData.values.find(value => value.alias === 'statusbar');
expect(statusbarValue).toBeFalsy();
});
@@ -174,6 +174,8 @@ test('can trash a media item', async ({umbracoApi, umbracoUi}) => {
// Act
await umbracoUi.media.clickActionsMenuForName(mediaFileName);
await umbracoUi.media.clickTrashButton();
// Verify the references list not displayed
await umbracoUi.content.isReferenceHeadlineVisible(false);
await umbracoUi.media.clickConfirmTrashButton();
// Assert
@@ -245,3 +247,79 @@ test('can empty the recycle bin', async ({umbracoApi, umbracoUi}) => {
expect(await umbracoApi.media.doesNameExist(mediaFileName)).toBeFalsy();
expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(mediaFileName)).toBeFalsy();
});
test('can trash a media node with a relation', async ({umbracoApi, umbracoUi}) => {
// Arrange
const documentPickerName = ['TestPicker', 'DocumentTypeForPicker'];
await umbracoApi.media.emptyRecycleBin();
await umbracoApi.media.createDefaultMediaFile(mediaFileName);
await umbracoApi.media.doesNameExist(mediaFileName);
// Create a document that have media picker is firstMediaFileName
await umbracoApi.document.createDefaultDocumentWithOneMediaLink(documentPickerName[0], mediaFileName, documentPickerName[1]);
await umbracoUi.media.goToSection(ConstantHelper.sections.media);
// Act
await umbracoUi.media.clickActionsMenuForName(mediaFileName);
await umbracoUi.media.clickTrashButton();
// Verify the references list
await umbracoUi.media.doesReferenceHeadlineHaveText(ConstantHelper.trashDeleteDialogMessage.referenceHeadline);
await umbracoUi.media.doesReferenceItemsHaveCount(1);
await umbracoUi.media.isReferenceItemNameVisible(documentPickerName[0]);
await umbracoUi.media.clickConfirmTrashButton();
// Assert
await umbracoUi.media.doesSuccessNotificationHaveText(NotificationConstantHelper.success.movedToRecycleBin);
await umbracoUi.media.isItemVisibleInRecycleBin(mediaFileName);
expect(await umbracoApi.media.doesNameExist(mediaFileName)).toBeFalsy();
expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(mediaFileName)).toBeTruthy();
// Clean
await umbracoApi.media.emptyRecycleBin();
await umbracoApi.document.ensureNameNotExists(documentPickerName[0]);
await umbracoApi.documentType.ensureNameNotExists(documentPickerName[1]);
});
test('can bulk trash media nodes with a relation', async ({umbracoApi, umbracoUi}) => {
// Arrange
const firstMediaFileName = 'FirstMediaFile';
const secondMediaFileName = 'SecondMediaFile';
const documentPickerName1 = ['TestPicker1', 'DocumentTypeForPicker1'];
const documentPickerName2 = ['TestPicker2', 'DocumentTypeForPicker2'];
await umbracoApi.media.emptyRecycleBin();
await umbracoApi.media.createDefaultMediaFile(firstMediaFileName);
await umbracoApi.media.createDefaultMediaFile(secondMediaFileName);
// Create a document that has a media picker with firstMediaFileName
await umbracoApi.document.createDefaultDocumentWithOneMediaLink(documentPickerName1[0], firstMediaFileName, documentPickerName1[1]);
// Create a document that has a media picker with secondMediaFileName
await umbracoApi.document.createDefaultDocumentWithOneMediaLink(documentPickerName2[0], secondMediaFileName, documentPickerName2[1]);
// Act
await umbracoUi.media.goToSection(ConstantHelper.sections.media);
await umbracoUi.media.selectMediaWithName(firstMediaFileName);
await umbracoUi.media.selectMediaWithName(secondMediaFileName);
await umbracoUi.media.clickBulkTrashButton();
// Verify the references list
await umbracoUi.media.doesReferenceHeadlineHaveText(ConstantHelper.trashDeleteDialogMessage.bulkReferenceHeadline);
await umbracoUi.media.doesReferenceItemsHaveCount(2);
await umbracoUi.media.isReferenceItemNameVisible(firstMediaFileName);
await umbracoUi.media.isReferenceItemNameVisible(secondMediaFileName);
await umbracoUi.media.clickConfirmTrashButton();
// Assert
await umbracoUi.media.isSuccessNotificationVisible();
expect(await umbracoApi.media.doesNameExist(firstMediaFileName)).toBeFalsy();
expect(await umbracoApi.media.doesNameExist(secondMediaFileName)).toBeFalsy();
expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(firstMediaFileName)).toBeTruthy();
expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(secondMediaFileName)).toBeTruthy();
await umbracoUi.media.isItemVisibleInRecycleBin(firstMediaFileName);
await umbracoUi.media.isItemVisibleInRecycleBin(secondMediaFileName, true, false);
// Clean
await umbracoApi.media.ensureNameNotExists(firstMediaFileName);
await umbracoApi.media.ensureNameNotExists(secondMediaFileName);
await umbracoApi.document.ensureNameNotExists(documentPickerName1[0]);
await umbracoApi.documentType.ensureNameNotExists(documentPickerName1[1]);
await umbracoApi.document.ensureNameNotExists(documentPickerName2[0]);
await umbracoApi.documentType.ensureNameNotExists(documentPickerName2[1]);
await umbracoApi.media.emptyRecycleBin();
});
@@ -0,0 +1,28 @@
// TODO: Consider refactoring the initialization process to improve readability or simplify the builder configuration.
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.CreateUmbracoBuilder()
.AddBackOffice()
.AddWebsite()
.AddDeliveryApi()
.AddComposers()
.Build();
WebApplication app = builder.Build();
await app.BootUmbracoAsync();
app.UseUmbraco()
.WithMiddleware(u =>
{
u.UseBackOffice();
u.UseWebsite();
})
.WithEndpoints(u =>
{
u.UseBackOfficeEndpoints();
u.UseWebsiteEndpoints();
});
await app.RunAsync();
@@ -0,0 +1,64 @@
{
"$schema": "appsettings-schema.json",
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Async",
"Args": {
"Configure": [
{
"Name": "Console"
}
]
}
}
]
},
"Umbraco": {
"CMS": {
"DeliveryApi": {
"Enabled": true,
"Media": {
"Enabled": true
}
},
"Unattended": {
"InstallUnattended": true,
"UnattendedUserName": "Playwright Test",
"UnattendedUserEmail": "playwright@umbraco.com",
"UnattendedUserPassword": "UmbracoAcceptance123!"
},
"Content": {
"ContentVersionCleanupPolicy": {
"EnableCleanup": false
}
},
"Global": {
"DisableElectionForSingleServer": true,
"InstallMissingDatabase": true,
"Id": "00000000-0000-0000-0000-000000000042",
"VersionCheckPeriod": 0,
"UseHttps": true
},
"HealthChecks": {
"Notification": {
"Enabled": false
}
},
"KeepAlive": {
"DisableKeepAliveTask": true
},
"WebRouting": {
"UmbracoApplicationUrl": "https://localhost:44331/"
}
}
}
}
@@ -0,0 +1,42 @@
import {expect} from '@playwright/test';
import {AliasHelper, test} from '@umbraco/playwright-testhelpers';
test('can get content from delivery api', async ({umbracoApi}) => {
// Arrange
const documentTypeName = 'TestDocumentType';
const contentName = 'TestContent';
const dataTypeName = 'Textstring';
const textStringValue = 'This is a test text string value';
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
const dataType = await umbracoApi.dataType.getByName(dataTypeName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataType.id, 'TestGroup');
const documentId = await umbracoApi.document.createDocumentWithTextContent(contentName, documentTypeId, textStringValue, dataTypeName);
const propertyValue = {
dataTypeName: AliasHelper.toAlias(dataTypeName),
dataTypeValue: textStringValue
}
// Act
await umbracoApi.document.publish(documentId);
// Assert
expect(await umbracoApi.deliveryApi.doesContentItemWithIdContainValues(documentId, contentName, AliasHelper.toAlias(documentTypeName), [propertyValue])).toBeTruthy();
// Clean
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
test('can get media image from delivery api', async ({umbracoApi}) => {
// Arrange
const mediaName = 'TestMedia';
const mediaTypeName = 'File';
await umbracoApi.media.ensureNameNotExists(mediaName);
const mediaId = await umbracoApi.media.createDefaultMediaFile(mediaName);
const mediaUrl = await umbracoApi.media.getMediaUrlById(mediaId);
// Assert
expect(await umbracoApi.deliveryApi.doesMediaItemWithIdContainValues(mediaId, mediaName, mediaTypeName, mediaUrl)).toBeTruthy();
// Clean
await umbracoApi.media.ensureNameNotExists(mediaName);
});
@@ -0,0 +1,25 @@
{
"$schema": "../../umbraco-package-schema.json",
"name": "Azure B2C Login",
"allowPublicAccess": true,
"extensions": [
{
"type": "authProvider",
"alias": "Test.AzureB2C",
"name": "Azure AD B2C",
"forProviderName": "Umbraco.AzureB2C",
"meta": {
"label": "Sign in with Azure AD B2C",
"defaultView": {
"icon": "icon-cloud"
},
"behavior": {
"autoRedirect": false
},
"linking": {
"allowManualLinking": true
}
}
}
]
}
@@ -0,0 +1,73 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Tests.AcceptanceTest.ExternalLogin.AzureADB2C
{
public static class AzureB2CAuthenticationExtensions
{
public static IUmbracoBuilder ConfigureAuthentication(this IUmbracoBuilder builder,
IConfiguration configuration)
{
var b2cSettings = new AzureB2CSettings();
builder.AddBackOfficeExternalLogins(logins =>
{
const string schemeName = AzureB2COptions.SchemeName;
var backOfficeScheme = Constants.Security.BackOfficeExternalAuthenticationTypePrefix + schemeName;
logins.AddBackOfficeLogin(backOfficeAuth =>
{
backOfficeAuth.AddOpenIdConnect(backOfficeScheme, options =>
{
options.RequireHttpsMetadata = true;
options.SaveTokens = true;
options.ClientId = b2cSettings.ClientId;
options.ClientSecret = b2cSettings.ClientSecret;
options.CallbackPath = "/umbraco-b2c-users-signin";
options.MetadataAddress =
$"https://{b2cSettings.Domain}/{b2cSettings.Tenant}/{b2cSettings.Policy}/v2.0/.well-known/openid-configuration";
options.ResponseType = OpenIdConnectResponseType.Code;
options.TokenValidationParameters.SaveSigninToken = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.TokenValidationParameters.NameClaimType = "name";
options.TokenValidationParameters.RoleClaimType = "role";
options.Events = new OpenIdConnectEvents
{
OnTokenResponseReceived = context =>
{
if (string.IsNullOrEmpty(context.TokenEndpointResponse.AccessToken))
{
context.TokenEndpointResponse.AccessToken = "empty_access_token";
}
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
var identity = context.Principal!.Identities.First();
var email = identity.FindFirst("emails")?.Value
?? identity.FindFirst(ClaimTypes.Email)?.Value;
if (!string.IsNullOrWhiteSpace(email))
{
identity.AddClaim(new Claim(ClaimTypes.Email, email));
identity.AddClaim(new Claim("email", email));
}
return Task.CompletedTask;
}
};
});
});
});
return builder;
}
}
}
@@ -0,0 +1,14 @@
using Umbraco.Cms.Core.Composing;
namespace Umbraco.Cms.Tests.AcceptanceTest.ExternalLogin.AzureADB2C
{
public class AzureB2CComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.ConfigureOptions<AzureB2COptions>();
builder.ConfigureAuthentication(builder.Config);
}
}
}
@@ -0,0 +1,31 @@
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.Security;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Tests.AcceptanceTest.ExternalLogin.AzureADB2C
{
public class AzureB2COptions : IConfigureNamedOptions<BackOfficeExternalLoginProviderOptions>
{
public const string SchemeName = "AzureB2C";
public void Configure(string? name, BackOfficeExternalLoginProviderOptions options)
{
if (name != Constants.Security.BackOfficeExternalAuthenticationTypePrefix + SchemeName)
return;
options.AutoLinkOptions = new ExternalSignInAutoLinkOptions(
autoLinkExternalAccount: true,
defaultUserGroups: [Constants.Security.AdminGroupAlias],
defaultCulture: "en-US",
allowManualLinking: true
)
{
OnAutoLinking = (user, loginInfo) => { user.IsApproved = true; },
OnExternalLogin = (user, loginInfo) => { return true; }
};
}
public void Configure(BackOfficeExternalLoginProviderOptions options) =>
Configure(Constants.Security.BackOfficeExternalAuthenticationTypePrefix + SchemeName, options);
}
}
@@ -0,0 +1,11 @@
namespace Umbraco.Cms.Tests.AcceptanceTest.ExternalLogin.AzureADB2C
{
public class AzureB2CSettings
{
public string Domain { get; set; } = Environment.GetEnvironmentVariable("AZUREADB2CDOMAIN") ?? string.Empty;
public string Tenant { get; set; } = Environment.GetEnvironmentVariable("AZUREADB2CTENANT") ?? string.Empty;
public string Policy { get; set; } = Environment.GetEnvironmentVariable("AZUREADB2CPOLICY") ?? string.Empty;
public string ClientId { get; set; } = Environment.GetEnvironmentVariable("AZUREADB2CCLIENTID") ?? string.Empty;
public string ClientSecret { get; set; } = Environment.GetEnvironmentVariable("AZUREADB2CCLIENTSECRET") ?? string.Empty;
}
}
@@ -0,0 +1,58 @@
{
"$schema": "appsettings-schema.json",
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Async",
"Args": {
"Configure": [
{
"Name": "Console"
}
]
}
}
]
},
"Umbraco": {
"CMS": {
"Unattended": {
"InstallUnattended": true,
"UnattendedUserName": "Playwright Test",
"UnattendedUserEmail": "playwright@umbraco.com",
"UnattendedUserPassword": "UmbracoAcceptance123!"
},
"Content": {
"ContentVersionCleanupPolicy": {
"EnableCleanup": false
}
},
"Global": {
"DisableElectionForSingleServer": true,
"InstallMissingDatabase": true,
"Id": "00000000-0000-0000-0000-000000000042",
"VersionCheckPeriod": 0,
"UseHttps": true
},
"HealthChecks": {
"Notification": {
"Enabled": false
}
},
"KeepAlive": {
"DisableKeepAliveTask": true
},
"WebRouting": {
"UmbracoApplicationUrl": "https://localhost:44331/"
}
}
}
}
@@ -0,0 +1,20 @@
import {ConstantHelper, test} from '@umbraco/playwright-testhelpers';
const azureEmail = process.env.AZUREADB2CTESTUSEREMAIL;
const azurePassword = process.env.AZUREADB2CTESTUSERPASSWORD;
// Really simple test to check if we can log in using Azure AD B2C
test('Log in to Umbraco using Azure AD B2C', async ({umbracoUi}) => {
test.slow();
// Arrange
await umbracoUi.goToBackOffice();
// Act
await umbracoUi.login.clickSignInWithAzureADB2CButton();
await umbracoUi.login.enterAzureADB2CEmail(azureEmail);
await umbracoUi.login.enterAzureADB2CPassword(azurePassword);
await umbracoUi.login.clickSignInButton();
// Assert
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
});

Some files were not shown because too many files have changed in this diff Show More