Skip to content

Commit 0eb96b0

Browse files
GeorgegriffGeorge Griffiths
andauthored
Custom locators playwright 0.12.1 (#2277)
* add support for custom locators in findElements * update most wait fors to use custom selectors * fix wait for text * update playwright version * add some missing test cases * fix selector registration * fix authentication test * fix frame logic * add show back * remove redundant tests * handle custom selectors better * fix show default * add custom check * remove show * fix wait for * deal with playwright breaking changes Co-authored-by: George Griffiths <george.griffiths@ibm.com>
1 parent 5342300 commit 0eb96b0

6 files changed

Lines changed: 84 additions & 63 deletions

File tree

docs/helpers/Playwright.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ Uses [Playwright][1] library to run tests inside:
1919

2020
This helper works with a browser out of the box with no additional tools required to install.
2121

22-
Requires `playwright` package version ^0.11.0 to be installed:
22+
Requires `playwright` package version ^0.12.1 to be installed:
2323

24-
npm i playwright@^0.11.0 --save
24+
npm i playwright@^0.12.1 --save
2525

2626
## Configuration
2727

docs/playwright.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ It's readable and simple and working using Playwright API!
2828
To start you need CodeceptJS with Playwright packages installed
2929

3030
```bash
31-
npm install codeceptjs playwright@^0.11.1 --save
31+
npm install codeceptjs playwright@^0.12.1 --save
3232
```
3333

3434
Or see [alternative installation options](http://codecept.io/installation/)
@@ -204,7 +204,7 @@ CodeceptJS allows you to implement custom actions like `I.createTodo` or use **P
204204

205205
## Extending
206206

207-
Playwright has a very [rich and flexible API](https://github.com/microsoft/playwright/blob/v0.10.0/docs/api.md). Sure, you can extend your test suites to use the methods listed there. CodeceptJS already prepares some objects for you and you can use them from your you helpers.
207+
Playwright has a very [rich and flexible API](https://github.com/microsoft/playwright/blob/v0.12.1/docs/api.md). Sure, you can extend your test suites to use the methods listed there. CodeceptJS already prepares some objects for you and you can use them from your you helpers.
208208

209209
Start with creating an `MyPlaywright` helper using `generate:helper` or `gh` command:
210210

@@ -240,7 +240,7 @@ async setPermissions() {
240240
return context.setPermissions('https://html5demos.com', ['geolocation']);
241241
}
242242

243-
> [▶ Learn more about BrowserContext](https://github.com/microsoft/playwright/blob/v0.10.0/docs/api.md#class-browsercontext)
243+
> [▶ Learn more about BrowserContext](https://github.com/microsoft/playwright/blob/v0.12.1/docs/api.md#class-browsercontext)
244244

245245
> [▶ Learn more about Helpers](http://codecept.io/helpers/)
246246

lib/helper/Playwright.js

Lines changed: 48 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const popupStore = new Popup();
3737
const consoleLogStore = new Console();
3838
const availableBrowsers = ['chromium', 'webkit', 'firefox'];
3939

40+
const { createValueEngine } = require('./extras/PlaywrightPropEngine');
4041
/**
4142
* Uses [Playwright](https://github.com/microsoft/playwright) library to run tests inside:
4243
*
@@ -46,10 +47,10 @@ const availableBrowsers = ['chromium', 'webkit', 'firefox'];
4647
*
4748
* This helper works with a browser out of the box with no additional tools required to install.
4849
*
49-
* Requires `playwright` package version ^0.11.0 to be installed:
50+
* Requires `playwright` package version ^0.12.1 to be installed:
5051
*
5152
* ```
52-
* npm i playwright@^0.11.0 --save
53+
* npm i playwright@^0.12.1 --save
5354
* ```
5455
*
5556
* ## Configuration
@@ -250,11 +251,17 @@ class Playwright extends Helper {
250251
try {
251252
requireg('playwright');
252253
} catch (e) {
253-
return ['playwright@^0.10'];
254+
return ['playwright@^0.12.1'];
254255
}
255256
}
256257

257-
_init() {
258+
async _init() {
259+
// register an internal selector engine for reading value property of elements in a selector
260+
try {
261+
await playwright.selectors.register('__value', createValueEngine);
262+
} catch (e) {
263+
console.warn(e);
264+
}
258265
}
259266

260267
_beforeSuite() {
@@ -566,7 +573,7 @@ class Playwright extends Helper {
566573

567574
if (this.config.basicAuth && (this.isAuthenticated !== true)) {
568575
if (url.includes(this.options.url)) {
569-
await this.page.authenticate(this.config.basicAuth);
576+
await this.browserContext.setHTTPCredentials(this.config.basicAuth);
570577
this.isAuthenticated = true;
571578
}
572579
}
@@ -1529,7 +1536,7 @@ class Playwright extends Helper {
15291536
const array = [];
15301537

15311538
for (let index = 0; index < els.length; index++) {
1532-
const a = await this._evaluateHandeInContext((el, attr) => el[attr] || el.getAttribute(attr), els[index], attr);
1539+
const a = await this._evaluateHandeInContext(([el, attr]) => el[attr] || el.getAttribute(attr), [els[index], attr]);
15331540
array.push(await a.jsonValue());
15341541
}
15351542

@@ -1573,21 +1580,15 @@ class Playwright extends Helper {
15731580
const matcher = await this.context;
15741581
let waiter;
15751582
const context = await this._getContext();
1576-
if (locator.isCSS()) {
1577-
const enabledFn = function (locator) {
1578-
const els = document.querySelectorAll(locator);
1579-
if (!els || els.length === 0) {
1580-
return false;
1581-
}
1582-
return Array.prototype.filter.call(els, el => !el.disabled).length > 0;
1583-
};
1584-
waiter = context.waitForFunction(enabledFn, { timeout: waitTimeout }, locator.value);
1583+
if (!locator.isXPath()) {
1584+
// playwright combined selectors
1585+
waiter = context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> :not([disabled])`, { timeout: waitTimeout });
15851586
} else {
1586-
const enabledFn = function (locator, $XPath) {
1587+
const enabledFn = function ([locator, $XPath]) {
15871588
eval($XPath); // eslint-disable-line no-eval
15881589
return $XPath(null, locator).filter(el => !el.disabled).length > 0;
15891590
};
1590-
waiter = context.waitForFunction(enabledFn, { timeout: waitTimeout }, locator.value, $XPath.toString());
1591+
waiter = context.waitForFunction(enabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout });
15911592
}
15921593
return waiter.catch((err) => {
15931594
throw new Error(`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`);
@@ -1603,21 +1604,15 @@ class Playwright extends Helper {
16031604
const matcher = await this.context;
16041605
let waiter;
16051606
const context = await this._getContext();
1606-
if (locator.isCSS()) {
1607-
const valueFn = function (locator, value) {
1608-
const els = document.querySelectorAll(locator);
1609-
if (!els || els.length === 0) {
1610-
return false;
1611-
}
1612-
return Array.prototype.filter.call(els, el => (el.value || '').indexOf(value) !== -1).length > 0;
1613-
};
1614-
waiter = context.waitForFunction(valueFn, { timeout: waitTimeout }, locator.value, value);
1607+
if (!locator.isXPath()) {
1608+
// uses a custom selector engine for finding value properties on elements
1609+
waiter = context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> __value=${value}`, { timeout: waitTimeout, waitFor: 'visible' });
16151610
} else {
1616-
const valueFn = function (locator, $XPath, value) {
1611+
const valueFn = function ([locator, $XPath, value]) {
16171612
eval($XPath); // eslint-disable-line no-eval
16181613
return $XPath(null, locator).filter(el => (el.value || '').indexOf(value) !== -1).length > 0;
16191614
};
1620-
waiter = context.waitForFunction(valueFn, { timeout: waitTimeout }, locator.value, $XPath.toString(), value);
1615+
waiter = context.waitForFunction(valueFn, [locator.value, $XPath.toString(), value], { timeout: waitTimeout });
16211616
}
16221617
return waiter.catch((err) => {
16231618
const loc = locator.toString();
@@ -1636,20 +1631,20 @@ class Playwright extends Helper {
16361631
let waiter;
16371632
const context = await this._getContext();
16381633
if (locator.isCSS()) {
1639-
const visibleFn = function (locator, num) {
1634+
const visibleFn = function ([locator, num]) {
16401635
const els = document.querySelectorAll(locator);
16411636
if (!els || els.length === 0) {
16421637
return false;
16431638
}
16441639
return Array.prototype.filter.call(els, el => el.offsetParent !== null).length === num;
16451640
};
1646-
waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, num);
1641+
waiter = context.waitForFunction(visibleFn, [locator.value, num], { timeout: waitTimeout });
16471642
} else {
1648-
const visibleFn = function (locator, $XPath, num) {
1643+
const visibleFn = function ([locator, $XPath, num]) {
16491644
eval($XPath); // eslint-disable-line no-eval
16501645
return $XPath(null, locator).filter(el => el.offsetParent !== null).length === num;
16511646
};
1652-
waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString(), num);
1647+
waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString(), num], { timeout: waitTimeout });
16531648
}
16541649
return waiter.catch((err) => {
16551650
throw new Error(`The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`);
@@ -1681,7 +1676,7 @@ class Playwright extends Helper {
16811676
locator = new Locator(locator, 'css');
16821677

16831678
const context = await this._getContext();
1684-
const waiter = context.waitForSelector(`${locator.type}=${locator.value}`, { timeout: waitTimeout });
1679+
const waiter = context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()}`, { timeout: waitTimeout });
16851680
return waiter.catch((err) => {
16861681
throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${err.message}`);
16871682
});
@@ -1696,7 +1691,7 @@ class Playwright extends Helper {
16961691
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
16971692
locator = new Locator(locator, 'css');
16981693
const context = await this._getContext();
1699-
const waiter = context.waitForSelector(`${locator.type}=${locator.value}`, { timeout: waitTimeout, visibility: 'visible' });
1694+
const waiter = context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()}`, { timeout: waitTimeout, waitFor: 'visible' });
17001695
return waiter.catch((err) => {
17011696
throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec\n${err.message}`);
17021697
});
@@ -1709,7 +1704,7 @@ class Playwright extends Helper {
17091704
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
17101705
locator = new Locator(locator, 'css');
17111706
const context = await this._getContext();
1712-
const waiter = context.waitForSelector(`${locator.type}=${locator.value}`, { timeout: waitTimeout, visibility: 'hidden' });
1707+
const waiter = context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()}`, { timeout: waitTimeout, waitFor: 'hidden' });
17131708
return waiter.catch((err) => {
17141709
throw new Error(`element (${locator.toString()}) still visible after ${waitTimeout / 1000} sec\n${err.message}`);
17151710
});
@@ -1722,7 +1717,7 @@ class Playwright extends Helper {
17221717
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
17231718
locator = new Locator(locator, 'css');
17241719
const context = await this._getContext();
1725-
return context.waitForSelector(`${locator.type}=${locator.value}`, { timeout: waitTimeout, visibility: 'hidden' }).catch((err) => {
1720+
return context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()}`, { timeout: waitTimeout, waitFor: 'hidden' }).catch((err) => {
17261721
throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`);
17271722
});
17281723
}
@@ -1743,7 +1738,7 @@ class Playwright extends Helper {
17431738
return this.page.waitForFunction((urlPart) => {
17441739
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)));
17451740
return currUrl.indexOf(urlPart) > -1;
1746-
}, { timeout: waitTimeout }, urlPart).catch(async (e) => {
1741+
}, urlPart, { timeout: waitTimeout }).catch(async (e) => {
17471742
const currUrl = await this._getPageUrl(); // Required because the waitForFunction can't return data.
17481743
if (/failed: timeout/i.test(e.message)) {
17491744
throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`);
@@ -1767,7 +1762,7 @@ class Playwright extends Helper {
17671762
return this.page.waitForFunction((urlPart) => {
17681763
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)));
17691764
return currUrl.indexOf(urlPart) > -1;
1770-
}, { timeout: waitTimeout }, urlPart).catch(async (e) => {
1765+
}, urlPart, { timeout: waitTimeout }).catch(async (e) => {
17711766
const currUrl = await this._getPageUrl(); // Required because the waitForFunction can't return data.
17721767
if (/failed: timeout/i.test(e.message)) {
17731768
throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`);
@@ -1788,26 +1783,21 @@ class Playwright extends Helper {
17881783

17891784
if (context) {
17901785
const locator = new Locator(context, 'css');
1791-
if (locator.isCSS()) {
1792-
waiter = contextObject.waitForFunction((locator, text) => {
1793-
const el = document.querySelector(locator);
1794-
if (!el) return false;
1795-
return el.innerText.indexOf(text) > -1;
1796-
}, { timeout: waitTimeout }, locator.value, text);
1786+
if (!locator.isXPath()) {
1787+
waiter = contextObject.waitFor(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> text=${text}`, { timeout: waitTimeout, waitFor: 'visible' });
17971788
}
17981789

17991790
if (locator.isXPath()) {
1800-
waiter = contextObject.waitForFunction((locator, text, $XPath) => {
1791+
waiter = contextObject.waitForFunction(([locator, text, $XPath]) => {
18011792
eval($XPath); // eslint-disable-line no-eval
18021793
const el = $XPath(null, locator);
18031794
if (!el.length) return false;
18041795
return el[0].innerText.indexOf(text) > -1;
1805-
}, { timeout: waitTimeout }, locator.value, text, $XPath.toString());
1796+
}, [locator.value, text, $XPath.toString()], { timeout: waitTimeout });
18061797
}
18071798
} else {
1808-
waiter = contextObject.waitForFunction(text => document.body && document.body.innerText.indexOf(text) > -1, { timeout: waitTimeout }, text);
1799+
waiter = contextObject.waitForFunction(text => document.body && document.body.innerText.indexOf(text) > -1, text, { timeout: waitTimeout });
18091800
}
1810-
18111801
return waiter.catch((err) => {
18121802
throw new Error(`Text "${text}" was not found on page after ${waitTimeout / 1000} sec\n${err.message}`);
18131803
});
@@ -1897,7 +1887,7 @@ class Playwright extends Helper {
18971887
}
18981888
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
18991889
const context = await this._getContext();
1900-
return context.waitForFunction(fn, { timeout: waitTimeout }, ...args);
1890+
return context.waitForFunction(fn, args, { timeout: waitTimeout });
19011891
}
19021892

19031893
/**
@@ -1942,17 +1932,14 @@ class Playwright extends Helper {
19421932

19431933
let waiter;
19441934
const context = await this._getContext();
1945-
if (locator.isCSS()) {
1946-
const visibleFn = function (locator) {
1947-
return document.querySelector(locator) === null;
1948-
};
1949-
waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value);
1935+
if (!locator.isXPath()) {
1936+
waiter = context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()}`, { timeout: waitTimeout, waitFor: 'detached' });
19501937
} else {
1951-
const visibleFn = function (locator, $XPath) {
1938+
const visibleFn = function ([locator, $XPath]) {
19521939
eval($XPath); // eslint-disable-line no-eval
19531940
return $XPath(null, locator).length === 0;
19541941
};
1955-
waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString());
1942+
waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString()], { timeout: waitTimeout });
19561943
}
19571944
return waiter.catch((err) => {
19581945
throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`);
@@ -1986,7 +1973,11 @@ module.exports = Playwright;
19861973

19871974
async function findElements(matcher, locator) {
19881975
locator = new Locator(locator, 'css');
1989-
if (!locator.isXPath()) return matcher.$$(locator.simplify());
1976+
if (locator.isCustom()) {
1977+
return matcher.$$(`${locator.type}=${locator.value}`);
1978+
} if (!locator.isXPath()) {
1979+
return matcher.$$(locator.simplify());
1980+
}
19901981
return matcher.$$(`xpath=${locator.value}`);
19911982
}
19921983

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module.exports.createValueEngine = () => {
2+
return {
3+
// Creates a selector that matches given target when queried at the root.
4+
// Can return undefined if unable to create one.
5+
create(root, target) {
6+
return null;
7+
},
8+
9+
// Returns the first element matching given selector in the root's subtree.
10+
query(root, selector) {
11+
if (!root) {
12+
return null;
13+
}
14+
return `${root.value}` === selector;
15+
},
16+
17+
// Returns all elements matching given selector in the root's subtree.
18+
queryAll(root, selector) {
19+
if (!root) {
20+
return null;
21+
}
22+
return `${root.value}` === selector;
23+
},
24+
};
25+
};

lib/locator.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const sprintf = require('sprintf-js').sprintf;
33

44
const xpathLocator = require('./utils').xpathLocator;
55

6+
const locatorTypes = ['css', 'by', 'xpath', 'id', 'name', 'fuzzy', 'frame'];
67
/** @class */
78
class Locator {
89
/**
@@ -96,6 +97,10 @@ class Locator {
9697
return this.type === 'xpath';
9798
}
9899

100+
isCustom() {
101+
return this.type && !locatorTypes.includes(this.type);
102+
}
103+
99104
isStrict() {
100105
return this.strict;
101106
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@
107107
"mocha-parallel-tests": "^2.2.2",
108108
"nightmare": "^3.0.2",
109109
"nodemon": "^1.19.4",
110-
"playwright": "^0.11.0",
110+
"playwright": "^0.12.1",
111111
"protractor": "^5.4.1",
112112
"puppeteer": "^1.20.0",
113113
"qrcode-terminal": "^0.12.0",

0 commit comments

Comments
 (0)