yanweiyuan3
2023-08-09 588bc7829387dfc761cc25f06f77d4c81818bd10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
/**
 * @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 };