cloudfoundry/stratos

View on GitHub
src/test-e2e/po/list.po.ts

Summary

Maintainability
C
1 day
Test Coverage
import { browser, by, element, Key, promise, protractor } from 'protractor';
import { ElementArrayFinder, ElementFinder } from 'protractor/built';

import { Component } from './component.po';
import { FormComponent } from './form.po';
import { MenuComponent } from './menu.po';
import { MetaCard, MetaCardTitleType } from './meta-card.po';

const until = protractor.ExpectedConditions;

export interface CardMetadata {
  index: number;
  title: string;
  click: () => void;
}

export interface TableData {
  [columnHeader: string]: string;
}

// Page Object for the List Table View
export class ListTableComponent extends Component {

  constructor(locator: ElementFinder) {
    super(locator);
  }

  getHeaderText(): promise.Promise<string> {
    return this.locator.element(by.css('.list-component__header__left--text')).getText();
  }

  getRows(): ElementArrayFinder {
    return this.locator.all(by.css('.app-table__row'));
  }

  getCell(row, column): ElementFinder {
    return this.getRows().get(row).all(by.css('.app-table__cell')).get(column);
  }

  waitForCellText(row: number, column: number, text: string) {
    const component = new Component(this.getCell(row, column));
    return component.waitForText(text);
  }

  findRowByCellContent(content) {
    const cell = this.locator.all(by.css('.app-table__cell')).filter(elem =>
      elem.getText().then(text => text === content)
    ).first();

    browser.wait(until.presenceOf(cell));
    return cell.element(by.xpath('ancestor::app-table-row'));
  }

  // Get the data in the table
  getTableDataRaw(): promise.Promise<any> {
    const getHeaders = this.locator.all(by.css('.app-table__header-cell')).map(headerCell => headerCell.getText());
    const getRows = this.locator.all(by.css('.app-table__row')).map(row => row.all(by.css('.app-table__cell')).map(cell => cell.getText()));
    return promise.all([getHeaders, getRows]).then(([headers, rows]) => {
      return {
        headers,
        rows
      };
    });
  }

  getTableData(): promise.Promise<TableData[]> {
    return this.getTableDataRaw().then(tableData => {
      const table = [];
      tableData.rows.forEach((row: string[]) => {
        const tableRow = {};
        row.forEach((cellValue, index) => {
          const headerName = (tableData.headers[index] || 'column-' + index) as string;
          tableRow[headerName.toLowerCase()] = cellValue;
        });
        table.push(tableRow);
      });
      return table;
    });
  }

  findRow(columnHeader: string, value: string, expected = true): promise.Promise<number> {
    return this.getTableData().then(data => {
      const rowIndex = data.findIndex(row => row[columnHeader] === value);
      if (rowIndex >= 0) {
        if (expected) {
          return rowIndex;
        }
        throw new Error(`Found row with header '${columnHeader}' and value '${value}' when not expecting one`);
      } else {
        if (expected) {
          throw new Error(`Could not find row with header '${columnHeader}' and value '${value}'`);
        }
        return -1;
      }
    });
  }

  selectRow(index: number, radioButton = true): promise.Promise<any> {
    return this.locator.all(by.css('.app-table__row')).then(rows => {
      expect(rows.length).toBeGreaterThan(index);
      return rows[index].element(by.css(radioButton ? '.mat-radio-button' : '.mat-checkbox')).click();
    });
  }

  waitUntilNotBusy(failMsg?: string) {
    return Component.waitUntilNotShown(
      this.locator.element(by.css('.table-row__deletion-bar-wrapper')),
      'Failed to wait for list busy indicator to be shown'
    ).then(() => Component.waitUntilNotShown(
      this.locator.element(by.css('.table-row-wrapper__blocked')),
      'Failed to wait for list busy indicator to be not shown'
    ));
  }

  getHighlightedRow(): promise.Promise<number> {
    return this.locator.all(by.css('.app-table__row'))
      .map((row, index) => row.getAttribute('class').then(classes => classes.indexOf('table-row-wrapper__highlighted')))
      .then(isHighlighted => {
        return promise.all(isHighlighted);
      })
      .then(isHighlighted => {
        for (let i = 0; i < isHighlighted.length; i++) {
          if (isHighlighted[i]) {
            return i;
          }
        }
        return -1;
      });
  }

