Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: move to Locator playwright #3738

Merged
merged 20 commits into from
Aug 19, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/helpers/Playwright.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ const elements = await this.helpers['Playwright']._locate({name: 'password'});

### _locateCheckable

Find a checkbox by providing human readable text:
Find a checkbox by providing human-readable text:
NOTE: Assumes the checkable element exists

```js
Expand Down
4 changes: 2 additions & 2 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ I.click('=sign-up'); // matches => [data-qa=sign-up],[data-test=sign-up]

Prints errors found in HTML code after each failed test.

It scans HTML and searches for elements with error classes.
It scans HTML and searches for elements with error classes.
If an element found prints a text from it to console and adds as artifact to the test.

Enable this plugin in config:
Expand Down Expand Up @@ -608,7 +608,7 @@ Scenario Outline: ...

## heal

Self-healing tests with OpenAI.
Self-healing tests with OpenAI.

This plugin is experimental and requires OpenAI API key.

Expand Down
137 changes: 59 additions & 78 deletions lib/helper/Playwright.js
Original file line number Diff line number Diff line change
Expand Up @@ -1123,7 +1123,7 @@ class Playwright extends Helper {
}

/**
* Find a checkbox by providing human readable text:
* Find a checkbox by providing human-readable text:
* NOTE: Assumes the checkable element exists
*
* ```js
Expand Down Expand Up @@ -1526,13 +1526,8 @@ class Playwright extends Helper {
const els = await findFields.call(this, field);
assertElementExists(els, field, 'Field');
const el = els[0];
const tag = await el.getProperty('tagName').then(el => el.jsonValue());
const editable = await el.getProperty('contenteditable').then(el => el.jsonValue());
if (tag === 'INPUT' || tag === 'TEXTAREA') {
await this._evaluateHandeInContext(el => el.value = '', el);
} else if (editable) {
await this._evaluateHandeInContext(el => el.innerHTML = '', el);
}

await el.fill('');

highlightActiveElement.call(this, el, this.page);

Expand All @@ -1556,21 +1551,16 @@ class Playwright extends Helper {
* ```
*/
async clearField(locator, options = {}) {
let result;
const isNewClearMethodPresent = typeof this.page.locator().clear === 'function';
const els = await findFields.call(this, locator);
assertElementExists(els, locator, 'Field to clear');

const el = els[0];

if (isNewClearMethodPresent) {
const els = await findFields.call(this, locator);
assertElementExists(els, locator, 'Field to clear');
// TODO: locator change required after #3677 implementation
const elXpath = await getXPathForElement(els[0]);
highlightActiveElement.call(this, el, this.page);

await this.page.locator(elXpath).clear(options);
result = await this._waitForAction();
} else {
result = await this.fillField(locator, '');
}
return result;
await el.fill('');
kobenguyent marked this conversation as resolved.
Show resolved Hide resolved

return this._waitForAction();
}

/**
Expand Down Expand Up @@ -1624,29 +1614,11 @@ class Playwright extends Helper {
const els = await findFields.call(this, select);
assertElementExists(els, select, 'Selectable field');
const el = els[0];
if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
throw new Error('Element is not <select>');
}

highlightActiveElement.call(this, el, this.page);
if (!Array.isArray(option)) option = [option];

for (const key in option) {
const opt = xpathLocator.literal(option[key]);
let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) });
if (optEl.length) {
this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
continue;
}
optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) });
if (optEl.length) {
this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
}
}
await this._evaluateHandeInContext((element) => {
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
}, el);

await el.selectOption(option);
return this._waitForAction();
}

Expand Down Expand Up @@ -1897,7 +1869,7 @@ class Playwright extends Helper {
const els = await this._locate(locator);
const texts = [];
for (const el of els) {
texts.push(await (await el.getProperty('innerText')).jsonValue());
texts.push(await (await el.innerText()));
}
this.debug(`Matched ${els.length} elements`);
return texts;
Expand All @@ -1919,7 +1891,7 @@ class Playwright extends Helper {
async grabValueFromAll(locator) {
const els = await findFields.call(this, locator);
this.debug(`Matched ${els.length} elements`);
return Promise.all(els.map(el => el.getProperty('value').then(t => t.jsonValue())));
return Promise.all(els.map(el => el.inputValue()));
}

/**
Expand All @@ -1938,7 +1910,7 @@ class Playwright extends Helper {
async grabHTMLFromAll(locator) {
const els = await this._locate(locator);
this.debug(`Matched ${els.length} elements`);
return Promise.all(els.map(el => el.$eval('xpath=.', element => element.innerHTML, el)));
return Promise.all(els.map(el => el.innerHTML()));
}

/**
Expand All @@ -1959,7 +1931,7 @@ class Playwright extends Helper {
async grabCssPropertyFromAll(locator, cssProperty) {
const els = await this._locate(locator);
this.debug(`Matched ${els.length} elements`);
const cssValues = await Promise.all(els.map(el => el.$eval('xpath=.', (el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)));
const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)));

return cssValues;
}
Expand All @@ -1975,21 +1947,20 @@ class Playwright extends Helper {
const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
const elemAmount = res.length;
const commands = [];
res.forEach((el) => {
Object.keys(cssPropertiesCamelCase).forEach((prop) => {
commands.push(el.$eval('xpath=.', (el) => {
const style = window.getComputedStyle ? getComputedStyle(el) : el.currentStyle;
return JSON.parse(JSON.stringify(style));
}, el)
.then((props) => {
if (isColorProperty(prop)) {
return convertColorToRGBA(props[prop]);
}
return props[prop];
}));
let props = [];

for (const element of res) {
const cssProperties = await element.evaluate((el) => getComputedStyle(el));

Object.keys(cssPropertiesCamelCase).forEach(prop => {
if (isColorProperty(prop)) {
props.push(convertColorToRGBA(cssProperties[prop]));
} else {
props.push(cssProperties[prop]);
}
});
});
let props = await Promise.all(commands);
}

const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
if (!Array.isArray(props)) props = [props];
let chunked = chunkArray(props, values.length);
Expand All @@ -2015,7 +1986,7 @@ class Playwright extends Helper {
res.forEach((el) => {
Object.keys(attributes).forEach((prop) => {
commands.push(el
.$eval('xpath=.', (el, attr) => el[attr] || el.getAttribute(attr), prop));
.evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop));
});
});
let attrs = await Promise.all(commands);
Expand Down Expand Up @@ -2074,8 +2045,7 @@ class Playwright extends Helper {
const array = [];

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

return array;
Expand Down Expand Up @@ -2517,16 +2487,26 @@ class Playwright extends Helper {
}
return;
}
let contentFrame;

if (!locator) {
this.context = this.page;
this.context = await this.page.frames()[0];
this.contextLocator = null;
return;
}

// iframe by selector
const els = await this._locate(locator);
assertElementExists(els, locator);
const contentFrame = await els[0].contentFrame();
// assertElementExists(els, locator);

// get content of the first iframe
if ((locator.frame && locator.frame === 'iframe') || locator.toLowerCase() === 'iframe') {
contentFrame = await this.page.frames()[1];
// get content of the iframe using its name
} else if (locator.toLowerCase().includes('name=')) {
const frameName = locator.split('=')[1].replace(/"/g, '').replaceAll(/]/g, '');
contentFrame = await this.page.frame(frameName);
}

if (contentFrame) {
this.context = contentFrame;
Expand Down Expand Up @@ -2699,7 +2679,8 @@ async function getXPathForElement(elementHandle) {
async function findElements(matcher, locator) {
if (locator.react) return findReact(matcher, locator);
locator = new Locator(locator, 'css');
return matcher.$$(buildLocatorString(locator));

return matcher.locator(buildLocatorString(locator)).all();
kobenguyent marked this conversation as resolved.
Show resolved Hide resolved
}

async function getVisibleElements(elements) {
Expand Down Expand Up @@ -2778,22 +2759,22 @@ async function findClickable(matcher, locator) {
async function proceedSee(assertType, text, context, strict = false) {
let description;
let allText;

if (!context) {
let el = await this.context;

if (el && !el.getProperty) {
// Fallback to body
el = await this.context.$('body');
el = await this.page.$('body');
}

allText = [await el.getProperty('innerText').then(p => p.jsonValue())];
allText = [await el.innerText()];
description = 'web application';
} else {
const locator = new Locator(context, 'css');
description = `element ${locator.toString()}`;
const els = await this._locate(locator);
assertElementExists(els, locator.toString());
allText = await Promise.all(els.map(el => el.getProperty('innerText').then(p => p.jsonValue())));
allText = await Promise.all(els.map(el => el.innerText()));
}

if (strict) {
Expand Down Expand Up @@ -2861,15 +2842,15 @@ async function proceedSeeInField(assertType, field, value) {
const els = await findFields.call(this, field);
assertElementExists(els, field, 'Field');
const el = els[0];
const tag = await el.getProperty('tagName').then(el => el.jsonValue());
const fieldType = await el.getProperty('type').then(el => el.jsonValue());
const tag = await el.evaluate(e => e.tagName);
const fieldType = await el.getAttribute('type');

const proceedMultiple = async (elements) => {
const fields = Array.isArray(elements) ? elements : [elements];

const elementValues = [];
for (const element of fields) {
elementValues.push(await element.getProperty('value').then(el => el.jsonValue()));
elementValues.push(await element.inputValue());
}

if (typeof value === 'boolean') {
Expand All @@ -2883,8 +2864,8 @@ async function proceedSeeInField(assertType, field, value) {
};

if (tag === 'SELECT') {
if (await el.getProperty('multiple')) {
const selectedOptions = await el.$$('option:checked');
if (await el.getAttribute('multiple')) {
const selectedOptions = await el.all('option:checked');
if (!selectedOptions.length) return null;

const options = await filterFieldsByValue(selectedOptions, value, true);
Expand Down Expand Up @@ -2915,7 +2896,7 @@ async function proceedSeeInField(assertType, field, value) {
async function filterFieldsByValue(elements, value, onlySelected) {
const matches = [];
for (const element of elements) {
const val = await element.getProperty('value').then(el => el.jsonValue());
const val = await element.getAttribute('value');
let isSelected = true;
if (onlySelected) {
isSelected = await elementSelected(element);
Expand All @@ -2939,12 +2920,12 @@ async function filterFieldsBySelectionState(elements, state) {
}

async function elementSelected(element) {
const type = await element.getProperty('type').then(el => !!el && el.jsonValue());
const type = await element.getAttribute('type');

if (type === 'checkbox' || type === 'radio') {
return element.isChecked();
}
return element.getProperty('selected').then(el => el.jsonValue());
return element.getAttribute('selected');
}

function isFrameLocator(locator) {
Expand Down
14 changes: 7 additions & 7 deletions test/acceptance/react_test.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
Feature('React Selectors');

Scenario('props @WebDriver @Puppeteer @Playwright', ({ I }) => {
Scenario('props @Puppeteer @Playwright', ({ I }) => {
I.amOnPage('https://codecept.io/test-react-calculator/');
I.click('7');
I.seeElement({ react: 't', props: { name: '5' } });
I.click('button', { react: 't', props: { name: '2' } });
I.click('button', { react: 't', props: { name: '+' } });
I.click('button', { react: 't', props: { name: '9' } });
I.click('button', { react: 't', props: { name: '=' } });
I.seeElement({ react: 't', props: { value: '81' } });
I.click({ react: 't', props: { name: '=' } });
I.seeElement({ react: 't', props: { value: '7' } });
I.click({ react: 't', props: { name: '+' } });
I.click({ react: 't', props: { name: '3' } });
I.click({ react: 't', props: { name: '=' } });
I.seeElement({ react: 't', props: { value: '10' } });
});

Scenario('component name @Puppeteer @Playwright', ({ I }) => {
Expand Down
Loading