@vuu-ui/vuu-playwright-helpers 4.0.0-beta.5 → 4.0.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,8 +3,16 @@
3
3
  Playwright helpers for Vuu components.
4
4
 
5
5
  All helpers extend `PlaywrightPageObject` and are instantiated from the Playwright `page`.
6
- `PwTable` locates its target element using a default CSS selector, which can be
7
- overridden at runtime. `PwTableHeaderCell` requires an explicit CSS selector.
6
+ Component helpers extend `PwComponent`, which locates the component root element using
7
+ a default CSS selector. This can be overridden at runtime by passing a CSS selector or a
8
+ `Locator`. `PwComponent` provides common helpers:
9
+
10
+ - `locator` - the component root `Locator`, for web-first assertions not covered by a helper
11
+ - `isReady()` - waits until the component is visible, resolves with the component
12
+ - `isVisible()` - is the component currently visible (no waiting)
13
+ - `assertVisible()`, `assertNotAttached()`
14
+
15
+ ## PwTable
8
16
 
9
17
  ```ts
10
18
  // uses the default selector (.vuuTable)
@@ -23,3 +31,36 @@ await table.editCell("currency", 2, "EUR", "cancel");
23
31
  Prefer `cell(colName, ariaRowIndex)` for new usage. `ariaRowIndex` is the 1-based
24
32
  ARIA row index, including header rows (with one header, the first data row is 2).
25
33
  The existing `locateCell` overloads remain available for gradual migration.
34
+
35
+ ## PwContextMenu, PwMenuItem
36
+
37
+ `PwTable.showContextMenu(colName, ariaRowIndex)` right clicks a cell and resolves with a
38
+ `PwContextMenu` once the menu is visible.
39
+
40
+ ```ts
41
+ const menu = await table.showContextMenu("ric", 2);
42
+
43
+ expect(await menu.hasMenuItems()).toBe(true);
44
+ expect(await menu.hasMenuItems(["Copy text", "Filter"])).toBe(true); // exact, in order
45
+ expect(await menu.menuItemLabels()).toEqual(["Copy text", "Filter"]);
46
+
47
+ const copy = menu.menuItem("Copy text"); // or by 0 based index, menu.menuItem(0)
48
+ expect(await copy.hasIcon("edit")).toBe(true);
49
+ expect(await copy.isEnabled()).toBe(true);
50
+ expect(await menu.menuItem("Filter").hasChildren()).toBe(true);
51
+
52
+ // sub menus, opened with mouse (default) or keyboard
53
+ const filterMenu = await menu.menuItem("Filter").openSubMenu();
54
+ await filterMenu.menuItem("Include value").click();
55
+ await menu.assertNotAttached();
56
+
57
+ // keyboard
58
+ await menu.press("ArrowDown");
59
+ await menu.assertFocusedMenuItem("Copy text");
60
+ await menu.press("Enter");
61
+ await menu.close(); // Escape
62
+ ```
63
+
64
+ When a key press depends on the focus change made by the previous key, call
65
+ `assertFocusedMenuItem` in between. Keys pressed before the menu has processed a
66
+ focus change may be delivered to the previously focused item.
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.0.0-beta.5",
2
+ "version": "4.0.0-beta.7",
3
3
  "author": "heswell",
4
4
  "license": "Apache-2.0",
5
5
  "main": "./src/index.js",
@@ -9,7 +9,7 @@
9
9
  "type-defs": "node ../../scripts/build-type-defs.mjs"
10
10
  },
11
11
  "dependencies": {
12
- "@vuu-ui/vuu-protocol-types": "4.0.0-beta.5"
12
+ "@vuu-ui/vuu-protocol-types": "4.0.0-beta.7"
13
13
  },
14
14
  "peerDependencies": {
15
15
  "@playwright/test": "^1.62.1"
@@ -0,0 +1,29 @@
1
+ import { expect } from "@playwright/test";
2
+ import { PlaywrightPageObject } from "./PlaywrightPageObject.js";
3
+ class PwComponent extends PlaywrightPageObject {
4
+ target;
5
+ constructor(page, target){
6
+ super(page), this.target = target;
7
+ }
8
+ get locator() {
9
+ if ("string" == typeof this.target || void 0 === this.target) return this.page.locator(this.target ?? this.defaultCssSelector);
10
+ return this.target;
11
+ }
12
+ async isReady(options) {
13
+ await this.locator.waitFor({
14
+ state: "visible",
15
+ ...options
16
+ });
17
+ return this;
18
+ }
19
+ isVisible() {
20
+ return this.locator.isVisible();
21
+ }
22
+ async assertVisible() {
23
+ await expect(this.locator).toBeVisible();
24
+ }
25
+ async assertNotAttached() {
26
+ await expect(this.locator).not.toBeAttached();
27
+ }
28
+ }
29
+ export { PwComponent };
@@ -0,0 +1,39 @@
1
+ import { expect } from "@playwright/test";
2
+ import { PwComponent } from "./PwComponent.js";
3
+ import { PwMenuItem } from "./PwMenuItem.js";
4
+ class PwContextMenu extends PwComponent {
5
+ defaultCssSelector = '.vuuContextMenuPanel[role="menu"]';
6
+ get menuItems() {
7
+ return this.locator.getByRole("menuitem");
8
+ }
9
+ async menuItemLabels() {
10
+ return (await this.menuItems.allInnerTexts()).map((text)=>text.trim());
11
+ }
12
+ async hasMenuItems(labels) {
13
+ if (void 0 === labels) return await this.menuItems.count() > 0;
14
+ const menuItemLabels = await this.menuItemLabels();
15
+ return menuItemLabels.length === labels.length && menuItemLabels.every((label, i)=>label === labels[i]);
16
+ }
17
+ menuItem(labelOrIndex) {
18
+ const locator = "number" == typeof labelOrIndex ? this.menuItems.nth(labelOrIndex) : this.menuItems.filter({
19
+ has: this.page.getByText(labelOrIndex, {
20
+ exact: true
21
+ })
22
+ });
23
+ return new PwMenuItem(this.page, locator);
24
+ }
25
+ get focusedMenuItem() {
26
+ return new PwMenuItem(this.page, this.menuItems.and(this.page.locator(':focus[tabindex="0"]')));
27
+ }
28
+ async assertFocusedMenuItem(label) {
29
+ await expect(this.focusedMenuItem.locator).toHaveText(label);
30
+ }
31
+ async press(...keys) {
32
+ for (const key of keys)await this.page.keyboard.press(key);
33
+ }
34
+ async close() {
35
+ await this.press("Escape");
36
+ await this.assertNotAttached();
37
+ }
38
+ }
39
+ export { PwContextMenu };
@@ -0,0 +1,58 @@
1
+ import { expect } from "@playwright/test";
2
+ import { PwComponent } from "./PwComponent.js";
3
+ import { PwContextMenu } from "./PwContextMenu.js";
4
+ import { byId } from "./selector-utils.js";
5
+ class PwMenuItem extends PwComponent {
6
+ defaultCssSelector = '[role="menuitem"]';
7
+ async label() {
8
+ return (await this.locator.innerText()).trim();
9
+ }
10
+ async hasChildren() {
11
+ return await this.locator.getAttribute("aria-haspopup") === "menu";
12
+ }
13
+ async isDisabled() {
14
+ return await this.locator.getAttribute("aria-disabled") === "true";
15
+ }
16
+ async isEnabled() {
17
+ return !await this.isDisabled();
18
+ }
19
+ async isExpanded() {
20
+ return await this.locator.getAttribute("aria-expanded") === "true";
21
+ }
22
+ isFocused() {
23
+ return this.locator.evaluate((element)=>element === element.ownerDocument.activeElement);
24
+ }
25
+ async hasIcon(iconName) {
26
+ return await this.locator.getAttribute("data-icon") === iconName;
27
+ }
28
+ async click(options) {
29
+ await this.locator.click(options);
30
+ }
31
+ async hover() {
32
+ await this.locator.hover();
33
+ }
34
+ async focus() {
35
+ await this.locator.focus();
36
+ }
37
+ async press(key) {
38
+ await this.locator.press(key);
39
+ }
40
+ async subMenu() {
41
+ const subMenuId = await this.locator.getAttribute("aria-controls");
42
+ if (null === subMenuId) throw Error(`PwMenuItem, subMenu, menu item "${await this.label()}" has no open sub menu`);
43
+ return new PwContextMenu(this.page, byId(subMenuId)).isReady();
44
+ }
45
+ async openSubMenu(using = "mouse") {
46
+ if (!await this.hasChildren()) throw Error(`PwMenuItem, openSubMenu, menu item "${await this.label()}" has no sub menu`);
47
+ if ("mouse" === using) await this.hover();
48
+ else {
49
+ await this.focus();
50
+ await this.press("ArrowRight");
51
+ }
52
+ await this.locator.and(this.page.locator('[aria-expanded="true"]')).waitFor();
53
+ const subMenu = await this.subMenu();
54
+ if ("keyboard" === using) await expect(subMenu.focusedMenuItem.locator).toHaveCount(1);
55
+ return subMenu;
56
+ }
57
+ }
58
+ export { PwMenuItem };
package/src/PwTable.js CHANGED
@@ -1,29 +1,24 @@
1
1
  import { expect } from "@playwright/test";
2
- import { PlaywrightPageObject } from "./PlaywrightPageObject.js";
2
+ import { PwComponent } from "./PwComponent.js";
3
+ import { PwContextMenu } from "./PwContextMenu.js";
3
4
  import { PwTableHeaderCell } from "./PwTableHeaderCell.js";
5
+ import { byDataIcon, byId, cssAttributeValue } from "./selector-utils.js";
4
6
  const byAriaRowIndex = (ariaRowIndex)=>`[aria-rowindex="${ariaRowIndex}"]:scope`;
5
7
  const xpathStringLiteral = (value)=>{
6
8
  if (!value.includes("'")) return `'${value}'`;
7
9
  if (!value.includes('"')) return `"${value}"`;
8
10
  return `concat('${value.split("'").join("', \"'\", '")}')`;