  editRow(index: number, fieldId: string, newValue: string): promise.Promise<any> {
    const cell = this.getRows().get(index);
    cell.element(by.css('app-table-cell-edit button')).click();
    const form = new FormComponent(cell);
    form.fill({ [fieldId]: newValue });
    return cell.element(by.id('table-cell-edit-done')).click();
  }

  openRowActionMenuByIndex(index: number): MenuComponent {
    return this.openRowActionMenuByRow(this.getRows().get(index));
  }

  openRowActionMenuByRow(row: ElementFinder): MenuComponent {
    row.element(by.css('app-table-cell-actions button')).click();
    const menu = new MenuComponent();
    menu.waitUntilShown();
    return menu;
  }

  toggleSort(headerTitle: string): promise.Promise<any> {
    return this.locator.element(by.cssContainingText('mat-header-row app-table-cell', headerTitle)).click();
  }

  getRowCount(): promise.Promise<number> {
    return this.getRows().count();
  }
}

// Page Object for the List Card View
export class ListCardComponent extends Component {

  static cardsCss = 'app-card:not(.row-filler)';

  constructor(locator: ElementFinder, private list: ListComponent) {
    super(locator);
  }

  getCardCount() {
    const noRows = this.locator.all(by.css('.no-rows'));
    return noRows.count().then(rows => {
      return rows === 1 ? 0 : this.getCards().count();
    });
  }

  getCards(): ElementArrayFinder {
    return this.locator.all(by.css(ListCardComponent.cardsCss));
  }

  getCard(index: number, metaType = MetaCardTitleType.CUSTOM): MetaCard {
    return new MetaCard(this.getCards().get(index), metaType);
  }

  private findCardElementByTitle(title: string, metaType = MetaCardTitleType.CUSTOM): ElementFinder {
    this.list.isTableView().then(isTableView => {
      if (isTableView) {
        return this.list.header.getCardListViewToggleButton().click();
      }
    });

    const card = this.locator.all(by.css(`${ListCardComponent.cardsCss} ${metaType}`)).filter(elem =>
      elem.getText().then(text => text === title)
    ).first();
    browser.wait(until.presenceOf(card));
    return card.element(by.xpath('ancestor::app-card'));
  }

  waitForCardByTitle(title: string, metaType = MetaCardTitleType.CUSTOM): promise.Promise<MetaCard> {
    const cardElement = this.findCardElementByTitle(title, metaType);
    return browser.wait(until.visibilityOf(cardElement), 10000).then(() => {
      // We've found the title, now get the actual element
      return new MetaCard(cardElement, metaType);
    });
  }

  findCardByTitle(title: string, metaType = MetaCardTitleType.CUSTOM, filter = false): promise.Promise<MetaCard> {
    if (filter) {
      this.list.header.waitUntilShown();
      this.list.header.setSearchText(title);
      return this.waitForCardByTitle(title, metaType);
    }

    const cardElement = this.findCardElementByTitle(title, metaType);
    return cardElement.isPresent().then(isPresent => {
      expect(isPresent).toBeTruthy();
      return Promise.resolve(new MetaCard(cardElement, metaType));
    });
  }

  getCardsMetadata(): promise.Promise<CardMetadata[]> {
    return this.getCards().map((elem, index) => {
      return {
        index,
        title: elem.element(by.css('.meta-card__title')).getText(),
        click: elem.click,
      };
    });
  }

}
// List Header (filter/search bar)
export class ListHeaderComponent extends Component {

  listLocator: ElementFinder;

  constructor(locator: ElementFinder) {
    super(locator.element(by.css('.list-component__header')));
    this.listLocator = locator;
  }

  private getFilterSection(): ElementFinder {
    return this.locator.element(by.css('.list-component__header__left--multi-filters'));
  }

  getFilterFormFields(): ElementArrayFinder {
    return this.getFilterSection().all(by.tagName('mat-form-field'));
  }

  getRightHeaderSection(): ElementFinder {
    return this.locator.element(by.css('.list-component__header__right'));
  }

  getLeftHeaderSection(): ElementFinder {
    return this.locator.element(by.css('.list-component__header__left'));
  }

  getSearchInputField(): ElementFinder {
    return this.getRightHeaderSection().all(by.css('#listSearchFilter')).first().element(by.css('input'));
  }

  setSearchText(text: string): promise.Promise<void> {
    const searchField = this.getSearchInputField();
    searchField.click();
    searchField.clear();
    return searchField.sendKeys(text);
  }

