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