| import { ColumnMenuName, FilterMenuName, SortDirection } from '../e2elib/lib/src/helper/enumhelper'; | 
| import { ContextMenu } from '../e2elib/lib/src/pageobjects/contextmenu/contextmenu.component'; | 
| import { List, QCellAdditionAttribute } from '../e2elib/lib/src/pageobjects/list/list.component'; | 
| import { ListCell } from '../e2elib/lib/src/pageobjects/list/listcell.component'; | 
| import { ListColumn } from '../e2elib/lib/src/pageobjects/list/listcolumn.component'; | 
| import { ListRow } from '../e2elib/lib/src/pageobjects/list/listrow.component'; | 
| import { protractor, by } from '../e2elib/node_modules/protractor'; | 
| import { FilterManager } from './filtermanager'; | 
| import { EnumLike, getEnumKeyEntries, getHtmlContent, lowercaseFirstLetter } from './utils'; | 
| import { promise } from '../e2elib/node_modules/@types/selenium-webdriver/'; | 
| import { QUtils } from '../e2elib/lib/src/main/qutils.class'; | 
|   | 
| export class ListBase extends List { | 
|   private readonly _dlgFilterManager = new FilterManager(); | 
|   | 
|   /** | 
|    * Configure the list to show specified columns. | 
|    * | 
|    * @param actionBarName action bar name for the list. | 
|    * @param columnMenu column which shall be toggled to show in the list | 
|    * @param columnMenus columns which shall be toggled to show in the list | 
|    * @example | 
|    * await list.configureColumns('abpList', 'OrderID'); | 
|    * await list2.configureColumns('abpList2', 'MachineID', 'MachineName', 'Speed', 'MachineType'); | 
|    */ | 
|   public async configureColumns(actionBarName: string, columnMenu?: ColumnMenuName | string, ...columnMenus: ColumnMenuName[] | string[]): Promise<void> { | 
|     await this.leftClickOnWhiteSpace(); | 
|     if (columnMenu) { | 
|       await super.configureColumns(actionBarName, columnMenu); | 
|     } | 
|     for (const menu of columnMenus) { | 
|       await super.configureColumns(actionBarName, menu); | 
|     } | 
|   } | 
|   | 
|   /** | 
|    * Simulate dragging a list row and drop it on another list row | 
|    * | 
|    * @param sourceRow The list row(s) to be dragged | 
|    * @param targetRow The list row to be dropped | 
|    * @param hasTooltips retrieve the info tooltip while drop is not allow | 
|    * | 
|    * @returns Promise<string> | 
|    */ | 
|   public async dropRowOnTargetRow(sourceRow: ListRow | ListRow[], targetRow: ListRow, hasTooltips: boolean = false): Promise<string> { | 
|     await this.selectRows(Array.isArray(sourceRow) ? sourceRow : [sourceRow]); | 
|     if (Array.isArray(sourceRow)) { | 
|       return QUtils.emitDragAndDropEvent(sourceRow[0].element, targetRow.element, hasTooltips); | 
|     } | 
|     return QUtils.emitDragAndDropEvent(sourceRow.element, targetRow.element, hasTooltips); | 
|   } | 
|   | 
|   /** | 
|    * Simulate dragging a list row and drop it on the white space of the list | 
|    * | 
|    * @param sourceRow Source row to drag | 
|    * @param hasTooltips retrieve the info tooltip while drop is not allow | 
|    */ | 
|   public async dropRowOnWhiteSpace(sourceRow: ListRow | ListRow[], hasTooltips: boolean = false): Promise<string> { | 
|     const dropTarget = this.element.element(by.css('q-scrollarea')); | 
|     await this.selectRows(Array.isArray(sourceRow) ? sourceRow : [sourceRow]); | 
|   | 
|     if (Array.isArray(sourceRow)) { | 
|       return QUtils.emitDragAndDropEvent(sourceRow[0].element, dropTarget, hasTooltips); | 
|     } | 
|     return QUtils.emitDragAndDropEvent(sourceRow.element, dropTarget, hasTooltips); | 
|   } | 
|   | 
|   /** | 
|    * Finds a row (by typing the specified search value) from the list, then gets the row. | 
|    * | 
|    * This method allows auto-navigation to the specified row. | 
|    * | 
|    * @param target target row to be navigated | 
|    * @param parents parent rows of the targeted row (optional: only should be specified for hierarchy list) | 
|    * | 
|    * @return List row found | 
|    */ | 
|   public async findRowByValue(target: ColumnValueSearch, parents?: ColumnValueSearch[]): Promise<ListRow> { | 
|     // Search through the list to ensure the row is visible to avoid undefined row when using getRowByValue() | 
|     if (parents) { | 
|       await this.searchListInHierarchy(target, parents); | 
|     } else { | 
|       await this.searchList(target.columnID, target.searchValue); | 
|     } | 
|     return this.getRowByValue([{ columnID: target.columnID, value: target.searchValue }]); | 
|   } | 
|   | 
|   /** | 
|    * Get all the cells within a list row | 
|    * | 
|    * @param row The row to retrieve it cells | 
|    * | 
|    * @return A Column to Cell Map object. | 
|    */ | 
|   private async getAllCellsInRow(row: ListRow): Promise<Map<string, ListCell>> { | 
|     const cellCount = await row.getCellCount(); | 
|     let columnName = ''; | 
|     const columnCellMap: Map<string, ListCell> = new Map<string, ListCell>(); | 
|   | 
|     for (let i = 0; i < cellCount; i++) { | 
|       const cell = await row.getCell(i); | 
|       const column = await this.getColumnByIndex(i); | 
|   | 
|       try { | 
|         columnName = await column.getValue(); | 
|       } catch { | 
|         await column.resizeColumn(50); | 
|         columnName = await column.getValue(); | 
|       } finally { | 
|         columnName = columnName === '' ? `column${i + 1}` : columnName; | 
|       } | 
|       columnCellMap.set(columnName, cell); | 
|     } | 
|   | 
|     return columnCellMap; | 
|   } | 
|   | 
|   /** | 
|    * Get the cells by columns of a list row | 
|    * | 
|    * @param row The row to retrieve it cells | 
|    * @param columnsIndexName The columns' name/index of the cells to retreive | 
|    */ | 
|   public async getCellsFromRow(row: ListRow, columnsIndexName?: string[] | number[] | EnumLike): Promise<Map<string | number, ListCell>> { | 
|     let columnCellMap: Map<string | number, ListCell> = new Map(); | 
|   | 
|     if (columnsIndexName) { | 
|       // type guarder that checks if columnsIndexName is array or EnumLike, true if it's EnumLike | 
|       const enumLike = (e: string[] | number[] | EnumLike): e is EnumLike => !Array.isArray(e); | 
|   | 
|       if (enumLike(columnsIndexName)) { | 
|         const keys = getEnumKeyEntries(columnsIndexName); | 
|         for (const key of keys) { | 
|           let cell: ListCell; | 
|           try { | 
|             cell = await row.getCell(columnsIndexName[key]); | 
|           } catch { | 
|             await this.resizeColumnWithCoveredName(columnsIndexName[key], 50); | 
|             cell = await row.getCell(columnsIndexName[key]); | 
|           } | 
|           columnCellMap.set(key, cell); | 
|         } | 
|       } else { | 
|         for (const column of columnsIndexName as string[] | number[]) { | 
|           let cell: ListCell; | 
|           try { | 
|             cell = await row.getCell(column); | 
|           } catch { | 
|             if (typeof column === 'string') { | 
|               await this.resizeColumnWithCoveredName(column, 50); | 
|             } | 
|             cell = await row.getCell(column); | 
|           } | 
|           columnCellMap.set(column, cell); | 
|         } | 
|       } | 
|     } else { | 
|       columnCellMap = await this.getAllCellsInRow(row); | 
|     } | 
|   | 
|     return columnCellMap; | 
|   } | 
|   | 
|   /** | 
|    * Get the cell value of the row by column | 
|    * | 
|    * @param columnIndexName Column name of the cell value | 
|    * @param row Row to retrieve the cell value | 
|    * @param isImgAttribute To determine whether pass-in column name is image attribute column | 
|    * @returns the cell value of the row | 
|    */ | 
|   public async getCellValueFromRow(columnIndexName: string | number, rows: ListRow, isImgAttribute?: boolean): Promise<string>; | 
|   | 
|   /** | 
|    * Loop through all the rows to get the cell value by column | 
|    * | 
|    * @param columnIndexName Column name of the cell value | 
|    * @param rows Rows to retrieve the cell value | 
|    * @param isImgAttribute To determine whether pass-in column name is image attribute column | 
|    * @returns Array of cell values | 
|    */ | 
|   public async getCellValueFromRow(columnIndexName: string | number, rows: ListRow[], isImgAttribute?: boolean): Promise<string[]>; | 
|   | 
|   public async getCellValueFromRow(columnIndexName: string | number, rows: ListRow | ListRow[], isImgAttribute: boolean = false): Promise<string | string[]> { | 
|     if (rows instanceof ListRow) { | 
|       const cell = await rows.getCell(columnIndexName); | 
|       const value = isImgAttribute ? await cell.getImageAttribute() : await cell.getValue(); | 
|       return value; | 
|     } else { | 
|       const promises: promise.Promise<string>[] = []; | 
|       for (const row of rows) { | 
|         const value = row.getCell(columnIndexName).then((cell: ListCell) => (isImgAttribute ? cell.getImageAttribute() : cell.getValue())); | 
|         promises.push(value); | 
|       } | 
|       const values = await protractor.promise.all(promises); | 
|       return values; | 
|     } | 
|   } | 
|   | 
|   /** | 
|    * Get all cell values from a given row. | 
|    * | 
|    * @description The function will obtain all cell values from the given row.\ | 
|    * If `columnIDs` is defined, the function will obtain cell values from the given columns, in the given row. | 
|    * | 
|    * @template T Generic type T which extends the properties of either `EnumLike`, `string[]` or `number[]`. | 
|    * @template K Generic type K which stands for key, which extends the properties from the key of type `T` only when it's enumerator-like. | 
|    * @param {ListRow} row `ListRow` The row to retrive its cell values | 
|    * @param {T} columnIDs `T` (Optional) Columns to determine the values to be retrieved. Accepts an enumerator, `string[]` or `number[]`. | 
|    * | 
|    * @return `Promise<Record<K, string>` or `Promise<Record<string, string>` -- depends on the parameter `columnIDs`.\ | 
|    * IF it's enumerator-like then former return type will be used, ELSE IF it's `string[]` or `number[]` then the latter return type will be used. | 
|    * | 
|    * @example |   ...  | Order ID |   Name  | ...and other columns | 
|    *          | imgYes |  12345   | Order 1 | ...and other values | 
|    * // assume row is illustrated like above. | 
|    * // first column is sized shorter than the column name resulting in '...'. Assume the column name as 'ImgPlanned' | 
|    * | 
|    * ----- example usage (most recommended usage) ----- | 
|    * enum OrderColumn { | 
|    * IMGPLANNED = 'imgPlanned'; | 
|    * ORDER_ID = 'Order ID'; | 
|    * NAME = 'Name'; | 
|    * } | 
|    * // type-safe call by passing the entire OrderColumn enum into the method. | 
|    * const {IMGPLANNED, ORDER_ID, NAME} = await getCellValuesInRow(row, OrderColumn); // safe, obtains value from column imgPlanned, Order ID and Name. | 
|    * const {NAME} = await getCellValuesInRow(row, OrderColumn); // safe, obtains value from column Name. | 
|    * const {IMGPLANNED, random1} = await getCellValuesInRow(row, OrderColumn) // ERROR, typescript will auto highlight 'random1' in red. | 
|    * | 
|    * // you can also call such as the line of code below, but TypeScript will not tell you whether the names are correct or not. | 
|    * const {imgPlanned, orderID} = await getCellValuesInRow(row, ['ImgPlanned', 'Order ID']); | 
|    * // thus this is not type-safe and we only recommend to perform type-safe calls. | 
|    */ | 
|   public async getCellValuesFromRow<T extends CellValue, K extends keyof T>( | 
|     row: ListRow, | 
|     columnIDs?: T, | 
|   ): Promise<Record<T extends undefined ? string : T extends any[] ? string : K, string>> { | 
|     const values = {} as any; // type assertion | 
|     const columnCellMap = await this.getCellsFromRow(row, columnIDs); | 
|     // type guarder | 
|     const enumLike = (e: any): e is EnumLike => !Array.isArray(e); | 
|     // if columnsIndexName exists and it's not enum-like, or doesn't exists at all (to prevent lowercasing if columnsIndexName is passed in as enumerator) | 
|     const isNotEnumLike = (columnIDs && !enumLike(columnIDs)) || !columnIDs; | 
|   | 
|     for (const [column, cell] of columnCellMap) { | 
|       let value = await cell.isImageAttributeCell() ? await cell.getImageAttribute() : await cell.getValue(); // Clarence added to check img attribute, create RfC for Abstraction API | 
|       let columnName = column; | 
|       if (isNotEnumLike) { | 
|         if (typeof column === 'string') { | 
|           columnName = column.replace(/\s/, ''); | 
|           columnName = lowercaseFirstLetter(columnName); | 
|         } else if (typeof column === 'number') { | 
|           columnName = column.toString(10); | 
|         } | 
|       } | 
|   | 
|       if (value === '') { | 
|         value = await cell.getImageAttribute(); | 
|       } | 
|   | 
|       values[columnName] = value; | 
|     } | 
|   | 
|     return values; | 
|   } | 
|   | 
|   /** | 
|    * Get the column by the title of the tooltip of the column | 
|    * | 
|    * @param title The title of the tooltip of the column | 
|    */ | 
|   public async getColumnByToolipTitle(title: string): Promise<ListColumn | undefined> { | 
|     const columnCount = await this.getColumnCount(); | 
|     let column: ListColumn; | 
|     let titleFound: string; | 
|   | 
|     for (let i = 0; i < columnCount; i++) { | 
|       column = await this.getColumnByIndex(i); | 
|       const tooltip = await column.getTooltip(true, false, true); | 
|       titleFound = getHtmlContent(tooltip)[0]; // Tooltip title will be at the first array item | 
|   | 
|       if (titleFound === title) { | 
|         return column; | 
|       } | 
|     } | 
|     return undefined; | 
|   } | 
|   | 
|   /** | 
|    * Get all highlighted rows component in the list | 
|    * | 
|    * @returns array of highlighted rows | 
|    */ | 
|   public async getHighlightedRows(): Promise<ListRow[]> { | 
|     const highlightedRows: ListRow[] = []; | 
|     const rows = await this.getAllRows(); | 
|     for (const row of rows) { | 
|       const isHighlighted = await row.isHighlighted(); | 
|   | 
|       if (isHighlighted) { | 
|         highlightedRows.push(row); | 
|       } | 
|     } | 
|   | 
|     return highlightedRows; | 
|   } | 
|   | 
|   /** | 
|    * Get value of inline chart in the cell | 
|    * | 
|    * @param cell List cell to get inline chart value | 
|    * @returns Key value map of chart | 
|    */ | 
|   public async getInlineChartValue(cell: ListCell): Promise<Map<string, string>> { | 
|     const valuePair: Map<string, string> = new Map(); | 
|     const cellValue = await cell.getValue(); | 
|     const dataKeyValues = cellValue.split(';'); | 
|     dataKeyValues.pop(); | 
|     for (const dataKeyValue of dataKeyValues) { | 
|       const pair = dataKeyValue.split(', '); | 
|       const dataKey = pair[0].split('Data-Key: ')[1]; | 
|       const dataValue = pair[1].split('Data-Value: ')[1]; | 
|       valuePair.set(dataKey, dataValue); | 
|     } | 
|     return valuePair; | 
|   } | 
|   | 
|   /** | 
|    * Get last row of availble rows in list | 
|    */ | 
|   public async getLastRow(): Promise<ListRow> { | 
|     const count = await this.getRowCount(); | 
|     const lastRow = await this.getRowByIndex(count - 1); | 
|   | 
|     return lastRow; | 
|   } | 
|   | 
|   /** | 
|    * Get row by passing name of the column, target value, and all parent cell values. Parent cell values and target cell value will be based on columnName. | 
|    * | 
|    * @param columnName Column name to get from | 
|    * @param targetValue Target cell value based on the column name defined | 
|    * @param parentValues All parent cell values, based on the column name defined, to expand | 
|    */ | 
|   public async getRowByCellValue(columnName: string, targetValue: string, parentValues?: string[]): Promise<ListRow> { | 
|     const targetRow: ColumnIDValueMap[] = [{ columnID: columnName, value: targetValue }]; | 
|     if (parentValues) { | 
|       const parents: ColumnIDValueMap[][] = []; | 
|       for (const parentValue of parentValues) { | 
|         const parentRow: ColumnIDValueMap[] = [{ columnID: columnName, value: parentValue }]; | 
|         parents.push(parentRow); | 
|       } | 
|       return this.getRowByValueInHierarchy(targetRow, parents); | 
|     } | 
|     return this.getRowByValue(targetRow); | 
|   } | 
|   | 
|   /** | 
|    * Get row by passing targetRow's ColumnIDValueMap and all parentRows' ColumnIDValueMap. | 
|    * | 
|    * This method can get row that is immediately visible in the current view. If the row is not visible in the current view, it won't able to get the row. | 
|    * | 
|    * @param targetRow target row's column header and cell value mapping | 
|    * @param parents parent rows' column header and cell value mapping | 
|    * @returns target list row | 
|    */ | 
|   public async getRowByValueInHierarchy(targetRow: ColumnIDValueMap[], parents: ColumnIDValueMap[][]): Promise<ListRow> { | 
|     // get first element and remove in array | 
|     const firstParent = parents.shift(); | 
|     if (firstParent === undefined) { | 
|       throw Error('No parent is found in parents parameter.'); | 
|     } | 
|     let parentRow = await this.getRowByValue(firstParent, true); | 
|     await parentRow.expandRow(); | 
|     // If there are still parent remaining, we'll loop through parent array and expand parent row | 
|     if (parents.length > 0) { | 
|       for (const parent of parents) { | 
|         parentRow = await this.getRowByValue(parent, false, parentRow); | 
|         await parentRow.expandRow(); | 
|       } | 
|     } | 
|   | 
|     return this.getRowByValue(targetRow, false, parentRow); | 
|   } | 
|   | 
|   /** | 
|    * Get Multiple Rows based on multiple Values for a specific column | 
|    * | 
|    * @param columnName Column name | 
|    * @param columnValues Values of the column | 
|    * @returns Array of ListRows | 
|    */ | 
|   public async getRowsByCellValuesFromOneColumn(columnName: string, columnValues: string[]): Promise<ListRow[]> { | 
|     const promises: promise.Promise<ListRow>[] = []; | 
|     for (const columnValue of columnValues) { | 
|       promises.push(this.getRowByValue([{ columnID: columnName, value: columnValue }])); | 
|     } | 
|     const values = await protractor.promise.all(promises); | 
|     return values; | 
|   } | 
|   | 
|   /** | 
|    * Check whether a row exists in the list | 
|    * | 
|    * @param targetRow Cell value for the target row | 
|    * @param parentRows All parent cell values, based on the column name defined, to expand | 
|    * | 
|    * @return a boolean (true = exists, false = not exists) indicating whether the row with the passed-in | 
|    * value exists in the list | 
|    */ | 
|   public async hasRow(targetRow: ColumnIDValueMap[], parentRows?: ColumnIDValueMap[][]): Promise<boolean> { | 
|     try { | 
|       if (parentRows) { | 
|         await this.getRowByValueInHierarchy(targetRow, parentRows); | 
|       } else { | 
|         await this.getRowByValue(targetRow); | 
|       } | 
|       return true; | 
|     } catch { | 
|       return false; | 
|     } | 
|   } | 
|   | 
|   /** | 
|    * Perform left click on the empty space in the list. | 
|    */ | 
|   public async leftClickOnWhiteSpace(): Promise<void> { | 
|     const scrollArea = this.element.element(by.css('q-scrollarea')); | 
|     await QUtils.leftClickElement(scrollArea); | 
|   } | 
|   | 
|   /** | 
|    * Open context menu on column | 
|    * | 
|    * @param columnID Target column name / index to open context menu | 
|    */ | 
|   public async openColumnMenu(columnID: string | number): Promise<void> { | 
|     const column = typeof columnID === 'string' ? await this.getColumnByValue(columnID) : await this.getColumnByIndex(columnID); | 
|     if (column) { | 
|       await column.columnMenu(); //  open context menu on the column | 
|     } | 
|   } | 
|   | 
|   /** | 
|    * Trigger filter dialog on column | 
|    * | 
|    * @param columnNameIndex Target column name or index | 
|    * @param contextMenu Context menu triggered | 
|    * @returns Filter manager dialog | 
|    */ | 
|   public async openFilterDialogOnColumn(columnNameIndex: string | number, contextMenu: ContextMenu): Promise<FilterManager> { | 
|     await this.openColumnMenu(columnNameIndex); | 
|     await contextMenu.selectFilterMenu(FilterMenuName.EditFilters); | 
|     await this._dlgFilterManager.waitUntilPresent(); | 
|     return this._dlgFilterManager; | 
|   } | 
|   | 
|   /** | 
|    * Find and resize covered column in list based on pass-in column name | 
|    * | 
|    * @param columnName Target column that need to resize | 
|    * @param size Pixel size to move | 
|    */ | 
|   public async resizeColumnWithCoveredName(columnName: string, size: number): Promise<void> { | 
|     const count = await this.getColumnCount(); | 
|     let isFound = false; | 
|     for (let i = 0; i < count; i++) { | 
|       const column = await this.getColumnByIndex(i); | 
|       try { | 
|         const columnHeader = await column.getValue(); | 
|         if (columnHeader === '…') { | 
|           throw Error; | 
|         } | 
|       } catch { | 
|         // Error will be thrown when getting value (...) in img attribut column | 
|         // Resize column and check whether resized column's name is matched with pass-in column name | 
|         await column.resizeColumn(size); | 
|         const columnNameToCompare = await column.getValue(); | 
|         if (columnNameToCompare !== columnName) { | 
|           await column.resizeColumn(size, false); | 
|         } else { | 
|           isFound = true; | 
|         } | 
|       } | 
|   | 
|       if (isFound) { | 
|         break; | 
|       } | 
|     } | 
|   } | 
|   | 
|   /** | 
|    * Check whether the cell value in rows matches the expected value | 
|    * | 
|    * @param rows rows to verify it cell value | 
|    * @param columnName name of column to identify the cell of the rows | 
|    * @param expectedValue expected value of the cell value | 
|    * @param isImgAttribute — [Default=false] To determine whether cell value is image attribute column | 
|    * | 
|    * @returns boolean indicating valid or invalid | 
|    */ | 
|   public async rowHasValidValue(rows: ListRow[], columnName: string | string[], expectedValue: string | string[], isImgAttribute: boolean = false): Promise<boolean> { | 
|     const columnNames = typeof columnName === 'string' ? [columnName] : columnName; | 
|     let areAllValid = false; | 
|   | 
|     for (const name of columnNames) { | 
|       const cellValues = await this.getCellValueFromRow(name, rows, isImgAttribute); | 
|       areAllValid = Array.isArray(expectedValue) | 
|         ? cellValues.every((cellValue: string) => expectedValue.includes(cellValue)) | 
|         : cellValues.every((cellValue: string) => cellValue === expectedValue); | 
|   | 
|       if (!areAllValid) { | 
|         break; | 
|       } | 
|     } | 
|   | 
|     return areAllValid; | 
|   } | 
|   | 
|   /** | 
|    * Perform search action on list | 
|    * | 
|    * @param columnIndexName Target column name / index | 
|    * @param searchValue Value to search | 
|    * @param defaultRowClick reset the list and scroll to top for re-search a new value | 
|    */ | 
|   public async searchList(columnIndexName: string | number, searchValue: string, defaultRowClick: boolean = true): Promise<void> { | 
|     let index: number; | 
|   | 
|     if (typeof columnIndexName === 'string') { | 
|       const column = await this.getColumnByValue(columnIndexName); | 
|       index = await column.getIndex(); | 
|     } else { | 
|       index = columnIndexName; | 
|     } | 
|     await super.searchList(index, searchValue, defaultRowClick); | 
|   } | 
|   | 
|   /** | 
|    * Perform search action on hierarchy list | 
|    * | 
|    * @param target target row to be navigated | 
|    * @param parents parent rows of the targeted row | 
|    */ | 
|   public async searchListInHierarchy(target: ColumnValueSearch, parents: ColumnValueSearch[]): Promise<void> { | 
|     if (parents.length > 0) { | 
|       for (let i = 0; i < parents.length; i++) { | 
|         const parent = parents[i]; | 
|         await this.searchList(parent.columnID, parent.searchValue); | 
|         const parentRow = (await this.getInboundSelectedRows())[0]; | 
|         await parentRow.expandRow(); | 
|         // click on its first child to update the list column | 
|         const firstChildRow = await parentRow.getChildRow(0); | 
|         if (firstChildRow) { | 
|           await firstChildRow.leftClick(); | 
|           const cellSearch = i === parents.length - 1 ? await firstChildRow.getCell(target.columnID) : await firstChildRow.getCell(parents[i + 1].columnID); | 
|           await cellSearch.leftClick(); | 
|         } | 
|       } | 
|     } | 
|     await this.searchList(target.columnID, target.searchValue, false); | 
|   } | 
|   | 
|   /** | 
|    * Select multiple rows | 
|    * | 
|    * @param rows The rows to be selected | 
|    */ | 
|   public async selectRows(rows: ListRow[]): Promise<void> { | 
|     await rows[0].leftClick(); | 
|     for (let i = 1; i < rows.length; i++) { | 
|       await rows[i].leftClick({ control: true }); | 
|     } | 
|   } | 
|   | 
|   /** | 
|    * Sort column in list | 
|    * | 
|    * @param columnName Target column that need to sort | 
|    * @param isAscending To sort column in ascending order (default = true, false for descending order) | 
|    * @param isImageAttribute Boolean value indicate whether the column is an image attribute column | 
|    * @param ctrl Ctrl modifier key when click (default = false) | 
|    */ | 
|   public async sortColumn(columnName: string, isAscending: boolean = true, isImageAttribute: boolean = false, ctrl: boolean = false): Promise<void> { | 
|     if (isImageAttribute) { | 
|       await this.resizeColumnWithCoveredName(columnName, 50); | 
|     } | 
|     const idColumn = await this.getColumnByValue(columnName); | 
|     const sortDirection = isAscending ? SortDirection.ASC : SortDirection.DESC; | 
|     await idColumn.setSortDirection(sortDirection, ctrl); | 
|   } | 
|   | 
|   /** | 
|    * Toggle ON/OFF the checkbox of a list row | 
|    * | 
|    * @param row Target row to toggle on/off the checkbox | 
|    * @param expectedState Boolean indicate the expected checkbox state (true = checked, false = unchecked) | 
|    * | 
|    * @return the checkbox state after it's toggled | 
|    */ | 
|   public async toggleRowCheckbox(row: ListRow, expectedState: boolean): Promise<boolean> { | 
|     const currentState = await row.isChecked(); | 
|   | 
|     if (currentState !== expectedState) { | 
|       await this.scrollToRow(row); // S&OP added to scroll to view before click | 
|       await row.checkboxClick(); | 
|     } | 
|   | 
|     return row.isChecked(); | 
|   } | 
| } | 
|   | 
| /** | 
|  * An interface that consists of a map of columnID and value. | 
|  * ``` | 
|  * { | 
|  *   columnID: string | number; | 
|  *   value: string; | 
|  * } | 
|  * ``` | 
|  */ | 
| export interface ColumnIDValueMap { | 
|   columnID: string | number; | 
|   value: string; | 
|   additionAttribute?: QCellAdditionAttribute[]; // S&OP added as e2elib able distinguish 2 rows with same name by ondrawimagename | 
| } | 
|   | 
| /** | 
|  * An interface used for searching in row. Contains a map of columnID and search value. | 
|  * ``` | 
|  * { | 
|  *   columnID: string | number; | 
|  *   searchValue: string; | 
|  * } | 
|  * ``` | 
|  */ | 
| export interface ColumnValueSearch { | 
|   columnID: string | number; | 
|   searchValue: string; | 
| } | 
|   | 
| export type CellValue = EnumLike | string[] | number[] | undefined; |