  clearSearchText() {
    const searchField = this.getSearchInputField();
    searchField.click();
    searchField.clear();
    searchField.sendKeys('a');
    searchField.sendKeys(Key.BACK_SPACE);
  }

  getSearchText(): promise.Promise<string> {
    return this.getSearchInputField().getAttribute('value');
  }

  getFilterFormField(id: string): ElementFinder {
    return this.getFilterSection().element(by.id(id));
  }

  getPlaceHolderText(id: string, ignoreMissing = false): promise.Promise<string> {
    const filter = this.getFilterFormField(id);
    return filter.isPresent().then(isPresent => {
      if (isPresent) {
        return filter.element(by.tagName('mat-placeholder')).getText();
      } else if (!ignoreMissing) {
        fail(`Failed to find filter with id '${id}'`);
      }
    });
  }

  getFilterOptions(id: string, ignoreMissing = false): promise.Promise<ElementFinder[]> {
    const filter = this.getFilterFormField(id);
    return filter.isPresent().then(isPresent => {
      if (isPresent) {
        filter.click();
        return element.all(by.id(id)).then((matOptions: ElementFinder[]) => matOptions);
      } else if (!ignoreMissing) {
        fail(`Failed to find filter with id '${id}'`);
      }
    });
  }

  getFilterText(id: string): promise.Promise<string> {
    return this.getFilterFormField(id).element(by.css('.mat-select-value')).getText();
  }

  selectFilterOption(id: string, valueIndex: number, ignoreMissing = false): promise.Promise<any> {
    return this.getFilterOptions(id, ignoreMissing).then(options => {
      if (options) {
        options[valueIndex].click();
      }
    });
  }

  getMultiFilterForm(): FormComponent {
    return new FormComponent(this.getFilterSection());
  }

  getRefreshListButton(): ElementFinder {
    return this.getRightHeaderSection().element(by.css('#app-list-refresh-button'));
  }

  getRefreshListButtonAnimated(): ElementFinder {
    return this.getRefreshListButton().element(by.css('.poll-icon.polling'));
  }

  refresh() {
    this.getRefreshListButton().click();
    return this.waitForNotRefreshing();
  }

  isRefreshing(): promise.Promise<boolean> {
    return this.getRefreshListButton().element(by.css('.poll-icon')).getCssValue('animation-play-state').then(state =>
      state === 'running'
    );
  }

  waitForRefreshing(): promise.Promise<any> {
    return browser.wait(until.visibilityOf(this.getRefreshListButtonAnimated()), 5000);
  }

  waitForNotRefreshing(): promise.Promise<any> {
    return browser.wait(until.invisibilityOf(this.getRefreshListButtonAnimated()), 10000);
  }

  getCardListViewToggleButton(): ElementFinder {
    return this.getRightHeaderSection().element(by.css('#list-card-toggle'));
  }

  private findSortSection(): ElementFinder {
    return this.locator.element(by.css('.list-component__header__right .sort'));
  }

  getSortFieldForm(): FormComponent {
    return new FormComponent(this.findSortSection());
  }

  toggleSortOrder() {
    this.findSortSection().element(by.css('button')).click();
  }

  getAdd(): ElementFinder {
    return this.locator.element(by.cssContainingText('.list-component__header__right button mat-icon', 'add'));
  }

  getInlineAdd(): ElementFinder {
    return this.locator.element(by.css('.list-component__header .add-container'));
  }

  getInlineAddForm(): ElementFinder {
    return this.getInlineAdd().element(by.css('form'));
  }

  getInlineAddFormAdd(): ElementFinder {
    return this.getInlineAdd().element(by.css('button:first-of-type'));
  }

  getInlineAddFormCancel(): ElementFinder {
    return this.getInlineAdd().element(by.id('addFormButtonCancel'));
  }

  getIconButton(iconText: string): ElementFinder {
    return this.getLeftHeaderSection().element(by.cssContainingText('button mat-icon', iconText));
  }

  clearFilters(): promise.Promise<any> {
    return this.getClearButton().click();
  }

  getClearButton() {
    return this.locator.element(by.cssContainingText('.list-component__header__right button mat-icon', 'highlight_off'));
  }

}

export class ListPaginationComponent extends Component {
  constructor(listComponent: ElementFinder) {
    super(listComponent.element(by.tagName('.list-component__paginator')));
  }

  getTotalResults() {
    return this.locator.element(by.css('.mat-paginator-range-label')).getText().then(label => {
      const index = label.indexOf('of ');
      if (index > 0) {
        const value = label.substr(index + 3).trim();
        return parseInt(value, 10);
      }
      return -1;
    });
  }

