diff --git a/.travis.yml b/.travis.yml
index 11e39bebb..f97f4facd 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,13 +1,24 @@
dist: trusty
sudo: required
+services:
+ - docker
+
+addons:
+ chrome: stable
+
language: node_js
node_js:
- "8"
+before_script:
+ # Disable services enabled by default
+ - sudo /etc/init.d/postgresql stop
+
install:
- npm install -g npm@latest
- npm ci
script:
- - npm run test:ci
+ # - docker-compose stop
+ - npm run build && npm run e2e:docker
diff --git a/e2e/components/component.ts b/e2e/components/component.ts
new file mode 100755
index 000000000..134287c0c
--- /dev/null
+++ b/e2e/components/component.ts
@@ -0,0 +1,43 @@
+/*!
+ * @license
+ * Alfresco Example Content Application
+ *
+ * Copyright (C) 2005 - 2018 Alfresco Software Limited
+ *
+ * This file is part of the Alfresco Example Content Application.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * The Alfresco Example Content Application is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Alfresco Example Content Application is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+
+import { ElementFinder, element, by, ExpectedConditions as EC, browser } from 'protractor';
+import { BROWSER_WAIT_TIMEOUT } from '../configs';
+
+export abstract class Component {
+ component: ElementFinder;
+
+ constructor(selector: string, ancestor?: ElementFinder) {
+ const locator = by.css(selector);
+
+ this.component = ancestor
+ ? ancestor.element(locator)
+ : element(locator);
+ }
+
+ wait() {
+ return browser.wait(EC.presenceOf(this.component), BROWSER_WAIT_TIMEOUT);
+ }
+}
diff --git a/e2e/components/components.ts b/e2e/components/components.ts
new file mode 100755
index 000000000..604be941d
--- /dev/null
+++ b/e2e/components/components.ts
@@ -0,0 +1,32 @@
+/*!
+ * @license
+ * Alfresco Example Content Application
+ *
+ * Copyright (C) 2005 - 2018 Alfresco Software Limited
+ *
+ * This file is part of the Alfresco Example Content Application.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * The Alfresco Example Content Application is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Alfresco Example Content Application is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+
+export * from './login/login';
+export * from './header/header';
+export * from './header/user-info';
+export * from './data-table/data-table';
+export * from './pagination/pagination';
+export * from './sidenav/sidenav';
+export * from './toolbar/toolbar';
diff --git a/e2e/components/data-table/data-table.ts b/e2e/components/data-table/data-table.ts
new file mode 100755
index 000000000..1bf8de816
--- /dev/null
+++ b/e2e/components/data-table/data-table.ts
@@ -0,0 +1,243 @@
+/*!
+ * @license
+ * Alfresco Example Content Application
+ *
+ * Copyright (C) 2005 - 2018 Alfresco Software Limited
+ *
+ * This file is part of the Alfresco Example Content Application.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * The Alfresco Example Content Application is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Alfresco Example Content Application is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+
+import { ElementFinder, ElementArrayFinder, promise, by, browser, ExpectedConditions as EC, protractor } from 'protractor';
+import { BROWSER_WAIT_TIMEOUT } from '../../configs';
+import { Component } from '../component';
+import { Utils } from '../../utilities/utils';
+
+export class DataTable extends Component {
+ private static selectors = {
+ root: 'adf-datatable',
+
+ head: '.adf-datatable-header',
+ columnHeader: '.adf-datatable-row .adf-datatable-table-cell-header',
+ sortedColumnHeader: `
+ .adf-data-table__header--sorted-asc,
+ .adf-data-table__header--sorted-desc
+ `,
+
+ body: '.adf-datatable-body',
+ row: '.adf-datatable-row[role]',
+ selectedRow: '.adf-datatable-row.is-selected',
+ cell: '.adf-data-table-cell',
+ locationLink: 'app-location-link',
+
+ selectedIcon: '.mat-icon',
+
+ emptyListContainer: 'div.adf-no-content-container',
+ emptyFolderDragAndDrop: '.adf-empty-list_template .adf-empty-folder',
+
+ emptyListTitle: '.app-empty-folder__title',
+ emptyListSubtitle: '.app-empty-folder__subtitle',
+ emptyListText: '.app-empty-folder__text'
+ };
+
+ head: ElementFinder = this.component.element(by.css(DataTable.selectors.head));
+ body: ElementFinder = this.component.element(by.css(DataTable.selectors.body));
+ cell = by.css(DataTable.selectors.cell);
+ locationLink = by.css(DataTable.selectors.locationLink);
+ emptyList: ElementFinder = this.component.element(by.css(DataTable.selectors.emptyListContainer));
+ emptyFolderDragAndDrop: ElementFinder = this.component.element(by.css(DataTable.selectors.emptyFolderDragAndDrop));
+ emptyListTitle: ElementFinder = this.component.element(by.css(DataTable.selectors.emptyListTitle));
+ emptyListSubtitle: ElementFinder = this.component.element(by.css(DataTable.selectors.emptyListSubtitle));
+ emptyListText: ElementArrayFinder = this.component.all(by.css(DataTable.selectors.emptyListText));
+
+ constructor(ancestor?: ElementFinder) {
+ super(DataTable.selectors.root, ancestor);
+ }
+
+ // Wait methods (waits for elements)
+ waitForHeader() {
+ return browser.wait(EC.presenceOf(this.head), BROWSER_WAIT_TIMEOUT);
+ }
+
+ waitForEmptyState() {
+ return browser.wait(EC.presenceOf(this.emptyList), BROWSER_WAIT_TIMEOUT);
+ }
+
+ // Header/Column methods
+ getColumnHeaders(): ElementArrayFinder {
+ const locator = by.css(DataTable.selectors.columnHeader);
+ return this.head.all(locator);
+ }
+
+ getNthColumnHeader(nth: number): ElementFinder {
+ return this.getColumnHeaders().get(nth - 1);
+ }
+
+ getColumnHeaderByLabel(label: string): ElementFinder {
+ const locator = by.cssContainingText(DataTable.selectors.columnHeader, label);
+ return this.head.element(locator);
+ }
+
+ getSortedColumnHeader(): ElementFinder {
+ const locator = by.css(DataTable.selectors.sortedColumnHeader);
+ return this.head.element(locator);
+ }
+
+ getSortingOrder() {
+ return this.getSortedColumnHeader().getAttribute('class')
+ .then(str => {
+ if (str.includes('asc')) {
+ return 'asc';
+ } else {
+ if (str.includes('desc')) {
+ return 'desc';
+ }
+ }
+ });
+ }
+
+ sortByColumn(columnName: string): promise.Promise {
+ const column = this.getColumnHeaderByLabel(columnName);
+ const click = browser.actions().mouseMove(column).click();
+
+ return click.perform();
+ }
+
+ // Rows methods
+ getRows(): ElementArrayFinder {
+ return this.body.all(by.css(DataTable.selectors.row));
+ }
+
+ getSelectedRows(): ElementArrayFinder {
+ return this.body.all(by.css(DataTable.selectors.selectedRow));
+ }
+
+ countSelectedRows(): promise.Promise {
+ return this.getSelectedRows().count();
+ }
+
+ getNthRow(nth: number): ElementFinder {
+ return this.getRows().get(nth - 1);
+ }
+
+ getRowName(name: string): ElementFinder {
+ return this.body.element(by.cssContainingText(`.adf-data-table-cell span`, name));
+ }
+
+ getItemNameTooltip(name: string): promise.Promise {
+ return this.getRowName(name).getAttribute('title');
+ }
+
+ countRows(): promise.Promise {
+ return this.getRows().count();
+ }
+
+ hasCheckMarkIcon(itemName: string) {
+ return this.getRowName(itemName).element(by.xpath(`./ancestor::div[contains(@class, 'adf-datatable-row')]`))
+ .element(by.css(DataTable.selectors.selectedIcon)).isPresent();
+ }
+
+ // Navigation/selection methods
+ doubleClickOnItemName(name: string): promise.Promise {
+ const dblClick = browser.actions()
+ .mouseMove(this.getRowName(name))
+ .click()
+ .click();
+
+ return dblClick.perform();
+ }
+
+ clickOnItemName(name: string): promise.Promise {
+ const item = this.getRowName(name);
+ return Utils.waitUntilElementClickable(item)
+ .then(() => this.getRowName(name).click());
+ }
+
+ selectMultipleItems(names: string[]): promise.Promise {
+ return this.clearSelection()
+ .then(() => browser.actions().sendKeys(protractor.Key.COMMAND).perform())
+ .then(() => {
+ names.forEach(name => {
+ this.clickOnItemName(name);
+ });
+ })
+ .then(() => browser.actions().sendKeys(protractor.Key.NULL).perform());
+ }
+
+ clearSelection(): promise.Promise {
+ return this.getSelectedRows().count()
+ .then(count => {
+ if (count !== 0) { browser.refresh().then(() => this.waitForHeader()); }
+ });
+ }
+
+ getItemLocation(name: string) {
+ return this.getRowName(name).element(by.xpath(`./ancestor::div[contains(@class, 'adf-datatable-row')]`))
+ .element(this.locationLink);
+ }
+
+ getItemLocationTooltip(name: string): promise.Promise {
+ return this.getItemLocation(name).$('a').getAttribute('title');
+ }
+
+ clickItemLocation(name: string) {
+ return this.getItemLocation(name).click();
+ }
+
+ // empty state methods
+ isEmptyList(): promise.Promise {
+ return this.emptyList.isPresent();
+ }
+
+ isEmptyWithDragAndDrop(): promise.Promise {
+ return this.emptyFolderDragAndDrop.isDisplayed();
+ }
+
+ getEmptyDragAndDropText(): promise.Promise {
+ return this.isEmptyWithDragAndDrop()
+ .then(() => {
+ return this.emptyFolderDragAndDrop.getText();
+ });
+ }
+
+ getEmptyStateTitle(): promise.Promise {
+ return this.isEmptyList()
+ .then(() => {
+ return this.emptyListTitle.getText();
+ });
+ }
+
+ getEmptyStateSubtitle(): promise.Promise {
+ return this.isEmptyList()
+ .then(() => {
+ return this.emptyListSubtitle.getText();
+ });
+ }
+
+ getEmptyStateText(): promise.Promise {
+ return this.isEmptyList()
+ .then(() => {
+ return this.emptyListText.getText();
+ });
+ }
+
+ getCellsContainingName(name: string) {
+ return this.getRows().all(by.cssContainingText(DataTable.selectors.cell, name))
+ .map(cell => cell.getText());
+ }
+}
diff --git a/e2e/components/dialog/create-edit-folder-dialog.ts b/e2e/components/dialog/create-edit-folder-dialog.ts
new file mode 100755
index 000000000..696746e63
--- /dev/null
+++ b/e2e/components/dialog/create-edit-folder-dialog.ts
@@ -0,0 +1,108 @@
+/*!
+ * @license
+ * Alfresco Example Content Application
+ *
+ * Copyright (C) 2005 - 2018 Alfresco Software Limited
+ *
+ * This file is part of the Alfresco Example Content Application.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * The Alfresco Example Content Application is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Alfresco Example Content Application is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+
+import { ElementFinder, by, browser, protractor, ExpectedConditions as EC, promise } from 'protractor';
+import { BROWSER_WAIT_TIMEOUT } from '../../configs';
+import { Component } from '../component';
+import { Utils } from '../../utilities/utils';
+
+export class CreateOrEditFolderDialog extends Component {
+ private static selectors = {
+ root: 'adf-folder-dialog',
+
+ title: '.mat-dialog-title',
+ nameInput: 'input[placeholder="Name" i]',
+ descriptionTextArea: 'textarea[placeholder="Description" i]',
+ button: '.mat-dialog-actions button',
+ validationMessage: '.mat-hint span'
+ };
+
+ title: ElementFinder = this.component.element(by.css(CreateOrEditFolderDialog.selectors.title));
+ nameInput: ElementFinder = this.component.element(by.css(CreateOrEditFolderDialog.selectors.nameInput));
+ descriptionTextArea: ElementFinder = this.component.element(by.css(CreateOrEditFolderDialog.selectors.descriptionTextArea));
+ createButton: ElementFinder = this.component.element(by.cssContainingText(CreateOrEditFolderDialog.selectors.button, 'Create'));
+ cancelButton: ElementFinder = this.component.element(by.cssContainingText(CreateOrEditFolderDialog.selectors.button, 'Cancel'));
+ updateButton: ElementFinder = this.component.element(by.cssContainingText(CreateOrEditFolderDialog.selectors.button, 'Update'));
+ validationMessage: ElementFinder = this.component.element(by.css(CreateOrEditFolderDialog.selectors.validationMessage));
+
+ constructor(ancestor?: ElementFinder) {
+ super(CreateOrEditFolderDialog.selectors.root, ancestor);
+ }
+
+ waitForDialogToOpen() {
+ return browser.wait(EC.presenceOf(this.title), BROWSER_WAIT_TIMEOUT)
+ .then(() => browser.wait(EC.presenceOf(browser.element(by.css('.cdk-overlay-backdrop'))), BROWSER_WAIT_TIMEOUT));
+
+ }
+
+ waitForDialogToClose() {
+ return browser.wait(EC.stalenessOf(this.title), BROWSER_WAIT_TIMEOUT);
+ }
+
+ getTitle(): promise.Promise {
+ return this.title.getText();
+ }
+
+ isValidationMessageDisplayed(): promise.Promise {
+ return this.validationMessage.isDisplayed();
+ }
+
+ getValidationMessage(): promise.Promise {
+ return this.isValidationMessageDisplayed()
+ .then(() => this.validationMessage.getText());
+ }
+
+ enterName(name: string) {
+ return this.nameInput.clear()
+ // .then(() => this.nameInput.sendKeys(name));
+ .then(() => Utils.typeInField(this.nameInput, name));
+ }
+
+ enterDescription(description: string) {
+ return this.descriptionTextArea.clear()
+ // .then(() => this.descriptionTextArea.sendKeys(description));
+ .then(() => Utils.typeInField(this.descriptionTextArea, description));
+ }
+
+ deleteNameWithBackspace(): promise.Promise {
+ return this.nameInput.clear()
+ .then(() => {
+ return this.nameInput.sendKeys(' ', protractor.Key.CONTROL, 'a', protractor.Key.NULL, protractor.Key.BACK_SPACE);
+ });
+ }
+
+ clickCreate() {
+ return this.createButton.click();
+ }
+
+ clickCancel() {
+ return this.cancelButton.click()
+ .then(() => this.waitForDialogToClose());
+ }
+
+ clickUpdate() {
+ return this.updateButton.click();
+ }
+}
diff --git a/e2e/components/header/header.ts b/e2e/components/header/header.ts
new file mode 100755
index 000000000..ef151d54d
--- /dev/null
+++ b/e2e/components/header/header.ts
@@ -0,0 +1,42 @@
+/*!
+ * @license
+ * Alfresco Example Content Application
+ *
+ * Copyright (C) 2005 - 2018 Alfresco Software Limited
+ *
+ * This file is part of the Alfresco Example Content Application.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * The Alfresco Example Content Application is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Alfresco Example Content Application is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+
+import { ElementFinder, by } from 'protractor';
+import { Component } from '../component';
+import { UserInfo } from './user-info';
+
+export class Header extends Component {
+ private locators = {
+ logoLink: by.css('.app-menu__title'),
+ userInfo: by.css('app-current-user')
+ };
+
+ logoLink: ElementFinder = this.component.element(this.locators.logoLink);
+ userInfo: UserInfo = new UserInfo(this.component);
+
+ constructor(ancestor?: ElementFinder) {
+ super('app-header', ancestor);
+ }
+}
diff --git a/e2e/components/header/user-info.ts b/e2e/components/header/user-info.ts
new file mode 100755
index 000000000..b5e840080
--- /dev/null
+++ b/e2e/components/header/user-info.ts
@@ -0,0 +1,64 @@
+/*!
+ * @license
+ * Alfresco Example Content Application
+ *
+ * Copyright (C) 2005 - 2018 Alfresco Software Limited
+ *
+ * This file is part of the Alfresco Example Content Application.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * The Alfresco Example Content Application is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Alfresco Example Content Application is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+
+import { ElementFinder, by, promise } from 'protractor';
+import { Menu } from '../menu/menu';
+import { Component } from '../component';
+
+export class UserInfo extends Component {
+ private locators = {
+ avatar: by.css('.current-user__avatar'),
+ fullName: by.css('.current-user__full-name'),
+ menuItems: by.css('[mat-menu-item]')
+ };
+
+ fullName: ElementFinder = this.component.element(this.locators.fullName);
+ avatar: ElementFinder = this.component.element(this.locators.avatar);
+
+ menu: Menu = new Menu();
+
+ constructor(ancestor?: ElementFinder) {
+ super('app-current-user', ancestor);
+ }
+
+ openMenu(): promise.Promise