@vuu-ui/vuu-playwright-helpers 4.0.0-beta.7 → 4.0.0
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 +50 -2
- package/package.json +2 -2
- package/src/PwChart.js +77 -0
- package/src/PwChartDataPoint.js +61 -0
- package/src/PwColumnPicker.js +44 -0
- package/src/PwContextMenu.js +14 -0
- package/src/PwTable.js +82 -7
- package/src/PwTableFooter.js +16 -0
- package/src/PwTableHeaderCell.js +8 -0
- package/src/index.js +4 -0
- package/types/PwChart.d.ts +124 -0
- package/types/PwChartDataPoint.d.ts +116 -0
- package/types/PwColumnPicker.d.ts +116 -0
- package/types/PwComponent.d.ts +45 -0
- package/types/PwContextMenu.d.ts +116 -8
- package/types/PwMenuItem.d.ts +150 -2
- package/types/PwTable.d.ts +358 -7
- package/types/PwTableFooter.d.ts +40 -0
- package/types/PwTableHeaderCell.d.ts +43 -0
- package/types/index.d.ts +5 -1
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
|
-
|
|
45
|
-
expect(
|
|
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
|
|
2
|
+
"version": "4.0.0",
|
|
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
|
|
12
|
+
"@vuu-ui/vuu-protocol-types": "4.0.0"
|
|
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 };
|
package/src/PwContextMenu.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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 };
|
package/src/PwTableHeaderCell.js
CHANGED
|
@@ -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
|
+
}
|