diff --git a/e2e/app.e2e-spec.ts b/e2e/app.e2e-spec.ts deleted file mode 100644 index 5ed9f0f66..000000000 --- a/e2e/app.e2e-spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { AppPage } from './app.po'; - -describe('alfresco-content-app App', () => { - let page: AppPage; - - beforeEach(() => { - page = new AppPage(); - }); - - it('should display welcome message', () => { - page.navigateTo(); - expect(page.getParagraphText()).toEqual('Welcome to app!'); - }); -}); diff --git a/e2e/app.po.ts b/e2e/app.po.ts deleted file mode 100644 index 82ea75ba5..000000000 --- a/e2e/app.po.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { browser, by, element } from 'protractor'; - -export class AppPage { - navigateTo() { - return browser.get('/'); - } - - getParagraphText() { - return element(by.css('app-root h1')).getText(); - } -} diff --git a/e2e/components/component.ts b/e2e/components/component.ts new file mode 100644 index 000000000..3d38761ef --- /dev/null +++ b/e2e/components/component.ts @@ -0,0 +1,35 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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 100644 index 000000000..cfcca0826 --- /dev/null +++ b/e2e/components/components.ts @@ -0,0 +1,24 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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 100644 index 000000000..1c6ac4e2e --- /dev/null +++ b/e2e/components/data-table/data-table.ts @@ -0,0 +1,110 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ElementFinder, ElementArrayFinder, promise, by, browser, ExpectedConditions as EC } from 'protractor'; +import { BROWSER_WAIT_TIMEOUT } from '../../configs'; +import { Component } from '../component'; + +export class DataTable extends Component { + private static selectors = { + root: 'adf-datatable', + + head: 'table > thead', + columnHeader: 'tr > th', + sortedColumnHeader: ` + th.adf-data-table__header--sorted-asc, + th.adf-data-table__header--sorted-desc + `, + + body: 'table > tbody', + row: 'tr' + }; + + private head: ElementFinder = this.component.element(by.css(DataTable.selectors.head)); + private body: ElementFinder = this.component.element(by.css(DataTable.selectors.body)); + + 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() {} + + // 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); + } + + sortByColumn(columnName: string): 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)); + } + + getNthRow(nth: number): ElementFinder { + return this.getRows().get(nth - 1); + } + + getRowByContainingText(text: string): ElementFinder { + const locator = by.cssContainingText(DataTable.selectors.row, text); + return this.body.element(locator); + } + + countRows(): promise.Promise { + return this.getRows().count(); + } + + // Navigation/selection methods + doubleClickOnRowByContainingText(text: string): promise.Promise { + const row = this.getRowByContainingText(text); + const dblClick = browser.actions().mouseMove(row).click().click(); + + return dblClick.perform(); + } + + clickOnRowByContainingText(text: string): promise.Promise { + const row = this.getRowByContainingText(text); + const click = browser.actions().mouseMove(row).click(); + + return click.perform(); + } +} diff --git a/e2e/components/dialog/create-edit-folder-dialog.ts b/e2e/components/dialog/create-edit-folder-dialog.ts new file mode 100644 index 000000000..11296bc76 --- /dev/null +++ b/e2e/components/dialog/create-edit-folder-dialog.ts @@ -0,0 +1,106 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ElementFinder, by, browser, protractor, ExpectedConditions as EC } from 'protractor'; +import { BROWSER_WAIT_TIMEOUT } from '../../configs'; +import { Component } from '../component'; + +export class CreateOrEditFolderDialog extends Component { + private static selectors = { + root: '.mat-dialog-container', + + title: '.mat-dialog-title', + nameInput: '.mat-dialog-container .mat-input-element[placeholder="Name"]', + descriptionTextArea: '.mat-dialog-container .mat-input-element[placeholder="Description"]', + 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); + } + + waitForDialogToClose() { + return browser.wait(EC.stalenessOf(this.title), BROWSER_WAIT_TIMEOUT); + } + + getTitle(): Promise { + return this.title.getText(); + } + + isValidationMessageDisplayed(): Promise { + return this.validationMessage.isDisplayed(); + } + + getValidationMessage(): Promise { + return this.isValidationMessageDisplayed() + .then(() => this.validationMessage.getText()) + .catch(() => ''); + } + + enterName(name: string): CreateOrEditFolderDialog { + const { nameInput } = this; + + nameInput.clear(); + nameInput.sendKeys(name); + + return this; + } + + deleteNameWithBackspace(): Promise { + const { nameInput } = this; + + return nameInput.clear() + .then(() => { + return nameInput.sendKeys(' ', protractor.Key.CONTROL, 'a', protractor.Key.NULL, protractor.Key.BACK_SPACE); + }); + } + + enterDescription(description: string): CreateOrEditFolderDialog { + const { descriptionTextArea } = this; + + descriptionTextArea.clear(); + descriptionTextArea.sendKeys(description); + + return this; + } + + 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 100644 index 000000000..6931c9717 --- /dev/null +++ b/e2e/components/header/header.ts @@ -0,0 +1,34 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ElementFinder, by } from 'protractor'; +import { Component } from '../component'; +import { UserInfo } from './user-info'; + +export class Header extends Component { + private locators = { + logoLink: by.css('.mdl-layout-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 100644 index 000000000..dcab9c9fb --- /dev/null +++ b/e2e/components/header/user-info.ts @@ -0,0 +1,56 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ElementFinder, ElementArrayFinder, element, 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('[md-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 { + const { menu, avatar } = this; + + return avatar.click() + .then(() => menu.wait()) + .then(() => menu); + } + + get name(): Promise { + return this.fullName.getText(); + } + + signOut(): Promise { + return this.openMenu() + .then(menu => { + menu.clickMenuItem('Sign out'); + }); + } +} diff --git a/e2e/components/login/login.ts b/e2e/components/login/login.ts new file mode 100644 index 000000000..d393b583f --- /dev/null +++ b/e2e/components/login/login.ts @@ -0,0 +1,67 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { by, ElementFinder } from 'protractor'; +import { Component } from '../component'; + +export class LoginComponent extends Component { + static selector = 'alfresco-login'; + + private locators = { + usernameInput: by.css('input#username'), + passwordInput: by.css('input#password'), + submitButton: by.css('button#login-button'), + errorMessage: by.css('.login-error-message') + }; + + usernameInput: ElementFinder = this.component.element(this.locators.usernameInput); + passwordInput: ElementFinder = this.component.element(this.locators.passwordInput); + submitButton: ElementFinder = this.component.element(this.locators.submitButton); + errorMessage: ElementFinder = this.component.element(this.locators.errorMessage); + + constructor(ancestor?: ElementFinder) { + super(LoginComponent.selector, ancestor); + } + + enterUsername(username: string): LoginComponent { + const { usernameInput } = this; + + usernameInput.clear(); + usernameInput.sendKeys(username); + + return this; + } + + enterPassword(password: string): LoginComponent { + const { passwordInput } = this; + + passwordInput.clear(); + passwordInput.sendKeys(password); + + return this; + } + + enterCredentials(username: string, password: string) { + this.enterUsername(username).enterPassword(password); + + return this; + } + + submit(): Promise { + return this.submitButton.click(); + } +} diff --git a/e2e/components/menu/menu.ts b/e2e/components/menu/menu.ts new file mode 100644 index 000000000..4051f998e --- /dev/null +++ b/e2e/components/menu/menu.ts @@ -0,0 +1,57 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ElementFinder, ElementArrayFinder, by, browser, ExpectedConditions as EC } from 'protractor'; +import { BROWSER_WAIT_TIMEOUT } from '../../configs'; +import { Component } from '../component'; + +export class Menu extends Component { + private static selectors = { + root: '.mat-menu-panel', + item: 'button.mat-menu-item' + }; + + items: ElementArrayFinder = this.component.all(by.css(Menu.selectors.item)); + + constructor(ancestor?: ElementFinder) { + super(Menu.selectors.root, ancestor); + } + + wait() { + return browser.wait(EC.visibilityOf(this.items.get(0)), BROWSER_WAIT_TIMEOUT); + } + + getNthItem(nth: number): ElementFinder { + return this.items.get(nth - 1); + } + + getItemByLabel(label: string): ElementFinder { + return this.component.element(by.cssContainingText(Menu.selectors.item, label)); + } + + getItemTooltip(label: string): Promise { + return this.getItemByLabel(label).getAttribute('title'); + } + + clicktNthItem(nth: number): Promise { + return this.getNthItem(nth).click(); + } + + clickMenuItem(label: string): Promise { + return this.getItemByLabel(label).click(); + } +} diff --git a/e2e/components/pagination/pagination.ts b/e2e/components/pagination/pagination.ts new file mode 100644 index 000000000..1a76878ff --- /dev/null +++ b/e2e/components/pagination/pagination.ts @@ -0,0 +1,67 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ElementFinder, ElementArrayFinder, promise, by, browser, ExpectedConditions as EC } from 'protractor'; +import { BROWSER_WAIT_TIMEOUT } from '../../configs'; +import { Menu } from '../menu/menu'; +import { Component } from '../component'; + +export class Pagination extends Component { + private static selectors = { + root: 'adf-pagination', + range: '.adf-pagination__range', + maxItems: '.adf-pagination__max-items', + currentPage: '.adf-pagination__current-page', + totalPages: '.adf-pagination__total-pages', + + previousButton: '.adf-pagination__previous-button', + nextButton: '.adf-pagination__next-button', + maxItemsButton: '.adf-pagination__max-items + button[md-icon-button]', + pagesButton: '.adf-pagination__current-page + button[md-icon-button]' + }; + + range: ElementFinder = this.component.element(by.css(Pagination.selectors.range)); + maxItems: ElementFinder = this.component.element(by.css(Pagination.selectors.maxItems)); + currentPage: ElementFinder = this.component.element(by.css(Pagination.selectors.currentPage)); + totalPages: ElementFinder = this.component.element(by.css(Pagination.selectors.totalPages)); + previousButton: ElementFinder = this.component.element(by.css(Pagination.selectors.previousButton)); + nextButton: ElementFinder = this.component.element(by.css(Pagination.selectors.nextButton)); + maxItemsButton: ElementFinder = this.component.element(by.css(Pagination.selectors.maxItemsButton)); + pagesButton: ElementFinder = this.component.element(by.css(Pagination.selectors.pagesButton)); + + menu: Menu = new Menu(); + + constructor(ancestor?: ElementFinder) { + super(Pagination.selectors.root, ancestor); + } + + openMaxItemsMenu(): Promise { + const { menu, maxItemsButton } = this; + + return maxItemsButton.click() + .then(() => menu.wait()) + .then(() => menu); + } + + openCurrentPageMenu(): Promise { + const { menu, pagesButton } = this; + + return this.pagesButton.click() + .then(() => menu.wait()) + .then(() => menu); + } +} diff --git a/e2e/components/sidenav/sidenav.ts b/e2e/components/sidenav/sidenav.ts new file mode 100644 index 000000000..38149b9d4 --- /dev/null +++ b/e2e/components/sidenav/sidenav.ts @@ -0,0 +1,63 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ElementFinder, ElementArrayFinder, by, promise } from 'protractor'; +import { Menu } from '../menu/menu'; +import { Component } from '../component'; + +export class Sidenav extends Component { + private static selectors = { + root: 'app-sidenav', + link: '.sidenav-menu__item-link', + activeLink: '.sidenav-menu__item-link--active', + newButton: '.sidenav__section--new__button' + }; + + links: ElementArrayFinder = this.component.all(by.css(Sidenav.selectors.link)); + activeLink: ElementFinder = this.component.element(by.css(Sidenav.selectors.activeLink)); + newButton: ElementArrayFinder = this.component.all(by.css(Sidenav.selectors.newButton)); + + menu: Menu = new Menu(); + + constructor(ancestor?: ElementFinder) { + super(Sidenav.selectors.root, ancestor); + } + + openNewMenu(): promise.Promise { + const { menu, newButton } = this; + + return newButton.click() + .then(() => menu.wait()) + .then(() => menu); + } + + isActiveByLabel(label: string): promise.Promise { + return this + .getLinkByLabel(label) + .getWebElement() + .then(element => element.getAttribute('class')) + .then(className => className.includes(Sidenav.selectors.activeLink.replace('.', ''))); + } + + getLinkByLabel(label: string): ElementFinder { + return this.component.element(by.cssContainingText(Sidenav.selectors.link, label)); + } + + navigateToLinkByLabel(label: string): promise.Promise { + return this.getLinkByLabel(label).click(); + } +} diff --git a/e2e/components/toolbar/toolbar-actions.ts b/e2e/components/toolbar/toolbar-actions.ts new file mode 100644 index 000000000..de2bd9409 --- /dev/null +++ b/e2e/components/toolbar/toolbar-actions.ts @@ -0,0 +1,56 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ElementFinder, ElementArrayFinder, by, promise } from 'protractor'; +import { Menu } from '../menu/menu'; +import { Component } from '../component'; + +export class ToolbarActions extends Component { + private static selectors = { + root: 'adf-toolbar', + button: '.mat-icon-button' + }; + + menu: Menu = new Menu(); + buttons: ElementArrayFinder = this.component.all(by.css(ToolbarActions.selectors.button)); + + constructor(ancestor?: ElementFinder) { + super(ToolbarActions.selectors.root, ancestor); + } + + isEmpty(): Promise { + return this.buttons.count().then(count => (count === 0)); + } + + getButtonByLabel(label: string): ElementFinder { + return this.component.element(by.cssContainingText(ToolbarActions.selectors.button, label)); + } + + getButtonByTitleAttribute(title: string): ElementFinder { + return this.component.element(by.css(`${ToolbarActions.selectors.button}[title="${title}"]`)); + } + + openMoreMenu(): promise.Promise { + const { menu } = this; + const moreButton = this.getButtonByTitleAttribute('More actions'); + + return moreButton + .click() + .then(() => menu.wait()) + .then(() => menu); + } +} diff --git a/e2e/components/toolbar/toolbar-breadcrumb.ts b/e2e/components/toolbar/toolbar-breadcrumb.ts new file mode 100644 index 000000000..8eac821f2 --- /dev/null +++ b/e2e/components/toolbar/toolbar-breadcrumb.ts @@ -0,0 +1,37 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ElementFinder, ElementArrayFinder, by } from 'protractor'; +import { Menu } from '../menu/menu'; +import { Component } from '../component'; + +export class ToolbarBreadcrumb extends Component { + private static selectors = { + root: 'adf-breadcrumb', + item: '.adf-breadcrumb-item' + }; + + items: ElementArrayFinder = this.component.all(by.css(ToolbarBreadcrumb.selectors.item)); + + constructor(ancestor?: ElementFinder) { + super(ToolbarBreadcrumb.selectors.root, ancestor); + } + + getNthItem(nth: number): ElementFinder { + return this.items.get(nth - 1); + } +} diff --git a/e2e/components/toolbar/toolbar.ts b/e2e/components/toolbar/toolbar.ts new file mode 100644 index 000000000..e33a93251 --- /dev/null +++ b/e2e/components/toolbar/toolbar.ts @@ -0,0 +1,34 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ElementFinder, ElementArrayFinder, by } from 'protractor'; +import { Component } from '../component'; +import { ToolbarActions } from './toolbar-actions'; +import { ToolbarBreadcrumb } from './toolbar-breadcrumb'; + +export class Toolbar extends Component { + private static selectors = { + root: '.inner-layout__header' + }; + + actions: ToolbarActions = new ToolbarActions(this.component); + breadcrumb: ToolbarBreadcrumb = new ToolbarBreadcrumb(this.component); + + constructor(ancestor?: ElementFinder) { + super(Toolbar.selectors.root, ancestor); + } +} diff --git a/e2e/configs.ts b/e2e/configs.ts new file mode 100644 index 000000000..6c179fcd4 --- /dev/null +++ b/e2e/configs.ts @@ -0,0 +1,53 @@ +export const BROWSER_RESOLUTION_WIDTH = 1200; +export const BROWSER_RESOLUTION_HEIGHT = 800; + +export const BROWSER_WAIT_TIMEOUT = 10000; + +// Application configs +export const APP_HOST = 'http://localhost:3000'; + +// Repository configs +export const REPO_API_HOST = 'http://localhost:8080'; +export const REPO_API_TENANT = '-default-'; + +// Admin details +export const ADMIN_USERNAME = 'admin'; +export const ADMIN_PASSWORD = 'admin'; +export const ADMIN_FULL_NAME = 'Administrator'; + +// Application Routes +export const APP_ROUTES = { + FAVORITES: '/favorites', + FILE_LIBRARIES: '/libraries', + LOGIN: '/login', + LOGOUT: '/logout', + PERSONAL_FILES: '/personal-files', + RECENT_FILES: '/recent-files', + SHARED_FILES: '/shared', + TRASHCAN: '/trashcan' +}; + +// Sidebar labels +export const SIDEBAR_LABELS = { + PERSONAL_FILES: 'Personal Files', + FILE_LIBRARIES: 'File Libraries', + SHARED_FILES: 'Shared', + RECENT_FILES: 'Recent Files', + FAVORITES: 'Favorites', + TRASH: 'Trash' +}; + +// Site visibility +export const SITE_VISIBILITY = { + PUBLIC: 'PUBLIC', + MODERATED: 'MODERATED', + PRIVATE: 'PRIVATE' +}; + +// Site roles +export const SITE_ROLES = { + SITE_CONSUMER: 'SiteConsumer', + SITE_COLLABORATOR: 'SiteCollaborator', + SITE_CONTRIBUTOR: 'SiteContributor', + SITE_MANAGER: 'SiteManager' +}; diff --git a/e2e/pages/browsing-page.ts b/e2e/pages/browsing-page.ts new file mode 100644 index 000000000..8d57e9553 --- /dev/null +++ b/e2e/pages/browsing-page.ts @@ -0,0 +1,31 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Header, DataTable, Pagination, Toolbar, Sidenav } from '../components/components'; +import { Page } from './page'; + +export class BrowsingPage extends Page { + header = new Header(this.app); + sidenav = new Sidenav(this.app); + toolbar = new Toolbar(this.app); + dataTable = new DataTable(this.app); + pagination = new Pagination(this.app); + + signOut(): Promise { + return this.header.userInfo.signOut(); + } +} diff --git a/e2e/pages/login-page.ts b/e2e/pages/login-page.ts new file mode 100644 index 000000000..f50527965 --- /dev/null +++ b/e2e/pages/login-page.ts @@ -0,0 +1,54 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { browser, ExpectedConditions as EC } from 'protractor'; +import { LoginComponent } from '../components/components'; +import { Page } from './page'; + +import { + ADMIN_USERNAME, + ADMIN_PASSWORD, + BROWSER_WAIT_TIMEOUT, + APP_ROUTES +} from '../configs'; + +export class LoginPage extends Page { + login: LoginComponent = new LoginComponent(this.app); + + /** @override */ + constructor() { + super(APP_ROUTES.LOGIN); + } + + /** @override */ + load(): Promise { + return super.load().then(() => { + const { submitButton } = this.login; + const hasSumbitButton = EC.presenceOf(submitButton); + + return browser.wait(hasSumbitButton, BROWSER_WAIT_TIMEOUT); + }); + } + + loginWith(username: string, password: string): Promise { + return this.login.enterCredentials(username, password).submit(); + } + + loginWithAdmin(): Promise { + return this.loginWith(ADMIN_USERNAME, ADMIN_PASSWORD); + } +} diff --git a/e2e/pages/logout-page.ts b/e2e/pages/logout-page.ts new file mode 100644 index 000000000..e600ce52e --- /dev/null +++ b/e2e/pages/logout-page.ts @@ -0,0 +1,31 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Page } from './page'; +import { APP_ROUTES } from '../configs'; + +export class LogoutPage extends Page { + /** @override */ + constructor() { + super(APP_ROUTES.LOGOUT); + } + + /** @override */ + load(): Promise { + return super.load(); + } +} diff --git a/e2e/pages/page.ts b/e2e/pages/page.ts new file mode 100644 index 000000000..19c073c74 --- /dev/null +++ b/e2e/pages/page.ts @@ -0,0 +1,59 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { browser, element, by, ElementFinder } from 'protractor'; + +export abstract class Page { + private static USE_HASH_STRATEGY: boolean = true; + + private locators = { + app: by.css('alfresco-content-app'), + overlay: by.css('.cdk-overlay-container'), + snackBar: by.css('simple-snack-bar') + }; + + public app: ElementFinder = element(this.locators.app); + public overlay: ElementFinder = element(this.locators.overlay); + public snackBar: ElementFinder = element(this.locators.snackBar); + + constructor(public url: string = '') {} + + get title(): Promise { + return browser.getTitle(); + } + + load(relativeUrl: string = ''): Promise { + const hash = Page.USE_HASH_STRATEGY ? '/#' : ''; + const path = `${hash}${this.url}${relativeUrl}`; + + return browser.get(path); + } + + refresh(): Promise { + return browser.refresh(); + } + + isSnackBarDisplayed(): Promise { + return this.snackBar.isDisplayed(); + } + + getSnackBarMessage(): Promise { + return this.isSnackBarDisplayed() + .then(() => this.snackBar.getText()) + .catch(() => ''); + } +} diff --git a/e2e/pages/pages.ts b/e2e/pages/pages.ts new file mode 100644 index 000000000..4e1dbbc46 --- /dev/null +++ b/e2e/pages/pages.ts @@ -0,0 +1,20 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './browsing-page'; +export * from './login-page'; +export * from './logout-page'; diff --git a/e2e/suites/actions/create-folder.test.ts b/e2e/suites/actions/create-folder.test.ts new file mode 100644 index 000000000..27d5f37de --- /dev/null +++ b/e2e/suites/actions/create-folder.test.ts @@ -0,0 +1,277 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { protractor, browser, by, ElementFinder } from 'protractor'; + +import { APP_ROUTES, BROWSER_WAIT_TIMEOUT } from '../../configs'; +import { LoginPage, LogoutPage, BrowsingPage } from '../../pages/pages'; +import { CreateOrEditFolderDialog } from '../../components/dialog/create-edit-folder-dialog'; +import { LocalStorageUtility } from '../../utilities/local-storage'; +import { RepoClient, NodeContentTree } from '../../utilities/repo-client/repo-client'; + +describe('Create folder', () => { + const username = 'jane.doe'; + const password = 'jane.doe'; + + const folderName1 = 'my-folder1'; + const folderName2 = 'my-folder2'; + const folderDescription = 'description of my folder'; + const duplicateFolderName = 'duplicate-folder-name'; + const nameWithSpaces = ' folder name '; + + const apis = { + admin: new RepoClient(), + user: new RepoClient(username, password) + }; + + const loginPage = new LoginPage(); + const logoutPage = new LogoutPage(); + const personalFilesPage = new BrowsingPage(APP_ROUTES.PERSONAL_FILES); + const createDialog = new CreateOrEditFolderDialog(); + const dataTable = personalFilesPage.dataTable; + + function openCreateDialog(): any { + return personalFilesPage.sidenav + .openNewMenu() + .then((menu) => { + menu.clickMenuItem('Create folder'); + }) + .then(() => createDialog.waitForDialogToOpen()); + } + + beforeAll(done => { + apis.admin.people.createUser(username, password) + .then(() => apis.user.nodes.createFolders([ duplicateFolderName ])) + .then(() => loginPage.load()) + .then(() => loginPage.loginWith(username, password)) + .then(done); + }); + + beforeEach(done => { + personalFilesPage.load() + .then(() => dataTable.waitForHeader()) + .then(done); + }); + + afterEach(done => { + browser.$('body').sendKeys(protractor.Key.ESCAPE).then(done); + }); + + afterAll(done => { + Promise + .all([ + apis.user.nodes.deleteNodes([ + folderName1, + folderName2, + duplicateFolderName, + nameWithSpaces.trim() + ]), + logoutPage.load() + .then(() => LocalStorageUtility.clear()) + ]) + .then(done); + }); + + it('option is enabled when having enough permissions', () => { + personalFilesPage.sidenav.openNewMenu() + .then((menu) => { + const isEnabled = menu.getItemByLabel('Create folder').getWebElement().isEnabled(); + + expect(isEnabled).toBe(true, 'Create folder is not enabled'); + }); + }); + + it('creates new folder with name', () => { + openCreateDialog() + .then(() => createDialog.enterName(folderName1).clickCreate()) + .then(() => createDialog.waitForDialogToClose()) + .then(() => dataTable.waitForHeader()) + .then(() => { + const isPresent = dataTable.getRowByContainingText(folderName1).isPresent(); + expect(isPresent).toBe(true, 'Folder not displayed in list view'); + }); + }); + + it('creates new folder with name and description', () => { + openCreateDialog() + .then(() => { + createDialog + .enterName(folderName2) + .enterDescription(folderDescription) + .clickCreate(); + }) + .then(() => createDialog.waitForDialogToClose()) + .then(() => dataTable.waitForHeader()) + .then(() => { + const isPresent = dataTable.getRowByContainingText(folderName2).isPresent(); + expect(isPresent).toBe(true, 'Folder not displayed in list view'); + }) + .then(() => { + apis.user.nodes.getNodeDescription(folderName2) + .then((description) => { + expect(description).toEqual(folderDescription, 'Description is not correct'); + }); + }); + }); + + it('enabled option tooltip', () => { + personalFilesPage.sidenav.openNewMenu() + .then(menu => { + const action = browser.actions().mouseMove(menu.getItemByLabel('Create folder')); + action.perform(); + + return menu; + }) + .then((menu) => { + const tooltip = menu.getItemTooltip('Create folder'); + expect(tooltip).toContain('Create new folder'); + }); + }); + + it('option is disabled when not enough permissions', () => { + // refactor after implementing Breadcrumb automation component + const breadcrumbRoot: ElementFinder = protractor.element(by.css('.adf-breadcrumb-item[title="User Homes"]')); + + browser.actions().mouseMove(breadcrumbRoot).click().perform() + .then(() => personalFilesPage.sidenav.openNewMenu()) + .then(menu => { + const isEnabled = menu.getItemByLabel('Create folder').getWebElement().isEnabled(); + expect(isEnabled).toBe(false, 'Create folder is not disabled'); + }); + }); + + it('disabled option tooltip', () => { + // refactor after implementing Breadcrumb automation component + const breadcrumbRoot: ElementFinder = protractor.element(by.css('.adf-breadcrumb-item[title="User Homes"]')); + + browser.actions().mouseMove(breadcrumbRoot).click().perform() + .then(() => personalFilesPage.sidenav.openNewMenu()) + .then(menu => { + const action = browser.actions().mouseMove(menu.getItemByLabel('Create folder')); + action.perform() + .then(() => { + const tooltip = menu.getItemTooltip('Create folder'); + expect(tooltip).toContain(`You can't create a folder here`); + }); + }); + }); + + it('dialog UI elements', () => { + openCreateDialog().then(() => { + const dialogTitle = createDialog.getTitle(); + const isFolderNameDisplayed = createDialog.nameInput.getWebElement().isDisplayed(); + const isDescriptionDisplayed = createDialog.descriptionTextArea.getWebElement().isDisplayed(); + const isCreateEnabled = createDialog.createButton.getWebElement().isEnabled(); + const isCancelEnabled = createDialog.cancelButton.getWebElement().isEnabled(); + + expect(dialogTitle).toBe('Create new folder'); + expect(isFolderNameDisplayed).toBe(true, 'Name input is not displayed'); + expect(isDescriptionDisplayed).toBe(true, 'Description field is not displayed'); + expect(isCreateEnabled).toBe(false, 'Create button is not disabled'); + expect(isCancelEnabled).toBe(true, 'Cancel button is not enabled'); + }); + }); + + it('with empty folder name', () => { + openCreateDialog() + .then(() => { + createDialog.deleteNameWithBackspace(); + }) + .then(() => { + const isCreateEnabled = createDialog.createButton.getWebElement().isEnabled(); + const validationMessage = createDialog.getValidationMessage(); + + expect(isCreateEnabled).toBe(false, 'Create button is enabled'); + expect(validationMessage).toMatch('Folder name is required'); + }); + }); + + it('with folder name ending with a dot "."', () => { + openCreateDialog() + .then(() => createDialog.enterName('folder-name.')) + .then((dialog) => { + const isCreateEnabled = dialog.createButton.getWebElement().isEnabled(); + const validationMessage = dialog.getValidationMessage(); + + expect(isCreateEnabled).toBe(false, 'Create button is not disabled'); + expect(validationMessage).toMatch(`Folder name can't end with a period .`); + }); + }); + + it('with folder name containing special characters', () => { + const namesWithSpecialChars = [ 'a*a', 'a"a', 'aa', `a\\a`, 'a/a', 'a?a', 'a:a', 'a|a' ]; + + openCreateDialog() + .then(() => { + namesWithSpecialChars.forEach(name => { + createDialog.enterName(name); + + const isCreateEnabled = createDialog.createButton.getWebElement().isEnabled(); + const validationMessage = createDialog.getValidationMessage(); + + expect(isCreateEnabled).toBe(false, 'Create button is not disabled'); + expect(validationMessage).toContain(`Folder name can't contain these characters`); + }); + }); + }); + + it('with folder name containing only spaces', () => { + openCreateDialog() + .then(() => createDialog.enterName(' ')) + .then((dialog) => { + const isCreateEnabled = dialog.createButton.getWebElement().isEnabled(); + const validationMessage = dialog.getValidationMessage(); + + expect(isCreateEnabled).toBe(false, 'Create button is not disabled'); + expect(validationMessage).toMatch(`Folder name can't contain only spaces`); + }); + }); + + it('cancel folder creation', () => { + openCreateDialog() + .then(() => { + createDialog + .enterName('test') + .enterDescription('test description') + .clickCancel(); + }) + .then(() => expect(createDialog.component.isPresent()).not.toBe(true, 'dialog is not closed')); + }); + + it('duplicate folder name', () => { + openCreateDialog() + .then(() => createDialog.enterName(duplicateFolderName).clickCreate()) + .then(() => { + personalFilesPage.getSnackBarMessage() + .then(message => { + expect(message).toEqual(`There's already a folder with this name. Try a different name.`); + expect(createDialog.component.isPresent()).toBe(true, 'dialog is not present'); + }); + }); + }); + + it('trim ending spaces from folder name', () => { + openCreateDialog() + .then(() => createDialog.enterName(nameWithSpaces).clickCreate()) + .then(() => createDialog.waitForDialogToClose()) + .then(() => dataTable.waitForHeader()) + .then(() => { + const isPresent = dataTable.getRowByContainingText(nameWithSpaces.trim()).isPresent(); + expect(isPresent).toBe(true, 'Folder not displayed in list view'); + }); + }); +}); diff --git a/e2e/suites/actions/edit-folder.test.ts b/e2e/suites/actions/edit-folder.test.ts new file mode 100644 index 000000000..e3c7b8e91 --- /dev/null +++ b/e2e/suites/actions/edit-folder.test.ts @@ -0,0 +1,211 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { protractor, element, browser, by, ElementFinder } from 'protractor'; +import { LoginPage, LogoutPage, BrowsingPage } from '../../pages/pages'; +import { APP_ROUTES, SITE_VISIBILITY, SITE_ROLES } from '../../configs'; +import { RepoClient } from '../../utilities/repo-client/repo-client'; +import { CreateOrEditFolderDialog } from '../../components/dialog/create-edit-folder-dialog'; +import { LocalStorageUtility } from '../../utilities/local-storage'; + +describe('Edit folder', () => { + const username = 'jane.doe'; + const password = 'jane.doe'; + + const folderName = 'my-folder'; + const folderDescription = 'my folder description'; + + const folderNameToEdit = 'folder-to-be-edited'; + const duplicateFolderName = 'duplicate-folder-name'; + + const folderNameEdited = 'edited-folder'; + const folderDescriptionEdited = 'my folder description edited'; + + const siteName = 'site-private'; + + const apis = { + admin: new RepoClient(), + user: new RepoClient(username, password) + }; + + const loginPage = new LoginPage(); + const logoutPage = new LogoutPage(); + const personalFilesPage = new BrowsingPage(); + const editDialog = new CreateOrEditFolderDialog(); + const dataTable = personalFilesPage.dataTable; + const editButton = personalFilesPage.toolbar.actions.getButtonByTitleAttribute('Edit'); + + beforeAll(done => { + Promise + .all([ + apis.admin.people.createUser(username, password), + apis.admin.sites.createSite(siteName, SITE_VISIBILITY.PRIVATE) + .then(() => apis.admin.nodes.createFolders([ folderName ], `Sites/${siteName}/documentLibrary`)) + ]) + .then(() => apis.admin.sites.addSiteMember(siteName, username, SITE_ROLES.SITE_CONSUMER)) + .then(() => Promise.all([ + apis.user.nodes.createNodeWithProperties( folderName, '', folderDescription ), + apis.user.nodes.createFolders([ folderNameToEdit, duplicateFolderName ]), + loginPage.load() + ])) + .then(() => loginPage.loginWith(username, password)) + .then(done); + }); + + beforeEach(done => { + personalFilesPage.load() + .then(() => dataTable.waitForHeader()) + .then(done); + }); + + afterEach(done => { + browser.$('body').sendKeys(protractor.Key.ESCAPE).then(done); + }); + + afterAll(done => { + Promise + .all([ + apis.admin.sites.deleteSite(siteName, true), + apis.user.nodes.deleteNodes([ folderName, folderNameEdited, duplicateFolderName ]), + logoutPage.load() + .then(() => LocalStorageUtility.clear()) + ]) + .then(done); + }); + + it('button is enabled when having permissions', () => { + dataTable.clickOnRowByContainingText(folderName) + .then(() => { + expect(editButton.isEnabled()).toBe(true); + }); + }); + + it('dialog UI defaults', () => { + dataTable.clickOnRowByContainingText(folderName) + .then(() => editButton.click()) + .then(() => { + expect(editDialog.getTitle()).toBe('Edit folder'); + expect(editDialog.nameInput.getWebElement().getAttribute('value')).toBe(folderName); + expect(editDialog.descriptionTextArea.getWebElement().getAttribute('value')).toBe(folderDescription); + expect(editDialog.updateButton.getWebElement().isEnabled()).toBe(true, 'upload button is not enabled'); + expect(editDialog.cancelButton.getWebElement().isEnabled()).toBe(true, 'cancel button is not enabled'); + }); + }); + + it('folder properties are modified when pressing OK', () => { + dataTable.clickOnRowByContainingText(folderNameToEdit) + .then(() => editButton.click()) + .then(() => { + editDialog + .enterName(folderNameEdited) + .enterDescription(folderDescriptionEdited) + .clickUpdate(); + }) + .then(() => editDialog.waitForDialogToClose()) + .then(() => dataTable.waitForHeader()) + .then(() => { + const isPresent = dataTable.getRowByContainingText(folderNameEdited).isPresent(); + expect(isPresent).toBe(true, 'Folder not displayed in list view'); + }) + .then(() => { + apis.user.nodes.getNodeDescription(folderNameEdited) + .then((description) => { + expect(description).toEqual(folderDescriptionEdited); + }); + }); + }); + + it('button is not displayed when not enough permissions', () => { + const fileLibrariesPage = new BrowsingPage(); + + fileLibrariesPage.sidenav.navigateToLinkByLabel('File Libraries') + .then(() => fileLibrariesPage.dataTable.doubleClickOnRowByContainingText(siteName)) + .then(() => fileLibrariesPage.dataTable.clickOnRowByContainingText(folderName)) + .then(() => { + expect(editButton.isPresent()).not.toBe(true, 'edit button is displayed'); + }); + }); + + it('with empty folder name', () => { + dataTable.clickOnRowByContainingText(folderName) + .then(() => editButton.click()) + .then(() => { + editDialog.deleteNameWithBackspace(); + }) + .then(() => { + expect(editDialog.updateButton.getWebElement().isEnabled()).toBe(false, 'upload button is not enabled'); + expect(editDialog.getValidationMessage()).toMatch('Folder name is required'); + }); + }); + + it('with name with special characters', () => { + const namesWithSpecialChars = [ 'a*a', 'a"a', 'aa', `a\\a`, 'a/a', 'a?a', 'a:a', 'a|a' ]; + + dataTable.clickOnRowByContainingText(folderName) + .then(() => editButton.click()) + .then(() => { + namesWithSpecialChars.forEach(name => { + editDialog.enterName(name); + + expect(editDialog.updateButton.getWebElement().isEnabled()).toBe(false, 'upload button is not disabled'); + expect(editDialog.getValidationMessage()).toContain(`Folder name can't contain these characters`); + }); + }); + }); + + it('with name ending with a dot', () => { + dataTable.clickOnRowByContainingText(folderName) + .then(() => editButton.click()) + .then(() => editDialog.nameInput.sendKeys('.')) + .then(() => { + expect(editDialog.updateButton.getWebElement().isEnabled()).toBe(false, 'upload button is not enabled'); + expect(editDialog.getValidationMessage()).toMatch(`Folder name can't end with a period .`); + }); + }); + + it('Cancel button', () => { + dataTable.clickOnRowByContainingText(folderName) + .then(() => editButton.click()) + .then(() => editDialog.clickCancel()) + .then(() => expect(editDialog.component.isPresent()).not.toBe(true, 'dialog is not closed')); + }); + + it('with duplicate folder name', () => { + dataTable.clickOnRowByContainingText(folderName) + .then(() => editButton.click()) + .then(() => editDialog.enterName(duplicateFolderName).clickUpdate()) + .then(() => { + personalFilesPage.getSnackBarMessage() + .then(message => { + expect(message).toEqual(`There's already a folder with this name. Try a different name.`); + expect(editDialog.component.isPresent()).toBe(true, 'dialog is not present'); + }); + }); + }); + + it('trim ending spaces', () => { + dataTable.clickOnRowByContainingText(folderName) + .then(() => editButton.click()) + .then(() => editDialog.nameInput.sendKeys(' ')) + .then(() => editDialog.clickUpdate()) + .then(() => editDialog.waitForDialogToClose()) + .then(() => { + expect(personalFilesPage.snackBar.isPresent()).not.toBe(true, 'notification appears'); + expect(dataTable.getRowByContainingText(folderName).isPresent()).toBe(true, 'Folder not displayed in list view'); + }); + }); +}); diff --git a/e2e/suites/application/page-titles.test.ts b/e2e/suites/application/page-titles.test.ts new file mode 100644 index 000000000..b9085a6f7 --- /dev/null +++ b/e2e/suites/application/page-titles.test.ts @@ -0,0 +1,126 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { browser } from 'protractor'; + +import { SIDEBAR_LABELS } from '../../configs'; +import { LoginPage, LogoutPage, BrowsingPage } from '../../pages/pages'; +import { LocalStorageUtility } from '../../utilities/local-storage'; + +describe('Page titles', () => { + const loginPage = new LoginPage(); + const logoutPage = new LogoutPage(); + const page = new BrowsingPage(); + + xit(''); + + describe('on Login / Logout pages', () => { + it('on Login page', () => { + loginPage.load() + .then(() => { + expect(browser.getTitle()).toContain('Sign in'); + }); + }); + + it('after logout', () => { + loginPage.load() + .then(() => loginPage.loginWithAdmin()) + .then(() => page.signOut()) + .then(() => { + expect(browser.getTitle()).toContain('Sign in'); + }); + }); + + it('when pressing Back after Logout', () => { + loginPage.load() + .then(() => loginPage.loginWithAdmin()) + .then(() => page.signOut()) + .then(() => browser.driver.navigate().back()) + .then(() => { + expect(browser.getTitle()).toContain('Sign in'); + }); + }); + }); + + describe('on list views', () => { + beforeAll(done => { + loginPage.load() + .then(() => loginPage.loginWithAdmin()) + .then(done); + }); + + afterAll(done => { + logoutPage.load() + .then(() => LocalStorageUtility.clear()) + .then(done); + }); + + it('Personal Files page', () => { + const label = SIDEBAR_LABELS.PERSONAL_FILES; + + page.sidenav.navigateToLinkByLabel(label) + .then(() => { + expect(browser.getTitle()).toContain(label); + }); + }); + + it('File Libraries page', () => { + const label = SIDEBAR_LABELS.FILE_LIBRARIES; + + page.sidenav.navigateToLinkByLabel(label) + .then(() => { + expect(browser.getTitle()).toContain(label); + }); + }); + + it('Shared Files page', () => { + const label = SIDEBAR_LABELS.SHARED_FILES; + + page.sidenav.navigateToLinkByLabel(label) + .then(() => { + expect(browser.getTitle()).toContain(label); + }); + }); + + it('Recent Files page', () => { + const label = SIDEBAR_LABELS.RECENT_FILES; + + page.sidenav.navigateToLinkByLabel(label) + .then(() => { + expect(browser.getTitle()).toContain(label); + }); + }); + + it('Favorites page', () => { + const label = SIDEBAR_LABELS.FAVORITES; + + page.sidenav.navigateToLinkByLabel(label) + .then(() => { + expect(browser.getTitle()).toContain(label); + }); + }); + + it('Trash page', () => { + const label = SIDEBAR_LABELS.TRASH; + + page.sidenav.navigateToLinkByLabel(label) + .then(() => { + expect(browser.getTitle()).toContain(label); + }); + }); + }); +}); diff --git a/e2e/suites/authentication/login.test.ts b/e2e/suites/authentication/login.test.ts new file mode 100644 index 000000000..922386dad --- /dev/null +++ b/e2e/suites/authentication/login.test.ts @@ -0,0 +1,178 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { browser } from 'protractor'; + +import { APP_ROUTES } from '../../configs'; +import { LoginPage, LogoutPage, BrowsingPage } from '../../pages/pages'; +import { LocalStorageUtility } from '../../utilities/local-storage'; +import { RepoClient } from '../../utilities/repo-client/repo-client'; + +describe('Login', () => { + const peopleApi = new RepoClient().people; + const loginPage = new LoginPage(); + const logoutPage = new LogoutPage(); + + const testUser = { + username: 'test.user@alfness', + password: 'test.user' + }; + + const russianUser = { + username: 'пользователь', + password: '密碼中國' + }; + + const johnDoe = { + username: 'john.doe', + password: 'john.doe', + firstName: 'John', + lastName: 'Doe' + }; + + beforeAll(done => { + Promise + .all([ + peopleApi.createUser(testUser.username, testUser.password), + peopleApi.createUser(russianUser.username, russianUser.password), + peopleApi.createUser(johnDoe.username, johnDoe.password, { + firstName: johnDoe.firstName, + lastName: johnDoe.lastName + }) + ]) + .then(done); + }); + + beforeEach(done => { + loginPage.load().then(done); + }); + + afterEach(done => { + logoutPage.load() + .then(() => LocalStorageUtility.clear()) + .then(done); + }); + + xit(''); + + describe('with valid credentials', () => { + it('navigate to "Personal Files"', () => { + const { username, password } = johnDoe; + + loginPage + .loginWith(username, password) + .then(() => { + expect(browser.getCurrentUrl()).toContain(APP_ROUTES.PERSONAL_FILES); + }); + }); + + it(`displays user's name in header`, () => { + const { userInfo } = new BrowsingPage(APP_ROUTES.PERSONAL_FILES).header; + const { username, password, firstName, lastName } = johnDoe; + + loginPage + .loginWith(username, password) + .then(() => { + expect(userInfo.name).toEqual(`${firstName} ${lastName}`); + }); + }); + + it(`logs in with user having username containing "@"`, () => { + const { username, password } = testUser; + + loginPage + .loginWith(username, password) + .then(() => { + expect(browser.getCurrentUrl()).toContain(APP_ROUTES.PERSONAL_FILES); + }); + }); + + it('logs in with user with non-latin characters', () => { + const { username, password } = russianUser; + + loginPage + .loginWith(username, password) + .then(() => { + expect(browser.getCurrentUrl()).toContain(APP_ROUTES.PERSONAL_FILES); + }); + }); + + it('redirects to Home Page when navigating to the Login page while already logged in', () => { + const { username, password } = johnDoe; + + loginPage + .loginWith(username, password) + .then(() => browser.get(APP_ROUTES.LOGIN) + .then(() => { + expect(browser.getCurrentUrl()).toContain(APP_ROUTES.PERSONAL_FILES); + }) + ); + }); + + it('redirects to Personal Files when pressing browser Back while already logged in ', () => { + const { username, password } = johnDoe; + + loginPage + .loginWith(username, password) + .then(() => browser.driver.navigate().back()) + .then(() => { + expect(browser.getCurrentUrl()).toContain(APP_ROUTES.PERSONAL_FILES); + }); + }); + }); + + describe('with invalid credentials', () => { + const { login: loginComponent } = loginPage; + const { submitButton, errorMessage } = loginComponent; + + it('disabled submit button when no credentials are entered', () => { + expect(submitButton.isEnabled()).toBe(false); + }); + + it('disabled submit button when password is empty', () => { + loginComponent.enterUsername('any-username'); + + expect(submitButton.isEnabled()).toBe(false); + }); + + it('disabled submit button when username is empty', () => { + loginPage.login.enterPassword('any-password'); + + expect(submitButton.isEnabled()).toBe(false); + }); + + it('shows error when entering nonexistent user', () => { + loginPage + .loginWith('nonexistent-user', 'any-password') + .then(() => { + expect(browser.getCurrentUrl()).toContain(APP_ROUTES.LOGIN); + expect(errorMessage.isDisplayed()).toBe(true); + }); + }); + + it('shows error when entering invalid password', () => { + const { username } = johnDoe; + + loginPage + .loginWith(username, 'incorrect-password') + .then(() => { + expect(browser.getCurrentUrl()).toContain(APP_ROUTES.LOGIN); + expect(errorMessage.isDisplayed()).toBe(true); + }); + }); + }); +}); diff --git a/e2e/suites/authentication/logout.test.ts b/e2e/suites/authentication/logout.test.ts new file mode 100644 index 000000000..050e2913a --- /dev/null +++ b/e2e/suites/authentication/logout.test.ts @@ -0,0 +1,76 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { browser } from 'protractor'; + +import { APP_ROUTES, BROWSER_WAIT_TIMEOUT } from '../../configs'; +import { LoginPage, LogoutPage, BrowsingPage } from '../../pages/pages'; +import { LocalStorageUtility } from '../../utilities/local-storage'; +import { RepoClient } from '../../utilities/repo-client/repo-client'; + +describe('Logout', () => { + const page = new BrowsingPage(); + const loginPage = new LoginPage(); + const logoutPage = new LogoutPage(); + + const peopleApi = new RepoClient().people; + + const johnDoe = { + username: 'john.doe', + password: 'john.doe' + }; + + beforeAll((done) => { + peopleApi + .createUser(johnDoe.username, johnDoe.password) + .then(done); + }); + + beforeEach((done) => { + loginPage.load() + .then(() => loginPage.loginWith(johnDoe.username, johnDoe.password)) + .then(done); + }); + + afterEach((done) => { + logoutPage.load() + .then(() => LocalStorageUtility.clear()) + .then(done); + }); + + it('redirects to Login page, on sign out', () => { + page.signOut() + .then(() => { + expect(browser.getCurrentUrl()).toContain(APP_ROUTES.LOGIN); + }); + }); + + xit('redirects to Login page when logging out by URL', () => { + browser.get(APP_ROUTES.LOGOUT) + .then(() => { + expect(browser.getCurrentUrl()).toContain(APP_ROUTES.LOGIN); + }); + }); + + it('redirects to Login page when pressing browser Back after logout', () => { + page.signOut() + .then(() => browser.driver.navigate().back()) + .then(() => { + expect(browser.getCurrentUrl()).toContain(APP_ROUTES.LOGIN); + }); + }); +}); diff --git a/e2e/suites/list-views/personal-files.test.ts b/e2e/suites/list-views/personal-files.test.ts new file mode 100644 index 000000000..891e729a6 --- /dev/null +++ b/e2e/suites/list-views/personal-files.test.ts @@ -0,0 +1,180 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { browser } from 'protractor'; + +import { APP_ROUTES } from '../../configs'; +import { LoginPage, LogoutPage, BrowsingPage } from '../../pages/pages'; +import { LocalStorageUtility } from '../../utilities/local-storage'; +import { RepoClient, NodeContentTree } from '../../utilities/repo-client/repo-client'; + +describe('Personal Files', () => { + const username = 'jane.doe'; + const password = 'jane.doe'; + + const apis = { + admin: new RepoClient(), + user: new RepoClient(username, password) + }; + + const loginPage = new LoginPage(); + const logoutPage = new LogoutPage(); + const personalFilesPage = new BrowsingPage(APP_ROUTES.PERSONAL_FILES); + const dataTable = personalFilesPage.dataTable; + + const adminContent: NodeContentTree = { + name: 'admin-folder' + }; + + const userContent: NodeContentTree = { + name: 'user-folder', + files: [ 'user-file.txt' ] + }; + + beforeAll(done => { + Promise + .all([ + apis.admin.people.createUser(username, password), + apis.admin.nodes.createContent(adminContent) + ]) + .then(() => apis.user.nodes.createContent(userContent)) + .then(done); + }); + + afterAll(done => { + Promise + .all([ + apis.admin.nodes.deleteNodes([ adminContent.name ]), + apis.user.nodes.deleteNodes([ userContent.name ]) + ]) + .then(done); + }); + + xit(''); + + describe(`Admin user's personal files`, () => { + beforeAll(done => { + loginPage.load() + .then(() => loginPage.loginWithAdmin()) + .then(done); + }); + + beforeEach(done => { + personalFilesPage.load() + .then(() => dataTable.waitForHeader()) + .then(done); + }); + + afterAll(done => { + logoutPage.load() + .then(() => LocalStorageUtility.clear()) + .then(done); + }); + + it('has "Data Dictionary" folder', () => { + expect(dataTable.getRowByContainingText('Data Dictionary').isPresent()).toBe(true); + }); + + it('has created content', () => { + expect(dataTable.getRowByContainingText('admin-folder').isPresent()).toBe(true); + }); + }); + + describe(`Regular user's personal files`, () => { + beforeAll(done => { + loginPage.load() + .then(() => loginPage.loginWith(username, password)) + .then(done); + }); + + beforeEach(done => { + personalFilesPage.load() + .then(() => dataTable.waitForHeader()) + .then(done); + }); + + afterAll(done => { + logoutPage.load() + .then(() => LocalStorageUtility.clear()) + .then(done); + }); + + it('has the right column count', () => { + expect(dataTable.getColumnHeaders().count()).toBe(5); + }); + + it('has the right columns', () => { + const labels = [ 'Name', 'Size', 'Modified', 'Modified by' ]; + const elements = labels.map(label => dataTable.getColumnHeaderByLabel(label)); + + elements.forEach((element, index) => { + expect(element.isPresent()).toBe(true, `"${labels[index]}" is missing`); + }); + }); + + it('has default sorted column', () => { + expect(dataTable.getSortedColumnHeader().getText()).toBe('Modified'); + }); + + it('has user created content', () => { + expect(dataTable.getRowByContainingText('user-folder').isPresent()) + .toBe(true); + }); + + it('navigates to folder', () => { + const getNodeIdPromise = apis.user.nodes + .getNodeByPath('/user-folder') + .then(response => response.data.entry.id); + + const navigatePromise = dataTable + .doubleClickOnRowByContainingText('user-folder') + .then(() => dataTable.waitForHeader()); + + Promise + .all([ + getNodeIdPromise, + navigatePromise + ]) + .then(([ nodeId ]) => { + expect(browser.getCurrentUrl()) + .toContain(nodeId, 'Node ID is not in the URL'); + + expect(dataTable.getRowByContainingText('user-file.txt').isPresent()) + .toBe(true, '"user-file.txt" is missing'); + }); + }); + + // Some tests regarding selection, breadcrumb and toolbar + // probably they can be move to a different suite + describe('Item selection', () => { + it('has toolbar when selected', done => { + const { actions } = personalFilesPage.toolbar; + + dataTable + .clickOnRowByContainingText('user-folder') + .then(() => { + expect(actions.isEmpty()).toBe(false, 'Toolbar to be present'); + }) + .then(() => actions.openMoreMenu()) + .then(menu => { + expect(menu.items.count()).toBeGreaterThan(0, 'More actions has items'); + }) + .then(done); + }); + }); + }); +}); diff --git a/e2e/suites/navigation/side-navigation.test.ts b/e2e/suites/navigation/side-navigation.test.ts new file mode 100644 index 000000000..93e6219fe --- /dev/null +++ b/e2e/suites/navigation/side-navigation.test.ts @@ -0,0 +1,124 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { browser } from 'protractor'; + +import { APP_ROUTES, SIDEBAR_LABELS } from '../../configs'; +import { LoginPage, LogoutPage, BrowsingPage } from '../../pages/pages'; +import { LocalStorageUtility } from '../../utilities/local-storage'; + +describe('Side navigation', () => { + const loginPage = new LoginPage(); + const logoutPage = new LogoutPage(); + const page = new BrowsingPage(); + + beforeAll(done => { + loginPage.load() + .then(() => loginPage.loginWithAdmin()) + .then(done); + }); + + beforeEach(done => { + page.dataTable.wait().then(done); + }); + + afterAll(done => { + logoutPage.load() + .then(() => LocalStorageUtility.clear()) + .then(done); + }); + + it('has "Personal Files" as default', () => { + expect(browser.getCurrentUrl()) + .toContain(APP_ROUTES.PERSONAL_FILES); + + expect(page.sidenav.isActiveByLabel('Personal Files')) + .toBe(true, 'Active link'); + }); + + it('navigates to "File Libraries"', () => { + const { sidenav, dataTable } = page; + + sidenav.navigateToLinkByLabel(SIDEBAR_LABELS.FILE_LIBRARIES) + .then(() => dataTable.wait()) + .then(() => browser.getCurrentUrl()) + .then(url => { + expect(url).toContain(APP_ROUTES.FILE_LIBRARIES); + expect(sidenav.isActiveByLabel(SIDEBAR_LABELS.FILE_LIBRARIES)).toBe(true); + }); + }); + + it('navigates to "Personal Files"', () => { + const { sidenav, dataTable } = page; + + sidenav.navigateToLinkByLabel(SIDEBAR_LABELS.PERSONAL_FILES) + .then(() => dataTable.wait()) + .then(() => browser.getCurrentUrl()) + .then(url => { + expect(url).toContain(APP_ROUTES.PERSONAL_FILES); + expect(sidenav.isActiveByLabel(SIDEBAR_LABELS.PERSONAL_FILES)).toBe(true); + }); + }); + + it('navigates to "Shared Files"', () => { + const { sidenav, dataTable } = page; + + sidenav.navigateToLinkByLabel(SIDEBAR_LABELS.SHARED_FILES) + .then(() => dataTable.wait()) + .then(() => browser.getCurrentUrl()) + .then(url => { + expect(url).toContain(APP_ROUTES.SHARED_FILES); + expect(sidenav.isActiveByLabel(SIDEBAR_LABELS.SHARED_FILES)).toBe(true); + }); + }); + + it('navigates to "Recent Files"', () => { + const { sidenav, dataTable } = page; + + sidenav.navigateToLinkByLabel(SIDEBAR_LABELS.RECENT_FILES) + .then(() => dataTable.wait()) + .then(() => browser.getCurrentUrl()) + .then(url => { + expect(url).toContain(APP_ROUTES.RECENT_FILES); + expect(sidenav.isActiveByLabel(SIDEBAR_LABELS.RECENT_FILES)).toBe(true); + }); + }); + + it('navigates to "Favorites"', () => { + const { sidenav, dataTable } = page; + + sidenav.navigateToLinkByLabel(SIDEBAR_LABELS.FAVORITES) + .then(() => dataTable.wait()) + .then(() => browser.getCurrentUrl()) + .then(url => { + expect(url).toContain(APP_ROUTES.FAVORITES); + expect(sidenav.isActiveByLabel(SIDEBAR_LABELS.FAVORITES)).toBe(true); + }); + }); + + it('navigates to "Trash"', () => { + const { sidenav, dataTable } = page; + + sidenav.navigateToLinkByLabel(SIDEBAR_LABELS.TRASH) + .then(() => dataTable.wait()) + .then(() => browser.getCurrentUrl()) + .then(url => { + expect(url).toContain(APP_ROUTES.TRASHCAN); + expect(sidenav.isActiveByLabel(SIDEBAR_LABELS.TRASH)).toBe(true); + }); + }); +}); diff --git a/e2e/suites/pagination/pagination.test.ts b/e2e/suites/pagination/pagination.test.ts new file mode 100644 index 000000000..f0298ff51 --- /dev/null +++ b/e2e/suites/pagination/pagination.test.ts @@ -0,0 +1,169 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { browser } from 'protractor'; + +import { APP_ROUTES } from '../../configs'; +import { LoginPage, LogoutPage, BrowsingPage } from '../../pages/pages'; +import { LocalStorageUtility } from '../../utilities/local-storage'; +import { RepoClient, NodeContentTree } from '../../utilities/repo-client/repo-client'; + +describe('Pagination', () => { + const username = 'jane.doe'; + const password = 'jane.doe'; + + const apis = { + admin: new RepoClient(), + user: new RepoClient(username, password) + }; + + const loginPage = new LoginPage(); + const logoutPage = new LogoutPage(); + + beforeAll(done => { + apis.admin.people + .createUser(username, password) + .then(done); + }); + + xit(''); + + describe(`on Personal Files`, () => { + const personalFilesPage = new BrowsingPage(APP_ROUTES.PERSONAL_FILES); + const { dataTable, pagination } = personalFilesPage; + + // Generate files + const content: NodeContentTree = { + name: 'user-folder', + files: Array(101) + .fill('file') + .map((name, index): string => `${name}-${index + 1}.txt`) + }; + + const { nodes: nodesApi } = apis.user; + + beforeAll(done => { + nodesApi.createContent(content) + .then(() => loginPage.load()) + .then(() => loginPage.loginWith(username, password)) + .then(done); + }); + + beforeEach(done => { + personalFilesPage + .load() + .then(() => dataTable.waitForHeader()) + .then(() => dataTable.doubleClickOnRowByContainingText(content.name)) + .then(() => dataTable.sortByColumn('Name')) + .then(done); + }); + + afterAll(done => { + logoutPage + .load() + .then(() => LocalStorageUtility.clear()) + .then(() => nodesApi.deleteNodes([ content.name ])) + .then(done); + }); + + it('has default details', () => { + expect(pagination.range.getText()).toContain('1-25 of 101', 'Range'); + expect(pagination.maxItems.getText()).toContain('25', 'Items per page'); + expect(pagination.currentPage.getText()).toContain('Page 1', 'Current page'); + expect(pagination.totalPages.getText()).toContain('of 5', 'Total pages'); + expect(pagination.previousButton.isEnabled()).toBe(false, 'Previous button'); + expect(pagination.nextButton.isEnabled()).toBe(true, 'Next button'); + }); + + it('has page sizes', () => { + pagination.openMaxItemsMenu() + .then(menu => { + const [ first, second, third ] = [1, 2, 3] + .map(nth => menu.getNthItem(nth).getText()); + + expect(first).toBe('25', 'Items per page'); + expect(second).toBe('50', 'Items per page'); + expect(third).toBe('100', 'Items per page'); + }); + }); + + it('changes the page size', () => { + pagination.openMaxItemsMenu() + .then(menu => menu.clickMenuItem('50')) + .then(() => dataTable.waitForHeader()) + .then(() => { + expect(pagination.maxItems.getText()).toContain('50', 'Items per page'); + expect(pagination.totalPages.getText()).toContain('of 3', 'Total pages'); + }); + }); + + it('has page items', () => { + pagination.openCurrentPageMenu() + .then(menu => { + expect(menu.items.count()).toBe(5); + }); + }); + + it('changes the current page from menu', () => { + pagination.openCurrentPageMenu() + .then(menu => menu.clicktNthItem(3)) + .then(() => dataTable.waitForHeader()) + .then(() => { + expect(pagination.range.getText()).toContain('51-75 of 101', 'Range'); + expect(pagination.currentPage.getText()).toContain('Page 3', 'Current page'); + expect(pagination.previousButton.isEnabled()).toBe(true, 'Previous button'); + expect(pagination.nextButton.isEnabled()).toBe(true, 'Next button'); + expect(dataTable.getRowByContainingText('file-60.txt').isPresent()) + .toBe(true, 'File not found on page'); + }); + }); + + it('navigates to next page', () => { + pagination.nextButton.click() + .then(() => dataTable.waitForHeader()) + .then(() => { + expect(pagination.range.getText()).toContain('26-50 of 101', 'Range'); + expect(dataTable.getRowByContainingText('file-30.txt').isPresent()) + .toBe(true, 'File not found on page'); + }); + }); + + it('navigates to previous page', () => { + pagination.openCurrentPageMenu() + .then(menu => menu.clicktNthItem(2)) + .then(() => dataTable.waitForHeader()) + .then(() => pagination.previousButton.click()) + .then(() => dataTable.waitForHeader()) + .then(() => { + expect(pagination.range.getText()).toContain('1-25 of 101', 'Range'); + expect(dataTable.getRowByContainingText('file-12.txt').isPresent()) + .toBe(true, 'File not found on page'); + }); + }); + + it('has one item on the last page', () => { + pagination.openCurrentPageMenu() + .then(menu => menu.clicktNthItem(5)) + .then(() => dataTable.waitForHeader()) + .then(() => { + expect(dataTable.countRows()).toBe(1, 'Single item on the last page'); + expect(pagination.currentPage.getText()).toContain('Page 5', 'Last page'); + expect(pagination.nextButton.isEnabled()).toBe(false, 'Next button is not enabled'); + }); + }); + }); +}); diff --git a/e2e/utilities/local-storage.ts b/e2e/utilities/local-storage.ts new file mode 100644 index 000000000..04c841895 --- /dev/null +++ b/e2e/utilities/local-storage.ts @@ -0,0 +1,34 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { browser } from 'protractor'; + +declare var window; + +export class LocalStorageUtility { + static clear(): Promise { + return browser.executeScript(() => { + return window.localStorage.clear(); + }); + } + + static getTicket(): Promise { + return browser.executeScript(() => { + return window.localStorage.getItem('ticket-ECM'); + }); + } +} diff --git a/e2e/utilities/repo-client/apis/nodes/node-body-create.ts b/e2e/utilities/repo-client/apis/nodes/node-body-create.ts new file mode 100644 index 000000000..ba3a5ceb4 --- /dev/null +++ b/e2e/utilities/repo-client/apis/nodes/node-body-create.ts @@ -0,0 +1,30 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const NODE_TYPE_FILE = 'cm:content'; +export const NODE_TYPE_FOLDER = 'cm:folder'; +export const NODE_TITLE = 'cm:title'; +export const NODE_DESCRIPTION = 'cm:description'; + +export class NodeBodyCreate { + constructor( + public name: string, + public nodeType: string, + public relativePath: string = '/', + public properties?: any[] + ) {} +} diff --git a/e2e/utilities/repo-client/apis/nodes/node-content-tree.ts b/e2e/utilities/repo-client/apis/nodes/node-content-tree.ts new file mode 100644 index 000000000..f9f68deaf --- /dev/null +++ b/e2e/utilities/repo-client/apis/nodes/node-content-tree.ts @@ -0,0 +1,77 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NodeBodyCreate, NODE_TYPE_FILE, NODE_TYPE_FOLDER, NODE_TITLE, NODE_DESCRIPTION } from './node-body-create'; + +export interface NodeContentTree { + name?: string; + files?: string[]; + folders?: (string|NodeContentTree)[]; + title?: string; + description?: string; +} + +export function flattenNodeContentTree(content: NodeContentTree, relativePath: string = '/'): NodeBodyCreate[] { + const { name, files, folders, title, description } = content; + let data: NodeBodyCreate[] = []; + let properties: any; + + properties = { + [NODE_TITLE]: title, + [NODE_DESCRIPTION]: description + }; + + if (name) { + data = data.concat([{ + nodeType: NODE_TYPE_FOLDER, + name, + relativePath, + properties + }]); + + relativePath = (relativePath === '/') + ? `/${name}` + : `${relativePath}/${name}`; + } + + if (folders) { + const foldersData: NodeBodyCreate[] = folders + .map((folder: (string|NodeContentTree)): NodeBodyCreate[] => { + const folderData: NodeContentTree = (typeof folder === 'string') + ? { name: folder } + : folder; + + return flattenNodeContentTree(folderData, relativePath); + }) + .reduce((nodesData: NodeBodyCreate[], folderData: NodeBodyCreate[]) => nodesData.concat(folderData), []); + + data = data.concat(foldersData); + } + + if (files) { + const filesData: NodeBodyCreate[] = files + .map((filename: string): NodeBodyCreate => ({ + nodeType: NODE_TYPE_FILE, + name: filename, + relativePath + })); + + data = data.concat(filesData); + } + + return data; +} diff --git a/e2e/utilities/repo-client/apis/nodes/nodes-api.ts b/e2e/utilities/repo-client/apis/nodes/nodes-api.ts new file mode 100644 index 000000000..227b566d2 --- /dev/null +++ b/e2e/utilities/repo-client/apis/nodes/nodes-api.ts @@ -0,0 +1,96 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RepoApi } from '../repo-api'; +import { NodeBodyCreate, NODE_TYPE_FILE, NODE_TYPE_FOLDER } from './node-body-create'; +import { NodeContentTree, flattenNodeContentTree } from './node-content-tree'; + +export class NodesApi extends RepoApi { + // nodes + getNodeByPath(relativePath: string = '/'): Promise { + return this + .get(`/nodes/-my-`, { parameters: { relativePath } }) + .catch(this.handleError); + } + + getNodeById(id: string): Promise { + return this + .get(`/nodes/${id}`) + .catch(this.handleError); + } + + deleteNodeById(id: string): Promise { + return this + .delete(`/nodes/${id}`) + .catch(this.handleError); + } + + deleteNodeByPath(path: string): Promise { + return this + .getNodeByPath(path) + .then((response: any): string => response.data.entry.id) + .then((id: string): any => this.deleteNodeById(id)) + .catch(this.handleError); + } + + getNodeDescription(name: string): Promise { + let description = 'cm:description'; + + return this + .getNodeByPath(name) + .then((response: any): string => response.data.entry.properties[description]) + .catch(this.handleError); + } + + deleteNodes(names: string[], relativePath: string = ''): Promise { + const deletions = names + .map((name: string): any => { + return this.deleteNodeByPath(`${relativePath}/${name}`); + }); + + return Promise.all(deletions); + } + + // children + getNodeChildren(nodeId: string): Promise { + return this + .get(`/nodes/${nodeId}/children`) + .catch(this.handleError); + } + + createChildren(data: NodeBodyCreate[]): Promise { + return this + .post(`/nodes/-my-/children`, { data }) + .catch(this.handleError); + } + + createContent(content: NodeContentTree, relativePath: string = '/'): Promise { + return this.createChildren(flattenNodeContentTree(content, relativePath)); + } + + createNodeWithProperties(name: string, title?: string, description?: string, relativePath: string = '/'): Promise { + return this.createContent({ name, title, description }, relativePath); + } + + createFolders(names: string[], relativePath: string = '/'): Promise { + return this.createContent({ folders: names }, relativePath); + } + + createFiles(names: string[], relativePath: string = '/'): Promise { + return this.createContent({ files: names }, relativePath); + } +} diff --git a/e2e/utilities/repo-client/apis/people/people-api-models.ts b/e2e/utilities/repo-client/apis/people/people-api-models.ts new file mode 100644 index 000000000..46f7f4feb --- /dev/null +++ b/e2e/utilities/repo-client/apis/people/people-api-models.ts @@ -0,0 +1,35 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class Person { + id?: string; + password?: string; + firstName?: string; + lastName?: string; + email?: string; + properties?: any; + + constructor(username: string, password: string, details: Person) { + this.id = username; + this.password = password || username; + this.firstName = username; + this.lastName = username; + this.email = `${username}@alfresco.com`; + + Object.assign(this, details); + } +} diff --git a/e2e/utilities/repo-client/apis/people/people-api.ts b/e2e/utilities/repo-client/apis/people/people-api.ts new file mode 100644 index 000000000..8f05548a1 --- /dev/null +++ b/e2e/utilities/repo-client/apis/people/people-api.ts @@ -0,0 +1,52 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RepoApi } from '../repo-api'; +import { Person } from './people-api-models'; + +export class PeopleApi extends RepoApi { + getUser(username: string) { + return this + .get(`/people/${username}`) + .catch(this.handleError); + } + + updateUser(username: string, details?: Person): Promise { + if (details.id) { + delete details.id; + } + + return this + .put(`/people/${username}`, { data: details }) + .catch(this.handleError); + } + + createUser(username: string, password: string, details?: Person): Promise { + const person: Person = new Person(username, password, details); + const onSuccess = (response) => response; + const onError = (response) => { + return (response.statusCode === 409) + ? Promise.resolve(this.updateUser(username, person)) + : Promise.reject(response); + }; + + return this + .post(`/people`, { data: person }) + .then(onSuccess, onError) + .catch(this.handleError); + } +} diff --git a/e2e/utilities/repo-client/apis/repo-api.ts b/e2e/utilities/repo-client/apis/repo-api.ts new file mode 100644 index 000000000..78cc802ae --- /dev/null +++ b/e2e/utilities/repo-client/apis/repo-api.ts @@ -0,0 +1,63 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RestClient, RestClientArgs, RestClientResponse } from '../../rest-client/rest-client'; +import { RepoClientAuth, RepoClientConfig } from '../repo-client-models'; + +export abstract class RepoApi { + private client: RestClient; + private defaults: RepoClientConfig = new RepoClientConfig(); + + constructor( + auth: RepoClientAuth = new RepoClientAuth(), + private config?: RepoClientConfig + ) { + const { username, password } = auth; + + this.client = new RestClient(username, password); + } + + private createEndpointUri(endpoint: string): string { + const { defaults, config } = this; + const { host, tenant } = Object.assign(defaults, config); + + return `${host}/alfresco/api/${tenant}/public/alfresco/versions/1${endpoint}`; + } + + protected handleError(response: RestClientResponse) { + const { request: { method, path, data }, data: error } = response; + + console.log(`ERROR on ${method}\n${path}\n${data}`); + console.log(error); + } + + protected get(endpoint: string, args: RestClientArgs = {}) { + return this.client.get(this.createEndpointUri(endpoint), args); + } + + protected post(endpoint: string, args: RestClientArgs = {}) { + return this.client.post(this.createEndpointUri(endpoint), args); + } + + protected put(endpoint: string, args: RestClientArgs = {}) { + return this.client.put(this.createEndpointUri(endpoint), args); + } + + protected delete(endpoint: string, args: RestClientArgs = {}) { + return this.client.delete(this.createEndpointUri(endpoint), args); + } +} diff --git a/e2e/utilities/repo-client/apis/sites/sites-api-models.ts b/e2e/utilities/repo-client/apis/sites/sites-api-models.ts new file mode 100644 index 000000000..07280f753 --- /dev/null +++ b/e2e/utilities/repo-client/apis/sites/sites-api-models.ts @@ -0,0 +1,34 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SITE_VISIBILITY } from '../../../../configs'; + +export class Site { + title?: string; + visibility?: string = SITE_VISIBILITY.PUBLIC; + id?: string; + description?: string; + + constructor(title: string, visibility: string, details: Site) { + this.title = title; + this.visibility = visibility; + this.id = title; + this.description = `${title} description`; + + Object.assign(this, details); + } +} diff --git a/e2e/utilities/repo-client/apis/sites/sites-api.ts b/e2e/utilities/repo-client/apis/sites/sites-api.ts new file mode 100644 index 000000000..05b3f7e6c --- /dev/null +++ b/e2e/utilities/repo-client/apis/sites/sites-api.ts @@ -0,0 +1,84 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RepoApi } from '../repo-api'; +import { Site } from './sites-api-models'; + +export class SitesApi extends RepoApi { + getSite(id: string) { + return this + .get(`/sites/${id}`) + .catch(this.handleError); + } + + updateSite(id: string, details?: Site): Promise { + if (details.id) { + delete details.id; + } + + return this + .put(`/sites/${id}`, { data: details }) + .catch(this.handleError); + } + + createSite(title: string, visibility: string, details?: Site): Promise { + const site: Site = new Site(title, visibility, details); + const onSuccess = (response) => response; + const onError = (response) => { + return (response.statusCode === 409) + ? Promise.resolve(this.updateSite(site.id, site)) + : Promise.reject(response); + }; + + return this + .post(`/sites`, { data: site }) + .then(onSuccess, onError) + .catch(this.handleError); + } + + deleteSite(id: string, permanent: boolean = true): Promise { + return this + .delete(`/sites/${id}?permanent=${permanent}`) + .catch(this.handleError); + } + + updateSiteMember(siteId: string, userId: string, role: string): Promise { + return this + .put(`/sites/${siteId}/members/${userId}`, { data: { role } }) + .catch(this.handleError); + } + + addSiteMember(siteId: string, userId: string, role: string): Promise { + const onSuccess = (response) => response; + const onError = (response) => { + return (response.statusCode === 409) + ? Promise.resolve(this.updateSiteMember(siteId, userId, role)) + : Promise.reject(response); + }; + + return this + .post(`/sites/${siteId}/members`, { data: { role, id: userId } }) + .then(onSuccess, onError) + .catch(this.handleError); + } + + deleteSiteMember(siteId: string, userId: string): Promise { + return this + .delete(`/sites/${siteId}/members/${userId}`) + .catch(this.handleError); + } +} diff --git a/e2e/utilities/repo-client/repo-client-models.ts b/e2e/utilities/repo-client/repo-client-models.ts new file mode 100644 index 000000000..2bc16c1c8 --- /dev/null +++ b/e2e/utilities/repo-client/repo-client-models.ts @@ -0,0 +1,38 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ADMIN_USERNAME, + ADMIN_PASSWORD, + REPO_API_HOST, + REPO_API_TENANT +} from '../../configs'; + +export class RepoClientAuth { + static DEFAULT_USERNAME: string = ADMIN_USERNAME; + static DEFAULT_PASSWORD: string = ADMIN_PASSWORD; + + constructor( + public username: string = RepoClientAuth.DEFAULT_USERNAME, + public password: string = RepoClientAuth.DEFAULT_PASSWORD + ) {} +} + +export class RepoClientConfig { + host?: string = REPO_API_HOST; + tenant?: string = REPO_API_TENANT; +} diff --git a/e2e/utilities/repo-client/repo-client.ts b/e2e/utilities/repo-client/repo-client.ts new file mode 100644 index 000000000..80d37360b --- /dev/null +++ b/e2e/utilities/repo-client/repo-client.ts @@ -0,0 +1,45 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RepoClientAuth, RepoClientConfig } from './repo-client-models'; + +import { PeopleApi } from './apis/people/people-api'; +import { NodesApi } from './apis/nodes/nodes-api'; +import { SitesApi } from './apis/sites/sites-api'; + +export class RepoClient { + public people: PeopleApi = new PeopleApi(this.auth, this.config); + public nodes: NodesApi = new NodesApi(this.auth, this.config); + public sites: SitesApi = new SitesApi(this.auth, this.config); + // public favorites: FavoritesApi = new FavoritesApi(this.auth, this.config); + // public shared: SharedLinksApi = new SharedLinksApi(this.auth, this.config); + + constructor( + private username: string = RepoClientAuth.DEFAULT_USERNAME, + private password: string = RepoClientAuth.DEFAULT_PASSWORD, + private config?: RepoClientConfig + ) {} + + private get auth(): RepoClientAuth { + const { username, password } = this; + return { username, password }; + } +} + +export * from './apis/nodes/node-body-create'; +export * from './apis/nodes/node-content-tree'; +export * from './apis/nodes/nodes-api'; diff --git a/e2e/utilities/reporters/console/console-logger.ts b/e2e/utilities/reporters/console/console-logger.ts new file mode 100644 index 000000000..80b1f786c --- /dev/null +++ b/e2e/utilities/reporters/console/console-logger.ts @@ -0,0 +1,71 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* tslint:disable */ +const chalk = require('chalk'); +/* tslint:enable */ + +export const log = { + i: 0, + + get indentation(): string { + return new Array(this.i).fill(' ').join(''); + }, + + indent() { + this.i++; + return this; + }, + + dedent() { + this.i--; + return this; + }, + + log(message: string = '', options: any = { ignoreIndentation: false }) { + const indentation = (!options.ignoreIndentation) + ? this.indentation + : ''; + + console.log(`${indentation}${message}`); + + return this; + }, + + blank() { + return this.log(); + }, + + info(message: string = '', options: any = { bold: false, title: false }) { + const { bold } = options; + const style = (bold ? chalk.bold : chalk).gray; + + return this.log(style(message), options); + }, + + success(message: string = '', options: any = { bold: false }) { + const style = options.bold ? chalk.bold.green : chalk.green; + + return this.log(style(message), options); + }, + + error(message: string = '', options: any = { bold: false }) { + const style = options.bold ? chalk.bold.red : chalk.red; + + return this.log(style(message), options); + } +}; diff --git a/e2e/utilities/reporters/console/console.ts b/e2e/utilities/reporters/console/console.ts new file mode 100644 index 000000000..8223def47 --- /dev/null +++ b/e2e/utilities/reporters/console/console.ts @@ -0,0 +1,82 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { log } from './console-logger'; + +const errors = []; + +export const consoleReporter = { + jasmineStarted(suiteInfo) { + log.blank().info( + `Running ${suiteInfo.totalSpecsDefined} tests`, + { bold: true, title: true } + ).blank(); + }, + + suiteStarted(suite) { + log.info(suite.description).indent(); + }, + + specDone: (spec) => { + const { + status, + description, + failedExpectations + } = spec; + + if (status === 'passed') { + log.success(`∙ ${description}`); + } + + if (status === 'failed') { + log.error(`✕ ${description}`, { bold: true }); + + errors.push(spec); + + failedExpectations.forEach((failed) => { + log.error(` ${failed.message}`); + }); + } + }, + + suiteDone: (result) => { + log.dedent(); + }, + + jasmineDone: (result) => { + if (!!errors.length) { + log .blank() + .blank() + .info(`${errors.length} failing tests`, { bold: true, title: true }); + + errors.forEach(error => { + log .blank() + .error(`✕ ${error.fullName}`, { bold: true }); + + error.failedExpectations.forEach(failed => { + log .info(`${failed.message}`) + .blank() + .error(`${failed.stack}`); + }); + }); + } else { + log.success(`All tests passed!`, { bold: true }); + } + + log.blank().blank(); + } +}; diff --git a/e2e/utilities/rest-client/rest-client-models.ts b/e2e/utilities/rest-client/rest-client-models.ts new file mode 100644 index 000000000..918bebee4 --- /dev/null +++ b/e2e/utilities/rest-client/rest-client-models.ts @@ -0,0 +1,55 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +interface RequestConfig { + timeout?: number; + noDelay?: boolean; + keepAlive?: boolean; + keepAliveDelay?: number; +} + +interface ResponseConfig { + timeout?: number; +} + +interface ResponseRequest { + method: string; + path: string; + data: string; +} + +export interface NodeRestClient { + get(uri: string, callback: Function): Function; + post(uri: string, callback: Function): Function; + put(uri: string, callback: Function): Function; + delete(uri: string, callback: Function): Function; +} + +export interface RestClientArgs { + data?: any; + parameters?: any; + headers?: any; + requestConfig?: RequestConfig; + responseConfig?: ResponseConfig; +} + +export interface RestClientResponse { + request: ResponseRequest; + data: any; + statusMessage: string; + statusCode: number; +} diff --git a/e2e/utilities/rest-client/rest-client.ts b/e2e/utilities/rest-client/rest-client.ts new file mode 100644 index 000000000..7f457e46e --- /dev/null +++ b/e2e/utilities/rest-client/rest-client.ts @@ -0,0 +1,81 @@ +/*! + * @license + * Copyright 2017 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Client } from 'node-rest-client'; +import { NodeRestClient, RestClientArgs, RestClientResponse } from './rest-client-models'; + +export * from './rest-client-models'; + +export class RestClient { + private static DEFAULT_HEADERS = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + + private client: NodeRestClient; + + constructor(user: string, password: string) { + this.client = (new Client({ user, password })); + } + + get(uri: string, args: RestClientArgs = {}): Promise { + return this.promisify('get', uri, args); + } + + post(uri: string, args: RestClientArgs = {}): Promise { + return this.promisify('post', uri, args); + } + + put(uri: string, args: RestClientArgs = {}): Promise { + return this.promisify('put', uri, args); + } + + delete(uri: string, args: RestClientArgs = {}): Promise { + return this.promisify('delete', uri, args); + } + + private createArgs(args: RestClientArgs = {}): RestClientArgs { + const data = JSON.stringify(args.data); + + return Object.assign({}, RestClient.DEFAULT_HEADERS, args, { data }); + } + + private promisify(fnName: string, uri: string, args: RestClientArgs): Promise { + const fn: Function = this.client[fnName]; + const fnArgs = [ encodeURI(uri), this.createArgs(args) ]; + + return new Promise((resolve, reject) => { + const fnCallback = (data, rawResponse) => { + const { + statusCode, statusMessage, + req: { method, path } + } = rawResponse; + + const response: RestClientResponse = { + data, statusCode, statusMessage, + request: { method, path, data: args.data } + }; + + (response.statusCode >= 400) + ? reject(response) + : resolve(response); + }; + + fn(...fnArgs, fnCallback); + }); + } +} diff --git a/protractor.conf.js b/protractor.conf.js index 7ee3b5ee8..2ff137ac5 100644 --- a/protractor.conf.js +++ b/protractor.conf.js @@ -6,7 +6,7 @@ const { SpecReporter } = require('jasmine-spec-reporter'); exports.config = { allScriptsTimeout: 11000, specs: [ - './e2e/**/*.e2e-spec.ts' + './e2e/**/*.test.ts' ], capabilities: { 'browserName': 'chrome'