Skip to content

Commit

Permalink
feat: introduce playwright locator (#4255)
Browse files Browse the repository at this point in the history
  • Loading branch information
kobenguyent authored Mar 19, 2024
1 parent 74a73e0 commit 1aff923
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 9 deletions.
3 changes: 3 additions & 0 deletions docs/helpers/Playwright.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Type: [object][6]
- `bypassCSP` **[boolean][26]?** bypass Content Security Policy or CSP
- `highlightElement` **[boolean][26]?** highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
- `recordHar` **[object][6]?** record HAR and will be saved to `output/har`. See more of [HAR options][3].
- `testIdAttribute` **[string][9]?** locate elements based on the testIdAttribute. See more of [locate by test id][49].



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

[48]: https://playwright.dev/docs/api/class-consolemessage#console-message-type

[49]: https://playwright.dev/docs/locators#locate-by-test-id
4 changes: 3 additions & 1 deletion docs/locators.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ CodeceptJS provides flexible strategies for locating elements:
* [Custom Locator Strategies](#custom-locators): by data attributes or whatever you prefer.
* [Shadow DOM](/shadow): to access shadow dom elements
* [React](/react): to access React elements by component names and props
* 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)

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

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.
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.

Examples:

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

Writing good locators can be tricky.
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/).
Expand Down
9 changes: 8 additions & 1 deletion lib/helper/Playwright.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const ElementNotFound = require('./errors/ElementNotFound');
const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnectionRefused');
const Popup = require('./extras/Popup');
const Console = require('./extras/Console');
const { findReact, findVue } = require('./extras/PlaywrightReactVueLocator');
const { findReact, findVue, findByPlaywrightLocator } = require('./extras/PlaywrightReactVueLocator');

let playwright;
let perfTiming;
Expand Down Expand Up @@ -100,6 +100,7 @@ const pathSeparator = path.sep;
* @prop {boolean} [bypassCSP] - bypass Content Security Policy or CSP
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
* @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).
* @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).
*/
const config = {};

Expand Down Expand Up @@ -379,6 +380,7 @@ class Playwright extends Helper {
highlightElement: false,
};

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

