Skip to content

Commit a50dcf4

Browse files
DavertMikDavertMik
authored andcommitted
implemented clickXY action (#5248)
Co-authored-by: DavertMik <[email protected]>
1 parent 26ac15d commit a50dcf4

File tree

5 files changed

+237
-0
lines changed

5 files changed

+237
-0
lines changed

lib/helper/Playwright.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2102,6 +2102,49 @@ class Playwright extends Helper {
21022102
return proceedClick.call(this, locator, context, { button: 'right' })
21032103
}
21042104

2105+
/**
2106+
* Performs click at specific coordinates.
2107+
* If locator is provided, the coordinates are relative to the element.
2108+
* If locator is not provided, the coordinates are global page coordinates.
2109+
*
2110+
* ```js
2111+
* // Click at global coordinates (100, 200)
2112+
* I.clickXY(100, 200);
2113+
*
2114+
* // Click at coordinates (50, 30) relative to element
2115+
* I.clickXY('#someElement', 50, 30);
2116+
* ```
2117+
*
2118+
* @param {CodeceptJS.LocatorOrString|number} locator Element to click on or X coordinate if no element.
2119+
* @param {number} [x] X coordinate relative to element, or Y coordinate if locator is a number.
2120+
* @param {number} [y] Y coordinate relative to element.
2121+
* @returns {Promise<void>}
2122+
*/
2123+
async clickXY(locator, x, y) {
2124+
// If locator is a number, treat it as global X coordinate
2125+
if (typeof locator === 'number') {
2126+
const globalX = locator
2127+
const globalY = x
2128+
await this.page.mouse.click(globalX, globalY)
2129+
return this._waitForAction()
2130+
}
2131+
2132+
// Locator is provided, click relative to element
2133+
const el = await this._locateElement(locator)
2134+
assertElementExists(el, locator, 'Element to click')
2135+
2136+
const box = await el.boundingBox()
2137+
if (!box) {
2138+
throw new Error(`Element ${locator} is not visible or has no bounding box`)
2139+
}
2140+
2141+
const absoluteX = box.x + x
2142+
const absoluteY = box.y + y
2143+
2144+
await this.page.mouse.click(absoluteX, absoluteY)
2145+
return this._waitForAction()
2146+
}
2147+
21052148
/**
21062149
*
21072150
* [Additional options](https://playwright.dev/docs/api/class-elementhandle#element-handle-check) for check available as 3rd argument.

lib/helper/Puppeteer.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1341,6 +1341,49 @@ class Puppeteer extends Helper {
13411341
return proceedClick.call(this, locator, context, { button: 'right' })
13421342
}
13431343

1344+
/**
1345+
* Performs click at specific coordinates.
1346+
* If locator is provided, the coordinates are relative to the element.
1347+
* If locator is not provided, the coordinates are global page coordinates.
1348+
*
1349+
* ```js
1350+
* // Click at global coordinates (100, 200)
1351+
* I.clickXY(100, 200);
1352+
*
1353+
* // Click at coordinates (50, 30) relative to element
1354+
* I.clickXY('#someElement', 50, 30);
1355+
* ```
1356+
*
1357+
* @param {CodeceptJS.LocatorOrString|number} locator Element to click on or X coordinate if no element.
1358+
* @param {number} [x] X coordinate relative to element, or Y coordinate if locator is a number.
1359+
* @param {number} [y] Y coordinate relative to element.
1360+
* @returns {Promise<void>}
1361+
*/
1362+
async clickXY(locator, x, y) {
1363+
// If locator is a number, treat it as global X coordinate
1364+
if (typeof locator === 'number') {
1365+
const globalX = locator
1366+
const globalY = x
1367+
await this.page.mouse.click(globalX, globalY)
1368+
return this._waitForAction()
1369+
}
1370+
1371+
// Locator is provided, click relative to element
1372+
const els = await this._locate(locator)
1373+
assertElementExists(els, locator, 'Element to click')
1374+
1375+
const box = await els[0].boundingBox()
1376+
if (!box) {
1377+
throw new Error(`Element ${locator} is not visible or has no bounding box`)
1378+
}
1379+
1380+
const absoluteX = box.x + x
1381+
const absoluteY = box.y + y
1382+
1383+
await this.page.mouse.click(absoluteX, absoluteY)
1384+
return this._waitForAction()
1385+
}
1386+
13441387
/**
13451388
* {{> checkOption }}
13461389
*/

lib/helper/WebDriver.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,6 +1113,75 @@ class WebDriver extends Helper {
11131113
await this.browser.buttonDown(2)
11141114
}
11151115

1116+
/**
1117+
* Performs click at specific coordinates.
1118+
* If locator is provided, the coordinates are relative to the element's top-left corner.
1119+
* If locator is not provided, the coordinates are relative to the body element.
1120+
*
1121+
* ```js
1122+
* // Click at coordinates (100, 200) relative to body
1123+
* I.clickXY(100, 200);
1124+
*
1125+
* // Click at coordinates (50, 30) relative to element's top-left corner
1126+
* I.clickXY('#someElement', 50, 30);
1127+
* ```
1128+
*
1129+
* @param {CodeceptJS.LocatorOrString|number} locator Element to click on or X coordinate if no element.
1130+
* @param {number} [x] X coordinate relative to element's top-left, or Y coordinate if locator is a number.
1131+
* @param {number} [y] Y coordinate relative to element's top-left.
1132+
* @returns {Promise<void>}
1133+
*/
1134+
async clickXY(locator, x, y) {
1135+
// If locator is a number, treat it as X coordinate and use body as base
1136+
if (typeof locator === 'number') {
1137+
const globalX = locator
1138+
const globalY = x
1139+
locator = '//body'
1140+
x = globalX
1141+
y = globalY
1142+
}
1143+
1144+
// Locate the base element
1145+
const res = await this._locate(withStrictLocator(locator), true)
1146+
assertElementExists(res, locator, 'Element to click')
1147+
const el = usingFirstElement(res)
1148+
1149+
// Get element position and size to calculate top-left corner
1150+
const location = await el.getLocation()
1151+
const size = await el.getSize()
1152+
1153+
// WebDriver clicks at center by default, so we need to offset from center to top-left
1154+
// then add our desired x, y coordinates
1155+
const offsetX = -(size.width / 2) + x
1156+
const offsetY = -(size.height / 2) + y
1157+
1158+
if (this.browser.isW3C) {
1159+
// Use performActions for W3C WebDriver
1160+
return this.browser.performActions([
1161+
{
1162+
type: 'pointer',
1163+
id: 'pointer1',
1164+
parameters: { pointerType: 'mouse' },
1165+
actions: [
1166+
{
1167+
type: 'pointerMove',
1168+
origin: el,
1169+
duration: 0,
1170+
x: Math.round(offsetX),
1171+
y: Math.round(offsetY),
1172+
},
1173+
{ type: 'pointerDown', button: 0 },
1174+
{ type: 'pointerUp', button: 0 },
1175+
],
1176+
},
1177+
])
1178+
}
1179+
1180+
// Fallback for non-W3C browsers
1181+
await el.moveTo({ xOffset: Math.round(offsetX), yOffset: Math.round(offsetY) })
1182+
return el.click()
1183+
}
1184+
11161185
/**
11171186
* {{> forceRightClick }}
11181187
*
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<html>
2+
<head>
3+
<style>
4+
body {
5+
margin: 0;
6+
padding: 20px;
7+
}
8+
#clickArea {
9+
width: 400px;
10+
height: 300px;
11+
background-color: #f0f0f0;
12+
border: 2px solid #333;
13+
position: relative;
14+
margin: 20px 0;
15+
}
16+
#output {
17+
font-family: monospace;
18+
margin-top: 20px;
19+
}
20+
</style>
21+
<script>
22+
let lastClickX = 0;
23+
let lastClickY = 0;
24+
25+
function handleClick(event) {
26+
const rect = event.currentTarget.getBoundingClientRect();
27+
lastClickX = event.clientX - rect.left;
28+
lastClickY = event.clientY - rect.top;
29+
30+
document.getElementById('output').innerHTML =
31+
'Clicked at: X=' + Math.round(lastClickX) + ', Y=' + Math.round(lastClickY);
32+
}
33+
34+
function handleGlobalClick(event) {
35+
lastClickX = event.clientX;
36+
lastClickY = event.clientY;
37+
38+
document.getElementById('globalOutput').innerHTML =
39+
'Global click at: X=' + Math.round(lastClickX) + ', Y=' + Math.round(lastClickY);
40+
}
41+
42+
window.addEventListener('load', function() {
43+
document.getElementById('clickArea').addEventListener('click', handleClick);
44+
document.body.addEventListener('click', handleGlobalClick);
45+
});
46+
</script>
47+
</head>
48+
<body>
49+
50+
<h1>Click Coordinates Test</h1>
51+
52+
<div id="clickArea">
53+
Click inside this area
54+
</div>
55+
56+
<div id="output">No clicks yet</div>
57+
<div id="globalOutput">No global clicks yet</div>
58+
59+
</body>
60+
</html>

test/helper/webapi.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,28 @@ export function tests() {
352352
})
353353
})
354354

355+
describe('#clickXY', () => {
356+
it('should click at global coordinates', async () => {
357+
await I.amOnPage('/form/click_coordinates')
358+
await I.dontSee('Global click at:')
359+
await I.clickXY(100, 50)
360+
await I.see('Global click at: X=100, Y=50')
361+
})
362+
363+
it('should click at coordinates relative to element', async () => {
364+
await I.amOnPage('/form/click_coordinates')
365+
await I.dontSee('Clicked at:')
366+
await I.clickXY('#clickArea', 50, 30)
367+
await I.see('Clicked at: X=50, Y=30')
368+
})
369+
370+
it('should click at different relative coordinates', async () => {
371+
await I.amOnPage('/form/click_coordinates')
372+
await I.clickXY('#clickArea', 100, 75)
373+
await I.see('Clicked at: X=100, Y=75')
374+
})
375+
})
376+
355377
describe('#checkOption', () => {
356378
it('should check option by css', async () => {
357379
await I.amOnPage('/form/checkbox')

0 commit comments

Comments
 (0)