/** 
 | 
 * @file        S&OP MatrixEditor component to wrap common methods the team encounter during development 
 | 
 * @description MatrixEditor class extending libappbase's MatrixEditorBase. 
 | 
 * All S&OP page objects inherit from our own class (inheriting e2e/libappbase), but we can propose common methods to them. 
 | 
 * @author      Clarence (clarence.chan@3ds.com) 
 | 
 * @copyright   Dassault Systèmes 
 | 
 */ 
 | 
import { CellLocator, MatrixEditorBase } from '../libappbase/matrixeditorbase'; 
 | 
import { browser, by } from '../e2elib/node_modules/protractor/built'; 
 | 
import { Timeout } from '../libmp/appmp'; 
 | 
import { QUtils } from '../e2elib/lib/src/main/qutils.class'; 
 | 
import { DialogBase } from '../libappbase/dialogbase'; 
 | 
import { Form } from '../e2elib/lib/src/pageobjects/form.component'; 
 | 
import { KeyboardKey } from '../e2elib/lib/src/helper/enumhelper'; 
 | 
import { ButtonSOP } from './buttonsop'; 
 | 
import { QContextMenu } from '../e2elib/lib/api/pageobjects/qcontextmenu.component'; 
 | 
import { ContextMenuSOP } from './contextmenusop'; 
 | 
import { QConsole } from '../e2elib/lib/src/helper/qconsole'; 
 | 
import { ColorSOP, ColorSOPList } from './colorsop'; 
 | 
  
 | 
export class MatrixEditorSOP<DialogType extends DialogBase> extends MatrixEditorBase { 
 | 
  /** 
 | 
   * Empty matrix cell value. Cannot use empty string as getCellValue will fail. 
 | 
   * Need use the Unicode representation of space. 
 | 
   * https://www.compart.com/en/unicode/U+0020 
 | 
   */ 
 | 
  public static CELLEMPTY = '\u0020'; 
 | 
  
 | 
  private readonly _dlgCreateEdit: DialogType; 
 | 
  public cmMenu: QContextMenu; 
 | 
  
