diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index fa3c56abf..16595dafc 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -1675,19 +1675,9 @@ class Playwright extends Helper { async waitForEnabled(locator, sec) { const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; locator = new Locator(locator, 'css'); - const matcher = await this.context; - let waiter; const context = await this._getContext(); - if (!locator.isXPath()) { - // playwright combined selectors - waiter = context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> __disabled=false`, { timeout: waitTimeout }); - } else { - const enabledFn = function ([locator, $XPath]) { - eval($XPath); // eslint-disable-line no-eval - return $XPath(null, locator).filter(el => !el.disabled).length > 0; - }; - waiter = context.waitForFunction(enabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout }); - } + // playwright combined selectors + const waiter = context.waitForSelector(`${buildLocatorString(locator)} >> __disabled=false`, { timeout: waitTimeout }); return waiter.catch((err) => { throw new Error(`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`); }); @@ -1699,19 +1689,9 @@ class Playwright extends Helper { async waitForValue(field, value, sec) { const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; const locator = new Locator(field, 'css'); - const matcher = await this.context; - let waiter; const context = await this._getContext(); - if (!locator.isXPath()) { - // uses a custom selector engine for finding value properties on elements - waiter = context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> __value=${value}`, { timeout: waitTimeout, state: 'visible' }); - } else { - const valueFn = function ([locator, $XPath, value]) { - eval($XPath); // eslint-disable-line no-eval - return $XPath(null, locator).filter(el => (el.value || '').indexOf(value) !== -1).length > 0; - }; - waiter = context.waitForFunction(valueFn, [locator.value, $XPath.toString(), value], { timeout: waitTimeout }); - } + // uses a custom selector engine for finding value properties on elements + const waiter = context.waitForSelector(`${buildLocatorString(locator)} >> __value=${value}`, { timeout: waitTimeout, state: 'visible' }); return waiter.catch((err) => { const loc = locator.toString(); throw new Error(`element (${loc}) is not in DOM or there is no element(${loc}) with value "${value}" after ${waitTimeout / 1000} sec\n${err.message}`); @@ -1753,7 +1733,7 @@ class Playwright extends Helper { * {{> waitForClickable }} */ async waitForClickable(locator, waitTimeout) { - console.log('I.waitForClickable is DEPRECATED: This is no longer needed, Playwright automatically waits for element to be clikable'); + console.log('I.waitForClickable is DEPRECATED: This is no longer needed, Playwright automatically waits for element to be clickable'); console.log('Remove usage of this function'); } @@ -1766,7 +1746,7 @@ class Playwright extends Helper { locator = new Locator(locator, 'css'); const context = await this._getContext(); - const waiter = context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()}`, { timeout: waitTimeout, state: 'attached' }); + const waiter = context.waitForSelector(buildLocatorString(locator), { timeout: waitTimeout, state: 'attached' }); return waiter.catch((err) => { throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${err.message}`); }); @@ -1781,7 +1761,7 @@ class Playwright extends Helper { const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; locator = new Locator(locator, 'css'); const context = await this._getContext(); - const waiter = context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()}`, { timeout: waitTimeout, state: 'visible' }); + const waiter = context.waitForSelector(buildLocatorString(locator), { timeout: waitTimeout, state: 'visible' }); return waiter.catch((err) => { throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec\n${err.message}`); }); @@ -1794,7 +1774,7 @@ class Playwright extends Helper { const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; locator = new Locator(locator, 'css'); const context = await this._getContext(); - const waiter = context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()}`, { timeout: waitTimeout, state: 'hidden' }); + const waiter = context.waitForSelector(buildLocatorString(locator), { timeout: waitTimeout, state: 'hidden' }); return waiter.catch((err) => { throw new Error(`element (${locator.toString()}) still visible after ${waitTimeout / 1000} sec\n${err.message}`); }); @@ -1807,7 +1787,7 @@ class Playwright extends Helper { const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; locator = new Locator(locator, 'css'); const context = await this._getContext(); - return context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()}`, { timeout: waitTimeout, state: 'hidden' }).catch((err) => { + return context.waitForSelector(buildLocatorString(locator), { timeout: waitTimeout, state: 'hidden' }).catch((err) => { throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`); }); } @@ -2061,14 +2041,19 @@ class Playwright extends Helper { module.exports = Playwright; -async function findElements(matcher, locator) { - locator = new Locator(locator, 'css'); +function buildLocatorString(locator) { if (locator.isCustom()) { - return matcher.$$(`${locator.type}=${locator.value}`); - } if (!locator.isXPath()) { - return matcher.$$(locator.simplify()); + return `${locator.type}=${locator.value}`; + } if (locator.isXPath()) { + // dont rely on heuristics of playwright for figuring out xpath + return `xpath=${locator.value}`; } - return matcher.$$(`xpath=${locator.value}`); + return locator.simplify(); +} + +async function findElements(matcher, locator) { + locator = new Locator(locator, 'css'); + return matcher.$$(buildLocatorString(locator)); } async function proceedClick(locator, context = null, options = {}) { diff --git a/test/data/app/view/form/custom_locator.php b/test/data/app/view/form/custom_locator.php new file mode 100644 index 000000000..d7ffadd1e --- /dev/null +++ b/test/data/app/view/form/custom_locator.php @@ -0,0 +1,56 @@ + +
+ + + + + + + + + + + + + + diff --git a/test/helper/webapi.js b/test/helper/webapi.js index 2bd18abc2..717908beb 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -6,6 +6,10 @@ const formContents = require('../../lib/utils').test.submittedData(dataFile); const fileExists = require('../../lib/utils').fileExists; const secret = require('../../lib/secret').secret; +const Locator = require('../../lib/locator'); +const customLocators = require('../../lib/plugin/customLocator'); + +let originalLocators; let I; let data; let siteUrl; @@ -1320,4 +1324,59 @@ module.exports.tests = function () { }); }); }); + + describe('#customLocators', () => { + beforeEach(() => { + originalLocators = Locator.filters; + Locator.filters = []; + }); + afterEach(() => { + // reset custom locators + Locator.filters = originalLocators; + }); + it('should support xpath custom locator by default', async () => { + customLocators({ + attribute: 'data-test-id', + enabled: true, + }); + await I.amOnPage('/form/custom_locator'); + await I.dontSee('Step One Button'); + await I.dontSeeElement('$step_1'); + await I.waitForVisible('$step_1', 2); + await I.seeElement('$step_1'); + await I.click('$step_1'); + await I.waitForVisible('$step_2', 2); + await I.see('Step Two Button'); + }); + it('can use css strategy for custom locator', async () => { + customLocators({ + attribute: 'data-test-id', + enabled: true, + strategy: 'css', + }); + await I.amOnPage('/form/custom_locator'); + await I.dontSee('Step One Button'); + await I.dontSeeElement('$step_1'); + await I.waitForVisible('$step_1', 2); + await I.seeElement('$step_1'); + await I.click('$step_1'); + await I.waitForVisible('$step_2', 2); + await I.see('Step Two Button'); + }); + it('can use xpath strategy for custom locator', async () => { + customLocators({ + attribute: 'data-test-id', + enabled: true, + strategy: 'xpath', + }); + await I.amOnPage('/form/custom_locator'); + await I.dontSee('Step One Button'); + await I.dontSeeElement('$step_1'); + await I.waitForVisible('$step_1', 2); + await I.seeElement('$step_1'); + await I.click('$step_1'); + await I.waitForVisible('$step_2', 2); + await I.see('Step Two Button'); + }); + }); }; diff --git a/test/runner/allure_test.js b/test/runner/allure_test.js index 501443e30..0e9976bae 100644 --- a/test/runner/allure_test.js +++ b/test/runner/allure_test.js @@ -50,11 +50,14 @@ describe('CodeceptJS Allure Plugin', () => { stdout.should.include('FAIL | 0 passed, 1 failed'); const files = fs.readdirSync(path.join(codecept_dir, 'output/failed')); - const testResultPath = files[0]; - assert(testResultPath.match(/\.xml$/), 'not a xml file'); - const file = fs.readFileSync(path.join(codecept_dir, 'output/failed', testResultPath), 'utf8'); - file.should.include('BeforeSuite of suite failing setup test suite: failed.'); - file.should.include('the before suite setup failed'); + // join all reports together + const reports = files.map((testResultPath) => { + assert(testResultPath.match(/\.xml$/), 'not a xml file'); + return fs.readFileSync(path.join(codecept_dir, 'output/failed', testResultPath), 'utf8'); + }).join(' '); + reports.should.include('BeforeSuite of suite failing setup test suite: failed.'); + reports.should.include('the before suite setup failed'); + reports.should.include('Skipped due to failure in \'before\' hook'); done(); }); }); @@ -68,11 +71,14 @@ describe('CodeceptJS Allure Plugin', () => { stdout.should.include('FAIL | 0 passed'); const files = fs.readdirSync(path.join(codecept_dir, 'output/failed')); - const testResultPath = files[0]; - assert(testResultPath.match(/\.xml$/), 'not a xml file'); - const file = fs.readFileSync(path.join(codecept_dir, 'output/failed', testResultPath), 'utf8'); - file.should.include('BeforeSuite of suite failing setup test suite: failed.'); - file.should.include('the before suite setup failed'); + const reports = files.map((testResultPath) => { + assert(testResultPath.match(/\.xml$/), 'not a xml file'); + return fs.readFileSync(path.join(codecept_dir, 'output/failed', testResultPath), 'utf8'); + }).join(' '); + reports.should.include('BeforeSuite of suite failing setup test suite: failed.'); + reports.should.include('the before suite setup failed'); + // the line below does not work in workers needs investigating https://github.com/Codeception/CodeceptJS/issues/2391 + // reports.should.include('Skipped due to failure in \'before\' hook'); done(); }); });