  private findPageSizeSection(): ElementFinder {
    return this.locator.element(by.css('.mat-paginator-page-size'));
  }

  getPageSize(customCtrlName?: string): promise.Promise<string> {
    return this.getPageSizeForm().getText(customCtrlName || 'mat-select-2');
  }

  setPageSize(pageSize, customCtrlName?: string): promise.Promise<void> {
    const name = customCtrlName || 'mat-select-2';
    // Only try to set the page size, if the page size control is shown
    // Pagination controls will be hidden if there are not enough items to require more than 1 page
    return this.getPageSizeForm().isDisplayed().then(displayed => {
      if (displayed) {
        this.scrollToBottom();
        this.getPageSizeForm().fill({ [name]: pageSize });
        return this.scrollToTop();
      }
    });
  }

  getPageSizeForm(): FormComponent {
    return new FormComponent(this.findPageSizeSection());
  }

  getNavFirstPage(): Component {
    return new Component(this.locator.element(by.css('.mat-paginator-navigation-first')));
  }

  getNavLastPage(): Component {
    return new Component(this.locator.element(by.css('.mat-paginator-navigation-last')));
  }

  getNavPreviousPage(): Component {
    return new Component(this.locator.element(by.css('.mat-paginator-navigation-previous')));
  }

  getNavNextPage(): Component {
    return new Component(this.locator.element(by.css('.mat-paginator-navigation-next')));
  }

}

export class ListEmptyComponent extends Component {
  constructor(listComponent: ElementFinder) {
    super(listComponent.element(by.css('.list-component__no-entries')));
  }

  getDefault(): Component {
    return new Component(element(by.css('.list-component__default-no-entries')));
  }

  getCustom(): Component {
    return new Component(element(by.css('app-no-content-message')));
  }

  getCustomLineOne(): promise.Promise<string> {
    return this.getCustom().getComponent().element(by.css('.first-line')).getText();
  }
}
/**
 * Page Object for the List component
 */
export class ListComponent extends Component {

  public table: ListTableComponent;

  public cards: ListCardComponent;

  public header: ListHeaderComponent;

  public pagination: ListPaginationComponent;

  public empty: ListEmptyComponent;

  constructor(public locator: ElementFinder = element(by.tagName('app-list'))) {
    super(locator);
    this.table = new ListTableComponent(locator);
    this.header = new ListHeaderComponent(locator);
    this.cards = new ListCardComponent(locator, this);
    this.pagination = new ListPaginationComponent(locator);
    this.empty = new ListEmptyComponent(locator);
  }

  isTableView(): promise.Promise<boolean> {
    const listElement = this.locator.element(by.css('.list-component'));
    return this.hasClass('list-component__table', listElement);
  }

  isCardsView(): promise.Promise<boolean> {
    const listElement = this.locator.element(by.css('.list-component'));
    return this.hasClass('list-component__cards', listElement);
  }

  private getLoadingIndicator(): ElementFinder {
    return this.locator.element(by.css('.list-component > mat-progress-bar'));
  }

  isLoading(): promise.Promise<boolean> {
    return this.locator.element(by.css('.list-component > mat-progress-bar')).isPresent();
  }

  waitForLoadingIndicator(): promise.Promise<any> {
    return browser.wait(until.visibilityOf(this.getLoadingIndicator()), 1000);
  }

  waitForNoLoadingIndicator(timeout = 10000): promise.Promise<any> {
    return browser.wait(until.invisibilityOf(this.getLoadingIndicator()), timeout);
  }

  getTotalResults(): promise.Promise<number> {
    // const paginator = new PaginatorComponent();
    return this.pagination.isDisplayed().then(havePaginator => {
      if (havePaginator) {
        return this.pagination.getTotalResults();
      }
      return this.isCardsView().then(haveCardsView => {
        if (haveCardsView) {
          return this.cards.getCards().count();
        }
        return this.table.getRows().count();
      });
    });
  }

  /**
   *
   * @param count Wait until the list has the specified total number of results
   */
  waitForTotalResultsToBe(count: number, timeout = 5000, timeoutMsg = 'Timed out waiting for total results') {
    const totalResultsIs = async (): Promise<boolean> => {
      const actual = await this.getTotalResults();
      return actual === count;
    };

    browser.wait(totalResultsIs, 10000, timeoutMsg);
  }
}