| /** | 
|  * @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 }; |