Merge remote-tracking branch 'origin/v8/dev' into netcore/dev
# Conflicts: # .gitignore # src/Umbraco.Web/Editors/ContentController.cs
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -163,6 +163,13 @@ build/hooks/
|
||||
build/temp/
|
||||
|
||||
|
||||
# Acceptance tests
|
||||
cypress.env.json
|
||||
/src/Umbraco.Tests.AcceptanceTest/cypress/support/chainable.ts
|
||||
/src/Umbraco.Tests.AcceptanceTest/package-lock.json
|
||||
/src/Umbraco.Tests.AcceptanceTest/cypress/videos/
|
||||
/src/Umbraco.Tests.AcceptanceTest/cypress/screenshots/
|
||||
|
||||
|
||||
# eof
|
||||
/src/Umbraco.Web.UI.Client/TESTS-*.xml
|
||||
|
||||
@@ -381,7 +381,7 @@
|
||||
{
|
||||
Write-Host "Restore NuGet"
|
||||
Write-Host "Logging to $($this.BuildTemp)\nuget.restore.log"
|
||||
$params = "-Source", $nugetsourceUmbraco
|
||||
$params = "-Source", $nugetsourceUmbraco
|
||||
&$this.BuildEnv.NuGet restore "$($this.SolutionRoot)\src\Umbraco.sln" > "$($this.BuildTemp)\nuget.restore.log" @params
|
||||
if (-not $?) { throw "Failed to restore NuGet packages." }
|
||||
})
|
||||
@@ -535,6 +535,7 @@
|
||||
# run
|
||||
if (-not $get)
|
||||
{
|
||||
cd
|
||||
if ($command.Length -eq 0)
|
||||
{
|
||||
$command = @( "Build" )
|
||||
|
||||
35
src/Umbraco.Tests.AcceptanceTest/README.md
Normal file
35
src/Umbraco.Tests.AcceptanceTest/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Umbraco Acceptance Tests
|
||||
|
||||
### Prerequisite
|
||||
- NodeJS 12+
|
||||
- A running installed Umbraco on url: [https://localhost:44331](https://localhost:44331) (Default development port)
|
||||
- Install using a `SqlServer`/`LocalDb` as the tests execute too fast for `SqlCE` to handle.
|
||||
- User information in `cypress.env.json` (See [Getting started](#getting-started))
|
||||
|
||||
### Getting started
|
||||
The tests is located in the project/folder named `Umbraco.Tests.AcceptanceTests`. Ensur to run `npm install` in that folder, or let your IDE do that.
|
||||
|
||||
Next, it is important you create a new file in the root of the project called `cypress.env.json`.
|
||||
This file is already added to `.gitignore` and can contain values that is different for each developer machine.
|
||||
|
||||
The file need the following content:
|
||||
```
|
||||
{
|
||||
"username": "<email for superadmin>",
|
||||
"password": "<password for superadmin>"
|
||||
}
|
||||
```
|
||||
Replace the `<email for superadmin>` and `<password for superadmin>` placeholders with correct info.
|
||||
|
||||
|
||||
|
||||
### Executing tests
|
||||
|
||||
There exists two npm scripts, that can be used to execute the test.
|
||||
|
||||
1. `npm run test`
|
||||
- Executes the tests headless.
|
||||
1. `npm run ui`
|
||||
- Executes the tests in a browser handled by a cypress application.
|
||||
|
||||
In case of errors it is recommended to use the UI to debug.
|
||||
10
src/Umbraco.Tests.AcceptanceTest/cypress.json
Normal file
10
src/Umbraco.Tests.AcceptanceTest/cypress.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"baseUrl": "https://localhost:44331",
|
||||
"viewportHeight": 1024,
|
||||
"viewportWidth": 1200,
|
||||
"env": {
|
||||
"username": "<insert username/email in cypress.env.json>",
|
||||
"password": "<insert password in cypress.env.json>"
|
||||
},
|
||||
"supportFile": "cypress/support/index.ts"
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Login', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/umbraco');
|
||||
});
|
||||
|
||||
it('Login with correct username and password', () => {
|
||||
const username = Cypress.env('username');
|
||||
const password = Cypress.env('password');
|
||||
//Precondition
|
||||
cy.get('.text-error').should('not.exist');
|
||||
|
||||
//Action
|
||||
cy.get('#umb-username').type(username);
|
||||
cy.get('#umb-passwordTwo').type(password);
|
||||
cy.get('[label-key="general_login"]').click();
|
||||
|
||||
//Assert
|
||||
cy.url().should('include', '/umbraco#/content')
|
||||
cy.get('#umb-username').should('not.exist');
|
||||
cy.get('#umb-passwordTwo').should('not.exist');
|
||||
});
|
||||
|
||||
|
||||
it('Login with correct username but wrong password', () => {
|
||||
const username = Cypress.env('username');
|
||||
const password = 'wrong';
|
||||
|
||||
//Precondition
|
||||
cy.get('.text-error').should('not.exist');
|
||||
|
||||
//Action
|
||||
cy.get('#umb-username').type(username);
|
||||
cy.get('#umb-passwordTwo').type(password);
|
||||
cy.get('[label-key="general_login"]').click();
|
||||
|
||||
//Assert
|
||||
cy.get('.text-error').should('exist');
|
||||
cy.get('#umb-username').should('exist');
|
||||
cy.get('#umb-passwordTwo').should('exist');
|
||||
});
|
||||
|
||||
it('Login with wrong username and wrong password', () => {
|
||||
const username = 'wrong-username';
|
||||
const password = 'wrong';
|
||||
|
||||
//Precondition
|
||||
cy.get('.text-error').should('not.exist');
|
||||
|
||||
//Action
|
||||
cy.get('#umb-username').type(username);
|
||||
cy.get('#umb-passwordTwo').type(password);
|
||||
cy.get('[label-key="general_login"]').click();
|
||||
|
||||
//Assert
|
||||
cy.get('.text-error').should('exist');
|
||||
cy.get('#umb-username').should('exist');
|
||||
cy.get('#umb-passwordTwo').should('exist');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/// <reference types="Cypress" />
|
||||
import {LabelDataTypeBuilder} from 'umbraco-cypress-testhelpers';
|
||||
context('Data Types', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create data type', () => {
|
||||
const name = "Test data type";
|
||||
|
||||
cy.umbracoEnsureDataTypeNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Data Types"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
cy.umbracoContextMenuAction("action-data-type").click();
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
|
||||
cy.get('select[name="selectedEditor"]').select('Label');
|
||||
|
||||
cy.get('.umb-property-editor select').select('Time');
|
||||
|
||||
//Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureDataTypeNameNotExists(name);
|
||||
});
|
||||
|
||||
it('Delete data type', () => {
|
||||
const name = "Test data type";
|
||||
cy.umbracoEnsureDataTypeNameNotExists(name);
|
||||
|
||||
const dataType = new LabelDataTypeBuilder()
|
||||
.withSaveNewAction()
|
||||
.withName(name)
|
||||
.build();
|
||||
|
||||
cy.saveDataType(dataType);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Data Types", name]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-delete").click();
|
||||
|
||||
cy.umbracoButtonByLabelKey("general_delete").click();
|
||||
|
||||
cy.contains(name).should('not.exist');
|
||||
|
||||
cy.umbracoEnsureDataTypeNameNotExists(name);
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/// <reference types="Cypress" />
|
||||
import { DocumentTypeBuilder } from 'umbraco-cypress-testhelpers';
|
||||
context('Document Types', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create document type', () => {
|
||||
const name = "Test document type";
|
||||
|
||||
cy.umbracoEnsureDocumentTypeNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Document Types"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
cy.umbracoContextMenuAction("action-documentType").click();
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
|
||||
cy.get('[data-element="group-add"]').click();
|
||||
|
||||
|
||||
cy.get('.umb-group-builder__group-title-input').type('Group name');
|
||||
cy.get('[data-element="property-add"]').click();
|
||||
cy.get('.editor-label').type('property name');
|
||||
cy.get('[data-element="editor-add"]').click();
|
||||
|
||||
//Search for textstring
|
||||
cy.get('.umb-search-field').type('Textstring');
|
||||
|
||||
// Choose first item
|
||||
cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click();
|
||||
|
||||
// Save property
|
||||
cy.get('.btn-success').last().click();
|
||||
|
||||
//Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureDocumentTypeNameNotExists(name);
|
||||
});
|
||||
|
||||
it('Delete document type', () => {
|
||||
const name = "Test document type";
|
||||
cy.umbracoEnsureDocumentTypeNameNotExists(name);
|
||||
|
||||
const dataType = new DocumentTypeBuilder()
|
||||
.withName(name)
|
||||
.build();
|
||||
|
||||
cy.saveDocumentType(dataType);
|
||||
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Document Types", name]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-delete").click();
|
||||
|
||||
cy.get('label.checkbox').click();
|
||||
cy.umbracoButtonByLabelKey("general_ok").click();
|
||||
|
||||
cy.contains(name).should('not.exist');
|
||||
|
||||
cy.umbracoEnsureDocumentTypeNameNotExists(name);
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Languages', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Add language', () => {
|
||||
const name = "Neddersass’sch (Nedderlannen)"; // Must be an option in the select box
|
||||
|
||||
cy.umbracoEnsureLanguageNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Languages"]).click();
|
||||
|
||||
cy.umbracoButtonByLabelKey("languages_addLanguage").click();
|
||||
|
||||
cy.get('select[name="newLang"]').select(name);
|
||||
|
||||
// //Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureLanguageNameNotExists(name);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Macros', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create macro', () => {
|
||||
const name = "Test macro";
|
||||
|
||||
cy.umbracoEnsureMacroNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Macros"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
|
||||
cy.get('form[name="createMacroForm"]').within(($form) => {
|
||||
cy.get('input[name="itemKey"]').type(name);
|
||||
cy.get(".btn-primary").click();
|
||||
});
|
||||
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.hash).to.include('#/settings/macros/edit/')
|
||||
});
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureMacroNameNotExists(name);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Media Types', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create media type', () => {
|
||||
const name = "Test media type";
|
||||
|
||||
cy.umbracoEnsureMediaTypeNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Media Types"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
cy.get('.menu-label').first().click(); // TODO: Fucked we cant use something like cy.umbracoContextMenuAction("action-mediaType").click();
|
||||
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
|
||||
cy.get('[data-element="group-add"]').click();
|
||||
|
||||
cy.get('.umb-group-builder__group-title-input').type('Group name');
|
||||
cy.get('[data-element="property-add"]').click();
|
||||
cy.get('.editor-label').type('property name');
|
||||
cy.get('[data-element="editor-add"]').click();
|
||||
|
||||
//Search for textstring
|
||||
cy.get('.umb-search-field').type('Textstring');
|
||||
|
||||
// Choose first item
|
||||
cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click();
|
||||
|
||||
// Save property
|
||||
cy.get('.btn-success').last().click();
|
||||
|
||||
//Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureMediaTypeNameNotExists(name);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Member Types', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create member type', () => {
|
||||
const name = "Test member type";
|
||||
|
||||
cy.umbracoEnsureMemberTypeNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Member Types"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
|
||||
cy.get('[data-element="group-add"]').click();
|
||||
|
||||
cy.get('.umb-group-builder__group-title-input').type('Group name');
|
||||
cy.get('[data-element="property-add"]').click();
|
||||
cy.get('.editor-label').type('property name');
|
||||
cy.get('[data-element="editor-add"]').click();
|
||||
|
||||
//Search for textstring
|
||||
cy.get('.umb-search-field').type('Textstring');
|
||||
|
||||
// Choose first item
|
||||
cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click();
|
||||
|
||||
// Save property
|
||||
cy.get('.btn-success').last().click();
|
||||
|
||||
//Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureMemberTypeNameNotExists(name);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Partial View Macro Files', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create new partial view macro', () => {
|
||||
const name = "TestPartialViewMacro";
|
||||
const fileName = name + ".cshtml";
|
||||
|
||||
cy.umbracoEnsurePartialViewMacroFileNameNotExists(fileName);
|
||||
cy.umbracoEnsureMacroNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Partial View Macro Files"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
cy.get('.menu-label').first().click(); // TODO: Fucked we cant use something like cy.umbracoContextMenuAction("action-label").click();
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
//Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsurePartialViewMacroFileNameNotExists(fileName);
|
||||
cy.umbracoEnsureMacroNameNotExists(name);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Partial Views', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create new empty partial view', () => {
|
||||
const name = "TestPartialView";
|
||||
const fileName = name + ".cshtml";
|
||||
|
||||
cy.umbracoEnsurePartialViewNameNotExists(fileName);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Partial Views"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
cy.get('.menu-label').first().click(); // TODO: Fucked we cant use something like cy.umbracoContextMenuAction("action-mediaType").click();
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
//Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsurePartialViewNameNotExists(fileName);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Relation Types', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create relation type', () => {
|
||||
const name = "Test relation type";
|
||||
|
||||
cy.umbracoEnsureRelationTypeNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Relation Types"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
|
||||
cy.get('form[name="createRelationTypeForm"]').within(($form) => {
|
||||
cy.get('input[name="relationTypeName"]').type(name);
|
||||
|
||||
cy.get('[name="relationType-direction"] input').first().click({force:true});
|
||||
|
||||
cy.get('select[name="relationType-parent"]').select('Document');
|
||||
|
||||
cy.get('select[name="relationType-child"]').select('Media');
|
||||
|
||||
cy.get(".btn-primary").click();
|
||||
});
|
||||
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.hash).to.include('#/settings/relationTypes/edit/')
|
||||
})
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureRelationTypeNameNotExists(name);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Scripts', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create new JavaScript file', () => {
|
||||
const name = "TestScript";
|
||||
const fileName = name + ".js";
|
||||
|
||||
cy.umbracoEnsureScriptNameNotExists(fileName);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Stylesheets"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
cy.get('.menu-label').first().click(); // TODO: Fucked we cant use something like cy.umbracoContextMenuAction("action-mediaType").click();
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
//Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureScriptNameNotExists(fileName);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Stylesheets', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create new style sheet file', () => {
|
||||
const name = "TestStylesheet";
|
||||
const fileName = name + ".css";
|
||||
|
||||
cy.umbracoEnsureStylesheetNameNotExists(fileName);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Stylesheets"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
cy.get('.menu-label').first().click(); // TODO: Fucked we cant use something like cy.umbracoContextMenuAction("action-mediaType").click();
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
//Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureStylesheetNameNotExists(fileName);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Templates', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create template', () => {
|
||||
const name = "Test template";
|
||||
|
||||
cy.umbracoEnsureTemplateNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('settings');
|
||||
cy.get('li .umb-tree-root:contains("Settings")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("settings", ["Templates"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
//Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureTemplateNameNotExists(name);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
|
||||
context('User Groups', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create user group', () => {
|
||||
const name = "Test Group";
|
||||
|
||||
cy.umbracoEnsureUserGroupNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('users');
|
||||
cy.get('[data-element="sub-view-userGroups"]').click();
|
||||
|
||||
cy.umbracoButtonByLabelKey("actions_createGroup").click();
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
// Assign sections
|
||||
cy.get('.umb-box:nth-child(1) .umb-property:nth-child(1) localize').click();
|
||||
cy.get('.umb-tree-item span').click({multiple:true});
|
||||
cy.get('.btn-success').last().click();
|
||||
|
||||
// Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureUserGroupNameNotExists(name);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Users', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create user', () => {
|
||||
const name = "Alice Bobson";
|
||||
const email = "alice-bobson@acceptancetest.umbraco";
|
||||
|
||||
cy.umbracoEnsureUserEmailNotExists(email);
|
||||
cy.umbracoSection('users');
|
||||
cy.umbracoButtonByLabelKey("user_createUser").click();
|
||||
|
||||
|
||||
cy.get('input[name="name"]').type(name);
|
||||
cy.get('input[name="email"]').type(email);
|
||||
|
||||
cy.get('.umb-node-preview-add').click();
|
||||
cy.get('.umb-user-group-picker-list-item:nth-child(1) > .umb-user-group-picker__action').click();
|
||||
cy.get('.umb-user-group-picker-list-item:nth-child(2) > .umb-user-group-picker__action').click();
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
cy.get('.umb-button > .btn > .umb-button__content').click();
|
||||
|
||||
|
||||
cy.umbracoButtonByLabelKey("user_goToProfile").should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureUserEmailNotExists(email);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
21
src/Umbraco.Tests.AcceptanceTest/cypress/plugins/index.js
Normal file
21
src/Umbraco.Tests.AcceptanceTest/cypress/plugins/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
30
src/Umbraco.Tests.AcceptanceTest/cypress/support/commands.js
Normal file
30
src/Umbraco.Tests.AcceptanceTest/cypress/support/commands.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add("login", (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
|
||||
import {Command} from 'umbraco-cypress-testhelpers';
|
||||
import {Chainable} from './chainable';
|
||||
new Chainable();
|
||||
new Command().registerCypressCommands();
|
||||
20
src/Umbraco.Tests.AcceptanceTest/cypress/support/index.ts
Normal file
20
src/Umbraco.Tests.AcceptanceTest/cypress/support/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
7
src/Umbraco.Tests.AcceptanceTest/cypress/tsconfig.json
Normal file
7
src/Umbraco.Tests.AcceptanceTest/cypress/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": [
|
||||
"../node_modules/cypress",
|
||||
"*/*.ts"
|
||||
]
|
||||
}
|
||||
5
src/Umbraco.Tests.AcceptanceTest/cypress/typings.d.ts
vendored
Normal file
5
src/Umbraco.Tests.AcceptanceTest/cypress/typings.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
// type definitions for Cypress object "cy"
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
// type definitions for custom commands like "createDefaultTodos"
|
||||
// <reference types="support" />
|
||||
15
src/Umbraco.Tests.AcceptanceTest/package.json
Normal file
15
src/Umbraco.Tests.AcceptanceTest/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"scripts": {
|
||||
"test": "npx cypress run",
|
||||
"ui": "npx cypress open"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.2",
|
||||
"ncp": "^2.0.0",
|
||||
"cypress": "^4.5.0",
|
||||
"umbraco-cypress-testhelpers": "1.0.0-beta-38"
|
||||
},
|
||||
"dependencies": {
|
||||
"typescript": "^3.9.2"
|
||||
}
|
||||
}
|
||||
36
src/Umbraco.Tests.AcceptanceTest/tsconfig.json
Normal file
36
src/Umbraco.Tests.AcceptanceTest/tsconfig.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./lib",
|
||||
"sourceMap": false,
|
||||
"declaration": true,
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"esModuleInterop": true,
|
||||
"importHelpers": true,
|
||||
"target": "es5",
|
||||
|
||||
"types": [
|
||||
"cypress"
|
||||
],
|
||||
"lib": [
|
||||
"es5",
|
||||
"dom"
|
||||
],
|
||||
"plugins": [
|
||||
{
|
||||
"name": "typescript-tslint-plugin",
|
||||
"alwaysShowRuleFailuresAsWarnings": false,
|
||||
"ignoreDefinitionFiles": true,
|
||||
"configFile": "tslint.json",
|
||||
"suppressWhileTypeErrorsPresent": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
3
src/Umbraco.Tests.AcceptanceTest/tslint.json
Normal file
3
src/Umbraco.Tests.AcceptanceTest/tslint.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["tslint:recommended", "tslint-config-prettier"]
|
||||
}
|
||||
@@ -509,7 +509,7 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
var display = JsonConvert.DeserializeObject<ContentItemDisplay>(response.Item2);
|
||||
Assert.AreEqual(2, display.Errors.Count());
|
||||
Assert.IsTrue(display.Errors.ContainsKey("Variants[0].Name"));
|
||||
Assert.IsTrue(display.Errors.ContainsKey("_content_variant_en-US_"));
|
||||
Assert.IsTrue(display.Errors.ContainsKey("_content_variant_en-US_null_"));
|
||||
}
|
||||
|
||||
// TODO: There are SOOOOO many more tests we should write - a lot of them to do with validation
|
||||
|
||||
@@ -22,19 +22,19 @@ namespace Umbraco.Tests.Web
|
||||
ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", null); //invariant property
|
||||
ms.AddPropertyError(new ValidationResult("title missing"), "title", "en-US"); //variant property
|
||||
|
||||
var result = ms.GetCulturesWithErrors(localizationService.Object, "en-US");
|
||||
var result = ms.GetVariantsWithErrors("en-US");
|
||||
|
||||
//even though there are 2 errors, they are both for en-US since that is the default language and one of the errors is for an invariant property
|
||||
Assert.AreEqual(1, result.Count);
|
||||
Assert.AreEqual("en-US", result[0]);
|
||||
Assert.AreEqual("en-US", result[0].culture);
|
||||
|
||||
ms = new ModelStateDictionary();
|
||||
ms.AddCultureValidationError("en-US", "generic culture error");
|
||||
ms.AddVariantValidationError("en-US", null, "generic culture error");
|
||||
|
||||
result = ms.GetCulturesWithErrors(localizationService.Object, "en-US");
|
||||
result = ms.GetVariantsWithErrors("en-US");
|
||||
|
||||
Assert.AreEqual(1, result.Count);
|
||||
Assert.AreEqual("en-US", result[0]);
|
||||
Assert.AreEqual("en-US", result[0].culture);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -47,11 +47,11 @@ namespace Umbraco.Tests.Web
|
||||
ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", null); //invariant property
|
||||
ms.AddPropertyError(new ValidationResult("title missing"), "title", "en-US"); //variant property
|
||||
|
||||
var result = ms.GetCulturesWithPropertyErrors(localizationService.Object, "en-US");
|
||||
var result = ms.GetVariantsWithPropertyErrors("en-US");
|
||||
|
||||
//even though there are 2 errors, they are both for en-US since that is the default language and one of the errors is for an invariant property
|
||||
Assert.AreEqual(1, result.Count);
|
||||
Assert.AreEqual("en-US", result[0]);
|
||||
Assert.AreEqual("en-US", result[0].culture);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
16217
src/Umbraco.Web.UI.Client/package-lock.json
generated
16217
src/Umbraco.Web.UI.Client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -210,7 +210,7 @@
|
||||
function appendRuntimeData() {
|
||||
$scope.content.variants.forEach((variant) => {
|
||||
variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant);
|
||||
variant.htmlId = "_content_variant_" + variant.compositeId;
|
||||
variant.htmlId = "_content_variant_" + variant.compositeId + "_";
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -328,9 +328,11 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
|
||||
*
|
||||
* @description
|
||||
* Returns a id for the variant that is unique between all variants on the content
|
||||
* Note "invariant" is used for the invariant culture,
|
||||
* "null" is used for the NULL segment
|
||||
*/
|
||||
buildCompositeVariantId: function (variant) {
|
||||
return (variant.language ? variant.language.culture : "invariant") + "_" + (variant.segment ? variant.segment : "");
|
||||
return (variant.language ? variant.language.culture : "invariant") + "_" + (variant.segment ? variant.segment : "null");
|
||||
},
|
||||
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ using Umbraco.Web.Models.ContentEditing;
|
||||
using Umbraco.Web.Models.Mapping;
|
||||
using Umbraco.Web.Mvc;
|
||||
using Umbraco.Web.Routing;
|
||||
using Umbraco.Core.Collections;
|
||||
using Umbraco.Web.WebApi;
|
||||
using Umbraco.Web.WebApi.Filters;
|
||||
using Constants = Umbraco.Core.Constants;
|
||||
@@ -720,12 +721,19 @@ namespace Umbraco.Web.Editors
|
||||
{
|
||||
if (variantCount > 1)
|
||||
{
|
||||
var cultureErrors = ModelState.GetCulturesWithErrors(Services.LocalizationService, cultureForInvariantErrors);
|
||||
foreach (var c in contentItem.Variants.Where(x => x.Save && !cultureErrors.Contains(x.Culture)).Select(x => x.Culture).ToArray())
|
||||
var variantErrors = ModelState.GetVariantsWithErrors(cultureForInvariantErrors);
|
||||
|
||||
var validVariants = contentItem.Variants
|
||||
.Where(x => x.Save && !variantErrors.Contains((x.Culture, x.Segment)))
|
||||
.Select(x => (culture: x.Culture, segment: x.Segment));
|
||||
|
||||
foreach (var (culture, segment) in validVariants)
|
||||
{
|
||||
AddSuccessNotification(notifications, c,
|
||||
var variantName = GetVariantName(culture, segment);
|
||||
|
||||
AddSuccessNotification(notifications, culture, segment,
|
||||
Services.TextService.Localize("speechBubbles/editContentSendToPublish"),
|
||||
Services.TextService.Localize("speechBubbles/editVariantSendToPublishText", new[] { _allLangs.Value[c].CultureName }));
|
||||
Services.TextService.Localize("speechBubbles/editVariantSendToPublishText", new[] { variantName }));
|
||||
}
|
||||
}
|
||||
else if (ModelState.IsValid)
|
||||
@@ -856,7 +864,7 @@ namespace Umbraco.Web.Editors
|
||||
//if there's more than 1 variant, then we need to add the culture specific error
|
||||
//messages based on the variants in error so that the messages show in the publish/save dialog
|
||||
if (variants.Count > 1)
|
||||
AddCultureValidationError(variant.Culture, "publish/contentPublishedFailedByMissingName");
|
||||
AddVariantValidationError(variant.Culture, variant.Segment, "publish/contentPublishedFailedByMissingName");
|
||||
else
|
||||
return false; //It's invariant and is missing critical data, it cannot be saved
|
||||
}
|
||||
@@ -910,18 +918,19 @@ namespace Umbraco.Web.Editors
|
||||
{
|
||||
if (variantCount > 1)
|
||||
{
|
||||
var cultureErrors = ModelState.GetCulturesWithErrors(Services.LocalizationService, cultureForInvariantErrors);
|
||||
var variantErrors = ModelState.GetVariantsWithErrors(cultureForInvariantErrors);
|
||||
|
||||
var savedWithoutErrors = contentItem.Variants
|
||||
.Where(x => x.Save && !cultureErrors.Contains(x.Culture) && x.Culture != null)
|
||||
.Select(x => x.Culture)
|
||||
.ToArray();
|
||||
.Where(x => x.Save && !variantErrors.Contains((x.Culture, x.Segment)))
|
||||
.Select(x => (culture: x.Culture, segment: x.Segment));
|
||||
|
||||
foreach (var c in savedWithoutErrors)
|
||||
foreach (var (culture, segment) in savedWithoutErrors)
|
||||
{
|
||||
AddSuccessNotification(notifications, c,
|
||||
var variantName = GetVariantName(culture, segment);
|
||||
|
||||
AddSuccessNotification(notifications, culture, segment,
|
||||
Services.TextService.Localize("speechBubbles/editContentSavedHeader"),
|
||||
Services.TextService.Localize(variantSavedLocalizationKey, new[] { _allLangs.Value[c].CultureName }));
|
||||
Services.TextService.Localize(variantSavedLocalizationKey, new[] { variantName }));
|
||||
}
|
||||
}
|
||||
else if (ModelState.IsValid)
|
||||
@@ -1054,14 +1063,16 @@ namespace Umbraco.Web.Editors
|
||||
if (!isPublished && releaseDates.Count == 0)
|
||||
{
|
||||
//can't continue, a mandatory variant is not published and not scheduled for publishing
|
||||
AddCultureValidationError(culture, "speechBubbles/scheduleErrReleaseDate2");
|
||||
// TODO: Add segment
|
||||
AddVariantValidationError(culture, null, "speechBubbles/scheduleErrReleaseDate2");
|
||||
isValid = false;
|
||||
continue;
|
||||
}
|
||||
if (!isPublished && releaseDates.Any(x => nonMandatoryVariantReleaseDates.Any(r => x.Date > r.Date)))
|
||||
{
|
||||
//can't continue, a mandatory variant is not published and it's scheduled for publishing after a non-mandatory
|
||||
AddCultureValidationError(culture, "speechBubbles/scheduleErrReleaseDate3");
|
||||
// TODO: Add segment
|
||||
AddVariantValidationError(culture, null, "speechBubbles/scheduleErrReleaseDate3");
|
||||
isValid = false;
|
||||
continue;
|
||||
}
|
||||
@@ -1075,7 +1086,7 @@ namespace Umbraco.Web.Editors
|
||||
//1) release date cannot be less than now
|
||||
if (variant.ReleaseDate.HasValue && variant.ReleaseDate < DateTime.Now)
|
||||
{
|
||||
AddCultureValidationError(variant.Culture, "speechBubbles/scheduleErrReleaseDate1");
|
||||
AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles/scheduleErrReleaseDate1");
|
||||
isValid = false;
|
||||
continue;
|
||||
}
|
||||
@@ -1083,7 +1094,7 @@ namespace Umbraco.Web.Editors
|
||||
//2) expire date cannot be less than now
|
||||
if (variant.ExpireDate.HasValue && variant.ExpireDate < DateTime.Now)
|
||||
{
|
||||
AddCultureValidationError(variant.Culture, "speechBubbles/scheduleErrExpireDate1");
|
||||
AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles/scheduleErrExpireDate1");
|
||||
isValid = false;
|
||||
continue;
|
||||
}
|
||||
@@ -1091,7 +1102,7 @@ namespace Umbraco.Web.Editors
|
||||
//3) expire date cannot be less than release date
|
||||
if (variant.ExpireDate.HasValue && variant.ReleaseDate.HasValue && variant.ExpireDate <= variant.ReleaseDate)
|
||||
{
|
||||
AddCultureValidationError(variant.Culture, "speechBubbles/scheduleErrExpireDate2");
|
||||
AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles/scheduleErrExpireDate2");
|
||||
isValid = false;
|
||||
continue;
|
||||
}
|
||||
@@ -1116,12 +1127,13 @@ namespace Umbraco.Web.Editors
|
||||
/// global notifications will be shown if all variant processing is successful and the save/publish dialog is closed, otherwise
|
||||
/// variant specific notifications are used to show success messages in the save/publish dialog.
|
||||
/// </remarks>
|
||||
private static void AddSuccessNotification(IDictionary<string, SimpleNotificationModel> notifications, string culture, string header, string msg)
|
||||
private static void AddSuccessNotification(IDictionary<string, SimpleNotificationModel> notifications, string culture, string segment, string header, string msg)
|
||||
{
|
||||
//add the global notification (which will display globally if all variants are successfully processed)
|
||||
notifications[string.Empty].AddSuccessNotification(header, msg);
|
||||
//add the variant specific notification (which will display in the dialog if all variants are not successfully processed)
|
||||
notifications.GetOrCreate(culture).AddSuccessNotification(header, msg);
|
||||
var key = culture + "_" + segment;
|
||||
notifications.GetOrCreate(key).AddSuccessNotification(header, msg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1171,17 +1183,16 @@ namespace Umbraco.Web.Editors
|
||||
return publishStatus;
|
||||
}
|
||||
|
||||
//All variants in this collection should have a culture if we get here! but we'll double check and filter here
|
||||
var cultureVariants = contentItem.Variants.Where(x => !x.Culture.IsNullOrWhiteSpace()).ToList();
|
||||
|
||||
var mandatoryCultures = _allLangs.Value.Values.Where(x => x.IsMandatory).Select(x => x.IsoCode).ToList();
|
||||
|
||||
var cultureErrors = ModelState.GetCulturesWithErrors(Services.LocalizationService, cultureForInvariantErrors);
|
||||
var variantErrors = ModelState.GetVariantsWithErrors(cultureForInvariantErrors);
|
||||
|
||||
var variants = contentItem.Variants.ToList();
|
||||
|
||||
//validate if we can publish based on the mandatory language requirements
|
||||
var canPublish = ValidatePublishingMandatoryLanguages(
|
||||
cultureErrors,
|
||||
contentItem, cultureVariants, mandatoryCultures,
|
||||
variantErrors,
|
||||
contentItem, variants, mandatoryCultures,
|
||||
mandatoryVariant => mandatoryVariant.Publish);
|
||||
|
||||
//Now check if there are validation errors on each variant.
|
||||
@@ -1191,11 +1202,11 @@ namespace Umbraco.Web.Editors
|
||||
|
||||
foreach (var variant in contentItem.Variants)
|
||||
{
|
||||
if (cultureErrors.Contains(variant.Culture))
|
||||
if (variantErrors.Contains((variant.Culture, variant.Segment)))
|
||||
variant.Publish = false;
|
||||
}
|
||||
|
||||
var culturesToPublish = cultureVariants.Where(x => x.Publish).Select(x => x.Culture).ToArray();
|
||||
var culturesToPublish = variants.Where(x => x.Publish).Select(x => x.Culture).ToArray();
|
||||
|
||||
if (canPublish)
|
||||
{
|
||||
@@ -1243,17 +1254,16 @@ namespace Umbraco.Web.Editors
|
||||
return publishStatus;
|
||||
}
|
||||
|
||||
//All variants in this collection should have a culture if we get here! but we'll double check and filter here
|
||||
var cultureVariants = contentItem.Variants.Where(x => !x.Culture.IsNullOrWhiteSpace()).ToList();
|
||||
|
||||
var mandatoryCultures = _allLangs.Value.Values.Where(x => x.IsMandatory).Select(x => x.IsoCode).ToList();
|
||||
|
||||
var cultureErrors = ModelState.GetCulturesWithErrors(Services.LocalizationService, cultureForInvariantErrors);
|
||||
var variantErrors = ModelState.GetVariantsWithErrors(cultureForInvariantErrors);
|
||||
|
||||
var variants = contentItem.Variants.ToList();
|
||||
|
||||
//validate if we can publish based on the mandatory languages selected
|
||||
var canPublish = ValidatePublishingMandatoryLanguages(
|
||||
cultureErrors,
|
||||
contentItem, cultureVariants, mandatoryCultures,
|
||||
variantErrors,
|
||||
contentItem, variants, mandatoryCultures,
|
||||
mandatoryVariant => mandatoryVariant.Publish);
|
||||
|
||||
//if none are published and there are validation errors for mandatory cultures, then we can't publish anything
|
||||
@@ -1265,19 +1275,19 @@ namespace Umbraco.Web.Editors
|
||||
//It is a requirement that this is performed AFTER ValidatePublishingMandatoryLanguages.
|
||||
foreach (var variant in contentItem.Variants)
|
||||
{
|
||||
if (cultureErrors.Contains(variant.Culture))
|
||||
if (variantErrors.Contains((variant.Culture, variant.Segment)))
|
||||
variant.Publish = false;
|
||||
}
|
||||
|
||||
//At this stage all variants might have failed validation which means there are no cultures flagged for publishing!
|
||||
var culturesToPublish = cultureVariants.Where(x => x.Publish).Select(x => x.Culture).ToArray();
|
||||
var culturesToPublish = variants.Where(x => x.Publish).Select(x => x.Culture).ToArray();
|
||||
canPublish = canPublish && culturesToPublish.Length > 0;
|
||||
|
||||
if (canPublish)
|
||||
{
|
||||
//try to publish all the values on the model - this will generally only fail if someone is tampering with the request
|
||||
//since there's no reason variant rules would be violated in normal cases.
|
||||
canPublish = PublishCulture(contentItem.PersistedContent, cultureVariants, defaultCulture);
|
||||
canPublish = PublishCulture(contentItem.PersistedContent, variants, defaultCulture);
|
||||
}
|
||||
|
||||
if (canPublish)
|
||||
@@ -1302,16 +1312,16 @@ namespace Umbraco.Web.Editors
|
||||
/// <summary>
|
||||
/// Validate if publishing is possible based on the mandatory language requirements
|
||||
/// </summary>
|
||||
/// <param name="culturesWithValidationErrors"></param>
|
||||
/// <param name="variantsWithValidationErrors"></param>
|
||||
/// <param name="contentItem"></param>
|
||||
/// <param name="cultureVariants"></param>
|
||||
/// <param name="variants"></param>
|
||||
/// <param name="mandatoryCultures"></param>
|
||||
/// <param name="publishingCheck"></param>
|
||||
/// <returns></returns>
|
||||
private bool ValidatePublishingMandatoryLanguages(
|
||||
IReadOnlyCollection<string> culturesWithValidationErrors,
|
||||
IReadOnlyCollection<(string culture, string segment)> variantsWithValidationErrors,
|
||||
ContentItemSave contentItem,
|
||||
IReadOnlyCollection<ContentVariantSave> cultureVariants,
|
||||
IReadOnlyCollection<ContentVariantSave> variants,
|
||||
IReadOnlyList<string> mandatoryCultures,
|
||||
Func<ContentVariantSave, bool> publishingCheck)
|
||||
{
|
||||
@@ -1322,11 +1332,11 @@ namespace Umbraco.Web.Editors
|
||||
{
|
||||
//Check if a mandatory language is missing from being published
|
||||
|
||||
var mandatoryVariant = cultureVariants.First(x => x.Culture.InvariantEquals(culture));
|
||||
var mandatoryVariant = variants.First(x => x.Culture.InvariantEquals(culture));
|
||||
|
||||
var isPublished = contentItem.PersistedContent.Published && contentItem.PersistedContent.IsCulturePublished(culture);
|
||||
var isPublishing = isPublished || publishingCheck(mandatoryVariant);
|
||||
var isValid = !culturesWithValidationErrors.InvariantContains(culture);
|
||||
var isValid = !variantsWithValidationErrors.Select(v => v.culture).InvariantContains(culture);
|
||||
|
||||
result.Add((mandatoryVariant, isPublished || isPublishing, isValid));
|
||||
}
|
||||
@@ -1341,19 +1351,19 @@ namespace Umbraco.Web.Editors
|
||||
if (r.publishing && !r.isValid)
|
||||
{
|
||||
//flagged for publishing but the mandatory culture is invalid
|
||||
AddCultureValidationError(r.model.Culture, "publish/contentPublishedFailedReqCultureValidationError");
|
||||
AddVariantValidationError(r.model.Culture, r.model.Segment, "publish/contentPublishedFailedReqCultureValidationError");
|
||||
canPublish = false;
|
||||
}
|
||||
else if (r.publishing && r.isValid && firstInvalidMandatoryCulture != null)
|
||||
{
|
||||
//in this case this culture also cannot be published because another mandatory culture is invalid
|
||||
AddCultureValidationError(r.model.Culture, "publish/contentPublishedFailedReqCultureValidationError", firstInvalidMandatoryCulture);
|
||||
AddVariantValidationError(r.model.Culture, r.model.Segment, "publish/contentPublishedFailedReqCultureValidationError", firstInvalidMandatoryCulture);
|
||||
canPublish = false;
|
||||
}
|
||||
else if (!r.publishing)
|
||||
{
|
||||
//cannot continue publishing since a required culture that is not currently being published isn't published
|
||||
AddCultureValidationError(r.model.Culture, "speechBubbles/contentReqCulturePublishError");
|
||||
AddVariantValidationError(r.model.Culture, r.model.Segment, "speechBubbles/contentReqCulturePublishError");
|
||||
canPublish = false;
|
||||
}
|
||||
}
|
||||
@@ -1378,7 +1388,7 @@ namespace Umbraco.Web.Editors
|
||||
var valid = persistentContent.PublishCulture(CultureImpact.Explicit(variant.Culture, defaultCulture.InvariantEquals(variant.Culture)));
|
||||
if (!valid)
|
||||
{
|
||||
AddCultureValidationError(variant.Culture, "speechBubbles/contentCultureValidationError");
|
||||
AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles/contentCultureValidationError");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1390,14 +1400,40 @@ namespace Umbraco.Web.Editors
|
||||
/// Adds a generic culture error for use in displaying the culture validation error in the save/publish/etc... dialogs
|
||||
/// </summary>
|
||||
/// <param name="culture">Culture to assign the error to</param>
|
||||
/// <param name="segment">Segment to assign the error to</param>
|
||||
/// <param name="localizationKey"></param>
|
||||
/// <param name="cultureToken">
|
||||
/// The culture used in the localization message, null by default which means <see cref="culture"/> will be used.
|
||||
/// </param>
|
||||
private void AddCultureValidationError(string culture, string localizationKey, string cultureToken = null)
|
||||
private void AddVariantValidationError(string culture, string segment, string localizationKey, string cultureToken = null)
|
||||
{
|
||||
var errMsg = Services.TextService.Localize(localizationKey, new[] { cultureToken == null ? _allLangs.Value[culture].CultureName : _allLangs.Value[cultureToken].CultureName });
|
||||
ModelState.AddCultureValidationError(culture, errMsg);
|
||||
var cultureToUse = cultureToken ?? culture;
|
||||
var variantName = GetVariantName(cultureToUse, segment);
|
||||
|
||||
var errMsg = Services.TextService.Localize(localizationKey, new[] { variantName });
|
||||
|
||||
ModelState.AddVariantValidationError(culture, segment, errMsg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the human readable variant name based on culture and segment
|
||||
/// </summary>
|
||||
/// <param name="culture">Culture</param>
|
||||
/// <param name="segment">Segment</param>
|
||||
/// <returns></returns>
|
||||
private string GetVariantName(string culture, string segment)
|
||||
{
|
||||
if(culture.IsNullOrWhiteSpace() && segment.IsNullOrWhiteSpace())
|
||||
{
|
||||
// TODO: Get name for default variant from somewhere?
|
||||
return "Default";
|
||||
}
|
||||
|
||||
var cultureName = culture == null ? null : _allLangs.Value[culture].CultureName;
|
||||
var variantName = string.Join(" — ", new[] { segment, cultureName }.Where(x => !x.IsNullOrWhiteSpace()));
|
||||
|
||||
// Format: <segment> [—] <culture name>
|
||||
return variantName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1839,11 +1875,11 @@ namespace Umbraco.Web.Editors
|
||||
if (!ModelState.IsValid && display.Variants.Count() > 1)
|
||||
{
|
||||
//Add any culture specific errors here
|
||||
var cultureErrors = ModelState.GetCulturesWithErrors(Services.LocalizationService, cultureForInvariantErrors);
|
||||
var variantErrors = ModelState.GetVariantsWithErrors(cultureForInvariantErrors);
|
||||
|
||||
foreach (var cultureError in cultureErrors)
|
||||
foreach (var (culture, segment) in variantErrors)
|
||||
{
|
||||
AddCultureValidationError(cultureError, "speechBubbles/contentCultureValidationError");
|
||||
AddVariantValidationError(culture, segment, "speechBubbles/contentCultureValidationError");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,11 +65,12 @@ namespace Umbraco.Web
|
||||
/// </summary>
|
||||
/// <param name="modelState"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <param name="segment"></param>
|
||||
/// <param name="errMsg"></param>
|
||||
internal static void AddCultureValidationError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState,
|
||||
string culture, string errMsg)
|
||||
internal static void AddVariantValidationError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState,
|
||||
string culture, string segment, string errMsg)
|
||||
{
|
||||
var key = "_content_variant_" + culture + "_";
|
||||
var key = "_content_variant_" + (culture.IsNullOrWhiteSpace() ? "invariant" : culture) + "_" + (segment.IsNullOrWhiteSpace() ? "null" : segment) + "_";
|
||||
if (modelState.ContainsKey(key)) return;
|
||||
modelState.AddModelError(key, errMsg);
|
||||
}
|
||||
@@ -83,23 +84,28 @@ namespace Umbraco.Web
|
||||
/// <returns>
|
||||
/// A list of cultures that have property validation errors. The default culture will be returned for any invariant property errors.
|
||||
/// </returns>
|
||||
internal static IReadOnlyList<string> GetCulturesWithPropertyErrors(this System.Web.Http.ModelBinding.ModelStateDictionary modelState,
|
||||
ILocalizationService localizationService, string cultureForInvariantErrors)
|
||||
internal static IReadOnlyList<(string culture, string segment)> GetVariantsWithPropertyErrors(this System.Web.Http.ModelBinding.ModelStateDictionary modelState,
|
||||
string cultureForInvariantErrors)
|
||||
{
|
||||
//Add any culture specific errors here
|
||||
var cultureErrors = modelState.Keys
|
||||
//Add any variant specific errors here
|
||||
var variantErrors = modelState.Keys
|
||||
.Where(key => key.StartsWith("_Properties.")) //only choose _Properties errors
|
||||
.Select(x => x.Split('.')) //split into parts
|
||||
.Where(x => x.Length >= 3 && x[0] == "_Properties") //only choose _Properties errors
|
||||
.Select(x => x[2]) //select the culture part
|
||||
.Where(x => !x.IsNullOrWhiteSpace()) //if it has a value
|
||||
//if it's marked "invariant" than return the default language, this is because we can only edit invariant properties on the default language
|
||||
.Where(x => x.Length >= 4 && !x[2].IsNullOrWhiteSpace() && !x[3].IsNullOrWhiteSpace())
|
||||
.Select(x => (culture: x[2], segment: x[3]))
|
||||
//if the culture is marked "invariant" than return the default language, this is because we can only edit invariant properties on the default language
|
||||
//so errors for those must show up under the default lang.
|
||||
.Select(x => x == "invariant" ? cultureForInvariantErrors : x)
|
||||
.WhereNotNull()
|
||||
//if the segment is marked "null" then return an actual null
|
||||
.Select(x =>
|
||||
{
|
||||
var culture = x.culture == "invariant" ? cultureForInvariantErrors : x.culture;
|
||||
var segment = x.segment == "null" ? null : x.segment;
|
||||
return (culture, segment);
|
||||
})
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
return cultureErrors;
|
||||
return variantErrors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -111,23 +117,33 @@ namespace Umbraco.Web
|
||||
/// <returns>
|
||||
/// A list of cultures that have validation errors. The default culture will be returned for any invariant errors.
|
||||
/// </returns>
|
||||
internal static IReadOnlyList<string> GetCulturesWithErrors(this System.Web.Http.ModelBinding.ModelStateDictionary modelState,
|
||||
ILocalizationService localizationService, string cultureForInvariantErrors)
|
||||
internal static IReadOnlyList<(string culture, string segment)> GetVariantsWithErrors(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, string cultureForInvariantErrors)
|
||||
{
|
||||
var propertyCultureErrors = modelState.GetCulturesWithPropertyErrors(localizationService, cultureForInvariantErrors);
|
||||
var propertyVariantErrors = modelState.GetVariantsWithPropertyErrors(cultureForInvariantErrors);
|
||||
|
||||
//now check the other special culture errors that are
|
||||
var genericCultureErrors = modelState.Keys
|
||||
//now check the other special variant errors that are
|
||||
var genericVariantErrors = modelState.Keys
|
||||
.Where(x => x.StartsWith("_content_variant_") && x.EndsWith("_"))
|
||||
.Select(x => x.TrimStart("_content_variant_").TrimEnd("_"))
|
||||
.Where(x => !x.IsNullOrWhiteSpace())
|
||||
.Select(x => x.TrimStart("_content_variant_").TrimEnd("_"))
|
||||
.Select(x =>
|
||||
{
|
||||
// Format "<culture>_<segment>"
|
||||
var cs = x.Split(new[] { '_' });
|
||||
return (culture: cs[0], segment: cs[1]);
|
||||
})
|
||||
.Where(x => !x.culture.IsNullOrWhiteSpace())
|
||||
//if it's marked "invariant" than return the default language, this is because we can only edit invariant properties on the default language
|
||||
//so errors for those must show up under the default lang.
|
||||
.Select(x => x == "invariant" ? cultureForInvariantErrors : x)
|
||||
.WhereNotNull()
|
||||
//if the segment is marked "null" then return an actual null
|
||||
.Select(x =>
|
||||
{
|
||||
var culture = x.culture == "invariant" ? cultureForInvariantErrors : x.culture;
|
||||
var segment = x.segment == "null" ? null : x.segment;
|
||||
return (culture, segment);
|
||||
})
|
||||
.Distinct();
|
||||
|
||||
return propertyCultureErrors.Union(genericCultureErrors).ToList();
|
||||
return propertyVariantErrors.Union(genericVariantErrors).Distinct().ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -59,6 +59,11 @@ Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "ht
|
||||
StartServerOnDebug = "false"
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTest\", "Umbraco.Tests.AcceptanceTest\", "{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}"
|
||||
ProjectSection(WebsiteProperties) = preProject
|
||||
SlnRelativePath = "Umbraco.Tests.AcceptanceTest\"
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web", "Umbraco.Web\Umbraco.Web.csproj", "{651E1350-91B6-44B7-BD60-7207006D7003}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Tests", "Umbraco.Tests\Umbraco.Tests.csproj", "{5D3B8245-ADA6-453F-A008-50ED04BFE770}"
|
||||
@@ -229,6 +234,7 @@ Global
|
||||
{FB5676ED-7A69-492C-B802-E7B24144C0FC} = {B5BD12C1-A454-435E-8A46-FF4A364C0382}
|
||||
{D6319409-777A-4BD0-93ED-B2DFD805B32C} = {B5BD12C1-A454-435E-8A46-FF4A364C0382}
|
||||
{A499779C-1B3B-48A8-B551-458E582E6E96} = {B5BD12C1-A454-435E-8A46-FF4A364C0382}
|
||||
{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817} = {B5BD12C1-A454-435E-8A46-FF4A364C0382}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC}
|
||||
|
||||
Reference in New Issue
Block a user