Skip to content

Commit b42190f

Browse files
authored
feat: wdio with devtools protocol (#4105)
1 parent 2bfe7d1 commit b42190f

File tree

7 files changed

+1381
-32
lines changed

7 files changed

+1381
-32
lines changed

Diff for: .github/workflows/webdriver.devtools.yml

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: WebDriver - Devtools Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- 3.x
7+
pull_request:
8+
branches:
9+
- '**'
10+
11+
env:
12+
CI: true
13+
# Force terminal colors. @see https://www.npmjs.com/package/colors
14+
FORCE_COLOR: 1
15+
16+
jobs:
17+
build:
18+
19+
runs-on: ubuntu-20.04
20+
strategy:
21+
matrix:
22+
node-version: [20.x]
23+
24+
steps:
25+
- uses: actions/checkout@v4
26+
- name: Use Node.js ${{ matrix.node-version }}
27+
uses: actions/setup-node@v4
28+
with:
29+
node-version: ${{ matrix.node-version }}
30+
- uses: shivammathur/setup-php@v2
31+
with:
32+
php-version: 8.0
33+
- name: npm install
34+
run: |
35+
npm install --legacy-peer-deps
36+
env:
37+
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
38+
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
39+
- name: start a server
40+
run: "php -S 127.0.0.1:8000 -t test/data/app &"
41+
- name: run unit tests
42+
run: ./node_modules/.bin/mocha test/helper/WebDriver_devtools_test.js --exit
43+
- name: run tests
44+
run: "./bin/codecept.js run -c test/acceptance/codecept.WebDriver.devtools.js --grep @WebDriver --debug"
45+

Diff for: docs/helpers/WebDriver.md

+23-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Type: [object][16]
4646
- `manualStart` **[boolean][32]?** do not start browser before a test, start it manually inside a helper with `this.helpers["WebDriver"]._startBrowser()`.
4747
- `timeouts` **[object][16]?** [WebDriver timeouts][37] defined as hash.
4848
- `highlightElement` **[boolean][32]?** highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
49+
- `devtoolsProtocol` **[boolean][32]?** enable devtools protocol. Default: false. More info: [https://webdriver.io/docs/automationProtocols/#devtools-protocol][38].
4950

5051

5152

@@ -109,6 +110,25 @@ website][3].
109110
}
110111
```
111112

113+
### Running with devtools protocol
114+
115+
```js
116+
{
117+
helpers: {
118+
WebDriver : {
119+
url: "http://localhost",
120+
browser: "chrome",
121+
devtoolsProtocol: true,
122+
desiredCapabilities: {
123+
chromeOptions: {
124+
args: [ "--headless", "--disable-gpu", "--no-sandbox" ]
125+
}
126+
}
127+
}
128+
}
129+
}
130+
```
131+
112132
### Internet Explorer
113133

114134
Additional configuration params can be used from [IE options][4]
@@ -2033,7 +2053,7 @@ I.setGeoLocation(121.21, 11.56, 10);
20332053
20342054
- `latitude` **[number][22]** to set.
20352055
- `longitude` **[number][22]** to set
2036-
- `altitude` **[number][22]?** (optional, null by default) to set
2056+
- `altitude` **[number][22]?** (optional, null by default) to set
20372057
20382058
Returns **void** automatically synchronized promise through #recorder
20392059
@@ -2475,3 +2495,5 @@ Returns **void** automatically synchronized promise through #recorder
24752495
[36]: http://codecept.io/acceptance/#smartwait
24762496
24772497
[37]: http://webdriver.io/docs/timeouts.html
2498+
2499+
[38]: https://webdriver.io/docs/automationProtocols/#devtools-protocol

Diff for: lib/helper/WebDriver.js

+77-28
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const webRoot = 'body';
6363
* @prop {boolean} [manualStart=false] - do not start browser before a test, start it manually inside a helper with `this.helpers["WebDriver"]._startBrowser()`.
6464
* @prop {object} [timeouts] [WebDriver timeouts](http://webdriver.io/docs/timeouts.html) defined as hash.
6565
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
66+
* @prop {boolean} [devtoolsProtocol=false] - enable devtools protocol. Default: false. More info: https://webdriver.io/docs/automationProtocols/#devtools-protocol.
6667
*/
6768
const config = {};
6869

@@ -133,6 +134,25 @@ const config = {};
133134
* }
134135
* ```
135136
*
137+
* ### Running with devtools protocol
138+
*
139+
* ```js
140+
* {
141+
* helpers: {
142+
* WebDriver : {
143+
* url: "http://localhost",
144+
* browser: "chrome",
145+
* devtoolsProtocol: true,
146+
* desiredCapabilities: {
147+
* chromeOptions: {
148+
* args: [ "--headless", "--disable-gpu", "--no-sandbox" ]
149+
* }
150+
* }
151+
* }
152+
* }
153+
* }
154+
* ```
155+
*
136156
* ### Internet Explorer
137157
*
138158
* Additional configuration params can be used from [IE options](https://seleniumhq.github.io/selenium/docs/api/rb/Selenium/WebDriver/IE/Options.html)
@@ -542,6 +562,10 @@ class WebDriver extends Helper {
542562
delete this.options.capabilities.hostname;
543563
delete this.options.capabilities.port;
544564
delete this.options.capabilities.path;
565+
if (this.options.devtoolsProtocol) {
566+
if (!['chrome', 'chromium'].includes(this.options.browser.toLowerCase())) throw Error('The devtools protocol is only working with Chrome or Chromium');
567+
this.options.automationProtocol = 'devtools';
568+
}
545569
this.browser = await webdriverio.remote(this.options);
546570
}
547571
} catch (err) {
@@ -1043,7 +1067,8 @@ class WebDriver extends Helper {
10431067
assertElementExists(res, field, 'Field');
10441068
const elem = usingFirstElement(res);
10451069
highlightActiveElement.call(this, elem);
1046-
return elem.setValue(value.toString());
1070+
await elem.clearValue();
1071+
await elem.setValue(value.toString());
10471072
}
10481073

10491074
/**
@@ -1055,6 +1080,10 @@ class WebDriver extends Helper {
10551080
assertElementExists(res, field, 'Field');
10561081
const elem = usingFirstElement(res);
10571082
highlightActiveElement.call(this, elem);
1083+
if (this.options.automationProtocol) {
1084+
const curentValue = await elem.getValue();
1085+
return elem.setValue(curentValue + value.toString());
1086+
}
10581087
return elem.addValue(value.toString());
10591088
}
10601089

@@ -1067,6 +1096,9 @@ class WebDriver extends Helper {
10671096
assertElementExists(res, field, 'Field');
10681097
const elem = usingFirstElement(res);
10691098
highlightActiveElement.call(this, elem);
1099+
if (this.options.automationProtocol) {
1100+
return elem.setValue('');
1101+
}
10701102
return elem.clearValue(getElementId(elem));
10711103
}
10721104

@@ -1120,7 +1152,7 @@ class WebDriver extends Helper {
11201152
const el = usingFirstElement(res);
11211153

11221154
// Remote Upload (when running Selenium Server)
1123-
if (this.options.remoteFileUpload) {
1155+
if (this.options.remoteFileUpload && !this.options.automationProtocol) {
11241156
try {
11251157
this.debugSection('File', 'Uploading file to remote server');
11261158
file = await this.browser.uploadFile(file);
@@ -1498,35 +1530,33 @@ class WebDriver extends Helper {
14981530
async seeCssPropertiesOnElements(locator, cssProperties) {
14991531
const res = await this._locate(locator);
15001532
assertElementExists(res, locator);
1533+
1534+
const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
15011535
const elemAmount = res.length;
1536+
let props = [];
15021537

1503-
let props = await forEachAsync(res, async (el) => {
1504-
return forEachAsync(Object.keys(cssProperties), async (prop) => {
1505-
const propValue = await this.browser.getElementCSSValue(getElementId(el), prop);
1506-
if (isColorProperty(prop) && propValue && propValue.value) {
1507-
return convertColorToRGBA(propValue.value);
1538+
for (const element of res) {
1539+
for (const prop of Object.keys(cssProperties)) {
1540+
const cssProp = await this.grabCssPropertyFrom(locator, prop);
1541+
if (isColorProperty(prop)) {
1542+
props.push(convertColorToRGBA(cssProp));
1543+
} else {
1544+
props.push(cssProp);
15081545
}
1509-
return propValue;
1510-
});
1511-
});
1512-
1513-
const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
1546+
}
1547+
}
15141548

15151549
const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
15161550
if (!Array.isArray(props)) props = [props];
15171551
let chunked = chunkArray(props, values.length);
15181552
chunked = chunked.filter((val) => {
15191553
for (let i = 0; i < val.length; ++i) {
1520-
const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
1521-
const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
1522-
if (_acutal !== _expected) return false;
1554+
// eslint-disable-next-line eqeqeq
1555+
if (val[i] != values[i]) return false;
15231556
}
15241557
return true;
15251558
});
1526-
return assert.ok(
1527-
chunked.length === elemAmount,
1528-
`expected all elements (${(new Locator(locator))}) to have CSS property ${JSON.stringify(cssProperties)}`,
1529-
);
1559+
return equals(`all elements (${(new Locator(locator))}) to have CSS property ${JSON.stringify(cssProperties)}`).assert(chunked.length, elemAmount);
15301560
}
15311561

15321562
/**
@@ -1546,9 +1576,9 @@ class WebDriver extends Helper {
15461576
let chunked = chunkArray(attrs, values.length);
15471577
chunked = chunked.filter((val) => {
15481578
for (let i = 0; i < val.length; ++i) {
1549-
const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
1579+
const _actual = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
15501580
const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
1551-
if (_acutal !== _expected) return false;
1581+
if (_actual !== _expected) return false;
15521582
}
15531583
return true;
15541584
});
@@ -1925,7 +1955,7 @@ class WebDriver extends Helper {
19251955
* {{> resizeWindow }}
19261956
*/
19271957
async resizeWindow(width, height) {
1928-
return this._resizeBrowserWindow(this.browser, width, height);
1958+
return this.browser.setWindowSize(width, height);
19291959
}
19301960

19311961
async _resizeBrowserWindow(browser, width, height) {
@@ -2312,6 +2342,9 @@ class WebDriver extends Helper {
23122342
async switchTo(locator) {
23132343
this.browser.isInsideFrame = true;
23142344
if (Number.isInteger(locator)) {
2345+
if (this.options.automationProtocol) {
2346+
return this.browser.switchToFrame(locator + 1);
2347+
}
23152348
return this.browser.switchToFrame(locator);
23162349
}
23172350
if (!locator) {
@@ -2453,9 +2486,19 @@ class WebDriver extends Helper {
24532486
*
24542487
* {{> setGeoLocation }}
24552488
*/
2456-
async setGeoLocation(latitude, longitude, altitude = null) {
2457-
console.log(`setGeoLocation deprecated:
2458-
* This command is deprecated due to using deprecated JSON Wire Protocol command. More info: https://webdriver.io/docs/api/jsonwp/#setgeolocation`);
2489+
async setGeoLocation(latitude, longitude) {
2490+
if (!this.options.automationProtocol) {
2491+
console.log(`setGeoLocation deprecated:
2492+
* This command is deprecated due to using deprecated JSON Wire Protocol command. More info: https://webdriver.io/docs/api/jsonwp/#setgeolocation
2493+
* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration`);
2494+
return;
2495+
}
2496+
this.geoLocation = { latitude, longitude };
2497+
const puppeteerBrowser = await this.browser.getPuppeteer();
2498+
await this.browser.call(async () => {
2499+
const pages = await puppeteerBrowser.pages();
2500+
await pages[0].setGeolocation({ latitude, longitude });
2501+
});
24592502
}
24602503

24612504
/**
@@ -2465,8 +2508,14 @@ class WebDriver extends Helper {
24652508
*
24662509
*/
24672510
async grabGeoLocation() {
2468-
console.log(`grabGeoLocation deprecated:
2469-
* This command is deprecated due to using deprecated JSON Wire Protocol command. More info: https://webdriver.io/docs/api/jsonwp/#getgeolocation`);
2511+
if (!this.options.automationProtocol) {
2512+
console.log(`grabGeoLocation deprecated:
2513+
* This command is deprecated due to using deprecated JSON Wire Protocol command. More info: https://webdriver.io/docs/api/jsonwp/#getgeolocation
2514+
* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration`);
2515+
return;
2516+
}
2517+
if (!this.geoLocation) return 'No GeoLocation is set!';
2518+
return this.geoLocation;
24702519
}
24712520

24722521
/**
@@ -2662,7 +2711,7 @@ async function proceedSeeField(assertType, field, value) {
26622711
}
26632712
};
26642713

2665-
const proceedSingle = el => this.browser.getElementAttribute(getElementId(el), 'value').then((res) => {
2714+
const proceedSingle = el => el.getValue().then((res) => {
26662715
if (res === null) {
26672716
throw new Error(`Element ${el.selector} has no value attribute`);
26682717
}

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"test:unit:webbapi:playwright": "mocha test/helper/Playwright_test.js",
5353
"test:unit:webbapi:puppeteer": "mocha test/helper/Puppeteer_test.js",
5454
"test:unit:webbapi:webDriver": "mocha test/helper/WebDriver_test.js",
55+
"test:unit:webbapi:webDriver:devtools": "mocha test/helper/WebDriver_devtools_test.js --exit",
5556
"test:unit:webbapi:testCafe": "mocha test/helper/TestCafe_test.js",
5657
"test:unit:expect": "mocha test/helper/Expect_test.js",
5758
"test:plugin": "mocha test/plugin/plugin_test.js",

Diff for: test/acceptance/codecept.WebDriver.devtools.js

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const TestHelper = require('../support/TestHelper');
2+
3+
module.exports.config = {
4+
tests: './*_test.js',
5+
timeout: 10000,
6+
output: './output',
7+
helpers: {
8+
WebDriver: {
9+
url: TestHelper.siteUrl(),
10+
browser: 'Chromium',
11+
windowSize: '500x700',
12+
devtoolsProtocol: true,
13+
waitForTimeout: 5000,
14+
capabilities: {
15+
chromeOptions: {
16+
args: ['--headless', '--disable-gpu', '--window-size=500,700'],
17+
},
18+
},
19+
},
20+
ScreenshotSessionHelper: {
21+
require: '../support/ScreenshotSessionHelper.js',
22+
outputPath: './output',
23+
},
24+
Expect: {},
25+
},
26+
include: {},
27+
bootstrap: async () => new Promise(done => {
28+
setTimeout(done, 5000);
29+
}), // let's wait for selenium
30+
mocha: {},
31+
name: 'acceptance',
32+
plugins: {
33+
screenshotOnFail: {
34+
enabled: true,
35+
},
36+
},
37+
gherkin: {
38+
features: './gherkin/*.feature',
39+
steps: ['./gherkin/steps.js'],
40+
},
41+
};

0 commit comments

Comments
 (0)