diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 6571d838e..ed5aa4c47 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -50,5 +50,5 @@ jobs: run: "BROWSER=firefox node ./bin/codecept.js run -c test/acceptance/codecept.Playwright.js --grep @Playwright --debug" - name: run webkit tests run: "BROWSER=webkit node ./bin/codecept.js run -c test/acceptance/codecept.Playwright.js --grep @Playwright --debug" - - name: run unit tests + - name: run chromium unit tests run: ./node_modules/.bin/mocha test/helper/Playwright_test.js --timeout 5000 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fc7134ff..1f1f67425 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,162 @@ +## 3.5.9 + +❤ī¸ Thanks all to those who contributed to make this release! ❤ī¸ + +🛩ī¸ *Features* +* feat: expose WebElement (#4043) - by @KobeNguyenT +``` +Now we expose the WebElements that are returned by the WebHelper and you could make the subsequence actions on them. + +// Playwright helper would return the Locator + +I.amOnPage('/form/focus_blur_elements'); +const webElements = await I.grabWebElements('#button'); +webElements[0].click(); +``` +* feat(playwright): support HAR replaying (#3990) - by @KobeNguyenT +``` +Replaying from HAR + + // Replay API requests from HAR. + // Either use a matching response from the HAR, + // or abort the request if nothing matches. + I.replayFromHar('./output/har/something.har', { url: "*/**/api/v1/fruits" }); + I.amOnPage('https://demo.playwright.dev/api-mocking'); + I.see('CodeceptJS'); +[Parameters] +harFilePath [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) Path to recorded HAR file +opts [object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)? [Options for replaying from HAR](https://playwright.dev/docs/api/class-page#page-route-from-har) +``` +* feat(playwright): support HAR recording (#3986) - by @KobeNguyenT +``` +A HAR file is an HTTP Archive file that contains a record of all the network requests that are made when a page is loaded. +It contains information about the request and response headers, cookies, content, timings, and more. +You can use HAR files to mock network requests in your tests. HAR will be saved to output/har. +More info could be found here https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har. + +... +recordHar: { + mode: 'minimal', // possible values: 'minimal'|'full'. + content: 'embed' // possible values: "omit"|"embed"|"attach". +} +... +``` +* improvement(playwright): support partial string for option (#4016) - by @KobeNguyenT +``` +await I.amOnPage('/form/select'); +await I.selectOption('Select your age', '21-'); +``` + +🐛 *Bug Fixes* +* fix(playwright): proceedSee could not find the element (#4006) - by @hatufacci +* fix(appium): remove the vendor prefix of 'bstack:options' (#4053) - by @mojtabaalavi +* fix(workers): event improvements (#3953) - by @KobeNguyenT +``` +Emit the new event: event.workers.result. + +CodeceptJS also exposes the env var `process.env.RUNS_WITH_WORKERS` when running tests with run-workers command so that you could handle the events better in your plugins/helpers. + +const { event } = require('codeceptjs'); + +module.exports = function() { + // this event would trigger the `_publishResultsToTestrail` when running `run-workers` command + event.dispatcher.on(event.workers.result, async () => { + await _publishResultsToTestrail(); + }); + + // this event would not trigger the `_publishResultsToTestrail` multiple times when running `run-workers` command + event.dispatcher.on(event.all.result, async () => { + // when running `run` command, this env var is undefined + if (!process.env.RUNS_WITH_WORKERS) await _publishResultsToTestrail(); + }); +} +``` +* fix: ai html updates (#3962) - by @davert +``` +replaced minify library with a modern and more secure fork. Fixes html-minifier@4.0.0 Regular Expression Denial of Service vulnerability #3829 +AI class is implemented as singleton +refactored heal.js plugin to work on edge cases +add configuration params on number of fixes performed by ay heal +improved recorder class to add more verbose log +improved recorder class to ignore some of errors +``` +* fix(appium): closeApp supports both Android/iOS (#4046) - by @KobeNguyenT +* fix: some security vulnerability of some packages (#4045) - by @KobeNguyenT +* fix: seeAttributesOnElements check condition (#4029) - by @KobeNguyenT +* fix: waitForText locator issue (#4039) - by @KobeNguyenT +``` +Fixed this error: + +locator.isVisible: Unexpected token "s" while parsing selector ":has-text('Were you able to resolve the resident's issue?') >> nth=0" + at Playwright.waitForText (node_modules\codeceptjs\lib\helper\Playwright.js:2584:79) +``` +* fix: move to sha256 (#4038) - by @KobeNguyenT +* fix: respect retries from retryfailedstep plugin in helpers (#4028) - by @KobeNguyenT +``` +Currently inside the _before() of helpers for example Playwright, the retries is set there, however, when retryFailedStep plugin is enabled, the retries of recorder is still using the value from _before() not the value from retryFailedStep plugin. + +Fix: + +- introduce the process.env.FAILED_STEP_RETIRES which could be access everywhere as the helper won't know anything about the plugin. +- set default retries of Playwright to 3 to be on the same page with Puppeteer. +``` +* fix: examples in test title (#4030) - by @KobeNguyenT +``` +When test title doesn't have the data in examples: + +Feature: Faker examples + + Scenario Outline: Below are the users + Examples: + | user | role | + | John | admin | + | Tim | client | + +Faker examples -- + [1] Starting recording promises + Timeouts: + Below are the users {"user":"John","role":"admin"} + ✔ OK in 4ms + + Below are the users {"user":"Tim","role":"client"} + ✔ OK in 1ms + +When test title includes the data in examples: + + +Feature: Faker examples + + Scenario Outline: Below are the users - - + Examples: + | user | role | + | John | admin | + | Tim | client | + + +Faker examples -- + [1] Starting recording promises + Timeouts: + Below are the users - John - admin + ✔ OK in 4ms + + Below are the users - Tim - client + ✔ OK in 1ms +``` +* fix: disable retryFailedStep when using with tryTo (#4022) - by @KobeNguyenT +* fix: locator builder returns error when class name contains hyphen (#4024) - by @KobeNguyenT +* fix: seeCssPropertiesOnElements failed when font-weight is a number (#4026) - by @KobeNguyenT +* fix(appium): missing await on some steps of runOnIOS and runOnAndroid (#4018) - by @KobeNguyenT +* fix(cli): no error of failed tests when using retry with scenario only (#4020) - by @KobeNguyenT +* fix: set getPageTimeout to 30s (#4031) - by @KobeNguyenT +* fix(appium): expose switchToContext (#4015) - by @KobeNguyenT +* fix: promise issue (#4013) - by @KobeNguyenT +* fix: seeCssPropertiesOnElements issue with improper condition (#4057) - by @KobeNguyenT + +📖 *Documentation* +* docs: Update clearCookie documentation for Playwright helper (#4005) - by @Hellosager +* docs: improve the example code for autoLogin (#4019) - by @KobeNguyenT + ![Screenshot 2023-11-22 at 14 40 11](https://github.com/codeceptjs/CodeceptJS/assets/7845001/c05ac436-efd0-4bc0-a46c-386f915c0f17) + ## 3.5.8 Thanks all to those who contributed to make this release! @@ -5,7 +164,7 @@ Thanks all to those who contributed to make this release! 🐛 *Bug Fixes* fix(appium): type of setNetworkConnection() (#3994) - by @mirao fix: improve the way to show deprecated appium v1 message (#3992) - by @KobeNguyenT -fix: missing exit condition of some wait functions - by @kobenguyent +fix: missing exit condition of some wait functions - by @KobeNguyenT ## 3.5.7 diff --git a/lib/colorUtils.js b/lib/colorUtils.js index 349de6a46..ce1f6465a 100644 --- a/lib/colorUtils.js +++ b/lib/colorUtils.js @@ -226,15 +226,25 @@ function isColorProperty(prop) { 'color', 'background', 'backgroundColor', + 'background-color', 'borderColor', + 'border-color', 'borderBottomColor', + 'border-bottom-color', 'borderLeftColor', + 'border-left-color', 'borderRightColor', 'borderTopColor', 'caretColor', 'columnRuleColor', 'outlineColor', 'textDecorationColor', + 'border-right-color', + 'border-top-color', + 'caret-color', + 'column-rule-color', + 'outline-color', + 'text-decoration-color', ].indexOf(prop) > -1; } diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 95cab8be0..714b20c14 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -2119,19 +2119,17 @@ class Playwright extends Helper { const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties); const elemAmount = res.length; - const commands = []; let props = []; for (const element of res) { - const cssProperties = await element.evaluate((el) => getComputedStyle(el)); - - Object.keys(cssPropertiesCamelCase).forEach(prop => { + for (const prop of Object.keys(cssProperties)) { + const cssProp = await this.grabCssPropertyFrom(locator, prop); if (isColorProperty(prop)) { - props.push(convertColorToRGBA(cssProperties[prop])); + props.push(convertColorToRGBA(cssProp)); } else { - props.push(cssProperties[prop]); + props.push(cssProp); } - }); + } } const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]); @@ -2139,9 +2137,8 @@ class Playwright extends Helper { let chunked = chunkArray(props, values.length); chunked = chunked.filter((val) => { for (let i = 0; i < val.length; ++i) { - const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10); - const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10); - if (_acutal !== _expected) return false; + // eslint-disable-next-line eqeqeq + if (val[i] != values[i]) return false; } return true; }); diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index e94b40817..41f2fd44a 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -1770,31 +1770,26 @@ class Puppeteer extends Helper { const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties); const elemAmount = res.length; - const commands = []; - res.forEach((el) => { - Object.keys(cssPropertiesCamelCase).forEach((prop) => { - commands.push(el.executionContext() - .evaluate((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 = await Promise.all(commands); + let props = []; + + for (const element of res) { + for (const prop of Object.keys(cssProperties)) { + const cssProp = await this.grabCssPropertyFrom(locator, prop); + if (isColorProperty(prop)) { + props.push(convertColorToRGBA(cssProp)); + } else { + props.push(cssProp); + } + } + } + const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]); if (!Array.isArray(props)) props = [props]; let chunked = chunkArray(props, values.length); chunked = chunked.filter((val) => { for (let i = 0; i < val.length; ++i) { - const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10); - const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10); - if (_acutal !== _expected) return false; + // eslint-disable-next-line eqeqeq + if (val[i] != values[i]) return false; } return true; }); diff --git a/package.json b/package.json index 90f6bf6a6..b891e1b7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeceptjs", - "version": "3.5.8", + "version": "3.5.9", "description": "Supercharged End 2 End Testing Framework for NodeJS", "keywords": [ "acceptance", @@ -59,7 +59,8 @@ "publish:site": "./runok.js publish:site", "update-contributor-faces": "./runok.js contributor:faces", "dtslint": "dtslint typings --localTs './node_modules/typescript/lib'", - "prepare": "husky install" + "prepare": "husky install", + "prepare-release": "./runok.js versioning && ./runok.js get:commit-log" }, "dependencies": { "@codeceptjs/configure": "0.10.0", diff --git a/runok.js b/runok.js index 8e600bd44..db25dc2e9 100755 --- a/runok.js +++ b/runok.js @@ -429,14 +429,14 @@ title: ${name} const changelog = fs.readFileSync(file).toString(); const _changelog = `## ${newVersion}\n -Thanks all to those who contributed to make this release! +❤ī¸ Thanks all to those who contributed to make this release! ❤ī¸ + +🛩ī¸ *Features* 🐛 *Bug Fixes* 📖 *Documentation* -🛩ī¸ *Features* - ${changelog}`; fs.writeFileSync(`./${file}`, _changelog); @@ -448,7 +448,7 @@ ${changelog}`; async getCommitLog() { console.log('Gathering commits...'); - const logs = await exec('git log --pretty=\'format:%s - by %aN\' $(git describe --abbrev=0 --tags)..HEAD'); + const logs = await exec('git log --pretty=\'format:* %s - by @%aN\' $(git describe --abbrev=0 --tags)..HEAD'); console.log(logs.data.stdout); }, diff --git a/test/helper/Playwright_test.js b/test/helper/Playwright_test.js index 7f7c6221f..3ed3cdc89 100644 --- a/test/helper/Playwright_test.js +++ b/test/helper/Playwright_test.js @@ -33,6 +33,7 @@ describe('Playwright', function () { I = new Playwright({ url: siteUrl, windowSize: '500x700', + browser: process.env.BROWSER || 'chromium', show: false, waitForTimeout: 5000, waitForAction: 500, @@ -112,6 +113,17 @@ describe('Playwright', function () { }); }); + describe('#seeCssPropertiesOnElements', () => { + it('should check background-color css property for given element', async () => { + try { + await I.amOnPage('https://codecept.io/helpers/Playwright/'); + await I.seeCssPropertiesOnElements('.navbar', { 'background-color': 'rgb(128, 90, 213)' }); + } catch (e) { + e.message.should.include('expected element (.navbar) to have CSS property { \'background-color\': \'rgb(128, 90, 213)\' }'); + } + }); + }); + webApiTests.tests(); describe('#click', () => { @@ -1051,6 +1063,7 @@ describe('Playwright', function () { describe('#startRecordingWebSocketMessages, #grabWebSocketMessages, #stopRecordingWebSocketMessages', () => { it('should throw error when calling grabWebSocketMessages before startRecordingWebSocketMessages', () => { + if (process.env.BROWSER === 'firefox') this.skip(); try { I.amOnPage('https://websocketstest.com/'); I.waitForText('Work for You!'); @@ -1061,6 +1074,7 @@ describe('Playwright', function () { }); it('should flush the WS messages', async () => { + if (process.env.BROWSER === 'firefox') this.skip(); await I.startRecordingWebSocketMessages(); I.amOnPage('https://websocketstest.com/'); I.waitForText('Work for You!'); @@ -1070,6 +1084,7 @@ describe('Playwright', function () { }); it('should see recording WS messages', async () => { + if (process.env.BROWSER === 'firefox') this.skip(); await I.startRecordingWebSocketMessages(); await I.amOnPage('https://websocketstest.com/'); I.waitForText('Work for You!'); @@ -1078,6 +1093,7 @@ describe('Playwright', function () { }); it('should not see recording WS messages', async () => { + if (process.env.BROWSER === 'firefox') this.skip(); await I.startRecordingWebSocketMessages(); await I.amOnPage('https://websocketstest.com/'); I.waitForText('Work for You!'); diff --git a/test/helper/webapi.js b/test/helper/webapi.js index 3c9ea7b1c..39f7cdfdd 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -1399,7 +1399,7 @@ module.exports.tests = function () { }); it('should check css property for several elements', async function () { - if (isHelper('TestCafe')) this.skip(); + if (isHelper('TestCafe') || process.env.BROWSER === 'firefox') this.skip(); try { await I.amOnPage('/');