if (availableBrowsers.indexOf(config.browser) < 0) {
Expand Down Expand Up @@ -464,6 +466,7 @@ class Playwright extends Helper {
try {
await playwright.selectors.register('__value', createValueEngine);
await playwright.selectors.register('__disabled', createDisabledEngine);
if (process.env.testIdAttribute) await playwright.selectors.setTestIdAttribute(process.env.testIdAttribute);
} catch (e) {
console.warn(e);
}
Expand Down Expand Up @@ -3455,13 +3458,16 @@ function buildLocatorString(locator) {
async function findElements(matcher, locator) {
if (locator.react) return findReact(matcher, locator);
if (locator.vue) return findVue(matcher, locator);
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator);
locator = new Locator(locator, 'css');

return matcher.locator(buildLocatorString(locator)).all();
}

async function findElement(matcher, locator) {
if (locator.react) return findReact(matcher, locator);
if (locator.vue) return findVue(matcher, locator);
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator);
locator = new Locator(locator, 'css');

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

locator = new Locator(locator);
if (!locator.isFuzzy()) return findElements.call(this, matcher, locator);
Expand Down
7 changes: 6 additions & 1 deletion lib/helper/extras/PlaywrightReactVueLocator.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ async function findVue(matcher, locator) {
return matcher.locator(_locator).all();
}

async function findByPlaywrightLocator(matcher, locator) {
if (locator && locator.toString().includes(process.env.testIdAttribute)) return matcher.getByTestId(locator.pw.value.split('=')[1]);
return matcher.locator(locator.pw).all();
}

function propBuilder(props) {
let _props = '';

Expand All @@ -35,4 +40,4 @@ function propBuilder(props) {
return _props;
}

module.exports = { findReact, findVue };
module.exports = { findReact, findVue, findByPlaywrightLocator };
24 changes: 23 additions & 1 deletion lib/locator.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { sprintf } = require('sprintf-js');

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

const locatorTypes = ['css', 'by', 'xpath', 'id', 'name', 'fuzzy', 'frame', 'shadow'];
const locatorTypes = ['css', 'by', 'xpath', 'id', 'name', 'fuzzy', 'frame', 'shadow', 'pw'];
/** @class */
class Locator {
/**
Expand Down Expand Up @@ -51,6 +51,9 @@ class Locator {
if (isShadow(locator)) {
this.type = 'shadow';
}
if (isPlaywrightLocator(locator)) {
this.type = 'pw';
}

Locator.filters.forEach(f => f(locator, this));
}
Expand All @@ -71,6 +74,8 @@ class Locator {
return this.value;
case 'shadow':
return { shadow: this.value };
case 'pw':
return { pw: this.value };
}
return this.value;
}
Expand Down Expand Up @@ -115,6 +120,13 @@ class Locator {
return this.type === 'css';
}

/**
* @returns {boolean}
*/
isPlaywrightLocator() {
return this.type === 'pw';
}

/**
* @returns {boolean}
*/
Expand Down Expand Up @@ -522,6 +534,16 @@ function removePrefix(xpath) {
.replace(/^(\.|\/)+/, '');
}

/**
* @private
* check if the locator is a Playwright locator
* @param {string} locator
* @returns {boolean}
*/
function isPlaywrightLocator(locator) {
return locator.includes('_react') || locator.includes('_vue') || locator.includes('data-testid');
}

/**
* @private
* @param {CodeceptJS.LocatorOrString} locator
Expand Down
17 changes: 15 additions & 2 deletions test/acceptance/react_test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const { I } = inject();

Feature('React Selectors');

Scenario('props @Puppeteer @Playwright', ({ I }) => {
Scenario('props @Puppeteer @Playwright', () => {
I.amOnPage('https://codecept.io/test-react-calculator/');
I.click('7');
I.click({ react: 't', props: { name: '=' } });
Expand All @@ -11,10 +13,21 @@ Scenario('props @Puppeteer @Playwright', ({ I }) => {
I.seeElement({ react: 't', props: { value: '10' } });
});

Scenario('component name @Puppeteer @Playwright', ({ I }) => {
Scenario('component name @Puppeteer @Playwright', () => {
I.amOnPage('http://negomi.github.io/react-burger-menu/');
I.click({ react: 'BurgerIcon' });
I.waitForVisible('#slide', 10);
I.click('Alerts');
I.seeElement({ react: 'Demo' });
});

Scenario('using playwright locator @Playwright', () => {
I.amOnPage('https://codecept.io/test-react-calculator/');
I.click('7');
I.click({ pw: '_react=t[name = "="]' });
I.seeElement({ pw: '_react=t[value = "7"]' });
I.click({ pw: '_react=t[name = "+"]' });
I.click({ pw: '_react=t[name = "3"]' });
I.click({ pw: '_react=t[name = "="]' });
I.seeElement({ pw: '_react=t[value = "10"]' });
});
2 changes: 1 addition & 1 deletion test/data/app/view/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<title>TestEd Beta 2.0</title>
<body>

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

<div class="notice" qa-id = "test"><?php if (isset($notice)) echo $notice; ?></div>
Expand Down
36 changes: 36 additions & 0 deletions test/helper/Playwright_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1678,3 +1678,39 @@ describe('Playwright - HAR', () => {
});
});
});

describe('using data-testid attribute', () => {
before(() => {
global.codecept_dir = path.join(__dirname, '/../data');
global.output_dir = path.join(`${__dirname}/../data/output`);

I = new Playwright({
url: siteUrl,
windowSize: '500x700',
show: false,
restart: true,
browser: 'chromium',
});
I._init();
return I._beforeSuite();
});

beforeEach(async () => {
return I._before().then(() => {
page = I.page;
browser = I.browser;
});
});

afterEach(async () => {
return I._after();
});

it('should find element by data-testid attribute', async () => {
await I.amOnPage('/');

const webElements = await I.grabWebElements({ pw: '[data-testid="welcome"]' });
assert.equal(webElements[0]._selector, '[data-testid="welcome"] >> nth=0');
assert.equal(webElements.length, 1);
});
});
21 changes: 21 additions & 0 deletions test/unit/locator_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,27 @@ describe('Locator', () => {
expect(l.value).to.equal('foo');
expect(l.toString()).to.equal('foo');
});

it('should create playwright locator - _react', () => {
const l = new Locator({ pw: '_react=button' });
expect(l.type).to.equal('pw');
expect(l.value).to.equal('_react=button');
expect(l.toString()).to.equal('{pw: _react=button}');
});

it('should create playwright locator - _vue', () => {
const l = new Locator({ pw: '_vue=button' });
expect(l.type).to.equal('pw');
expect(l.value).to.equal('_vue=button');
expect(l.toString()).to.equal('{pw: _vue=button}');
});

it('should create playwright locator - data-testid', () => {
const l = new Locator({ pw: '[data-testid="directions"]' });
expect(l.type).to.equal('pw');
expect(l.value).to.equal('[data-testid="directions"]');
expect(l.toString()).to.equal('{pw: [data-testid="directions"]}');
});
});

describe('with object argument', () => {
Expand Down
4 changes: 2 additions & 2 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,8 +442,8 @@ declare namespace CodeceptJS {
| { react: string }
| { vue: string }
| { shadow: string[] }
| { custom: string };

| { custom: string }
| { pw: string };
interface CustomLocators {}
interface OtherLocators { props?: object }
type LocatorOrString =
Expand Down

0 comments on commit 1aff923

Please sign in to comment.