Skip to content

Commit 19ef9fa

Browse files
VorobeykoDavertMik
authored andcommitted
add new method waitForClickable (#2022)
* add new method waitForClickable * Fix comments * Add waitForClickable for webdriver * Fix comments * fix Puppeteer tests
1 parent b81e4f6 commit 19ef9fa

File tree

11 files changed

+317
-6
lines changed

11 files changed

+317
-6
lines changed

Diff for: docs/helpers/Puppeteer.md

+18
Original file line numberDiff line numberDiff line change
@@ -1684,6 +1684,24 @@ I.wait(2); // wait 2 secs
16841684
16851685
16861686
1687+
### waitForClickable
1688+
1689+
Waits for element to be clickable (by default waits for 1sec).
1690+
Element can be located by CSS or XPath.
1691+
1692+
```js
1693+
I.waitForClickable('.btn.continue');
1694+
I.waitForClickable('.btn.continue', 5); // wait for 5 secs
1695+
```
1696+
1697+
#### Parameters
1698+
1699+
- `locator` CodeceptJS.LocatorOrString element located by CSS|XPath|strict locator.
1700+
- `waitTimeout`
1701+
- `sec` [number][9]? (optional, `1` by default) time in seconds to wait
1702+
1703+
1704+
16871705
### waitForDetached
16881706
16891707
Waits for an element to become not attached to the DOM on a page (by default waits for 1sec).

Diff for: docs/helpers/WebDriver.md

+18
Original file line numberDiff line numberDiff line change
@@ -1836,6 +1836,24 @@ I.wait(2); // wait 2 secs
18361836
18371837
18381838
1839+
### waitForClickable
1840+
1841+
Waits for element to be clickable (by default waits for 1sec).
1842+
Element can be located by CSS or XPath.
1843+
1844+
```js
1845+
I.waitForClickable('.btn.continue');
1846+
I.waitForClickable('.btn.continue', 5); // wait for 5 secs
1847+
```
1848+
1849+
#### Parameters
1850+
1851+
- `locator` CodeceptJS.LocatorOrString element located by CSS|XPath|strict locator.
1852+
- `waitTimeout`
1853+
- `sec` [number][22]? (optional, `1` by default) time in seconds to wait
1854+
1855+
1856+
18391857
### waitForDetached
18401858
18411859
Waits for an element to become not attached to the DOM on a page (by default waits for 1sec).

Diff for: docs/webapi/waitForClickable.mustache

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Waits for element to be clickable (by default waits for 1sec).
2+
Element can be located by CSS or XPath.
3+
4+
```js
5+
I.waitForClickable('.btn.continue');
6+
I.waitForClickable('.btn.continue', 5); // wait for 5 secs
7+
```
8+
9+
@param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
10+
@param {number} [sec] (optional, `1` by default) time in seconds to wait

Diff for: lib/helper/Puppeteer.js

+17
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const { urlEquals } = require('../assert/equal');
77
const { equals } = require('../assert/equal');
88
const { empty } = require('../assert/empty');
99
const { truth } = require('../assert/truth');
10+
const isElementClickable = require('./scripts/isElementClickable');
1011
const {
1112
xpathLocator,
1213
ucfirst,
@@ -1694,6 +1695,22 @@ class Puppeteer extends Helper {
16941695
});
16951696
}
16961697