9
11
  };
10
- class PwTable extends PlaywrightPageObject {
11
- cssSelector;
12
+ class PwTable extends PwComponent {
12
13
  defaultCssSelector = ".vuuTable";
13
- constructor(page, cssSelector){
14
- super(page), this.cssSelector = cssSelector;
15
- }
16
- get #locator() {
17
- return this.page.locator(this.cssSelector ?? this.defaultCssSelector);
18
- }
19
14
  get contentContainer() {
20
- return this.#locator.locator(".vuuTable-contentContainer");
15
+ return this.locator.locator(".vuuTable-contentContainer");
21
16
  }
22
17
  get scrollbarContainer() {
23
- return this.#locator.locator(".vuuTable-scrollbarContainer");
18
+ return this.locator.locator(".vuuTable-scrollbarContainer");
24
19
  }
25
20
  row(row) {
26
- return this.#locator.locator(`[aria-rowindex="${row}"]`);
21
+ return this.locator.locator(`[aria-rowindex="${row}"]`);
27
22
  }
28
23
  cell(colName, ariaRowIndex) {
29
24
  return this.locateCell(ariaRowIndex, colName);
@@ -36,23 +31,45 @@ class PwTable extends PlaywrightPageObject {
36
31
  await expect(textbox).toHaveValue(value);
37
32
  await textbox.press("commit" === action ? "Enter" : "Escape");
38
33
  }
34
+ async columnNames() {
35
+ return this.locator.locator(".vuuTableHeaderCell .vuuTableHeaderCell-label").allTextContents();
36
+ }
37
+ async rowCount() {
38
+ const ariaRowCount = await this.locator.getByRole("table").getAttribute("aria-rowcount");
39
+ if (null === ariaRowCount) throw Error("PwTable, rowCount, table does not have an aria-rowcount");
40
+ return parseInt(ariaRowCount, 10);
41
+ }
42
+ async cellIcon(columnName, ariaRowIndex, iconName) {
43
+ const cell = await this.cell(columnName, ariaRowIndex);
44
+ return cell.locator(byDataIcon(iconName));
45
+ }
46
+ cellsWithIcon(iconName) {
47
+ return this.locator.getByRole("cell").filter({
48
+ has: this.page.locator(byDataIcon(iconName))
49
+ });
50
+ }
39
51
  headerCell(columnName) {
40
- const escapedColumnName = Array.from(columnName, (character)=>{
41
- const code = character.charCodeAt(0);
42
- return '"' === character || "\\" === character || code < 32 || 127 === code ? `\\${code.toString(16)} ` : character;
43
- }).join("");
44
- return new PwTableHeaderCell(this.page, `${this.cssSelector ?? this.defaultCssSelector} >> .vuuTableHeaderCell[data-column-name="${escapedColumnName}"]`);
52
+ return new PwTableHeaderCell(this.page, this.locator.locator(`.vuuTableHeaderCell[data-column-name="${cssAttributeValue(columnName)}"]`));
53
+ }
54
+ async showContextMenu(columnName, ariaRowIndex) {
55
+ const cell = await this.cell(columnName, ariaRowIndex);
56
+ await cell.click({
57
+ button: "right"
58
+ });
59
+ const menu = await new PwContextMenu(this.page).isReady();
60
+ const menuId = await menu.locator.getAttribute("id");
61
+ return menuId ? new PwContextMenu(this.page, byId(menuId)) : menu;
45
62
  }
46
63
  locateCell(rowOrText, column) {
47
- if ("number" == typeof rowOrText && "number" == typeof column) return this.#locator.locator(`[aria-rowindex="${rowOrText}"] > [aria-colindex="${column}"]`);
64
+ if ("number" == typeof rowOrText && "number" == typeof column) return this.locator.locator(`[aria-rowindex="${rowOrText}"] > [aria-colindex="${column}"]`);
48
65
  if ("number" == typeof rowOrText && "string" == typeof column) {
49
- const columnHeader = this.#locator.locator(`xpath=.//*[@data-column-name=${xpathStringLiteral(column)}]`).first();
66
+ const columnHeader = this.locator.locator(`xpath=.//*[@data-column-name=${xpathStringLiteral(column)}]`).first();
50
67
  return columnHeader.getAttribute("aria-colindex").then((ariaColIndex)=>{
51
68
  if (null === ariaColIndex) throw Error(`PwTable, locateCell, column "${column}" does not have an aria-colindex`);
52
- return this.#locator.locator(`[aria-rowindex="${rowOrText}"] > [aria-colindex="${ariaColIndex}"]`);
69
+ return this.locator.locator(`[aria-rowindex="${rowOrText}"] > [aria-colindex="${ariaColIndex}"]`);
53
70
  });
54
71
  }
55
- if ("string" == typeof rowOrText) return this.#locator.getByRole("cell", {
72
+ if ("string" == typeof rowOrText) return this.locator.getByRole("cell", {
56
73
  name: rowOrText
57
74
  });
58
75
  }
@@ -70,9 +87,6 @@ class PwTable extends PlaywrightPageObject {
70
87
  }
71
88
  }
72
89
  }
