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

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
@@ -41,8 +41,16 @@ The existing `locateCell` overloads remain available for gradual migration.
41
41
  const menu = await table.showContextMenu("ric", 2);
42
42
 
43
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"]);
44
+ // exact match, in order - a partial list is false
45
+ expect(
46
+ await menu.hasMenuItems(["Copy text", "Filter", "Sort", "Delete row"]),
47
+ ).toBe(true);
48
+ expect(await menu.menuItemLabels()).toEqual([
49
+ "Copy text",
50
+ "Filter",
51
+ "Sort",
52
+ "Delete row",
53
+ ]);
46
54
 
47
55
  const copy = menu.menuItem("Copy text"); // or by 0 based index, menu.menuItem(0)
48
56
  expect(await copy.hasIcon("edit")).toBe(true);
@@ -61,6 +69,46 @@ await menu.press("Enter");
61
69
  await menu.close(); // Escape
62
70
  ```
63
71
 
72
+ To assert that right clicking a cell does not show a context menu, use
73
+ `assertNoContextMenu`. It waits for the page to render after the right click
74
+ before asserting, so it cannot pass before a menu would have appeared.
75
+
76
+ ```ts
77
+ await table.assertNoContextMenu("ccy", 2);
78
+ ```
79
+
64
80
  When a key press depends on the focus change made by the previous key, call
65
81
  `assertFocusedMenuItem` in between. Keys pressed before the menu has processed a
66
82
  focus change may be delivered to the previously focused item.
83
+
84
+ ## PwChart, PwChartDataPoint
85
+
86
+ `PwChart` wraps a Vuu `Chart` (ECharts, svg renderer). Data points are located by
87
+ series name (the Vuu column name) and 0 based data index, or category (x axis)
88
+ value, mirroring `PwTable.cell(colName, ariaRowIndex)`.
89
+
90
+ ```ts
91
+ const chart = await new PwChart(page).isReady();
92
+ expect(await chart.seriesNames()).toEqual(["price", "volume"]);
93
+ expect(await chart.categories()).toHaveLength(25);
94
+
95
+ const point = await chart.dataPoint("price", 3); // or chart.dataPoint("price", "2026-04-05")
96
+ expect(point.value).toEqual(103);
97
+ expect(point.isExcluded).toBe(false);
98
+ await point.hover();
99
+ await point.click(); // selects, with a chart selectionModel
100
+ await expect(point.symbol).toHaveAttribute("fill", "purple");
101
+
102
+ const menu = await chart.showContextMenu("price", 3); // or point.showContextMenu()
103
+ await menu.menuItem("Exclude Item").click();
104
+ await chart.assertNoContextMenu("price", 3); // when no menu is configured
105
+ ```
106
+
107
+ The data point properties (`value`, `x`, `y`, `isExcluded`) are a snapshot, taken
108
+ when the data point is obtained. Mouse actions resolve the position again and wait
109
+ until it is stable, so they work while the chart is resizing. To assert a change to
110
+ the data, obtain the data point again, e.g.
111
+ `await expect.poll(async () => (await chart.dataPoint("price", 3)).isExcluded).toBe(true)`.
112
+
113
+ The Vuu `Chart` exposes its ECharts instance on the chart canvas element
114
+ (`echartsInstance`), which `PwChart` uses to map data points to pixel positions.
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.0.0-beta.7",
2
+ "version": "4.0.1",
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.7"
12
+ "@vuu-ui/vuu-protocol-types": "4.0.1"
13
13
  },
14
14
  "peerDependencies": {
15
15
  "@playwright/test": "^1.62.1"
package/src/PwChart.js ADDED
@@ -0,0 +1,77 @@
1
+ import { expect } from "@playwright/test";
2
+ import { PwComponent } from "./PwComponent.js";
3
+ import { PwContextMenu } from "./PwContextMenu.js";
4
+ import { PwChartDataPoint } from "./PwChartDataPoint.js";
5
+ class PwChart extends PwComponent {
6
+ defaultCssSelector = ".vuuChart";
7
+ get canvas() {
8
+ return this.locator.locator("[_echarts_instance_]");
9
+ }
10
+ async isReady(options) {
11
+ await super.isReady(options);
12
+ await expect.poll(()=>this.canvas.evaluate((element)=>(element.echartsInstance?.getOption().series ?? []).some((series)=>series.data.length > 0)), options).toBe(true);
13
+ return this;
14
+ }
15
+ async seriesNames() {
16
+ await this.isReady();
17
+ return this.canvas.evaluate((element)=>{
18
+ const series = element.echartsInstance?.getOption().series ?? [];
19
+ return Array.from(new Set(series.map(({ name })=>name)));
20
+ });
21
+ }
22
+ async categories() {
23
+ await this.isReady();
24
+ return this.canvas.evaluate((element)=>element.echartsInstance?.getOption().xAxis?.[0]?.data ?? []);
25
+ }
26
+ async dataPoint(seriesName, dataIndexOrCategory) {
27
+ await this.isReady();
28
+ const resolve = ()=>this.resolveDataPoint(seriesName, dataIndexOrCategory);
29
+ return new PwChartDataPoint(this.page, this.canvas, await resolve(), resolve);
30
+ }
31
+ async resolveDataPoint(seriesName, dataIndexOrCategory) {
32
+ const info = await this.canvas.evaluate((element, [seriesName, dataIndexOrCategory])=>{
33
+ const chart = element.echartsInstance;
34
+ if (!chart) return "chart has no echartsInstance";
35
+ const { series = [], xAxis = [] } = chart.getOption();
36
+ const categories = xAxis[0]?.data ?? [];
37
+ const dataIndex = "number" == typeof dataIndexOrCategory ? dataIndexOrCategory : categories.indexOf(dataIndexOrCategory);
38
+ if (-1 === dataIndex) return `no category "${dataIndexOrCategory}"`;
39
+ const namedSeries = series.filter(({ name })=>name === seriesName);
40
+ if (0 === namedSeries.length) return `no series "${seriesName}"`;
41
+ const valueSeries = namedSeries.find(({ data })=>"number" == typeof data[dataIndex]?.value);
42
+ if (!valueSeries) return `series "${seriesName}" has no value at data index ${dataIndex}`;
43
+ const value = valueSeries.data[dataIndex].value;
44
+ const [x, y] = chart.convertToPixel({
45
+ seriesId: valueSeries.id
46
+ }, [
47
+ dataIndex,
48
+ value
49
+ ]);
50
+ return {
51
+ seriesName,
52
+ dataIndex,
53
+ category: categories[dataIndex],
54
+ value,
55
+ isExcluded: valueSeries.id !== seriesName,
56
+ x,
57
+ y
58
+ };
59
+ }, [
60
+ seriesName,
61
+ dataIndexOrCategory
62
+ ]);
63
+ if ("string" == typeof info) throw Error(`PwChart, dataPoint, ${info}`);
64
+ return info;
65
+ }
66
+ async showContextMenu(seriesName, dataIndexOrCategory) {
67
+ const dataPoint = await this.dataPoint(seriesName, dataIndexOrCategory);
68
+ return dataPoint.showContextMenu();
69
+ }
70
+ async assertNoContextMenu(seriesName, dataIndexOrCategory) {
71
+ const dataPoint = await this.dataPoint(seriesName, dataIndexOrCategory);
72
+ await PwContextMenu.assertNotOpened(this.page, ()=>dataPoint.click({
73
+ button: "right"
74
+ }));
75
+ }
76
+ }
77
+ export { PwChart };
@@ -0,0 +1,61 @@
1
+ import { PlaywrightPageObject } from "./PlaywrightPageObject.js";
2
+ import { PwContextMenu } from "./PwContextMenu.js";
3
+ class PwChartDataPoint extends PlaywrightPageObject {
4
+ canvas;
5
+ resolve;
6
+ seriesName;
7
+ dataIndex;
8
+ category;
9
+ value;
10
+ isExcluded;
11
+ x;
12
+ y;
13
+ constructor(page, canvas, info, resolve){
14
+ super(page), this.canvas = canvas, this.resolve = resolve;
15
+ this.seriesName = info.seriesName;
16
+ this.dataIndex = info.dataIndex;
17
+ this.category = info.category;
18
+ this.value = info.value;
19
+ this.isExcluded = info.isExcluded;
20
+ this.x = info.x;
21
+ this.y = info.y;
22
+ }
23
+ get symbol() {
24
+ const position = `,${round(this.x)},${round(this.y)})`;
25
+ return this.canvas.locator(`svg path[transform$="${position}"]`);
26
+ }
27
+ async click(options) {
28
+ const position = await this.stablePosition();
29
+ await this.canvas.click({
30
+ ...options,
31
+ position
32
+ });
33
+ }
34
+ async hover() {
35
+ await this.canvas.hover({
36
+ position: await this.stablePosition()
37
+ });
38
+ }
39
+ showContextMenu() {
40
+ return PwContextMenu.open(this.page, ()=>this.click({
41
+ button: "right"
42
+ }));
43
+ }
44
+ async stablePosition() {
45
+ let { x, y } = await this.resolve();
46
+ for(let attempt = 0; attempt < MAX_STABLE_ATTEMPTS; attempt++){
47
+ await nextAnimationFrames(this.page);
48
+ const current = await this.resolve();
49
+ if (current.x === x && current.y === y) return {
50
+ x,
51
+ y
52
+ };
53
+ ({ x, y } = current);
54
+ }
55
+ throw Error(`PwChartDataPoint, ${this.seriesName}[${this.dataIndex}] position is not stable`);
56
+ }
57
+ }
58
+ const MAX_STABLE_ATTEMPTS = 50;
59
+ const round = (value)=>parseFloat(value.toFixed(4));
60
+ const nextAnimationFrames = (page)=>page.evaluate(()=>new Promise((resolve)=>requestAnimationFrame(()=>requestAnimationFrame(()=>resolve()))));
61
+ export { PwChartDataPoint };
@@ -0,0 +1,44 @@
1
+ import { expect } from "@playwright/test";
2
+ import { PwComponent } from "./PwComponent.js";
3
+ import { cssAttributeValue } from "./selector-utils.js";
4
+ class PwColumnPicker extends PwComponent {
5
+ defaultCssSelector = ".vuuColumnPicker";
6
+ get selectedOptions() {
7
+ return this.locator.locator(".vuuItemPicker-selectedList [role='option']");
8
+ }
9
+ get availableOptions() {
10
+ return this.locator.locator(".vuuItemPicker-availableList [role='option']");
11
+ }
12
+ selectedColumns() {
13
+ return this.optionNames(this.selectedOptions);
14
+ }
15
+ availableColumns() {
16
+ return this.optionNames(this.availableOptions);
17
+ }
18
+ async addColumn(columnName) {
19
+ await this.clickAction(this.option(this.availableOptions, columnName));
20
+ }
21
+ async removeColumn(columnName) {
22
+ await this.clickAction(this.option(this.selectedOptions, columnName));
23
+ }
24
+ async search(text) {
25
+ await this.locator.getByRole("search").getByRole("textbox").fill(text);
26
+ }
27
+ async assertSelectedColumns(columnNames) {
28
+ await expect.poll(()=>this.selectedColumns()).toEqual(columnNames);
29
+ }
30
+ async assertAvailableColumns(columnNames) {
31
+ await expect.poll(()=>this.availableColumns()).toEqual(expect.arrayContaining(columnNames));
32
+ }
33
+ async clickAction(option) {
34
+ await option.hover();
35
+ await option.locator(".vuuItemPickerListItem-action").click();
36
+ }
37
+ option(options, columnName) {
38
+ return options.and(this.page.locator(`[data-name="${cssAttributeValue(columnName)}"]`));
39
+ }
40
+ optionNames(options) {
41
+ return options.evaluateAll((elements)=>elements.map((element)=>element.getAttribute("data-name") ?? ""));
42
+ }
43
+ }
44
+ export { PwColumnPicker };
@@ -1,8 +1,22 @@
1
1
  import { expect } from "@playwright/test";
2
2
  import { PwComponent } from "./PwComponent.js";
3
3
  import { PwMenuItem } from "./PwMenuItem.js";
4
+ import { byId } from "./selector-utils.js";
4
5
  class PwContextMenu extends PwComponent {
5
6
  defaultCssSelector = '.vuuContextMenuPanel[role="menu"]';
7
+ static async open(page, openAction) {
8
+ await openAction();
9
+ const menu = await new PwContextMenu(page).isReady();
10
+ const menuId = await menu.locator.getAttribute("id");
11
+ const contextMenu = menuId ? new PwContextMenu(page, byId(menuId)) : menu;
12
+ await expect(contextMenu.locator.and(page.locator(":focus-within"))).toHaveCount(1);
13
+ return contextMenu;
14
+ }
15
+ static async assertNotOpened(page, openAction) {
16
+ await openAction();
17
+ await page.evaluate(()=>new Promise((resolve)=>requestAnimationFrame(()=>requestAnimationFrame(()=>resolve()))));
18
+ await new PwContextMenu(page).assertNotAttached();
19
+ }
6
20
  get menuItems() {
7
21
  return this.locator.getByRole("menuitem");
8
22
  }
package/src/PwTable.js CHANGED
@@ -2,7 +2,8 @@ import { expect } from "@playwright/test";
2
2
  import { PwComponent } from "./PwComponent.js";
3
3
  import { PwContextMenu } from "./PwContextMenu.js";
4
4
  import { PwTableHeaderCell } from "./PwTableHeaderCell.js";
5
- import { byDataIcon, byId, cssAttributeValue } from "./selector-utils.js";
5
+ import { byDataIcon, cssAttributeValue } from "./selector-utils.js";
6
+ const SCROLL_TOLERANCE = 1;
6
7
  const byAriaRowIndex = (ariaRowIndex)=>`[aria-rowindex="${ariaRowIndex}"]:scope`;
7
8
  const xpathStringLiteral = (value)=>{
8
9
  if (!value.includes("'")) return `'${value}'`;
@@ -17,6 +18,56 @@ class PwTable extends PwComponent {
17
18
  get scrollbarContainer() {
18
19
  return this.locator.locator(".vuuTable-scrollbarContainer");
19
20
  }
21
+ scrollPosition() {
22
+ return this.scrollbarContainer.evaluate(({ scrollLeft, scrollTop })=>({
23
+ scrollLeft,
24
+ scrollTop
25
+ }));
26
+ }
27
+ scrollUp(distance) {
28
+ return this.scroll("vertical", -1, distance);
29
+ }
30
+ scrollDown(distance) {
31
+ return this.scroll("vertical", 1, distance);
32
+ }
33
+ scrollLeft(distance) {
34
+ return this.scroll("horizontal", -1, distance);
35
+ }
36
+ scrollRight(distance) {
37
+ return this.scroll("horizontal", 1, distance);
38
+ }
39
+ async scroll(axis, direction, distance) {
40
+ if ("number" == typeof distance && distance < 0) throw Error("PwTable, scroll, distance must not be negative, use the opposite direction");
41
+ await this.scrollbarContainer.evaluate((el, { axis, direction, distance })=>{
42
+ const vertical = "vertical" === axis;
43
+ const current = vertical ? el.scrollTop : el.scrollLeft;
44
+ const max = vertical ? el.scrollHeight - el.clientHeight : el.scrollWidth - el.clientWidth;
45
+ const pos = "start" === distance ? 0 : "end" === distance ? max : current + direction * ("page" === distance ? el.clientHeight : distance);
46
+ el.scrollTo({
47
+ [vertical ? "top" : "left"]: Math.min(max, Math.max(0, pos)),
48
+ behavior: "instant"
49
+ });
50
+ }, {
51
+ axis,
52
+ direction,
53
+ distance
54
+ });
55
+ await expect.poll(()=>this.isContentScrollSynced(), {
56
+ message: "PwTable, content container scroll position not synced"
57
+ }).toBe(true);
58
+ }
59
+ isContentScrollSynced() {
60
+ return this.locator.evaluate((table, tolerance)=>{
61
+ const scrollbar = table.querySelector(".vuuTable-scrollbarContainer");
62
+ const content = table.querySelector(".vuuTable-contentContainer");
63
+ if (!scrollbar || !content) return false;
64
+ const isSynced = (sourcePos, sourceMax, targetPos, targetMax)=>{
65
+ const expected = sourceMax > 0 ? sourcePos / sourceMax * targetMax : 0;
66
+ return Math.abs(targetPos - expected) <= tolerance;
67
+ };
68
+ return isSynced(scrollbar.scrollTop, scrollbar.scrollHeight - scrollbar.clientHeight, content.scrollTop, content.scrollHeight - content.clientHeight) && isSynced(scrollbar.scrollLeft, scrollbar.scrollWidth - scrollbar.clientWidth, content.scrollLeft, content.scrollWidth - content.clientWidth);
69
+ }, SCROLL_TOLERANCE);
70
+ }
20
71
  row(row) {
21
72
  return this.locator.locator(`[aria-rowindex="${row}"]`);
22
73
  }
@@ -34,11 +85,31 @@ class PwTable extends PwComponent {
34
85
  async columnNames() {
35
86
  return this.locator.locator(".vuuTableHeaderCell .vuuTableHeaderCell-label").allTextContents();
36
87
  }
88
+ headerColumnNames() {
89
+ return this.locator.locator(".vuuTableHeaderCell[data-column-name]").evaluateAll((elements)=>elements.map((element)=>element.getAttribute("data-column-name") ?? ""));
90
+ }
91
+ async assertColumns(columnNames) {
92
+ await expect.poll(()=>this.headerColumnNames()).toEqual(columnNames);
93
+ }
37
94
  async rowCount() {
38
95
  const ariaRowCount = await this.locator.getByRole("table").getAttribute("aria-rowcount");
39
96
  if (null === ariaRowCount) throw Error("PwTable, rowCount, table does not have an aria-rowcount");
40
97
  return parseInt(ariaRowCount, 10);
41
98
  }
99
+ get dataLoaded() {
100
+ return expect.poll(()=>this.rowCount()).toBeGreaterThan(0);
101
+ }
102
+ async columnValues(columnName) {
103
+ const headerCell = this.locator.locator(`xpath=.//*[@role='columnheader' and @data-column-name=${xpathStringLiteral(columnName)}]`).first();
104
+ const ariaColIndex = await headerCell.getAttribute("aria-colindex");
105
+ if (null === ariaColIndex) throw Error(`PwTable, columnValues, column "${columnName}" does not have an aria-colindex`);
106
+ const cells = this.locator.locator(`[role="row"] > [role="cell"][aria-colindex="${ariaColIndex}"]`);
107
+ const values = await cells.evaluateAll((elements)=>elements.map((cell)=>({
108
+ rowIndex: Number(cell.parentElement?.getAttribute("aria-rowindex")),
109
+ text: cell.textContent ?? ""
110
+ })));
111
+ return values.sort((v1, v2)=>v1.rowIndex - v2.rowIndex).map(({ text })=>text);
112
+ }
42
113
  async cellIcon(columnName, ariaRowIndex, iconName) {
43
114
  const cell = await this.cell(columnName, ariaRowIndex);
44
115
  return cell.locator(byDataIcon(iconName));
@@ -53,12 +124,16 @@ class PwTable extends PwComponent {
53
124
  }
54
125
  async showContextMenu(columnName, ariaRowIndex) {
55
126
  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;
127
+ return PwContextMenu.open(this.page, ()=>cell.click({
128
+ button: "right"
129
+ }));
130
+ }
131
+ async assertNoContextMenu(columnName, ariaRowIndex) {
132
+ const cell = await this.cell(columnName, ariaRowIndex);
133
+ await PwContextMenu.assertNotOpened(this.page, ()=>cell.click({
134
+ button: "right"
135
+ }));
136
+ await expect(cell).not.toContainClass("ContextOpen");
62
137
  }
63
138
  locateCell(rowOrText, column) {
64
139
  if ("number" == typeof rowOrText && "number" == typeof column) return this.locator.locator(`[aria-rowindex="${rowOrText}"] > [aria-colindex="${column}"]`);
@@ -0,0 +1,16 @@
1
+ import { PwColumnPicker } from "./PwColumnPicker.js";
2
+ import { PwComponent } from "./PwComponent.js";
3
+ class PwTableFooter extends PwComponent {
4
+ defaultCssSelector = ".vuuTableFooter";
5
+ action(name) {
6
+ return this.locator.getByRole("button", {
7
+ name,
8
+ exact: true
9
+ });
10
+ }
11
+ async showColumnPicker(name = "Show column picker") {
12
+ await this.action(name).click();
13
+ return new PwColumnPicker(this.page).isReady();
14
+ }
15
+ }
16
+ export { PwTableFooter };
@@ -1,5 +1,6 @@
1
1
  import { expect } from "@playwright/test";
2
2
  import { PwComponent } from "./PwComponent.js";
3
+ import { PwContextMenu } from "./PwContextMenu.js";
3
4
  class PwTableHeaderCell extends PwComponent {
4
5
  defaultCssSelector = ".vuuTableHeaderCell";
5
6
  constructor(page, target){
@@ -11,5 +12,12 @@ class PwTableHeaderCell extends PwComponent {
11
12
  async assertSelected() {
12
13
  await expect(this.locator).toContainClass("vuuTableHeaderCell-selected");
13
14
  }
15
+ async showColumnMenu() {
16
+ await this.locator.getByRole("button", {
17
+ name: "Open Column Menu",
18
+ exact: true
19
+ }).click();
20
+ return new PwContextMenu(this.page, this.page.locator(".saltMenuPanel[role='menu']")).isReady();
21
+ }
14
22
  }
15
23
  export { PwTableHeaderCell };
package/src/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  export { PlaywrightPageObject } from "./PlaywrightPageObject.js";
2
+ export { PwChart } from "./PwChart.js";
3
+ export { PwChartDataPoint } from "./PwChartDataPoint.js";
4
+ export { PwColumnPicker } from "./PwColumnPicker.js";
2
5
  export { PwComponent } from "./PwComponent.js";
3
6
  export { PwContextMenu } from "./PwContextMenu.js";
4
7
  export { PwMenuItem } from "./PwMenuItem.js";
5
8
  export { PwTable } from "./PwTable.js";
9
+ export { PwTableFooter } from "./PwTableFooter.js";
6
10
  export { PwTableHeaderCell } from "./PwTableHeaderCell.js";
@@ -0,0 +1,124 @@
1
+ import { type Locator } from "@playwright/test";
2
+ import { PwComponent } from "./PwComponent";
3
+ import { PwContextMenu } from "./PwContextMenu";
4
+ import { PwChartDataPoint } from "./PwChartDataPoint";
5
+ /**
6
+ * A Vuu Chart (VuuChart, an ECharts line chart). Data points are identified by
7
+ * series name, which is the Vuu column name, and 0 based data index, which is
8
+ * also the index of the category on the x axis.
9
+ *
10
+ * The chart must use the svg renderer for symbol locators (PwChartDataPoint.symbol).
11
+ *
12
+ * @example
13
+ * test("chart renders price and volume", async ({ mount, page }) => {
14
+ * await mount("Chart/LineChart/SimpleLineChart");
15
+ * const chart = await new PwChart(page).isReady();
16
+ * expect(await chart.seriesNames()).toEqual(["price", "volume"]);
17
+ * const point = await chart.dataPoint("price", 0);
18
+ * expect(point.value).toEqual(100);
19
+ * });
20
+ */
21
+ export declare class PwChart extends PwComponent {
22
+ protected readonly defaultCssSelector = ".vuuChart";
23
+ /**
24
+ * Locator for the element that renders the chart. This element receives
25
+ * mouse events and holds the ECharts instance.
26
+ *
27
+ * @example
28
+ * test("chart renders svg", async ({ page }) => {
29
+ * const chart = new PwChart(page);
30
+ * await expect(chart.canvas.locator("svg")).toBeVisible();
31
+ * });
32
+ */
33
+ get canvas(): Locator;
34
+ /**
35
+ * Wait until the chart is visible and has rendered series data. Resolves
36
+ * with the chart, so it can be chained.
37
+ *
38
+ * @example
39
+ * test("chart is ready", async ({ mount, page }) => {
40
+ * await mount("Chart/LineChart/SimpleLineChart");
41
+ * const chart = await new PwChart(page).isReady();
42
+ * expect(await chart.categories()).toHaveLength(25);
43
+ * });
44
+ */
45
+ isReady(options?: {
46
+ timeout?: number;
47
+ }): Promise<this>;
48
+ /**
49
+ * The names of the rendered series, in render order. These are the Vuu
50
+ * column names of the series columns.
51
+ *
52
+ * @example
53
+ * test("price only", async ({ page }) => {
54
+ * await page.getByRole("button", { name: "Price Only" }).click();
55
+ * const chart = await new PwChart(page).isReady();
56
+ * await expect.poll(() => chart.seriesNames()).toEqual(["price"]);
57
+ * });
58
+ */
59
+ seriesNames(): Promise<string[]>;
60
+ /**
61
+ * The category (x axis) values, in order.
62
+ *
63
+ * @example
64
+ * test("first category", async ({ page }) => {
65
+ * const chart = await new PwChart(page).isReady();
66
+ * expect((await chart.categories())[0]).toEqual("2026-04-02");
67
+ * });
68
+ */
69
+ categories(): Promise<string[]>;
70
+ /**
71
+ * Locate a data point by series name and 0 based data index, or by category
72
+ * (x axis) value. Waits for the chart to be ready. Rejects if the series or
73
+ * data point does not exist, or the data point has no value.
74
+ *
75
+ * @param seriesName the series (Vuu column) name
76
+ * @param dataIndexOrCategory 0 based data index, or category value
77
+ *
78
+ * @example
79
+ * test("locate data points", async ({ page }) => {
80
+ * const chart = new PwChart(page);
81
+ * const byIndex = await chart.dataPoint("price", 3);
82
+ * const byCategory = await chart.dataPoint("price", "2026-04-05");
83
+ * expect(byIndex.value).toEqual(byCategory.value);
84
+ * });
85
+ */
86
+ dataPoint(seriesName: string, dataIndexOrCategory: number | string): Promise<PwChartDataPoint>;
87
+ private resolveDataPoint;
88
+ /**
89
+ * Right click a data point to invoke the context menu. Resolves once the
90
+ * menu is visible and has focus. Waits until the test timeout if no context
91
+ * menu is shown, use assertNoContextMenu to test for that.
92
+ *
93
+ * @param seriesName the series (Vuu column) name
94
+ * @param dataIndexOrCategory 0 based data index, or category value
95
+ *
96
+ * @example
97
+ * test("exclude data point", async ({ mount, page }) => {
98
+ * await mount("Chart/LineChart/DataExclusions");
99
+ * const chart = new PwChart(page);
100
+ * const menu = await chart.showContextMenu("price", 3);
101
+ * expect(await menu.hasMenuItems(["Exclude Item"])).toBe(true);
102
+ * await menu.menuItem("Exclude Item").click();
103
+ * await expect
104
+ * .poll(async () => (await chart.dataPoint("price", 3)).isExcluded)
105
+ * .toBe(true);
106
+ * });
107
+ */
108
+ showContextMenu(seriesName: string, dataIndexOrCategory: number | string): Promise<PwContextMenu>;
109
+ /**
110
+ * Right click a data point and assert that no context menu is shown. Waits
111
+ * for the page to render after the right click before asserting, so it
112
+ * cannot pass before a menu would have appeared.
113
+ *
114
+ * @param seriesName the series (Vuu column) name
115
+ * @param dataIndexOrCategory 0 based data index, or category value
116
+ *
117
+ * @example
118
+ * test("no context menu in view mode", async ({ mount, page }) => {
119
+ * await mount("Chart/LineChart/EditableChart");
120
+ * await new PwChart(page).assertNoContextMenu("price", 3);
121
+ * });
122
+ */
123
+ assertNoContextMenu(seriesName: string, dataIndexOrCategory: number | string): Promise<void>;
124
+ }