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