Skip to content

Commit 1aff923

Browse files
authored
feat: introduce playwright locator (#4255)
1 parent 74a73e0 commit 1aff923

File tree

10 files changed

+118
-9
lines changed

10 files changed

+118
-9
lines changed

docs/helpers/Playwright.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ Type: [object][6]
8080
- `bypassCSP` **[boolean][26]?** bypass Content Security Policy or CSP
8181
- `highlightElement` **[boolean][26]?** highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
8282
- `recordHar` **[object][6]?** record HAR and will be saved to `output/har`. See more of [HAR options][3].
83+
- `testIdAttribute` **[string][9]?** locate elements based on the testIdAttribute. See more of [locate by test id][49].
8384

8485

8586

@@ -2775,3 +2776,5 @@ Returns **void** automatically synchronized promise through #recorder
27752776
[47]: https://playwright.dev/docs/browsers/#google-chrome--microsoft-edge
27762777

27772778
[48]: https://playwright.dev/docs/api/class-consolemessage#console-message-type
2779+
2780+
[49]: https://playwright.dev/docs/locators#locate-by-test-id

docs/locators.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ CodeceptJS provides flexible strategies for locating elements:
1414
* [Custom Locator Strategies](#custom-locators): by data attributes or whatever you prefer.
1515
* [Shadow DOM](/shadow): to access shadow dom elements
1616
* [React](/react): to access React elements by component names and props
17+
* Playwright: to access locator supported by Playwright, namely [_react](https://playwright.dev/docs/other-locators#react-locator), [_vue](https://playwright.dev/docs/other-locators#vue-locator), [data-testid](https://playwright.dev/docs/locators#locate-by-test-id)
1718

1819
Most methods in CodeceptJS use locators which can be either a string or an object.
1920

20-
If the locator is an object, it should have a single element, with the key signifying the locator type (`id`, `name`, `css`, `xpath`, `link`, `react`, `class` or `shadow`) and the value being the locator itself. This is called a "strict" locator.
21+
If the locator is an object, it should have a single element, with the key signifying the locator type (`id`, `name`, `css`, `xpath`, `link`, `react`, `class`, `shadow` or `pw`) and the value being the locator itself. This is called a "strict" locator.
2122

2223
Examples:
2324

@@ -26,6 +27,7 @@ Examples:
2627
* {css: 'input[type=input][value=foo]'} matches `<input type="input" value="foo">`
2728
* {xpath: "//input[@type='submit'][contains(@value, 'foo')]"} matches `<input type="submit" value="foobar">`
2829
* {class: 'foo'} matches `<div class="foo">`
30+
* { pw: '_react=t[name = "="]' }
2931

3032
Writing good locators can be tricky.
3133
The Mozilla team has written an excellent guide titled [Writing reliable locators for Selenium and WebDriver tests](https://blog.mozilla.org/webqa/2013/09/26/writing-reliable-locators-for-selenium-and-webdriver-tests/).

lib/helper/Playwright.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const ElementNotFound = require('./errors/ElementNotFound');
3333
const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnectionRefused');
3434
const Popup = require('./extras/Popup');
3535
const Console = require('./extras/Console');
36-
const { findReact, findVue } = require('./extras/PlaywrightReactVueLocator');
36+
const { findReact, findVue, findByPlaywrightLocator } = require('./extras/PlaywrightReactVueLocator');
3737

3838
let playwright;
3939
let perfTiming;
@@ -100,6 +100,7 @@ const pathSeparator = path.sep;
100100
* @prop {boolean} [bypassCSP] - bypass Content Security Policy or CSP
101101
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
102102
* @prop {object} [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har).
103+
* @prop {string} [testIdAttribute=data-testid] - locate elements based on the testIdAttribute. See more of [locate by test id](https://playwright.dev/docs/locators#locate-by-test-id).
103104
*/
104105
const config = {};
105106

@@ -379,6 +380,7 @@ class Playwright extends Helper {
379380
highlightElement: false,
380381
};
381382

383+
process.env.testIdAttribute = 'data-testid';
382384
config = Object.assign(defaults, config);
383385

384386
if (availableBrowsers.indexOf(config.browser) < 0) {
@@ -464,6 +466,7 @@ class Playwright extends Helper {
464466
try {
465467
await playwright.selectors.register('__value', createValueEngine);
466468
await playwright.selectors.register('__disabled', createDisabledEngine);
469+
if (process.env.testIdAttribute) await playwright.selectors.setTestIdAttribute(process.env.testIdAttribute);
467470
} catch (e) {
468471
console.warn(e);
469472
}
@@ -3455,13 +3458,16 @@ function buildLocatorString(locator) {
34553458
async function findElements(matcher, locator) {
34563459
if (locator.react) return findReact(matcher, locator);
34573460
if (locator.vue) return findVue(matcher, locator);
3461+
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator);
34583462
locator = new Locator(locator, 'css');
34593463

34603464
return matcher.locator(buildLocatorString(locator)).all();
34613465
}
34623466

34633467
async function findElement(matcher, locator) {
34643468
if (locator.react) return findReact(matcher, locator);
3469+
if (locator.vue) return findVue(matcher, locator);
3470+
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator);
34653471
locator = new Locator(locator, 'css');
34663472

34673473
return matcher.locator(buildLocatorString(locator)).first();
@@ -3517,6 +3523,7 @@ async function proceedClick(locator, context = null, options = {}) {
35173523
async function findClickable(matcher, locator) {
35183524
if (locator.react) return findReact(matcher, locator);
35193525
if (locator.vue) return findVue(matcher, locator);
3526+
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator);
35203527

35213528
locator = new Locator(locator);
35223529
if (!locator.isFuzzy()) return findElements.call(this, matcher, locator);

lib/helper/extras/PlaywrightReactVueLocator.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ async function findVue(matcher, locator) {
2020
return matcher.locator(_locator).all();
2121
}
2222

23+
async function findByPlaywrightLocator(matcher, locator) {
24+
if (locator && locator.toString().includes(process.env.testIdAttribute)) return matcher.getByTestId(locator.pw.value.split('=')[1]);
25+
return matcher.locator(locator.pw).all();
26+
}
27+
2328
function propBuilder(props) {
2429
let _props = '';
2530

@@ -35,4 +40,4 @@ function propBuilder(props) {
3540
return _props;
3641
}
3742

38-
module.exports = { findReact, findVue };
43+
module.exports = { findReact, findVue, findByPlaywrightLocator };

lib/locator.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const { sprintf } = require('sprintf-js');
33

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

6-
const locatorTypes = ['css', 'by', 'xpath', 'id', 'name', 'fuzzy', 'frame', 'shadow'];
6+
const locatorTypes = ['css', 'by', 'xpath', 'id', 'name', 'fuzzy', 'frame', 'shadow', 'pw'];
77
/** @class */
88
class Locator {
99
/**
@@ -51,6 +51,9 @@ class Locator {
5151
if (isShadow(locator)) {
5252
this.type = 'shadow';
5353
}
54+
if (isPlaywrightLocator(locator)) {
55+
this.type = 'pw';
56+
}
5457

5558
Locator.filters.forEach(f => f(locator, this));
5659
}
@@ -71,6 +74,8 @@ class Locator {
7174
return this.value;
7275
case 'shadow':
7376
return { shadow: this.value };
77+
case 'pw':
78+
return { pw: this.value };
7479
}
7580
return this.value;
7681
}
@@ -115,6 +120,13 @@ class Locator {
115120
return this.type === 'css';
116121
}
117122

123+
/**
124+
* @returns {boolean}
125+
*/
126+
isPlaywrightLocator() {
127+
return this.type === 'pw';
128+
}
129+
118130
/**
119131
* @returns {boolean}
120132
*/
@@ -522,6 +534,16 @@ function removePrefix(xpath) {
522534
.replace(/^(\.|\/)+/, '');
523535
}
524536

537+
/**
538+
* @private
539+
* check if the locator is a Playwright locator
540+
* @param {string} locator
541+
* @returns {boolean}
542+
*/
543+
function isPlaywrightLocator(locator) {
544+
return locator.includes('_react') || locator.includes('_vue') || locator.includes('data-testid');
545+
}
546+
525547
/**
526548
* @private
527549
* @param {CodeceptJS.LocatorOrString} locator

test/acceptance/react_test.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
const { I } = inject();
2+
13
Feature('React Selectors');
24

3-
Scenario('props @Puppeteer @Playwright', ({ I }) => {
5+
Scenario('props @Puppeteer @Playwright', () => {
46
I.amOnPage('https://codecept.io/test-react-calculator/');
57
I.click('7');
68
I.click({ react: 't', props: { name: '=' } });
@@ -11,10 +13,21 @@ Scenario('props @Puppeteer @Playwright', ({ I }) => {
1113
I.seeElement({ react: 't', props: { value: '10' } });
1214
});
1315

14-
Scenario('component name @Puppeteer @Playwright', ({ I }) => {
16+
Scenario('component name @Puppeteer @Playwright', () => {
1517
I.amOnPage('http://negomi.github.io/react-burger-menu/');
1618
I.click({ react: 'BurgerIcon' });
1719
I.waitForVisible('#slide', 10);
1820
I.click('Alerts');
1921
I.seeElement({ react: 'Demo' });
2022
});
23+
24+
Scenario('using playwright locator @Playwright', () => {
25+
I.amOnPage('https://codecept.io/test-react-calculator/');
26+
I.click('7');
27+
I.click({ pw: '_react=t[name = "="]' });
28+
I.seeElement({ pw: '_react=t[value = "7"]' });
29+
I.click({ pw: '_react=t[name = "+"]' });
30+
I.click({ pw: '_react=t[name = "3"]' });
31+
I.click({ pw: '_react=t[name = "="]' });
32+
I.seeElement({ pw: '_react=t[value = "10"]' });
33+
});

test/data/app/view/index.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<title>TestEd Beta 2.0</title>
33
<body>
44

5-
<h1>Welcome to test app!</h1>
5+
<h1 data-testid="welcome">Welcome to test app!</h1>
66
<h2>With&nbsp;special&nbsp;space chars</h1>
77

88
<div class="notice" qa-id = "test"><?php if (isset($notice)) echo $notice; ?></div>

test/helper/Playwright_test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1678,3 +1678,39 @@ describe('Playwright - HAR', () => {
16781678
});
16791679
});
16801680
});
1681+
1682+
describe('using data-testid attribute', () => {
1683+
before(() => {
1684+
global.codecept_dir = path.join(__dirname, '/../data');
1685+
global.output_dir = path.join(`${__dirname}/../data/output`);
1686+
1687+
I = new Playwright({
1688+
url: siteUrl,
1689+
windowSize: '500x700',
1690+
show: false,
1691+
restart: true,
1692+
browser: 'chromium',
1693+
});
1694+
I._init();
1695+
return I._beforeSuite();
1696+
});
1697+
1698+
beforeEach(async () => {
1699+
return I._before().then(() => {
1700+
page = I.page;
1701+
browser = I.browser;
1702+
});
1703+
});
1704+
1705+
afterEach(async () => {
1706+
return I._after();
1707+
});
1708+
1709+
it('should find element by data-testid attribute', async () => {
1710+
await I.amOnPage('/');
1711+
1712+
const webElements = await I.grabWebElements({ pw: '[data-testid="welcome"]' });
1713+
assert.equal(webElements[0]._selector, '[data-testid="welcome"] >> nth=0');
1714+
assert.equal(webElements.length, 1);
1715+
});
1716+
});

test/unit/locator_test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,27 @@ describe('Locator', () => {
243243
expect(l.value).to.equal('foo');
244244
expect(l.toString()).to.equal('foo');
245245
});
246+
247+
it('should create playwright locator - _react', () => {
248+
const l = new Locator({ pw: '_react=button' });
249+
expect(l.type).to.equal('pw');
250+
expect(l.value).to.equal('_react=button');
251+
expect(l.toString()).to.equal('{pw: _react=button}');
252+
});
253+
254+
it('should create playwright locator - _vue', () => {
255+
const l = new Locator({ pw: '_vue=button' });
256+
expect(l.type).to.equal('pw');
257+
expect(l.value).to.equal('_vue=button');
258+
expect(l.toString()).to.equal('{pw: _vue=button}');
259+
});
260+
261+
it('should create playwright locator - data-testid', () => {
262+
const l = new Locator({ pw: '[data-testid="directions"]' });
263+
expect(l.type).to.equal('pw');
264+
expect(l.value).to.equal('[data-testid="directions"]');
265+
expect(l.toString()).to.equal('{pw: [data-testid="directions"]}');
266+
});
246267
});
247268

248269
describe('with object argument', () => {

typings/index.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -442,8 +442,8 @@ declare namespace CodeceptJS {
442442
| { react: string }
443443
| { vue: string }
444444
| { shadow: string[] }
445-
| { custom: string };
446-
445+
| { custom: string }
446+
| { pw: string };
447447
interface CustomLocators {}
448448
interface OtherLocators { props?: object }
449449
type LocatorOrString =

0 commit comments

Comments
 (0)