diff --git a/Directory.Build.props b/Directory.Build.props index 81e3c40a9b..f9fb83457d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -29,7 +29,7 @@ - true + false true 14.0.0 true diff --git a/Directory.Packages.props b/Directory.Packages.props index fa6d14e980..47df4cf510 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,24 +12,24 @@ - - + + - - - - + + + + - + - - + + @@ -47,7 +47,7 @@ - + @@ -75,7 +75,7 @@ - + @@ -87,5 +87,7 @@ + + \ No newline at end of file diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 977ce443d0..9e7207bf07 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -5,10 +5,6 @@ parameters: displayName: Run SQL Server Integration Tests type: boolean default: false - - name: sqlServerAcceptanceTests - displayName: Run SQL Server Acceptance Tests - type: boolean - default: false - name: myGetDeploy displayName: Deploy to MyGet type: boolean @@ -517,10 +513,7 @@ stages: workingDirectory: tests/Umbraco.Tests.AcceptanceTest # Test - - ${{ if eq(parameters.isNightly, true) }}: - pwsh: npm run test --ignore-certificate-errors - ${{ else }}: - pwsh: npm run smokeTest --ignore-certificate-errors + - pwsh: npm run smokeTest --ignore-certificate-errors displayName: Run Playwright tests continueOnError: true workingDirectory: tests/Umbraco.Tests.AcceptanceTest @@ -556,8 +549,6 @@ stages: - job: displayName: E2E Tests (SQL Server) - # condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerAcceptanceTests}}) # Outcommented due to timeouts - condition: eq(${{parameters.sqlServerAcceptanceTests}}, True) variables: # Connection string CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True @@ -593,7 +584,8 @@ stages: - pwsh: | "UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL) UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD) - URL=$(ASPNETCORE_URLS)" | Out-File .env + 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 @@ -660,10 +652,7 @@ stages: workingDirectory: tests/Umbraco.Tests.AcceptanceTest # Test - - ${{ if eq(parameters.isNightly, true) }}: - pwsh: npm run test --ignore-certificate-errors - ${{ else }}: - pwsh: npm run smokeTest --ignore-certificate-errors + - pwsh: npm run smokeTest --ignore-certificate-errors displayName: Run Playwright tests continueOnError: true workingDirectory: tests/Umbraco.Tests.AcceptanceTest @@ -714,7 +703,7 @@ stages: dependsOn: - Unit - Integration -# - E2E + # - E2E condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.myGetDeploy}})) jobs: - job: diff --git a/build/nightly-E2E-test-pipelines.yml b/build/nightly-E2E-test-pipelines.yml new file mode 100644 index 0000000000..6a069ef38c --- /dev/null +++ b/build/nightly-E2E-test-pipelines.yml @@ -0,0 +1,400 @@ +name: Nightly_E2E_Test_$(TeamProject)_$(Build.DefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) + +pr: none +trigger: none + +schedules: + - cron: '0 0 * * *' + displayName: Daily midnight build + branches: + include: + - v14/dev + - v15/dev + +variables: + nodeVersion: 20 + solution: umbraco.sln + buildConfiguration: Release + UMBRACO__CMS__GLOBAL__ID: 00000000-0000-0000-0000-000000000042 + DOTNET_NOLOGO: true + DOTNET_GENERATE_ASPNET_CERTIFICATE: false + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + npm_config_cache: $(Pipeline.Workspace)/.npm_client + NODE_OPTIONS: --max_old_space_size=16384 + +parameters: + - name: runSmokeTests + displayName: Run the smoke tests + type: boolean + default: false + +stages: + ############################################### + ## Build + ############################################### + - stage: Build + jobs: + - job: A + displayName: Build Umbraco CMS + pool: + vmImage: 'ubuntu-latest' + steps: + - checkout: self + fetchDepth: 0 + submodules: true + - 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: + command: restore + projects: $(solution) + - task: DotNetCoreCLI@2 + name: build + displayName: Run dotnet build and generate NuGet packages + inputs: + command: build + projects: $(solution) + arguments: '--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg' + - task: PublishPipelineArtifact@1 + displayName: Publish nupkg + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/nupkg + artifactName: nupkg + - task: PublishPipelineArtifact@1 + displayName: Publish build artifacts + inputs: + targetPath: $(Build.SourcesDirectory) + artifactName: build_output + + - stage: E2E + displayName: E2E Tests + dependsOn: Build + variables: + npm_config_cache: $(Pipeline.Workspace)/.npm_e2e + # Enable console logging in Release mode + SERILOG__WRITETO__0__NAME: Async + SERILOG__WRITETO__0__ARGS__CONFIGURE__0__NAME: Console + # Set unattended install settings + UMBRACO__CMS__UNATTENDED__INSTALLUNATTENDED: true + UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERNAME: Playwright Test + UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD: UmbracoAcceptance123! + UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL: playwright@umbraco.com + # Custom Umbraco settings + UMBRACO__CMS__CONTENT__CONTENTVERSIONCLEANUPPOLICY__ENABLECLEANUP: false + UMBRACO__CMS__GLOBAL__DISABLEELECTIONFORSINGLESERVER: true + UMBRACO__CMS__GLOBAL__INSTALLMISSINGDATABASE: true + UMBRACO__CMS__GLOBAL__ID: 00000000-0000-0000-0000-000000000042 + UMBRACO__CMS__GLOBAL__VERSIONCHECKPERIOD: 0 + UMBRACO__CMS__GLOBAL__USEHTTPS: true + UMBRACO__CMS__HEALTHCHECKS__NOTIFICATION__ENABLED: false + UMBRACO__CMS__KEEPALIVE__DISABLEKEEPALIVETASK: true + UMBRACO__CMS__WEBROUTING__UMBRACOAPPLICATIONURL: https://localhost:44331/ + ASPNETCORE_URLS: https://localhost:44331 + jobs: + # E2E Tests + - job: + displayName: E2E Tests (SQLite) + timeoutInMinutes: 180 + variables: + # Connection string + CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=Umbraco;Mode=Memory;Cache=Shared;Foreign Keys=True;Pooling=True + CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.Sqlite + strategy: + matrix: + Linux: + vmImage: 'ubuntu-latest' + Windows: + vmImage: 'windows-latest' + 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 + + - 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 + dotnet dev-certs https + displayName: Build application + workingDirectory: $(Agent.BuildDirectory)/app + + # 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 + + # 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 + - ${{ if eq(parameters.runSmokeTests, true) }}: + pwsh: npm run smokeTest --ignore-certificate-errors + ${{ else }}: + pwsh: npm run test --ignore-certificate-errors + 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 Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)' + + - job: + displayName: E2E Tests (SQL Server) + timeoutInMinutes: 180 + variables: + # Connection string + CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True + CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient + strategy: + matrix: + Linux: + vmImage: 'ubuntu-latest' + SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD) + CONNECTIONSTRINGS__UMBRACODBDSN: 'Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True' + Windows: + vmImage: 'windows-latest' + 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 + + - 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 + dotnet dev-certs https + displayName: Build application + workingDirectory: $(Agent.BuildDirectory)/app + + # 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')) + + - 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 + + # 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 + - ${{ if eq(parameters.runSmokeTests, true) }}: + pwsh: npm run smokeTest --ignore-certificate-errors + ${{ else }}: + pwsh: npm run test --ignore-certificate-errors + 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')) + + # 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 Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)' diff --git a/build/nightly-build-trigger.yml b/build/nightly-build-trigger.yml index 16cc06533d..7e128b2af7 100644 --- a/build/nightly-build-trigger.yml +++ b/build/nightly-build-trigger.yml @@ -26,7 +26,7 @@ steps: useSameBranch: true waitForQueuedBuildsToFinish: false storeInEnvironmentVariable: false - templateParameters: 'sqlServerIntegrationTests: true, sqlServerAcceptanceTests: true, forceReleaseTestFilter: true, myGetDeploy: true, isNightly: true' + templateParameters: 'sqlServerIntegrationTests: true, forceReleaseTestFilter: true, myGetDeploy: true, isNightly: true' authenticationMethod: 'OAuth Token' enableBuildInQueueCondition: false dependentOnSuccessfulBuildCondition: false diff --git a/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj b/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj index a73fc16574..a1bd4e1694 100644 --- a/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj +++ b/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj @@ -13,6 +13,9 @@ + + + diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs index a2277820bf..c008dad102 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.ContentEditing.Validation; using Umbraco.Cms.Core.Services.OperationStatus; @@ -9,6 +11,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Content; public abstract class ContentControllerBase : ManagementApiControllerBase { + protected IActionResult ContentEditingOperationStatusResult(ContentEditingOperationStatus status) => OperationStatusResult(status, problemDetailsBuilder => status switch { @@ -96,7 +99,8 @@ public abstract class ContentControllerBase : ManagementApiControllerBase } var errors = new SortedDictionary(); - var missingPropertyAliases = new List(); + + var missingPropertyModels = new List(); foreach (PropertyValidationError validationError in validationResult.ValidationErrors) { TValueModel? requestValue = requestModel.Values.FirstOrDefault(value => @@ -105,7 +109,7 @@ public abstract class ContentControllerBase : ManagementApiControllerBase && value.Segment == validationError.Segment); if (requestValue is null) { - missingPropertyAliases.Add(validationError.Alias); + missingPropertyModels.Add(MapMissingProperty(validationError)); continue; } @@ -119,7 +123,16 @@ public abstract class ContentControllerBase : ManagementApiControllerBase .WithTitle("Validation failed") .WithDetail("One or more properties did not pass validation") .WithRequestModelErrors(errors) - .WithExtension("missingProperties", missingPropertyAliases.ToArray()) + .WithExtension("missingValues", missingPropertyModels.ToArray()) .Build())); } + + private PropertyValidationResponseModel MapMissingProperty(PropertyValidationError source) => + new() + { + Alias = source.Alias, + Segment = source.Segment, + Culture = source.Culture, + Messages = source.ErrorMessages, + }; } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateDocumentControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateDocumentControllerBase.cs index d6983964c5..669c2cdc93 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateDocumentControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateDocumentControllerBase.cs @@ -18,18 +18,21 @@ public abstract class CreateDocumentControllerBase : DocumentControllerBase protected async Task HandleRequest(CreateDocumentRequestModel requestModel, Func> authorizedHandler) { - IEnumerable cultures = requestModel.Variants - .Where(v => v.Culture is not null) - .Select(v => v.Culture!); - AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( - User, - ContentPermissionResource.WithKeys(ActionNew.ActionLetter, requestModel.Parent?.Id, cultures), - AuthorizationPolicies.ContentPermissionByResource); + // TODO This have temporarily been uncommented, to support the client sends values from all cultures, even when the user do not have access to the languages. + // The values are ignored in the ContentEditingService - if (!authorizationResult.Succeeded) - { - return Forbidden(); - } + // IEnumerable cultures = requestModel.Variants + // .Where(v => v.Culture is not null) + // .Select(v => v.Culture!); + // AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + // User, + // ContentPermissionResource.WithKeys(ActionNew.ActionLetter, requestModel.Parent?.Id, cultures), + // AuthorizationPolicies.ContentPermissionByResource); + // + // if (!authorizationResult.Succeeded) + // { + // return Forbidden(); + // } return await authorizedHandler(); } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/SearchDocumentItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/SearchDocumentItemController.cs index 415fd156d4..f3b7cb1caf 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/SearchDocumentItemController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/SearchDocumentItemController.cs @@ -21,16 +21,21 @@ public class SearchDocumentItemController : DocumentItemControllerBase _documentPresentationFactory = documentPresentationFactory; } + [NonAction] + [Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")] + public async Task Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100) + => await SearchFromParent(cancellationToken, query, skip, take); + [HttpGet("search")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedModel), StatusCodes.Status200OK)] - public async Task Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100) + public async Task SearchFromParent(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null) { - PagedModel searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Document, query, skip, take); + PagedModel searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Document, query, parentId, skip, take); var result = new PagedModel { Items = searchResult.Items.OfType().Select(_documentPresentationFactory.CreateItemResponseModel), - Total = searchResult.Total + Total = searchResult.Total, }; return await Task.FromResult(Ok(result)); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentControllerBase.cs index 2d41fe94fe..4b585e78b9 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentControllerBase.cs @@ -17,18 +17,21 @@ public abstract class UpdateDocumentControllerBase : DocumentControllerBase protected async Task HandleRequest(Guid id, UpdateDocumentRequestModel requestModel, Func> authorizedHandler) { - IEnumerable cultures = requestModel.Variants - .Where(v => v.Culture is not null) - .Select(v => v.Culture!); - AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( - User, - ContentPermissionResource.WithKeys(ActionUpdate.ActionLetter, id, cultures), - AuthorizationPolicies.ContentPermissionByResource); + // TODO This have temporarily been uncommented, to support the client sends values from all cultures, even when the user do not have access to the languages. + // The values are ignored in the ContentEditingService - if (!authorizationResult.Succeeded) - { - return Forbidden(); - } + // IEnumerable cultures = requestModel.Variants + // .Where(v => v.Culture is not null) + // .Select(v => v.Culture!); + // AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + // User, + // ContentPermissionResource.WithKeys(ActionUpdate.ActionLetter, id, cultures), + // AuthorizationPolicies.ContentPermissionByResource); + // + // if (!authorizationResult.Succeeded) + // { + // return Forbidden(); + // } return await authorizedHandler(); } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/SearchMediaItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/SearchMediaItemController.cs index 37543444fb..81d4f17748 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/SearchMediaItemController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/SearchMediaItemController.cs @@ -21,16 +21,21 @@ public class SearchMediaItemController : MediaItemControllerBase _mediaPresentationFactory = mediaPresentationFactory; } + [NonAction] + [Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")] + public async Task Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100) + => await SearchFromParent(cancellationToken, query, skip, take, null); + [HttpGet("search")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedModel), StatusCodes.Status200OK)] - public async Task Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100) + public async Task SearchFromParent(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null) { - PagedModel searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Media, query, skip, take); + PagedModel searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Media, query, parentId, skip, take); var result = new PagedModel { Items = searchResult.Items.OfType().Select(_mediaPresentationFactory.CreateItemResponseModel), - Total = searchResult.Total + Total = searchResult.Total, }; return await Task.FromResult(Ok(result)); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeDefaultController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeDefaultController.cs index 62b69f0a23..da6ab4b18a 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeDefaultController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeDefaultController.cs @@ -1,19 +1,37 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Services; using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Security; public class BackOfficeDefaultController : Controller { + private readonly IRuntime _umbracoRuntime; + + [ActivatorUtilitiesConstructor] + public BackOfficeDefaultController(IRuntime umbracoRuntime) + => _umbracoRuntime = umbracoRuntime; + + [Obsolete("Use the non obsoleted constructor instead. Scheduled to be removed in v17")] + public BackOfficeDefaultController() + : this(StaticServiceProvider.Instance.GetRequiredService()) + { + } + [HttpGet] [AllowAnonymous] public async Task Index(CancellationToken cancellationToken) { // force authentication to occur since this is not an authorized endpoint - AuthenticateResult result = await this.AuthenticateBackOfficeAsync(); + // a user can not be authenticated if no users have been created yet, or the user repository is unavailable + AuthenticateResult result = _umbracoRuntime.State.Level < RuntimeLevel.Upgrade + ? AuthenticateResult.Fail("RuntimeLevel " + _umbracoRuntime.State.Level + " does not support authentication") + : await this.AuthenticateBackOfficeAsync(); // if we are not authenticated then we need to redirect to the login page if (!result.Succeeded) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Server/ConfigurationServerController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Server/ConfigurationServerController.cs index 1107f499af..048ed55206 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Server/ConfigurationServerController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Server/ConfigurationServerController.cs @@ -1,9 +1,11 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Api.Management.ViewModels.Server; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; namespace Umbraco.Cms.Api.Management.Controllers.Server; @@ -11,8 +13,20 @@ namespace Umbraco.Cms.Api.Management.Controllers.Server; public class ConfigurationServerController : ServerControllerBase { private readonly SecuritySettings _securitySettings; + private readonly GlobalSettings _globalSettings; - public ConfigurationServerController(IOptions securitySettings) => _securitySettings = securitySettings.Value; + [Obsolete("Use the constructor that accepts all arguments. Will be removed in V16.")] + public ConfigurationServerController(IOptions securitySettings) + : this(securitySettings, StaticServiceProvider.Instance.GetRequiredService>()) + { + } + + [ActivatorUtilitiesConstructor] + public ConfigurationServerController(IOptions securitySettings, IOptions globalSettings) + { + _securitySettings = securitySettings.Value; + _globalSettings = globalSettings.Value; + } [HttpGet("configuration")] [MapToApiVersion("1.0")] @@ -22,6 +36,7 @@ public class ConfigurationServerController : ServerControllerBase var responseModel = new ServerConfigurationResponseModel { AllowPasswordReset = _securitySettings.AllowPasswordReset, + VersionCheckPeriod = _globalSettings.VersionCheckPeriod }; return Task.FromResult(Ok(responseModel)); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Server/UpgradeCheckServerController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Server/UpgradeCheckServerController.cs new file mode 100644 index 0000000000..10a3894457 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Server/UpgradeCheckServerController.cs @@ -0,0 +1,45 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Server; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Controllers.Server; + +[ApiVersion("1.0")] +[Authorize(Policy = AuthorizationPolicies.RequireAdminAccess)] +public class UpgradeCheckServerController : ServerControllerBase +{ + private readonly IUpgradeService _upgradeService; + private readonly IUmbracoVersion _umbracoVersion; + + public UpgradeCheckServerController(IUpgradeService upgradeService, IUmbracoVersion umbracoVersion) + { + _upgradeService = upgradeService; + _umbracoVersion = umbracoVersion; + } + + [HttpGet("upgrade-check")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(UpgradeCheckResponseModel), StatusCodes.Status200OK)] + public async Task UpgradeCheck(CancellationToken cancellationToken) + { + UpgradeResult upgradeResult = await _upgradeService.CheckUpgrade(_umbracoVersion.SemanticVersion); + + var responseModel = new UpgradeCheckResponseModel + { + Type = upgradeResult.UpgradeType, + Comment = upgradeResult.Comment, + Url = upgradeResult.UpgradeUrl.IsNullOrWhiteSpace() + ? string.Empty + : $"{upgradeResult.UpgradeUrl}?version={_umbracoVersion.Version.ToString(3)}" + }; + + return Ok(responseModel); + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/ApplicationBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/ApplicationBuilderExtensions.cs index 9018b97bee..870c4d3e1e 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/ApplicationBuilderExtensions.cs @@ -33,6 +33,7 @@ internal static class ApplicationBuilderExtensions { innerBuilder.UseExceptionHandler(exceptionBuilder => exceptionBuilder.Run(async context => { + var isDebug = context.RequestServices.GetRequiredService().IsDebugMode; Exception? exception = context.Features.Get()?.Error; if (exception is null) { @@ -42,16 +43,16 @@ internal static class ApplicationBuilderExtensions var response = new ProblemDetails { Title = exception.Message, - Detail = exception.StackTrace, + Detail = isDebug ? exception.StackTrace : null, Status = StatusCodes.Status500InternalServerError, - Instance = exception.GetType().Name, + Instance = isDebug ? exception.GetType().Name : null, Type = "Error" }; await context.Response.WriteAsJsonAsync(response); })); }); - internal static IApplicationBuilder UseEndpoints(this IApplicationBuilder applicationBuilder) +internal static IApplicationBuilder UseEndpoints(this IApplicationBuilder applicationBuilder) { IServiceProvider provider = applicationBuilder.ApplicationServices; diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs index e56dadb400..3a2c860ade 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs @@ -29,6 +29,7 @@ internal static class BackOfficeAuthPolicyBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddAuthorization(CreatePolicies); return builder; @@ -46,7 +47,7 @@ internal static class BackOfficeAuthPolicyBuilderExtensions options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy => { policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); - policy.RequireAuthenticatedUser(); + policy.Requirements.Add(new BackOfficeRequirement()); }); options.AddPolicy(AuthorizationPolicies.RequireAdminAccess, policy => diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentNotificationPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentNotificationPresentationFactory.cs index 549163df91..7daf762b78 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentNotificationPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentNotificationPresentationFactory.cs @@ -27,13 +27,14 @@ internal sealed class DocumentNotificationPresentationFactory : IDocumentNotific .ToArray() ?? Array.Empty(); - var availableActionIds = _actionCollection.Where(a => a.ShowInNotifier).Select(a => a.Letter.ToString()).ToArray(); - - return await Task.FromResult( - availableActionIds.Select(actionId => new DocumentNotificationResponseModel + return await Task.FromResult(_actionCollection + .Where(action => action.ShowInNotifier) + .Select(action => new DocumentNotificationResponseModel { - ActionId = actionId, - Subscribed = subscribedActionIds.Contains(actionId) - }).ToArray()); + ActionId = action.Letter, + Alias = action.Alias, + Subscribed = subscribedActionIds.Contains(action.Letter) + }) + .ToArray()); } } diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 62bf741f9b..6482df8801 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -9875,6 +9875,14 @@ "format": "int32", "default": 100 } + }, + { + "name": "parentId", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + } } ], "responses": { @@ -15545,6 +15553,14 @@ "format": "int32", "default": 100 } + }, + { + "name": "parentId", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + } } ], "responses": { @@ -25176,6 +25192,41 @@ ] } }, + "/umbraco/management/api/v1/server/upgrade-check": { + "get": { + "tags": [ + "Server" + ], + "operationId": "GetServerUpgradeCheck", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpgradeCheckResponseModel" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/item/static-file": { "get": { "tags": [ @@ -36244,6 +36295,7 @@ "DocumentNotificationResponseModel": { "required": [ "actionId", + "alias", "subscribed" ], "type": "object", @@ -36251,6 +36303,9 @@ "actionId": { "type": "string" }, + "alias": { + "type": "string" + }, "subscribed": { "type": "boolean" } @@ -37907,7 +37962,9 @@ }, "providerProperties": { "type": "object", - "additionalProperties": { }, + "additionalProperties": { + "nullable": true + }, "nullable": true } }, @@ -42446,12 +42503,17 @@ }, "ServerConfigurationResponseModel": { "required": [ - "allowPasswordReset" + "allowPasswordReset", + "versionCheckPeriod" ], "type": "object", "properties": { "allowPasswordReset": { "type": "boolean" + }, + "versionCheckPeriod": { + "type": "integer", + "format": "int32" } }, "additionalProperties": false @@ -44514,6 +44576,26 @@ }, "additionalProperties": false }, + "UpgradeCheckResponseModel": { + "required": [ + "comment", + "type", + "url" + ], + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "comment": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, "UpgradeSettingsResponseModel": { "required": [ "currentState", @@ -45275,4 +45357,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/DenyLocalLogin/DenyLocalLoginHandler.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/DenyLocalLogin/DenyLocalLoginHandler.cs index e57eb6c742..cd9a675bfc 100644 --- a/src/Umbraco.Cms.Api.Management/Security/Authorization/DenyLocalLogin/DenyLocalLoginHandler.cs +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/DenyLocalLogin/DenyLocalLoginHandler.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization.Infrastructure; +using Umbraco.Cms.Api.Management.Security.Authorization.User; namespace Umbraco.Cms.Api.Management.Security.Authorization.DenyLocalLogin; @@ -24,12 +25,12 @@ public class DenyLocalLoginHandler : MustSatisfyRequirementAuthorizationHandler< if (isDenied is false) { - // AuthorizationPolicies.BackOfficeAccess policy adds this requirement by policy.RequireAuthenticatedUser() + // AuthorizationPolicies.BackOfficeAccess policy adds this requirement by policy.Requirements.Add(new BackOfficeRequirement()); // Since we want to "allow anonymous" for some endpoints (i.e. BackOfficeController.Login()), it is necessary to succeed this requirement - IEnumerable denyAnonymousUserRequirements = context.PendingRequirements.OfType(); - foreach (DenyAnonymousAuthorizationRequirement denyAnonymousUserRequirement in denyAnonymousUserRequirements) + IEnumerable backOfficeRequirements = context.PendingRequirements.OfType(); + foreach (BackOfficeRequirement backOfficeRequirement in backOfficeRequirements) { - context.Succeed(denyAnonymousUserRequirement); + context.Succeed(backOfficeRequirement); } } diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/User/BackOfficeHandler.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/BackOfficeHandler.cs new file mode 100644 index 0000000000..ff79e344ae --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/BackOfficeHandler.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Authorization; +using Umbraco.Cms.Core.Security; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.User; + +/// +/// Ensures authorization is successful for a back office user. +/// +public class BackOfficeHandler : MustSatisfyRequirementAuthorizationHandler +{ + private readonly IBackOfficeSecurityAccessor _backOfficeSecurity; + + public BackOfficeHandler(IBackOfficeSecurityAccessor backOfficeSecurity) + { + _backOfficeSecurity = backOfficeSecurity; + } + + protected override Task IsAuthorized(AuthorizationHandlerContext context, BackOfficeRequirement requirement) + { + + if (context.HasFailed is false && context.HasSucceeded is true) + { + return Task.FromResult(true); + } + + if (!_backOfficeSecurity.BackOfficeSecurity?.IsAuthenticated() ?? false) + { + return Task.FromResult(false); + } + + var userApprovalSucceeded = !requirement.RequireApproval || + (_backOfficeSecurity.BackOfficeSecurity?.CurrentUser?.IsApproved ?? false); + return Task.FromResult(userApprovalSucceeded); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/User/BackOfficeRequirement.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/BackOfficeRequirement.cs new file mode 100644 index 0000000000..8c6f97b24f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/BackOfficeRequirement.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.User; + +/// +/// Authorization requirement for the . +/// +public class BackOfficeRequirement : IAuthorizationRequirement +{ + /// + /// Initializes a new instance of the class. + /// + /// Flag for whether back-office user approval is required. + public BackOfficeRequirement(bool requireApproval = true) => RequireApproval = requireApproval; + + /// + /// Gets a value indicating whether back-office user approval is required. + /// + public bool RequireApproval { get; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Content/PropertyValidationResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Content/PropertyValidationResponseModel.cs new file mode 100644 index 0000000000..6f8d918c3e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Content/PropertyValidationResponseModel.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Content; + +public class PropertyValidationResponseModel +{ + public string[] Messages { get; set; } = Array.Empty(); + + public string Alias { get; set; } = string.Empty; + + public string? Culture { get; set; } + + public string? Segment { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentNotificationsResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentNotificationsResponseModel.cs index b3ae3b8f6e..cf7665a643 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentNotificationsResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentNotificationsResponseModel.cs @@ -4,5 +4,7 @@ public class DocumentNotificationResponseModel { public required string ActionId { get; set; } + public required string Alias { get; set; } + public required bool Subscribed { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Server/ServerConfigurationResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Server/ServerConfigurationResponseModel.cs index f759cdca32..a424a24798 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Server/ServerConfigurationResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Server/ServerConfigurationResponseModel.cs @@ -3,4 +3,6 @@ public class ServerConfigurationResponseModel { public bool AllowPasswordReset { get; set; } + + public int VersionCheckPeriod { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Server/UpgradeCheckResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Server/UpgradeCheckResponseModel.cs new file mode 100644 index 0000000000..0c84f0a837 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Server/UpgradeCheckResponseModel.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Server; + +public class UpgradeCheckResponseModel +{ + public required string Type { get; init; } + + public required string Comment { get; init; } + + public required string Url { get; init; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Server/VersionResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Server/VersionResponseModel.cs index f53e8f17d0..d421d99095 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Server/VersionResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Server/VersionResponseModel.cs @@ -2,6 +2,7 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Server; +[Obsolete("Not used. Will be removed in V15.")] public class VersionResponseModel { [Required] diff --git a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Umbraco.Cms.Persistence.EFCore.SqlServer.csproj b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Umbraco.Cms.Persistence.EFCore.SqlServer.csproj index a43a5f01f2..a1864f636f 100644 --- a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Umbraco.Cms.Persistence.EFCore.SqlServer.csproj +++ b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Umbraco.Cms.Persistence.EFCore.SqlServer.csproj @@ -7,6 +7,9 @@ + + + diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj b/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj index 084c1b3b95..b6a342b7b8 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj +++ b/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj @@ -18,6 +18,9 @@ + + + diff --git a/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj b/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj index b567868e11..3a914df904 100644 --- a/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj +++ b/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj @@ -40,9 +40,14 @@ - - - + + + + + + + + diff --git a/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj b/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj index ac3b5b2c4e..71e433cc12 100644 --- a/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj +++ b/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj @@ -47,7 +47,7 @@ - + diff --git a/src/Umbraco.Core/Constants-Composing.cs b/src/Umbraco.Core/Constants-Composing.cs index e55c32d01a..defdf2fa93 100644 --- a/src/Umbraco.Core/Constants-Composing.cs +++ b/src/Umbraco.Core/Constants-Composing.cs @@ -13,7 +13,7 @@ public static partial class Constants public static readonly string[] UmbracoCoreAssemblyNames = { "Umbraco.Core", "Umbraco.Infrastructure", "Umbraco.PublishedCache.NuCache", "Umbraco.Examine.Lucene", - "Umbraco.Web.Common", "Umbraco.Web.BackOffice", "Umbraco.Web.Website", + "Umbraco.Web.Common", "Umbraco.Cms.Api.Common","Umbraco.Cms.Api.Delivery","Umbraco.Cms.Api.Management", "Umbraco.Web.Website", }; } } diff --git a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs index e4b11ccb6c..66c5002604 100644 --- a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs +++ b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs @@ -8,7 +8,7 @@ using Umbraco.Cms.Core.Models.Entities; namespace Umbraco.Extensions; /// -/// Provides extension methods that return udis for Umbraco entities. +/// Provides extension methods that return UDIs for Umbraco entities. /// public static class UdiGetterExtensions { @@ -19,11 +19,177 @@ public static class UdiGetterExtensions /// /// The entity identifier of the entity. /// - public static GuidUdi GetUdi(this ITemplate entity) + public static Udi GetUdi(this IEntity entity) { ArgumentNullException.ThrowIfNull(entity); - return new GuidUdi(Constants.UdiEntityType.Template, entity.Key).EnsureClosed(); + return entity switch + { + // Concrete types + EntityContainer container => container.GetUdi(), + Script script => script.GetUdi(), + Stylesheet stylesheet => stylesheet.GetUdi(), + // Interfaces + IContentBase contentBase => contentBase.GetUdi(), + IContentTypeComposition contentTypeComposition => contentTypeComposition.GetUdi(), + IDataType dataType => dataType.GetUdi(), + IDictionaryItem dictionaryItem => dictionaryItem.GetUdi(), + ILanguage language => language.GetUdi(), + IMemberGroup memberGroup => memberGroup.GetUdi(), + IPartialView partialView => partialView.GetUdi(), + IRelationType relationType => relationType.GetUdi(), + ITemplate template => template.GetUdi(), + IWebhook webhook => webhook.GetUdi(), + _ => throw new NotSupportedException($"Entity type {entity.GetType().FullName} is not supported."), + }; + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// + /// The entity identifier of the entity. + /// + public static GuidUdi GetUdi(this EntityContainer entity) + { + ArgumentNullException.ThrowIfNull(entity); + + string entityType; + if (entity.ContainedObjectType == Constants.ObjectTypes.DataType) + { + entityType = Constants.UdiEntityType.DataTypeContainer; + } + else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentType) + { + entityType = Constants.UdiEntityType.DocumentTypeContainer; + } + else if (entity.ContainedObjectType == Constants.ObjectTypes.MediaType) + { + entityType = Constants.UdiEntityType.MediaTypeContainer; + } + else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentBlueprint) + { + entityType = Constants.UdiEntityType.DocumentBlueprintContainer; + } + else + { + throw new NotSupportedException($"Contained object type {entity.ContainedObjectType} is not supported."); + } + + return new GuidUdi(entityType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// + /// The entity identifier of the entity. + /// + public static StringUdi GetUdi(this Script entity) + { + ArgumentNullException.ThrowIfNull(entity); + + return GetUdiFromPath(Constants.UdiEntityType.Script, entity.Path); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// + /// The entity identifier of the entity. + /// + public static StringUdi GetUdi(this Stylesheet entity) + { + ArgumentNullException.ThrowIfNull(entity); + + return GetUdiFromPath(Constants.UdiEntityType.Stylesheet, entity.Path); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// + /// The entity identifier of the entity. + /// + public static GuidUdi GetUdi(this IContentBase entity) + { + ArgumentNullException.ThrowIfNull(entity); + + return entity switch + { + IContent content => content.GetUdi(), + IMedia media => media.GetUdi(), + IMember member => member.GetUdi(), + _ => throw new NotSupportedException($"Content base type {entity.GetType().FullName} is not supported."), + }; + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// + /// The entity identifier of the entity. + /// + public static GuidUdi GetUdi(this IContent entity) + { + ArgumentNullException.ThrowIfNull(entity); + + string entityType = entity.Blueprint ? Constants.UdiEntityType.DocumentBlueprint : Constants.UdiEntityType.Document; + + return new GuidUdi(entityType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// + /// The entity identifier of the entity. + /// + public static GuidUdi GetUdi(this IMedia entity) + { + ArgumentNullException.ThrowIfNull(entity); + + return new GuidUdi(Constants.UdiEntityType.Media, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// + /// The entity identifier of the entity. + /// + public static GuidUdi GetUdi(this IMember entity) + { + ArgumentNullException.ThrowIfNull(entity); + + return new GuidUdi(Constants.UdiEntityType.Member, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// + /// The entity identifier of the entity. + /// + public static GuidUdi GetUdi(this IContentTypeComposition entity) + { + ArgumentNullException.ThrowIfNull(entity); + + return entity switch + { + IContentType contentType => contentType.GetUdi(), + IMediaType mediaType => mediaType.GetUdi(), + IMemberType memberType => memberType.GetUdi(), + _ => throw new NotSupportedException($"Composition type {entity.GetType().FullName} is not supported."), + }; } /// @@ -68,42 +234,6 @@ public static class UdiGetterExtensions return new GuidUdi(Constants.UdiEntityType.MemberType, entity.Key).EnsureClosed(); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// - /// The entity identifier of the entity. - /// - public static GuidUdi GetUdi(this IMemberGroup entity) - { - ArgumentNullException.ThrowIfNull(entity); - - return new GuidUdi(Constants.UdiEntityType.MemberGroup, entity.Key).EnsureClosed(); - } - - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// - /// The entity identifier of the entity. - /// - public static GuidUdi GetUdi(this IContentTypeComposition entity) - { - ArgumentNullException.ThrowIfNull(entity); - - string entityType = entity switch - { - IContentType => Constants.UdiEntityType.DocumentType, - IMediaType => Constants.UdiEntityType.MediaType, - IMemberType => Constants.UdiEntityType.MemberType, - _ => throw new NotSupportedException(string.Format("Composition type {0} is not supported.", entity.GetType().FullName)), - }; - - return new GuidUdi(entityType, entity.Key).EnsureClosed(); - } - /// /// Gets the entity identifier of the entity. /// @@ -118,129 +248,6 @@ public static class UdiGetterExtensions return new GuidUdi(Constants.UdiEntityType.DataType, entity.Key).EnsureClosed(); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// - /// The entity identifier of the entity. - /// - public static GuidUdi GetUdi(this EntityContainer entity) - { - ArgumentNullException.ThrowIfNull(entity); - - string entityType; - if (entity.ContainedObjectType == Constants.ObjectTypes.DataType) - { - entityType = Constants.UdiEntityType.DataTypeContainer; - } - else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentType) - { - entityType = Constants.UdiEntityType.DocumentTypeContainer; - } - else if (entity.ContainedObjectType == Constants.ObjectTypes.MediaType) - { - entityType = Constants.UdiEntityType.MediaTypeContainer; - } - else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentBlueprint) - { - entityType = Constants.UdiEntityType.DocumentBlueprintContainer; - } - else - { - throw new NotSupportedException(string.Format("Contained object type {0} is not supported.", entity.ContainedObjectType)); - } - - return new GuidUdi(entityType, entity.Key).EnsureClosed(); - } - - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// - /// The entity identifier of the entity. - /// - public static GuidUdi GetUdi(this IMedia entity) - { - ArgumentNullException.ThrowIfNull(entity); - - return new GuidUdi(Constants.UdiEntityType.Media, entity.Key).EnsureClosed(); - } - - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// - /// The entity identifier of the entity. - /// - public static GuidUdi GetUdi(this IContent entity) - { - ArgumentNullException.ThrowIfNull(entity); - - string entityType = entity.Blueprint ? Constants.UdiEntityType.DocumentBlueprint : Constants.UdiEntityType.Document; - - return new GuidUdi(entityType, entity.Key).EnsureClosed(); - } - - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// - /// The entity identifier of the entity. - /// - public static GuidUdi GetUdi(this IMember entity) - { - ArgumentNullException.ThrowIfNull(entity); - - return new GuidUdi(Constants.UdiEntityType.Member, entity.Key).EnsureClosed(); - } - - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// - /// The entity identifier of the entity. - /// - public static StringUdi GetUdi(this Stylesheet entity) - { - ArgumentNullException.ThrowIfNull(entity); - - return GetUdiFromPath(Constants.UdiEntityType.Stylesheet, entity.Path); - } - - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// - /// The entity identifier of the entity. - /// - public static StringUdi GetUdi(this Script entity) - { - ArgumentNullException.ThrowIfNull(entity); - - return GetUdiFromPath(Constants.UdiEntityType.Script, entity.Path); - } - - /// - /// Gets the UDI from a path. - /// - /// The type of the entity. - /// The path. - /// - /// The entity identifier of the entity. - /// - private static StringUdi GetUdiFromPath(string entityType, string path) - { - string id = path.TrimStart(Constants.CharArrays.ForwardSlash).Replace("\\", "/"); - - return new StringUdi(entityType, id).EnsureClosed(); - } - /// /// Gets the entity identifier of the entity. /// @@ -262,11 +269,11 @@ public static class UdiGetterExtensions /// /// The entity identifier of the entity. /// - public static StringUdi GetUdi(this IPartialView entity) + public static StringUdi GetUdi(this ILanguage entity) { ArgumentNullException.ThrowIfNull(entity); - return GetUdiFromPath(Constants.UdiEntityType.PartialView, entity.Path); + return new StringUdi(Constants.UdiEntityType.Language, entity.IsoCode).EnsureClosed(); } /// @@ -276,19 +283,25 @@ public static class UdiGetterExtensions /// /// The entity identifier of the entity. /// - public static GuidUdi GetUdi(this IContentBase entity) + public static GuidUdi GetUdi(this IMemberGroup entity) { ArgumentNullException.ThrowIfNull(entity); - string type = entity switch - { - IContent => Constants.UdiEntityType.Document, - IMedia => Constants.UdiEntityType.Media, - IMember => Constants.UdiEntityType.Member, - _ => throw new NotSupportedException(string.Format("Content base type {0} is not supported.", entity.GetType().FullName)), - }; + return new GuidUdi(Constants.UdiEntityType.MemberGroup, entity.Key).EnsureClosed(); + } - return new GuidUdi(type, entity.Key).EnsureClosed(); + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// + /// The entity identifier of the entity. + /// + public static StringUdi GetUdi(this IPartialView entity) + { + ArgumentNullException.ThrowIfNull(entity); + + return GetUdiFromPath(Constants.UdiEntityType.PartialView, entity.Path); } /// @@ -305,6 +318,20 @@ public static class UdiGetterExtensions return new GuidUdi(Constants.UdiEntityType.RelationType, entity.Key).EnsureClosed(); } + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// + /// The entity identifier of the entity. + /// + public static GuidUdi GetUdi(this ITemplate entity) + { + ArgumentNullException.ThrowIfNull(entity); + + return new GuidUdi(Constants.UdiEntityType.Template, entity.Key).EnsureClosed(); + } + /// /// Gets the entity identifier of the entity. /// @@ -320,56 +347,17 @@ public static class UdiGetterExtensions } /// - /// Gets the entity identifier of the entity. + /// Gets the UDI from a path. /// - /// The entity. + /// The type of the entity. + /// The path. /// /// The entity identifier of the entity. /// - public static StringUdi GetUdi(this ILanguage entity) + private static StringUdi GetUdiFromPath(string entityType, string path) { - ArgumentNullException.ThrowIfNull(entity); + string id = path.TrimStart(Constants.CharArrays.ForwardSlash).Replace("\\", "/"); - return new StringUdi(Constants.UdiEntityType.Language, entity.IsoCode).EnsureClosed(); - } - - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// - /// The entity identifier of the entity. - /// - public static Udi GetUdi(this IEntity entity) - { - ArgumentNullException.ThrowIfNull(entity); - - return entity switch - { - // Concrete types - EntityContainer container => container.GetUdi(), - Stylesheet stylesheet => stylesheet.GetUdi(), - Script script => script.GetUdi(), - // Content types - IContentType contentType => contentType.GetUdi(), - IMediaType mediaType => mediaType.GetUdi(), - IMemberType memberType => memberType.GetUdi(), - IContentTypeComposition contentTypeComposition => contentTypeComposition.GetUdi(), - // Content - IContent content => content.GetUdi(), - IMedia media => media.GetUdi(), - IMember member => member.GetUdi(), - IContentBase contentBase => contentBase.GetUdi(), - // Other - IDataType dataTypeComposition => dataTypeComposition.GetUdi(), - IDictionaryItem dictionaryItem => dictionaryItem.GetUdi(), - ILanguage language => language.GetUdi(), - IMemberGroup memberGroup => memberGroup.GetUdi(), - IPartialView partialView => partialView.GetUdi(), - IRelationType relationType => relationType.GetUdi(), - ITemplate template => template.GetUdi(), - IWebhook webhook => webhook.GetUdi(), - _ => throw new NotSupportedException(string.Format("Entity type {0} is not supported.", entity.GetType().FullName)), - }; + return new StringUdi(entityType, id).EnsureClosed(); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs b/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs index 61c6b2d13f..9f0a224e76 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs @@ -6,5 +6,16 @@ namespace Umbraco.Cms.Core.Media.EmbedProviders; /// Wrapper class for OEmbed response. /// [DataContract] -public class OEmbedResponse : OEmbedResponseBase; +public class OEmbedResponse : OEmbedResponseBase +{ + + // these is only here to avoid breaking changes. In theory it should still be source code compatible to remove them. + public new double? ThumbnailHeight => base.ThumbnailHeight; + + public new double? ThumbnailWidth => base.ThumbnailWidth; + + public new double? Height => base.Height; + + public new double? Width => base.Width; +} diff --git a/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponseBase.cs b/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponseBase.cs index 57e4c878c7..c71ecb8391 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponseBase.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponseBase.cs @@ -34,7 +34,7 @@ public abstract class OEmbedResponseBase public string? ThumbnailUrl { get; set; } [DataMember(Name = "thumbnail_height")] - public T? ThumbnailHeight { get; set; } + public virtual T? ThumbnailHeight { get; set; } [DataMember(Name = "thumbnail_width")] public T? ThumbnailWidth { get; set; } diff --git a/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs index 9cf0d52251..e6190b049a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs @@ -16,14 +16,10 @@ public class UpgradeCheckRepository : IUpgradeCheckRepository { try { - if (_httpClient == null) - { - _httpClient = new HttpClient(); - } + _httpClient ??= new HttpClient { Timeout = TimeSpan.FromSeconds(1) }; using var content = new StringContent(_jsonSerializer.Serialize(new CheckUpgradeDto(version)), Encoding.UTF8, "application/json"); - _httpClient.Timeout = TimeSpan.FromSeconds(1); using HttpResponseMessage task = await _httpClient.PostAsync(RestApiUpgradeChecklUrl, content); var json = await task.Content.ReadAsStringAsync(); UpgradeResult? result = _jsonSerializer.Deserialize(json); diff --git a/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs index 6caa7593ab..92e3678e7d 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs @@ -7,7 +7,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.Validators; /// /// A validator that validates that the value is not null or empty (if it is a string) /// -public sealed class RequiredValidator : IValueRequiredValidator, IValueValidator +public class RequiredValidator : IValueRequiredValidator, IValueValidator { [Obsolete($"Use the constructor that does not accept {nameof(ILocalizedTextService)}. Will be removed in V15.")] public RequiredValidator(ILocalizedTextService textService) @@ -24,7 +24,7 @@ public sealed class RequiredValidator : IValueRequiredValidator, IValueValidator ValidateRequired(value, valueType); /// - public IEnumerable ValidateRequired(object? value, string? valueType) + public virtual IEnumerable ValidateRequired(object? value, string? valueType) { if (value == null) { diff --git a/src/Umbraco.Core/Services/ContentEditingService.cs b/src/Umbraco.Core/Services/ContentEditingService.cs index 2ad8365bcf..bc15e7ea44 100644 --- a/src/Umbraco.Core/Services/ContentEditingService.cs +++ b/src/Umbraco.Core/Services/ContentEditingService.cs @@ -1,9 +1,13 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services; @@ -12,7 +16,11 @@ internal sealed class ContentEditingService { private readonly ITemplateService _templateService; private readonly ILogger _logger; + private readonly IUserService _userService; + private readonly ILocalizationService _localizationService; + private readonly ILanguageService _languageService; + [Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 16.")] public ContentEditingService( IContentService contentService, IContentTypeService contentTypeService, @@ -24,10 +32,46 @@ internal sealed class ContentEditingService IUserIdKeyResolver userIdKeyResolver, ITreeEntitySortingService treeEntitySortingService, IContentValidationService contentValidationService) + : this( + contentService, + contentTypeService, + propertyEditorCollection, + dataTypeService, + templateService, + logger, + scopeProvider, + userIdKeyResolver, + treeEntitySortingService, + contentValidationService, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService() + ) + { + + } + + public ContentEditingService( + IContentService contentService, + IContentTypeService contentTypeService, + PropertyEditorCollection propertyEditorCollection, + IDataTypeService dataTypeService, + ITemplateService templateService, + ILogger logger, + ICoreScopeProvider scopeProvider, + IUserIdKeyResolver userIdKeyResolver, + ITreeEntitySortingService treeEntitySortingService, + IContentValidationService contentValidationService, + IUserService userService, + ILocalizationService localizationService, + ILanguageService languageService) : base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, contentValidationService, treeEntitySortingService) { _templateService = templateService; _logger = logger; + _userService = userService; + _localizationService = localizationService; + _languageService = languageService; } public async Task GetAsync(Guid key) @@ -65,7 +109,7 @@ internal sealed class ContentEditingService ContentEditingOperationStatus validationStatus = result.Status; ContentValidationResult validationResult = result.Result.ValidationResult; - IContent content = result.Result.Content!; + IContent content = await EnsureOnlyAllowedFieldsAreUpdated(result.Result.Content!, userKey); ContentEditingOperationStatus updateTemplateStatus = await UpdateTemplateAsync(content, createModel.TemplateKey); if (updateTemplateStatus != ContentEditingOperationStatus.Success) { @@ -78,6 +122,53 @@ internal sealed class ContentEditingService : Attempt.FailWithStatus(saveStatus, new ContentCreateResult { Content = content }); } + /// + /// A temporary method that ensures the data is sent in is overridden by the original data, in cases where the user do not have permissions to change the data. + /// + private async Task EnsureOnlyAllowedFieldsAreUpdated(IContent contentWithPotentialUnallowedChanges, Guid userKey) + { + if (contentWithPotentialUnallowedChanges.ContentType.VariesByCulture() is false) + { + return contentWithPotentialUnallowedChanges; + } + + IContent? existingContent = await GetAsync(contentWithPotentialUnallowedChanges.Key); + + IUser? user = await _userService.GetAsync(userKey); + + if (user is null) + { + return contentWithPotentialUnallowedChanges; + } + + var allowedLanguageIds = user.CalculateAllowedLanguageIds(_localizationService)!; + + var allowedCultures = (await _languageService.GetIsoCodesByIdsAsync(allowedLanguageIds)).ToHashSet(); + + foreach (var culture in contentWithPotentialUnallowedChanges.EditedCultures ?? contentWithPotentialUnallowedChanges.PublishedCultures) + { + if (allowedCultures.Contains(culture)) + { + continue; + } + + + // else override the updates values with the original values. + foreach (IProperty property in contentWithPotentialUnallowedChanges.Properties) + { + if (property.PropertyType.VariesByCulture() is false) + { + continue; + } + + var value = existingContent?.Properties.First(x=>x.Alias == property.Alias).GetValue(culture, null, false); + property.SetValue(value, culture, null); + } + } + + return contentWithPotentialUnallowedChanges; + } + public async Task> UpdateAsync(Guid key, ContentUpdateModel updateModel, Guid userKey) { IContent? content = ContentService.GetById(key); @@ -102,6 +193,8 @@ internal sealed class ContentEditingService ContentEditingOperationStatus validationStatus = result.Status; ContentValidationResult validationResult = result.Result.ValidationResult; + content = await EnsureOnlyAllowedFieldsAreUpdated(content, userKey); + ContentEditingOperationStatus updateTemplateStatus = await UpdateTemplateAsync(content, updateModel.TemplateKey); if (updateTemplateStatus != ContentEditingOperationStatus.Success) { diff --git a/src/Umbraco.Core/Services/FileService.cs b/src/Umbraco.Core/Services/FileService.cs index 8ef37b76bf..b9c435ebab 100644 --- a/src/Umbraco.Core/Services/FileService.cs +++ b/src/Umbraco.Core/Services/FileService.cs @@ -523,8 +523,6 @@ public class FileService : RepositoryService, IFileService /// /// List of to save /// Optional id of the user - // FIXME: we need to re-implement PackageDataInstallation.ImportTemplates so it imports templates in the correct order - // instead of relying on being able to save invalid templates (child templates whose master has yet to be created) [Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")] public void SaveTemplate(IEnumerable templates, int userId = Constants.Security.SuperUserId) { diff --git a/src/Umbraco.Core/Services/IDataTypeService.cs b/src/Umbraco.Core/Services/IDataTypeService.cs index 8536c8a528..e2f808cc7e 100644 --- a/src/Umbraco.Core/Services/IDataTypeService.cs +++ b/src/Umbraco.Core/Services/IDataTypeService.cs @@ -196,7 +196,7 @@ public interface IDataTypeService : IService /// /// Alias of the property editor /// Collection of configured for the property editor - Task> GetByEditorAliasAsync(string propertyEditorAlias); + Task> GetByEditorAliasAsync(string propertyEditorAlias) => Task.FromResult(GetByEditorAlias(propertyEditorAlias)); /// /// Gets all for a given editor UI alias @@ -246,5 +246,5 @@ public interface IDataTypeService : IService /// /// Aliases of the property editors /// Collection of configured for the property editors - Task> GetByEditorAliasAsync(string[] propertyEditorAlias); + Task> GetByEditorAliasAsync(string[] propertyEditorAlias) => Task.FromResult(propertyEditorAlias.SelectMany(x=>GetByEditorAlias(x))); } diff --git a/src/Umbraco.Core/Services/IIndexedEntitySearchService.cs b/src/Umbraco.Core/Services/IIndexedEntitySearchService.cs index 047f5e7e2f..4463733146 100644 --- a/src/Umbraco.Core/Services/IIndexedEntitySearchService.cs +++ b/src/Umbraco.Core/Services/IIndexedEntitySearchService.cs @@ -13,4 +13,8 @@ namespace Umbraco.Cms.Core.Services; public interface IIndexedEntitySearchService { PagedModel Search(UmbracoObjectTypes objectType, string query, int skip = 0, int take = 100, bool ignoreUserStartNodes = false); + + // default implementation to avoid breaking changes falls back to old behaviour + PagedModel Search(UmbracoObjectTypes objectType, string query, Guid? parentId, int skip = 0, int take = 100, bool ignoreUserStartNodes = false) + => Search(objectType,query, skip, take, ignoreUserStartNodes); } diff --git a/src/Umbraco.Core/Services/IPackageDataInstallation.cs b/src/Umbraco.Core/Services/IPackageDataInstallation.cs index 5d9eaa22d2..957c2d1349 100644 --- a/src/Umbraco.Core/Services/IPackageDataInstallation.cs +++ b/src/Umbraco.Core/Services/IPackageDataInstallation.cs @@ -65,16 +65,28 @@ public interface IPackageDataInstallation /// An enumerable list of generated languages IReadOnlyList ImportLanguages(IEnumerable languageElements, int userId); + [Obsolete("Use Async version instead, Scheduled to be removed in v17")] IEnumerable ImportTemplate(XElement templateElement, int userId); + Task> ImportTemplateAsync(XElement templateElement, int userId) => Task.FromResult(ImportTemplate(templateElement, userId)); + /// /// Imports and saves package xml as /// /// Xml to import /// Optional user id /// An enumerable list of generated Templates + [Obsolete("Use Async version instead, Scheduled to be removed in v17")] IReadOnlyList ImportTemplates(IReadOnlyCollection templateElements, int userId); + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Optional user id + /// An enumerable list of generated Templates + Task> ImportTemplatesAsync(IReadOnlyCollection templateElements, int userId) => Task.FromResult(ImportTemplates(templateElements, userId)); + Guid GetContentTypeKey(XElement contentType); string? GetEntityTypeAlias(XElement entityType); diff --git a/src/Umbraco.Core/Services/OperationStatus/ContentTypeOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/ContentTypeOperationStatus.cs index 32f28eaf16..2379f51c02 100644 --- a/src/Umbraco.Core/Services/OperationStatus/ContentTypeOperationStatus.cs +++ b/src/Umbraco.Core/Services/OperationStatus/ContentTypeOperationStatus.cs @@ -5,10 +5,7 @@ public enum ContentTypeOperationStatus Success, DuplicateAlias, InvalidAlias, - NameCannotBeEmpty, - NameTooLong, InvalidPropertyTypeAlias, - PropertyTypeAliasCannotEqualContentTypeAlias, DuplicatePropertyTypeAlias, DataTypeNotFound, InvalidInheritance, @@ -21,6 +18,9 @@ public enum ContentTypeOperationStatus NotFound, NotAllowed, CancelledByNotification, + PropertyTypeAliasCannotEqualContentTypeAlias, + NameCannotBeEmpty, + NameTooLong, InvalidElementFlagDocumentHasContent, InvalidElementFlagElementIsUsedInPropertyEditorConfiguration, InvalidElementFlagComparedToParent, diff --git a/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs b/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs index e843d7954b..82d33ee480 100644 --- a/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs +++ b/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs @@ -356,8 +356,16 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher throw new ArgumentNullException(nameof(entityService)); } - UdiParser.TryParse(searchFrom, true, out Udi? udi); - searchFrom = udi == null ? searchFrom : entityService.GetId(udi).Result.ToString(); + if (Guid.TryParse(searchFrom, out Guid guid)) + { + searchFrom = entityService.GetId(guid, objectType).Result.ToString(); + } + else + { + // fallback to Udi for legacy reasons as the calling methods take string? + UdiParser.TryParse(searchFrom, true, out Udi? udi); + searchFrom = udi == null ? searchFrom : entityService.GetId(udi).Result.ToString(); + } TreeEntityPath? entityPath = int.TryParse(searchFrom, NumberStyles.Integer, CultureInfo.InvariantCulture, out var searchFromId) && diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 6201691d9f..6d99898cd6 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -26,6 +26,7 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PropertyEditors.Validators; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; @@ -237,6 +238,8 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + return builder; } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index b7bc79997f..5bd401be67 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -113,7 +113,9 @@ public static partial class UmbracoBuilderExtensions factory.GetRequiredService(), factory.GetRequiredService(), factory.GetRequiredService(), - factory.GetRequiredService()); + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService()); private static LocalizedTextServiceFileSources CreateLocalizedTextServiceFileSourcesFactory( IServiceProvider container) diff --git a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs index fa5a51bc72..3388da27e5 100644 --- a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs +++ b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs @@ -53,7 +53,6 @@ public class FilePermissionHelper : IFilePermissionHelper { hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Bin), hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Umbraco), - hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoPath), hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Packages), }; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index d2bed38ce3..713168a6c7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -63,18 +63,19 @@ public class UmbracoPlan : MigrationPlan To("{21C42760-5109-4C03-AB4F-7EA53577D1F5}"); To("{6158F3A3-4902-4201-835E-1ED7F810B2D8}"); To("{985AF2BA-69D3-4DBA-95E0-AD3FA7459FA7}"); + To("{CC47C751-A81B-489A-A2BC-0240245DB687}"); // To 14.0.0 To("{419827A0-4FCE-464B-A8F3-247C6092AF55}"); - To("{69E12556-D9B3-493A-8E8A-65EC89FB658D}"); - To("{F2B16CD4-F181-4BEE-81C9-11CF384E6025}"); - To("{A8E01644-9F2E-4988-8341-587EF5B7EA69}"); + To("{69E12556-D9B3-493A-8E8A-65EC89FB658D}"); + To("{F2B16CD4-F181-4BEE-81C9-11CF384E6025}"); + To("{A8E01644-9F2E-4988-8341-587EF5B7EA69}"); To("{E073DBC0-9E8E-4C92-8210-9CB18364F46E}"); To("{80D282A4-5497-47FF-991F-BC0BCE603121}"); To("{96525697-E9DC-4198-B136-25AD033442B8}"); To("{7FC5AC9B-6F56-415B-913E-4A900629B853}"); To("{1539A010-2EB5-4163-8518-4AE2AA98AFC6}"); - To("{C567DE81-DF92-4B99-BEA8-CD34EF99DA5D}"); + To("{C567DE81-DF92-4B99-BEA8-CD34EF99DA5D}"); To("{0D82C836-96DD-480D-A924-7964E458BD34}"); To("{1A0FBC8A-6FC6-456C-805C-B94816B2E570}"); To("{302DE171-6D83-4B6B-B3C0-AC8808A16CA1}"); @@ -91,5 +92,8 @@ public class UmbracoPlan : MigrationPlan // To 14.2.0 To("{20ED404C-6FF9-4F91-8AC9-2B298E0002EB}"); + + // To 14.3.0 + To("{EEF792FC-318C-4921-9859-51EBF07A53A3}"); // Execute again, to ensure all that migrated to 14.0.0 without 13.5 will have this } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs index 58f3084231..b95b37beff 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs @@ -53,5 +53,9 @@ public class UmbracoPremigrationPlan : MigrationPlan // To 14.0.0 To("{76FBF80E-37E6-462E-ADC1-25668F56151D}"); + To("{37CF4AC3-8489-44BC-A7E8-64908FEEC656}"); + To("{7BCB5352-B2ED-4D4B-B27D-ECDED930B50A}"); + To("{3E69BF9B-BEAB-41B1-BB11-15383CCA1C7F}"); + To("{F12C609B-86B9-4386-AFA4-78E02857247C}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_3_0/AlignUpgradedDatabase.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_3_0/AlignUpgradedDatabase.cs index 84171e8717..6ee48ce0e7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_3_0/AlignUpgradedDatabase.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_3_0/AlignUpgradedDatabase.cs @@ -120,23 +120,48 @@ public class AlignUpgradedDatabase : MigrationBase // We need to do this to ensure we don't try to rename the constraint if it doesn't exist. const string tableName = "umbracoContentVersion"; const string columnName = "VersionDate"; + const string newColumnName = "versionDate"; + const string expectedConstraintName = "DF_umbracoContentVersion_versionDate"; + ColumnInfo? versionDateColumn = columns .FirstOrDefault(x => x is { TableName: tableName, ColumnName: columnName }); - if (versionDateColumn is null) + // we only want to rename the column if necessary + if (versionDateColumn is not null) { // The column was not found I.E. the column is correctly named - return; + RenameColumn(tableName, columnName, newColumnName, columns); } - RenameColumn(tableName, columnName, "versionDate", columns); - // Renames the default constraint for the column, // apparently the content version table used to be prefixed with cms and not umbraco // We don't have a fluid way to rename the default constraint so we have to use raw SQL // This should be okay though since we are only running this migration on SQL Server + Sql constraintNameQuery = Database.SqlContext.Sql(@$" +SELECT obj_Constraint.NAME AS 'constraintName' + FROM sys.objects obj_table + JOIN sys.objects obj_Constraint + ON obj_table.object_id = obj_Constraint.parent_object_id + JOIN sys.sysconstraints constraints + ON constraints.constid = obj_Constraint.object_id + JOIN sys.columns columns + ON columns.object_id = obj_table.object_id + AND columns.column_id = constraints.colid + WHERE obj_table.NAME = '{tableName}' + AND columns.NAME = '{newColumnName}' + AND obj_Constraint.type = 'D' +"); + var currentConstraintName = Database.ExecuteScalar(constraintNameQuery); + + + // only rename the constraint if necessary + if (currentConstraintName == expectedConstraintName) + { + return; + } + Sql renameConstraintQuery = Database.SqlContext.Sql( - "EXEC sp_rename N'DF_cmsContentVersion_VersionDate', N'DF_umbracoContentVersion_versionDate', N'OBJECT'"); + $"EXEC sp_rename N'{currentConstraintName}', N'{expectedConstraintName}', N'OBJECT'"); Database.Execute(renameConstraintQuery); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_5_0/ChangeRedirectUrlToNvarcharMax.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_5_0/ChangeRedirectUrlToNvarcharMax.cs new file mode 100644 index 0000000000..4f28b0fe88 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_5_0/ChangeRedirectUrlToNvarcharMax.cs @@ -0,0 +1,41 @@ +using System.Linq.Expressions; +using System.Text; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_13_5_0; + +public class ChangeRedirectUrlToNvarcharMax : MigrationBase +{ + public ChangeRedirectUrlToNvarcharMax(IMigrationContext context) : base(context) + { + } + + protected override void Migrate() + { + // We don't need to run this migration for SQLite, since ntext is not a thing there, text is just text. + if (DatabaseType == DatabaseType.SQLite) + { + return; + } + + string tableName = RedirectUrlDto.TableName; + string colName = "url"; + + // Determine the current datatype of the column within the database + string colDataType = Database.ExecuteScalar($"SELECT TOP(1) CHARACTER_MAXIMUM_LENGTH FROM INFORMATION_SCHEMA.COLUMNS" + + $" WHERE TABLE_NAME = '{tableName}' AND COLUMN_NAME = '{colName}'"); + + // 255 is the old length, -1 indicate MAX length + if (colDataType == "255") + { + // Upgrade to MAX length + Database.Execute($"Drop Index IX_umbracoRedirectUrl_culture_hash on {Constants.DatabaseSchema.Tables.RedirectUrl}"); + Database.Execute($"ALTER TABLE {tableName} ALTER COLUMN {colName} nvarchar(MAX) NOT NULL"); + Database.Execute($"CREATE INDEX IX_umbracoRedirectUrl_culture_hash ON {Constants.DatabaseSchema.Tables.RedirectUrl} (urlHash, contentKey, culture, createDateUtc)"); + } + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUserGroups.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUserGroups.cs index ad8e5091ea..461ed59c8e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUserGroups.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUserGroups.cs @@ -20,15 +20,24 @@ public class AddGuidsToUserGroups : UnscopedMigrationBase protected override void Migrate() { + // If the new column already exists we'll do nothing. + if (ColumnExists(Constants.DatabaseSchema.Tables.UserGroup, NewColumnName)) + { + Context.Complete(); + return; + } + // SQL server can simply add the column, but for SQLite this won't work, // so we'll have to create a new table and copy over data. if (DatabaseType != DatabaseType.SQLite) { MigrateSqlServer(); + Context.Complete(); return; } MigrateSqlite(); + Context.Complete(); } private void MigrateSqlServer() @@ -37,11 +46,6 @@ public class AddGuidsToUserGroups : UnscopedMigrationBase using IDisposable notificationSuppression = scope.Notifications.Suppress(); ScopeDatabase(scope); - if (ColumnExists(Constants.DatabaseSchema.Tables.UserGroup, NewColumnName)) - { - return; - } - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); AddColumnIfNotExists(columns, NewColumnName); @@ -68,12 +72,6 @@ public class AddGuidsToUserGroups : UnscopedMigrationBase using IDisposable notificationSuppression = scope.Notifications.Suppress(); ScopeDatabase(scope); - // If the new column already exists we'll do nothing. - if (ColumnExists(Constants.DatabaseSchema.Tables.UserGroup, NewColumnName)) - { - return; - } - // This isn't pretty, // But since you cannot alter columns, we have to copy the data over and delete the old table. // However we cannot do this due to foreign keys, so temporarily disable these keys while migrating. diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUsers.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUsers.cs index fe730fd2b8..6d5dbce1d8 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUsers.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUsers.cs @@ -26,6 +26,12 @@ internal class AddGuidsToUsers : UnscopedMigrationBase protected override void Migrate() { + if (ColumnExists(Constants.DatabaseSchema.Tables.User, NewColumnName)) + { + Context.Complete(); + return; + } + InvalidateBackofficeUserAccess = true; using IScope scope = _scopeProvider.CreateScope(); using IDisposable notificationSuppression = scope.Notifications.Suppress(); @@ -75,11 +81,6 @@ internal class AddGuidsToUsers : UnscopedMigrationBase private void MigrateSqlite() { - if (ColumnExists(Constants.DatabaseSchema.Tables.User, NewColumnName)) - { - return; - } - /* * We commit the initial transaction started by the scope. This is required in order to disable the foreign keys. * We then begin a new transaction, this transaction will be committed or rolled back by the scope, like normal. diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs index 00e97a6ad1..4d5b5680b1 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs @@ -2,11 +2,13 @@ using System.Globalization; using System.Net; using System.Xml.Linq; using System.Xml.XPath; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; @@ -34,6 +36,8 @@ namespace Umbraco.Cms.Infrastructure.Packaging private readonly IConfigurationEditorJsonSerializer _serializer; private readonly IMediaService _mediaService; private readonly IMediaTypeService _mediaTypeService; + private readonly ITemplateContentParserService _templateContentParserService; + private readonly ITemplateService _templateService; private readonly IEntityService _entityService; private readonly IContentTypeService _contentTypeService; private readonly IContentService _contentService; @@ -52,7 +56,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging IShortStringHelper shortStringHelper, IConfigurationEditorJsonSerializer serializer, IMediaService mediaService, - IMediaTypeService mediaTypeService) + IMediaTypeService mediaTypeService, + ITemplateContentParserService templateContentParserService, + ITemplateService templateService) { _dataValueEditorFactory = dataValueEditorFactory; _logger = logger; @@ -68,6 +74,44 @@ namespace Umbraco.Cms.Infrastructure.Packaging _serializer = serializer; _mediaService = mediaService; _mediaTypeService = mediaTypeService; + _templateContentParserService = templateContentParserService; + _templateService = templateService; + } + + [Obsolete("Please use new constructor, scheduled for removal in v15")] + public PackageDataInstallation( + IDataValueEditorFactory dataValueEditorFactory, + ILogger logger, + IFileService fileService, + ILocalizationService localizationService, + IDataTypeService dataTypeService, + IEntityService entityService, + IContentTypeService contentTypeService, + IContentService contentService, + PropertyEditorCollection propertyEditors, + IScopeProvider scopeProvider, + IShortStringHelper shortStringHelper, + IConfigurationEditorJsonSerializer serializer, + IMediaService mediaService, + IMediaTypeService mediaTypeService) + : this( + dataValueEditorFactory, + logger, + fileService, + localizationService, + dataTypeService, + entityService, + contentTypeService, + contentService, + propertyEditors, + scopeProvider, + shortStringHelper, + serializer, + mediaService, + mediaTypeService, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) + { } // Also remove factory service registration when this constructor is removed @@ -103,7 +147,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging shortStringHelper, serializer, mediaService, - mediaTypeService) + mediaTypeService, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } #region Install/Uninstall @@ -1663,16 +1709,25 @@ namespace Umbraco.Cms.Infrastructure.Packaging #region Templates + [Obsolete("Use Async version instead, Scheduled to be removed in v17")] public IEnumerable ImportTemplate(XElement templateElement, int userId) => ImportTemplates(new[] { templateElement }, userId); + public async Task> ImportTemplateAsync(XElement templateElement, int userId) + => ImportTemplatesAsync(new[] {templateElement}, userId).GetAwaiter().GetResult(); + + + [Obsolete("Use Async version instead, Scheduled to be removed in v17")] + public IReadOnlyList ImportTemplates(IReadOnlyCollection templateElements, int userId) + => ImportTemplatesAsync(templateElements, userId).GetAwaiter().GetResult(); + /// /// Imports and saves package xml as /// /// Xml to import /// Optional user id /// An enumerable list of generated Templates - public IReadOnlyList ImportTemplates(IReadOnlyCollection templateElements, int userId) + public async Task> ImportTemplatesAsync(IReadOnlyCollection templateElements, int userId) { var templates = new List(); @@ -1682,20 +1737,19 @@ namespace Umbraco.Cms.Infrastructure.Packaging { var dependencies = new List(); XElement elementCopy = tempElement; - //Ensure that the Master of the current template is part of the import, otherwise we ignore this dependency as part of the dependency sorting. - if (string.IsNullOrEmpty((string?)elementCopy.Element("Master")) == false && - templateElements.Any(x => (string?)x.Element("Alias") == (string?)elementCopy.Element("Master"))) + + //Ensure that the Master of the current template is part of the import, otherwise we ignore this dependency as part of the dependency sorting.' + var masterTemplate = _templateContentParserService.MasterTemplateAlias(tempElement.Value); + if (masterTemplate is not null && templateElements.Any(x => (string?)x.Element("Alias") == masterTemplate)) { - dependencies.Add((string)elementCopy.Element("Master")!); + dependencies.Add(masterTemplate); } - else if (string.IsNullOrEmpty((string?)elementCopy.Element("Master")) == false && - templateElements.Any(x => - (string?)x.Element("Alias") == (string?)elementCopy.Element("Master")) == false) + else { _logger.LogInformation( "Template '{TemplateAlias}' has an invalid Master '{TemplateMaster}', so the reference has been ignored.", (string?)elementCopy.Element("Alias"), - (string?)elementCopy.Element("Master")); + masterTemplate); } graph.AddItem(TopoGraph.CreateNode((string)elementCopy.Element("Alias")!, elementCopy, dependencies)); @@ -1712,9 +1766,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging var design = templateElement.Element("Design")?.Value; XElement? masterElement = templateElement.Element("Master"); - var existingTemplate = _fileService.GetTemplate(alias) as Template; + var existingTemplate = await _templateService.GetAsync(alias) as Template; - Template? template = existingTemplate ?? new Template(_shortStringHelper, templateName, alias); + Template template = existingTemplate ?? new Template(_shortStringHelper, templateName, alias); // For new templates, use the serialized key if avaialble. if (existingTemplate == null && Guid.TryParse(templateElement.Element("Key")?.Value, out Guid key)) @@ -1737,9 +1791,16 @@ namespace Umbraco.Cms.Infrastructure.Packaging templates.Add(template); } - if (templates.Any()) + foreach (ITemplate template in templates) { - _fileService.SaveTemplate(templates, userId); + if (template.Id > 0) + { + await _templateService.UpdateAsync(template, Constants.Security.SuperUserKey); + } + else + { + await _templateService.CreateAsync(template, Constants.Security.SuperUserKey); + } } return templates; diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/RedirectUrlDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/RedirectUrlDto.cs index 453ab1e308..3582a7eee5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/RedirectUrlDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/RedirectUrlDto.cs @@ -38,6 +38,7 @@ internal class RedirectUrlDto [Column("url")] [NullSetting(NullSetting = NullSettings.NotNull)] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] public string Url { get; set; } = null!; [Column("culture")] diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index 025bdd887c..ef442b768f 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -2,6 +2,7 @@ // See LICENSE for more details. using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Cache.PropertyEditors; @@ -10,6 +11,7 @@ using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.Editors; +using Umbraco.Cms.Core.PropertyEditors.Validators; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; @@ -66,14 +68,17 @@ public class RichTextPropertyEditor : DataEditor internal class RichTextPropertyValueEditor : BlockValuePropertyValueEditorBase { private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly ILocalizedTextService _localizedTextService; private readonly IHtmlSanitizer _htmlSanitizer; private readonly HtmlImageSourceParser _imageSourceParser; private readonly HtmlLocalLinkParser _localLinkParser; private readonly RichTextEditorPastedImages _pastedImages; private readonly IJsonSerializer _jsonSerializer; private readonly IBlockEditorElementTypeCache _elementTypeCache; + private readonly IRichTextRequiredValidator _richTextRequiredValidator; private readonly ILogger _logger; + [Obsolete("Use non-obsolete constructor. This is schedules for removal in v16.")] public RichTextPropertyValueEditor( DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, @@ -91,20 +96,64 @@ public class RichTextPropertyEditor : DataEditor IBlockEditorElementTypeCache elementTypeCache, IPropertyValidationService propertyValidationService, DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection) + : this( + attribute, + propertyEditors, + dataTypeReadCache, + logger, + backOfficeSecurityAccessor, + localizedTextService, + shortStringHelper, + imageSourceParser, + localLinkParser, + pastedImages, + jsonSerializer, + ioHelper, + htmlSanitizer, + elementTypeCache, + propertyValidationService, + dataValueReferenceFactoryCollection, + StaticServiceProvider.Instance.GetRequiredService()) + { + + } + + public RichTextPropertyValueEditor( + DataEditorAttribute attribute, + PropertyEditorCollection propertyEditors, + IDataTypeConfigurationCache dataTypeReadCache, + ILogger logger, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + HtmlImageSourceParser imageSourceParser, + HtmlLocalLinkParser localLinkParser, + RichTextEditorPastedImages pastedImages, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + IHtmlSanitizer htmlSanitizer, + IBlockEditorElementTypeCache elementTypeCache, + IPropertyValidationService propertyValidationService, + DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection, + IRichTextRequiredValidator richTextRequiredValidator) : base(attribute, propertyEditors, dataTypeReadCache, localizedTextService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactoryCollection) { _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _localizedTextService = localizedTextService; _imageSourceParser = imageSourceParser; _localLinkParser = localLinkParser; _pastedImages = pastedImages; _htmlSanitizer = htmlSanitizer; _elementTypeCache = elementTypeCache; + _richTextRequiredValidator = richTextRequiredValidator; _jsonSerializer = jsonSerializer; _logger = logger; Validators.Add(new RichTextEditorBlockValidator(propertyValidationService, CreateBlockEditorValues(), elementTypeCache, jsonSerializer, logger)); } + public override IValueRequiredValidator RequiredValidator => _richTextRequiredValidator; + /// public override object? ConfigurationObject { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/Validators/IRichTextRequiredValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/Validators/IRichTextRequiredValidator.cs new file mode 100644 index 0000000000..7358e92f38 --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/Validators/IRichTextRequiredValidator.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Core.PropertyEditors; + +namespace Umbraco.Cms.Core.PropertyEditors.Validators; + +internal interface IRichTextRequiredValidator : IValueRequiredValidator +{ +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/Validators/RichTextRequiredValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/Validators/RichTextRequiredValidator.cs new file mode 100644 index 0000000000..b239f0bda5 --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/Validators/RichTextRequiredValidator.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Core.PropertyEditors.Validators; + +internal class RichTextRequiredValidator : RequiredValidator, IRichTextRequiredValidator +{ + private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; + + public RichTextRequiredValidator(ILocalizedTextService textService, IJsonSerializer jsonSerializer, ILogger logger) : base(textService) + { + _jsonSerializer = jsonSerializer; + _logger = logger; + } + + public override IEnumerable ValidateRequired(object? value, string? valueType) => base.ValidateRequired(GetValue(value), valueType); + + private object? GetValue(object? value) + { + if(RichTextPropertyEditorHelper.TryParseRichTextEditorValue(value, _jsonSerializer, _logger, out RichTextEditorValue? richTextEditorValue)) + { + return richTextEditorValue?.Markup; + } + + return value; + } +} diff --git a/src/Umbraco.Infrastructure/Services/Implement/IndexedEntitySearchService.cs b/src/Umbraco.Infrastructure/Services/Implement/IndexedEntitySearchService.cs index 0055df6244..0f097df262 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/IndexedEntitySearchService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/IndexedEntitySearchService.cs @@ -20,6 +20,15 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService } public PagedModel Search(UmbracoObjectTypes objectType, string query, int skip = 0, int take = 100, bool ignoreUserStartNodes = false) + => Search(objectType, query, null, skip, take, ignoreUserStartNodes); + + public PagedModel Search( + UmbracoObjectTypes objectType, + string query, + Guid? parentId, + int skip = 0, + int take = 100, + bool ignoreUserStartNodes = false) { UmbracoEntityTypes entityType = objectType switch { @@ -37,7 +46,8 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService pageSize, pageNumber, out var totalFound, - ignoreUserStartNodes: ignoreUserStartNodes); + ignoreUserStartNodes: ignoreUserStartNodes, + searchFrom: parentId?.ToString()); Guid[] keys = searchResults.Select( result => diff --git a/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs b/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs index c4e152e0b5..53d6c8ba6c 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs @@ -1,3 +1,6 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.Loader; using System.Xml.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; @@ -354,8 +357,16 @@ public class PackagingService : IPackagingService if (!string.IsNullOrEmpty(packageManifest.Version)) { + // Always use package version from manifest installedPackage.Version = packageManifest.Version; } + else if (string.IsNullOrEmpty(installedPackage.Version) && + string.IsNullOrEmpty(installedPackage.PackageId) is false && + TryGetAssemblyInformationalVersion(installedPackage.PackageId, out string? version)) + { + // Use version of the assembly with the same name as the package ID + installedPackage.Version = version; + } } // Return all packages with an ID or name in the package manifest or package migrations @@ -414,4 +425,20 @@ public class PackagingService : IPackagingService return packageFile.CreateReadStream(); } + + private static bool TryGetAssemblyInformationalVersion(string name, [NotNullWhen(true)] out string? version) + { + foreach (Assembly assembly in AssemblyLoadContext.Default.Assemblies) + { + AssemblyName assemblyName = assembly.GetName(); + if (string.Equals(assemblyName.Name, name, StringComparison.OrdinalIgnoreCase) && + assembly.TryGetInformationalVersion(out version)) + { + return true; + } + } + + version = null; + return false; + } } diff --git a/src/Umbraco.PublishedCache.NuCache/Property.cs b/src/Umbraco.PublishedCache.NuCache/Property.cs index afe3b54484..845b8312c5 100644 --- a/src/Umbraco.PublishedCache.NuCache/Property.cs +++ b/src/Umbraco.PublishedCache.NuCache/Property.cs @@ -351,6 +351,12 @@ internal class Property : PublishedPropertyBase public CacheValue For(string? culture, string? segment) { + // As noted on IPropertyValue, null value means invariant + // But as we need an actual string value to build a CompositeStringStringKey + // We need to convert null to empty + culture ??= string.Empty; + segment ??= string.Empty; + if (culture == string.Empty && segment == string.Empty) { return this; diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs index 8d471428de..324781b5a3 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs @@ -43,14 +43,14 @@ public class AspNetCoreHostingEnvironment : IHostingEnvironment _webHostEnvironment = webHostEnvironment ?? throw new ArgumentNullException(nameof(webHostEnvironment)); _urlProviderMode = _webRoutingSettings.CurrentValue.UrlProviderMode; - SetSiteName(hostingSettings.CurrentValue.SiteName); + SetSiteNameAndDebugMode(hostingSettings.CurrentValue); // We have to ensure that the OptionsMonitor is an actual options monitor since we have a hack // where we initially use an OptionsMonitorAdapter, which doesn't implement OnChange. // See summery of OptionsMonitorAdapter for more information. if (hostingSettings is OptionsMonitor) { - hostingSettings.OnChange(settings => SetSiteName(settings.SiteName)); + hostingSettings.OnChange(settings => SetSiteNameAndDebugMode(settings)); } ApplicationPhysicalPath = webHostEnvironment.ContentRootPath; @@ -95,7 +95,7 @@ public class AspNetCoreHostingEnvironment : IHostingEnvironment _hostingSettings.CurrentValue.ApplicationVirtualPath?.EnsureStartsWith('/') ?? "/"; /// - public bool IsDebugMode => _hostingSettings.CurrentValue.Debug; + public bool IsDebugMode { get; private set; } public string LocalTempPath { @@ -188,8 +188,12 @@ public class AspNetCoreHostingEnvironment : IHostingEnvironment } } - private void SetSiteName(string? siteName) => - SiteName = string.IsNullOrWhiteSpace(siteName) + private void SetSiteNameAndDebugMode(HostingSettings hostingSettings) + { + SiteName = string.IsNullOrWhiteSpace(hostingSettings.SiteName) ? _webHostEnvironment.ApplicationName - : siteName; + : hostingSettings.SiteName; + + IsDebugMode = hostingSettings.Debug; + } } diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 7258f44208..ea1c5dad99 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -30,6 +30,8 @@ + + diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index bb6abdc884..b2c598f6ef 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit bb6abdc88452bbd3a47bf867dcb1332f536ad264 +Subproject commit b2c598f6ef0b62bb64186c61125f4d00177b48ca diff --git a/src/Umbraco.Web.UI.Login/src/controllers/slim-backoffice-initializer.ts b/src/Umbraco.Web.UI.Login/src/controllers/slim-backoffice-initializer.ts index 45422bf272..681ab098a7 100644 --- a/src/Umbraco.Web.UI.Login/src/controllers/slim-backoffice-initializer.ts +++ b/src/Umbraco.Web.UI.Login/src/controllers/slim-backoffice-initializer.ts @@ -2,7 +2,10 @@ import { UmbBundleExtensionInitializer, UmbServerExtensionRegistrator } from "@umbraco-cms/backoffice/extension-api"; -import { umbExtensionsRegistry } from "@umbraco-cms/backoffice/extension-registry"; +import { + UmbAppEntryPointExtensionInitializer, + umbExtensionsRegistry +} from "@umbraco-cms/backoffice/extension-registry"; import type { UmbElement } from "@umbraco-cms/backoffice/element-api"; import { UmbControllerBase } from "@umbraco-cms/backoffice/class-api"; import { UUIIconRegistryEssential } from "@umbraco-cms/backoffice/external/uui"; @@ -21,6 +24,7 @@ export class UmbSlimBackofficeController extends UmbControllerBase { constructor(host: UmbElement) { super(host); new UmbBundleExtensionInitializer(host, umbExtensionsRegistry); + new UmbAppEntryPointExtensionInitializer(host, umbExtensionsRegistry); new UmbServerExtensionRegistrator(host, umbExtensionsRegistry).registerPublicExtensions(); this.#uuiIconRegistry.attach(host); diff --git a/src/Umbraco.Web.UI/Program.cs b/src/Umbraco.Web.UI/Program.cs index aa718b5ec3..ad68d28351 100644 --- a/src/Umbraco.Web.UI/Program.cs +++ b/src/Umbraco.Web.UI/Program.cs @@ -3,7 +3,9 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.CreateUmbracoBuilder() .AddBackOffice() .AddWebsite() +#if UseDeliveryApi .AddDeliveryApi() +#endif .AddComposers() .Build(); @@ -23,6 +25,9 @@ app.UseUmbraco() }) .WithEndpoints(u => { + /*#if (UmbracoRelease = 'LTS') + u.UseInstallerEndpoints(); + #endif */ u.UseBackOfficeEndpoints(); u.UseWebsiteEndpoints(); }); diff --git a/templates/Umbraco.Templates.csproj b/templates/Umbraco.Templates.csproj index 57830bff09..b75df9f9a5 100644 --- a/templates/Umbraco.Templates.csproj +++ b/templates/Umbraco.Templates.csproj @@ -19,6 +19,7 @@ + UmbracoProject\Views\Partials\blocklist\%(RecursiveDir)%(Filename)%(Extension) UmbracoProject\Views\Partials\blocklist @@ -47,7 +48,7 @@ - + <_PackageFiles Include="%(_TemplateJsonFiles.DestinationFile)"> %(_TemplateJsonFiles.RelativeDir) diff --git a/templates/UmbracoDockerCompose/.env b/templates/UmbracoDockerCompose/.env new file mode 100644 index 0000000000..38985be5c5 --- /dev/null +++ b/templates/UmbracoDockerCompose/.env @@ -0,0 +1 @@ +DB_PASSWORD=Password1234 diff --git a/templates/UmbracoDockerCompose/.template.config/dotnetcli.host.json b/templates/UmbracoDockerCompose/.template.config/dotnetcli.host.json new file mode 100644 index 0000000000..aaab590168 --- /dev/null +++ b/templates/UmbracoDockerCompose/.template.config/dotnetcli.host.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/dotnetcli.host.json", + "symbolInfo": { + "ProjectName": { + "longName": "ProjectName", + "shortName": "P" + }, + "DatabasePassword": { + "longName": "DatabasePassword", + "shortName": "dbpw" + }, + "Port": + { + "longName": "Port", + "shortName": "p" + } + }, + "usageExamples": [ + "dotnet new umbraco-compose -P MyProject", + "dotnet new umbraco-compose --ProjectName MyProject", + "dotnet new umbraco-compose -P -MyProject -dbpw MyStr0ngP@ssword", + "dotnet new umbraco-compose -P -MyProject --DatabasePassword MyStr0ngP@ssword" + ] +} diff --git a/templates/UmbracoDockerCompose/.template.config/ide.host.json b/templates/UmbracoDockerCompose/.template.config/ide.host.json new file mode 100644 index 0000000000..ae8c5fb05c --- /dev/null +++ b/templates/UmbracoDockerCompose/.template.config/ide.host.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/ide.host.json", + "order": 0, + "icon": "../../icon.png", + "description": { + "id": "UmbracoDockerCompose", + "text": "Umbraco Docker Compose - Docker compose for Umbraco CMS and associated database" + }, + "symbolInfo": [ + { + "id": "ProjectName", + "isVisible": true + }, + { + "id": "DatabasePassword", + "isVisible": true + }, + { + "id": "Port", + "isVisible": true + } + ] +} diff --git a/templates/UmbracoDockerCompose/.template.config/template.json b/templates/UmbracoDockerCompose/.template.config/template.json new file mode 100644 index 0000000000..6f6877b7e0 --- /dev/null +++ b/templates/UmbracoDockerCompose/.template.config/template.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Umbraco HQ", + "classifications": [ + "Web", + "CMS", + "Umbraco" + ], + "name": "Umbraco Docker Compose", + "description": "Creates the prerequisites for developing Umbraco in Docker containers", + "groupIdentity": "Umbraco.Templates.UmbracoDockerCompose", + "identity": "Umbraco.Templates.UmbracoDockerCompose", + "shortName": "umbraco-compose", + "tags": { + "type": "item" + }, + "symbols": { + "ProjectName": { + "type": "parameter", + "description": "The name of the project the Docker Compose file will be created for", + "datatype": "string", + "replaces": "UmbracoProject", + "isRequired": true + }, + "DatabasePassword": { + "type": "parameter", + "description": "The password to the database, will be stored in .env file", + "datatype": "string", + "replaces": "Password1234", + "defaultValue": "Password1234" + }, + "Port": { + "type": "parameter", + "description": "The port forward on the docker container, this is the port you use to access the site", + "datatype": "string", + "replaces": "TEMPLATE_PORT", + "defaultValue": "44372" + }, + "ImageName": { + "type": "generated", + "generator": "casing", + "parameters": { + "source": "ProjectName", + "toLower": true + }, + "replaces": "umbraco_image" + } + } +} diff --git a/templates/UmbracoDockerCompose/Database/Dockerfile b/templates/UmbracoDockerCompose/Database/Dockerfile new file mode 100644 index 0000000000..4e74b6435e --- /dev/null +++ b/templates/UmbracoDockerCompose/Database/Dockerfile @@ -0,0 +1,21 @@ +FROM mcr.microsoft.com/azure-sql-edge:latest + +ENV ACCEPT_EULA=Y + +USER root + +RUN mkdir /var/opt/sqlserver + +RUN chown mssql /var/opt/sqlserver + +ENV MSSQL_BACKUP_DIR="/var/opt/mssql" +ENV MSSQL_DATA_DIR="/var/opt/mssql/data" +ENV MSSQL_LOG_DIR="/var/opt/mssql/log" + +EXPOSE 1433/tcp +COPY setup.sql / +COPY startup.sh / +COPY healthcheck.sh / + +ENTRYPOINT [ "/bin/bash", "startup.sh" ] +CMD [ "/opt/mssql/bin/sqlservr" ] diff --git a/templates/UmbracoDockerCompose/Database/healthcheck.sh b/templates/UmbracoDockerCompose/Database/healthcheck.sh new file mode 100644 index 0000000000..2964e17dc4 --- /dev/null +++ b/templates/UmbracoDockerCompose/Database/healthcheck.sh @@ -0,0 +1,15 @@ +value="$(/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -d master -Q "SELECT state_desc FROM sys.databases WHERE name = 'umbracoDb'" | awk 'NR==3')" + +# This checks for any non-zero length string, and $value will be empty when the database does not exist. +if [ -n "$value" ] +then + echo "ONLINE" + return 0 # With docker 0 = success +else + echo "OFFLINE" + return 1 # And 1 = unhealthy +fi + +# This is useful for debugging +# echo "Value is:" +# echo "$value" diff --git a/templates/UmbracoDockerCompose/Database/setup.sql b/templates/UmbracoDockerCompose/Database/setup.sql new file mode 100644 index 0000000000..466030dd50 --- /dev/null +++ b/templates/UmbracoDockerCompose/Database/setup.sql @@ -0,0 +1,10 @@ +USE [master] +GO + +IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'UmbracoDb') + BEGIN + CREATE DATABASE [umbracoDb] + END; +GO + +USE UmbracoDb; \ No newline at end of file diff --git a/templates/UmbracoDockerCompose/Database/startup.sh b/templates/UmbracoDockerCompose/Database/startup.sh new file mode 100644 index 0000000000..c4fad8f0ae --- /dev/null +++ b/templates/UmbracoDockerCompose/Database/startup.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e + +# Taken from: https://github.com/CarlSargunar/Umbraco-Docker-Workshop +if [ "$1" = '/opt/mssql/bin/sqlservr' ]; then + # If this is the container's first run, initialize the application database + if [ ! -f /tmp/app-initialized ]; then + # Initialize the application database asynchronously in a background process. This allows a) the SQL Server process to be the main process in the container, which allows graceful shutdown and other goodies, and b) us to only start the SQL Server process once, as opposed to starting, stopping, then starting it again. + function initialize_app_database() { + # Wait a bit for SQL Server to start. SQL Server's process doesn't provide a clever way to check if it's up or not, and it needs to be up before we can import the application database + sleep 15s + + #run the setup script to create the DB and the schema in the DB + /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -d master -i setup.sql + + # Note that the container has been initialized so future starts won't wipe changes to the data + touch /tmp/app-initialized + } + initialize_app_database & + fi +fi + +exec "$@" diff --git a/templates/UmbracoDockerCompose/docker-compose.yml b/templates/UmbracoDockerCompose/docker-compose.yml new file mode 100644 index 0000000000..7acae5d8d6 --- /dev/null +++ b/templates/UmbracoDockerCompose/docker-compose.yml @@ -0,0 +1,102 @@ +services: + umb_database: + container_name: umbraco_image_database + build: + context: ./Database + environment: + SA_PASSWORD: ${DB_PASSWORD} + MSSQL_SA_PASSWORD: ${DB_PASSWORD} + ports: + - "1433:1433" + - "1434:1434" + volumes: + - umb_database:/var/opt/mssql + networks: + - umbnet + healthcheck: + # This healthcheck is to make sure that the database is up and running before the umbraco container starts. + # It works by querying the database for the state of the umbracoDb database, ensuring it exists. + test: ./healthcheck.sh + interval: 5m + timeout: 5s + retries: 3 + start_period: 15s # Bootstrap duration, for this duration failures does not count towards max retries. + start_interval: 5s # How long after the health check has started to run the healthcheck again. + + umbraco_image: + image: umbraco_image + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ConnectionStrings__umbracoDbDSN=Server=umb_database;Database=umbracoDb;User Id=sa;Password=${DB_PASSWORD};TrustServerCertificate=true; + - ConnectionStrings__umbracoDbDSN_ProviderName=Microsoft.Data.SqlClient + volumes: + - umb_media:/app/wwwroot/media + - umb_scripts:/app/wwwroot/scripts + - umb_styles:/app/wwwroot/css + - umb_logs:/app/umbraco/Logs + - umb_views:/app/Views + - umb_data:/app/umbraco + - umb_models:/app/umbraco/models + build: + context: . + dockerfile: UmbracoProject/Dockerfile + args: + - BUILD_CONFIGURATION=Debug + + depends_on: + umb_database: + condition: service_healthy + restart: always + ports: + - "TEMPLATE_PORT:8080" + networks: + - umbnet + develop: + # This allows you to run docker compose watch, after doing so the container will rebuild when the models are changed. + # Once a restart only feature is implemented (https://github.com/docker/compose/issues/11446) + # It would be really nice to add a restart only watch to \Views, since the file watchers for recompilation of Razor views does not work with docker. + watch: + - path: ./UmbracoProject/umbraco/models + action: rebuild + +# These volumes are all made as bind mounts, meaning that they are bound to the host machine's file system. +# This is to better facilitate local development in the IDE, so the views, models, etc... are available in the IDE. +# This can be changed by removing the driver and driver_opts from the volumes. +volumes: + umb_media: + driver: local + driver_opts: + type: none + device: ./UmbracoProject/wwwroot/media + o: bind + umb_scripts: + driver: local + driver_opts: + type: none + device: ./UmbracoProject/wwwroot/scripts + o: bind + umb_styles: + driver: local + driver_opts: + type: none + device: ./UmbracoProject/wwwroot/css + o: bind + umb_logs: + umb_views: + driver: local + driver_opts: + type: none + device: ./UmbracoProject/Views + o: bind + umb_data: + umb_models: + driver: local + driver_opts: + type: none + device: ./UmbracoProject/umbraco/models + o: bind + umb_database: + +networks: + umbnet: + driver: bridge diff --git a/templates/UmbracoProject/.dockerignore b/templates/UmbracoProject/.dockerignore new file mode 100644 index 0000000000..2f32bfe4fe --- /dev/null +++ b/templates/UmbracoProject/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/templates/UmbracoProject/.template.config/dotnetcli.host.json b/templates/UmbracoProject/.template.config/dotnetcli.host.json index dfd6f80184..b18020b6de 100644 --- a/templates/UmbracoProject/.template.config/dotnetcli.host.json +++ b/templates/UmbracoProject/.template.config/dotnetcli.host.json @@ -8,12 +8,25 @@ }, "UmbracoVersion": { "longName": "version", - "shortName": "v" + "shortName": "v", + "isHidden": true + }, + "UmbracoRelease": { + "longName": "release", + "shortName": "r" }, "UseHttpsRedirect": { "longName": "use-https-redirect", "shortName": "" }, + "UseDeliveryApi": { + "longName": "use-delivery-api", + "shortName": "da" + }, + "Docker": { + "longName": "add-docker", + "shortName": "" + }, "SkipRestore": { "longName": "no-restore", "shortName": "" @@ -58,6 +71,18 @@ "longName": "PackageTestSiteName", "shortName": "p", "isHidden": true + }, + "ModelsBuilderMode": { + "longName": "models-mode", + "shortName": "mm" + }, + "StarterKit": { + "longName": "starter-kit", + "shortName": "sk" + }, + "DevelopmentMode": { + "longName": "development-mode", + "shortName": "dm" } }, "usageExamples": [ diff --git a/templates/UmbracoProject/.template.config/ide.host.json b/templates/UmbracoProject/.template.config/ide.host.json index 1a302779cc..90de3b977a 100644 --- a/templates/UmbracoProject/.template.config/ide.host.json +++ b/templates/UmbracoProject/.template.config/ide.host.json @@ -9,11 +9,22 @@ "symbolInfo": [ { "id": "UmbracoVersion", - "isVisible": true + "isVisible": false }, { "id": "UseHttpsRedirect", "isVisible": true, + "persistenceScope": "templateGroup", + "defaultValue": "true" + }, + { + "id": "UseDeliveryApi", + "isVisible": true, + "persistenceScope": "templateGroup" + }, + { + "id": "ModelsBuilderMode", + "isVisible": true, "persistenceScope": "templateGroup" }, { @@ -54,6 +65,23 @@ { "id": "NoNodesViewPath", "isVisible": true + }, + { + "id": "Docker", + "isVisible": true + }, + { + "id": "StarterKit", + "isVisible": true + }, + { + "id": "UmbracoRelease", + "isVisible": true + }, + { + "id": "DevelopmentMode", + "isVisible": true, + "defaultValue": "IDEDevelopment" } ] } diff --git a/templates/UmbracoProject/.template.config/starterkits.template.json b/templates/UmbracoProject/.template.config/starterkits.template.json new file mode 100644 index 0000000000..5d2a5dabf9 --- /dev/null +++ b/templates/UmbracoProject/.template.config/starterkits.template.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "symbols": { + "StarterKit": { + "displayName": "Starter kit", + "type": "parameter", + "datatype": "choice", + "description": "Choose a starter kit to install.", + "defaultValue": "None", + "replaces": "STARTER_KIT_NAME", + // The choice here should be the name of the starter kit package, since it will be used directly for package reference. + "choices": [ + { + "choice": "None", + "description": "No starter kit." + }, + { + "choice": "Umbraco.TheStarterKit", + "description": "The Umbraco starter kit.", + "displayName": "The Starter Kit" + } + ] + }, + // Used to determine the version of the starter kit to install. + // there should be cases for Latest, LTS and Custom for every starterkit added above. + // This has the benefit that all maintenance of starter kits in template can be done from this file. + "StarterKitVersion": { + "type": "generated", + "generator": "switch", + "replaces": "STARTER_KIT_VERSION", + "parameters": { + "evaluator": "C++", + "datatype": "string", + "cases": [ + { + "condition": "(StarterKit == 'Umbraco.TheStarterKit' && (UmbracoRelease == 'Latest' || UmbracoRelease == 'Custom'))", + "value": "14.0.0" + }, + { + "condition": "(StarterKit == 'Umbraco.TheStarterKit' && UmbracoRelease == 'LTS')", + "value": "13.0.0" + } + ] + } + } + } +} diff --git a/templates/UmbracoProject/.template.config/template.json b/templates/UmbracoProject/.template.config/template.json index b17352476e..355ed6f7a8 100644 --- a/templates/UmbracoProject/.template.config/template.json +++ b/templates/UmbracoProject/.template.config/template.json @@ -18,6 +18,7 @@ "sourceName": "UmbracoProject", "defaultName": "UmbracoProject1", "preferNameDirectory": true, + "additionalConfigFiles": [ "starterkits.template.json"], "sources": [ { "modifiers": [ @@ -26,6 +27,13 @@ "exclude": [ ".gitignore" ] + }, + { + "condition": "(!Docker)", + "exclude": [ + "Dockerfile", + ".dockerignore" + ] } ] } @@ -46,13 +54,72 @@ "defaultValue": "net8.0", "replaces": "net8.0" }, + "UmbracoRelease": { + "displayName": "Umbraco Version", + "description": "The Umbraco release to use, either latest or latest long term supported", + "type": "parameter", + "datatype": "choice", + "defaultValue": "Latest", + "choices": [ + { + "choice": "Latest", + "description": "The latest umbraco release" + }, + { + "choice": "LTS", + "description": "The most recent long term supported version", + "displayName": "Long Term Supported" + } + ], + "isRequired": false + }, "UmbracoVersion": { - "displayName": "Umbraco version", - "description": "The version of Umbraco.Cms to add as PackageReference.", + "displayName": "Custom Version", + "description": "The selected custom version of Umbraco, this is obsoleted, and will be removed in a future version of the template.", "type": "parameter", "datatype": "string", - "defaultValue": "*", - "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" + "defaultValue": "null", + "replaces": "CUSTOM_VERSION", + "isRequired": false + }, + "FinalVersion" : { + "type": "generated", + "generator": "switch", + "datatype": "text", + "description": "The calculated version of Umbraco to use", + "replaces": "UMBRACO_VERSION_FROM_TEMPLATE", + "parameters": { + "evaluator": "C++", + "datatype": "text", + "cases": [ + { + "condition": "(UmbracoRelease == 'Latest')", + "value": "*" + }, + { + "condition": "(UmbracoRelease == 'LTS')", + "value": "13.5.0" + } + ] + } + }, + "DotnetVersion": + { + "type": "generated", + "generator": "switch", + "datatype": "text", + "description": "Not relevant at the moment, but if we need to change the dotnet version based on the Umbraco version, we can do it here", + "replaces": "DOTNET_VERSION_FROM_TEMPLATE", + "parameters": { + "evaluator": "C++", + "datatype": "text", + "cases": [ + { + "condition": "(true)", + "value": "net8.0" + } + ] + } }, "UseHttpsRedirect": { "displayName": "Use HTTPS redirect", @@ -61,6 +128,20 @@ "datatype": "bool", "defaultValue": "false" }, + "UseDeliveryApi": { + "displayName": "Use Delivery API", + "description": "Enables the Delivery API", + "type": "parameter", + "datatype": "bool", + "defaultValue": "false" + }, + "Docker": { + "displayName": "Add Docker file", + "description": "Adds a docker file to the project.", + "type": "parameter", + "datatype": "bool", + "defaultValue": "false" + }, "SkipRestore": { "displayName": "Skip restore", "description": "If specified, skips the automatic restore of the project on create.", @@ -244,6 +325,58 @@ "defaultValue": "", "replaces": "PACKAGE_PROJECT_NAME_FROM_TEMPLATE" }, + "DevelopmentMode": { + "type": "parameter", + "displayName": "Development mode", + "datatype": "choice", + "description": "Choose the development mode to use for the project.", + "defaultValue": "BackofficeDevelopment", + "choices": [ + { + "choice": "BackofficeDevelopment", + "description": "Enables backoffice development, allowing you to develop from within the backoffice, this is the default behaviour.", + "displayName": "Backoffice Development" + }, + { + "choice": "IDEDevelopment", + "description": "Configures appsettings.Development.json to Development runtime mode and SourceCodeAuto models builder mode, and configures appsettings.json to Production runtime mode, Nothing models builder mode, and enables UseHttps", + "displayName": "IDE Development" + } + ] + }, + "ModelsBuilderMode": { + "type": "parameter", + "displayName": "Models builder mode", + "datatype": "choice", + "description": "Choose the models builder mode to use for the project. When development mode is set to IDEDevelopment this only changes the models builder mode appsetttings.development.json", + "defaultValue": "Default", + "replaces": "MODELS_MODE", + "choices": [ + { + "choice": "Default", + "description": "Let DevelopmentMode determine the models builder mode." + }, + { + "choice": "InMemoryAuto", + "description": "Generate models in memory, automatically updating when a content type change, this means no need for app rebuild, however models are only available in views.", + "displayName": "In Memory Auto" + }, + { + "choice": "SourceCodeManual", + "description": "Generate models as source code, only updating when requested manually, this means a interaction and rebuild is required when content type(s) change, however models are available in code.", + "displayName": "Source Code Manual" + }, + { + "choice": "SourceCodeAuto", + "description": "Generate models as source code, automatically updating when a content type change, this means a rebuild is required when content type(s) change, however models are available in code.", + "displayName": "Source Code Auto" + }, + { + "choice": "Nothing", + "description": "No models are generated, this is recommended for production assuming generated models are used for development." + } + ] + }, "Namespace": { "type": "derived", "valueSource": "name", diff --git a/templates/UmbracoProject/Dockerfile b/templates/UmbracoProject/Dockerfile new file mode 100644 index 0000000000..e3eda648dd --- /dev/null +++ b/templates/UmbracoProject/Dockerfile @@ -0,0 +1,33 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["UmbracoProject/UmbracoProject.csproj", "UmbracoProject/"] +RUN dotnet restore "UmbracoProject/UmbracoProject.csproj" +COPY . . +WORKDIR "/src/UmbracoProject" +RUN dotnet build "UmbracoProject.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "UmbracoProject.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +# We need to make sure that the user running the app has write access to the umbraco folder, in order to write logs and other files. +# Since these are volumes they are created as root by the docker daemon. +USER root +RUN mkdir umbraco +RUN mkdir umbraco/Logs +RUN chown $APP_UID umbraco --recursive +#if (UmbracoRelease = 'LTS') +RUN chown $APP_UID wwwroot/umbraco --recursive +#endif +USER $APP_UID +ENTRYPOINT ["dotnet", "UmbracoProject.dll"] diff --git a/templates/UmbracoProject/UmbracoProject.csproj b/templates/UmbracoProject/UmbracoProject.csproj index ee8dd5e56e..019d5d2990 100644 --- a/templates/UmbracoProject/UmbracoProject.csproj +++ b/templates/UmbracoProject/UmbracoProject.csproj @@ -1,13 +1,20 @@ - net8.0 + DOTNET_VERSION_FROM_TEMPLATE enable enable Umbraco.Cms.Web.UI + + + + @@ -21,11 +28,13 @@ true + false false + diff --git a/templates/UmbracoProject/appsettings.Development.json b/templates/UmbracoProject/appsettings.Development.json index 17b9f86361..0521b835ed 100644 --- a/templates/UmbracoProject/appsettings.Development.json +++ b/templates/UmbracoProject/appsettings.Development.json @@ -25,6 +25,11 @@ //#endif "Umbraco": { "CMS": { + //#if (UseHttpsRedirect || DevelopmentMode == "IDEDevelopment") + "Global": { + "UseHttps": false + }, + //#endif //#if (UsingUnattenedInstall) "Unattended": { "InstallUnattended": true, @@ -36,12 +41,22 @@ "Content": { "MacroErrors": "Throw" }, + //#if (DevelopmentMode == "IDEDevelopment") + "Runtime": { + "Mode": "Development" + }, + //#if (ModelsBuilderMode == "Default") + "ModelsBuilder": { + "ModelsMode": "SourceCodeAuto" + }, + ////#else + //"ModelsBuilder": { + // "ModelsMode": "MODELS_MODE" + //}, + //#endif + //#endif "Hosting": { "Debug": true - }, - "RuntimeMinification": { - "UseInMemoryCache": true, - "CacheBuster": "Timestamp" } } } diff --git a/templates/UmbracoProject/appsettings.json b/templates/UmbracoProject/appsettings.json index 6678478951..23520bbe6b 100644 --- a/templates/UmbracoProject/appsettings.json +++ b/templates/UmbracoProject/appsettings.json @@ -20,7 +20,7 @@ "CMS": { "Global": { "Id": "TELEMETRYID_FROM_TEMPLATE", - //#if (UseHttpsRedirect) + //#if (UseHttpsRedirect || DevelopmentMode == "IDEDevelopment") "UseHttps": true, //#endif //#if (HasNoNodesViewPath) @@ -37,6 +37,24 @@ "Unattended": { "UpgradeUnattended": true }, + //#if (UseDeliveryApi) + "DeliveryApi": { + "Enabled": true + }, + //#endif + //#if (ModelsBuilderMode != "Default" && DevelopmentMode == "BackOfficeDevelopment") + "ModelsBuilder": { + "ModelsMode": "MODELS_MODE" + }, + //#endif + //#if (DevelopmentMode == "IDEDevelopment") + "Runtime": { + "Mode": "Production" + }, + "ModelsBuilder": { + "ModelsMode": "Nothing" + }, + //#endif "Security": { "AllowConcurrentLogins": false } diff --git a/tests/Directory.Packages.props b/tests/Directory.Packages.props index e01362ca11..34370c21df 100644 --- a/tests/Directory.Packages.props +++ b/tests/Directory.Packages.props @@ -4,8 +4,8 @@ - - + + @@ -17,7 +17,7 @@ - + diff --git a/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/ArticleDOC.doc b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/ArticleDOC.doc new file mode 100644 index 0000000000..9232c5f1fd Binary files /dev/null and b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/ArticleDOC.doc differ diff --git a/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/ArticleDOCX.docx b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/ArticleDOCX.docx new file mode 100644 index 0000000000..afbc888bc8 Binary files /dev/null and b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/ArticleDOCX.docx differ diff --git a/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/AudioOGA.oga b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/AudioOGA.oga new file mode 100644 index 0000000000..ab0649d220 Binary files /dev/null and b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/AudioOGA.oga differ diff --git a/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/AudioOPUS.opus b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/AudioOPUS.opus new file mode 100644 index 0000000000..b5c7df1ea7 Binary files /dev/null and b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/AudioOPUS.opus differ diff --git a/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/AudioWEBA.weba b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/AudioWEBA.weba new file mode 100644 index 0000000000..1c18a1f2db Binary files /dev/null and b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/AudioWEBA.weba differ diff --git a/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/Ogv.ogv b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/Ogv.ogv new file mode 100644 index 0000000000..e979ba49e9 Binary files /dev/null and b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/Ogv.ogv differ diff --git a/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/Webm.webm b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/Webm.webm new file mode 100644 index 0000000000..ac90535d4d Binary files /dev/null and b/tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/Webm.webm differ diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 80c133b996..bb227d8a12 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -7,24 +7,18 @@ "name": "acceptancetest", "hasInstallScript": true, "dependencies": { - "@umbraco/json-models-builders": "^2.0.14", - "@umbraco/playwright-testhelpers": "^2.0.0-beta.73", + "@umbraco/json-models-builders": "^2.0.20", + "@umbraco/playwright-testhelpers": "^2.0.0-beta.84", "camelize": "^1.0.0", "dotenv": "^16.3.1", - "faker": "^4.1.0", - "form-data": "^4.0.0", - "node-fetch": "^2.6.7", - "xhr2": "^0.2.1" + "node-fetch": "^2.6.7" }, "devDependencies": { "@playwright/test": "^1.43", "@types/node": "^20.9.0", - "del": "^6.0.0", - "ncp": "^2.0.0", "prompt": "^1.2.0", "tslib": "^2.4.0", - "typescript": "^4.8.3", - "wait-on": "^7.2.0" + "typescript": "^4.8.3" } }, "node_modules/@colors/colors": { @@ -36,190 +30,55 @@ "node": ">=0.1.90" } }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "dev": true - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dev": true, - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@playwright/test": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz", - "integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.0.tgz", + "integrity": "sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==", "dev": true, "dependencies": { - "playwright": "1.43.1" + "playwright": "1.46.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, - "node_modules/@sideway/address": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", - "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", - "dev": true, - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", - "dev": true - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "dev": true - }, "node_modules/@types/node": { - "version": "20.10.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.7.tgz", - "integrity": "sha512-fRbIKb8C/Y2lXxB5eVMj4IU7xpdox0Lh8bUPEdtLysaylsml1hOOx1+STloRs/B9nf7C6kPRmmg/V7aQW7usNg==", + "version": "20.14.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz", + "integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@umbraco/json-models-builders": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.14.tgz", - "integrity": "sha512-fP6hVSSph1iFQ1c65UH80AM6QK3r1CzuIiYOvZh+QOoVzpVFtH1VCHL3J2k8AwaHWLVAEopcvtvH5kkl7Luqww==", + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.20.tgz", + "integrity": "sha512-LmTtklne1HlhMr1nALA+P5FrjIC9jL3A6Pcxj4dy+IPnTgnU2vMYaQIfE8wwz5Z5fZ5AAhWx/Zpdi8xCTbVSuQ==", + "license": "MIT", "dependencies": { "camelize": "^1.0.1" } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "2.0.0-beta.73", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.73.tgz", - "integrity": "sha512-CCURatZa7Ipui9ZTqdZmkpx89Sr5AJLoXogniq6mv84mSVGeCQFYzHvw1op2UE8nkKY5/wyqfrCihjrbW5v8lw==", + "version": "2.0.0-beta.84", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.84.tgz", + "integrity": "sha512-vH13Lg48knTkkLVTwhMXUKTOdjtmixFj0wF5Qhgb++13u4AVDb+oW+TbFwTjSYaLeNMraq5Uhwmto/XuJPs2Rw==", + "license": "MIT", "dependencies": { - "@umbraco/json-models-builders": "2.0.14", + "@umbraco/json-models-builders": "2.0.20", "node-fetch": "^2.6.7" } }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/async": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", "dev": true }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", - "dev": true, - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/camelize": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", @@ -228,15 +87,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/colors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", @@ -246,23 +96,6 @@ "node": ">=0.1.90" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, "node_modules/cycle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", @@ -272,57 +105,15 @@ "node": ">=0.4.0" } }, - "node_modules/del": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", - "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", - "dev": true, - "dependencies": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "url": "https://dotenvx.com" } }, "node_modules/eyes": { @@ -334,343 +125,24 @@ "node": "> 0.1.90" } }, - "node_modules/faker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz", - "integrity": "sha512-ILKg69P6y/D8/wSmDXw35Ly0re8QzQ8pMfBCflsGiZG2ZjMUNLYNexA6lz5pkmJlepVdsiDFUxYAzPQ9/+iGLA==" - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", - "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, - "node_modules/joi": { - "version": "17.11.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", - "integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==", - "dev": true, - "dependencies": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, - "node_modules/ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", - "dev": true, - "bin": { - "ncp": "bin/ncp" - } - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -690,88 +162,34 @@ } } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/playwright": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz", - "integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.0.tgz", + "integrity": "sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==", "dev": true, "dependencies": { - "playwright-core": "1.43.1" + "playwright-core": "1.46.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz", - "integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.0.tgz", + "integrity": "sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==", "dev": true, "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/prompt": { @@ -790,32 +208,6 @@ "node": ">= 6.0.0" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", @@ -828,16 +220,6 @@ "node": ">=0.8" } }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/revalidator": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz", @@ -847,62 +229,6 @@ "node": ">= 0.4.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -912,27 +238,15 @@ "node": "*" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true }, "node_modules/typescript": { @@ -954,25 +268,6 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, - "node_modules/wait-on": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz", - "integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==", - "dev": true, - "dependencies": { - "axios": "^1.6.1", - "joi": "^17.11.0", - "lodash": "^4.17.21", - "minimist": "^1.2.8", - "rxjs": "^7.8.1" - }, - "bin": { - "wait-on": "bin/wait-on" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -1012,20 +307,6 @@ "dependencies": { "lodash": "^4.17.14" } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/xhr2": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", - "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==", - "engines": { - "node": ">= 6" - } } } } diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 9cccd40a36..30706ad5de 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -13,21 +13,15 @@ "devDependencies": { "@playwright/test": "^1.43", "@types/node": "^20.9.0", - "del": "^6.0.0", - "ncp": "^2.0.0", "prompt": "^1.2.0", "tslib": "^2.4.0", - "typescript": "^4.8.3", - "wait-on": "^7.2.0" + "typescript": "^4.8.3" }, "dependencies": { - "@umbraco/json-models-builders": "^2.0.14", - "@umbraco/playwright-testhelpers": "^2.0.0-beta.73", + "@umbraco/json-models-builders": "^2.0.20", + "@umbraco/playwright-testhelpers": "^2.0.0-beta.84", "camelize": "^1.0.0", "dotenv": "^16.3.1", - "faker": "^4.1.0", - "form-data": "^4.0.0", - "node-fetch": "^2.6.7", - "xhr2": "^0.2.1" + "node-fetch": "^2.6.7" } } diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ApiTesting/DataType/DataType.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/ApiTesting/DataType/DataType.spec.ts index e5bbf36b89..2024224119 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/ApiTesting/DataType/DataType.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ApiTesting/DataType/DataType.spec.ts @@ -9,6 +9,7 @@ test.describe('DataType tests', () => { const dataTypeName = 'TestDataType'; const folderName = 'TestDataTypeFolder'; const editorAlias = 'Umbraco.DateTime'; + const editorUiAlias = 'Umb.PropertyEditorUi.DatePicker'; const dataTypeData = [ { "alias": "tester", @@ -29,7 +30,7 @@ test.describe('DataType tests', () => { test('can create dataType', async ({umbracoApi}) => { // Act - dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, dataTypeData); + dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData); // Assert expect(umbracoApi.dataType.doesExist(dataTypeId)).toBeTruthy(); @@ -37,7 +38,7 @@ test.describe('DataType tests', () => { test('can update dataType', async ({umbracoApi}) => { // Arrange - dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, []); + dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, []); const dataType = await umbracoApi.dataType.get(dataTypeId); dataType.values = dataTypeData; @@ -52,7 +53,7 @@ test.describe('DataType tests', () => { test('can delete dataType', async ({umbracoApi}) => { // Arrange - dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, dataTypeData); + dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData); expect(await umbracoApi.dataType.doesExist(dataTypeId)).toBeTruthy(); // Act @@ -65,7 +66,7 @@ test.describe('DataType tests', () => { test('can move a dataType to a folder', async ({umbracoApi}) => { // Arrange await umbracoApi.dataType.ensureNameNotExists(folderName); - dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, dataTypeData); + dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData); expect(await umbracoApi.dataType.doesNameExist(dataTypeName)).toBeTruthy(); dataTypeFolderId = await umbracoApi.dataType.createFolder(folderName); expect(await umbracoApi.dataType.doesFolderExist(dataTypeFolderId)).toBeTruthy(); @@ -82,7 +83,7 @@ test.describe('DataType tests', () => { test('can copy a dataType to a folder', async ({umbracoApi}) => { // Arrange await umbracoApi.dataType.ensureNameNotExists(folderName); - dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, dataTypeData); + dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData); dataTypeFolderId = await umbracoApi.dataType.createFolder(folderName); const dataType = await umbracoApi.dataType.get(dataTypeId); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts index b2cbeb9c6e..a680f656be 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts @@ -1,4 +1,4 @@ -import { expect } from '@playwright/test'; +import {expect} from '@playwright/test'; import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; let documentTypeId = ''; @@ -67,7 +67,7 @@ test('can open document type', async ({umbracoApi, umbracoUi}) => { test('can open template', async ({umbracoApi, umbracoUi}) => { // Arrange - const templateName = "TestTemplateForContent"; + const templateName = 'TestTemplateForContent'; await umbracoApi.template.ensureNameNotExists(templateName); const templateId = await umbracoApi.template.createDefaultTemplate(templateName); documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedTemplate(documentTypeName, templateId, true); @@ -88,8 +88,8 @@ test('can open template', async ({umbracoApi, umbracoUi}) => { test('can change template', async ({umbracoApi, umbracoUi}) => { // Arrange - const firstTemplateName = "TestTemplateOneForContent"; - const secondTemplateName = "TestTemplateTwoForContent"; + const firstTemplateName = 'TestTemplateOneForContent'; + const secondTemplateName = 'TestTemplateTwoForContent'; await umbracoApi.template.ensureNameNotExists(firstTemplateName); await umbracoApi.template.ensureNameNotExists(secondTemplateName); const firstTemplateId = await umbracoApi.template.createDefaultTemplate(firstTemplateName); @@ -115,8 +115,8 @@ test('can change template', async ({umbracoApi, umbracoUi}) => { test('cannot change to a template that is not allowed in the document type', async ({umbracoApi, umbracoUi}) => { // Arrange - const firstTemplateName = "TestTemplateOneForContent"; - const secondTemplateName = "TestTemplateTwoForContent"; + const firstTemplateName = 'TestTemplateOneForContent'; + const secondTemplateName = 'TestTemplateTwoForContent'; await umbracoApi.template.ensureNameNotExists(firstTemplateName); await umbracoApi.template.ensureNameNotExists(secondTemplateName); const firstTemplateId = await umbracoApi.template.createDefaultTemplate(firstTemplateName); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCheckboxList.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCheckboxList.spec.ts index 6435272481..1cfe2a419f 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCheckboxList.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCheckboxList.spec.ts @@ -1,4 +1,4 @@ -import { ConstantHelper, test, AliasHelper } from '@umbraco/playwright-testhelpers'; +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; import {expect} from "@playwright/test"; const contentName = 'TestContent'; @@ -18,6 +18,7 @@ test.afterEach(async ({umbracoApi}) => { test('can create content with the checkbox list data type', async ({umbracoApi, umbracoUi}) => { // Arrange + const expectedState = 'Draft'; const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); await umbracoUi.content.goToSection(ConstantHelper.sections.content); @@ -33,26 +34,27 @@ test('can create content with the checkbox list data type', async ({umbracoApi, await umbracoUi.content.isSuccessNotificationVisible(); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); expect(contentData.values).toEqual([]); }); test('can publish content with the checkbox list data type', async ({umbracoApi, umbracoUi}) => { // Arrange + const expectedState = 'Published'; const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act - await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); - await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickSaveAndPublishButton(); // Assert await umbracoUi.content.doesSuccessNotificationsHaveCount(2); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); expect(contentData.values).toEqual([]); }); @@ -61,14 +63,12 @@ test('can create content with the custom checkbox list data type', async ({umbra const customDataTypeName = 'CustomCheckboxList'; const optionValues = ['testOption1', 'testOption2']; const customDataTypeId = await umbracoApi.dataType.createCheckboxListDataType(customDataTypeName, optionValues); - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act - await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); - await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.chooseCheckboxListOption(optionValues[0]); await umbracoUi.content.clickSaveAndPublishButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithContentPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithContentPicker.spec.ts index ce18c79e9a..5764b4b7fc 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithContentPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithContentPicker.spec.ts @@ -8,11 +8,10 @@ const contentPickerDocumentTypeName = 'DocumentTypeForContentPicker'; const contentPickerName = 'TestContentPicker'; let contentPickerDocumentTypeId = ''; -test.beforeEach(async ({umbracoApi, umbracoUi}) => { +test.beforeEach(async ({umbracoApi}) => { await umbracoApi.documentType.ensureNameNotExists(documentTypeName); await umbracoApi.document.ensureNameNotExists(contentName); contentPickerDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(contentPickerDocumentTypeName); - await umbracoUi.goToBackOffice(); }); test.afterEach(async ({umbracoApi}) => { @@ -24,9 +23,11 @@ test.afterEach(async ({umbracoApi}) => { test('can create content with the content picker datatype', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange + const expectedState = 'Draft'; const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId); + await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act @@ -38,24 +39,25 @@ test('can create content with the content picker datatype', {tag: '@smoke'}, asy await umbracoUi.content.clickSaveButton(); // Assert - await umbracoUi.content.doesSuccessNotificationsHaveCount(1); + await umbracoUi.content.isSuccessNotificationVisible(); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); expect(contentData.values[0].value).toEqual(contentPickerId); }); test('can publish content with the content picker data type', async ({umbracoApi, umbracoUi}) => { // Arrange + const expectedState = 'Published'; const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId); + await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act - await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); - await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.addContentPicker(contentPickerName); await umbracoUi.content.clickSaveAndPublishButton(); @@ -63,6 +65,7 @@ test('can publish content with the content picker data type', async ({umbracoApi await umbracoUi.content.doesSuccessNotificationsHaveCount(2); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); expect(contentData.values[0].value).toEqual(contentPickerId); }); @@ -70,15 +73,15 @@ test('can open content picker in the content', async ({umbracoApi, umbracoUi}) = // Arrange const customDataTypeName = 'CustomContentPicker'; const customDataTypeId = await umbracoApi.dataType.createContentPickerDataTypeWithShowOpenButton(customDataTypeName); - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + // Create content to pick await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId); + await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act - await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); - await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.addContentPicker(contentPickerName); // Assert @@ -97,19 +100,18 @@ test('can choose start node for the content picker in the content', async ({umbr const childContentPickerName = 'TestChildContentPicker'; await umbracoApi.documentType.ensureNameNotExists(childContentPickerDocumentTypeName); const childContentPickerDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childContentPickerDocumentTypeName); - const contentPickerDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(contentPickerName, childContentPickerDocumentTypeId); + contentPickerDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(contentPickerName, childContentPickerDocumentTypeId); const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId); await umbracoApi.document.createDefaultDocumentWithParent(childContentPickerName, childContentPickerDocumentTypeId, contentPickerId); // Create a custom content picker with start node const customDataTypeId = await umbracoApi.dataType.createContentPickerDataTypeWithStartNode(customDataTypeName, contentPickerId); - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + 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.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); - await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickChooseButton(); // Assert @@ -128,19 +130,18 @@ test.skip('can ignore user start node for the content picker in the content', as const childContentPickerName = 'TestChildContentPicker'; await umbracoApi.documentType.ensureNameNotExists(childContentPickerDocumentTypeName); const childContentPickerDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childContentPickerDocumentTypeName); - const contentPickerDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(contentPickerName, childContentPickerDocumentTypeId); + contentPickerDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(contentPickerName, childContentPickerDocumentTypeId); const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId); await umbracoApi.document.createDefaultDocumentWithParent(childContentPickerName, childContentPickerDocumentTypeId, contentPickerId); // Create a custom content picker with the setting "ignore user start node" is enable const customDataTypeId = await umbracoApi.dataType.createContentPickerDataTypeWithIgnoreUserStartNodes(customDataTypeName, contentPickerId); - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + 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.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); - await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickChooseButton(); // Assert @@ -158,6 +159,7 @@ test('can remove content picker in the content', async ({umbracoApi, umbracoUi}) const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId); await umbracoApi.document.createDocumentWithContentPicker(contentName, documentTypeId, contentPickerId); + await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDropdown.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDropdown.spec.ts index 07343cfaa1..7a4c246b01 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDropdown.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDropdown.spec.ts @@ -1,4 +1,4 @@ -import { ConstantHelper, test, AliasHelper } from '@umbraco/playwright-testhelpers'; +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; import {expect} from "@playwright/test"; const contentName = 'TestContent'; @@ -20,6 +20,7 @@ for (const dataTypeName of dataTypeNames) { 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); @@ -35,26 +36,27 @@ for (const dataTypeName of dataTypeNames) { 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); - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act - await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); - await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickSaveAndPublishButton(); // Assert await umbracoUi.content.doesSuccessNotificationsHaveCount(2); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); expect(contentData.values).toEqual([]); }); @@ -65,14 +67,12 @@ for (const dataTypeName of dataTypeNames) { const selectedOptions = dataTypeName === 'Dropdown' ? [optionValues[0]] : optionValues; const isMultiple = dataTypeName === 'Dropdown' ? false : true; const customDataTypeId = await umbracoApi.dataType.createDropdownDataType(customDataTypeName, isMultiple, optionValues); - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act - await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); - await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.chooseDropdownOption(selectedOptions); await umbracoUi.content.clickSaveAndPublishButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts new file mode 100644 index 0000000000..f376000f81 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts @@ -0,0 +1,105 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Image Cropper'; +const imageFileName = 'Umbraco.png'; +const imageFilePath = './fixtures/mediaLibrary/' + imageFileName; +const defaultFocalPoint = { + left: 0.5, + top: 0.5, +}; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the image cropper data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.uploadFile(imageFilePath); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(imageFileName)); + // TODO: is no longer null, we need to set an expected crops value + // expect(contentData.values[0].value.crops).toEqual([]); + expect(contentData.values[0].value.focalPoint).toEqual(defaultFocalPoint); +}); + +test('can publish content with the image cropper data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.uploadFile(imageFilePath); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(imageFileName)); + // TODO: is no longer null, we need to set an expected crops value + // expect(contentData.values[0].value.crops).toEqual([]); + expect(contentData.values[0].value.focalPoint).toEqual(defaultFocalPoint); +}); + +test('can create content with the custom image cropper data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const customDataTypeName = 'CustomImageCropper'; + const cropValue = ['TestCropLabel', 100, 50]; + const customDataTypeId = await umbracoApi.dataType.createImageCropperDataTypeWithOneCrop(customDataTypeName, cropValue[0], cropValue[1], cropValue[2]); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.uploadFile(imageFilePath); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName)); + expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(imageFileName)); + expect(contentData.values[0].value.focalPoint).toEqual(defaultFocalPoint); + expect(contentData.values[0].value.crops[0].alias).toEqual(AliasHelper.toAlias(cropValue[0])); + expect(contentData.values[0].value.crops[0].width).toEqual(cropValue[1]); + expect(contentData.values[0].value.crops[0].height).toEqual(cropValue[2]); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts index b8cb9f4c66..f855841738 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts @@ -1,4 +1,4 @@ -import { ConstantHelper, test, AliasHelper } from '@umbraco/playwright-testhelpers'; +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; import {expect} from "@playwright/test"; const dataTypeName = 'Media Picker'; @@ -13,7 +13,6 @@ test.beforeEach(async ({umbracoApi, umbracoUi}) => { await umbracoApi.document.ensureNameNotExists(contentName); await umbracoApi.media.ensureNameNotExists(mediaFileName); mediaFileId = await umbracoApi.media.createDefaultMediaFile(mediaFileName); - await umbracoUi.goToBackOffice(); }); test.afterEach(async ({umbracoApi}) => { @@ -24,8 +23,10 @@ test.afterEach(async ({umbracoApi}) => { test('can create content with the media picker data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange + const expectedState = 'Draft'; const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act @@ -41,6 +42,7 @@ test('can create content with the media picker data type', {tag: '@smoke'}, asyn await umbracoUi.content.isSuccessNotificationVisible(); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); expect(contentData.values[0].value[0].mediaKey).toEqual(mediaFileId); expect(contentData.values[0].value[0].mediaTypeAlias).toEqual(mediaTypeName); @@ -50,8 +52,10 @@ test('can create content with the media picker data type', {tag: '@smoke'}, asyn test('can publish content with the media picker data type', async ({umbracoApi, umbracoUi}) => { // Arrange + const expectedState = 'Published'; const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act @@ -67,6 +71,7 @@ test('can publish content with the media picker data type', async ({umbracoApi, await umbracoUi.content.doesSuccessNotificationsHaveCount(2); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); expect(contentData.values[0].value[0].mediaKey).toEqual(mediaFileId); expect(contentData.values[0].value[0].mediaTypeAlias).toEqual(mediaTypeName); @@ -79,6 +84,7 @@ test('can remove a media picker in the content', async ({umbracoApi, umbracoUi}) const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); await umbracoApi.document.createDocumentWithOneMediaPicker(contentName, documentTypeId, mediaFileId); + await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act @@ -87,7 +93,7 @@ test('can remove a media picker in the content', async ({umbracoApi, umbracoUi}) await umbracoUi.content.clickSaveButton(); // Assert - await umbracoUi.content.doesSuccessNotificationsHaveCount(1); + await umbracoUi.content.isSuccessNotificationVisible(); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.values).toEqual([]); @@ -103,14 +109,13 @@ test('can limit the media picker in the content by setting the start node', asyn await umbracoApi.media.ensureNameNotExists(childMediaName); await umbracoApi.media.createDefaultMediaFileAndParentId(childMediaName, mediaFolderId); const customDataTypeId = await umbracoApi.dataType.createMediaPickerDataTypeWithStartNodeId(customDataTypeName, mediaFolderId); - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + 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.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); - await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickChooseMediaPickerButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMemberPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMemberPicker.spec.ts new file mode 100644 index 0000000000..b049976a1c --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMemberPicker.spec.ts @@ -0,0 +1,97 @@ +import { ConstantHelper, test, AliasHelper } from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const dataTypeName = 'Member Picker'; +const contentName = 'TestContent'; +const memberName = 'TestMemberForContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const memberTypeName = 'Test Member Type'; +const memberInfo = ['testmember@acceptance.test', 'testmember', '0123456789']; +let memberId = ''; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.memberType.ensureNameNotExists(memberTypeName); + await umbracoApi.member.ensureNameNotExists(memberName); + const memberTypeId = await umbracoApi.memberType.createDefaultMemberType(memberTypeName); + memberId = await umbracoApi.member.createDefaultMember(memberName, memberTypeId, memberInfo[0], memberInfo[1], memberInfo[2]); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.memberType.ensureNameNotExists(memberTypeName); + await umbracoApi.member.ensureNameNotExists(memberName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the member picker data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickChooseMemberPickerButton(); + await umbracoUi.content.selectMemberByName(memberName); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value).toEqual(memberId); +}); + +test('can publish content with the member picker data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickChooseMemberPickerButton(); + await umbracoUi.content.selectMemberByName(memberName); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value).toEqual(memberId); +}); + +test('can remove a member picker in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithMemberPicker(contentName, documentTypeId, memberId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.removeMemberPickerByName(memberName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values).toEqual([]); +}); + diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultiURLPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultiURLPicker.spec.ts new file mode 100644 index 0000000000..6c6135b3e3 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultiURLPicker.spec.ts @@ -0,0 +1,258 @@ +import { ConstantHelper, test, AliasHelper } from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const dataTypeName = 'Multi URL Picker'; +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const link = 'https://docs.umbraco.com'; +const linkTitle = 'Umbraco Documentation'; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the document link', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + // Create a document to link + const documentTypeForLinkedDocumentName = 'TestDocumentType'; + const documentTypeForLinkedDocumentId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeForLinkedDocumentName); + const linkedDocumentName = 'LinkedDocument'; + const linkedDocumentId = await umbracoApi.document.createDefaultDocument(linkedDocumentName, documentTypeForLinkedDocumentId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickAddMultiURLPickerButton(); + await umbracoUi.content.selectLinkByName(linkedDocumentName); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.length).toBe(1); + expect(contentData.values[0].value[0].type).toEqual('document'); + expect(contentData.values[0].value[0].icon).toEqual('icon-document'); + expect(contentData.values[0].value[0].target).toBeNull(); + expect(contentData.values[0].value[0].unique).toEqual(linkedDocumentId); + // Uncomment this when the front-end is ready. Currently the link title is not auto filled after choosing document to link + //expect(contentData.values[0].value[0].name).toEqual(linkedDocumentId); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(documentTypeForLinkedDocumentName); + await umbracoApi.document.ensureNameNotExists(linkedDocumentName); +}); + +test('can publish content with the document link', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + // Create a document to link + const documentTypeForLinkedDocumentName = 'TestDocumentType'; + const documentTypeForLinkedDocumentId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeForLinkedDocumentName); + const linkedDocumentName = 'ContentToPick'; + const linkedDocumentId = await umbracoApi.document.createDefaultDocument(linkedDocumentName, documentTypeForLinkedDocumentId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickAddMultiURLPickerButton(); + await umbracoUi.content.selectLinkByName(linkedDocumentName); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.length).toBe(1); + expect(contentData.values[0].value[0].type).toEqual('document'); + expect(contentData.values[0].value[0].icon).toEqual('icon-document'); + expect(contentData.values[0].value[0].target).toBeNull(); + expect(contentData.values[0].value[0].unique).toEqual(linkedDocumentId); + // Uncomment this when the front-end is ready. Currently the link title is not auto filled after choosing document to link + //expect(contentData.values[0].value[0].name).toEqual(linkedDocumentId); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(documentTypeForLinkedDocumentName); + await umbracoApi.document.ensureNameNotExists(linkedDocumentName); +}); + +test('can create content with the external link', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickAddMultiURLPickerButton(); + await umbracoUi.content.enterLink(link); + await umbracoUi.content.enterLinkTitle(linkTitle); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.length).toBe(1); + expect(contentData.values[0].value[0].type).toEqual('external'); + expect(contentData.values[0].value[0].icon).toEqual('icon-link'); + expect(contentData.values[0].value[0].name).toEqual(linkTitle); + expect(contentData.values[0].value[0].url).toEqual(link); +}); + +test('can create content with the media link', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + // Create a media to pick + const mediaFileName = 'TestMediaFileForContent'; + await umbracoApi.media.ensureNameNotExists(mediaFileName); + const mediaFileId = await umbracoApi.media.createDefaultMediaWithImage(mediaFileName); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickAddMultiURLPickerButton(); + await umbracoUi.content.selectLinkByName(mediaFileName); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.length).toBe(1); + expect(contentData.values[0].value[0].type).toEqual('media'); + expect(contentData.values[0].value[0].icon).toEqual('icon-picture'); + expect(contentData.values[0].value[0].unique).toEqual(mediaFileId); + // Uncomment this when the front-end is ready. Currently the link title is not auto filled after choosing media to link + //expect(contentData.values[0].value[0].name).toEqual(mediaFileName); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaFileName); +}); + +test('can add multiple links in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + // Create a media to pick + const mediaFileName = 'TestMediaFileForContent'; + await umbracoApi.media.ensureNameNotExists(mediaFileName); + const mediaFileId = await umbracoApi.media.createDefaultMediaWithImage(mediaFileName); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + // Add media link + await umbracoUi.content.clickAddMultiURLPickerButton(); + await umbracoUi.content.selectLinkByName(mediaFileName); + await umbracoUi.content.clickSubmitButton(); + // Add external link + await umbracoUi.content.clickAddMultiURLPickerButton(); + await umbracoUi.content.enterLink(link); + await umbracoUi.content.enterLinkTitle(linkTitle); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.length).toBe(2); + // Verify the information of the first URL picker + expect(contentData.values[0].value[0].type).toEqual('media'); + expect(contentData.values[0].value[0].icon).toEqual('icon-picture'); + expect(contentData.values[0].value[0].unique).toEqual(mediaFileId); + // Uncomment this when the front-end is ready. Currently the link title is not auto filled after choosing media to link + //expect(contentData.values[0].value[0].name).toEqual(mediaFileName); + // Verify the information of the second URL picker + expect(contentData.values[0].value[1].type).toEqual('external'); + expect(contentData.values[0].value[1].icon).toEqual('icon-link'); + expect(contentData.values[0].value[1].name).toEqual(linkTitle); + expect(contentData.values[0].value[1].url).toEqual(link); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaFileName); +}); + +test('can remove the URL picker in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithExternalLinkURLPicker(contentName, documentTypeId, link, linkTitle); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.removeUrlPickerByName(linkTitle); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values).toEqual([]); +}); + +test('can edit the URL picker in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const updatedLinkTitle = 'Updated Umbraco Documentation'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithExternalLinkURLPicker(contentName, documentTypeId, link, linkTitle); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickEditUrlPickerButtonByName(linkTitle); + await umbracoUi.content.enterLinkTitle(updatedLinkTitle); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.length).toBe(1); + expect(contentData.values[0].value[0].type).toEqual('external'); + expect(contentData.values[0].value[0].icon).toEqual('icon-link'); + expect(contentData.values[0].value[0].name).toEqual(updatedLinkTitle); + expect(contentData.values[0].value[0].url).toEqual(link); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleImageMediaPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleImageMediaPicker.spec.ts new file mode 100644 index 0000000000..907889247c --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleImageMediaPicker.spec.ts @@ -0,0 +1,120 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const dataTypeName = 'Multiple Image Media Picker'; +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const firstMediaFileName = 'TestFirstMedia'; +const secondMediaFileName = 'TestSecondMedia'; +const mediaTypeName = 'Image'; +let firstMediaFileId = ''; +let secondMediaFileId = ''; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.media.ensureNameNotExists(firstMediaFileName); + firstMediaFileId = await umbracoApi.media.createDefaultMediaWithImage(firstMediaFileName); + await umbracoApi.media.ensureNameNotExists(secondMediaFileName); + secondMediaFileId = await umbracoApi.media.createDefaultMediaWithImage(secondMediaFileName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.media.ensureNameNotExists(firstMediaFileName); + await umbracoApi.media.ensureNameNotExists(secondMediaFileName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with multiple image media picker data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can publish content with multiple image media picker data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can add multiple images to the multiple image media picker', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickChooseMediaPickerButton(); + await umbracoUi.content.clickMediaByNameInMediaPicker(firstMediaFileName); + await umbracoUi.content.clickMediaByNameInMediaPicker(secondMediaFileName); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.length).toBe(2); + expect(contentData.values[0].value[0].mediaKey).toEqual(firstMediaFileId); + expect(contentData.values[0].value[0].mediaTypeAlias).toEqual(mediaTypeName); + expect(contentData.values[0].value[1].mediaKey).toEqual(secondMediaFileId); + expect(contentData.values[0].value[1].mediaTypeAlias).toEqual(mediaTypeName); +}); + +test('can remove a media picker in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithTwoMediaPicker(contentName, documentTypeId, firstMediaFileId, secondMediaFileId, AliasHelper.toAlias(dataTypeName)); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.removeMediaPickerByName(firstMediaFileName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.length).toBe(1); + expect(contentData.values[0].value[0].mediaKey).toEqual(secondMediaFileId); + expect(contentData.values[0].value[0].mediaTypeAlias).toEqual(mediaTypeName); +}); + diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleMediaPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleMediaPicker.spec.ts index 0093866fdd..2cf94e8a89 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleMediaPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleMediaPicker.spec.ts @@ -1,12 +1,12 @@ -import { ConstantHelper, test, AliasHelper } from '@umbraco/playwright-testhelpers'; +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; import {expect} from "@playwright/test"; const dataTypeName = 'Multiple Media Picker'; const contentName = 'TestContent'; const documentTypeName = 'TestDocumentTypeForContent'; const firstMediaFileName = 'TestFirstMedia'; -const firstMediaTypeName = 'File'; const secondMediaFileName = 'TestSecondMedia'; +const firstMediaTypeName = 'File'; const secondMediaTypeName = 'Image'; let firstMediaFileId = ''; let secondMediaFileId = ''; @@ -24,12 +24,13 @@ test.beforeEach(async ({umbracoApi, umbracoUi}) => { test.afterEach(async ({umbracoApi}) => { await umbracoApi.media.ensureNameNotExists(firstMediaFileName); await umbracoApi.media.ensureNameNotExists(secondMediaFileName); - await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.document.ensureNameNotExists(contentName); await umbracoApi.documentType.ensureNameNotExists(documentTypeName); }); test('can create content with multiple media picker data type', async ({umbracoApi, umbracoUi}) => { // Arrange + const expectedState = 'Draft'; const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); await umbracoUi.content.goToSection(ConstantHelper.sections.content); @@ -39,45 +40,55 @@ test('can create content with multiple media picker data type', async ({umbracoA await umbracoUi.content.clickCreateButton(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); - await umbracoUi.content.selectMediaByName(firstMediaFileName); - await umbracoUi.content.clickSubmitButton(); - await umbracoUi.content.selectMediaByName(secondMediaFileName); - await umbracoUi.content.clickSubmitButton(); await umbracoUi.content.clickSaveButton(); // Assert - await umbracoUi.content.doesSuccessNotificationsHaveCount(1); + await umbracoUi.content.isSuccessNotificationVisible(); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); - expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); - expect(contentData.values[0].value.length).toBe(2); - expect(contentData.values[0].value[0].mediaKey).toEqual(firstMediaFileId); - expect(contentData.values[0].value[0].mediaTypeAlias).toEqual(firstMediaTypeName); - expect(contentData.values[0].value[1].mediaKey).toEqual(secondMediaFileId); - expect(contentData.values[0].value[1].mediaTypeAlias).toEqual(secondMediaTypeName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); }); test('can publish content with multiple media picker data type', async ({umbracoApi, umbracoUi}) => { // Arrange + const expectedState = 'Published'; const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act - await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); - await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.enterContentName(contentName); - await umbracoUi.content.selectMediaByName(firstMediaFileName); - await umbracoUi.content.clickSubmitButton(); - await umbracoUi.content.selectMediaByName(secondMediaFileName); - await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickSaveAndPublishButton(); // Assert await umbracoUi.content.doesSuccessNotificationsHaveCount(2); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can add multiple media files to the multiple media picker', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickChooseMediaPickerButton(); + await umbracoUi.content.clickMediaByNameInMediaPicker(firstMediaFileName); + await umbracoUi.content.clickMediaByNameInMediaPicker(secondMediaFileName); + await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); expect(contentData.values[0].value.length).toBe(2); expect(contentData.values[0].value[0].mediaKey).toEqual(firstMediaFileId); @@ -90,7 +101,7 @@ test('can remove a media picker in the content', async ({umbracoApi, umbracoUi}) // Arrange const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); - await umbracoApi.document.createDocumentWithTwoMediaPicker(contentName, documentTypeId, firstMediaFileId, secondMediaFileId); + await umbracoApi.document.createDocumentWithTwoMediaPicker(contentName, documentTypeId, firstMediaFileId, secondMediaFileId, AliasHelper.toAlias(dataTypeName)); await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act @@ -99,7 +110,7 @@ test('can remove a media picker in the content', async ({umbracoApi, umbracoUi}) await umbracoUi.content.clickSaveButton(); // Assert - await umbracoUi.content.doesSuccessNotificationsHaveCount(1); + await umbracoUi.content.isSuccessNotificationVisible(); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); @@ -108,3 +119,4 @@ test('can remove a media picker in the content', async ({umbracoApi, umbracoUi}) expect(contentData.values[0].value[0].mediaTypeAlias).toEqual(secondMediaTypeName); }); + diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithNumeric.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithNumeric.spec.ts new file mode 100644 index 0000000000..946c16b312 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithNumeric.spec.ts @@ -0,0 +1,65 @@ +import { ConstantHelper, test, AliasHelper } from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Numeric'; +const number = 10; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the numeric data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.enterNumeric(number); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value).toEqual(number); +}); + +test('can publish content with the numeric data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.enterNumeric(number); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value).toEqual(number); +}); + diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithPropertyEditors.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithPropertyEditors.spec.ts index dcfaba9ff7..074a4840f0 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithPropertyEditors.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithPropertyEditors.spec.ts @@ -47,30 +47,6 @@ test('can create content with the Rich Text Editor datatype', {tag: '@smoke'}, a expect(contentData.values[0].value).toEqual(expectedContentValue); }); -test('can create content with the text area datatype', async ({umbracoApi, umbracoUi}) => { - // Arrange - const dataTypeName = 'Textarea'; - const contentText = 'This is Textarea content!'; - const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); - await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - - // Act - await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); - await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.enterContentName(contentName); - await umbracoUi.content.enterTextArea(contentText); - await umbracoUi.content.clickSaveAndPublishButton(); - - // Assert - await umbracoUi.content.doesSuccessNotificationsHaveCount(2); - expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); - const contentData = await umbracoApi.document.getByName(contentName); - expect(contentData.values[0].value).toEqual(contentText); -}); - // TODO: Remove skip when the front-end is ready. Currently it returns error when publishing a content test.skip('can create content with the upload file datatype', async ({umbracoApi, umbracoUi}) => { // Arrange @@ -96,30 +72,6 @@ test.skip('can create content with the upload file datatype', async ({umbracoApi expect(contentData.values[0].value.src).toContainEqual(uploadFilePath); }); -test('can create content with the tags datatype', async ({umbracoApi, umbracoUi}) => { - // Arrange - const dataTypeName = 'Tags'; - const tagName = 'test'; - const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); - await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - - // Act - await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); - await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.enterContentName(contentName); - await umbracoUi.content.addTags(tagName); - await umbracoUi.content.clickSaveAndPublishButton(); - - // Assert - await umbracoUi.content.doesSuccessNotificationsHaveCount(2); - expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); - const contentData = await umbracoApi.document.getByName(contentName); - expect(contentData.values[0].value).toEqual([tagName]); -}); - // TODO: Remove skip and update the test when the front-end is ready. Currently the list of content is not displayed. test.skip('can create content with the list view - content datatype', async ({umbracoApi, umbracoUi}) => { // Arrange diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithRadiobox.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithRadiobox.spec.ts new file mode 100644 index 0000000000..195483125b --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithRadiobox.spec.ts @@ -0,0 +1,87 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Radiobox'; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the radiobox data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can publish content with the radiobox data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can create content with the custom radiobox data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const customDataTypeName = 'CustomRadiobox'; + const optionValues = ['testOption1', 'testOption2']; + const customDataTypeId = await umbracoApi.dataType.createRadioboxDataType(customDataTypeName, optionValues); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.chooseRadioboxOption(optionValues[0]); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName)); + expect(contentData.values[0].value).toEqual(optionValues[0]); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTags.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTags.spec.ts new file mode 100644 index 0000000000..33856a5b35 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTags.spec.ts @@ -0,0 +1,85 @@ +import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Tags'; +const tagsName = ['testTag1', 'testTag2']; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with one tag', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickPlusIconButton(); + await umbracoUi.content.enterTag(tagsName[0]); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].value).toEqual([tagsName[0]]); +}); + +test('can publish content with multiple tags', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickPlusIconButton(); + await umbracoUi.content.enterTag(tagsName[0]); + await umbracoUi.content.enterTag(tagsName[1]); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].value).toEqual(tagsName); +}); + +test('can remove a tag in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithTags(contentName, documentTypeId, [tagsName[0]]); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.removeTagByName(tagsName[0]); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values).toEqual([]); +}); + diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextarea.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextarea.spec.ts new file mode 100644 index 0000000000..54fc3c5f11 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextarea.spec.ts @@ -0,0 +1,104 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Textarea'; +const text = 'This is the content with textarea'; +const customDataTypeName = 'Custom Textarea'; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the textarea data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can publish content with the textarea data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can input text into the textarea', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.enterTextArea(text); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value).toEqual(text); +}); + +test('cannot input the text that exceeds the allowed amount of characters', async ({umbracoApi, umbracoUi}) => { + // Arrange + const maxChars = 100; + const textExceedMaxChars = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam mattis porttitor orci id cursus. Nulla'; + const warningMessage = 'This field exceeds the allowed amount of characters'; + const dataTypeId = await umbracoApi.dataType.createTextareaDataType(customDataTypeName, maxChars); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.enterTextArea(textExceedMaxChars); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isTextWithExactNameVisible(warningMessage); + await umbracoUi.content.isSuccessNotificationVisible(); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextstring.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextstring.spec.ts new file mode 100644 index 0000000000..f834ae35de --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextstring.spec.ts @@ -0,0 +1,104 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Textstring'; +const text = 'This is the content with textstring'; +const customDataTypeName = 'Custom Textstring'; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the textstring data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can publish content with the textstring data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can input text into the textstring', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.enterTextstring(text); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value).toEqual(text); +}); + +test('cannot input the text that exceeds the allowed amount of characters', async ({umbracoApi, umbracoUi}) => { + // Arrange + const maxChars = 20; + const textExceedMaxChars = 'Lorem ipsum dolor sit'; + const warningMessage = 'This field exceeds the allowed amount of characters'; + const dataTypeId = await umbracoApi.dataType.createTextstringDataType(customDataTypeName, maxChars); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.enterTextstring(textExceedMaxChars); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isTextWithExactNameVisible(warningMessage); + await umbracoUi.content.isSuccessNotificationVisible(); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTrueFalse.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTrueFalse.spec.ts new file mode 100644 index 0000000000..479176b401 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTrueFalse.spec.ts @@ -0,0 +1,142 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'True/false'; +const customDataTypeName = 'Custom Truefalse'; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the true/false data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can publish content with the true/false data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can toggle the true/false value in the content ', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickToggleButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual('truefalse'); + expect(contentData.values[0].value).toEqual(true); +}); + +test('can toggle the true/false value with the initial state enabled', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeId = await umbracoApi.dataType.createTrueFalseDataTypeWithInitialState(customDataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickToggleButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName)); + expect(contentData.values[0].value).toEqual(false); +}); + +test('can show the label on for the true/false in the content ', async ({umbracoApi, umbracoUi}) => { + // Arrange + const labelOn = 'Test Label On'; + const dataTypeId = await umbracoApi.dataType.createTrueFalseDataTypeWithLabelOn(customDataTypeName, labelOn); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickToggleButton(); + + // Assert + await umbracoUi.content.doesToggleHaveLabel(labelOn); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + +test('can show the label off for the true/false in the content ', async ({umbracoApi, umbracoUi}) => { + // Arrange + const labelOff = 'Test Label Off'; + const dataTypeId = await umbracoApi.dataType.createTrueFalseDataTypeWithLabelOff(customDataTypeName, labelOff); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + + // Assert + await umbracoUi.content.doesToggleHaveLabel(labelOff); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadArticle.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadArticle.spec.ts new file mode 100644 index 0000000000..33c25bcd53 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadArticle.spec.ts @@ -0,0 +1,112 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Upload Article'; +const uploadFilePath = './fixtures/mediaLibrary/'; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the upload article data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can publish content with the upload article data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +const uploadFiles = [ + {fileExtension: 'pdf', fileName: 'Article.pdf'}, + {fileExtension: 'docx', fileName: 'ArticleDOCX.docx'}, + {fileExtension: 'doc', fileName: 'ArticleDOC.doc'} +]; +for (const uploadFile of uploadFiles) { + test(`can upload an article with the ${uploadFile.fileExtension} extension in the content`, async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.uploadFile(uploadFilePath + uploadFile.fileName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(uploadFile.fileName)); + }); +} + +// TODO: Remove skip when the front-end is ready. Currently the uploaded file still displays after removing. +test.skip('can remove an article file in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const uploadFileName = 'Article.pdf'; + const mimeType = 'application/pdf'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithUploadFile(contentName, documentTypeId, dataTypeName, uploadFileName, mimeType); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickRemoveFilesButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values).toEqual([]); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadAudio.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadAudio.spec.ts new file mode 100644 index 0000000000..21114c52b5 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadAudio.spec.ts @@ -0,0 +1,113 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Upload Audio'; +const uploadFilePath = './fixtures/mediaLibrary/'; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the upload audio data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can publish content with the upload audio data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +const uploadFiles = [ + {fileExtension: 'mp3', fileName: 'Audio.mp3'}, + {fileExtension: 'weba', fileName: 'AudioWEBA.weba'}, + {fileExtension: 'oga', fileName: 'AudioOGA.oga'}, + {fileExtension: 'opus', fileName: 'AudioOPUS.opus'} +]; +for (const uploadFile of uploadFiles) { + test(`can upload an audio with the ${uploadFile.fileExtension} extension in the content`, async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.uploadFile(uploadFilePath + uploadFile.fileName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(uploadFile.fileName)); + }); +} + +// TODO: Remove skip when the front-end is ready. Currently the uploaded file still displays after removing. +test.skip('can remove an audio file in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const uploadFileName = 'Audio.mp3'; + const mineType = 'audio/mpeg'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithUploadFile(contentName, documentTypeId, dataTypeName, uploadFileName, mineType); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickRemoveFilesButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values).toEqual([]); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadFile.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadFile.spec.ts new file mode 100644 index 0000000000..eec9b3febe --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadFile.spec.ts @@ -0,0 +1,111 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Upload File'; +const uploadFilePath = './fixtures/mediaLibrary/'; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the upload file data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can publish content with the upload file data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +const uploadFiles = [ + {fileExtension: 'txt', fileName: 'File.txt'}, + {fileExtension: 'png', fileName: 'Umbraco.png'} +]; +for (const uploadFile of uploadFiles) { + test(`can upload a file with the ${uploadFile.fileExtension} extension in the content`, async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.uploadFile(uploadFilePath + uploadFile.fileName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(uploadFile.fileName)); + }); +} + +// TODO: Remove skip when the front-end is ready. Currently the uploaded file still displays after removing. +test.skip('can remove a text file in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const uploadFileName = 'File.txt'; + const mineType = 'text/plain'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithUploadFile(contentName, documentTypeId, dataTypeName, uploadFileName, mineType); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickRemoveFilesButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values).toEqual([]); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadVectorGraphics.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadVectorGraphics.spec.ts new file mode 100644 index 0000000000..16491d2502 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadVectorGraphics.spec.ts @@ -0,0 +1,106 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Upload Vector Graphics'; +const uploadVectorGraphicsPath = './fixtures/mediaLibrary/'; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the upload vector graphics data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can publish content with the upload vector graphics data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test(`can upload a file with the svg extension in the content`, async ({umbracoApi, umbracoUi}) => { + // Arrange + const vectorGraphicsName = 'VectorGraphics.svg'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.uploadFile(uploadVectorGraphicsPath + vectorGraphicsName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(vectorGraphicsName)); +}); + +// TODO: Remove skip when the front-end is ready. Currently the uploaded vector graphics file still displays after removing. +test.skip('can remove an svg file in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const uploadVectorGraphicsName = 'VectorGraphics.svg'; + const mineType = 'image/svg+xml'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithUploadFile(contentName, documentTypeId, dataTypeName, uploadVectorGraphicsName, mineType); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickRemoveFilesButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values).toEqual([]); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadVideo.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadVideo.spec.ts new file mode 100644 index 0000000000..e81dccf58b --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadVideo.spec.ts @@ -0,0 +1,112 @@ +import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Upload Video'; +const uploadVideoPath = './fixtures/mediaLibrary/'; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the upload video data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can publish content with the upload video data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +const uploadVideos = [ + {fileExtension: 'mp4', fileName: 'Video.mp4'}, + {fileExtension: 'webm', fileName: 'Webm.webm'}, + {fileExtension: 'ogv', fileName: 'Ogv.ogv'} +]; +for (const uploadVideo of uploadVideos) { + test(`can upload a video with the ${uploadVideo.fileExtension} extension in the content`, async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.uploadFile(uploadVideoPath + uploadVideo.fileName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName)); + expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(uploadVideo.fileName)); + }); +} + +// TODO: Remove skip when the front-end is ready. Currently the uploaded video still displays after removing. +test.skip('can remove a mp4 file in the content', async ({umbracoApi, umbracoUi}) => { + // Arrange + const uploadFileName = 'Video.mp4'; + const mineType = 'video/mp4'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithUploadFile(contentName, documentTypeId, dataTypeName, uploadFileName, mineType); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickRemoveFilesButton(); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values).toEqual([]); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CultureAndHostnames.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CultureAndHostnames.spec.ts index 4a09329f9b..369bbb7692 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CultureAndHostnames.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CultureAndHostnames.spec.ts @@ -40,6 +40,7 @@ test('can add a culture', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { await umbracoUi.content.clickSaveModalButton(); // Assert + await umbracoUi.waitForTimeout(2000); const domainsData = await umbracoApi.document.getDomains(contentId); expect(domainsData.defaultIsoCode).toEqual(isoCode); }); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/Block/BlockGridBlockAdvanced.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/Block/BlockGridBlockAdvanced.spec.ts new file mode 100644 index 0000000000..3f6c56c50c --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/Block/BlockGridBlockAdvanced.spec.ts @@ -0,0 +1,277 @@ +import {test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const blockGridEditorName = 'TestBlockGridEditor'; +const elementTypeName = 'BlockGridElement'; +const dataTypeName = 'Textstring'; +const groupName = 'testGroup'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockGridEditorName); + await umbracoUi.goToBackOffice(); + await umbracoUi.dataType.goToSettingsTreeItem('Data Types'); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockGridEditorName); +}); + +//TODO: It is not possible to add a view to a block +test.skip('can add a custom view to a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); +}); + +//TODO: It is not possible to add a view to a block +test.skip('can remove a custom view from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); +}); + +// TODO: Stylesheets are currently saved as arrays +test.skip('can remove a custom stylesheet from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const stylesheetName = 'TestStylesheet.css' + const stylesheetPath = '/wwwroot/css/' + stylesheetName; + await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName); + await umbracoApi.stylesheet.createDefaultStylesheet(stylesheetName); + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAdvancedSettingsInBlock(blockGridEditorName, contentElementTypeId, undefined, stylesheetPath, undefined, undefined, undefined); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainStylesheet(blockGridEditorName, contentElementTypeId, stylesheetPath)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); +}); + +test('can update overlay size in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const overlaySize = 'medium'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.updateBlockOverlaySize(overlaySize); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainOverlaySize(blockGridEditorName, contentElementTypeId, overlaySize)).toBeTruthy(); +}); + +test('can enable inline editing mode in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.clickInlineEditingMode(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainInlineEditing(blockGridEditorName, contentElementTypeId, true)).toBeTruthy(); +}); + +test('can disable inline editing mode in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAdvancedSettingsInBlock(blockGridEditorName, contentElementTypeId, undefined, undefined, 'small', true); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainInlineEditing(blockGridEditorName, contentElementTypeId, true)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.clickInlineEditingMode(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainInlineEditing(blockGridEditorName, contentElementTypeId, false)).toBeTruthy(); +}); + +test('can enable hide content editor in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.clickBlockGridHideContentEditorButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainHideContentEditor(blockGridEditorName, contentElementTypeId, true)).toBeTruthy(); +}); + +test('can disable hide content editor in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAdvancedSettingsInBlock(blockGridEditorName, contentElementTypeId, undefined, undefined, 'small', false, true); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainHideContentEditor(blockGridEditorName, contentElementTypeId, true)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.clickBlockGridHideContentEditorButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainHideContentEditor(blockGridEditorName, contentElementTypeId, false)).toBeTruthy(); +}); + +test('can add a background color to a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const backGroundColor = '#000000'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.selectBlockBackgroundColor(backGroundColor); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainBackgroundColor(blockGridEditorName, contentElementTypeId, backGroundColor)).toBeTruthy(); +}); + +test('can remove a background color to a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const backGroundColor = '#000000'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithCatalogueAppearanceInBlock(blockGridEditorName, contentElementTypeId, backGroundColor); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainBackgroundColor(blockGridEditorName, contentElementTypeId, backGroundColor)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.selectBlockBackgroundColor(''); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainBackgroundColor(blockGridEditorName, contentElementTypeId, '')).toBeTruthy(); +}); + +test('can add a icon color to a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const iconColor = '#000000'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.selectBlockIconColor(iconColor); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainIconColor(blockGridEditorName, contentElementTypeId, iconColor)).toBeTruthy(); +}); + +test('can remove a icon color from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const iconColor = '#000000'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithCatalogueAppearanceInBlock(blockGridEditorName, contentElementTypeId, '', iconColor); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainIconColor(blockGridEditorName, contentElementTypeId, iconColor)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.selectBlockIconColor(''); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainIconColor(blockGridEditorName, contentElementTypeId, '')).toBeTruthy(); +}); + +// TODO: Thumbnails are not showing correctly +test.skip('can add a thumbnail to a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const mediaName = 'TestMedia'; + await umbracoApi.media.ensureNameNotExists(mediaName); + await umbracoApi.media.createDefaultMediaWithImage(mediaName); + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + const mediaUrl = await umbracoApi.media.getMediaPathByName(mediaName); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); + await umbracoUi.dataType.chooseBlockThumbnailWithPath(mediaUrl.fileName, mediaUrl.mediaPath); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); +}); + +// TODO: Thumbnails are not showing correctly +test.skip('can remove a thumbnail from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAdvancedTab(); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/Block/BlockGridBlockAreas.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/Block/BlockGridBlockAreas.spec.ts new file mode 100644 index 0000000000..d33141220a --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/Block/BlockGridBlockAreas.spec.ts @@ -0,0 +1,356 @@ +import {test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const blockGridEditorName = 'TestBlockGridEditor'; +const elementTypeName = 'BlockGridElement'; +const dataTypeName = 'Textstring'; +const groupName = 'testGroup'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockGridEditorName); + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); + await umbracoUi.goToBackOffice(); + await umbracoUi.dataType.goToSettingsTreeItem('Data Types'); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockGridEditorName); + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); +}); + +test('can update grid columns for areas for a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const gridColumns = 6; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.enterGridColumnsForArea(gridColumns); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaGridColumns(blockGridEditorName, contentElementTypeId, gridColumns)).toBeTruthy(); +}); + +test('can add an area for a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.addAreaButton(); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithAlias(blockGridEditorName, contentElementTypeId)).toBeTruthy(); +}); + +// TODO: There are currently issues when trying to select the locator. +test.skip('can resize an area for a block', async ({umbracoApi, umbracoUi}) => { +// Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); +}); + +test('can update alias an area for a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const newAlias = 'NewAlias'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.goToAreaByAlias(areaAlias); + await umbracoUi.dataType.enterAreaAlias(newAlias); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithAlias(blockGridEditorName, contentElementTypeId, newAlias)).toBeTruthy(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaCount(blockGridEditorName, contentElementTypeId, 1)).toBeTruthy(); +}); + +test('can remove an area for a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithAlias(blockGridEditorName, contentElementTypeId, areaAlias)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.clickRemoveAreaByAlias(areaAlias); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithAlias(blockGridEditorName, contentElementTypeId, areaAlias)).toBeFalsy(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaCount(blockGridEditorName, contentElementTypeId, 0)).toBeTruthy(); +}); + +test('can add multiple areas for a block', async ({umbracoApi, umbracoUi}) => { +// Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaCount(blockGridEditorName, contentElementTypeId, 1)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.addAreaButton(); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithAlias(blockGridEditorName, contentElementTypeId)).toBeTruthy(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithAlias(blockGridEditorName, contentElementTypeId, areaAlias)).toBeTruthy(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaCount(blockGridEditorName, contentElementTypeId, 2)).toBeTruthy(); +}); + +test('can add create button label for an area in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const createButtonLabel = 'CreateButtonLabel'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.goToAreaByAlias(areaAlias); + await umbracoUi.dataType.enterCreateButtonLabelInArea(createButtonLabel); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithCreateButtonLabel(blockGridEditorName, contentElementTypeId, areaAlias, createButtonLabel)).toBeTruthy(); +}); + +test('can remove create button label for an area in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const createButtonLabel = 'CreateButtonLabel'; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias, createButtonLabel); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.goToAreaByAlias(areaAlias); + await umbracoUi.dataType.enterCreateButtonLabelInArea(''); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithCreateButtonLabel(blockGridEditorName, contentElementTypeId, areaAlias, '')).toBeTruthy(); +}); + +//TODO: Frontend issue. when value is inserted to the min or max, it is set as a string instead of number +test.skip('can add min allowed for an area in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const minAllowed = 3; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.goToAreaByAlias(areaAlias); + await umbracoUi.dataType.enterMinAllowedInArea(minAllowed); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithMinAllowed(blockGridEditorName, contentElementTypeId, areaAlias, minAllowed)).toBeTruthy(); +}); + +test('can remove min allowed for an area in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const minAllowed = 6; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias, null, undefined, undefined, minAllowed); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithMinAllowed(blockGridEditorName, contentElementTypeId, areaAlias, minAllowed)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.goToAreaByAlias(areaAlias); + await umbracoUi.dataType.enterMinAllowedInArea(undefined); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithMinAllowed(blockGridEditorName, contentElementTypeId, areaAlias, minAllowed)).toBeFalsy(); +}); + +//TODO: Frontend issue. when value is inserted to the min or max, it is set as a string instead of number +test.skip('can add add max allowed for an area in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const maxAllowed = 7; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.goToAreaByAlias(areaAlias); + await umbracoUi.dataType.enterMaxAllowedInArea(maxAllowed); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithMaxAllowed(blockGridEditorName, contentElementTypeId, areaAlias, maxAllowed)).toBeTruthy(); +}); + +test('can remove max allowed for an area in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const areaAlias = 'TestArea'; + const maxAllowed = 7; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias, null, undefined, undefined, undefined, maxAllowed); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithMaxAllowed(blockGridEditorName, contentElementTypeId, areaAlias, maxAllowed)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.goToAreaByAlias(areaAlias); + await umbracoUi.dataType.enterMaxAllowedInArea(undefined); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainAreaWithMaxAllowed(blockGridEditorName, contentElementTypeId, areaAlias, maxAllowed)).toBeFalsy(); +}); + +// TODO: There is no frontend validation for min and max values +test.skip('min can not be more than max an area in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const areaAlias = 'TestArea'; + const minAllowed = 6; + const maxAllowed = 7; + const newMinAllowed = 8; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias, null, undefined, undefined, minAllowed, maxAllowed); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.goToAreaByAlias(areaAlias); + await umbracoUi.dataType.enterMinAllowedInArea(newMinAllowed); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); +}); + +// TODO: It is currently not possible to add a specified allowance +test.skip('can add specified allowance for an area in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const areaAlias = 'TestArea'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithAnAreaInABlock(blockGridEditorName, contentElementTypeId, areaAlias); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.goToBlockAreasTab(); + await umbracoUi.dataType.goToAreaByAlias(areaAlias); + await umbracoUi.dataType.clickAddSpecifiedAllowanceButton(); + await umbracoUi.dataType.clickAreaSubmitButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); +}); + +// TODO: It is currently not possible to add a specified allowance +test.skip('can update specified allowance for an area in a block', async ({umbracoApi, umbracoUi}) => { + +}); + +// TODO: It is currently not possible to add a specified allowance +test.skip('can remove specified allowance for an area in a block', async ({umbracoApi, umbracoUi}) => { + +}); + +// TODO: It is currently not possible to add a specified allowance +test.skip('can add multiple specified allowances for an area in a block', async ({umbracoApi, umbracoUi}) => { + +}); + +// TODO: It is currently not possible to add a specified allowance +test.skip('can add specified allowance with min and max for an area in a block', async ({umbracoApi, umbracoUi}) => { +}); + +// TODO: It is currently not possible to add a specified allowance +test.skip('can remove min and max from specified allowance for an area in a block', async ({umbracoApi, umbracoUi}) => { + +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/Block/BlockGridBlockSettings.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/Block/BlockGridBlockSettings.spec.ts new file mode 100644 index 0000000000..bf0ca4905e --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/Block/BlockGridBlockSettings.spec.ts @@ -0,0 +1,253 @@ +import {test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const blockGridEditorName = 'TestBlockGridEditor'; +const elementTypeName = 'BlockGridElement'; +const dataTypeName = 'Textstring'; +const groupName = 'testGroup'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockGridEditorName); + await umbracoUi.goToBackOffice(); + await umbracoUi.dataType.goToSettingsTreeItem('Data Types'); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockGridEditorName); +}); + +test('can add a label to a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const labelText = 'TestLabel'; + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.enterBlockLabelText(labelText); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockGridEditorName, elementTypeId, labelText)).toBeTruthy(); +}); + +test('can remove a label from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const labelText = 'TestLabel'; + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithEditorAppearance(blockGridEditorName, elementTypeId, labelText); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockGridEditorName, elementTypeId, labelText)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.enterBlockLabelText(''); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockGridEditorName, elementTypeId, labelText)).toBeFalsy(); +}); + +test('can open content model in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.openBlockContentModel(); + + // Assert + await umbracoUi.dataType.isElementWorkspaceOpenInBlock(elementTypeName); +}); + +test('can add a settings model to a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + const secondElementName = 'SecondElementTest'; + const settingsElementTypeId = await umbracoApi.documentType.createDefaultElementType(secondElementName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.addBlockSettingsModel(secondElementName); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithSettingsTypeIds(blockGridEditorName, [settingsElementTypeId])).toBeTruthy(); +}); + +test('can remove a settings model from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + const secondElementName = 'SecondElementTest'; + const settingsElementTypeId = await umbracoApi.documentType.createDefaultElementType(secondElementName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithContentAndSettingsElementType(blockGridEditorName, contentElementTypeId, settingsElementTypeId); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithSettingsTypeIds(blockGridEditorName, [settingsElementTypeId])).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.removeBlockSettingsModel(); + await umbracoUi.dataType.clickConfirmRemoveButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithSettingsTypeIds(blockGridEditorName, [settingsElementTypeId])).toBeFalsy(); +}); + +test('can enable allow in root from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickAllowInRootForBlock(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockHaveAllowInRootEnabled(blockGridEditorName, contentElementTypeId)).toBeTruthy(); +}); + +test('can enable allow in areas from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickAllowInAreasForBlock(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockHaveAllowInAreasEnabled(blockGridEditorName, contentElementTypeId)).toBeTruthy(); +}); + +test('can add a column span to a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const columnSpan = [1]; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickShowResizeOptions(); + await umbracoUi.dataType.clickAvailableColumnSpans(columnSpan); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainColumnSpanOptions(blockGridEditorName, contentElementTypeId, columnSpan)).toBeTruthy(); +}); + +test('can add multiple column spans to a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const columnSpan = [1, 3, 6, 8]; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickShowResizeOptions(); + await umbracoUi.dataType.clickAvailableColumnSpans(columnSpan); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainColumnSpanOptions(blockGridEditorName, contentElementTypeId, columnSpan)).toBeTruthy(); +}); + +test('can remove a column span from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const columnSpan = [4]; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithSizeOptions(blockGridEditorName, contentElementTypeId, columnSpan[0]); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainColumnSpanOptions(blockGridEditorName, contentElementTypeId, columnSpan)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickAvailableColumnSpans(columnSpan); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainColumnSpanOptions(blockGridEditorName, contentElementTypeId, [])).toBeTruthy(); +}); + +test('can add min and max row span to a block', async ({umbracoApi, umbracoUi}) => { +// Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const rowSpanMin = 2; + const rowSpanMax = 6; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickShowResizeOptions(); + await umbracoUi.dataType.enterMinRowSpan(rowSpanMin); + await umbracoUi.dataType.enterMaxRowSpan(rowSpanMax); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainRowSpanOptions(blockGridEditorName, contentElementTypeId, rowSpanMin, rowSpanMax)).toBeTruthy(); +}); + +test('can remove min and max row spans from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const rowSpanMin = undefined; + const rowSpanMax = undefined; + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithSizeOptions(blockGridEditorName, contentElementTypeId, undefined, 2, 6); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainRowSpanOptions(blockGridEditorName, contentElementTypeId, 2, 6)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.enterMinRowSpan(rowSpanMin); + await umbracoUi.dataType.enterMaxRowSpan(rowSpanMax); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainRowSpanOptions(blockGridEditorName, contentElementTypeId, rowSpanMin, rowSpanMax)).toBeTruthy(); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/BlockGridEditor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/BlockGridEditor.spec.ts new file mode 100644 index 0000000000..fa717aa183 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/BlockGridEditor.spec.ts @@ -0,0 +1,399 @@ +import {test} from "@umbraco/playwright-testhelpers"; +import {expect} from "@playwright/test"; + +const blockGridEditorName = 'TestBlockGridEditor'; +const elementTypeName = 'BlockGridElement'; +const dataTypeName = 'Textstring'; +const groupName = 'testGroup'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockGridEditorName); + await umbracoUi.goToBackOffice(); + await umbracoUi.dataType.goToSettingsTreeItem('Data Types'); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockGridEditorName); +}); + +test('can create a block grid editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const blockGridLocatorName = 'Block Grid'; + const blockGridEditorAlias = 'Umbraco.BlockGrid'; + const blockGridEditorUiAlias = 'Umb.PropertyEditorUi.BlockGrid'; + + // Act + await umbracoUi.dataType.clickActionsMenuAtRoot(); + await umbracoUi.dataType.clickCreateButton(); + await umbracoUi.dataType.clickNewDataTypeThreeDotsButton(); + await umbracoUi.dataType.enterDataTypeName(blockGridEditorName); + await umbracoUi.dataType.clickSelectAPropertyEditorButton(); + await umbracoUi.dataType.selectAPropertyEditor(blockGridLocatorName); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesNameExist(blockGridEditorName)).toBeTruthy(); + const dataTypeData = await umbracoApi.dataType.getByName(blockGridEditorName); + expect(dataTypeData.editorAlias).toBe(blockGridEditorAlias); + expect(dataTypeData.editorUiAlias).toBe(blockGridEditorUiAlias); +}); + +test('can rename a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const wrongName = 'BlockListEditorTest'; + await umbracoApi.dataType.createEmptyBlockGrid(wrongName); + + // Act + await umbracoUi.dataType.goToDataType(wrongName); + await umbracoUi.dataType.enterDataTypeName(blockGridEditorName); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesNameExist(blockGridEditorName)).toBeTruthy(); + expect(await umbracoApi.dataType.doesNameExist(wrongName)).toBeFalsy(); +}); + +test('can delete a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const blockGridId = await umbracoApi.dataType.createEmptyBlockGrid(blockGridEditorName); + + // Act + await umbracoUi.dataType.clickRootFolderCaretButton(); + await umbracoUi.dataType.clickActionsMenuForDataType(blockGridEditorName); + await umbracoUi.dataType.clickDeleteExactButton(); + await umbracoUi.dataType.clickConfirmToDeleteButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesExist(blockGridId)).toBeFalsy(); + await umbracoUi.dataType.isTreeItemVisible(blockGridEditorName, false); +}); + +test('can add a block to a block grid editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createEmptyBlockGrid(blockGridEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.clickAddBlockButton(); + await umbracoUi.dataType.clickLabelWithName(elementTypeName); + await umbracoUi.dataType.clickChooseModalButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockGridEditorName, [elementTypeId])).toBeTruthy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); +}); + +test('can add multiple blocks to a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const secondElementName = 'SecondBlockGridElement'; + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + const secondElementTypeId = await umbracoApi.documentType.createDefaultElementType(secondElementName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.clickAddBlockButton(); + await umbracoUi.dataType.clickLabelWithName(secondElementName); + await umbracoUi.dataType.clickChooseModalButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockGridEditorName, [elementTypeId, secondElementTypeId])).toBeTruthy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); + await umbracoApi.documentType.ensureNameNotExists(secondElementTypeId); +}); + +test('can remove a block from a block grid editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.clickRemoveBlockWithName(elementTypeName); + await umbracoUi.dataType.clickConfirmRemoveButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockGridEditorName, [elementTypeId])).toBeFalsy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); +}); + +test('can add a block to a group in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createEmptyBlockGrid(blockGridEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.clickAddGroupButton(); + await umbracoUi.dataType.enterGroupName(groupName); + await umbracoUi.dataType.clickAddBlockButton(1); + await umbracoUi.dataType.clickLabelWithName(elementTypeName); + await umbracoUi.dataType.clickChooseModalButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockGridGroupContainCorrectBlocks(blockGridEditorName, groupName, [elementTypeId])).toBeTruthy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); +}); + +test('can add multiple blocks to a group in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const secondElementName = 'SecondBlockGridElement'; + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + const secondElementTypeId = await umbracoApi.documentType.createDefaultElementType(secondElementName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlockInAGroup(blockGridEditorName, elementTypeId, groupName); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.clickAddBlockButton(1); + await umbracoUi.dataType.clickLabelWithName(secondElementName); + await umbracoUi.dataType.clickChooseModalButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockGridGroupContainCorrectBlocks(blockGridEditorName, groupName, [elementTypeId, secondElementTypeId])).toBeTruthy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); + await umbracoApi.documentType.ensureNameNotExists(secondElementName); +}); + +test('can delete a block in a group from a block grid editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlockInAGroup(blockGridEditorName, elementTypeId, groupName); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockGridEditorName, [elementTypeId])).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.clickRemoveBlockWithName(elementTypeName); + await umbracoUi.dataType.clickConfirmRemoveButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockGridEditorName, [elementTypeId])).toBeFalsy(); +}); + +test('can move a block from a group to another group in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const secondGroupName = 'MoveToHereGroup'; + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlockInAGroup(blockGridEditorName, elementTypeId, groupName); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockGridEditorName, [elementTypeId])).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.clickAddGroupButton(); + await umbracoUi.dataType.enterGroupName(secondGroupName, 1); + // Drag and Drop + const dragFromLocator = await umbracoUi.dataType.getLinkWithName(elementTypeName); + const dragToLocator = await umbracoUi.dataType.getAddButtonInGroupWithName(secondGroupName); + await umbracoUi.dataType.dragAndDrop(dragFromLocator, dragToLocator, -10, 0, 10); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockGridGroupContainCorrectBlocks(blockGridEditorName, secondGroupName, [elementTypeId])).toBeTruthy(); + expect(await umbracoApi.dataType.doesBlockGridGroupContainCorrectBlocks(blockGridEditorName, groupName, [elementTypeId])).toBeFalsy(); +}); + +// TODO: When deleting a group should there not be a confirmation button? and should the block be moved another group when the group it was in is deleted? +test.skip('can delete a group in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockGridWithABlockInAGroup(blockGridEditorName, elementTypeId, groupName); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockGridEditorName, [elementTypeId])).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); +}); + +test('can add a min and max amount to a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const minAmount = 1; + const maxAmount = 2; + await umbracoApi.dataType.createEmptyBlockGrid(blockGridEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.enterMinAmount(minAmount.toString()); + await umbracoUi.dataType.enterMaxAmount(maxAmount.toString()); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const dataTypeData = await umbracoApi.dataType.getByName(blockGridEditorName); + expect(dataTypeData.values[0].value.min).toBe(minAmount); + expect(dataTypeData.values[0].value.max).toBe(maxAmount); +}); + +test('max can not be less than min in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const minAmount = 2; + const oldMaxAmount = 2; + const newMaxAmount = 1; + await umbracoApi.dataType.createBlockGridWithMinAndMaxAmount(blockGridEditorName, minAmount, oldMaxAmount); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.enterMaxAmount(newMaxAmount.toString()); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(false); + await umbracoUi.dataType.doesAmountContainErrorMessageWithText('The low value must not be exceed the high value'); + const dataTypeData = await umbracoApi.dataType.getByName(blockGridEditorName); + expect(dataTypeData.values[0].value.min).toBe(minAmount); + // The max value should not be updated + expect(dataTypeData.values[0].value.max).toBe(oldMaxAmount); +}); + +test('can enable live editing mode in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.createEmptyBlockGrid(blockGridEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + // This wait is currently necessary, sometimes there are issues when clicking the liveEdtingMode button + await umbracoUi.waitForTimeout(2000); + await umbracoUi.dataType.clickLiveEditingMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isLiveEditingModeEnabledForBlockEditor(blockGridEditorName, true)).toBeTruthy(); +}); + +test('can disable live editing mode in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.createBlockGridWithLiveEditingMode(blockGridEditorName, true); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + // This wait is currently necessary, sometimes there are issues when clicking the liveEditingMode button + await umbracoUi.waitForTimeout(2000); + await umbracoUi.dataType.clickLiveEditingMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isLiveEditingModeEnabledForBlockEditor(blockGridEditorName, false)).toBeTruthy(); +}); + +test('can add editor width in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const propertyEditorWidth = '100%'; + await umbracoApi.dataType.createEmptyBlockGrid(blockGridEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.enterEditorWidth(propertyEditorWidth); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockGridEditorName, propertyEditorWidth)).toBeTruthy(); +}); + +test('can remove editor width in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const propertyEditorWidth = '100%'; + await umbracoApi.dataType.createBlockGridWithPropertyEditorWidth(blockGridEditorName, propertyEditorWidth); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.enterEditorWidth(''); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockGridEditorName, propertyEditorWidth)).toBeFalsy(); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockGridEditorName, '')).toBeTruthy(); +}); + +test('can add a create button label in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const createButtonLabel = 'Create Block'; + await umbracoApi.dataType.createEmptyBlockGrid(blockGridEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.enterCreateButtonLabel(createButtonLabel); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockGridContainCreateButtonLabel(blockGridEditorName, createButtonLabel)).toBeTruthy(); +}); + +test('can remove a create button label in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const createButtonLabel = 'Create Block'; + await umbracoApi.dataType.createBlockGridWithCreateButtonLabel(blockGridEditorName, createButtonLabel); + expect(await umbracoApi.dataType.doesBlockGridContainCreateButtonLabel(blockGridEditorName, createButtonLabel)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.enterCreateButtonLabel(''); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockGridContainCreateButtonLabel(blockGridEditorName, createButtonLabel)).toBeFalsy(); + expect(await umbracoApi.dataType.doesBlockGridContainCreateButtonLabel(blockGridEditorName, '')).toBeTruthy(); +}); + +test('can update grid columns in a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const gridColumns = 3; + await umbracoApi.dataType.createEmptyBlockGrid(blockGridEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockGridEditorName); + await umbracoUi.dataType.enterGridColumns(gridColumns); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockGridContainGridColumns(blockGridEditorName, gridColumns)).toBeTruthy(); +}); + +// TODO: wait until fixed by frontend, currently you are able to insert multiple stylesheets +test.skip('can add a stylesheet a block grid editor', async ({umbracoApi, umbracoUi}) => { +}); + +test.skip('can remove a stylesheet in a block grid editor', async ({umbracoApi, umbracoUi}) => { +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListBlocks.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListBlocks.spec.ts new file mode 100644 index 0000000000..737cc34e9f --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListBlocks.spec.ts @@ -0,0 +1,419 @@ +import {test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const blockListEditorName = 'TestBlockListEditor'; +const elementTypeName = 'BlockListElement'; +const dataTypeName = 'Textstring'; +const groupName = 'testGroup'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockListEditorName); + await umbracoUi.goToBackOffice(); + await umbracoUi.dataType.goToSettingsTreeItem('Data Types'); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockListEditorName); +}); + +test('can add a label to a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const labelText = 'ThisIsALabel'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.enterBlockLabelText(labelText); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockListEditorName, elementTypeId, labelText)).toBeTruthy(); +}); + +test('can update a label for a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const labelText = 'ThisIsALabel'; + const newLabelText = 'ThisIsANewLabel'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithEditorAppearance(blockListEditorName, elementTypeId, labelText); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockListEditorName, elementTypeId, labelText)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.enterBlockLabelText(newLabelText); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockListEditorName, elementTypeId, newLabelText)).toBeTruthy(); +}); + +test('can remove a label from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const labelText = 'ThisIsALabel'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithEditorAppearance(blockListEditorName, elementTypeId, labelText); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockListEditorName, elementTypeId, labelText)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.enterBlockLabelText(""); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockListEditorName, elementTypeId, "")).toBeTruthy(); +}); + +test('can update overlay size for a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const overlaySize = 'medium'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithEditorAppearance(blockListEditorName, elementTypeId, ""); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.updateBlockOverlaySize(overlaySize); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].editorSize).toEqual(overlaySize); +}); + +test('can open content model in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.openBlockContentModel(); + + // Assert + await umbracoUi.dataType.isElementWorkspaceOpenInBlock(elementTypeName); +}); + +// TODO: Is this an issue? should you be able to remove the contentModel so you have none? +// There is currently frontend issues +test.skip('can remove a content model from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.removeBlockContentModel(); + await umbracoUi.dataType.clickConfirmRemoveButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const blockData = await umbracoApi.dataType.getByName(blockListEditorName); +}); + +test('can add a settings model to a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + const secondElementName = 'SecondElementTest'; + const settingsElementTypeId = await umbracoApi.documentType.createDefaultElementType(secondElementName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.addBlockSettingsModel(secondElementName); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithSettingsTypeIds(blockListEditorName, [settingsElementTypeId])).toBeTruthy(); +}); + +test('can remove a settings model from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + const secondElementName = 'SecondElementTest'; + const settingsElementTypeId = await umbracoApi.documentType.createDefaultElementType(secondElementName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithContentAndSettingsElementType(blockListEditorName, contentElementTypeId, settingsElementTypeId); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithSettingsTypeIds(blockListEditorName, [settingsElementTypeId])).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.removeBlockSettingsModel(); + await umbracoUi.dataType.clickConfirmRemoveButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithSettingsTypeIds(blockListEditorName, [settingsElementTypeId])).toBeFalsy(); +}); + +test('can add a background color to a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const backgroundColor = '#ff0000'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.selectBlockBackgroundColor(backgroundColor); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].backgroundColor).toEqual(backgroundColor); +}); + +test('can update a background color for a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const backgroundColor = '#ff0000'; + const newBackgroundColor = '#ff4444'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithCatalogueAppearance(blockListEditorName, contentElementTypeId, backgroundColor); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].backgroundColor).toEqual(backgroundColor); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.selectBlockBackgroundColor(newBackgroundColor); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].backgroundColor).toEqual(newBackgroundColor); +}); + +test('can delete a background color from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const backgroundColor = '#ff0000'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithCatalogueAppearance(blockListEditorName, contentElementTypeId, backgroundColor); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].backgroundColor).toEqual(backgroundColor); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.selectBlockBackgroundColor(''); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].backgroundColor).toEqual(''); +}); + +test('can add a icon color to a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const iconColor = '#ff0000'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.selectBlockIconColor(iconColor); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].iconColor).toEqual(iconColor); +}); + +test('can update a icon color for a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const iconColor = '#ff0000'; + const newIconColor = '#ff4444'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithCatalogueAppearance(blockListEditorName, contentElementTypeId, "", iconColor); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].iconColor).toEqual(iconColor); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.selectBlockIconColor(newIconColor); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].iconColor).toEqual(newIconColor); +}); + +test('can delete a icon color from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const iconColor = '#ff0000'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithCatalogueAppearance(blockListEditorName, contentElementTypeId, '', iconColor); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].iconColor).toEqual(iconColor); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.selectBlockIconColor(''); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].iconColor).toEqual(''); +}); + +// TODO: Currently it is not possible to update a stylesheet to a block +test.skip('can update a custom stylesheet for a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const stylesheetName = 'TestStylesheet.css'; + const stylesheetPath = '/wwwroot/css/' + stylesheetName; + const encodedStylesheetPath = await umbracoApi.stylesheet.encodeStylesheetPath(stylesheetPath); + const secondStylesheetName = 'SecondStylesheet.css'; + const secondStylesheetPath = '/wwwroot/css/' + secondStylesheetName; + const encodedSecondStylesheetPath = await umbracoApi.stylesheet.encodeStylesheetPath(secondStylesheetPath); + await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName); + await umbracoApi.stylesheet.ensureNameNotExists(secondStylesheetName); + await umbracoApi.stylesheet.createDefaultStylesheet(stylesheetName); + await umbracoApi.stylesheet.createDefaultStylesheet(secondStylesheetName); + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithCatalogueAppearance(blockListEditorName, contentElementTypeId, '', '', encodedStylesheetPath); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + // Removes first stylesheet + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].stylesheet[0]).toEqual(encodedSecondStylesheetPath); + + // Clean + await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName); + await umbracoApi.stylesheet.ensureNameNotExists(secondStylesheetName); +}); + +// TODO: Currently it is not possible to delete a stylesheet to a block +test.skip('can delete a custom stylesheet from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const stylesheetName = 'TestStylesheet.css'; + const stylesheetPath = '/wwwroot/css/' + stylesheetName; + const encodedStylesheetPath = await umbracoApi.stylesheet.encodeStylesheetPath(stylesheetPath); + await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName); + await umbracoApi.stylesheet.createDefaultStylesheet(stylesheetName); + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithCatalogueAppearance(blockListEditorName, contentElementTypeId, '', '', encodedStylesheetPath); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].stylesheet[0]).toEqual(encodedStylesheetPath); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickRemoveCustomStylesheetWithName(stylesheetName); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].stylesheet[0]).toBeUndefined(); + + // Clean + await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName); +}); + +test('can enable hide content editor in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickBlockListHideContentEditorButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].forceHideContentEditorInOverlay).toEqual(true); +}); + +test('can disable hide content editor in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithHideContentEditor(blockListEditorName, contentElementTypeId, true); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].forceHideContentEditorInOverlay).toEqual(true); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickBlockListHideContentEditorButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].forceHideContentEditorInOverlay).toEqual(false); +}); + +// TODO: Thumbnails are not showing in the UI +test.skip('can add a thumbnail to a block ', {tag: '@smoke'}, async ({page, umbracoApi, umbracoUi}) => { + +}); + +// TODO: Thumbnails are not showing in the UI +test.skip('can remove a thumbnail to a block ', {tag: '@smoke'}, async ({page, umbracoApi, umbracoUi}) => { + +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts new file mode 100644 index 0000000000..24142bca82 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts @@ -0,0 +1,311 @@ +import {test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const blockListEditorName = 'TestBlockListEditor'; +const elementTypeName = 'BlockListElement'; +const dataTypeName = 'Textstring'; +const groupName = 'testGroup'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockListEditorName); + await umbracoUi.goToBackOffice(); + await umbracoUi.dataType.goToSettingsTreeItem('Data Types'); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockListEditorName); +}); + +test('can create a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const blockListLocatorName = 'Block List'; + const blockListEditorAlias = 'Umbraco.BlockList'; + const blockListEditorUiAlias = 'Umb.PropertyEditorUi.BlockList'; + + // Act + await umbracoUi.dataType.clickActionsMenuAtRoot(); + await umbracoUi.dataType.clickCreateButton(); + await umbracoUi.dataType.clickNewDataTypeThreeDotsButton(); + await umbracoUi.dataType.enterDataTypeName(blockListEditorName); + await umbracoUi.dataType.clickSelectAPropertyEditorButton(); + await umbracoUi.dataType.selectAPropertyEditor(blockListLocatorName); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesNameExist(blockListEditorName)).toBeTruthy(); + const dataTypeData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(dataTypeData.editorAlias).toBe(blockListEditorAlias); + expect(dataTypeData.editorUiAlias).toBe(blockListEditorUiAlias); +}); + +test('can rename a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const wrongName = 'BlockGridEditorTest'; + await umbracoApi.dataType.createEmptyBlockListDataType(wrongName); + + // Act + await umbracoUi.dataType.goToDataType(wrongName); + await umbracoUi.dataType.enterDataTypeName(blockListEditorName); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesNameExist(blockListEditorName)).toBeTruthy(); + expect(await umbracoApi.dataType.doesNameExist(wrongName)).toBeFalsy(); +}); + +test('can delete a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const blockListId = await umbracoApi.dataType.createEmptyBlockListDataType(blockListEditorName); + + // Act + await umbracoUi.dataType.clickRootFolderCaretButton(); + await umbracoUi.dataType.clickActionsMenuForDataType(blockListEditorName); + await umbracoUi.dataType.clickDeleteExactButton(); + await umbracoUi.dataType.clickConfirmToDeleteButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesExist(blockListId)).toBeFalsy(); + await umbracoUi.dataType.isTreeItemVisible(blockListEditorName, false); +}); + +test('can add a block to a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, 'testGroup', dataTypeName, textStringData.id); + await umbracoApi.dataType.createEmptyBlockListDataType(blockListEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickAddBlockButton(); + await umbracoUi.dataType.clickLabelWithName(elementTypeName); + await umbracoUi.dataType.clickChooseButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockListEditorName, [elementTypeId])).toBeTruthy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); +}); + +test('can add multiple blocks to a block list editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const secondElementTypeName = 'SecondBlockListElement'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + const secondElementTypeId = await umbracoApi.documentType.createDefaultElementType(secondElementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickAddBlockButton(); + await umbracoUi.dataType.clickLabelWithName(secondElementTypeName); + await umbracoUi.dataType.clickChooseButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockListEditorName, [elementTypeId, secondElementTypeId])).toBeTruthy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); + await umbracoApi.documentType.ensureNameNotExists(secondElementTypeName); +}); + +test('can remove a block from a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickRemoveBlockWithName(elementTypeName); + await umbracoUi.dataType.clickConfirmRemoveButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockListEditorName, [elementTypeId])).toBeFalsy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); +}); + +test('can add a min and max amount to a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const minAmount = 1; + const maxAmount = 2; + await umbracoApi.dataType.createEmptyBlockListDataType(blockListEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.enterMinAmount(minAmount.toString()); + await umbracoUi.dataType.enterMaxAmount(maxAmount.toString()); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const dataTypeData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(dataTypeData.values[0].value.min).toBe(minAmount); + expect(dataTypeData.values[0].value.max).toBe(maxAmount); +}); + +test('max can not be less than min', async ({umbracoApi, umbracoUi}) => { + // Arrange + const minAmount = 2; + const oldMaxAmount = 2; + const newMaxAmount = 1; + await umbracoApi.dataType.createBlockListDataTypeWithMinAndMaxAmount(blockListEditorName, minAmount, oldMaxAmount); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.enterMaxAmount(newMaxAmount.toString()); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(false); + const dataTypeData = await umbracoApi.dataType.getByName(blockListEditorName); + await umbracoUi.dataType.doesAmountContainErrorMessageWithText('The low value must not be exceed the high value'); + expect(dataTypeData.values[0].value.min).toBe(minAmount); + // The max value should not be updated + expect(dataTypeData.values[0].value.max).toBe(oldMaxAmount); +}); + +test('can enable single block mode', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.createBlockListDataTypeWithSingleBlockMode(blockListEditorName, false); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickSingleBlockMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isSingleBlockModeEnabledForBlockList(blockListEditorName, true)).toBeTruthy(); +}); + +test('can disable single block mode', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.createBlockListDataTypeWithSingleBlockMode(blockListEditorName, true); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickSingleBlockMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isSingleBlockModeEnabledForBlockList(blockListEditorName, false)).toBeTruthy(); +}); + +test('can enable live editing mode', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.createBlockListDataTypeWithLiveEditingMode(blockListEditorName, false); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickLiveEditingMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isLiveEditingModeEnabledForBlockEditor(blockListEditorName, true)).toBeTruthy(); +}); + +test('can disable live editing mode', async ({umbracoApi, umbracoUi}) => { +// Arrange + await umbracoApi.dataType.createBlockListDataTypeWithLiveEditingMode(blockListEditorName, true); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickLiveEditingMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isLiveEditingModeEnabledForBlockEditor(blockListEditorName, false)).toBeTruthy(); +}); + +test('can enable inline editing mode', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.createBlockListDataTypeWithInlineEditingMode(blockListEditorName, false); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickInlineEditingMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isInlineEditingModeEnabledForBlockList(blockListEditorName, true)).toBeTruthy(); +}); + +test('can disable inline editing mode', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.createBlockListDataTypeWithInlineEditingMode(blockListEditorName, true); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickInlineEditingMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isInlineEditingModeEnabledForBlockList(blockListEditorName, false)).toBeTruthy(); +}); + +test('can add a property editor width', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const propertyWidth = '50%'; + await umbracoApi.dataType.createEmptyBlockListDataType(blockListEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.enterPropertyEditorWidth(propertyWidth); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockListEditorName, propertyWidth)).toBeTruthy(); +}); + +test('can update a property editor width', async ({umbracoApi, umbracoUi}) => { + // Arrange + const oldPropertyWidth = '50%'; + const newPropertyWidth = '100%'; + await umbracoApi.dataType.createBlockListDataTypeWithPropertyEditorWidth(blockListEditorName, oldPropertyWidth); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockListEditorName, oldPropertyWidth)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.enterPropertyEditorWidth(newPropertyWidth); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockListEditorName, newPropertyWidth)).toBeTruthy(); +}); + +test('can remove a property editor width', async ({umbracoApi, umbracoUi}) => { + // Arrange + const propertyWidth = '50%'; + await umbracoApi.dataType.createBlockListDataTypeWithPropertyEditorWidth(blockListEditorName, propertyWidth); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockListEditorName, propertyWidth)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.enterPropertyEditorWidth(''); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockListEditorName, '')).toBeTruthy(); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ContentPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ContentPicker.spec.ts index 71d67999a0..9cf26d793c 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ContentPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ContentPicker.spec.ts @@ -13,8 +13,8 @@ test.beforeEach(async ({umbracoUi, umbracoApi}) => { test.afterEach(async ({umbracoApi}) => { if (dataTypeDefaultData !== null) { - await umbracoApi.dataType.update(dataTypeDefaultData.id, dataTypeDefaultData); - } + await umbracoApi.dataType.update(dataTypeDefaultData.id, dataTypeDefaultData); + } }); test('can show open button', async ({umbracoApi, umbracoUi}) => { @@ -93,10 +93,6 @@ test('can remove start node', async ({umbracoApi, umbracoUi}) => { const contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); expect(await umbracoApi.document.doesExist(contentId)).toBeTruthy(); - const expectedDataTypeValues = { - "alias": "startNodeId", - "value": "" - } const removedDataTypeValues = [{ "alias": "startNodeId", "value": contentId @@ -114,7 +110,7 @@ test('can remove start node', async ({umbracoApi, umbracoUi}) => { // Assert dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); - expect(dataTypeData.values).toContainEqual(expectedDataTypeValues); + expect(dataTypeData.values).toEqual([]); // Clean await umbracoApi.document.ensureNameNotExists(contentName); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataType.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataType.spec.ts index 5fa452f211..cf3b771973 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataType.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataType.spec.ts @@ -2,8 +2,6 @@ import {expect} from "@playwright/test"; const dataTypeName = 'TestDataType'; -const editorAlias = 'Umbraco.ColorPicker'; -const propertyEditorName = 'Color Picker'; test.beforeEach(async ({umbracoApi, umbracoUi}) => { await umbracoApi.dataType.ensureNameNotExists(dataTypeName); @@ -22,7 +20,7 @@ test('can create a data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) await umbracoUi.dataType.clickNewDataTypeThreeDotsButton(); await umbracoUi.dataType.enterDataTypeName(dataTypeName); await umbracoUi.dataType.clickSelectAPropertyEditorButton(); - await umbracoUi.dataType.selectAPropertyEditor(propertyEditorName); + await umbracoUi.dataType.selectAPropertyEditor('Text Box'); await umbracoUi.dataType.clickSaveButton(); // Assert @@ -34,7 +32,7 @@ test('can rename a data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) // Arrange const wrongDataTypeName = 'Wrong Data Type'; await umbracoApi.dataType.ensureNameNotExists(wrongDataTypeName); - await umbracoApi.dataType.create(wrongDataTypeName, editorAlias, []); + await umbracoApi.dataType.createTextstringDataType(wrongDataTypeName); expect(await umbracoApi.dataType.doesNameExist(wrongDataTypeName)).toBeTruthy(); // Act @@ -49,7 +47,7 @@ test('can rename a data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) test('can delete a data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange - await umbracoApi.dataType.create(dataTypeName, editorAlias, []); + await umbracoApi.dataType.createTextstringDataType(dataTypeName); expect(await umbracoApi.dataType.doesNameExist(dataTypeName)).toBeTruthy(); // Act @@ -67,7 +65,7 @@ test('can change property editor in a data type', {tag: '@smoke'}, async ({umbra const updatedEditorAlias = 'Umbraco.TextArea'; const updatedEditorUiAlias = 'Umb.PropertyEditorUi.TextArea'; - await umbracoApi.dataType.create(dataTypeName, editorAlias, []); + await umbracoApi.dataType.createTextstringDataType(dataTypeName); expect(await umbracoApi.dataType.doesNameExist(dataTypeName)).toBeTruthy(); // Act @@ -98,16 +96,17 @@ test('cannot create a data type without selecting the property editor', {tag: '@ test('can change settings', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange + const maxCharsValue = 126; const expectedDataTypeValues = { - alias: "useLabel", - value: true + "alias": "maxChars", + "value": maxCharsValue }; - await umbracoApi.dataType.create(dataTypeName, editorAlias, []); + await umbracoApi.dataType.createTextstringDataType(dataTypeName); expect(await umbracoApi.dataType.doesNameExist(dataTypeName)).toBeTruthy(); // Act await umbracoUi.dataType.goToDataType(dataTypeName); - await umbracoUi.dataType.clickIncludeLabelsSlider(); + await umbracoUi.dataType.enterMaximumAllowedCharactersValue(maxCharsValue.toString()); await umbracoUi.dataType.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeFolder.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeFolder.spec.ts index 40a0c38802..2b5ce0857d 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeFolder.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeFolder.spec.ts @@ -4,6 +4,7 @@ import {expect} from "@playwright/test"; const dataTypeName = 'TestDataType'; const dataTypeFolderName = 'TestDataTypeFolder'; const editorAlias = 'Umbraco.ColorPicker'; +const editorUiAlias = 'Umb.PropertyEditorUi.ColorPicker'; const propertyEditorName = 'Color Picker'; test.beforeEach(async ({umbracoApi, umbracoUi}) => { @@ -35,9 +36,9 @@ test('can rename a data type folder', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.dataType.clickRootFolderCaretButton(); await umbracoUi.dataType.clickActionsMenuForDataType(wrongDataTypeFolderName); - await umbracoUi.dataType.clickRenameButton(); + await umbracoUi.dataType.clickRenameFolderButton(); await umbracoUi.dataType.enterFolderName(dataTypeFolderName); - await umbracoUi.dataType.clickUpdateFolderButton(); + await umbracoUi.dataType.clickConfirmRenameFolderButton(); // Assert expect(await umbracoApi.dataType.doesNameExist(dataTypeFolderName)).toBeTruthy(); @@ -125,10 +126,10 @@ test('cannot delete a non-empty data type folder', async ({umbracoApi, umbracoUi let dataTypeFolderId = await umbracoApi.dataType.createFolder(dataTypeFolderName); expect(await umbracoApi.dataType.doesNameExist(dataTypeFolderName)).toBeTruthy(); await umbracoApi.dataType.ensureNameNotExists(dataTypeName); - await umbracoApi.dataType.create(dataTypeName, editorAlias, [], dataTypeFolderId); + await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, [], dataTypeFolderId); expect(await umbracoApi.dataType.doesNameExist(dataTypeName)).toBeTruthy(); await umbracoUi.reloadPage(); - + // Act await umbracoUi.dataType.clickRootFolderCaretButton(); await umbracoUi.dataType.deleteDataTypeFolder(dataTypeFolderName); @@ -138,8 +139,8 @@ test('cannot delete a non-empty data type folder', async ({umbracoApi, umbracoUi expect(await umbracoApi.dataType.doesNameExist(dataTypeName)).toBeTruthy(); expect(await umbracoApi.dataType.doesNameExist(dataTypeFolderName)).toBeTruthy(); const dataTypeChildren = await umbracoApi.dataType.getChildren(dataTypeFolderId); - expect(dataTypeChildren[0].name).toBe(dataTypeName); - expect(dataTypeChildren[0].isFolder).toBeFalsy(); + expect(dataTypeChildren[0].name).toBe(dataTypeName); + expect(dataTypeChildren[0].isFolder).toBeFalsy(); // Clean await umbracoApi.dataType.ensureNameNotExists(dataTypeName); @@ -148,7 +149,7 @@ test('cannot delete a non-empty data type folder', async ({umbracoApi, umbracoUi test('can move a data type to a data type folder', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.dataType.ensureNameNotExists(dataTypeName); - const dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, []); + const dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias,[]); expect(await umbracoApi.dataType.doesNameExist(dataTypeName)).toBeTruthy(); await umbracoApi.dataType.ensureNameNotExists(dataTypeFolderName); const dataTypeFolderId = await umbracoApi.dataType.createFolder(dataTypeFolderName); @@ -171,7 +172,7 @@ test('can move a data type to a data type folder', async ({umbracoApi, umbracoUi test('can duplicate a data type to a data type folder', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.dataType.ensureNameNotExists(dataTypeName); - await umbracoApi.dataType.create(dataTypeName, editorAlias, []); + await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, []); expect(await umbracoApi.dataType.doesNameExist(dataTypeName)).toBeTruthy(); await umbracoApi.dataType.ensureNameNotExists(dataTypeFolderName); const dataTypeFolderId = await umbracoApi.dataType.createFolder(dataTypeFolderName); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ListView.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ListView.spec.ts index 58e84ec307..ef96da64f5 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ListView.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ListView.spec.ts @@ -103,7 +103,7 @@ for (const listViewType of listViewTypes) { "isSystem": 1, }] }]; - + // Remove all existing values and add a column displayed to remove dataTypeData = await umbracoApi.dataType.getByName(listViewType); dataTypeData.values = removedDataTypeValues; @@ -131,7 +131,7 @@ for (const listViewType of listViewTypes) { "icon": "icon-thumbnails-small", "collectionView": layoutsData, "isSystem": true, - "name": "Grid", + "name": "Grid", "selected": true }; @@ -160,11 +160,11 @@ for (const listViewType of listViewTypes) { "icon": "icon-thumbnails-small", "collectionView": layoutsData, "isSystem": true, - "name": "Grid", + "name": "Grid", "selected": true }] }]; - + // Remove all existing values and add a layout to remove dataTypeData = await umbracoApi.dataType.getByName(listViewType); dataTypeData.values = removedDataTypeValues; @@ -200,14 +200,14 @@ for (const listViewType of listViewTypes) { test('can update bulk action permission', async ({umbracoApi, umbracoUi}) => { // Arrange - const bulkActionPermissionValue = 'Allow bulk delete'; + const bulkActionPermissionValue = 'Allow bulk trash'; const expectedDataTypeValues = { "alias": "bulkActionPermissions", "value": { - "allowBulkCopy": false, - "allowBulkDelete": true, - "allowBulkMove": false, - "allowBulkPublish": false, + "allowBulkCopy": false, + "allowBulkDelete": true, + "allowBulkMove": false, + "allowBulkPublish": false, "allowBulkUnpublish": false } }; diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts index 63a2c4912a..9e7a82858e 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts @@ -15,15 +15,15 @@ for (const dataTypeName of dataTypes) { test.afterEach(async ({umbracoApi}) => { if (dataTypeDefaultData !== null) { - await umbracoApi.dataType.update(dataTypeDefaultData.id, dataTypeDefaultData); - } + await umbracoApi.dataType.update(dataTypeDefaultData.id, dataTypeDefaultData); + } }); test('can update pick multiple items', async ({umbracoApi, umbracoUi}) => { // Arrange const expectedDataTypeValues = { "alias": "multiple", - "value": dataTypeName === 'Media Picker' || dataTypeName === 'Image Media Picker' ? true : false, + "value": dataTypeName === 'Media Picker' || dataTypeName === 'Image Media Picker' ? true : false, }; // Act @@ -147,7 +147,7 @@ for (const dataTypeName of dataTypes) { dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); expect(dataTypeData.values).toContainEqual(expectedDataTypeValues); }); - + test('can remove accepted types', async ({umbracoApi, umbracoUi}) => { // Arrange const mediaTypeName = 'Audio'; @@ -156,15 +156,12 @@ for (const dataTypeName of dataTypes) { "alias": "filter", "value": mediaTypeData.id }]; - const expectedDataTypeValues = [{ - "alias": "filter", - "value": "" - }]; + const expectedDataTypeValues = []; // Remove all existing options and add an option to remove dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); dataTypeData.values = removedDataTypeValues; - await umbracoApi.dataType.update(dataTypeData.id, dataTypeData); + await umbracoApi.dataType.update(dataTypeData.id, dataTypeData); // Act await umbracoUi.dataType.goToDataType(dataTypeName); @@ -179,59 +176,57 @@ for (const dataTypeName of dataTypes) { test('can add start node', async ({umbracoApi, umbracoUi}) => { // Arrange // Create media - const mediaTypeName = 'Article'; const mediaName = 'TestStartNode'; await umbracoApi.media.ensureNameNotExists(mediaName); - const mediaId = await umbracoApi.media.createDefaultMedia(mediaName, mediaTypeName); + const mediaId = await umbracoApi.media.createDefaultMediaWithArticle(mediaName); expect(await umbracoApi.media.doesNameExist(mediaName)).toBeTruthy(); - + const expectedDataTypeValues = { "alias": "startNodeId", "value": mediaId }; - + // Act await umbracoUi.dataType.goToDataType(dataTypeName); await umbracoUi.dataType.clickChooseStartNodeButton(); await umbracoUi.dataType.addMediaStartNode(mediaName); await umbracoUi.dataType.clickSaveButton(); - + // Assert dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); expect(dataTypeData.values).toContainEqual(expectedDataTypeValues); - + // Clean await umbracoApi.media.ensureNameNotExists(mediaName); }); - + test('can remove start node', async ({umbracoApi, umbracoUi}) => { // Arrange // Create media - const mediaTypeName = 'Article'; const mediaName = 'TestStartNode'; await umbracoApi.media.ensureNameNotExists(mediaName); - const mediaId = await umbracoApi.media.createDefaultMedia(mediaName, mediaTypeName); + const mediaId = await umbracoApi.media.createDefaultMediaWithArticle(mediaName); expect(await umbracoApi.media.doesNameExist(mediaName)).toBeTruthy(); const removedDataTypeValues = [{ "alias": "startNodeId", "value": mediaId }]; - + // Remove all existing values and add a start node to remove dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); dataTypeData.values = removedDataTypeValues; await umbracoApi.dataType.update(dataTypeData.id, dataTypeData); - + // Act await umbracoUi.dataType.goToDataType(dataTypeName); await umbracoUi.dataType.removeMediaStartNode(mediaName); await umbracoUi.dataType.clickSaveButton(); - + // Assert dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); expect(dataTypeData.values).toEqual([]); - + // Clean await umbracoApi.media.ensureNameNotExists(mediaName); }); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Numeric.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Numeric.spec.ts index 5c57cc43c7..3183f972bb 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Numeric.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Numeric.spec.ts @@ -14,11 +14,12 @@ test.beforeEach(async ({umbracoUi, umbracoApi}) => { test.afterEach(async ({umbracoApi}) => { if (dataTypeDefaultData !== null) { - await umbracoApi.dataType.update(dataTypeDefaultData.id, dataTypeDefaultData); - } + await umbracoApi.dataType.update(dataTypeDefaultData.id, dataTypeDefaultData); + } }); -test('can update minimum value', async ({umbracoApi, umbracoUi}) => { +// TODO: unskip when fixed, currently flaky +test.skip('can update minimum value', async ({umbracoApi, umbracoUi}) => { // Arrange const minimumValue = -5; const expectedDataTypeValues = { diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/LogViewer/LogViewer.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/LogViewer/LogViewer.spec.ts index 535dd6d071..3173d4b930 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/LogViewer/LogViewer.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/LogViewer/LogViewer.spec.ts @@ -68,7 +68,8 @@ test('can create a saved search', {tag: '@smoke'}, async ({umbracoApi, umbracoUi await umbracoApi.logViewer.deleteSavedSearch(searchName); }); -test('can create a complex saved search', async ({umbracoApi, umbracoUi}) => { +// TODO: unskip, currently flaky +test.skip('can create a complex saved search', async ({umbracoApi, umbracoUi}) => { // Arrange const searchName = 'ComplexTest'; const search = "@Level='Fatal' or @Level='Error' or @Level='Warning'"; @@ -185,10 +186,10 @@ test('can use a saved search', async ({umbracoApi, umbracoUi}) => { const search = "StartsWith(@MessageTemplate, 'The token')"; await umbracoApi.logViewer.deleteSavedSearch(searchName); await umbracoApi.logViewer.createSavedSearch(searchName, search); - // Need to reload page to get the latest saved search list after creating new saved search by api - await umbracoUi.reloadPage(); + await umbracoUi.logViewer.goToSettingsTreeItem('Log Viewer'); // Act + await umbracoUi.waitForTimeout(4000); await umbracoUi.logViewer.clickSavedSearchByName(searchName); await umbracoUi.logViewer.waitUntilLoadingSpinnerInvisible(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts index d116a46e8f..8dcf57923a 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts @@ -35,11 +35,12 @@ test('can rename a media file', async ({umbracoApi, umbracoUi}) => { // Arrange const wrongMediaFileName = 'NotACorrectName'; await umbracoApi.media.ensureNameNotExists(wrongMediaFileName); - await umbracoApi.media.createDefaultMedia(wrongMediaFileName, mediaTypeName); + await umbracoApi.media.createDefaultMediaFile(wrongMediaFileName); await umbracoUi.media.goToSection(ConstantHelper.sections.media); // Arrange - await umbracoUi.media.clickLabelWithName(wrongMediaFileName, true); + await umbracoUi.waitForTimeout(1000); + await umbracoUi.media.clickLabelWithName(wrongMediaFileName, true, true); await umbracoUi.media.enterMediaItemName(mediaFileName); await umbracoUi.media.clickSaveButton(); @@ -49,11 +50,10 @@ test('can rename a media file', async ({umbracoApi, umbracoUi}) => { expect(await umbracoApi.media.doesNameExist(mediaFileName)).toBeTruthy(); }); -// The File type is skipped because there are frontend issues with the mediaType const mediaFileTypes = [ {fileName: 'Article', filePath: 'Article.pdf'}, {fileName: 'Audio', filePath: 'Audio.mp3'}, - // {fileName: 'File', filePath: 'File.txt'}, + {fileName: 'File', filePath: 'File.txt'}, {fileName: 'Image', filePath: 'Umbraco.png'}, {fileName: 'Vector Graphics (SVG)', filePath: 'VectorGraphics.svg'}, {fileName: 'Video', filePath: 'Video.mp4'} @@ -66,8 +66,8 @@ for (const mediaFileType of mediaFileTypes) { await umbracoUi.media.goToSection(ConstantHelper.sections.media); // Act - await umbracoUi.media.clickCreateMediaItemButton(); - await umbracoUi.media.clickMediaTypeWithNameButton(mediaFileType.fileName); + await umbracoUi.waitForTimeout(1000); + await umbracoUi.media.clickCreateMediaWithType(mediaFileType.fileName); await umbracoUi.media.enterMediaItemName(mediaFileType.fileName); await umbracoUi.media.uploadFile('./fixtures/mediaLibrary/' + mediaFileType.filePath); await umbracoUi.media.clickSaveButton(); @@ -82,9 +82,10 @@ for (const mediaFileType of mediaFileTypes) { }); } -test('can delete a media file', async ({umbracoApi, umbracoUi}) => { +// TODO: Currently there is no delete button for the media, only trash, is this correct? +test.skip('can delete a media file', async ({umbracoApi, umbracoUi}) => { // Arrange - await umbracoApi.media.createDefaultMedia(mediaFileName, mediaTypeName); + await umbracoApi.media.createDefaultMediaFile(mediaFileName); await umbracoUi.media.goToSection(ConstantHelper.sections.media); await umbracoApi.media.doesNameExist(mediaFileName); @@ -117,7 +118,8 @@ test('can create a folder', async ({umbracoApi, umbracoUi}) => { await umbracoApi.media.ensureNameNotExists(folderName); }); -test('can delete a folder', async ({umbracoApi, umbracoUi}) => { +// TODO: Currently there is no delete button for the media, only trash, is this correct? +test.skip('can delete a folder', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.media.ensureNameNotExists(folderName); await umbracoApi.media.createDefaultMediaFolder(folderName); @@ -144,7 +146,7 @@ test('can create a folder in a folder', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.media.clickActionsMenuForName(parentFolderName); await umbracoUi.media.clickCreateModalButton(); - await umbracoUi.media.clickExactLinkWithName('Folder'); + await umbracoUi.media.clickMediaTypeName('Folder'); await umbracoUi.media.enterMediaItemName(folderName); await umbracoUi.media.clickSaveButton(); @@ -162,8 +164,8 @@ test('can search for a media file', async ({umbracoApi, umbracoUi}) => { // Arrange const secondMediaFile = 'SecondMediaFile'; await umbracoApi.media.ensureNameNotExists(secondMediaFile); - await umbracoApi.media.createDefaultMedia(mediaFileName, mediaTypeName); - await umbracoApi.media.createDefaultMedia(secondMediaFile, mediaTypeName); + await umbracoApi.media.createDefaultMediaFile(mediaFileName); + await umbracoApi.media.createDefaultMediaFile(secondMediaFile); await umbracoUi.media.goToSection(ConstantHelper.sections.media); // Act @@ -180,7 +182,7 @@ test('can search for a media file', async ({umbracoApi, umbracoUi}) => { test('can trash a media item', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.media.emptyRecycleBin(); - await umbracoApi.media.createDefaultMedia(mediaFileName, mediaTypeName); + await umbracoApi.media.createDefaultMediaFile(mediaFileName); await umbracoUi.media.goToSection(ConstantHelper.sections.media); await umbracoApi.media.doesNameExist(mediaFileName); @@ -201,7 +203,7 @@ test('can trash a media item', async ({umbracoApi, umbracoUi}) => { test('can restore a media item from the recycle bin', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.media.emptyRecycleBin(); - await umbracoApi.media.createDefaultMedia(mediaFileName, mediaTypeName); + await umbracoApi.media.createDefaultMediaFile(mediaFileName); await umbracoApi.media.trashMediaItem(mediaFileName); await umbracoUi.media.goToSection(ConstantHelper.sections.media); @@ -219,11 +221,10 @@ test('can restore a media item from the recycle bin', async ({umbracoApi, umbrac await umbracoApi.media.emptyRecycleBin(); }); -// TODO: unskip when the frontend is ready. Currently you are unable to delete a media item from the recycle bin. You have to empty the recycle bin. -test.skip('can delete a media item from the recycle bin', async ({umbracoApi, umbracoUi}) => { +test('can delete a media item from the recycle bin', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.media.emptyRecycleBin(); - await umbracoApi.media.createDefaultMedia(mediaFileName, mediaTypeName); + await umbracoApi.media.createDefaultMediaFile(mediaFileName); await umbracoApi.media.trashMediaItem(mediaFileName); await umbracoUi.media.goToSection(ConstantHelper.sections.media); @@ -232,7 +233,7 @@ test.skip('can delete a media item from the recycle bin', async ({umbracoApi, um await umbracoUi.media.deleteMediaItem(mediaFileName); // Assert - await umbracoUi.media.isMediaItemVisibleInRecycleBin(mediaFileName); + await umbracoUi.media.isMediaItemVisibleInRecycleBin(mediaFileName, false); expect(await umbracoApi.media.doesNameExist(mediaFileName)).toBeFalsy(); expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(mediaFileName)).toBeFalsy(); }); @@ -240,7 +241,7 @@ test.skip('can delete a media item from the recycle bin', async ({umbracoApi, um test('can empty the recycle bin', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.media.emptyRecycleBin(); - await umbracoApi.media.createDefaultMedia(mediaFileName, mediaTypeName); + await umbracoApi.media.createDefaultMediaFile(mediaFileName); await umbracoApi.media.trashMediaItem(mediaFileName); await umbracoUi.media.goToSection(ConstantHelper.sections.media); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/MemberGroups.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/MemberGroups.spec.ts index 472b0ebc3a..80ce20aff8 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/MemberGroups.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/MemberGroups.spec.ts @@ -13,10 +13,10 @@ test.afterEach(async ({umbracoApi}) => { await umbracoApi.memberGroup.ensureNameNotExists(memberGroupName); }); -test('can create a member group', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { +test('can create a member group', {tag: '@smoke'}, async ({page, umbracoApi, umbracoUi}) => { // Act await umbracoUi.memberGroup.clickMemberGroupsTab(); - await umbracoUi.memberGroup.clickCreateButton(); + await umbracoUi.memberGroup.clickMemberGroupCreateButton(); await umbracoUi.memberGroup.enterMemberGroupName(memberGroupName); await umbracoUi.memberGroup.clickSaveButton(); @@ -30,7 +30,7 @@ test('can create a member group', {tag: '@smoke'}, async ({umbracoApi, umbracoUi test('cannot create member group with empty name', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.memberGroup.clickMemberGroupsTab(); - await umbracoUi.memberGroup.clickCreateButton(); + await umbracoUi.memberGroup.clickMemberGroupCreateButton(); await umbracoUi.memberGroup.clickSaveButton(); // Assert @@ -38,14 +38,15 @@ test('cannot create member group with empty name', async ({umbracoApi, umbracoUi expect(await umbracoApi.memberGroup.doesNameExist(memberGroupName)).toBeFalsy(); }); -test('cannot create member group with duplicate name', async ({umbracoApi, umbracoUi}) => { +// TODO: unskip, currently flaky +test.skip('cannot create member group with duplicate name', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.memberGroup.create(memberGroupName); expect(await umbracoApi.memberGroup.doesNameExist(memberGroupName)).toBeTruthy(); // Act await umbracoUi.memberGroup.clickMemberGroupsTab(); - await umbracoUi.memberGroup.clickCreateButton(); + await umbracoUi.memberGroup.clickCreateButton(true); await umbracoUi.memberGroup.enterMemberGroupName(memberGroupName); await umbracoUi.memberGroup.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/Members.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/Members.spec.ts index 4fde4683a6..f44a164a20 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/Members.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/Members.spec.ts @@ -106,7 +106,7 @@ test('can edit password', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.member.clickMemberLinkByName(memberName); await umbracoUi.member.clickChangePasswordButton(); - await umbracoUi.member.enterPassword(updatedPassword); + await umbracoUi.member.enterNewPassword(updatedPassword); await umbracoUi.member.enterConfirmNewPassword(updatedPassword); await umbracoUi.member.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Dashboard/ExamineManagement.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Dashboard/ExamineManagement.spec.ts index 3dd1798dc1..67c956efb7 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Dashboard/ExamineManagement.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Dashboard/ExamineManagement.spec.ts @@ -30,7 +30,7 @@ test('can view the details of an index', async ({umbracoApi, umbracoUi}) => { await umbracoUi.examineManagement.clickIndexByName(indexName); // Assert - await umbracoUi.examineManagement.doesIndexHaveHealthStatus(indexName, indexData.healthStatus); + await umbracoUi.examineManagement.doesIndexHaveHealthStatus(indexName, indexData.healthStatus.status); await umbracoUi.examineManagement.doesIndexPropertyHaveValue('documentCount', indexData.documentCount.toString()); await umbracoUi.examineManagement.doesIndexPropertyHaveValue('fieldCount', indexData.fieldCount.toString()); await umbracoUi.examineManagement.doesIndexPropertyHaveValue('CommitCount', indexData.providerProperties.CommitCount.toString()); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeDesignTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeDesignTab.spec.ts index eeb2e13200..eb87b7288a 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeDesignTab.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeDesignTab.spec.ts @@ -176,7 +176,8 @@ test('can create a document type with multiple groups', async ({umbracoApi, umbr expect(await umbracoApi.documentType.doesGroupContainCorrectPropertyEditor(documentTypeName, secondDataTypeName, secondDataType.id, secondGroupName)).toBeTruthy(); }); -test('can create a document type with multiple tabs', async ({umbracoApi, umbracoUi}) => { +// TODO: unskip, currently flaky +test.skip('can create a document type with multiple tabs', async ({umbracoApi, umbracoUi}) => { // Arrange const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); const secondDataTypeName = 'Image Media Picker'; @@ -229,7 +230,7 @@ test('can create a document type with a composition', {tag: '@smoke'}, async ({u await umbracoApi.documentType.ensureNameNotExists(compositionDocumentTypeName); }); -test('can remove a composition form a document type', async ({umbracoApi, umbracoUi}) => { +test('can remove a composition from a document type', async ({umbracoApi, umbracoUi}) => { // Arrange const compositionDocumentTypeName = 'CompositionDocumentType'; await umbracoApi.documentType.ensureNameNotExists(compositionDocumentTypeName); @@ -248,7 +249,7 @@ test('can remove a composition form a document type', async ({umbracoApi, umbrac // Assert await umbracoUi.documentType.isSuccessNotificationVisible(); - expect(await umbracoUi.documentType.doesGroupHaveValue(groupName)).toBeFalsy(); + await umbracoUi.documentType.isGroupVisible(groupName, false); const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); expect(documentTypeData.compositions).toEqual([]); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeFolder.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeFolder.spec.ts index b6a8660ad9..a96999f441 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeFolder.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeFolder.spec.ts @@ -49,6 +49,7 @@ test('can delete a document type folder', {tag: '@smoke'}, async ({umbracoApi, u test('can rename a document type folder', async ({umbracoApi, umbracoUi}) => { // Arrange const oldFolderName = 'OldName'; + await umbracoApi.documentType.ensureNameNotExists(oldFolderName); await umbracoApi.documentType.createFolder(oldFolderName); // Act @@ -57,7 +58,7 @@ test('can rename a document type folder', async ({umbracoApi, umbracoUi}) => { await umbracoUi.documentType.clickActionsMenuForName(oldFolderName); await umbracoUi.documentType.clickRenameFolderButton(); await umbracoUi.documentType.enterFolderName(documentFolderName); - await umbracoUi.documentType.clickUpdateFolderButton(); + await umbracoUi.documentType.clickConfirmRenameFolderButton(); // Assert await umbracoUi.documentType.isSuccessNotificationVisible(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeStructureTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeStructureTab.spec.ts index 2bf10c9157..1a82360e12 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeStructureTab.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeStructureTab.spec.ts @@ -76,7 +76,7 @@ test('can configure a collection for a document type', async ({umbracoApi, umbra // Arrange const collectionDataTypeName = 'TestCollection'; await umbracoApi.dataType.ensureNameNotExists(collectionDataTypeName); - const collectionDataTypeId = await umbracoApi.dataType.create(collectionDataTypeName, 'Umbraco.ListView', [], null, 'Umb.PropertyEditorUi.CollectionView'); + const collectionDataTypeId = await umbracoApi.dataType.create(collectionDataTypeName, 'Umbraco.ListView', 'Umb.PropertyEditorUi.Collection', []); await umbracoApi.documentType.createDefaultDocumentType(documentTypeName); await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeFolder.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeFolder.spec.ts index 7605e1eb78..83006c9e54 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeFolder.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeFolder.spec.ts @@ -44,6 +44,7 @@ test('can delete a media type folder', async ({umbracoApi, umbracoUi}) => { test('can rename a media type folder', async ({umbracoApi, umbracoUi}) => { // Arrange const oldFolderName = 'OldName'; + await umbracoApi.mediaType.ensureNameNotExists(oldFolderName); await umbracoApi.mediaType.createFolder(oldFolderName); // Act @@ -51,7 +52,7 @@ test('can rename a media type folder', async ({umbracoApi, umbracoUi}) => { await umbracoUi.mediaType.clickActionsMenuForName(oldFolderName); await umbracoUi.mediaType.clickRenameFolderButton(); await umbracoUi.mediaType.enterFolderName(mediaTypeFolderName); - await umbracoUi.mediaType.clickUpdateFolderButton(); + await umbracoUi.mediaType.clickConfirmRenameFolderButton(); // Assert await umbracoUi.mediaType.isSuccessNotificationVisible(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeStructureTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeStructureTab.spec.ts index fda9090443..3ed918f9cf 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeStructureTab.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeStructureTab.spec.ts @@ -98,7 +98,7 @@ test('can configure a collection for a media type', async ({umbracoApi, umbracoU // Arrange const collectionDataTypeName = 'TestCollection'; await umbracoApi.dataType.ensureNameNotExists(collectionDataTypeName); - const collectionDataTypeId = await umbracoApi.dataType.create(collectionDataTypeName, 'Umbraco.ListView', [], null, 'Umb.PropertyEditorUi.CollectionView'); + const collectionDataTypeId = await umbracoApi.dataType.create(collectionDataTypeName, 'Umbraco.ListView', 'Umb.PropertyEditorUi.Collection', []); await umbracoApi.mediaType.createDefaultMediaType(mediaTypeName); // Act diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts index 21e81f80aa..9b1ad8f8be 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts @@ -29,7 +29,7 @@ test('can create an empty partial view', {tag: '@smoke'}, async ({umbracoApi, um await umbracoUi.partialView.isSuccessNotificationVisible(); expect(await umbracoApi.partialView.doesNameExist(partialViewFileName)).toBeTruthy(); // Verify the new partial view is displayed under the Partial Views section - await umbracoUi.partialView.isPartialViewRootTreeItemVisibile(partialViewFileName); + await umbracoUi.partialView.isPartialViewRootTreeItemVisible(partialViewFileName); }); test('can create a partial view from snippet', async ({umbracoApi, umbracoUi}) => { @@ -62,7 +62,7 @@ test('can create a partial view from snippet', async ({umbracoApi, umbracoUi}) = } // Verify the new partial view is displayed under the Partial Views section - await umbracoUi.partialView.isPartialViewRootTreeItemVisibile(partialViewFileName); + await umbracoUi.partialView.isPartialViewRootTreeItemVisible(partialViewFileName); }); test('can rename a partial view', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { @@ -84,12 +84,13 @@ test('can rename a partial view', {tag: '@smoke'}, async ({umbracoApi, umbracoUi expect(await umbracoApi.partialView.doesNameExist(partialViewFileName)).toBeTruthy(); expect(await umbracoApi.partialView.doesNameExist(wrongPartialViewFileName)).toBeFalsy(); // Verify the old partial view is NOT displayed under the Partial Views section - await umbracoUi.partialView.isPartialViewRootTreeItemVisibile(wrongPartialViewFileName, false, false); + await umbracoUi.partialView.isPartialViewRootTreeItemVisible(wrongPartialViewFileName, false, false); // Verify the new partial view is displayed under the Partial Views section - await umbracoUi.partialView.isPartialViewRootTreeItemVisibile(partialViewFileName, true, false); + await umbracoUi.partialView.isPartialViewRootTreeItemVisible(partialViewFileName, true, false); }); -test('can update a partial view content', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { +// TODO: unskip when fixed +test.skip('can update a partial view content', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange const updatedPartialViewContent = defaultPartialViewContent + '@{\r\n' + @@ -248,7 +249,7 @@ test('can delete a partial view', {tag: '@smoke'}, async ({umbracoApi, umbracoUi expect(await umbracoApi.partialView.doesExist(partialViewFileName)).toBeFalsy(); // Verify the partial view is NOT displayed under the Partial Views section await umbracoUi.partialView.clickRootFolderCaretButton(); - await umbracoUi.partialView.isPartialViewRootTreeItemVisibile(partialViewFileName, false); + await umbracoUi.partialView.isPartialViewRootTreeItemVisible(partialViewFileName, false); }); // TODO: Remove skip when the front-end is ready. Currently the returned items count is not updated after choosing the root content. diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialViewFolder.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialViewFolder.spec.ts index add9815b5e..58741a3d7a 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialViewFolder.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialViewFolder.spec.ts @@ -27,7 +27,7 @@ test('can create a folder', async ({umbracoApi, umbracoUi}) => { expect(await umbracoApi.partialView.doesFolderExist(folderName)).toBeTruthy(); // Verify the partial view folder is displayed under the Partial Views section await umbracoUi.partialView.clickRootFolderCaretButton(); - await umbracoUi.partialView.isPartialViewRootTreeItemVisibile(folderName, true, false); + await umbracoUi.partialView.isPartialViewRootTreeItemVisible(folderName, true, false); }); test('can delete a folder', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { @@ -46,7 +46,7 @@ test('can delete a folder', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => expect(await umbracoApi.partialView.doesFolderExist(folderName)).toBeFalsy(); // Verify the partial view folder is NOT displayed under the Partial Views section await umbracoUi.partialView.clickRootFolderCaretButton(); - await umbracoUi.partialView.isPartialViewRootTreeItemVisibile(folderName, false, false); + await umbracoUi.partialView.isPartialViewRootTreeItemVisible(folderName, false, false); }); test('can create a partial view in a folder', async ({umbracoApi, umbracoUi}) => { @@ -69,9 +69,9 @@ test('can create a partial view in a folder', async ({umbracoApi, umbracoUi}) => const childrenData = await umbracoApi.partialView.getChildren(folderPath); expect(childrenData[0].name).toEqual(partialViewFileName); // Verify the partial view is displayed in the folder under the Partial Views section - await umbracoUi.partialView.isPartialViewRootTreeItemVisibile(partialViewFileName, false, false); + await umbracoUi.partialView.isPartialViewRootTreeItemVisible(partialViewFileName, false, false); await umbracoUi.partialView.clickCaretButtonForName(folderName); - await umbracoUi.partialView.isPartialViewRootTreeItemVisibile(partialViewFileName, true, false); + await umbracoUi.partialView.isPartialViewRootTreeItemVisible(partialViewFileName, true, false); // Clean await umbracoApi.partialView.ensureNameNotExists(partialViewFileName); @@ -119,7 +119,7 @@ test('can create a folder in a folder', async ({umbracoApi, umbracoUi}) => { const partialViewChildren = await umbracoApi.partialView.getChildren('/' + folderName); expect(partialViewChildren[0].path).toBe('/' + folderName + '/' + childFolderName); await umbracoUi.partialView.clickCaretButtonForName(folderName); - await umbracoUi.partialView.isPartialViewRootTreeItemVisibile(childFolderName, true, false); + await umbracoUi.partialView.isPartialViewRootTreeItemVisible(childFolderName, true, false); }); test('can create a folder in a folder in a folder', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { @@ -142,7 +142,7 @@ test('can create a folder in a folder in a folder', {tag: '@smoke'}, async ({umb const partialViewChildren = await umbracoApi.partialView.getChildren('/' + folderName + '/' + childFolderName); expect(partialViewChildren[0].path).toBe('/' + folderName + '/' + childFolderName + '/' + childOfChildFolderName); await umbracoUi.partialView.clickCaretButtonForName(childFolderName); - await umbracoUi.partialView.isPartialViewRootTreeItemVisibile(childOfChildFolderName, true, false); + await umbracoUi.partialView.isPartialViewRootTreeItemVisible(childOfChildFolderName, true, false); }); test('cannot delete non-empty folder', async ({umbracoApi, umbracoUi}) => { diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Template/Templates.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Template/Templates.spec.ts index f1304ed4d6..46fdf12ff5 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Template/Templates.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Template/Templates.spec.ts @@ -176,7 +176,6 @@ test.skip('can use query builder with Order By statement for a template', async test('can use query builder with Where statement for a template', async ({umbracoApi, umbracoUi}) => { // Arrange - //Arrange const propertyAliasValue = 'Name'; const operatorValue = 'is'; const constrainValue = 'Test Content'; @@ -202,8 +201,6 @@ test('can use query builder with Where statement for a template', async ({umbrac // Act await umbracoUi.template.goToTemplate(templateName); - // TODO: refactor later - await umbracoUi.waitForTimeout(1000); await umbracoUi.template.addQueryBuilderWithWhereStatement(propertyAliasValue, operatorValue, constrainValue); // Verify that the code is shown await umbracoUi.template.isQueryBuilderCodeShown(expectedCode); diff --git a/tests/Umbraco.Tests.Common/Builders/ContentBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ContentBuilder.cs index 05cc4b80dd..53c2f50f10 100644 --- a/tests/Umbraco.Tests.Common/Builders/ContentBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/ContentBuilder.cs @@ -53,6 +53,7 @@ public class ContentBuilder private int? _sortOrder; private bool? _trashed; private DateTime? _updateDate; + private bool? _blueprint; private int? _versionId; DateTime? IWithCreateDateBuilder.CreateDate @@ -145,6 +146,13 @@ public class ContentBuilder set => _updateDate = value; } + public ContentBuilder WithBlueprint(bool blueprint) + { + _blueprint = blueprint; + + return this; + } + public ContentBuilder WithVersionId(int versionId) { _versionId = versionId; @@ -217,6 +225,7 @@ public class ContentBuilder { var id = _id ?? 0; var versionId = _versionId ?? 0; + var blueprint = _blueprint ?? false; var key = _key ?? Guid.NewGuid(); var parentId = _parentId ?? -1; var parent = _parent; @@ -253,6 +262,7 @@ public class ContentBuilder content.Id = id; content.VersionId = versionId; + content.Blueprint = blueprint; content.Key = key; content.CreateDate = createDate; content.UpdateDate = updateDate; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UdiGetterExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UdiGetterExtensionsTests.cs index b5da0a4f2f..f5a5e79234 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UdiGetterExtensionsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UdiGetterExtensionsTests.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; @@ -13,15 +14,22 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Extensions; [TestFixture] public class UdiGetterExtensionsTests { - [TestCase("style.css", "umb://stylesheet/style.css")] - [TestCase("editor\\style.css", "umb://stylesheet/editor/style.css")] - [TestCase("editor/style.css", "umb://stylesheet/editor/style.css")] - public void GetUdiForStylesheet(string path, string expected) + [TestCase(Constants.ObjectTypes.Strings.DataType, "6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://data-type-container/6ad82c70685c4e049b36d81bd779d16f")] + [TestCase(Constants.ObjectTypes.Strings.DocumentType, "6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://document-type-container/6ad82c70685c4e049b36d81bd779d16f")] + [TestCase(Constants.ObjectTypes.Strings.MediaType, "6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://media-type-container/6ad82c70685c4e049b36d81bd779d16f")] + [TestCase(Constants.ObjectTypes.Strings.DocumentBlueprint, "6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://document-blueprint-container/6ad82c70685c4e049b36d81bd779d16f")] + public void GetUdiForEntityContainer(Guid containedObjectType, Guid key, string expected) { - var builder = new StylesheetBuilder(); - var stylesheet = builder.WithPath(path).Build(); - var result = stylesheet.GetUdi(); - Assert.AreEqual(expected, result.ToString()); + EntityContainer entity = new EntityContainer(containedObjectType) + { + Key = key + }; + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); } [TestCase("script.js", "umb://script/script.js")] @@ -29,10 +37,195 @@ public class UdiGetterExtensionsTests [TestCase("editor/script.js", "umb://script/editor/script.js")] public void GetUdiForScript(string path, string expected) { - var builder = new ScriptBuilder(); - var script = builder.WithPath(path).Build(); - var result = script.GetUdi(); - Assert.AreEqual(expected, result.ToString()); + Script entity = new ScriptBuilder() + .WithPath(path) + .Build(); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + } + + [TestCase("style.css", "umb://stylesheet/style.css")] + [TestCase("editor\\style.css", "umb://stylesheet/editor/style.css")] + [TestCase("editor/style.css", "umb://stylesheet/editor/style.css")] + public void GetUdiForStylesheet(string path, string expected) + { + Stylesheet entity = new StylesheetBuilder() + .WithPath(path) + .Build(); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + } + + [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", false, "umb://document/6ad82c70685c4e049b36d81bd779d16f")] + [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", true, "umb://document-blueprint/6ad82c70685c4e049b36d81bd779d16f")] + public void GetUdiForContent(Guid key, bool blueprint, string expected) + { + Content entity = new ContentBuilder() + .WithKey(key) + .WithBlueprint(blueprint) + .WithContentType(ContentTypeBuilder.CreateBasicContentType()) + .Build(); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IContentBase)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + } + + [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://media/6ad82c70685c4e049b36d81bd779d16f")] + public void GetUdiForMedia(Guid key, string expected) + { + Media entity = new MediaBuilder() + .WithKey(key) + .WithMediaType(MediaTypeBuilder.CreateImageMediaType()) + .Build(); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IContentBase)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + } + + [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://member/6ad82c70685c4e049b36d81bd779d16f")] + public void GetUdiForMember(Guid key, string expected) + { + Member entity = new MemberBuilder() + .WithKey(key) + .WithMemberType(MemberTypeBuilder.CreateSimpleMemberType()) + .Build(); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IContentBase)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + } + + [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://document-type/6ad82c70685c4e049b36d81bd779d16f")] + public void GetUdiForContentType(Guid key, string expected) + { + IContentType entity = new ContentTypeBuilder() + .WithKey(key) + .Build(); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IContentTypeComposition)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + } + + [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://media-type/6ad82c70685c4e049b36d81bd779d16f")] + public void GetUdiForMediaType(Guid key, string expected) + { + IMediaType entity = new MediaTypeBuilder() + .WithKey(key) + .Build(); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IContentTypeComposition)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + } + + [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://member-type/6ad82c70685c4e049b36d81bd779d16f")] + public void GetUdiForMemberType(Guid key, string expected) + { + IMemberType entity = new MemberTypeBuilder() + .WithKey(key) + .Build(); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IContentTypeComposition)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + } + + [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://data-type/6ad82c70685c4e049b36d81bd779d16f")] + public void GetUdiForDataType(Guid key, string expected) + { + DataType entity = new DataTypeBuilder() + .WithKey(key) + .Build(); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + } + + [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://dictionary-item/6ad82c70685c4e049b36d81bd779d16f")] + public void GetUdiForDictionaryItem(Guid key, string expected) + { + DictionaryItem entity = new DictionaryItemBuilder() + .WithKey(key) + .Build(); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + } + + [TestCase("en-US", "umb://language/en-US")] + [TestCase("en", "umb://language/en")] + public void GetUdiForLanguage(string isoCode, string expected) + { + ILanguage entity = new LanguageBuilder() + .WithCultureInfo(isoCode) + .Build(); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + } + + [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://member-group/6ad82c70685c4e049b36d81bd779d16f")] + public void GetUdiForMemberGroup(Guid key, string expected) + { + MemberGroup entity = new MemberGroupBuilder() + .WithKey(key) + .Build(); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); } [TestCase("test.cshtml", "umb://partial-view/test.cshtml")] @@ -40,26 +233,53 @@ public class UdiGetterExtensionsTests [TestCase("editor/test.cshtml", "umb://partial-view/editor/test.cshtml")] public void GetUdiForPartialView(string path, string expected) { - var builder = new PartialViewBuilder(); - var partialView = builder + IPartialView entity = new PartialViewBuilder() .WithPath(path) .Build(); - var result = partialView.GetUdi(); - Assert.AreEqual(expected, result.ToString()); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + } + + [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://relation-type/6ad82c70685c4e049b36d81bd779d16f")] + public void GetUdiForRelationType(Guid key, string expected) + { + IRelationTypeWithIsDependency entity = new RelationTypeBuilder() + .WithKey(key) + .Build(); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + } + + [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://template/6ad82c70685c4e049b36d81bd779d16f")] + public void GetUdiForTemplate(Guid key, string expected) + { + ITemplate entity = new TemplateBuilder() + .WithKey(key) + .Build(); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); } [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://webhook/6ad82c70685c4e049b36d81bd779d16f")] - public void GetUdiForWebhook(string key, string expected) + public void GetUdiForWebhook(Guid key, string expected) { - var builder = new WebhookBuilder(); - var webhook = builder - .WithKey(Guid.Parse(key)) + Webhook entity = new WebhookBuilder() + .WithKey(key) .Build(); - Udi result = webhook.GetUdi(); - Assert.AreEqual(expected, result.ToString()); + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); - result = ((IEntity)webhook).GetUdi(); - Assert.AreEqual(expected, result.ToString()); + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); } } diff --git a/umbraco.sln b/umbraco.sln index 59015768a8..87d95422f8 100644 --- a/umbraco.sln +++ b/umbraco.sln @@ -141,6 +141,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{20CE9C97 build\azure-pipelines.yml = build\azure-pipelines.yml build\nightly-build-trigger.yml = build\nightly-build-trigger.yml build\templates\backoffice-install.yml = build\templates\backoffice-install.yml + build\nightly-E2E-test-pipelines.yml = build\nightly-E2E-test-pipelines.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "csharp-docs", "csharp-docs", "{F2BF84D9-0A14-40AF-A0F3-B9BBBBC16A44}" @@ -184,7 +185,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Cms.Persistence.EFC EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Cms.Persistence.EFCore.SqlServer", "src\Umbraco.Cms.Persistence.EFCore.SqlServer\Umbraco.Cms.Persistence.EFCore.SqlServer.csproj", "{9276C3F0-0DC9-46C9-BF32-9EE79D92AE02}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Tests.AcceptanceTest.UmbracoProject", "tests\Umbraco.Tests.AcceptanceTest.UmbracoProject\Umbraco.Tests.AcceptanceTest.UmbracoProject.csproj", "{A13FF0A0-69FA-468A-9F79-565401D5C341}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Tests.AcceptanceTest.UmbracoProject", "tests\Umbraco.Tests.AcceptanceTest.UmbracoProject\Umbraco.Tests.AcceptanceTest.UmbracoProject.csproj", "{A13FF0A0-69FA-468A-9F79-565401D5C341}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Cms.Api.Management", "src\Umbraco.Cms.Api.Management\Umbraco.Cms.Api.Management.csproj", "{B4929148-3BD9-4589-829D-7C31FFCFF6D7}" EndProject @@ -272,11 +273,8 @@ Global {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {2A5027D9-F71D-4957-929E-F7A56AA1B95A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2A5027D9-F71D-4957-929E-F7A56AA1B95A}.Debug|Any CPU.Build.0 = Debug|Any CPU {2A5027D9-F71D-4957-929E-F7A56AA1B95A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2A5027D9-F71D-4957-929E-F7A56AA1B95A}.Release|Any CPU.Build.0 = Release|Any CPU {2A5027D9-F71D-4957-929E-F7A56AA1B95A}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU - {2A5027D9-F71D-4957-929E-F7A56AA1B95A}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {32F6A309-EC1E-4CDB-BA80-C804CF680BEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {32F6A309-EC1E-4CDB-BA80-C804CF680BEE}.Debug|Any CPU.Build.0 = Debug|Any CPU {32F6A309-EC1E-4CDB-BA80-C804CF680BEE}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -356,9 +354,7 @@ Global {9276C3F0-0DC9-46C9-BF32-9EE79D92AE02}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {9276C3F0-0DC9-46C9-BF32-9EE79D92AE02}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {A13FF0A0-69FA-468A-9F79-565401D5C341}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A13FF0A0-69FA-468A-9F79-565401D5C341}.Debug|Any CPU.Build.0 = Debug|Any CPU {A13FF0A0-69FA-468A-9F79-565401D5C341}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A13FF0A0-69FA-468A-9F79-565401D5C341}.Release|Any CPU.Build.0 = Release|Any CPU {A13FF0A0-69FA-468A-9F79-565401D5C341}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {A13FF0A0-69FA-468A-9F79-565401D5C341}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {B4929148-3BD9-4589-829D-7C31FFCFF6D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU diff --git a/version.json b/version.json index 2fb06038d0..a9105a4057 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "14.3.0-rc", + "version": "14.4.0-rc", "assemblyVersion": { "precision": "build" },