Skip to content

Commit 302f1d4

Browse files
committed
test(tools): press annotations with the select tool
Drag a ruler and a rectangle handle, click a ruler's line and drag a polygon's edge with the select tool. The drags must move the handle with no application error, and the line and edge presses must select the annotation without moving a handle.
1 parent 1f853b5 commit 302f1d4

1 file changed

Lines changed: 210 additions & 0 deletions

File tree

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import { type ChainablePromiseElement } from 'webdriverio';
2+
import AppPage from '../pageobjects/volview.page';
3+
import { clickAt, setupTest, waitForCircleCount } from './annotationTestUtils';
4+
5+
// One input source held across action chains, so the press lands exactly where
6+
// the hover left the pointer. Releasing actions would reset it to the origin.
7+
const HOVERING_MOUSE = 'hovering-mouse';
8+
const hoveringMouse = () => browser.action('pointer', { id: HOVERING_MOUSE });
9+
10+
const INSTANT = 0;
11+
const NUDGE_PX = 2;
12+
const DRAG_MS = 200;
13+
const DRAG_DY = 40;
14+
const TOLERANCE_PX = 3;
15+
16+
// vtk.js reports the first move after an idle period as StartMouseMove, which
17+
// the widget manager does not pick on, so land on (x, y) with a second move.
18+
const nudgeTo = (x: number, y: number) =>
19+
hoveringMouse()
20+
.move({ duration: INSTANT, x: x + NUDGE_PX, y: y + NUDGE_PX })
21+
.move({ duration: INSTANT, x, y })
22+
.perform(true);
23+
24+
const pressAtPointer = () => hoveringMouse().down().up().perform(true);
25+
26+
const dragFromPointer = (x: number, y: number) =>
27+
hoveringMouse().down().move({ duration: DRAG_MS, x, y }).up().perform(true);
28+
29+
// The widget manager sets the hover cursor once its pick lands on an annotation
30+
// and restores the default one once it lands on nothing.
31+
const waitForViewCursor = (
32+
axialView: ChainablePromiseElement,
33+
cursor: string,
34+
timeoutMsg: string
35+
) =>
36+
browser.waitUntil(
37+
async () => {
38+
const current = await axialView.$('div.view').getCSSProperty('cursor');
39+
return current.value === cursor;
40+
},
41+
{ timeout: 5000, timeoutMsg }
42+
);
43+
44+
const getHandleCenters = async (axialView: ChainablePromiseElement) => {
45+
const centers: Array<{ x: number; y: number }> = [];
46+
for (const circle of await axialView.$$('svg circle')) {
47+
centers.push({
48+
x: Number(await circle.getAttribute('cx')),
49+
y: Number(await circle.getAttribute('cy')),
50+
});
51+
}
52+
return centers;
53+
};
54+
55+
// BoundingRectangle.vue draws this around the selected annotation
56+
const waitForSelection = (
57+
axialView: ChainablePromiseElement,
58+
timeoutMsg: string
59+
) =>
60+
browser.waitUntil(
61+
async () => {
62+
const rects = await axialView.$$('svg rect[stroke="lightgray"]');
63+
return (await rects.length) === 1;
64+
},
65+
{ timeout: 5000, timeoutMsg }
66+
);
67+
68+
const placeAnnotation = async (
69+
icon: string,
70+
points: Array<[number, number]>
71+
) => {
72+
await AppPage.selectTool(icon);
73+
for (const [x, y] of points) {
74+
await clickAt(x, y);
75+
}
76+
};
77+
78+
// Confirms the press landed on the annotation, then hovers empty space so the
79+
// DOM reflects everything the press did before the handles are compared.
80+
const settleAfterPress = async (
81+
axialView: ChainablePromiseElement,
82+
awayX: number,
83+
awayY: number
84+
) => {
85+
await waitForSelection(axialView, 'Pressing the annotation should select it');
86+
await nudgeTo(awayX, awayY);
87+
await waitForViewCursor(
88+
axialView,
89+
'default',
90+
'Leaving the annotation should restore the default cursor'
91+
);
92+
};
93+
94+
const expectHandlesUnmoved = (
95+
before: Array<{ x: number; y: number }>,
96+
after: Array<{ x: number; y: number }>
97+
) => {
98+
expect(after.length).toBe(before.length);
99+
before.forEach((handle, index) => {
100+
expect(Math.abs(after[index].x - handle.x)).toBeLessThanOrEqual(
101+
TOLERANCE_PX
102+
);
103+
expect(Math.abs(after[index].y - handle.y)).toBeLessThanOrEqual(
104+
TOLERANCE_PX
105+
);
106+
});
107+
};
108+
109+
const TOOL_CASES = [
110+
{ tool: 'ruler', icon: 'mdi-ruler' },
111+
{ tool: 'rectangle', icon: 'mdi-vector-square' },
112+
];
113+
114+
// The select tool resolves its own pick on press. The annotation widgets read
115+
// the widget manager's standing pick while handling that same press.
116+
describe('Pressing an annotation with the select tool', () => {
117+
TOOL_CASES.forEach(({ tool, icon }) => {
118+
it(`drags the ${tool} handle without an application error`, async () => {
119+
const { axialView, centerX, centerY } = await setupTest();
120+
const handleX = Math.round(centerX - 60);
121+
const handleY = Math.round(centerY - 60);
122+
123+
await placeAnnotation(icon, [
124+
[handleX, handleY],
125+
[centerX + 60, centerY + 60],
126+
]);
127+
await waitForCircleCount(axialView, 2, `${tool} should have two handles`);
128+
const [before] = await getHandleCenters(axialView);
129+
130+
await AppPage.selectTool('mdi-cursor-default');
131+
await nudgeTo(handleX, handleY);
132+
await waitForViewCursor(
133+
axialView,
134+
'pointer',
135+
'Hovering the handle should pick it'
136+
);
137+
await dragFromPointer(handleX, handleY + DRAG_DY);
138+
139+
await browser.waitUntil(
140+
async () => {
141+
const [after] = await getHandleCenters(axialView);
142+
return Math.abs(after.y - before.y - DRAG_DY) <= TOLERANCE_PX;
143+
},
144+
{
145+
timeout: 5000,
146+
timeoutMsg: `The ${tool} handle should follow the drag`,
147+
}
148+
);
149+
const [after] = await getHandleCenters(axialView);
150+
expect(Math.abs(after.x - before.x)).toBeLessThanOrEqual(TOLERANCE_PX);
151+
expect(await AppPage.getNotificationsCount()).toBe(0);
152+
});
153+
});
154+
155+
it('keeps the ruler handles in place when its line is clicked', async () => {
156+
const { axialView, centerX, centerY } = await setupTest();
157+
const midX = Math.round(centerX);
158+
const midY = Math.round(centerY);
159+
160+
await placeAnnotation('mdi-ruler', [
161+
[midX - 60, midY - 60],
162+
[midX + 60, midY + 60],
163+
]);
164+
await waitForCircleCount(axialView, 2, 'Ruler should have two handles');
165+
const before = await getHandleCenters(axialView);
166+
167+
await AppPage.selectTool('mdi-cursor-default');
168+
await nudgeTo(midX, midY);
169+
await waitForViewCursor(
170+
axialView,
171+
'pointer',
172+
'Hovering the line should pick the ruler'
173+
);
174+
await pressAtPointer();
175+
await settleAfterPress(axialView, midX + 140, midY - 140);
176+
177+
expectHandlesUnmoved(before, await getHandleCenters(axialView));
178+
expect(await AppPage.getNotificationsCount()).toBe(0);
179+
});
180+
181+
// The polygon moves its active handle on mouse move, so a drag is what
182+
// would expose an unintended grab of the edge.
183+
it('keeps the polygon handles in place when its edge is dragged', async () => {
184+
const { axialView, centerX, centerY } = await setupTest();
185+
const midX = Math.round(centerX);
186+
const topY = Math.round(centerY - 80);
187+
188+
await placeAnnotation('mdi-pentagon-outline', [
189+
[midX - 80, topY],
190+
[midX + 80, topY],
191+
[midX, centerY + 80],
192+
[midX - 80, topY], // close
193+
]);
194+
await waitForCircleCount(axialView, 3, 'Polygon should have three handles');
195+
const before = await getHandleCenters(axialView);
196+
197+
await AppPage.selectTool('mdi-cursor-default');
198+
await nudgeTo(midX, topY);
199+
await waitForViewCursor(
200+
axialView,
201+
'pointer',
202+
'Hovering the edge should pick the polygon'
203+
);
204+
await dragFromPointer(midX, topY + DRAG_DY);
205+
await settleAfterPress(axialView, midX + 140, topY - 60);
206+
207+
expectHandlesUnmoved(before, await getHandleCenters(axialView));
208+
expect(await AppPage.getNotificationsCount()).toBe(0);
209+
});
210+
});

0 commit comments

Comments
 (0)