/**
|
* @file S&OP MatrixEditor component to wrap common methods the team encounter during development
|
* @description MatrixEditor class extending libappbase's MatrixEditorBase.
|
* All S&OP page objects inherit from our own class (inheriting e2e/libappbase), but we can propose common methods to them.
|
* @author Clarence (clarence.chan@3ds.com)
|
* @copyright Dassault Systèmes
|
*/
|
import { CellLocator, MatrixEditorBase } from '../libappbase/matrixeditorbase';
|
import { browser, by } from '../e2elib/node_modules/protractor/built';
|
import { Timeout } from '../libmp/appmp';
|
import { QUtils } from '../e2elib/lib/src/main/qutils.class';
|
import { DialogBase } from '../libappbase/dialogbase';
|
import { Form } from '../e2elib/lib/src/pageobjects/form.component';
|
import { KeyboardKey } from '../e2elib/lib/src/helper/enumhelper';
|
import { ButtonSOP } from './buttonsop';
|
import { QContextMenu } from '../e2elib/lib/api/pageobjects/qcontextmenu.component';
|
import { ContextMenuSOP } from './contextmenusop';
|
import { QConsole } from '../e2elib/lib/src/helper/qconsole';
|
import { ColorSOP, ColorSOPList } from './colorsop';
|
|
export class MatrixEditorSOP<DialogType extends DialogBase> extends MatrixEditorBase {
|
/**
|
* Empty matrix cell value. Cannot use empty string as getCellValue will fail.
|
* Need use the Unicode representation of space.
|
* https://www.compart.com/en/unicode/U+0020
|
*/
|
public static CELLEMPTY = '\u0020';
|
|
private readonly _dlgCreateEdit: DialogType;
|
public cmMenu: QContextMenu;
|
|
public constructor(componentPath: string, contextMenuName: string, dialog: DialogType) {
|
super(componentPath);
|
this.cmMenu = new QContextMenu(contextMenuName);
|
this._dlgCreateEdit = dialog;
|
}
|
|
/**
|
* Focus matrix (to allow action buttons to refresh the enable state) and click the action button.
|
*
|
* @param actionButton Action button to click (e.g Create/Edit button). Always use button defined in view instead of instantiating one inside the matrix/list/dialog.
|
* @returns The dialog that triggered from the action button click (e.g Create dialog when clicking Create action button)
|
*/
|
public async clickActionButton(actionButton: ButtonSOP): Promise<DialogType> {
|
await this.focus();
|
await actionButton.click();
|
|
// TODO: For now always return dialog assuming is create/edit that brings up dialog. Find generic way decide if need to e.g Delete doesn't require it.
|
return this.getDialog();
|
}
|
|
/**
|
* Click matrix top left to select all cells (built-in software behavior).
|
* Wait for first row to proper selected by inspecting the first row cell has the correct CSS style to indicate selected.
|
* This to prevent caller from calling getCell method too early when matrix cells not proper selected yet resulting in possible
|
* stale element reference. (This is a theory for now, will need monitor more pipeline runs to deem stable).
|
*/
|
public async clickTopLeftSelectAllCells(): Promise<void> {
|
await this.selectAll();
|
|
await browser.wait(
|
async () => {
|
try {
|
const cell = await this.getCell(0, 0, 0); // Get first row, first cell and inspect CSS if being focused (seems to work even if cell has no object/grey out)
|
const style = await cell.element.getAttribute('class');
|
|
return style.indexOf('componentFocused') >= 0;
|
} catch {
|
return false; // if getCell() error, keep trying
|
}
|
},
|
Timeout.Short,
|
'Timeout waiting for all cells to be selected.',
|
);
|
}
|
|
/**
|
* Edits the cells with the provided cell locators to the new value.
|
*
|
* @param cellLocators Location of cells that needs to be editted
|
* @param value New value of cells
|
*/
|
public async editCellValues(cellLocators: CellLocator[], value: string): Promise<void> {
|
await this.selectCells(cellLocators);
|
await QUtils.keyBoardAction([value]);
|
await QUtils.keyBoardAction([KeyboardKey.ENTER]);
|
await QConsole.waitForStable();
|
}
|
|
/**
|
* Wait dialog present and returns it.
|
*
|
* @returns The dialog instance.
|
*/
|
public async getDialog(): Promise<DialogType> {
|
return this.waitAndReturnDialog(this._dlgCreateEdit);
|
}
|
|
/**
|
* Right click matrix cell and select context menu to popup dialog.
|
*
|
* @param contextMenu Context menu item names (if menu has sub-menus, pass each menu delimited by pipe |).
|
* @param rowID RowID based on string or index.
|
* @param attributeID AttributeID based on string or index. Pass 0 if matrix not using.
|
* @param columnID ColumnID based on string or index.
|
* @param altDialog [Optional] Alternative dialog to use instead of the main dialog set during constructor. Typical use case if list triggers few different dialogs.
|
* This cater also for WebMessageBox (the dialog prompt to request user to confirm deletion for example).
|
* @returns Tuple consisting of main dialog and alternative dialog (which can be undefined).
|
*/
|
public async rightClickCellSelectContextmenu<AltDialog extends Form>(contextMenu: string, rowID: number | string, attributeID: number | string, columnID: number | string, altDialog?: AltDialog): Promise<[DialogType, AltDialog | undefined]> {
|
await this.focus(); // Below fail to find row if matrix not focus
|
|
const cell = await this.getCell(rowID, attributeID, columnID);
|
|
// If delimiter pipe present = sub-menu present, tokenize into array
|
const menuPaths = ContextMenuSOP.getMenuPaths(contextMenu);
|
await cell.rightClick(undefined, this.cmMenu, menuPaths);
|
|
const dlg = await this.getDialog();
|
if (altDialog) {
|
altDialog = await this.waitAndReturnDialog(altDialog);
|
}
|
|
// Return tuple (1st dialog = main dialog set during constructor, 2nd dialog only used if list triggers more than 1 dialog thus possibly undefined)
|
return [dlg, altDialog];
|
}
|
|
/**
|
* Right click matrix row and select context menu to popup dialog.
|
*
|
* @param contextMenu Context menu item name.
|
* @param rowID RowID based on string or index.
|
* @param attributeID AttributeID based on string or index. Pass 0 (number) if matrix not using.
|
* @returns The dialog instance.
|
*/
|
public async rightClickRowSelectContextmenu(contextMenu: string, rowID: number | string, attributeID?: number | string): Promise<DialogType> {
|
await this.focus(); // Below fail to find row if matrix not focus
|
|
await this.rightClickOnRow(rowID, attributeID);
|
await this.cmMenu.selectMenuItem(contextMenu);
|
|
return this.getDialog();
|
}
|
|
/**
|
* Right click matrix and select context menu to popup dialog.
|
*
|
* @param contextMenu Context menu item name.
|
* @returns The dialog instance.
|
*/
|
public async rightClickMatrixSelectContextmenu(contextMenu: string): Promise<DialogType> {
|
await this.focus(); // Below fail to find row if matrix not focus
|
|
// Right click qColumnHeading which is the top left cell since no row/column is present for us to interact with
|
await QUtils.rightClickElementWithMenu(this.element.element(by.className('qColumnHeading')), undefined, undefined, this.cmMenu, contextMenu);
|
|
return this.getDialog();
|
}
|
|
/**
|
* Use DOWN keyboard action until the first row header that is currently visible is the sought one
|
*
|
* @param rowHeader name of header to be scrolled to
|
*/
|
public async scrollToRowByRowHeader(rowHeader: string): Promise<void> {
|
await this.focus(); // Focus first else control might still be in other UI element
|
|
let firstVisibleRowHeader = await this.getRowHeader(0);
|
while (firstVisibleRowHeader !== rowHeader) {
|
await QUtils.keyBoardAction([KeyboardKey.DOWN]);
|
firstVisibleRowHeader = await this.getRowHeader(0);
|
}
|
}
|
|
/**
|
* Select one or more cells by holding down the Ctrl key.
|
*
|
* @param cellLocators One or more cell to select. CellLocator interface to specify the row, attribute and column info.
|
*/
|
public async selectCells(cellLocators: CellLocator[]): Promise<void> {
|
for (let index = 0; index < cellLocators.length; index++) {
|
const elem = cellLocators[index];
|
const cell = await this.getCell(elem.rowName, elem.attributeId!, elem.columnName);
|
if (index === 0) {
|
await QUtils.leftClickElement(cell.element);
|
} else {
|
// Hold Ctrl key to multi-select cells
|
await QUtils.leftClickElement(cell.element, { control: true });
|
}
|
}
|
}
|
|
/**
|
* Copy cell(s) with shortcut 'Ctrl + C'.
|
*/
|
public async copyCellsWithShortcut(): Promise<void> {
|
await QUtils.keyBoardAction(['c'], true);
|
await QConsole.waitForStable();
|
}
|
|
/**
|
* Paste cell(s) with shortcut 'Ctrl + V'.
|
*/
|
public async pasteCellsWithShortcut(): Promise<void> {
|
await QUtils.keyBoardAction(['v'], true);
|
await this.waitForScreenUpdate();
|
}
|
|
/**
|
* Verify matrix cell value. If matrix does not use attribute for row, pass attributeID = 0 (number).
|
*
|
* @param rowID Row ID based on string or index.
|
* @param attributeID AttributeID based on string or index. Pass 0 (number) if matrix not using.
|
* @param columnID ColumnID based on string or index.
|
* @param expectedCellValue Value to assert.
|
*/
|
public async verifyCellValue(rowID: number | string, attributeID: number | string, columnID: number | string, expectedCellValue: string): Promise<void> {
|
const cell = await this.getCell(rowID, attributeID, columnID);
|
const cellValue = await cell.getValue();
|
|
const attributeIDString = attributeID === 0 ? '' : `, attribute = ${attributeID}`; // If matrix using attribute(s) per row, else show empty in failure message
|
expect(cellValue).toBe(expectedCellValue, `Verify fail for matrix cell value (row = ${rowID}${attributeIDString} and column = ${columnID}.`);
|
}
|
|
/**
|
* Verify total row matches, optionally providing a failure message to append if expect fails.
|
*
|
* @param expectedTotal Expected total row count.
|
* @param additionalFailureMessage [Optional] Failure message to append (use to define scenario specific information).
|
*/
|
public async verifyTotalRow(expectedTotal: number, additionalFailureMessage?: string): Promise<void> {
|
const appendFailureMessage = additionalFailureMessage ? additionalFailureMessage : '';
|
|
expect(await this.getRowCount()).toBe(expectedTotal, `Verify matrix total row fail. ${appendFailureMessage}`);
|
}
|
|
/**
|
* Compare matrix cell background color code with pass-in color code, these values should be same.
|
* If not, fail the test with error message.
|
*
|
* @param row Matrix row name.
|
* @param column Matrix column name.
|
* @param colorCode RGB color code for the browser being used to run this script. Chrome & Firefox has different color code for the same color.
|
*/
|
public async verifyCellColor(row: number | string, column: string, colorCode: string): Promise<void> {
|
const matrixCell = await this.getCell(row, '', column);
|
const value = await matrixCell.element.getCssValue('background-color');
|
expect(value).toBe(colorCode, 'Verify matrix cell color fail.');
|
}
|
|
/**
|
* Verify if cell has no data and the color of cell is gray.
|
*
|
* @param rowID Matrix row id.
|
* @param attributeID Matrix attribute ID (if matrix enables multiple attributes).
|
* @param columnID Matrix column name.
|
*/
|
public async verifyCellHasNoData(rowID: string | number, attributeID: string | number, columnID: string): Promise<void> {
|
await this.verifyCellValue(rowID, attributeID, columnID, MatrixEditorSOP.CELLEMPTY);
|
await this.verifyCellColor(rowID, columnID, matrixCellColors.noData().Rgb);
|
}
|
|
/**
|
* Verifies if the cell is not editable and checks the tooltip shown.
|
*
|
* @param rowID Row of cell to check if disabled
|
* @param attributeID Attribute of cell to check if disabled
|
* @param columnID Column of cell to check if disabled
|
* @param tooltip Tooltip expected when clicked cell is attempted to be edited
|
*/
|
public async verifyCellNotEditable(rowID: string | number, attributeID: string | number, columnID: string | number, tooltip?: string): Promise<void> {
|
const cell = await this.getCell(rowID, attributeID, columnID);
|
expect(await cell.isEditable()).toBe(false, `Expected cell not editable at row "${rowID}" and column "${columnID}".`);
|
if (tooltip) {
|
const toastMessage = await cell.doubleClick(true);
|
expect(toastMessage).toBe(tooltip, `Verify cell disabled tooltip fail at row "${rowID}" and column "${columnID}".`);
|
}
|
}
|
|
/**
|
* Verify that the matrix column header exists.
|
*
|
* @param matrixColumnHeader The name of the matrix column header.
|
*/
|
public async verifyColumnExist(matrixColumnHeader: string): Promise<void> {
|
expect(await this.hasColumn(matrixColumnHeader)).toBe(true, `Unable to find matrix column '${matrixColumnHeader}'.`);
|
}
|
|
/**
|
* Verify that the matrix row exists.
|
*
|
* @param matrixRow The name of the matrix row.
|
*/
|
public async verifyRowExist(matrixRow: string): Promise<void> {
|
expect(await this.hasRow(matrixRow)).toBe(true, `Unable to find matrix row '${matrixRow}'.`);
|
}
|
|
/**
|
* Verify that the matrix row doesn't exists.
|
*
|
* @param matrixRow The name of the matrix row.
|
*/
|
public async verifyRowNotExist(matrixRow: string): Promise<void> {
|
expect(await this.hasRow(matrixRow)).toBe(false, `Expect matrix row '${matrixRow}' not exist.`);
|
}
|
|
/**
|
* Verify that the sequence of matrix row.
|
*
|
* @param expectedRowsSequence Expected row names to verify in sequence.
|
*/
|
public async verifyRowsSequence(expectedRowsSequence: string[]): Promise<void> {
|
expect(await this.getRowCount()).toBe(expectedRowsSequence.length, `Verify rows sequence failed. There should be exactly ${expectedRowsSequence.length} rows.`);
|
const currentMatrixRow: string[] = [];
|
const errMessage: string[] = [];
|
|
for (let index = 0; index < expectedRowsSequence.length; index++) {
|
currentMatrixRow.push(await this.getRowHeader(index));
|
const currentExpectedMatrixRow = expectedRowsSequence[index];
|
const currentActualMatrixRow = await this.getRowHeader(index);
|
if (currentActualMatrixRow !== currentExpectedMatrixRow) {
|
errMessage.push(currentExpectedMatrixRow);
|
}
|
}
|
expect(errMessage.length === 0).toBe(true, `Rows "${errMessage.join(', ')}" not sorted in correct order. Current matrix rows "${currentMatrixRow.join(', ')}" `);
|
}
|
|
private async waitAndReturnDialog<T extends Form>(dlg: T): Promise<T> {
|
await dlg.waitForScreenUpdate();
|
return dlg;
|
}
|
}
|
|
// Step description to re-use in spec file to prevent scriptor re-write each time
|
const stepMatrix = {
|
copyCellsWithShortcut: (matrixName: string): string => `In matrix ${matrixName}, copy cells with shortcut 'Ctrl + C'.`,
|
editCellValue: (matrixName: string, columnName: string, rowName: string, value: string): string => `In matrix ${matrixName}, set value of cell at row "${columnName}" and column "${rowName}" to "${value}".`,
|
editCellValues: (matrixName: string, cellLocators: CellLocator[], value: string): string => {
|
const arr: string[] = cellLocators.map(({ rowName, columnName }: CellLocator) => `row = "${rowName}" with column = "${columnName}"`);
|
return `In matrix ${matrixName}, set value of cells at ${arr.join(', ')} to "${value}".`;
|
},
|
pasteCellsWithShortcut: (matrixName: string): string => `In matrix ${matrixName}, paste cells with shortcut 'Ctrl + V'.`,
|
rightClickCellSelectContextmenu: (matrixName: string, rowName: string, columnName: string, menuLabel: string): string => `In matrix ${matrixName}, right click cell for row = '${rowName}', column = '${columnName}' and select menu ${menuLabel}.`,
|
rightClickMatrixSelectContextmenu: (matrixName: string, menuLabel: string): string => `In matrix ${matrixName}, right click and select menu ${menuLabel}.`,
|
rightClickRowSelectContextmenu: (matrixName: string, rowName: string, menuLabel: string): string => `In matrix ${matrixName}, right click row '${rowName}' and select menu ${menuLabel}.`,
|
selectCells: (matrixName: string, cellLocators: CellLocator[]): string => {
|
const arr: string[] = cellLocators.map(({ rowName, columnName }: CellLocator) => `row = "${rowName}" with column = "${columnName}"`);
|
return `In matrix ${matrixName}, select cells which ${arr.join(', ')}.`;
|
},
|
verifyCellColor: (matrixName: string, rowName: string, columnName: string, expectedColor: string): string => `In matrix ${matrixName}, verify cell color to be '${expectedColor}' for row = '${rowName}' and column = '${columnName}'.`,
|
verifyCellHasNoData: (matrixName: string, rowID: string | number, attributeID: string | number, columnID: string): string => `In matrix ${matrixName}, verify cell color to be 'gray' and cell to have no data for row = '${rowID}', attribute = '${attributeID}' and column = '${columnID}'.`,
|
verifyCellNotEditable: (matrixName: string, rowID: string | number, attributeID: string | number, columnID: string | number, tooltip?: string): string => {
|
const tooltipMsg = tooltip ? ` and shows tooltip "${tooltip}" when attempting to edit the cell` : '';
|
|
return `In matrix ${matrixName}, verify cell at row "${rowID}", column "${columnID}" and attribute "${attributeID}" is not editable${tooltipMsg}.`;
|
},
|
verifyCellValue: (matrixName: string, rowID: number | string, columnID: number | string, expectedCellValue: string): string => `In matrix ${matrixName}, verify "${expectedCellValue}" is the value of row "${rowID}" and column "${columnID}".`,
|
verifyRowExists: (matrixName: string, rowName: string): string => `In matrix ${matrixName}, verify row exists for row = ${rowName}.`,
|
verifyRowsSequence: (matrixName: string, verifyRows: string[]): string => `In matrix ${matrixName}, verify rows sequence = "${verifyRows.join(', ')}".`,
|
verifyTotalRow: (matrixName: string, totalRow: number): string => `In matrix ${matrixName}, verify total row = ${totalRow}.`,
|
};
|
|
// Default matrix cell colors (assuming no representation override) for example if cell has data or not.
|
const matrixCellColors: ColorSOPList = {
|
hasData: (): ColorSOP => ({ Rgb: 'rgba(0, 0, 0, 0)', Color: 'White', Hex: '' }),
|
noData: (): ColorSOP => ({ Rgb: 'rgba(0, 0, 0, 0.125)', Color: 'Grey', Hex: '' }),
|
};
|
|
export { stepMatrix as StepMatrix };
|
export { matrixCellColors as MatrixCellColors };
|