73
- async assertVisible() {
74
- return expect(this.#locator).toBeVisible();
75
- }
76
90
  getCellText(arg1, arg2, arg3, arg4) {
77
91
  if ("number" == typeof arg1 && "number" == typeof arg2) return [
78
92
  this.locateCell(arg1, arg2),
@@ -128,16 +142,16 @@ class PwTable extends PlaywrightPageObject {
128
142
  const offsetFromEnd = totalRowCount - to;
129
143
  const trailingBufferedRows = offsetFromEnd < renderBufferSize ? Math.min(0, offsetFromEnd) : renderBufferSize;
130
144
  const renderedRowCount = to - from + leadingBufferedRows + trailingBufferedRows;
131
- await expect(this.#locator.getByRole("row")).toHaveCount(renderedRowCount + headerCount);
145
+ await expect(this.locator.getByRole("row")).toHaveCount(renderedRowCount + headerCount);
132
146
  const firstRenderedRow = from - leadingBufferedRows + headerCount + 1;
133
147
  const lastVisibleRow = to + headerCount;
134
148
  const lastRenderedRow = to + headerCount + trailingBufferedRows;
135
- await expect(this.#locator.getByRole("row").locator(byAriaRowIndex(firstRenderedRow))).toBeVisible();
136
- await expect(this.#locator.getByRole("row").locator(byAriaRowIndex(lastVisibleRow))).toBeVisible();
149
+ await expect(this.locator.getByRole("row").locator(byAriaRowIndex(firstRenderedRow))).toBeVisible();
150
+ await expect(this.locator.getByRole("row").locator(byAriaRowIndex(lastVisibleRow))).toBeVisible();
137
151
  if (trailingBufferedRows > 0) {
138
- await expect(this.#locator.getByRole("row").locator(byAriaRowIndex(lastVisibleRow + 1))).not.toBeInViewport();
139
- await expect(this.#locator.getByRole("row").locator(byAriaRowIndex(lastRenderedRow))).not.toBeInViewport();
140
- await expect(this.#locator.getByRole("row").locator(byAriaRowIndex(lastRenderedRow + 1))).not.toBeAttached();
152
+ await expect(this.locator.getByRole("row").locator(byAriaRowIndex(lastVisibleRow + 1))).not.toBeInViewport();
153
+ await expect(this.locator.getByRole("row").locator(byAriaRowIndex(lastRenderedRow))).not.toBeInViewport();
154
+ await expect(this.locator.getByRole("row").locator(byAriaRowIndex(lastRenderedRow + 1))).not.toBeAttached();
141
155
  }
142
156
  }
143
157
  async assertRenderedColumns({ rendered, visible }) {
@@ -1,15 +1,15 @@
1
1
  import { expect } from "@playwright/test";
2
- import { PlaywrightPageObject } from "./PlaywrightPageObject.js";
3
- class PwTableHeaderCell extends PlaywrightPageObject {
4
- cssSelector;
5
- constructor(page, cssSelector){
6
- super(page), this.cssSelector = cssSelector;
2
+ import { PwComponent } from "./PwComponent.js";
3
+ class PwTableHeaderCell extends PwComponent {
4
+ defaultCssSelector = ".vuuTableHeaderCell";
5
+ constructor(page, target){
6
+ super(page, target);
7
7
  }
8
8
  get isSelected() {
9
- return this.page.locator(this.cssSelector).evaluate((element)=>element.classList.contains("vuuTableHeaderCell-selected"));
9
+ return this.locator.evaluate((element)=>element.classList.contains("vuuTableHeaderCell-selected"));
10
10
  }
11
11
  async assertSelected() {
12
- await expect(this.page.locator(this.cssSelector)).toContainClass("vuuTableHeaderCell-selected");
12
+ await expect(this.locator).toContainClass("vuuTableHeaderCell-selected");
13
13
  }
14
14
  }
15
15
  export { PwTableHeaderCell };
package/src/index.js CHANGED
@@ -1,3 +1,6 @@
1
1
  export { PlaywrightPageObject } from "./PlaywrightPageObject.js";
2
+ export { PwComponent } from "./PwComponent.js";
3
+ export { PwContextMenu } from "./PwContextMenu.js";
4
+ export { PwMenuItem } from "./PwMenuItem.js";
2
5
  export { PwTable } from "./PwTable.js";
3
6
  export { PwTableHeaderCell } from "./PwTableHeaderCell.js";
@@ -0,0 +1,7 @@
1
+ const cssAttributeValue = (value)=>Array.from(value, (character)=>{
2
+ const code = character.charCodeAt(0);
3
+ return '"' === character || "\\" === character || code < 32 || 127 === code ? `\\${code.toString(16)} ` : character;
4
+ }).join("");
5
+ const byDataIcon = (iconName)=>`[data-icon="${cssAttributeValue(iconName)}"]`;
6
+ const byId = (id)=>`[id="${cssAttributeValue(id)}"]`;
7
+ export { byDataIcon, byId, cssAttributeValue };
@@ -0,0 +1,43 @@
1
+ import { type Locator, type Page } from "@playwright/test";
2
+ import { PlaywrightPageObject } from "./PlaywrightPageObject";
3
+ /**
4
+ * Identifies the element a component helper operates on. Either a css selector,
5
+ * resolved against the page, or a Locator.
6
+ */
7
+ export type PwComponentTarget = string | Locator;
8
+ /**
9
+ * Base class for helpers that wrap a single component instance. Subclasses
10
+ * provide a defaultCssSelector, which can be overridden at runtime by passing
11
+ * a css selector or Locator to the constructor.
12
+ */
13
+ export declare abstract class PwComponent extends PlaywrightPageObject {
14
+ protected readonly target?: PwComponentTarget | undefined;
15
+ protected abstract readonly defaultCssSelector: string;
16
+ /**
17
+ * @param page the Playwright page
18
+ * @param target optional css selector or Locator for the component, overrides defaultCssSelector
19
+ */
20
+ constructor(page: Page, target?: PwComponentTarget | undefined);
21
+ /**
22
+ * Locator for the component root element. Use this for web-first assertions
23
+ * that are not covered by a helper method.
24
+ */
25
+ get locator(): Locator;
26
+ /**
27
+ * Wait until the component is attached and visible. Resolves with the
28
+ * component, so it can be chained.
29
+ *
30
+ * @example
31
+ * const menu = await new PwContextMenu(page).isReady();
32
+ */
33
+ isReady(options?: {
34
+ timeout?: number;
35
+ }): Promise<this>;
36
+ /**
37
+ * Is the component currently visible. Does not wait or retry, prefer
38
+ * assertVisible within tests unless the component is known to be stable.
39
+ */
40
+ isVisible(): Promise<boolean>;
41
+ assertVisible(): Promise<void>;
42
+ assertNotAttached(): Promise<void>;
43
+ }
@@ -0,0 +1,65 @@
1
+ import { type Locator } from "@playwright/test";
2
+ import { PwComponent } from "./PwComponent";
3
+ import { PwMenuItem } from "./PwMenuItem";
4
+ /**
5
+ * A context menu (or one of its sub menus). Usually obtained from
6
+ * PwTable.showContextMenu.
7
+ *
8
+ * @example
9
+ * test("cell menu", async ({ page }) => {
10
+ * const table = new PwTable(page);
11
+ * const menu = await table.showContextMenu("ric", 2);
12
+ * expect(await menu.hasMenuItems(["Copy text", "Filter"])).toBe(true);
13
+ * const filterMenu = await menu.menuItem("Filter").openSubMenu();
14
+ * await filterMenu.menuItem("Include value").click();
15
+ * await menu.assertNotAttached();
16
+ * });
17
+ */
18
+ export declare class PwContextMenu extends PwComponent {
19
+ protected readonly defaultCssSelector = ".vuuContextMenuPanel[role=\"menu\"]";
20
+ /**
21
+ * Locator for the menu items of this menu. Sub menus are rendered in their
22
+ * own (portalled) panels, so their items are not included.
23
+ */
24
+ get menuItems(): Locator;
25
+ /**
26
+ * The labels of the menu items, in rendered order.
27
+ */
28
+ menuItemLabels(): Promise<string[]>;
29
+ /**
30
+ * With no argument, does the menu have any menu items. With labels, does
31
+ * the menu have exactly these menu items, in this order.
32
+ */
33
+ hasMenuItems(labels?: string[]): Promise<boolean>;
34
+ /**
35
+ * Menu item by label, or by 0 based index.
36
+ */
37
+ menuItem(labelOrIndex: string | number): PwMenuItem;
38
+ /**
39
+ * The menu item that currently has keyboard focus. The item must also be the
40
+ * menu's active item (tabindex="0"), which confirms the menu has processed the
41
+ * focus change and is ready for the next key press.
42
+ */
43
+ get focusedMenuItem(): PwMenuItem;
44
+ /**
45
+ * Assert the focused menu item. Wait on this between key presses, keys
46
+ * pressed before the menu has processed a focus change may be misdirected.
47
+ */
48
+ assertFocusedMenuItem(label: string): Promise<void>;
49
+ /**
50
+ * Press key(s) within the menu. Keys are sent to the focused element, which
51
+ * is assumed to be within the menu. Multiple keys are pressed without waiting
52
+ * for the menu to respond, use assertFocusedMenuItem between key presses where
53
+ * a key depends on the focus change of the previous key.
54
+ *
55
+ * @example
56
+ * await menu.press("ArrowDown");
57
+ * await menu.assertFocusedMenuItem("Filter");
58
+ * await menu.press("Enter");
59
+ */
60
+ press(...keys: string[]): Promise<void>;
61
+ /**
62
+ * Close the menu by pressing Escape.
63
+ */
64
+ close(): Promise<void>;
65
+ }
@@ -0,0 +1,48 @@
1
+ import { type Locator } from "@playwright/test";
2
+ import { PwComponent } from "./PwComponent";
3
+ import { PwContextMenu } from "./PwContextMenu";
4
+ /**
5
+ * A single item within a PwContextMenu. Usually obtained from
6
+ * PwContextMenu.menuItem rather than constructed directly.
7
+ */
8
+ export declare class PwMenuItem extends PwComponent {
9
+ protected readonly defaultCssSelector = "[role=\"menuitem\"]";
10
+ /**
11
+ * The text label of the menu item.
12
+ */
13
+ label(): Promise<string>;
14
+ /**
15
+ * Does this menu item open a sub menu.
16
+ */
17
+ hasChildren(): Promise<boolean>;
18
+ isDisabled(): Promise<boolean>;
19
+ isEnabled(): Promise<boolean>;
20
+ /**
21
+ * Is the sub menu of this menu item open. Always false for leaf items.
22
+ */
23
+ isExpanded(): Promise<boolean>;
24
+ isFocused(): Promise<boolean>;
25
+ /**
26
+ * Is the menu item decorated with the given icon (data-icon attribute).
27
+ */
28
+ hasIcon(iconName: string): Promise<boolean>;
29
+ /**
30
+ * Click the menu item. Playwright will not click a disabled item unless
31
+ * { force: true } is passed.
32
+ */
33
+ click(options?: Parameters<Locator["click"]>[0]): Promise<void>;
34
+ hover(): Promise<void>;
35
+ focus(): Promise<void>;
36
+ press(key: string): Promise<void>;
37
+ /**
38
+ * The sub menu opened by this menu item. The sub menu must already be open,
39
+ * use openSubMenu to open it.
40
+ */
41
+ subMenu(): Promise<PwContextMenu>;
42
+ /**
43
+ * Open the sub menu of this menu item, using the mouse (hover) or keyboard
44
+ * (ArrowRight). Resolves once the sub menu is visible and, when opened with
45
+ * the keyboard, once focus has moved into the sub menu.
46
+ */
47
+ openSubMenu(using?: "mouse" | "keyboard"): Promise<PwContextMenu>;
48
+ }
@@ -1,17 +1,11 @@
1
- import { type Locator, type Page } from "@playwright/test";
1
+ import { type Locator } from "@playwright/test";
2
2
  import type { VuuRange } from "@vuu-ui/vuu-protocol-types";
3
- import { PlaywrightPageObject } from "./PlaywrightPageObject";
3
+ import { PwComponent } from "./PwComponent";
4
+ import { PwContextMenu } from "./PwContextMenu";
4
5
  import { PwTableHeaderCell } from "./PwTableHeaderCell";
5
6
  type AriaRole = "cell" | "textbox" | "combobox";
6
- export declare class PwTable extends PlaywrightPageObject {
7
- #private;
8
- private readonly cssSelector?;
7
+ export declare class PwTable extends PwComponent {
9
8
  protected readonly defaultCssSelector = ".vuuTable";
10
- /**
11
- * @param page the Playwright page
12
- * @param cssSelector optional selector for the Table, overrides defaultCssSelector
13
- */
14
- constructor(page: Page, cssSelector?: string | undefined);
15
9
  get contentContainer(): Locator;
16
10
  get scrollbarContainer(): Locator;
17
11
  /**
@@ -23,7 +17,65 @@ export declare class PwTable extends PlaywrightPageObject {
23
17
  */
24
18
  cell(colName: string, ariaRowIndex: number): Promise<Locator>;
25
19
  editCell(columnName: string, ariaRowIndex: number, value: string, action?: "commit" | "cancel"): Promise<void>;
20
+ /**
21
+ * The labels displayed in the column headers, in rendered order.
22
+ * Header cells without a label (e.g. the checkbox selection column) are omitted.
23
+ *
24
+ * @example
25
+ * test("renders expected columns", async ({ page }) => {
26
+ * const table = new PwTable(page);
27
+ * expect(await table.columnNames()).toEqual(["Ccy", "Exchange", "Price"]);
28
+ * });
29
+ */
30
+ columnNames(): Promise<string[]>;
31
+ /**
32
+ * The total row count, read from aria-rowcount on the element with role="table".
33
+ * This is the dataSource size, not the number of rendered rows.
34
+ *
35
+ * @example
36
+ * test("loads all instruments", async ({ page }) => {
37
+ * const table = new PwTable(page);
38
+ * expect(await table.rowCount()).toEqual(1000);
39
+ * });
40
+ */
41
+ rowCount(): Promise<number>;
42
+ /**
43
+ * Locate the icon with the given data-icon name within a cell.
44
+ *
45
+ * @example
46
+ * test("flags stale prices", async ({ page }) => {
47
+ * const table = new PwTable(page);
48
+ * await expect(await table.cellIcon("status", 3, "warning")).toBeVisible();
49
+ * });
50
+ */
51
+ cellIcon(columnName: string, ariaRowIndex: number, iconName: string): Promise<Locator>;
52
+ /**
53
+ * Locate all rendered (non header) cells containing an icon with the given
54
+ * data-icon name. Each matching cell is counted once, however many such icons it holds.
55
+ *
56
+ * @example
57
+ * test("flags stale prices", async ({ page }) => {
58
+ * const table = new PwTable(page);
59
+ * await expect(table.cellsWithIcon("warning")).toHaveCount(4);
60
+ * });
61
+ */
62
+ cellsWithIcon(iconName: string): Locator;
26
63
  headerCell(columnName: string): PwTableHeaderCell;
64
+ /**
65
+ * Right click a cell to invoke the context menu. Resolves once the menu is
66
+ * visible. Rejects if no context menu is shown.
67
+ *
68
+ * @param columnName name of the column
69
+ * @param ariaRowIndex 1-based aria row index, including headers (with one header, the first data row is 2)
70
+ *
71
+ * @example
72
+ * test("shows cell menu", async ({ page }) => {
73
+ * const table = new PwTable(page);
74
+ * const menu = await table.showContextMenu("ric", 2);
75
+ * expect(await menu.menuItem("Copy text").isEnabled()).toBe(true);
76
+ * });
77
+ */
78
+ showContextMenu(columnName: string, ariaRowIndex: number): Promise<PwContextMenu>;
27
79
  /**
28
80
  * Locate cell by cell contents
29
81
  * @param text
@@ -40,7 +92,6 @@ export declare class PwTable extends PlaywrightPageObject {
40
92
  locateCell(row: number, columnName: string): Promise<Locator>;
41
93
  locateColumnHeader(column: number): Locator;
42
94
  assertCellIsEditable(row: number, column: number, isEditable: boolean | undefined, value?: string): Promise<void>;
43
- assertVisible(): Promise<void>;
44
95
  private getCellText;
45
96
  assertCellValue(cell: Locator, text: string, role?: AriaRole): Promise<void>;
46
97
  assertCellValue(row: number, column: number, text: string, role?: AriaRole): Promise<void>;
@@ -1,8 +1,12 @@
1
1
  import { type Page } from "@playwright/test";
2
- import { PlaywrightPageObject } from "./PlaywrightPageObject";
3
- export declare class PwTableHeaderCell extends PlaywrightPageObject {
4
- private readonly cssSelector;
5
- constructor(page: Page, cssSelector: string);
2
+ import { PwComponent, type PwComponentTarget } from "./PwComponent";
3
+ export declare class PwTableHeaderCell extends PwComponent {
4
+ protected readonly defaultCssSelector = ".vuuTableHeaderCell";
5
+ /**
6
+ * @param page the Playwright page
7
+ * @param target css selector or Locator for the header cell
8
+ */
9
+ constructor(page: Page, target: PwComponentTarget);
6
10
  get isSelected(): Promise<boolean>;
7
11
  assertSelected(): Promise<void>;
8
12
  }
package/types/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
1
  export { PlaywrightPageObject } from "./PlaywrightPageObject";
2
+ export { PwComponent, type PwComponentTarget } from "./PwComponent";
3
+ export { PwContextMenu } from "./PwContextMenu";
4
+ export { PwMenuItem } from "./PwMenuItem";
2
5
  export { PwTable } from "./PwTable";
3
6
  export { PwTableHeaderCell } from "./PwTableHeaderCell";
@@ -0,0 +1,3 @@
1
+ export declare const cssAttributeValue: (value: string) => string;
2
+ export declare const byDataIcon: (iconName: string) => string;
3
+ export declare const byId: (id: string) => string;