 | 
  public constructor(componentPath: string, contextMenuName: string, dialog: DialogType) { 
 | 
    super(componentPath); 
 | 
    this.cmMenu = new QContextMenu(contextMenuName); 
 | 
    this._dlgCreateEdit = dialog; 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Focus matrix (to allow action buttons to refresh the enable state) and click the action button. 
 | 
   * 
 | 
   * @param actionButton Action button to click (e.g Create/Edit button). Always use button defined in view instead of instantiating one inside the matrix/list/dialog. 
 | 
   * @returns The dialog that triggered from the action button click (e.g Create dialog when clicking Create action button) 
 | 
   */ 
 | 
  public async clickActionButton(actionButton: ButtonSOP): Promise<DialogType> { 
 | 
    await this.focus(); 
 | 
    await actionButton.click(); 
 | 
  
 | 
    // TODO: For now always return dialog assuming is create/edit that brings up dialog. Find generic way decide if need to e.g Delete doesn't require it. 
 | 
    return this.getDialog(); 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Click matrix top left to select all cells (built-in software behavior). 
 | 
   * Wait for first row to proper selected by inspecting the first row cell has the correct CSS style to indicate selected. 
 | 
   * This to prevent caller from calling getCell method too early when matrix cells not proper selected yet resulting in possible 
 | 
   * stale element reference. (This is a theory for now, will need monitor more pipeline runs to deem stable). 
 | 
   */ 
 | 
  public async clickTopLeftSelectAllCells(): Promise<void> { 
 | 
    await this.selectAll(); 
 | 
  
 | 
    await browser.wait( 
 | 
      async () => { 
 | 
        try { 
 | 
          const cell = await this.getCell(0, 0, 0); // Get first row, first cell and inspect CSS if being focused (seems to work even if cell has no object/grey out) 
 | 
          const style = await cell.element.getAttribute('class'); 
 | 
  
 | 
          return style.indexOf('componentFocused') >= 0; 
 | 
        } catch { 
 | 
          return false; // if getCell() error, keep trying 
 | 
        } 
 | 
      }, 
 | 
      Timeout.Short, 
 | 
      'Timeout waiting for all cells to be selected.', 
 | 
    ); 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Edits the cells with the provided cell locators to the new value. 
 | 
   * 
 | 
   * @param cellLocators Location of cells that needs to be editted 
 | 
   * @param value New value of cells 
 | 
   */ 
 | 
  public async editCellValues(cellLocators: CellLocator[], value: string): Promise<void> { 
 | 
    await this.selectCells(cellLocators); 
 | 
    await QUtils.keyBoardAction([value]); 
 | 
    await QUtils.keyBoardAction([KeyboardKey.ENTER]); 
 | 
    await QConsole.waitForStable(); 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Wait dialog present and returns it. 
 | 
   * 
 | 
   * @returns The dialog instance. 
 | 
   */ 
 | 
  public async getDialog(): Promise<DialogType> { 
 | 
    return this.waitAndReturnDialog(this._dlgCreateEdit); 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Right click matrix cell and select context menu to popup dialog. 
 | 
   * 
 | 
   * @param contextMenu Context menu item names (if menu has sub-menus, pass each menu delimited by pipe |). 
 | 
   * @param rowID RowID based on string or index. 
 | 
   * @param attributeID AttributeID based on string or index. Pass 0 if matrix not using. 
 | 
   * @param columnID ColumnID based on string or index. 
 | 
   * @param altDialog [Optional] Alternative dialog to use instead of the main dialog set during constructor. Typical use case if list triggers few different dialogs. 
 | 
   * This cater also for WebMessageBox (the dialog prompt to request user to confirm deletion for example). 
 | 
   * @returns Tuple consisting of main dialog and alternative dialog (which can be undefined). 
 | 
   */ 
 | 
  public async rightClickCellSelectContextmenu<AltDialog extends Form>(contextMenu: string, rowID: number | string, attributeID: number | string, columnID: number | string, altDialog?: AltDialog): Promise<[DialogType, AltDialog | undefined]> { 
 | 
    await this.focus(); // Below fail to find row if matrix not focus 
 | 
  
 | 
    const cell = await this.getCell(rowID, attributeID, columnID); 
 | 
  
 | 
    // If delimiter pipe present = sub-menu present, tokenize into array 
 | 
    const menuPaths = ContextMenuSOP.getMenuPaths(contextMenu); 
 | 
    await cell.rightClick(undefined, this.cmMenu, menuPaths); 
 | 
  
 | 
    const dlg = await this.getDialog(); 
 | 
    if (altDialog) { 
 | 
      altDialog = await this.waitAndReturnDialog(altDialog); 
 | 
    } 
 | 
  
 | 
    // Return tuple (1st dialog = main dialog set during constructor, 2nd dialog only used if list triggers more than 1 dialog thus possibly undefined) 
 | 
    return [dlg, altDialog]; 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Right click matrix row and select context menu to popup dialog. 
 | 
   * 
 | 
   * @param contextMenu Context menu item name. 
 | 
   * @param rowID RowID based on string or index. 
 | 
   * @param attributeID AttributeID based on string or index. Pass 0 (number) if matrix not using. 
 | 
   * @returns The dialog instance. 
 | 
   */ 
 | 
  public async rightClickRowSelectContextmenu(contextMenu: string, rowID: number | string, attributeID?: number | string): Promise<DialogType> { 
 | 
    await this.focus(); // Below fail to find row if matrix not focus 
 | 
  
 | 
    await this.rightClickOnRow(rowID, attributeID); 
 | 
    await this.cmMenu.selectMenuItem(contextMenu); 
 | 
  
 | 
    return this.getDialog(); 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Right click matrix and select context menu to popup dialog. 
 | 
   * 
 | 
   * @param contextMenu Context menu item name. 
 | 
   * @returns The dialog instance. 
 | 
   */ 
 | 
  public async rightClickMatrixSelectContextmenu(contextMenu: string): Promise<DialogType> { 
 | 
    await this.focus(); // Below fail to find row if matrix not focus 
 | 
  
 | 
    // Right click qColumnHeading which is the top left cell since no row/column is present for us to interact with 
 | 
    await QUtils.rightClickElementWithMenu(this.element.element(by.className('qColumnHeading')), undefined, undefined, this.cmMenu, contextMenu); 
 | 
  
 | 
    return this.getDialog(); 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Use DOWN keyboard action until the first row header that is currently visible is the sought one 
 | 
   * 
 | 
   * @param rowHeader name of header to be scrolled to 
 | 
   */ 
 | 
  public async scrollToRowByRowHeader(rowHeader: string): Promise<void> { 
 | 
    await this.focus(); // Focus first else control might still be in other UI element 
 | 
  
 | 
    let firstVisibleRowHeader = await this.getRowHeader(0); 
 | 
    while (firstVisibleRowHeader !== rowHeader) { 
 | 
      await QUtils.keyBoardAction([KeyboardKey.DOWN]); 
 | 
      firstVisibleRowHeader = await this.getRowHeader(0); 
 | 
    } 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Select one or more cells by holding down the Ctrl key. 
 | 
   * 
 | 
   * @param cellLocators One or more cell to select. CellLocator interface to specify the row, attribute and column info. 
 | 
   */ 
 | 
  public async selectCells(cellLocators: CellLocator[]): Promise<void> { 
 | 
    for (let index = 0; index < cellLocators.length; index++) { 
 | 
      const elem = cellLocators[index]; 
 | 
      const cell = await this.getCell(elem.rowName, elem.attributeId!, elem.columnName); 
 | 
      if (index === 0) { 
 | 
        await QUtils.leftClickElement(cell.element); 
 | 
      } else { 
 | 
        // Hold Ctrl key to multi-select cells 
 | 
        await QUtils.leftClickElement(cell.element, { control: true }); 
 | 
      } 
 | 
    } 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Copy cell(s) with shortcut 'Ctrl + C'. 
 | 
   */ 
 | 
  public async copyCellsWithShortcut(): Promise<void> { 
 | 
    await QUtils.keyBoardAction(['c'], true); 
 | 
    await QConsole.waitForStable(); 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Paste cell(s) with shortcut 'Ctrl + V'. 
 | 
   */ 
 | 
  public async pasteCellsWithShortcut(): Promise<void> { 
 | 
    await QUtils.keyBoardAction(['v'], true); 
 | 
    await this.waitForScreenUpdate(); 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Verify matrix cell value. If matrix does not use attribute for row, pass attributeID = 0 (number). 
 | 
   * 
 | 
   * @param rowID Row ID based on string or index. 
 | 
   * @param attributeID AttributeID based on string or index. Pass 0 (number) if matrix not using. 
 | 
   * @param columnID ColumnID based on string or index. 
 | 
   * @param expectedCellValue Value to assert. 
 | 
   */ 
 | 
  public async verifyCellValue(rowID: number | string, attributeID: number | string, columnID: number | string, expectedCellValue: string): Promise<void> { 
 | 
    const cell = await this.getCell(rowID, attributeID, columnID); 
 | 
    const cellValue = await cell.getValue(); 
 | 
  
 | 
    const attributeIDString = attributeID === 0 ? '' : `, attribute = ${attributeID}`; // If matrix using attribute(s) per row, else show empty in failure message 
 | 
    expect(cellValue).toBe(expectedCellValue, `Verify fail for matrix cell value (row = ${rowID}${attributeIDString} and column = ${columnID}.`); 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Verify total row matches, optionally providing a failure message to append if expect fails. 
 | 
   * 
 | 
   * @param expectedTotal Expected total row count. 
 | 
   * @param additionalFailureMessage [Optional] Failure message to append (use to define scenario specific information). 
 | 
   */ 
 | 
  public async verifyTotalRow(expectedTotal: number, additionalFailureMessage?: string): Promise<void> { 
 | 
    const appendFailureMessage = additionalFailureMessage ? additionalFailureMessage : ''; 
 | 
  
 | 
    expect(await this.getRowCount()).toBe(expectedTotal, `Verify matrix total row fail. ${appendFailureMessage}`); 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Compare matrix cell background color code with pass-in color code, these values should be same. 
 | 
   * If not, fail the test with error message. 
 | 
   * 
 | 
   * @param row Matrix row name. 
 | 
   * @param column Matrix column name. 
 | 
   * @param colorCode RGB color code for the browser being used to run this script. Chrome & Firefox has different color code for the same color. 
 | 
   */ 
 | 
  public async verifyCellColor(row: number | string, column: string, colorCode: string): Promise<void> { 
 | 
    const matrixCell = await this.getCell(row, '', column); 
 | 
    const value = await matrixCell.element.getCssValue('background-color'); 
 | 
    expect(value).toBe(colorCode, 'Verify matrix cell color fail.'); 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Verify if cell has no data and the color of cell is gray. 
 | 
   * 
 | 
   * @param rowID Matrix row id. 
 | 
   * @param attributeID Matrix attribute ID (if matrix enables multiple attributes). 
 | 
   * @param columnID Matrix column name. 
 | 
   */ 
 | 
  public async verifyCellHasNoData(rowID: string | number, attributeID: string | number, columnID: string): Promise<void> { 
 | 
    await this.verifyCellValue(rowID, attributeID, columnID, MatrixEditorSOP.CELLEMPTY); 
 | 
    await this.verifyCellColor(rowID, columnID, matrixCellColors.noData().Rgb); 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Verifies if the cell is not editable and checks the tooltip shown. 
 | 
   * 
 | 
   * @param rowID Row of cell to check if disabled 
 | 
   * @param attributeID Attribute of cell to check if disabled 
 | 
   * @param columnID Column of cell to check if disabled 
 | 
   * @param tooltip Tooltip expected when clicked cell is attempted to be edited 
 | 
   */ 
 | 
  public async verifyCellNotEditable(rowID: string | number, attributeID: string | number, columnID: string | number, tooltip?: string): Promise<void> { 
 | 
    const cell = await this.getCell(rowID, attributeID, columnID); 
 | 
    expect(await cell.isEditable()).toBe(false, `Expected cell not editable at row "${rowID}" and column "${columnID}".`); 
 | 
    if (tooltip) { 
 | 
      const toastMessage = await cell.doubleClick(true); 
 | 
      expect(toastMessage).toBe(tooltip, `Verify cell disabled tooltip fail at row "${rowID}" and column "${columnID}".`); 
 | 
    } 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Verify that the matrix column header exists. 
 | 
   * 
 | 
   * @param matrixColumnHeader The name of the matrix column header. 
 | 
   */ 
 | 
  public async verifyColumnExist(matrixColumnHeader: string): Promise<void> { 
 | 
    expect(await this.hasColumn(matrixColumnHeader)).toBe(true, `Unable to find matrix column '${matrixColumnHeader}'.`); 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Verify that the matrix row exists. 
 | 
   * 
 | 
   * @param matrixRow The name of the matrix row. 
 | 
   */ 
 | 
  public async verifyRowExist(matrixRow: string): Promise<void> { 
 | 
    expect(await this.hasRow(matrixRow)).toBe(true, `Unable to find matrix row '${matrixRow}'.`); 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Verify that the matrix row doesn't exists. 
 | 
   * 
 | 
   * @param matrixRow The name of the matrix row. 
 | 
   */ 
 | 
  public async verifyRowNotExist(matrixRow: string): Promise<void> { 
 | 
    expect(await this.hasRow(matrixRow)).toBe(false, `Expect matrix row '${matrixRow}' not exist.`); 
 | 
  } 
 | 
  
 | 
  /** 
 | 
   * Verify that the sequence of matrix row. 
 | 
   * 
 | 
   * @param expectedRowsSequence Expected row names to verify in sequence. 
 | 
   */ 
 | 
  public async verifyRowsSequence(expectedRowsSequence: string[]): Promise<void> { 
 | 
    expect(await this.getRowCount()).toBe(expectedRowsSequence.length, `Verify rows sequence failed. There should be exactly ${expectedRowsSequence.length} rows.`); 
 | 
    const currentMatrixRow: string[] = []; 
 | 
    const errMessage: string[] = []; 
 | 
  
 | 
    for (let index = 0; index < expectedRowsSequence.length; index++) { 
 | 
      currentMatrixRow.push(await this.getRowHeader(index)); 
 | 
      const currentExpectedMatrixRow = expectedRowsSequence[index]; 
 | 
      const currentActualMatrixRow = await this.getRowHeader(index); 
 | 
      if (currentActualMatrixRow !== currentExpectedMatrixRow) { 
 | 
        errMessage.push(currentExpectedMatrixRow); 
 | 
      } 
 | 
    } 
 | 
    expect(errMessage.length === 0).toBe(true, `Rows "${errMessage.join(', ')}" not sorted in correct order. Current matrix rows "${currentMatrixRow.join(', ')}" `); 
 | 
  } 
 | 
  
 | 
  private async waitAndReturnDialog<T extends Form>(dlg: T): Promise<T> { 
 | 
    await dlg.waitForScreenUpdate(); 
 | 
    return dlg; 
 | 
  } 
 | 
} 
 | 
  
 | 
// Step description to re-use in spec file to prevent scriptor re-write each time 
 | 
const stepMatrix = { 
 | 
  copyCellsWithShortcut: (matrixName: string): string => `In matrix ${matrixName}, copy cells with shortcut 'Ctrl + C'.`, 
 | 
  editCellValue: (matrixName: string, columnName: string, rowName: string, value: string): string => `In matrix ${matrixName}, set value of cell at row "${columnName}" and column "${rowName}" to "${value}".`, 
 | 
  editCellValues: (matrixName: string, cellLocators: CellLocator[], value: string): string => { 
 | 
    const arr: string[] = cellLocators.map(({ rowName, columnName }: CellLocator) => `row = "${rowName}" with column = "${columnName}"`); 
 | 
    return `In matrix ${matrixName}, set value of cells at ${arr.join(', ')} to "${value}".`; 
 | 
  }, 
 | 
  pasteCellsWithShortcut: (matrixName: string): string => `In matrix ${matrixName}, paste cells with shortcut 'Ctrl + V'.`, 
 | 
  rightClickCellSelectContextmenu: (matrixName: string, rowName: string, columnName: string, menuLabel: string): string => `In matrix ${matrixName}, right click cell for row = '${rowName}', column = '${columnName}' and select menu ${menuLabel}.`, 
 | 
  rightClickMatrixSelectContextmenu: (matrixName: string, menuLabel: string): string => `In matrix ${matrixName}, right click and select menu ${menuLabel}.`, 
 | 
  rightClickRowSelectContextmenu: (matrixName: string, rowName: string, menuLabel: string): string => `In matrix ${matrixName}, right click row '${rowName}' and select menu ${menuLabel}.`, 
 | 
  selectCells: (matrixName: string, cellLocators: CellLocator[]): string => { 
 | 
    const arr: string[] = cellLocators.map(({ rowName, columnName }: CellLocator) => `row = "${rowName}" with column = "${columnName}"`); 
 | 
    return `In matrix ${matrixName}, select cells which ${arr.join(', ')}.`; 
 | 
  }, 
 | 
  verifyCellColor: (matrixName: string, rowName: string, columnName: string, expectedColor: string): string => `In matrix ${matrixName}, verify cell color to be '${expectedColor}' for row = '${rowName}' and column = '${columnName}'.`, 
 | 
  verifyCellHasNoData: (matrixName: string, rowID: string | number, attributeID: string | number, columnID: string): string => `In matrix ${matrixName}, verify cell color to be 'gray' and cell to have no data for row = '${rowID}', attribute = '${attributeID}' and column = '${columnID}'.`, 
 | 
  verifyCellNotEditable: (matrixName: string, rowID: string | number, attributeID: string | number, columnID: string | number, tooltip?: string): string => { 
 | 
    const tooltipMsg = tooltip ? ` and shows tooltip "${tooltip}" when attempting to edit the cell` : ''; 
 | 
  
 | 
    return `In matrix ${matrixName}, verify cell at row "${rowID}", column "${columnID}" and attribute "${attributeID}" is not editable${tooltipMsg}.`; 
 | 
  }, 
 | 
  verifyCellValue: (matrixName: string, rowID: number | string, columnID: number | string, expectedCellValue: string): string => `In matrix ${matrixName}, verify "${expectedCellValue}" is the value of row "${rowID}" and column "${columnID}".`, 
 | 
  verifyRowExists: (matrixName: string, rowName: string): string => `In matrix ${matrixName}, verify row exists for row = ${rowName}.`, 
 | 
  verifyRowsSequence: (matrixName: string, verifyRows: string[]): string => `In matrix ${matrixName}, verify rows sequence = "${verifyRows.join(', ')}".`, 
 | 
  verifyTotalRow: (matrixName: string, totalRow: number): string => `In matrix ${matrixName}, verify total row = ${totalRow}.`, 
 | 
}; 
 | 
  
 | 
// Default matrix cell colors (assuming no representation override) for example if cell has data or not. 
 | 
const matrixCellColors: ColorSOPList = { 
 | 
  hasData: (): ColorSOP => ({ Rgb: 'rgba(0, 0, 0, 0)', Color: 'White', Hex: '' }), 
 | 
  noData: (): ColorSOP => ({ Rgb: 'rgba(0, 0, 0, 0.125)', Color: 'Grey', Hex: '' }), 
 | 
}; 
 | 
  
 | 
export { stepMatrix as StepMatrix }; 
 | 
export { matrixCellColors as MatrixCellColors }; 
 |