1698+
/**
1699+
* {{> waitForClickable }}
1700+
*/
1701+
async waitForClickable(locator, waitTimeout) {
1702+
const els = await this._locate(locator);
1703+
assertElementExists(els, locator);
1704+
1705+
return this.waitForFunction(isElementClickable, [els[0]], waitTimeout).catch(async (e) => {
1706+
if (/failed: timeout/i.test(e.message)) {
1707+
throw new Error(`element ${new Locator(locator).toString()} still not clickable after ${waitTimeout || this.options.waitForTimeout / 1000} sec`);
1708+
} else {
1709+
throw e;
1710+
}
1711+
});
1712+
}
1713+
16971714
/**
16981715
* {{> waitForElement }}
16991716
* {{ react }}

Diff for: lib/helper/WebDriver.js

+15
Original file line numberDiff line numberDiff line change
@@ -1738,6 +1738,21 @@ class WebDriver extends Helper {
17381738
}, aSec * 1000, `element (${locator}) still not present on page after ${aSec} sec`);
17391739
}
17401740

1741+
/**
1742+
* {{> waitForClickable }}
1743+
*/
1744+
async waitForClickable(locator, waitTimeout) {
1745+
waitTimeout = waitTimeout || this.options.waitForTimeout;
1746+
let res = await this._locate(locator);
1747+
res = usingFirstElement(res);
1748+
assertElementExists(res, locator);
1749+
1750+
return res.waitForClickable({
1751+
timeout: waitTimeout,
1752+
timeoutMsg: `element ${res.selector} still not clickable after ${waitTimeout} sec`,
1753+
});
1754+
}
1755+
17411756
async waitUntilExists(locator, sec = null) {
17421757
console.log(`waitUntilExists deprecated:
17431758
* use 'waitForElement' to wait for element to be attached

Diff for: lib/helper/scripts/isElementClickable.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
function isElementClickable(elem) {
2+
if (!elem.getBoundingClientRect || !elem.scrollIntoView || !document.elementFromPoint) {
3+
return false;
4+
}
5+
6+
const isElementInViewport = (elem) => {
7+
const rect = elem.getBoundingClientRect();
8+
const verticleInView = (rect.top <= window.innerHeight) && ((rect.top + rect.height) > 0);
9+
const horizontalInView = (rect.left <= window.innerWidth) && ((rect.left + rect.width) > 0);
10+
return horizontalInView && verticleInView;
11+
};
12+
13+
const getOverlappingElement = (elem) => {
14+
const rect = elem.getBoundingClientRect();
15+
const x = rect.left + (elem.clientWidth / 2);
16+
const y = rect.top + (elem.clientHeight / 2);
17+
return document.elementFromPoint(x, y);
18+
};
19+
20+
const isClickable = elem => elem.disabled !== true && isElementInViewport(elem) && getOverlappingElement(elem) === elem;
21+
return isClickable(elem);
22+
}
23+
24+
module.exports = isElementClickable;

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@
106106
"tsd-jsdoc": "^2.3.0",
107107
"typescript": "^2.9.2",
108108
"wdio-docker-service": "^1.5.0",
109-
"webdriverio": "^5.13.2",
109+
"webdriverio": "^5.16.6",
110110
"xmldom": "^0.1.27",
111111
"xpath": "0.0.27"
112112
},

Diff for: test/data/app/view/form/wait_for_clickable.php

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<html>
2+
<body>
3+
<style>
4+
#notInViewportTop {
5+
margin-top: -9999999px
6+
}
7+
#notInViewportBottom {
8+
margin-bottom: -9999999px
9+
}
10+
#notInViewportLeft {
11+
margin-left: -9999999px
12+
}
13+
#notInViewportRight {
14+
margin-right: -9999999px
15+
}
16+
</style>
17+
<input id="text" type="text" name="test" value="some text">
18+
19+
<button id="button" type="button" name="button1" disabled value="first">A Button</button>
20+
21+
<div id="notInViewportTop">Div not in viewport by top</div>
22+
<div id="notInViewportBottom">Div not in viewport by bottom</div>
23+
<div id="notInViewportLeft">Div not in viewport by left</div>
24+
<div id="notInViewportRight">Div not in viewport by right</div>
25+
26+
<div id="div1" style="position:absolute; top:100; left:0;">
27+
<button id="div1_button" type="button" name="button1" value="first">First Button</button>
28+
</div>
29+
<div id="div2" style="position:absolute; top:100; left:0;">
30+
<button id="div2_button" type="button" name="button1" value="first">Second Button</button>
31+
</div>
32+
33+
</body>
34+
</html>

Diff for: test/helper/Puppeteer_test.js

+76-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
const TestHelper = require('../support/TestHelper');
22
const Puppeteer = require('../../lib/helper/Puppeteer');
33
const puppeteer = require('puppeteer');
4-
const should = require('chai').should();
54
const expect = require('chai').expect;
65
const assert = require('assert');
76
const path = require('path');
8-
const fs = require('fs');
9-
const fileExists = require('../../lib/utils').fileExists;
107
const AssertionFailedError = require('../../lib/assert/error');
11-
const formContents = require('../../lib/utils').test.submittedData(path.join(__dirname, '/../data/app/db'));
12-
const expectError = require('../../lib/utils').test.expectError;
138
const webApiTests = require('./webapi');
149
const FileSystem = require('../../lib/helper/FileSystem');
1510

@@ -773,6 +768,82 @@ describe('Puppeteer', function () {
773768
await FS.seeFile('downloads/avatar.jpg');
774769
});
775770
});
771+
772+
describe('#waitForClickable', () => {
773+
it('should wait for clickable', async () => {
774+
await I.amOnPage('/form/wait_for_clickable');
775+
await I.waitForClickable({ css: 'input#text' });
776+
});
777+
778+
it('should wait for clickable by XPath', async () => {
779+
await I.amOnPage('/form/wait_for_clickable');
780+
await I.waitForClickable({ xpath: './/input[@id="text"]' });
781+
});
782+
783+
it('should fail for disabled element', async () => {
784+
await I.amOnPage('/form/wait_for_clickable');
785+
await I.waitForClickable({ css: '#button' }, 0.1).then((isClickable) => {
786+
if (isClickable) throw new Error('Element is clickable, but must be unclickable');
787+
}).catch((e) => {
788+
e.message.should.include('element {css: #button} still not clickable after 0.1 sec');
789+
});
790+
});
791+
792+
it('should fail for disabled element by XPath', async () => {
793+
await I.amOnPage('/form/wait_for_clickable');
794+
await I.waitForClickable({ xpath: './/button[@id="button"]' }, 0.1).then((isClickable) => {
795+
if (isClickable) throw new Error('Element is clickable, but must be unclickable');
796+
}).catch((e) => {
797+
e.message.should.include('element {xpath: .//button[@id="button"]} still not clickable after 0.1 sec');
798+
});
799+
});
800+
801+
it('should fail for element not in viewport by top', async () => {
802+
await I.amOnPage('/form/wait_for_clickable');
803+
await I.waitForClickable({ css: '#notInViewportTop' }, 0.1).then((isClickable) => {
804+
if (isClickable) throw new Error('Element is clickable, but must be unclickable');
805+
}).catch((e) => {
806+
e.message.should.include('element {css: #notInViewportTop} still not clickable after 0.1 sec');
807+
});
808+
});
809+
810+
it('should fail for element not in viewport by bottom', async () => {
811+
await I.amOnPage('/form/wait_for_clickable');
812+
await I.waitForClickable({ css: '#notInViewportBottom' }, 0.1).then((isClickable) => {
813+
if (isClickable) throw new Error('Element is clickable, but must be unclickable');
814+
}).catch((e) => {
815+
e.message.should.include('element {css: #notInViewportBottom} still not clickable after 0.1 sec');
816+
});
817+
});
818+
819+
it('should fail for element not in viewport by left', async () => {
820+
await I.amOnPage('/form/wait_for_clickable');
821+
await I.waitForClickable({ css: '#notInViewportLeft' }, 0.1).then((isClickable) => {
822+
if (isClickable) throw new Error('Element is clickable, but must be unclickable');
823+
}).catch((e) => {
824+
e.message.should.include('element {css: #notInViewportLeft} still not clickable after 0.1 sec');
825+
});
826+
});
827+
828+
it('should fail for element not in viewport by right', async () => {
829+
await I.amOnPage('/form/wait_for_clickable');
830+
await I.waitForClickable({ css: '#notInViewportRight' }, 0.1).then((isClickable) => {
831+
if (isClickable) throw new Error('Element is clickable, but must be unclickable');
832+
}).catch((e) => {
833+
e.message.should.include('element {css: #notInViewportRight} still not clickable after 0.1 sec');
834+
});
835+
});
836+
837+
it('should fail for overlapping element', async () => {
838+
await I.amOnPage('/form/wait_for_clickable');
839+
await I.waitForClickable({ css: '#div2_button' }, 0.1);
840+
await I.waitForClickable({ css: '#div1_button' }, 0.1).then((isClickable) => {
841+
if (isClickable) throw new Error('Element is clickable, but must be unclickable');
842+
}).catch((e) => {
843+
e.message.should.include('element {css: #div1_button} still not clickable after 0.1 sec');
844+
});
845+
});
846+
});
776847
});
777848

778849
let remoteBrowser;

Diff for: test/helper/WebDriver_test.js

+76
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,82 @@ describe('WebDriver', function () {
10221022
});
10231023
});
10241024

1025+
describe('#waitForClickable', () => {
1026+
it('should wait for clickable', async () => {
1027+
await wd.amOnPage('/form/wait_for_clickable');
1028+
await wd.waitForClickable({ css: 'input#text' });
1029+
});
1030+
1031+
it('should wait for clickable by XPath', async () => {
1032+
await wd.amOnPage('/form/wait_for_clickable');
1033+
await wd.waitForClickable({ xpath: './/input[@id="text"]' });
1034+
});
1035+
1036+
it('should fail for disabled element', async () => {
1037+
await wd.amOnPage('/form/wait_for_clickable');
1038+
await wd.waitForClickable({ css: '#button' }, 0.1).then((isClickable) => {
1039+
if (isClickable) throw new Error('Element is clickable, but must be unclickable');
1040+
}).catch((e) => {
1041+
e.message.should.include('element #button still not clickable after 0.1 sec');
1042+
});
1043+
});
1044+
1045+
it('should fail for disabled element by XPath', async () => {
1046+
await wd.amOnPage('/form/wait_for_clickable');
1047+
await wd.waitForClickable({ xpath: './/button[@id="button"]' }, 0.1).then((isClickable) => {
1048+
if (isClickable) throw new Error('Element is clickable, but must be unclickable');
1049+
}).catch((e) => {
1050+
e.message.should.include('element .//button[@id="button"] still not clickable after 0.1 sec');
1051+
});
1052+
});
1053+
1054+
it('should fail for element not in viewport by top', async () => {
1055+
await wd.amOnPage('/form/wait_for_clickable');
1056+
await wd.waitForClickable({ css: '#notInViewportTop' }, 0.1).then((isClickable) => {
1057+
if (isClickable) throw new Error('Element is clickable, but must be unclickable');
1058+
}).catch((e) => {
1059+
e.message.should.include('element #notInViewportTop still not clickable after 0.1 sec');
1060+
});
1061+
});
1062+
1063+
it('should fail for element not in viewport by bottom', async () => {
1064+
await wd.amOnPage('/form/wait_for_clickable');
1065+
await wd.waitForClickable({ css: '#notInViewportBottom' }, 0.1).then((isClickable) => {
1066+
if (isClickable) throw new Error('Element is clickable, but must be unclickable');
1067+
}).catch((e) => {
1068+
e.message.should.include('element #notInViewportBottom still not clickable after 0.1 sec');
1069+
});
1070+
});
1071+
1072+
it('should fail for element not in viewport by left', async () => {
1073+
await wd.amOnPage('/form/wait_for_clickable');
1074+
await wd.waitForClickable({ css: '#notInViewportLeft' }, 0.1).then((isClickable) => {
1075+
if (isClickable) throw new Error('Element is clickable, but must be unclickable');
1076+
}).catch((e) => {
1077+
e.message.should.include('element #notInViewportLeft still not clickable after 0.1 sec');
1078+
});
1079+
});
1080+
1081+
it('should fail for element not in viewport by right', async () => {
1082+
await wd.amOnPage('/form/wait_for_clickable');
1083+
await wd.waitForClickable({ css: '#notInViewportRight' }, 0.1).then((isClickable) => {
1084+
if (isClickable) throw new Error('Element is clickable, but must be unclickable');
1085+
}).catch((e) => {
1086+
e.message.should.include('element #notInViewportRight still not clickable after 0.1 sec');
1087+
});
1088+
});
1089+
1090+
it('should fail for overlapping element', async () => {
1091+
await wd.amOnPage('/form/wait_for_clickable');
1092+
await wd.waitForClickable({ css: '#div2_button' }, 0.1);
1093+
await wd.waitForClickable({ css: '#div1_button' }, 0.1).then((isClickable) => {
1094+
if (isClickable) throw new Error('Element is clickable, but must be unclickable');
1095+
}).catch((e) => {
1096+
e.message.should.include('element #div1_button still not clickable after 0.1 sec');
1097+
});
1098+
});
1099+
});
1100+
10251101
describe('GeoLocation', () => {
10261102
it('should set the geoLocation', async () => {
10271103
await wd.setGeoLocation(37.4043, -122.0748);

0 commit comments

Comments
 (0)