| /** | 
|  * @file        S&OP List component to wrap common methods the team encounter during development | 
|  * @description List class extending libappbase's ListBase. | 
|  * 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 { ListRow } from '../e2elib/lib/src/pageobjects/list/listrow.component'; | 
| import { DialogBase } from '../libappbase/dialogbase'; | 
| import { ColumnIDValueMap, ListBase } from '../libappbase/listbase'; | 
| import { lowercaseFirstLetter } from '../libappbase/utils'; | 
| import { Timeout } from '../libmp/appmp'; | 
| import { QUtils } from '../e2elib/lib/src/main/qutils.class'; | 
| import { ContextMenuItemSOP, ContextMenuSOP } from './contextmenusop'; | 
| import { Form } from '../e2elib/lib/src/pageobjects/form.component'; | 
| import { WebMessageBox } from '../libappbase/webmessagebox'; | 
| import { ButtonSOP } from './buttonsop'; | 
| import { QContextMenu } from '../e2elib/lib/api/pageobjects/qcontextmenu.component'; | 
| import { QToastButtonName } from '../e2elib/lib/api/types'; | 
| import { DataHierarchy, DataHierarchyProvider } from './datahierarchy'; | 
|   | 
| /** | 
|  * List row checkbox check/uncheck status, visually decorated to indicate if checkbox itself is checked or descendants row checkbox check/uncheck status | 
|  * (example: checkbox decorated with light triangle if any descendants checked). | 
|  */ | 
| export enum DataRowCheckBoxDecoration { | 
|   DarkDecorated = 'DarkDecorated', | 
|   LightDecorated = 'LightDecorated', | 
|   None = 'None', | 
| } | 
|   | 
| type listDataHierarchy = [DataHierarchyProvider, DataHierarchy]; | 
|   | 
| export class ListSOP<DialogType extends DialogBase, ColumnType> extends ListBase { | 
|   public readonly dlgCreateEdit: DialogType; | 
|   /** | 
|    * To retrieve row primary key column names when assert fails (for error message to be informative) | 
|    */ | 
|   protected rowPrimaryColumnNames: ColumnType; | 
|   /** | 
|    * Provide the single column name which is for All constraints. In case team renamed this. | 
|    */ | 
|   protected rowAllConstraintsColumnName: ColumnType; | 
|   private readonly _cmMenuMap: Map<string, QContextMenu> = new Map(); | 
|   | 
|   public static getFieldsKeyValueAsString(fields: any): string { | 
|     const arr: string[] = []; | 
|     for (const [key, value] of Object.entries(fields)) { | 
|       arr.push(`${key} = "${value}"`); | 
|     } | 
|   | 
|     return arr.join(', '); | 
|   } | 
|   | 
|   public static getFieldsKeyValueArrayAsString(parents?: any[], delimiter: string = ', '): string { | 
|     const arr: string[] = []; | 
|     if (parents) { | 
|       for (const x of parents) { | 
|         arr.push(ListSOP.getFieldsKeyValueAsString(x)); | 
|       } | 
|     } | 
|     return arr.join(delimiter); | 
|   } | 
|   | 
|   public constructor(componentPath: string, dialog: DialogType) { | 
|     super(componentPath); | 
|     this.dlgCreateEdit = dialog; | 
|   } | 
|   | 
|   /** | 
|    * Subclass to override. Based on query to DataHierarchyProvider, format into the subclass row and parent values format (as each subclass list has its own column definitions). | 
|    * | 
|    * @param _hierarchyProvider The object that holds the list hierarchy data (parent-child names). | 
|    * @param _name The child row name to retrieve parent hierarchy information. | 
|    * @returns Row and parent values based on the subclass column values. | 
|    */ | 
|   public getHierarchy(_hierarchyProvider: DataHierarchyProvider, _name: DataHierarchy): [ColumnType, ColumnType[] | undefined] { | 
|     throw Error('Subclass to override getHierarchy.'); | 
|   } | 
|   | 
|   /** | 
|    * If no row provided, focus list (to allow action buttons to refresh the enable state) and click the action button. | 
|    * If row(s) provided, the selection must be done prior to calling this method. | 
|    * | 
|    * @param actionButton Action button to click (e.g Create/Edit button). Always use button defined in view instead of instantiating one inside the list/dialog. | 
|    * @param contextMenu (Optional) Context menu item name of the button to click. | 
|    * @param rows (Optional) Selected row(s) to then click action bar button. | 
|    * @returns The dialog that triggered from the action button click (e.g Create dialog when clicking Create action button) | 
|    */ | 
|   public async clickActionButton<AltDialog extends Form>(actionButton: ButtonSOP, contextMenu?: string, rows?: ListRow | ListRow[], altDialog?: AltDialog): Promise<DialogType> { | 
|     // If no row(s) provided, focus on the list | 
|     if (rows === undefined) { | 
|       await this.focus(); | 
|     } | 
|   | 
|     await actionButton.waitForScreenUpdate(Timeout.ButtonState); | 
|   | 
|     if (contextMenu) { | 
|       await actionButton.clickDropdownAndSelectMenu(contextMenu); | 
|     } else { | 
|       await actionButton.click(); | 
|     } | 
|   | 
|     // If alternative dialog defined, wait for it before proceed next | 
|     if (altDialog) { | 
|       altDialog = await this.waitAndReturnDialog(altDialog); | 
|     } | 
|   | 
|     // 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 action bar button which triggers a dialog prompt for user to choose Yes or No to proceed. | 
|    * | 
|    * @param actionButton Action button to click (e.g Create/Edit button). Always use button defined in view instead of instantiating one inside the list/dialog. | 
|    * @param dismissPrompt Click Yes or No to dismiss dialog. | 
|    */ | 
|   public async clickActionButtonAndDismissPrompt(actionButton: ButtonSOP, dismissPrompt: QToastButtonName = QToastButtonName.Yes): Promise<void> { | 
|     await actionButton.click(); | 
|   | 
|     const webMessageBox = new WebMessageBox(); | 
|     await webMessageBox.waitUntilPresent(false); | 
|   | 
|     if (dismissPrompt === QToastButtonName.Yes) { | 
|       await webMessageBox.selectYes(); | 
|     } else { | 
|       await webMessageBox.selectNo(); | 
|     } | 
|     await webMessageBox.waitUntilHidden(); | 
|   } | 
|   | 
|   /** | 
|    * Collapse the child row and all its parent rows. | 
|    * | 
|    * @param columns Most inner row. | 
|    * @param parentValues One or more parent rows if any. | 
|    */ | 
|   public async collapseChildRowAndParents(columns: ColumnType, parentValues?: ColumnType[]): Promise<void> { | 
|     if (parentValues) { | 
|       // Collapse the immediate parent and go to next parent | 
|       for (let i = parentValues.length - 1; i >= 0; i--) { | 
|         // If root row, there's no parent thus we pass undefined | 
|         // Else get all its parent (slice 2nd argument i is not inclusive) | 
|         const curParent = i === 0 ? undefined : parentValues.slice(0, i); | 
|   | 
|         const curRow = await this.getRow(parentValues[i], curParent); | 
|         await curRow.collapseRow(); | 
|       } | 
|     } else { | 
|       const rootRow = await this.getRow(columns); | 
|       await rootRow.collapseRow(); | 
|     } | 
|   } | 
|   | 
|   /** | 
|    * Open dialog for edit via double click. | 
|    * | 
|    * @param columns The row to retrieve | 
|    * @param parentValues (Optional) The parent row to identify the target row | 
|    * @return the dialog that is opened | 
|    */ | 
|   public async doubleClickRow(columns: ColumnType, parentValues?: ColumnType[]): Promise<DialogType> { | 
|     const row = await this.getRow(columns, parentValues); | 
|     await row.doubleClick(); | 
|     return this.getDialog(); | 
|   } | 
|   | 
|   /** | 
|    * Expand root or children row in hierarchy list. | 
|    * | 
|    * @param columns The row to retrieve | 
|    * @param parentValues [Optional] One or more parent row to identify the target row | 
|    */ | 
|   public async expandRow(columns: ColumnType, parentValues?: ColumnType[]): Promise<void> { | 
|     if (parentValues === undefined) { | 
|       // Cannot use getRow() as parentValues not defined and listbase getRowByValueInHierarchy expects parents always | 
|       const row = await this.getRow(columns); | 
|       await row.expandRow(); | 
|     } else { | 
|       // Lazy and save lines of code, call getRow which uses getRowByValueInHierarchy (which will expand in hierarchy list) | 
|       await this.getRow(columns, parentValues); | 
|     } | 
|   } | 
|   | 
|   /** | 
|    * Convert List interface field (key) to string. | 
|    * Use case, when invoke e2e List/ListBase method which argument is column name (string), we need convert the interface field (key) to string. | 
|    * This is compromise as we need the List interface to be used as argument for other methods such as verifyRowExist, etc. Thus this minor | 
|    * inconvinence to need to extract/convert List interface field (key) to its string equivalent (colum name). | 
|    * | 
|    * @param interfaceColumn The interface field to extract column name as string | 
|    * @returns The column name as string which can be use to invoke e2e List/ListBase methods. | 
|    * @example | 
|    * interface ListStrategyColumn { Name?: string } | 
|    * | 
|    * <List>.extractColumnName({Name: ''}); // Just provide empty value, does not matter | 
|    * This returns the Name as string 'Name' | 
|    */ | 
|   public extractColumnName(interfaceColumn: ColumnType): string { | 
|     const keys = Object.keys(interfaceColumn); | 
|     // TODO: Cannot use expect as we may want to use this method in "it" description as expect can only use within "it"/beforeall, etc | 
|     // See if need explore other ways to check and warn user if more than 1 interface fields defined? | 
|     // expect(keys.length).toBe(1, 'Configuration error: Can only define 1 field from the List interface as we want to extract 1 interface field to convert to string.'); | 
|   | 
|     return keys[0]; | 
|   } | 
|   | 
|   public async getDialog(): Promise<DialogType> { | 
|     return this.waitAndReturnDialog(this.dlgCreateEdit); | 
|   } | 
|   | 
|   /** | 
|    * Get a list row by defining one or more columns with the option to search based on parent row(s). | 
|    * | 
|    * @param targetRowValues The row to retrieve. Pass in listDataHierarchy if data is hierarchical in nature and parentValues argument will be ignored. | 
|    * @param parentValues One or more parent row to identify the target row | 
|    * @returns ListRow representing the target row | 
|    */ | 
|   public async getRow(targetRowValues: ColumnType | listDataHierarchy, parentValues?: ColumnType[]): Promise<ListRow> { | 
|     [targetRowValues, parentValues] = this.getRowAndParentColumnTypes(targetRowValues, parentValues); | 
|   | 
|     // Get the target row mapping format that e2elib List recognizes | 
|     const columnToValueMaps: ColumnIDValueMap[] = this.getColumnIDValueMapFromColumnType(targetRowValues); | 
|   | 
|     // If parent defined (hierarchical list), get the parent(s) mapping format that e2elib List recogizes | 
|     if (parentValues) { | 
|       const parents: ColumnIDValueMap[][] = []; | 
|       // Can define to get row based on more than 1 parent row | 
|       for (const parentValue of parentValues) { | 
|         const parentRow: ColumnIDValueMap[] = this.getColumnIDValueMapFromColumnType(parentValue); | 
|         parents.push(parentRow); | 
|       } | 
|       return this.getRowByValueInHierarchy(columnToValueMaps, parents); | 
|     } | 
|   | 
|     // If no parent defined, use the simpler API | 
|     return this.getRowByValue(columnToValueMaps); | 
|   } | 
|   | 
|   /** | 
|    * Get a list rows by defining one or more columns with the option to search based on parent row(s). | 
|    * | 
|    * @param targetRowValues The rows to retrieve | 
|    * @param parentValues One or more parent row to identify the target rows | 
|    * @returns ListRows representing the target rows | 
|    */ | 
|   public async getRows(targetRowValues: ColumnType[], parentValues?: ColumnType[]): Promise<ListRow[]> { | 
|     const rows: ListRow[] = []; | 
|     for (const x of targetRowValues) { | 
|       const r = await this.getRow(x, parentValues); | 
|       rows.push(r); | 
|     } | 
|   | 
|     return rows; | 
|   } | 
|   | 
|   /** | 
|    * Get ListRows based on the row indexes (0-based). e2elib does not have method to retrieve multiple rows based on indexes. | 
|    * | 
|    * @param indexes Row numbers to retrieve (e.g [0, 1]). | 
|    * @returns One or more ListRows. | 
|    */ | 
|   public async getRowsByIndex(indexes: number[]): Promise<ListRow[]> { | 
|     const rows: ListRow[] = []; | 
|     for (const i of indexes) { | 
|       const row = await this.getRowByIndex(i); | 
|       rows.push(row); | 
|     } | 
|   | 
|     return rows; | 
|   } | 
|   | 
|   /** | 
|    * Get row image attribute name (resize column if needed). | 
|    * | 
|    * @param row List row. | 
|    * @param columnName Image attribute column name. | 
|    * @returns The image name for the row. | 
|    */ | 
|   public async getRowImageAttribute(row: ListRow, columnName: string): Promise<string> { | 
|     const recordCellValues: Record<string, string> = await this.getCellValuesFromRow(row, [columnName]); | 
|     const [, imageName] = Object.entries(recordCellValues)[0]; | 
|   | 
|     return imageName; | 
|   } | 
|   | 
|   /** | 
|    * Get the row's primary key column values, to output during assert failure for easier debugging. | 
|    * | 
|    * @param row ListRow object to retrieve the primary key column values. | 
|    * @returns String in the format 'Row with column 'x': value = y, column 'x2': value = y2' | 
|    */ | 
|   public async getRowPrimaryColumnNames(row: ListRow): Promise<string> { | 
|     const names: string[] = []; | 
|   | 
|     if (this.rowPrimaryColumnNames) { | 
|       const recordCellValues: Record<string, string> = await this.getCellValuesFromRow(row, Object.keys(this.rowPrimaryColumnNames)); | 
|       for (const [cellName, cellValue] of Object.entries(recordCellValues)) { | 
|         names.push(`column '${cellName}': value = ${cellValue}`); | 
|       } | 
|     } else { | 
|       names.push('<no primary column names defined, please set when instantiating List>'); | 
|     } | 
|   | 
|     return `Row with ${  names.join()}`; | 
|   } | 
|   | 
|   /** | 
|    * Right click on list white space (empty area). | 
|    * Use case right click on empty area and for script script to verify list menu/action bar button not enabled as no row selected. | 
|    */ | 
|   public async rightClickOnWhiteSpace(): Promise<void> { | 
|     await QUtils.rightClickElementWithMenu(this.listArea); | 
|   } | 
|   | 
|   /** | 
|    * Select all rows (via Ctrl+A shortcut), right click and select context menu. | 
|    * Example usage: Select all rows to delete via context menu. | 
|    * | 
|    * @param contextMenu Context menu item name. | 
|    */ | 
|   public async selectAllAndSelectContextMenu(contextMenu: ContextMenuItemSOP): Promise<void> { | 
|     await this.focus(); // Focus list before press Ctrl+A shortcut to select all rows | 
|     await QUtils.keyBoardAction(['a'], true); | 
|   | 
|     // Ensure there's selected rows | 
|     const selectedRows = await this.getInboundSelectedRows(); | 
|     expect(selectedRows.length > 0).toBe(true, 'Expected all rows to be selected using Ctrl+A shortcut.'); | 
|   | 
|     // Right click first row and select menu | 
|     const row = await this.getRowByIndex(0); | 
|     await this.selectContextMenu(contextMenu, row); | 
|   } | 
|   | 
|   /** | 
|    * Right click on row(s) if provided else right click on list and select context menu. | 
|    * Typical use to click create context menu to bring up create/edit dialog as well as right click one or more rows to edit/delete/copy. | 
|    * | 
|    * @param contextMenu Context menu to click. | 
|    * @param rows [Optional] One or more rows to right click before selecting context menu. | 
|    * @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 selectContextMenu<AltDialog extends Form>(contextMenu: ContextMenuItemSOP, rows?: ListRow | ListRow[], altDialog?: AltDialog): Promise<[DialogType, AltDialog | undefined]> { | 
|     // e2e rightClick needs 1 row to right click on, thus if 1 or more rows defined, get the first row | 
|     const row = Array.isArray(rows) ? rows[0] : rows; | 
|   | 
|     if (row) { | 
|       const qmodifiers = Array.isArray(rows) ? { ctrl: true } : undefined; | 
|       await row.rightClick(qmodifiers, this.findCreateContextMenu(contextMenu.ContextMenu), contextMenu.Name); | 
|     } else { | 
|       await this.rightClick(undefined, this.findCreateContextMenu(contextMenu.ContextMenu), contextMenu.Name); | 
|     } | 
|   | 
|     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 on list or row and select context menu. A web message box prompts and define whether to click Yes or No. | 
|    * | 
|    * @param contextMenu Context menu item to click. | 
|    * @param rows One or more row to right click on. | 
|    * @param dismissPrompt Click Yes or No to dismiss dialog. | 
|    */ | 
|   public async selectContextMenuAndDismissPrompt(contextMenu: ContextMenuItemSOP, rows?: ListRow | ListRow[], dismissPrompt: QToastButtonName = QToastButtonName.Yes): Promise<void> { | 
|     const webMessageBox = new WebMessageBox(); | 
|     await this.selectContextMenu(contextMenu, rows, webMessageBox); | 
|   | 
|     await webMessageBox.waitUntilPresent(false); | 
|     if (dismissPrompt === QToastButtonName.Yes) { | 
|       await webMessageBox.selectYes(); | 
|     } else { | 
|       await webMessageBox.selectNo(); | 
|     } | 
|     await webMessageBox.waitUntilHidden(); | 
|   } | 
|   | 
|   /** | 
|    * Select a row based on one or more columns. | 
|    * | 
|    * @param columns The columns criteria to select a row. Derived class to provide actual interface to govern what columns to be select per list. | 
|    * @returns The selected row. | 
|    */ | 
|   public async selectRow(columns: ColumnType, parentValues?: ColumnType[], holdCtrl?: boolean): Promise<ListRow> { | 
|     const columnToValueMaps: ColumnIDValueMap[] = this.getColumnIDValueMapFromColumnType(columns); | 
|   | 
|     let parentRow: ListRow | undefined; | 
|     if (parentValues) { | 
|       const immediateParentRow = parentValues.pop() as ColumnType; // Take immediate parent (remove it from the array) | 
|   | 
|       // Guard against only 1 parent hierarchy (as after pop will have empty parentValues) | 
|       parentValues = parentValues.length === 0 ? undefined : parentValues; | 
|   | 
|       parentRow = await this.getRow(immediateParentRow, parentValues); | 
|     } | 
|   | 
|     await this.selectListRowsByValue([columnToValueMaps], holdCtrl, undefined, undefined, parentRow); | 
|     // Not using getHighlightedRows and getInboundSelectedRows as the above select too fast for get 'selected' rows to be correct | 
|     // It wrongly select the 1st row in list, so we use getRow() to be absolutely sure | 
|     return this.getRow(columns); | 
|   } | 
|   | 
|   /** | 
|    * Select one or more rows. Method name to distinguish with e2e selectRows. | 
|    * | 
|    * @param columns The columns criteria to select a row. Derived class to provide actual interface to govern what columns to be select per list. | 
|    * @returns Selected row(s). | 
|    */ | 
|   public async selectListRows(columns: ColumnType[]): Promise<ListRow[]> { | 
|     const rows: ListRow[] = []; | 
|     for (let x = 0; x < columns.length; x++) { | 
|       // 2nd row onwards press Ctrl to multi-select | 
|       const row = x >= 1 ? await this.selectRow(columns[x], undefined, true) : await this.selectRow(columns[x]); | 
|       rows.push(row); | 
|     } | 
|   | 
|     return rows; | 
|   } | 
|   | 
|   /** | 
|    * Verify that the children of the parent row is either ALL checked or unchecked | 
|    * | 
|    * @param isChecked True if all children are checked, false if all childrens are unchecked | 
|    * @param targetRowValues The rows to retrieve | 
|    * @param parentValues The parent row | 
|    */ | 
|   public async verifyChildrenRowsChecked(isChecked: Boolean, targetRowValues: ColumnType, parentValues?: ColumnType[]): Promise<void> { | 
|     const [effectiveTargetRowValues, effectiveParentValues] = this.getRowAndParentColumnTypes(targetRowValues, parentValues); | 
|     const parentRow = await this.getRow(effectiveTargetRowValues, effectiveParentValues); | 
|     const numberOfChild = await parentRow.getChildRowCount(); | 
|     for (let i = 0; i < numberOfChild; i++) { | 
|       const currentChild = await parentRow.getChildRow(i); | 
|       await this.verifyRowChecked(currentChild, undefined, isChecked); | 
|     } | 
|   } | 
|   | 
|   /** | 
|    * Select rows and right click to assert if context menu is enabled. | 
|    * | 
|    * @param contextMenu Context menu name to verify. | 
|    * @param rows One or most ListRow to select and right click. | 
|    * @param errorMessage [Optional] Custom error when assert fails, else use default message. | 
|    */ | 
|   public async verifyContextMenuEnabled(contextMenu: ContextMenuItemSOP, rows?: ListRow[], errorMessage?: string, expectedTooltip?: string): Promise<void> { | 
|     await this.verifyContextMenuEnabledHelper(true, contextMenu, rows, errorMessage, expectedTooltip); | 
|   } | 
|   | 
|   /** | 
|    * Select rows and right click to assert if context menu is disabled. | 
|    * | 
|    * @param contextMenu Context menu name to verify. | 
|    * @param rows One or most ListRow to select and right click. | 
|    * @param errorMessage [Optional] Custom error when assert fails, else use default message. | 
|    */ | 
|   public async verifyContextMenuDisabled(contextMenu: ContextMenuItemSOP, rows?: ListRow[], errorMessage?: string, expectedTooltip?: string): Promise<void> { | 
|     await this.verifyContextMenuEnabledHelper(false, contextMenu, rows, errorMessage, expectedTooltip); | 
|   } | 
|   | 
|   /** | 
|    * Verify specific row cell(s) if rendered as bold (technical term, computed element font weight >= 700). | 
|    * Example: Verify a row's Name and ID columns are bold. | 
|    * | 
|    * @param row ListRow to verify. | 
|    * @param columns The columns for the row to verify. | 
|    */ | 
|   public async verifyRowCellBold(row: ListRow, columns: ColumnType): Promise<void> { | 
|     const errorMsg: string[] = []; | 
|   | 
|     for (const [colName, expectedBold] of Object.entries(columns)) { | 
|       const expectedCellBold = expectedBold === 'true'; | 
|       const fontWeight = await (await row.getCell(colName)).element.getCssValue('font-weight'); | 
|       const cssBold = Number(fontWeight); | 
|   | 
|       // https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight | 
|       const matchExpectation = expectedCellBold ? cssBold >= 700 : cssBold < 700; | 
|       if (!matchExpectation) { | 
|         const errorCell = expectedCellBold ? 'expected = bold, actual = not bold' : 'expected = not bold, actual = bold'; | 
|         errorMsg.push(`Column ${colName}: ${errorCell}.`); | 
|       } | 
|     } | 
|   | 
|     if (errorMsg.length >= 1) { | 
|       throw Error(`${await this.getRowPrimaryColumnNames(row)}. ${errorMsg.join('<br>')}`); | 
|     } | 
|   } | 
|   | 
|   /** | 
|    * Verify if a row exists in list. Can use one or more columns as criteria. | 
|    * | 
|    * @param columns The columns criteria to use. Derived class to provide actual interface to govern what columns can be used. | 
|    */ | 
|   public async verifyRowExists(columns: ColumnType, parentValues?: ColumnType[]): Promise<void> { | 
|     await this.verifyRowExistHelper(true, columns, parentValues); | 
|   } | 
|   | 
|   public async verifyRowNotExist(columns: ColumnType, parentValues?: ColumnType[]): Promise<void> { | 
|     await this.verifyRowExistHelper(false, columns, parentValues); | 
|   } | 
|   | 
|   /** | 
|    * Verify if the row is checked or unchecked. | 
|    * | 
|    * @param targetRowValues The row to retrieve. | 
|    * @param parentValues One or more parent row to identify the target row. | 
|    */ | 
|   public async verifyRowChecked(targetRowValues: ColumnType | ListRow | null, parentValues?: ColumnType[], expectedCheck: Boolean = true): Promise<void> { | 
|     if (targetRowValues === null) { | 
|       throw Error('No row have been identified. Please make sure a row is provided.'); | 
|     } | 
|     const errorSuffix = expectedCheck ? 'checked' : 'unchecked'; | 
|     const isCheck = targetRowValues instanceof ListRow ? await targetRowValues.isChecked() : await (await this.getRow(targetRowValues, parentValues)).isChecked(); | 
|     expect(isCheck).toBe(expectedCheck, `Expect row ${ListSOP.getFieldsKeyValueAsString(targetRowValues)} to be ${errorSuffix}.`); | 
|   } | 
|   | 
|   /** | 
|    * Verify list row checkbox visual decoration to indicate if checkbox itself is checked or descendants row checkbox check/uncheck status | 
|    * Example : Checkbox decorated with light triangle if any descendants checked | 
|    * Checkbox decorated with dark triangle if Row checkbox is checked | 
|    * | 
|    * @param targetRowValues The target row checkbox to be varified. Pass in listDataHierarchy if data is hierarchical in nature and parentValues argument will be ignored. | 
|    * @param expectedDecoration The decoration in the checkbox, example : DataRowCheckBoxDecoration.LightDecorated | 
|    * @param parentValues All the parent of the target row | 
|    */ | 
|   public async verifyRowCheckBoxDecoration(targetRowValues: ColumnType | listDataHierarchy, expectedDecoration: DataRowCheckBoxDecoration, parentValues?: ColumnType[]): Promise<void> { | 
|     const [effectiveTargetRowValues, effectiveParentValues] = this.getRowAndParentColumnTypes(targetRowValues, parentValues); | 
|     const row = await this.getRow(effectiveTargetRowValues, effectiveParentValues); | 
|     switch (expectedDecoration) { | 
|       case DataRowCheckBoxDecoration.None: | 
|         expect(await row.isDecorated()).toBe(false, `Verify row checkbox for ${ListSOP.getFieldsKeyValueAsString(targetRowValues)}. Expected checkbox to not have decorate.`); | 
|         return; | 
|       case DataRowCheckBoxDecoration.LightDecorated: | 
|         expect(await row.isLightDecorated()).toBe(true, `Verify row checkbox for ${ListSOP.getFieldsKeyValueAsString(targetRowValues)}. Expected checkbox to have light blue triangle decoration at bottom right.`); | 
|         return; | 
|       case DataRowCheckBoxDecoration.DarkDecorated: | 
|         expect(await row.isDarkDecorated()).toBe(true, `Verify row checkbox for ${ListSOP.getFieldsKeyValueAsString(targetRowValues)}. Expected checkbox to have dark blue triangle decoration at bottom right.`); | 
|         return; | 
|       default: | 
|         throw Error(`Invalid DataRowCheckBoxDecoration value '${expectedDecoration}' passed to verify row checkbox decoration.`); | 
|     } | 
|   } | 
|   | 
|   /** | 
|    * Verify row's All Constraints column. | 
|    * | 
|    * @param row Row to verify. | 
|    * @param hasConstraint Expected to have violation or not. | 
|    */ | 
|   public async verifyRowHasConstraintViolation(row: ListRow, hasConstraint: boolean = true): Promise<void> { | 
|     if (!this.rowAllConstraintsColumnName) { | 
|       throw Error(`Please define 'rowAllConstraintsColumnName' during ${this.componentName} constructor. This represents the all constraints column name.`); | 
|     } | 
|   | 
|     // Use libappbase method as it will resize the column if not visible (else some column will show as ... which fails the test) | 
|     const recordCellValues: Record<string, string> = await this.getCellValuesFromRow(row, Object.keys(this.rowAllConstraintsColumnName)); | 
|   | 
|     // Get the first result (index 0) as we assume only 1 constraint column in list | 
|     const [, actualHasConstraint] = Object.entries(recordCellValues)[0]; | 
|     const expectedImgName = hasConstraint ? 'SADSMILEY3D' : 'EMPTY'; | 
|     expect(actualHasConstraint).toBe(expectedImgName, `Verify all constraints column for ${  await this.getRowPrimaryColumnNames(row)}`); | 
|   } | 
|   | 
|   /** | 
|    * Verify row is a root level row in list (no parent row). | 
|    * | 
|    * @param row Row to verify. | 
|    */ | 
|   public async verifyRowHasNoParentRow(row: ListRow): Promise<void> { | 
|     const parentRow = await row.getParentRow(); | 
|     if (parentRow !== null) { | 
|       throw Error(`Expect '${await this.getRowPrimaryColumnNames(row)}' to have no parent row, currently belongs to parent '${await this.getRowPrimaryColumnNames(parentRow)}'.`); | 
|     } | 
|   } | 
|   | 
|   /** | 
|    * Verify row contains the ondraw image. | 
|    * | 
|    * @param row Row to verify. | 
|    * @param imgName Image name to verify if exists as part of row ondraw images. | 
|    * @returns True if ondraw image is found. | 
|    */ | 
|   public async verifyRowHasOnDrawImage(row: ListRow, imgName: string): Promise<boolean> { | 
|     const rowOnDrawImages = await row.getOndrawImageElements(); | 
|     const foundImage = rowOnDrawImages.indexOf(imgName) !== -1; | 
|   | 
|     expect(foundImage).toBe(true, `Not able find ondraw image '${imgName}' for ${await this.getRowPrimaryColumnNames(row)}. Current row ondraw image(s) = ${rowOnDrawImages.join(', ')}`); | 
|   | 
|     return foundImage; | 
|   } | 
|   | 
|   public async verifyRowValues(row: ListRow, expectedColumnValues: ColumnType): Promise<void> { | 
|     // Use LibAppBase method to get the target row's cell values that we interested (based on passed in interface) | 
|     // The LibAppbase method needs array like type so we pass in the ColumnType (interface) keys | 
|     const recordCellValues: Record<string, string> = await this.getCellValuesFromRow(row, Object.keys(expectedColumnValues)); | 
|     const expectedColumnValuesEntries = Object.entries(expectedColumnValues); | 
|   | 
|     // For each cell value, compare against expected values | 
|     const errorMessages: string[] = []; | 
|     for (const [cellName, cellValue] of Object.entries(recordCellValues)) { | 
|       let expectedValue = ''; | 
|       for (const [expectedColName, expectedColValue] of expectedColumnValuesEntries) { | 
|         // Libappbase's ListBase.getCellValuesFromRow() convert 1st letter to lowercase and remove space between words | 
|         // Thus we need do the same before comparing if the column names matches | 
|         let expectedColName2 = expectedColName.replace(/\s/, ''); | 
|         expectedColName2 = lowercaseFirstLetter(expectedColName2); | 
|         if (expectedColName2 === cellName) { | 
|           expectedValue = expectedColValue; | 
|         } | 
|       } | 
|   | 
|       if (cellValue !== expectedValue) { | 
|         // TODO: 'cellName' returned from LibAppBase method changse the first letter to lowercase (and remove spaces between column name), but OK for now until there's concern | 
|         errorMessages.push(`Column '${cellName}': expected value = ${expectedValue}, actual = ${cellValue}`); | 
|       } | 
|     } | 
|   | 
|     if (errorMessages.length > 0) { | 
|       errorMessages.unshift(await this.getRowPrimaryColumnNames(row)); // Insert row primary key column names to easy identify the issue | 
|     } | 
|   | 
|     expect(errorMessages.join('<br>')).toBe(''); | 
|   } | 
|   | 
|   /** | 
|    * 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 list "${await this.getComponentLabel()}" total row fail. ${appendFailureMessage}`); | 
|   } | 
|   | 
|   /// // Private methods goes here | 
|   | 
|   /** | 
|    * Convert interface key-value pairs of column name and value into e2elib ColumnIDValueMap format. | 
|    * | 
|    * @param columns The interface key-value pairs of column name and value. | 
|    * @returns ColumnIDValueMap format to be used to invoke operations on e2elib List | 
|    */ | 
|   private getColumnIDValueMapFromColumnType(columns: ColumnType): ColumnIDValueMap[] { | 
|     const columnToValueMaps: ColumnIDValueMap[] = []; | 
|     let onDrawImageName = ''; | 
|     for (const [colName, colValue] of Object.entries(columns)) { | 
|       // If ondrawimage, do not create as column-value pair as we will append to the first column-value later on | 
|       if (colName === 'OndrawImageName') { | 
|         onDrawImageName = colValue as string; | 
|       } else { | 
|         columnToValueMaps.push({ columnID: colName, value: colValue}); | 
|       } | 
|   | 
|       // If ondraw image defined, find the first column to append the additional ondraw image name (assumption first column indeed has the ondraw image) | 
|       if (onDrawImageName !== '') { | 
|         columnToValueMaps[0].additionAttribute = [{attribute: 'OndrawImageName', value: onDrawImageName}]; | 
|       } | 
|     } | 
|   | 
|     return columnToValueMaps; | 
|   } | 
|   | 
|   /** | 
|    * Returns the context menu from stored map if previously created, else create, store and return it. | 
|    * | 
|    * @param name Context menu name. | 
|    * @returns Context menu from stored map. | 
|    */ | 
|   private findCreateContextMenu(name: string): QContextMenu | undefined { | 
|     let cmMenu: QContextMenu | undefined; | 
|     if (this._cmMenuMap.has(name)) { | 
|       cmMenu = this._cmMenuMap.get(name); | 
|     } else { | 
|       cmMenu = new QContextMenu(name); | 
|       this._cmMenuMap.set(name, cmMenu); | 
|     } | 
|   | 
|     return cmMenu; | 
|   } | 
|   | 
|   /** | 
|    * Return parent row(s) column names and values, to be used in assert error. | 
|    * Simple format: <column name 1>,<column value 1>,<column name 2>,<column value 2>.... | 
|    * | 
|    * @param columns Array of interface key-value pairs of column name and value. Each array element denotes a parent row. | 
|    * @returns Parent row(s) column names and values. | 
|    */ | 
|   private getFormattedParentRows(columns: ColumnType[] | undefined): string { | 
|     const value: string[] = []; | 
|     for (const c of columns!) { | 
|       value.push(Object.entries(c).join()); // <column name 1>,<column value 1>,<column name 2>,<column value 2>.... | 
|     } | 
|   | 
|     return `<br>Parent row(s) = ${  value.join('<br>')}`; // Put a newline for each parent row | 
|   } | 
|   | 
|   private getRowAndParentColumnTypes(targetRowValues: ColumnType | listDataHierarchy, parentValues?: ColumnType[]): [ColumnType, ColumnType[] | undefined] { | 
|     let thisRow: ColumnType; | 
|     let parentRows: ColumnType[] | undefined; | 
|     // Cast argument to check if it's listDataHierarchy | 
|     const isArgListDataHierarchy = (targetRowValues as listDataHierarchy)[0] !== undefined; | 
|   | 
|     if (isArgListDataHierarchy) { | 
|       [thisRow, parentRows] = this.getHierarchy(targetRowValues[0], targetRowValues[1]); | 
|     } else { | 
|       thisRow = targetRowValues as ColumnType; | 
|       parentRows = parentValues; | 
|     } | 
|   | 
|     return [thisRow, parentRows]; | 
|   } | 
|   | 
|   private async verifyRowExistHelper(checkExist: boolean, columns: ColumnType, parentValues?: ColumnType[]): Promise<void> { | 
|     const columnToValueMaps: ColumnIDValueMap[] = this.getColumnIDValueMapFromColumnType(columns); | 
|   | 
|     // If parent defined (hierarchical list), get the parent(s) mapping format that e2elib List recogizes | 
|     let parents!: ColumnIDValueMap[][]; | 
|     if (parentValues) { | 
|       // Can define to get row based on more than 1 parent row | 
|       parents = []; | 
|       for (const parentValue of parentValues) { | 
|         const parentRow: ColumnIDValueMap[] = this.getColumnIDValueMapFromColumnType(parentValue); | 
|         parents.push(parentRow); | 
|       } | 
|     } | 
|   | 
|     const hasRow = await this.hasRow(columnToValueMaps, parents); | 
|     const errorMsgParentRows = !hasRow && parentValues ? this.getFormattedParentRows(parentValues) : ''; // If row not found, output parent row(s) if parent defined | 
|     const errorMsgPrefix = checkExist ? 'Cannot find row for' : 'Expected not to find row for'; | 
|   | 
|     expect(hasRow).toBe(checkExist, `${errorMsgPrefix} ${Object.entries(columns).join()}${errorMsgParentRows}.`); // Simple way to output the key-value pairs separated by comma (e.g Start,14-Jan-2021,Rate,0.567) | 
|   } | 
|   | 
|   /** | 
|    * Select rows and right click to assert if context menu is enabled/disabled. | 
|    * | 
|    * @param checkEnabled Boolean indicating whether context menu is expected to be enabled or not. | 
|    * @param contextMenu Context menu name to verify. | 
|    * @param rows One or most ListRow to select and right click. | 
|    * @param errorMessage [Optional] Custom error when assert fails, else use default message. | 
|    * @param expectedTooltip [Optional] Tooltip to assert. | 
|    */ | 
|   private async verifyContextMenuEnabledHelper(checkEnabled: boolean, contextMenu: ContextMenuItemSOP, rows?: ListRow[], errorMessage?: string, expectedTooltip?: string): Promise<void> { | 
|     const cmMenu = this.findCreateContextMenu(contextMenu.ContextMenu); | 
|     try { | 
|       if (rows) { | 
|         await this.selectRows(rows); | 
|         await rows[0].rightClick({ ctrl: true }, cmMenu); | 
|       } else { | 
|         // If no row provided, right click on list | 
|         await this.rightClickOnWhiteSpace(); | 
|       } | 
|   | 
|       // After select and right click, need let context menu enabed/disabled properly | 
|       await this.waitForScreenUpdate(Timeout.ButtonState); | 
|   | 
|       const failMessageSuffix = checkEnabled ? 'enabled' : 'disabled'; | 
|       const failMessage = errorMessage ? errorMessage : `Expected context menu ${contextMenu} to be ${failMessageSuffix}.`; | 
|       // Only query tooltip if an expected tooltip defined to prevent error tooltip not visible thrown | 
|       const [isEnabled, disabledTooltip] = await ContextMenuSOP.verifyIsMenuItemClickable(cmMenu!, contextMenu.Name, expectedTooltip !== undefined); | 
|       expect(isEnabled).toBe(checkEnabled, failMessage); | 
|   | 
|       // If defined expected tooltip and currently menu disabled, to assert the tooltip matches | 
|       if (expectedTooltip && !isEnabled) { | 
|         expect(disabledTooltip).toBe(expectedTooltip, `Verify fail for menu '${contextMenu}' disabled tooltip.`); | 
|       } | 
|   | 
|       await cmMenu!.dismiss(); // Dismiss context menu else subsequent actions will fail as menu blocks the list | 
|     } catch (e) { | 
|       const isMenuVisible = await cmMenu!.isVisible(); | 
|       if (isMenuVisible) { | 
|         // Dismiss context menu else subsequent actions will fail as menu blocks the list | 
|         await cmMenu!.dismiss(); | 
|       } | 
|   | 
|       throw e; // Re-throw error else it will silently disappear | 
|     } | 
|   } | 
|   | 
|   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 stepList = { | 
|   doubleClickRow: (listName: string, fields: any): string => `In list ${listName}, double click row ${ListSOP.getFieldsKeyValueAsString(fields)}.`, | 
|   dropRowsOnTargetRow: (sourceListName: string, sourceRows: any[], targetRow: any, targetListName?: string): string => { | 
|     const targetList = targetListName ? ` in target list ${targetListName}` : ''; | 
|     return `In list ${sourceListName}, drag row(s) ${ListSOP.getFieldsKeyValueArrayAsString(sourceRows)} onto row ${ListSOP.getFieldsKeyValueAsString(targetRow)}${targetList}.`; | 
|   }, | 
|   dropRowOnWhiteSpace: (sourceListName: string, sourceRows: any[], targetListName: string): string => { | 
|     return `From list "${sourceListName}", drag row(s) ${ListSOP.getFieldsKeyValueArrayAsString(sourceRows)} and drop onto whitespace in list "${targetListName}".`; | 
|   }, | 
|   expandRowInList: (listName: string, row: string | any[]): string => { | 
|     // If array, we will output "parent > child level 1 > child level 2 and so on...." | 
|     const values = typeof row === 'string' ? row : ListSOP.getFieldsKeyValueArrayAsString(row, ' > '); | 
|   | 
|     return `In list "${listName}", click the small arrow to expand row "${values}".`; | 
|   }, | 
|   focus: (listName: string): string => `Focus list "${listName}".`, | 
|   focusListClickActionButton: (listName: string, actionButton: string): string => `Focus list "${listName}" and click action bar button ${actionButton}.`, | 
|   rightClickSelectMenu: (listName: string, menuLabel: string): string => `In list "${listName}", right click select menu "${menuLabel}".`, | 
|   selectAllRows: (listName: string): string => `In list ${listName}, select all rows.`, | 
|   selectRow: (listName: string, fields: any): string => `In list ${listName}, select row ${ListSOP.getFieldsKeyValueAsString(fields)}.`, | 
|   selectAllRowsAndClickMenu: (listName: string, menuLabel: string): string => `In list "${listName}", select all rows and right click select menu "${menuLabel}".`, | 
|   selectRows: (listName: string, fields: any[]): string => { | 
|     const fieldsArr: string[] = []; | 
|     fields.forEach((x: any) => fieldsArr.push(ListSOP.getFieldsKeyValueAsString(x))); | 
|   | 
|     const result = `In list ${listName}, select rows ${fieldsArr.join(' || ')}.`; | 
|     return result; | 
|   }, | 
|   selectRowAndClickMenu: (listName: string, fields: any, menuLabel: string): string => `In list "${listName}", select row "${ListSOP.getFieldsKeyValueAsString(fields)}" and right click select menu "${menuLabel}".`, | 
|   selectRowsAndClickMenu: (listName: string, fields: any[], menuLabel: string): string => `In list "${listName}", select rows "${ListSOP.getFieldsKeyValueArrayAsString(fields, ' | ')}" and right click select menu "${menuLabel}".`, | 
|   selectRowNumAndClickActionButton: (listName: string, rowNum: number, actionButton: string): string => `In list "${listName}", select row number ${rowNum} and click action bar button ${actionButton}.`, | 
|   selectRowNumAndClickMenu: (listName: string, rowNum: number, menuLabel: string): string => `In list ${listName}, select row number ${rowNum} and right click select menu "${menuLabel}".`, | 
|   selectRowsByIndex: (listName: string, rowIndexes: number[]): string => `In list ${listName}, select rows with index ${rowIndexes.join(', ')}.`, | 
|   toggleRowCheckbox: (listName: string, fields: any, expectedChecked: boolean): string => `In list ${listName}, toggle row checked = ${expectedChecked} for ${ListSOP.getFieldsKeyValueAsString(fields)}.`, | 
|   verifyCheckBoxRowDecoration: (listName: string, rowName: string, expectedDecoration: DataRowCheckBoxDecoration): string => `In list ${listName}, verify that the checkbox's bottom right of current row = ${rowName} is ${expectedDecoration}.`, | 
|   verifyCheckBoxRowNoDecoration: (listName: string, rowName: string): string => `In list ${listName}, verify that the checkbox's bottom right of current row = ${rowName} is not decorated.`, | 
|   verifyChildrenRowsChecked: (listName: string, fields: string): string => `In list ${listName}, verify all children checkbox of parent row = ${fields} have been checked.`, | 
|   verifyChildrenRowsUnchecked: (listName: string, fields: string): string => `In list ${listName}, verify all children checkbox of parent row = ${fields} have been unchecked.`, | 
|   verifyContextMenuDisabled: (listName: string, menuLabel: string, expectedTooltip?: string): string => { | 
|     const finalExpectedTooltip = expectedTooltip ? ` with precondition "${expectedTooltip}"` : ''; | 
|     return `In list ${listName}, verify menu '${menuLabel}' disabled${finalExpectedTooltip}.`; | 
|   }, | 
|   verifyRowChecked: (listName: string, fields: any): string => `In list ${listName}, verify row ${ListSOP.getFieldsKeyValueAsString(fields)} is checked.`, | 
|   verifyRowConstraintViolation: (listName: string, fields: any, expectedHasViolation: boolean): string => `In list ${listName}, verify constraint violation = ${expectedHasViolation} for row ${ListSOP.getFieldsKeyValueAsString(fields)}.`, | 
|   // Some test will select row index without knowing specific, thus next step will just verify row deleted without explicit specify details. | 
|   verifyRowDeleted: (listName: string): string => `In list ${listName}, verify row deleted.`, | 
|   verifyRowExists: (listName: string, fields: any, parents?: any[]): string => `In list ${listName}, verify row exists for ${ListSOP.getFieldsKeyValueAsString(fields)} and parent = ${ListSOP.getFieldsKeyValueArrayAsString(parents)}.`, | 
|   verifyRowNotExist: (listName: string, fields: any, parents?: any[]): string => `In list ${listName}, verify row not exist for ${ListSOP.getFieldsKeyValueAsString(fields)} and parent = ${ListSOP.getFieldsKeyValueArrayAsString(parents)}.`, | 
|   verifyRowValues: (listName: string, fields: any, verifyValues: any): string => `In list ${listName}, verify row ${ListSOP.getFieldsKeyValueAsString(fields)} with columns value equal ${ListSOP.getFieldsKeyValueAsString(verifyValues)}.`, | 
|   verifyRowValuesForRowNum: (listName: string, rowNum: number, fields: any): string => `In list ${listName}, verify row number ${rowNum} values equal ${ListSOP.getFieldsKeyValueAsString(fields)}.`, | 
|   verifyTotalRow: (listName: string, expectedTotal: number): string => `In list ${listName}, verify total row = ${expectedTotal}.`, | 
|   verifyRowHasOnDrawImage: (listName: string, fields: any, image: string): string => `In list ${listName}, verify row ${ListSOP.getFieldsKeyValueAsString(fields)} has on draw image "${image}".`, | 
| }; | 
|   | 
| export { stepList as StepList }; |