diff --git a/docs/helpers/Playwright.md b/docs/helpers/Playwright.md
index 99bf03983..fb63eadcc 100644
--- a/docs/helpers/Playwright.md
+++ b/docs/helpers/Playwright.md
@@ -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].
@@ -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
diff --git a/docs/locators.md b/docs/locators.md
index b7e4ae906..2349bc705 100644
--- a/docs/locators.md
+++ b/docs/locators.md
@@ -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:
@@ -26,6 +27,7 @@ Examples:
* {css: 'input[type=input][value=foo]'} matches ``
* {xpath: "//input[@type='submit'][contains(@value, 'foo')]"} matches ``
* {class: 'foo'} matches `
`
+* { 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/).
diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js
index 2e1603c1d..a56d2314d 100644
--- a/lib/helper/Playwright.js
+++ b/lib/helper/Playwright.js
@@ -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;
@@ -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 = {};
@@ -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) {
@@ -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);
}
@@ -3455,6 +3458,7 @@ 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();
@@ -3462,6 +3466,8 @@ async function findElements(matcher, locator) {
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();
@@ -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);
diff --git a/lib/helper/extras/PlaywrightReactVueLocator.js b/lib/helper/extras/PlaywrightReactVueLocator.js
index 59f8a26eb..c761db4bb 100644
--- a/lib/helper/extras/PlaywrightReactVueLocator.js
+++ b/lib/helper/extras/PlaywrightReactVueLocator.js
@@ -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 = '';
@@ -35,4 +40,4 @@ function propBuilder(props) {
return _props;
}
-module.exports = { findReact, findVue };
+module.exports = { findReact, findVue, findByPlaywrightLocator };
diff --git a/lib/locator.js b/lib/locator.js
index a08549b10..9c6efd53f 100644
--- a/lib/locator.js
+++ b/lib/locator.js
@@ -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 {
/**
@@ -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));
}
@@ -71,6 +74,8 @@ class Locator {
return this.value;
case 'shadow':
return { shadow: this.value };
+ case 'pw':
+ return { pw: this.value };
}
return this.value;
}
@@ -115,6 +120,13 @@ class Locator {
return this.type === 'css';
}
+ /**
+ * @returns {boolean}
+ */
+ isPlaywrightLocator() {
+ return this.type === 'pw';
+ }
+
/**
* @returns {boolean}
*/
@@ -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
diff --git a/test/acceptance/react_test.js b/test/acceptance/react_test.js
index 86913cb92..5f5e16b38 100644
--- a/test/acceptance/react_test.js
+++ b/test/acceptance/react_test.js
@@ -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: '=' } });
@@ -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"]' });
+});
diff --git a/test/data/app/view/index.php b/test/data/app/view/index.php
index c25c9565e..1c42159ff 100755
--- a/test/data/app/view/index.php
+++ b/test/data/app/view/index.php
@@ -2,7 +2,7 @@
TestEd Beta 2.0
-