From f1acf77dcafbc95e6290813a4011ec5cb7e9e290 Mon Sep 17 00:00:00 2001 From: KobeN <7845001+kobenguyent@users.noreply.github.com> Date: Wed, 7 Feb 2024 05:52:08 +0100 Subject: [PATCH] feat(webdriver): network traffics manipulation (#4166) --- docs/helpers/Playwright.md | 39 +- docs/helpers/WebDriver.md | 339 ++++++++++++------ docs/webapi/dontSeeTraffic.mustache | 13 + docs/webapi/flushNetworkTraffics.mustache | 5 + .../grabRecordedNetworkTraffics.mustache | 10 + docs/webapi/seeTraffic.mustache | 36 ++ docs/webapi/startRecordingTraffic.mustache | 8 + docs/webapi/stopRecordingTraffic.mustache | 5 + lib/helper/Playwright.js | 212 +---------- lib/helper/WebDriver.js | 264 +++++++++++++- lib/helper/networkTraffics/utils.js | 137 +++++++ test/helper/WebDriver_devtools_test.js | 131 +++++++ typings/tests/helpers/Playwright.types.ts | 2 +- 13 files changed, 864 insertions(+), 337 deletions(-) create mode 100644 docs/webapi/dontSeeTraffic.mustache create mode 100644 docs/webapi/flushNetworkTraffics.mustache create mode 100644 docs/webapi/grabRecordedNetworkTraffics.mustache create mode 100644 docs/webapi/seeTraffic.mustache create mode 100644 docs/webapi/startRecordingTraffic.mustache create mode 100644 docs/webapi/stopRecordingTraffic.mustache create mode 100644 lib/helper/networkTraffics/utils.js diff --git a/docs/helpers/Playwright.md b/docs/helpers/Playwright.md index 5337c18f1..591c6cec7 100644 --- a/docs/helpers/Playwright.md +++ b/docs/helpers/Playwright.md @@ -805,6 +805,8 @@ I.dontSeeTraffic({ name: 'Unexpected API Call of "user" endpoint', url: /api.exa - `opts.name` **[string][9]** A name of that request. Can be any value. Only relevant to have a more meaningful error message in case of fail. - `opts.url` **([string][9] | [RegExp][11])** Expected URL of request in network traffic. Can be a string or a regular expression. +Returns **void** automatically synchronized promise through #recorder + ### doubleClick Performs a double-click on an element matched by link|button|label|CSS or XPath. @@ -921,6 +923,10 @@ Returns **void** automatically synchronized promise through #recorder Resets all recorded network requests. +```js +I.flushNetworkTraffics(); +``` + ### flushWebSocketMessages Resets all recorded WS messages. @@ -1309,7 +1315,7 @@ expect(traffics[0].response.status).to.equal(200); expect(traffics[0].response.body).to.contain({ name: 'this was mocked' }); ``` -Returns **[Promise][22]<[Array][10]<any>>** +Returns **[Array][10]** recorded network traffics ### grabSource @@ -2096,13 +2102,13 @@ Verifies that a certain request is part of network traffic. I.amOnPage('https://openai.com/blog/chatgpt'); I.startRecordingTraffic(); await I.seeTraffic({ - name: 'sentry event', - url: 'https://images.openai.com/blob/cf717bdb-0c8c-428a-b82b-3c3add87a600', - parameters: { - width: '1919', - height: '1138', + name: 'sentry event', + url: 'https://images.openai.com/blob/cf717bdb-0c8c-428a-b82b-3c3add87a600', + parameters: { + width: '1919', + height: '1138', }, -}); + }); ``` ```js @@ -2110,12 +2116,12 @@ await I.seeTraffic({ I.amOnPage('https://openai.com/blog/chatgpt'); I.startRecordingTraffic(); await I.seeTraffic({ - name: 'event', - url: 'https://cloudflareinsights.com/cdn-cgi/rum', - requestPostData: { - st: 2, + name: 'event', + url: 'https://cloudflareinsights.com/cdn-cgi/rum', + requestPostData: { + st: 2, }, -}); + }); ``` #### Parameters @@ -2127,7 +2133,7 @@ await I.seeTraffic({ - `opts.requestPostData` **[Object][6]?** Expected that request contains post data in network traffic - `opts.timeout` **[number][20]?** Timeout to wait for request in seconds. Default is 10 seconds. -Returns **[Promise][22]<any>** +Returns **void** automatically synchronized promise through #recorder ### selectOption @@ -2195,15 +2201,12 @@ I.setPlaywrightRequestHeaders({ ### startRecordingTraffic -Starts recording the network traffics. -This also resets recorded network requests. +Resets all recorded network requests. ```js -I.startRecordingTraffic(); +I.flushNetworkTraffics(); ``` -Returns **void** - ### startRecordingWebSocketMessages Starts recording of websocket messages. diff --git a/docs/helpers/WebDriver.md b/docs/helpers/WebDriver.md index cbc9c2f29..d00e5ef29 100644 --- a/docs/helpers/WebDriver.md +++ b/docs/helpers/WebDriver.md @@ -37,24 +37,24 @@ Type: [object][17] - `browser` **[string][18]** Browser in which to perform testing. - `basicAuth` **[string][18]?** (optional) the basic authentication to pass to base url. Example: {username: 'username', password: 'password'} - `host` **[string][18]?** WebDriver host to connect. -- `port` **[number][23]?** WebDriver port to connect. +- `port` **[number][24]?** WebDriver port to connect. - `protocol` **[string][18]?** protocol for WebDriver server. - `path` **[string][18]?** path to WebDriver server. -- `restart` **[boolean][33]?** restart browser between tests. -- `smartWait` **([boolean][33] | [number][23])?** **enables [SmartWait][37]**; wait for additional milliseconds for element to appear. Enable for 5 secs: "smartWait": 5000. -- `disableScreenshots` **[boolean][33]?** don't save screenshots on failure. -- `fullPageScreenshots` **[boolean][33]?** (optional - make full page screenshots on failure. -- `uniqueScreenshotNames` **[boolean][33]?** option to prevent screenshot override if you have scenarios with the same name in different suites. -- `keepBrowserState` **[boolean][33]?** keep browser state between tests when `restart` is set to false. -- `keepCookies` **[boolean][33]?** keep cookies between tests when `restart` set to false. +- `restart` **[boolean][34]?** restart browser between tests. +- `smartWait` **([boolean][34] | [number][24])?** **enables [SmartWait][38]**; wait for additional milliseconds for element to appear. Enable for 5 secs: "smartWait": 5000. +- `disableScreenshots` **[boolean][34]?** don't save screenshots on failure. +- `fullPageScreenshots` **[boolean][34]?** (optional - make full page screenshots on failure. +- `uniqueScreenshotNames` **[boolean][34]?** option to prevent screenshot override if you have scenarios with the same name in different suites. +- `keepBrowserState` **[boolean][34]?** keep browser state between tests when `restart` is set to false. +- `keepCookies` **[boolean][34]?** keep cookies between tests when `restart` set to false. - `windowSize` **[string][18]?** default window size. Set to `maximize` or a dimension in the format `640x480`. -- `waitForTimeout` **[number][23]?** sets default wait time in _ms_ for all `wait*` functions. +- `waitForTimeout` **[number][24]?** sets default wait time in _ms_ for all `wait*` functions. - `desiredCapabilities` **[object][17]?** Selenium's [desired capabilities][7]. -- `manualStart` **[boolean][33]?** do not start browser before a test, start it manually inside a helper with `this.helpers["WebDriver"]._startBrowser()`. -- `timeouts` **[object][17]?** [WebDriver timeouts][38] defined as hash. -- `highlightElement` **[boolean][33]?** highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose). -- `logLevel` **[string][18]?** level of logging verbosity. Default: silent. Options: trace | debug | info | warn | error | silent. More info: [https://webdriver.io/docs/configuration/#loglevel][39] -- `devtoolsProtocol` **[boolean][33]?** enable devtools protocol. Default: false. More info: [https://webdriver.io/docs/automationProtocols/#devtools-protocol][40]. +- `manualStart` **[boolean][34]?** do not start browser before a test, start it manually inside a helper with `this.helpers["WebDriver"]._startBrowser()`. +- `timeouts` **[object][17]?** [WebDriver timeouts][39] defined as hash. +- `highlightElement` **[boolean][34]?** highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose). +- `logLevel` **[string][18]?** level of logging verbosity. Default: silent. Options: trace | debug | info | warn | error | silent. More info: [https://webdriver.io/docs/configuration/#loglevel][40] +- `devtoolsProtocol` **[boolean][34]?** enable devtools protocol. Default: false. More info: [https://webdriver.io/docs/automationProtocols/#devtools-protocol][41]. @@ -856,6 +856,27 @@ I.dontSeeInTitle('Error'); Returns **void** automatically synchronized promise through #recorder +### dontSeeTraffic + +_Note:_ Only works when devtoolsProtocol is enabled. + +Verifies that a certain request is not part of network traffic. + +Examples: + +```js +I.dontSeeTraffic({ name: 'Unexpected API Call', url: 'https://api.example.com' }); +I.dontSeeTraffic({ name: 'Unexpected API Call of "user" endpoint', url: /api.example.com.*user/ }); +``` + +#### Parameters + +- `opts` **[Object][17]** options when checking the traffic network. + - `opts.name` **[string][18]** A name of that request. Can be any value. Only relevant to have a more meaningful error message in case of fail. + - `opts.url` **([string][18] | [RegExp][23])** Expected URL of request in network traffic. Can be a string or a regular expression. + +Returns **void** automatically synchronized promise through #recorder + ### doubleClick Performs a double-click on an element matched by link|button|label|CSS or XPath. @@ -908,7 +929,7 @@ I.dragSlider('#slider', -70); #### Parameters - `locator` **([string][18] | [object][17])** located by label|name|CSS|XPath|strict locator. -- `offsetX` **[number][23]** position to drag. +- `offsetX` **[number][24]** position to drag. Returns **void** automatically synchronized promise through #recorder @@ -917,7 +938,7 @@ Returns **void** automatically synchronized promise through #recorder Executes async script on page. Provided function should execute a passed callback (as first argument) to signal it is finished. -Example: In Vue.js to make components completely rendered we are waiting for [nextTick][24]. +Example: In Vue.js to make components completely rendered we are waiting for [nextTick][25]. ```js I.executeAsyncScript(function(done) { @@ -938,13 +959,13 @@ let val = await I.executeAsyncScript(function(url, done) { #### Parameters - `args` **...any** to be passed to function. -- `fn` **([string][18] | [function][25])** function to be executed in browser context. +- `fn` **([string][18] | [function][26])** function to be executed in browser context. -Returns **[Promise][26]<any>** script return value +Returns **[Promise][27]<any>** script return value ### executeScript -Wraps [execute][27] command. +Wraps [execute][28] command. Executes sync script on a page. Pass arguments to function as additional parameters. @@ -973,9 +994,9 @@ let date = await I.executeScript(function(el) { #### Parameters - `args` **...any** to be passed to function. -- `fn` **([string][18] | [function][25])** function to be executed in browser context. +- `fn` **([string][18] | [function][26])** function to be executed in browser context. -Returns **[Promise][26]<any>** script return value +Returns **[Promise][27]<any>** script return value ### fillField @@ -1005,6 +1026,16 @@ This action supports [React locators](https://codecept.io/react#locators) {{ custom }} +### flushNetworkTraffics + +_Note:_ Only works when devtoolsProtocol is enabled. + +Resets all recorded network requests. + +```js +I.flushNetworkTraffics(); +``` + ### focus Calls [focus][20] on the matching element. @@ -1020,7 +1051,7 @@ I.see('#add-to-cart-bnt'); #### Parameters - `locator` **([string][18] | [object][17])** field located by label|name|CSS|XPath|strict locator. -- `options` **any?** Playwright only: [Additional options][28] for available options object as 2nd argument. +- `options` **any?** Playwright only: [Additional options][29] for available options object as 2nd argument. Returns **void** automatically synchronized promise through #recorder @@ -1099,7 +1130,7 @@ Useful for referencing a specific handle when calling `I.switchToWindow(handle)` const windows = await I.grabAllWindowHandles(); ``` -Returns **[Promise][26]<[Array][29]<[string][18]>>** +Returns **[Promise][27]<[Array][30]<[string][18]>>** ### grabAttributeFrom @@ -1116,7 +1147,7 @@ let hint = await I.grabAttributeFrom('#tooltip', 'title'); - `locator` **([string][18] | [object][17])** element located by CSS|XPath|strict locator. - `attr` **[string][18]** attribute name. -Returns **[Promise][26]<[string][18]>** attribute value +Returns **[Promise][27]<[string][18]>** attribute value ### grabAttributeFromAll @@ -1132,7 +1163,7 @@ let hints = await I.grabAttributeFromAll('.tooltip', 'title'); - `locator` **([string][18] | [object][17])** element located by CSS|XPath|strict locator. - `attr` **[string][18]** attribute name. -Returns **[Promise][26]<[Array][29]<[string][18]>>** attribute value +Returns **[Promise][27]<[Array][30]<[string][18]>>** attribute value ### grabBrowserLogs @@ -1144,7 +1175,7 @@ let logs = await I.grabBrowserLogs(); console.log(JSON.stringify(logs)) ``` -Returns **([Promise][26]<[Array][29]<[object][17]>> | [undefined][30])** all browser logs +Returns **([Promise][27]<[Array][30]<[object][17]>> | [undefined][31])** all browser logs ### grabCookie @@ -1178,7 +1209,7 @@ const value = await I.grabCssPropertyFrom('h3', 'font-weight'); - `locator` **([string][18] | [object][17])** element located by CSS|XPath|strict locator. - `cssProperty` **[string][18]** CSS property name. -Returns **[Promise][26]<[string][18]>** CSS value +Returns **[Promise][27]<[string][18]>** CSS value ### grabCssPropertyFromAll @@ -1194,7 +1225,7 @@ const values = await I.grabCssPropertyFromAll('h3', 'font-weight'); - `locator` **([string][18] | [object][17])** element located by CSS|XPath|strict locator. - `cssProperty` **[string][18]** CSS property name. -Returns **[Promise][26]<[Array][29]<[string][18]>>** CSS value +Returns **[Promise][27]<[Array][30]<[string][18]>>** CSS value ### grabCurrentUrl @@ -1206,7 +1237,7 @@ let url = await I.grabCurrentUrl(); console.log(`Current URL is [${url}]`); ``` -Returns **[Promise][26]<[string][18]>** current URL +Returns **[Promise][27]<[string][18]>** current URL ### grabCurrentWindowHandle @@ -1217,7 +1248,7 @@ Useful for referencing it when calling `I.switchToWindow(handle)` const window = await I.grabCurrentWindowHandle(); ``` -Returns **[Promise][26]<[string][18]>** +Returns **[Promise][27]<[string][18]>** ### grabElementBoundingRect @@ -1245,7 +1276,7 @@ const width = await I.grabElementBoundingRect('h3', 'width'); - `prop` - `elementSize` **[string][18]?** x, y, width or height of the given element. -Returns **([Promise][26]<DOMRect> | [Promise][26]<[number][23]>)** Element bounding rectangle +Returns **([Promise][27]<DOMRect> | [Promise][27]<[number][24]>)** Element bounding rectangle ### grabGeoLocation @@ -1258,7 +1289,7 @@ Resumes test execution, so **should be used inside async function with `await`** let geoLocation = await I.grabGeoLocation(); ``` -Returns **[Promise][26]<{latitude: [number][23], longitude: [number][23], altitude: [number][23]}>** +Returns **[Promise][27]<{latitude: [number][24], longitude: [number][24], altitude: [number][24]}>** ### grabHTMLFrom @@ -1275,7 +1306,7 @@ let postHTML = await I.grabHTMLFrom('#post'); - `locator` - `element` **([string][18] | [object][17])** located by CSS|XPath|strict locator. -Returns **[Promise][26]<[string][18]>** HTML code for an element +Returns **[Promise][27]<[string][18]>** HTML code for an element ### grabHTMLFromAll @@ -1291,7 +1322,7 @@ let postHTMLs = await I.grabHTMLFromAll('.post'); - `locator` - `element` **([string][18] | [object][17])** located by CSS|XPath|strict locator. -Returns **[Promise][26]<[Array][29]<[string][18]>>** HTML code for an element +Returns **[Promise][27]<[Array][30]<[string][18]>>** HTML code for an element ### grabNumberOfOpenTabs @@ -1302,7 +1333,7 @@ Resumes test execution, so **should be used inside async function with `await`** let tabs = await I.grabNumberOfOpenTabs(); ``` -Returns **[Promise][26]<[number][23]>** number of open tabs +Returns **[Promise][27]<[number][24]>** number of open tabs ### grabNumberOfVisibleElements @@ -1317,7 +1348,7 @@ let numOfElements = await I.grabNumberOfVisibleElements('p'); - `locator` **([string][18] | [object][17])** located by CSS|XPath|strict locator. -Returns **[Promise][26]<[number][23]>** number of visible elements +Returns **[Promise][27]<[number][24]>** number of visible elements ### grabPageScrollPosition @@ -1328,7 +1359,7 @@ Resumes test execution, so **should be used inside an async function with `await let { x, y } = await I.grabPageScrollPosition(); ``` -Returns **[Promise][26]<PageScrollPosition>** scroll position +Returns **[Promise][27]<PageScrollPosition>** scroll position ### grabPopupText @@ -1338,7 +1369,22 @@ Grab the text within the popup. If no popup is visible then it will return null. await I.grabPopupText(); ``` -Returns **[Promise][26]<[string][18]>** +Returns **[Promise][27]<[string][18]>** + +### grabRecordedNetworkTraffics + +_Note:_ Only works when devtoolsProtocol is enabled. + +Grab the recording network traffics + +```js +const traffics = await I.grabRecordedNetworkTraffics(); +expect(traffics[0].url).to.equal('https://reqres.in/api/comments/1'); +expect(traffics[0].response.status).to.equal(200); +expect(traffics[0].response.body).to.contain({ name: 'this was mocked' }); +``` + +Returns **[Array][30]** recorded network traffics ### grabSource @@ -1349,7 +1395,7 @@ Resumes test execution, so **should be used inside async function with `await`** let pageSource = await I.grabSource(); ``` -Returns **[Promise][26]<[string][18]>** source code +Returns **[Promise][27]<[string][18]>** source code ### grabTextFrom @@ -1366,7 +1412,7 @@ If multiple elements found returns first element. - `locator` **([string][18] | [object][17])** element located by CSS|XPath|strict locator. -Returns **[Promise][26]<[string][18]>** attribute value +Returns **[Promise][27]<[string][18]>** attribute value ### grabTextFromAll @@ -1381,7 +1427,7 @@ let pins = await I.grabTextFromAll('#pin li'); - `locator` **([string][18] | [object][17])** element located by CSS|XPath|strict locator. -Returns **[Promise][26]<[Array][29]<[string][18]>>** attribute value +Returns **[Promise][27]<[Array][30]<[string][18]>>** attribute value ### grabTitle @@ -1392,7 +1438,7 @@ Resumes test execution, so **should be used inside async with `await`** operator let title = await I.grabTitle(); ``` -Returns **[Promise][26]<[string][18]>** title +Returns **[Promise][27]<[string][18]>** title ### grabValueFrom @@ -1408,7 +1454,7 @@ let email = await I.grabValueFrom('input[name=email]'); - `locator` **([string][18] | [object][17])** field located by label|name|CSS|XPath|strict locator. -Returns **[Promise][26]<[string][18]>** attribute value +Returns **[Promise][27]<[string][18]>** attribute value ### grabValueFromAll @@ -1423,7 +1469,7 @@ let inputs = await I.grabValueFromAll('//form/input'); - `locator` **([string][18] | [object][17])** field located by label|name|CSS|XPath|strict locator. -Returns **[Promise][26]<[Array][29]<[string][18]>>** attribute value +Returns **[Promise][27]<[Array][30]<[string][18]>>** attribute value ### grabWebElements @@ -1438,7 +1484,7 @@ const webElements = await I.grabWebElements('#button'); - `locator` **([string][18] | [object][17])** element located by CSS|XPath|strict locator. -Returns **[Promise][26]<any>** WebElement of being used Web helper +Returns **[Promise][27]<any>** WebElement of being used Web helper ### moveCursorTo @@ -1455,8 +1501,8 @@ I.moveCursorTo('#submit', 5,5); - `locator` **([string][18] | [object][17])** located by CSS|XPath|strict locator. - `xOffset` - `yOffset` -- `offsetX` **[number][23]** (optional, `0` by default) X-axis offset. -- `offsetY` **[number][23]** (optional, `0` by default) Y-axis offset. +- `offsetX` **[number][24]** (optional, `0` by default) X-axis offset. +- `offsetY` **[number][24]** (optional, `0` by default) Y-axis offset. Returns **void** automatically synchronized promise through #recorder @@ -1481,7 +1527,7 @@ _Note:_ In case a text field or textarea is focused be aware that some browsers Presses a key in the browser (on a focused element). -_Hint:_ For populating text field or textarea, it is recommended to use [`fillField`][31]. +_Hint:_ For populating text field or textarea, it is recommended to use [`fillField`][32]. ```js I.pressKey('Backspace'); @@ -1540,7 +1586,7 @@ Some of the supported key names are: #### Parameters -- `key` **([string][18] | [Array][29]<[string][18]>)** key or array of keys to press. +- `key` **([string][18] | [Array][30]<[string][18]>)** key or array of keys to press. Returns **void** automatically synchronized promise through #recorder @@ -1548,7 +1594,7 @@ Returns **void** automatically synchronized promise through #recorder Presses a key in the browser and leaves it in a down state. -To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][32]). +To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][33]). ```js I.pressKeyDown('Control'); @@ -1566,7 +1612,7 @@ Returns **void** automatically synchronized promise through #recorder Releases a key in the browser which was previously set to a down state. -To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][32]). +To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][33]). ```js I.pressKeyDown('Control'); @@ -1599,8 +1645,8 @@ First parameter can be set to `maximize`. #### Parameters -- `width` **[number][23]** width in pixels or `maximize`. -- `height` **[number][23]** height in pixels. +- `width` **[number][24]** width in pixels or `maximize`. +- `height` **[number][24]** height in pixels. Returns **void** automatically synchronized promise through #recorder @@ -1684,7 +1730,7 @@ I.saveScreenshot('debug.png', true) //resizes to available scrollHeight and scro #### Parameters - `fileName` **[string][18]** file name to save. -- `fullPage` **[boolean][33]** (optional, `false` by default) flag to enable fullscreen screenshot mode. +- `fullPage` **[boolean][34]** (optional, `false` by default) flag to enable fullscreen screenshot mode. Returns **void** automatically synchronized promise through #recorder @@ -1701,7 +1747,7 @@ I.scrollIntoView('#submit', { behavior: "smooth", block: "center", inline: "cent #### Parameters - `locator` **([string][18] | [object][17])** located by CSS|XPath|strict locator. -- `scrollIntoViewOptions` **(ScrollIntoViewOptions | [boolean][33])** either alignToTop=true|false or scrollIntoViewOptions. See [https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView][34]. +- `scrollIntoViewOptions` **(ScrollIntoViewOptions | [boolean][34])** either alignToTop=true|false or scrollIntoViewOptions. See [https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView][35]. Returns **void** automatically synchronized promise through #recorder @@ -1738,8 +1784,8 @@ I.scrollTo('#submit', 5, 5); #### Parameters - `locator` **([string][18] | [object][17])** located by CSS|XPath|strict locator. -- `offsetX` **[number][23]** (optional, `0` by default) X-axis offset. -- `offsetY` **[number][23]** (optional, `0` by default) Y-axis offset. +- `offsetX` **[number][24]** (optional, `0` by default) X-axis offset. +- `offsetY` **[number][24]** (optional, `0` by default) Y-axis offset. Returns **void** automatically synchronized promise through #recorder @@ -1959,7 +2005,7 @@ I.seeNumberOfElements('#submitBtn', 1); #### Parameters - `locator` **([string][18] | [object][17])** element located by CSS|XPath|strict locator. -- `num` **[number][23]** number of elements. +- `num` **[number][24]** number of elements. Returns **void** automatically synchronized promise through #recorder @@ -1979,7 +2025,7 @@ I.seeNumberOfVisibleElements('.buttons', 3); #### Parameters - `locator` **([string][18] | [object][17])** element located by CSS|XPath|strict locator. -- `num` **[number][23]** number of elements. +- `num` **[number][24]** number of elements. Returns **void** automatically synchronized promise through #recorder @@ -2016,6 +2062,50 @@ I.seeTitleEquals('Test title.'); Returns **void** automatically synchronized promise through #recorder +### seeTraffic + +_Note:_ Only works when devtoolsProtocol is enabled. + +Verifies that a certain request is part of network traffic. + +```js +// checking the request url contains certain query strings +I.amOnPage('https://openai.com/blog/chatgpt'); +I.startRecordingTraffic(); +await I.seeTraffic({ + name: 'sentry event', + url: 'https://images.openai.com/blob/cf717bdb-0c8c-428a-b82b-3c3add87a600', + parameters: { + width: '1919', + height: '1138', + }, + }); +``` + +```js +// checking the request url contains certain post data +I.amOnPage('https://openai.com/blog/chatgpt'); +I.startRecordingTraffic(); +await I.seeTraffic({ + name: 'event', + url: 'https://cloudflareinsights.com/cdn-cgi/rum', + requestPostData: { + st: 2, + }, + }); +``` + +#### Parameters + +- `opts` **[Object][17]** options when checking the traffic network. + - `opts.name` **[string][18]** A name of that request. Can be any value. Only relevant to have a more meaningful error message in case of fail. + - `opts.url` **[string][18]** Expected URL of request in network traffic + - `opts.parameters` **[Object][17]?** Expected parameters of that request in network traffic + - `opts.requestPostData` **[Object][17]?** Expected that request contains post data in network traffic + - `opts.timeout` **[number][24]?** Timeout to wait for request in seconds. Default is 10 seconds. + +Returns **void** automatically synchronized promise through #recorder + ### selectOption Selects an option in a drop-down select. @@ -2040,13 +2130,13 @@ I.selectOption('Which OS do you use?', ['Android', 'iOS']); #### Parameters - `select` **([string][18] | [object][17])** field located by label|name|CSS|XPath|strict locator. -- `option` **([string][18] | [Array][29]<any>)** visible text or value of option. +- `option` **([string][18] | [Array][30]<any>)** visible text or value of option. Returns **void** automatically synchronized promise through #recorder ### setCookie -Uses Selenium's JSON [cookie format][35]. +Uses Selenium's JSON [cookie format][36]. Sets cookie(s). Can be a single cookie object or an array of cookies: @@ -2063,7 +2153,7 @@ I.setCookie([ #### Parameters -- `cookie` **(Cookie | [Array][29]<Cookie>)** a cookie object or array of cookie objects. +- `cookie` **(Cookie | [Array][30]<Cookie>)** a cookie object or array of cookie objects. Returns **void** automatically synchronized promise through #recorder @@ -2080,12 +2170,35 @@ I.setGeoLocation(121.21, 11.56, 10); #### Parameters -- `latitude` **[number][23]** to set. -- `longitude` **[number][23]** to set -- `altitude` **[number][23]?** (optional, null by default) to set +- `latitude` **[number][24]** to set. +- `longitude` **[number][24]** to set +- `altitude` **[number][24]?** (optional, null by default) to set Returns **void** automatically synchronized promise through #recorder +### startRecordingTraffic + +_Note:_ Only works when devtoolsProtocol is enabled. + +Starts recording the network traffics. +This also resets recorded network requests. + +```js +I.startRecordingTraffic(); +``` + +Returns **void** automatically synchronized promise through #recorder + +### stopRecordingTraffic + +_Note:_ Only works when devtoolsProtocol is enabled. + +Stops recording of network traffic. Recorded traffic is not flashed. + +```js +I.stopRecordingTraffic(); +``` + ### switchTo Switches frame or in case of null locator reverts to parent. @@ -2112,8 +2225,8 @@ I.switchToNextTab(2); #### Parameters -- `num` **[number][23]?** (optional) number of tabs to switch forward, default: 1. -- `sec` **([number][23] | null)?** (optional) time in seconds to wait. +- `num` **[number][24]?** (optional) number of tabs to switch forward, default: 1. +- `sec` **([number][24] | null)?** (optional) time in seconds to wait. Returns **void** automatically synchronized promise through #recorder @@ -2128,8 +2241,8 @@ I.switchToPreviousTab(2); #### Parameters -- `num` **[number][23]?** (optional) number of tabs to switch backward, default: 1. -- `sec` **[number][23]??** (optional) time in seconds to wait. +- `num` **[number][24]?** (optional) number of tabs to switch backward, default: 1. +- `sec` **[number][24]??** (optional) time in seconds to wait. Returns **void** automatically synchronized promise through #recorder @@ -2155,7 +2268,7 @@ await I.switchToWindow( window ); Types out the given text into an active field. To slow down typing use a second parameter, to set interval between key presses. -_Note:_ Should be used when [`fillField`][31] is not an option. +_Note:_ Should be used when [`fillField`][32] is not an option. ```js // passing in a string @@ -2174,8 +2287,8 @@ I.type(secret('123456')); #### Parameters - `keys` -- `delay` **[number][23]?** (optional) delay in ms between key presses -- `key` **([string][18] | [Array][29]<[string][18]>)** or array of keys to type. +- `delay` **[number][24]?** (optional) delay in ms between key presses +- `key` **([string][18] | [Array][30]<[string][18]>)** or array of keys to type. Returns **void** automatically synchronized promise through #recorder @@ -2202,12 +2315,12 @@ Returns **void** automatically synchronized promise through #recorder ### useWebDriverTo -Use [webdriverio][36] API inside a test. +Use [webdriverio][37] API inside a test. First argument is a description of an action. Second argument is async function that gets this helper as parameter. -{ [`browser`][36]) } object from WebDriver API is available. +{ [`browser`][37]) } object from WebDriver API is available. ```js I.useWebDriverTo('open multiple windows', async ({ browser }) { @@ -2219,7 +2332,7 @@ I.useWebDriverTo('open multiple windows', async ({ browser }) { #### Parameters - `description` **[string][18]** used to show in logs. -- `fn` **[function][25]** async functuion that executed with WebDriver helper as argument +- `fn` **[function][26]** async functuion that executed with WebDriver helper as argument ### wait @@ -2231,7 +2344,7 @@ I.wait(2); // wait 2 secs #### Parameters -- `sec` **[number][23]** number of second to wait. +- `sec` **[number][24]** number of second to wait. Returns **void** automatically synchronized promise through #recorder @@ -2249,7 +2362,7 @@ I.waitForClickable('.btn.continue', 5); // wait for 5 secs - `locator` **([string][18] | [object][17])** element located by CSS|XPath|strict locator. - `waitTimeout` -- `sec` **[number][23]?** (optional, `1` by default) time in seconds to wait +- `sec` **[number][24]?** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2280,7 +2393,7 @@ I.waitForDetached('#popup'); #### Parameters - `locator` **([string][18] | [object][17])** element located by CSS|XPath|strict locator. -- `sec` **[number][23]** (optional, `1` by default) time in seconds to wait +- `sec` **[number][24]** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2297,7 +2410,7 @@ I.waitForElement('.btn.continue', 5); // wait for 5 secs #### Parameters - `locator` **([string][18] | [object][17])** element located by CSS|XPath|strict locator. -- `sec` **[number][23]?** (optional, `1` by default) time in seconds to wait +- `sec` **[number][24]?** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2309,7 +2422,7 @@ Element can be located by CSS or XPath. #### Parameters - `locator` **([string][18] | [object][17])** element located by CSS|XPath|strict locator. -- `sec` **[number][23]** (optional) time in seconds to wait, 1 by default. +- `sec` **[number][24]** (optional) time in seconds to wait, 1 by default. Returns **void** automatically synchronized promise through #recorder @@ -2330,9 +2443,9 @@ I.waitForFunction((count) => window.requests == count, [3], 5) // pass args and #### Parameters -- `fn` **([string][18] | [function][25])** to be executed in browser context. -- `argsOrSec` **([Array][29]<any> | [number][23])?** (optional, `1` by default) arguments for function or seconds. -- `sec` **[number][23]?** (optional, `1` by default) time in seconds to wait +- `fn` **([string][18] | [function][26])** to be executed in browser context. +- `argsOrSec` **([Array][30]<any> | [number][24])?** (optional, `1` by default) arguments for function or seconds. +- `sec` **[number][24]?** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2348,7 +2461,7 @@ I.waitForInvisible('#popup'); #### Parameters - `locator` **([string][18] | [object][17])** element located by CSS|XPath|strict locator. -- `sec` **[number][23]** (optional, `1` by default) time in seconds to wait +- `sec` **[number][24]** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2362,8 +2475,8 @@ I.waitForNumberOfTabs(2); #### Parameters -- `expectedTabs` **[number][23]** expecting the number of tabs. -- `sec` **[number][23]** number of secs to wait. +- `expectedTabs` **[number][24]** expecting the number of tabs. +- `sec` **[number][24]** number of secs to wait. Returns **void** automatically synchronized promise through #recorder @@ -2381,7 +2494,7 @@ I.waitForText('Thank you, form has been submitted', 5, '#modal'); #### Parameters - `text` **[string][18]** to wait for. -- `sec` **[number][23]** (optional, `1` by default) time in seconds to wait +- `sec` **[number][24]** (optional, `1` by default) time in seconds to wait - `context` **([string][18] | [object][17])?** (optional) element located by CSS|XPath|strict locator. Returns **void** automatically synchronized promise through #recorder @@ -2398,7 +2511,7 @@ I.waitForValue('//input', "GoodValue"); - `field` **([string][18] | [object][17])** input field. - `value` **[string][18]** expected value. -- `sec` **[number][23]** (optional, `1` by default) time in seconds to wait +- `sec` **[number][24]** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2414,7 +2527,7 @@ I.waitForVisible('#popup'); #### Parameters - `locator` **([string][18] | [object][17])** element located by CSS|XPath|strict locator. -- `sec` **[number][23]** (optional, `1` by default) time in seconds to wait +- `sec` **[number][24]** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2429,7 +2542,7 @@ I.waitInUrl('/info', 2); #### Parameters - `urlPart` **[string][18]** value to check. -- `sec` **[number][23]** (optional, `1` by default) time in seconds to wait +- `sec` **[number][24]** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2444,8 +2557,8 @@ I.waitNumberOfVisibleElements('a', 3); #### Parameters - `locator` **([string][18] | [object][17])** element located by CSS|XPath|strict locator. -- `num` **[number][23]** number of elements. -- `sec` **[number][23]** (optional, `1` by default) time in seconds to wait +- `num` **[number][24]** number of elements. +- `sec` **[number][24]** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2461,7 +2574,7 @@ I.waitToHide('#popup'); #### Parameters - `locator` **([string][18] | [object][17])** element located by CSS|XPath|strict locator. -- `sec` **[number][23]** (optional, `1` by default) time in seconds to wait +- `sec` **[number][24]** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2477,7 +2590,7 @@ I.waitUrlEquals('http://127.0.0.1:8000/info'); #### Parameters - `urlPart` **[string][18]** value to check. -- `sec` **[number][23]** (optional, `1` by default) time in seconds to wait +- `sec` **[number][24]** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2525,38 +2638,40 @@ Returns **void** automatically synchronized promise through #recorder [22]: https://webdriver.io/docs/timeouts.html -[23]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number +[23]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp + +[24]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number -[24]: https://vuejs.org/v2/api/#Vue-nextTick +[25]: https://vuejs.org/v2/api/#Vue-nextTick -[25]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function +[26]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function -[26]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise +[27]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise -[27]: http://webdriver.io/api/protocol/execute.html +[28]: http://webdriver.io/api/protocol/execute.html -[28]: https://playwright.dev/docs/api/class-locator#locator-focus +[29]: https://playwright.dev/docs/api/class-locator#locator-focus -[29]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array +[30]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array -[30]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined +[31]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined -[31]: #fillfield +[32]: #fillfield -[32]: #click +[33]: #click -[33]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean +[34]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean -[34]: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView +[35]: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView -[35]: https://code.google.com/p/selenium/wiki/JsonWireProtocol#Cookie_JSON_Object +[36]: https://code.google.com/p/selenium/wiki/JsonWireProtocol#Cookie_JSON_Object -[36]: https://webdriver.io/docs/api.html +[37]: https://webdriver.io/docs/api.html -[37]: http://codecept.io/acceptance/#smartwait +[38]: http://codecept.io/acceptance/#smartwait -[38]: http://webdriver.io/docs/timeouts.html +[39]: http://webdriver.io/docs/timeouts.html -[39]: https://webdriver.io/docs/configuration/#loglevel +[40]: https://webdriver.io/docs/configuration/#loglevel -[40]: https://webdriver.io/docs/automationProtocols/#devtools-protocol +[41]: https://webdriver.io/docs/automationProtocols/#devtools-protocol diff --git a/docs/webapi/dontSeeTraffic.mustache b/docs/webapi/dontSeeTraffic.mustache new file mode 100644 index 000000000..e52aa283e --- /dev/null +++ b/docs/webapi/dontSeeTraffic.mustache @@ -0,0 +1,13 @@ +Verifies that a certain request is not part of network traffic. + +Examples: + +```js +I.dontSeeTraffic({ name: 'Unexpected API Call', url: 'https://api.example.com' }); +I.dontSeeTraffic({ name: 'Unexpected API Call of "user" endpoint', url: /api.example.com.*user/ }); +``` + +@param {Object} opts - options when checking the traffic network. +@param {string} opts.name A name of that request. Can be any value. Only relevant to have a more meaningful error message in case of fail. +@param {string|RegExp} opts.url Expected URL of request in network traffic. Can be a string or a regular expression. +@return {void} automatically synchronized promise through #recorder diff --git a/docs/webapi/flushNetworkTraffics.mustache b/docs/webapi/flushNetworkTraffics.mustache new file mode 100644 index 000000000..51082d041 --- /dev/null +++ b/docs/webapi/flushNetworkTraffics.mustache @@ -0,0 +1,5 @@ +Resets all recorded network requests. + +```js +I.flushNetworkTraffics(); +``` diff --git a/docs/webapi/grabRecordedNetworkTraffics.mustache b/docs/webapi/grabRecordedNetworkTraffics.mustache new file mode 100644 index 000000000..90cc824fb --- /dev/null +++ b/docs/webapi/grabRecordedNetworkTraffics.mustache @@ -0,0 +1,10 @@ +Grab the recording network traffics + +```js +const traffics = await I.grabRecordedNetworkTraffics(); +expect(traffics[0].url).to.equal('https://reqres.in/api/comments/1'); +expect(traffics[0].response.status).to.equal(200); +expect(traffics[0].response.body).to.contain({ name: 'this was mocked' }); +``` + +@return { Array } recorded network traffics diff --git a/docs/webapi/seeTraffic.mustache b/docs/webapi/seeTraffic.mustache new file mode 100644 index 000000000..54b314430 --- /dev/null +++ b/docs/webapi/seeTraffic.mustache @@ -0,0 +1,36 @@ +Verifies that a certain request is part of network traffic. + +```js +// checking the request url contains certain query strings +I.amOnPage('https://openai.com/blog/chatgpt'); +I.startRecordingTraffic(); +await I.seeTraffic({ + name: 'sentry event', + url: 'https://images.openai.com/blob/cf717bdb-0c8c-428a-b82b-3c3add87a600', + parameters: { + width: '1919', + height: '1138', + }, + }); +``` + +```js +// checking the request url contains certain post data +I.amOnPage('https://openai.com/blog/chatgpt'); +I.startRecordingTraffic(); +await I.seeTraffic({ + name: 'event', + url: 'https://cloudflareinsights.com/cdn-cgi/rum', + requestPostData: { + st: 2, + }, + }); +``` + +@param {Object} opts - options when checking the traffic network. +@param {string} opts.name A name of that request. Can be any value. Only relevant to have a more meaningful error message in case of fail. +@param {string} opts.url Expected URL of request in network traffic +@param {Object} [opts.parameters] Expected parameters of that request in network traffic +@param {Object} [opts.requestPostData] Expected that request contains post data in network traffic +@param {number} [opts.timeout] Timeout to wait for request in seconds. Default is 10 seconds. +@return {void} automatically synchronized promise through #recorder diff --git a/docs/webapi/startRecordingTraffic.mustache b/docs/webapi/startRecordingTraffic.mustache new file mode 100644 index 000000000..e408917fc --- /dev/null +++ b/docs/webapi/startRecordingTraffic.mustache @@ -0,0 +1,8 @@ +Starts recording the network traffics. +This also resets recorded network requests. + +```js +I.startRecordingTraffic(); +``` + +@returns {void} automatically synchronized promise through #recorder diff --git a/docs/webapi/stopRecordingTraffic.mustache b/docs/webapi/stopRecordingTraffic.mustache new file mode 100644 index 000000000..678284057 --- /dev/null +++ b/docs/webapi/stopRecordingTraffic.mustache @@ -0,0 +1,5 @@ +Stops recording of network traffic. Recorded traffic is not flashed. + +```js +I.stopRecordingTraffic(); +``` diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index cedc0a2fd..77dcfeb0d 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -51,6 +51,7 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright const { seeElementError, dontSeeElementError, dontSeeElementInDOMError, seeElementInDOMError, } = require('./errors/ElementAssertion'); +const { createAdvancedTestResults, allParameterValuePairsMatchExtreme, extractQueryObjects } = require('./networkTraffics/utils'); const { log } = require('../output'); const pathSeparator = path.sep; @@ -2976,14 +2977,8 @@ class Playwright extends Helper { } /** - * Starts recording the network traffics. - * This also resets recorded network requests. + * {{> flushNetworkTraffics }} * - * ```js - * I.startRecordingTraffic(); - * ``` - * - * @return {void} */ startRecordingTraffic() { this.flushNetworkTraffics(); @@ -3010,18 +3005,8 @@ class Playwright extends Helper { } /** - * Grab the recording network traffics - * - * ```js - * const traffics = await I.grabRecordedNetworkTraffics(); - * expect(traffics[0].url).to.equal('https://reqres.in/api/comments/1'); - * expect(traffics[0].response.status).to.equal(200); - * expect(traffics[0].response.body).to.contain({ name: 'this was mocked' }); - * ``` - * - * @return { Promise> } - * - */ + * {{> grabRecordedNetworkTraffics }} + */ async grabRecordedNetworkTraffics() { if (!this.recording || !this.recordedAtLeastOnce) { throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.'); @@ -3129,18 +3114,14 @@ class Playwright extends Helper { } /** - * Resets all recorded network requests. + * {{> flushNetworkTraffics }} */ flushNetworkTraffics() { this.requests = []; } /** - * Stops recording of network traffic. Recorded traffic is not flashed. - * - * ```js - * I.stopRecordingTraffic(); - * ``` + * {{> stopRecordingTraffic }} */ stopRecordingTraffic() { this.page.removeAllListeners('request'); @@ -3148,42 +3129,7 @@ class Playwright extends Helper { } /** - * Verifies that a certain request is part of network traffic. - * - * ```js - * // checking the request url contains certain query strings - * I.amOnPage('https://openai.com/blog/chatgpt'); - * I.startRecordingTraffic(); - * await I.seeTraffic({ - * name: 'sentry event', - * url: 'https://images.openai.com/blob/cf717bdb-0c8c-428a-b82b-3c3add87a600', - * parameters: { - * width: '1919', - * height: '1138', - * }, - * }); - * ``` - * - * ```js - * // checking the request url contains certain post data - * I.amOnPage('https://openai.com/blog/chatgpt'); - * I.startRecordingTraffic(); - * await I.seeTraffic({ - * name: 'event', - * url: 'https://cloudflareinsights.com/cdn-cgi/rum', - * requestPostData: { - * st: 2, - * }, - * }); - * ``` - * - * @param {Object} opts - options when checking the traffic network. - * @param {string} opts.name A name of that request. Can be any value. Only relevant to have a more meaningful error message in case of fail. - * @param {string} opts.url Expected URL of request in network traffic - * @param {Object} [opts.parameters] Expected parameters of that request in network traffic - * @param {Object} [opts.requestPostData] Expected that request contains post data in network traffic - * @param {number} [opts.timeout] Timeout to wait for request in seconds. Default is 10 seconds. - * @return { Promise<*> } + * {{> seeTraffic }} */ async seeTraffic({ name, url, parameters, requestPostData, timeout = 10, @@ -3265,18 +3211,7 @@ class Playwright extends Helper { } /** - * Verifies that a certain request is not part of network traffic. - * - * Examples: - * - * ```js - * I.dontSeeTraffic({ name: 'Unexpected API Call', url: 'https://api.example.com' }); - * I.dontSeeTraffic({ name: 'Unexpected API Call of "user" endpoint', url: /api.example.com.*user/ }); - * ``` - * - * @param {Object} opts - options when checking the traffic network. - * @param {string} opts.name A name of that request. Can be any value. Only relevant to have a more meaningful error message in case of fail. - * @param {string|RegExp} opts.url Expected URL of request in network traffic. Can be a string or a regular expression. + * {{> dontSeeTraffic }} * */ dontSeeTraffic({ name, url }) { @@ -3988,134 +3923,3 @@ async function highlightActiveElement(element) { }); } } - -const createAdvancedTestResults = (url, dataToCheck, requests) => { - // Creates advanced test results for a network traffic check. - // Advanced test results only applies when expected parameters are set - if (!dataToCheck) return ''; - - let urlFound = false; - let advancedResults; - requests.forEach((request) => { - // url not found in this request. continue with next request - if (urlFound || !request.url.match(new RegExp(url))) return; - urlFound = true; - - // Url found. Now we create advanced test report for that URL and show which parameters failed - if (!request.requestPostData) { - advancedResults = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), dataToCheck); - } else if (request.requestPostData) { - advancedResults = allRequestPostDataValuePairsMatchExtreme(request.requestPostData, dataToCheck); - } - }); - return advancedResults; -}; - -const extractQueryObjects = (queryString) => { - // Converts a string of GET parameters into an array of parameter objects. Each parameter object contains the properties "name" and "value". - if (queryString.indexOf('?') === -1) { - return []; - } - const queryObjects = []; - - const queryPart = queryString.split('?')[1]; - - const queryParameters = queryPart.split('&'); - - queryParameters.forEach((queryParameter) => { - const keyValue = queryParameter.split('='); - const queryObject = {}; - // eslint-disable-next-line prefer-destructuring - queryObject.name = keyValue[0]; - queryObject.value = decodeURIComponent(keyValue[1]); - queryObjects.push(queryObject); - }); - - return queryObjects; -}; - -const allParameterValuePairsMatchExtreme = (queryStringObject, advancedExpectedParameterValuePairs) => { - // More advanced check if all request parameters match with the expectations - let littleReport = '\nQuery parameters:\n'; - let success = true; - - for (const expectedKey in advancedExpectedParameterValuePairs) { - if (!Object.prototype.hasOwnProperty.call(advancedExpectedParameterValuePairs, expectedKey)) { - continue; - } - let parameterFound = false; - const expectedValue = advancedExpectedParameterValuePairs[expectedKey]; - - for (const queryParameter of queryStringObject) { - if (queryParameter.name === expectedKey) { - parameterFound = true; - if (expectedValue === undefined) { - littleReport += ` ${expectedKey.padStart(10, ' ')}\n`; - } else if (typeof expectedValue === 'object' && expectedValue.base64) { - const decodedActualValue = Buffer.from(queryParameter.value, 'base64').toString('utf8'); - if (decodedActualValue === expectedValue.base64) { - littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`; - } else { - littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`; - success = false; - } - } else if (queryParameter.value === expectedValue) { - littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`; - } else { - littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${queryParameter.value}"\n`; - success = false; - } - } - } - - if (parameterFound === false) { - littleReport += ` ✖ ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> parameter not found in request\n`; - success = false; - } - } - - return success ? true : littleReport; -}; - -const allRequestPostDataValuePairsMatchExtreme = (RequestPostDataObject, advancedExpectedRequestPostValuePairs) => { - // More advanced check if all request post data match with the expectations - let littleReport = '\nRequest Post Data:\n'; - let success = true; - - for (const expectedKey in advancedExpectedRequestPostValuePairs) { - if (!Object.prototype.hasOwnProperty.call(advancedExpectedRequestPostValuePairs, expectedKey)) { - continue; - } - let keyFound = false; - const expectedValue = advancedExpectedRequestPostValuePairs[expectedKey]; - - for (const [key, value] of Object.entries(RequestPostDataObject)) { - if (key === expectedKey) { - keyFound = true; - if (expectedValue === undefined) { - littleReport += ` ${expectedKey.padStart(10, ' ')}\n`; - } else if (typeof expectedValue === 'object' && expectedValue.base64) { - const decodedActualValue = Buffer.from(value, 'base64').toString('utf8'); - if (decodedActualValue === expectedValue.base64) { - littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`; - } else { - littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`; - success = false; - } - } else if (value === expectedValue) { - littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`; - } else { - littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${value}"\n`; - success = false; - } - } - } - - if (keyFound === false) { - littleReport += ` ✖ ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> key not found in request\n`; - success = false; - } - } - - return success ? true : littleReport; -}; diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index 648b6219c..cb40ba21f 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -36,6 +36,7 @@ const { blurElement } = require('./scripts/blurElement'); const { dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElementInDOMError, } = require('./errors/ElementAssertion'); +const { allParameterValuePairsMatchExtreme, extractQueryObjects, createAdvancedTestResults } = require('./networkTraffics/utils'); const SHADOW = 'shadow'; const webRoot = 'body'; @@ -452,6 +453,11 @@ class WebDriver extends Helper { this.activeSessionName = ''; this.customLocatorStrategies = config.customLocatorStrategies; + // for network stuff + this.requests = []; + this.recording = false; + this.recordedAtLeastOnce = false; + this._setConfig(config); Locator.addFilter((locator, result) => { @@ -633,6 +639,11 @@ class WebDriver extends Helper { if (this.browser.capabilities && this.browser.capabilities.platformName) { this.browser.capabilities.platformName = this.browser.capabilities.platformName.toLowerCase(); } + + if (this.options.automationProtocol) { + this.puppeteerBrowser = await this.browser.getPuppeteer(); + } + return this.browser; } @@ -2603,9 +2614,9 @@ class WebDriver extends Helper { return; } this.geoLocation = { latitude, longitude }; - const puppeteerBrowser = await this.browser.getPuppeteer(); + await this.browser.call(async () => { - const pages = await puppeteerBrowser.pages(); + const pages = await this.puppeteerBrowser.pages(); await pages[0].setGeolocation({ latitude, longitude }); }); } @@ -2667,6 +2678,255 @@ class WebDriver extends Helper { runInWeb(fn) { return fn(); } + + /** + * + * _Note:_ Only works when devtoolsProtocol is enabled. + * + * {{> flushNetworkTraffics }} + */ + flushNetworkTraffics() { + if (!this.options.automationProtocol) { + console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration'); + return; + } + this.requests = []; + } + + /** + * + * _Note:_ Only works when devtoolsProtocol is enabled. + * + * {{> stopRecordingTraffic }} + */ + stopRecordingTraffic() { + if (!this.options.automationProtocol) { + console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration'); + return; + } + this.page.removeAllListeners('request'); + this.recording = false; + } + + /** + * + * _Note:_ Only works when devtoolsProtocol is enabled. + * + * {{> startRecordingTraffic }} + * + */ + async startRecordingTraffic() { + if (!this.options.automationProtocol) { + console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration'); + return; + } + this.flushNetworkTraffics(); + this.recording = true; + this.recordedAtLeastOnce = true; + + this.page = (await this.puppeteerBrowser.pages())[0]; + await this.page.setRequestInterception(true); + + this.page.on('request', (request) => { + const information = { + url: request.url(), + method: request.method(), + requestHeaders: request.headers(), + requestPostData: request.postData(), + response: request.response(), + }; + + this.debugSection('REQUEST: ', JSON.stringify(information)); + + if (typeof information.requestPostData === 'object') { + information.requestPostData = JSON.parse(information.requestPostData); + } + request.continue(); + this.requests.push(information); + }); + } + + /** + * + * _Note:_ Only works when devtoolsProtocol is enabled. + * + * {{> grabRecordedNetworkTraffics }} + */ + async grabRecordedNetworkTraffics() { + if (!this.options.automationProtocol) { + console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration'); + return; + } + if (!this.recording || !this.recordedAtLeastOnce) { + throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.'); + } + + const promises = this.requests.map(async (request) => { + const resp = await request.response; + + if (!resp) { + return { + url: '', + response: { + status: '', + statusText: '', + body: '', + }, + }; + } + + let body; + try { + // There's no 'body' for some requests (redirect etc...) + body = JSON.parse((await resp.body()).toString()); + } catch (e) { + // only interested in JSON, not HTML responses. + } + + return { + url: resp.url(), + response: { + status: resp.status(), + statusText: resp.statusText(), + body, + }, + }; + }); + return Promise.all(promises); + } + + /** + * + * _Note:_ Only works when devtoolsProtocol is enabled. + * + * {{> seeTraffic }} + */ + async seeTraffic({ + name, url, parameters, requestPostData, timeout = 10, + }) { + if (!this.options.automationProtocol) { + console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration'); + return; + } + if (!name) { + throw new Error('Missing required key "name" in object given to "I.seeTraffic".'); + } + + if (!url) { + throw new Error('Missing required key "url" in object given to "I.seeTraffic".'); + } + + if (!this.recording || !this.recordedAtLeastOnce) { + throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.'); + } + + for (let i = 0; i <= timeout * 2; i++) { + const found = this._isInTraffic(url, parameters); + if (found) { + return true; + } + await new Promise((done) => { + setTimeout(done, 1000); + }); + } + + // check request post data + if (requestPostData && this._isInTraffic(url)) { + const advancedTestResults = createAdvancedTestResults(url, requestPostData, this.requests); + + assert.equal(advancedTestResults, true, `Traffic named "${name}" found correct URL ${url}, BUT the post data did not match:\n ${advancedTestResults}`); + } else if (parameters && this._isInTraffic(url)) { + const advancedTestResults = createAdvancedTestResults(url, parameters, this.requests); + + assert.fail( + `Traffic named "${name}" found correct URL ${url}, BUT the query parameters did not match:\n` + + `${advancedTestResults}`, + ); + } else { + assert.fail( + `Traffic named "${name}" not found in recorded traffic within ${timeout} seconds.\n` + + `Expected url: ${url}.\n` + + `Recorded traffic:\n${this._getTrafficDump()}`, + ); + } + } + + /** + * + * _Note:_ Only works when devtoolsProtocol is enabled. + * + * {{> dontSeeTraffic }} + * + */ + dontSeeTraffic({ name, url }) { + if (!this.options.automationProtocol) { + console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration'); + return; + } + if (!this.recordedAtLeastOnce) { + throw new Error('Failure in test automation. You use "I.dontSeeTraffic", but "I.startRecordingTraffic" was never called before.'); + } + + if (!name) { + throw new Error('Missing required key "name" in object given to "I.dontSeeTraffic".'); + } + + if (!url) { + throw new Error('Missing required key "url" in object given to "I.dontSeeTraffic".'); + } + + if (this._isInTraffic(url)) { + assert.fail(`Traffic with name "${name}" (URL: "${url}') found, but was not expected to be found.`); + } + } + + /** + * Checks if URL with parameters is part of network traffic. Returns true or false. Internal method for this helper. + * + * @param url URL to look for. + * @param [parameters] Parameters that this URL needs to contain + * @return {boolean} Whether or not URL with parameters is part of network traffic. + * @private + */ + _isInTraffic(url, parameters) { + let isInTraffic = false; + this.requests.forEach((request) => { + if (isInTraffic) { + return; // We already found traffic. Continue with next request + } + + if (!request.url.match(new RegExp(url))) { + return; // url not found in this request. continue with next request + } + + // URL has matched. Now we check the parameters + + if (parameters) { + const advancedReport = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), parameters); + if (advancedReport === true) { + isInTraffic = true; + } + } else { + isInTraffic = true; + } + }); + + return isInTraffic; + } + + /** + * Returns all URLs of all network requests recorded so far during execution of test scenario. + * + * @return {string} List of URLs recorded as a string, separated by new lines after each URL + * @private + */ + _getTrafficDump() { + let dumpedTraffic = ''; + this.requests.forEach((request) => { + dumpedTraffic += `${request.method} - ${request.url}\n`; + }); + return dumpedTraffic; + } } async function proceedSee(assertType, text, context, strict = false) { diff --git a/lib/helper/networkTraffics/utils.js b/lib/helper/networkTraffics/utils.js new file mode 100644 index 000000000..dfb771c46 --- /dev/null +++ b/lib/helper/networkTraffics/utils.js @@ -0,0 +1,137 @@ +const createAdvancedTestResults = (url, dataToCheck, requests) => { + // Creates advanced test results for a network traffic check. + // Advanced test results only applies when expected parameters are set + if (!dataToCheck) return ''; + + let urlFound = false; + let advancedResults; + requests.forEach((request) => { + // url not found in this request. continue with next request + if (urlFound || !request.url.match(new RegExp(url))) return; + urlFound = true; + + // Url found. Now we create advanced test report for that URL and show which parameters failed + if (!request.requestPostData) { + advancedResults = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), dataToCheck); + } else if (request.requestPostData) { + advancedResults = allRequestPostDataValuePairsMatchExtreme(request.requestPostData, dataToCheck); + } + }); + return advancedResults; +}; + +const extractQueryObjects = (queryString) => { + // Converts a string of GET parameters into an array of parameter objects. Each parameter object contains the properties "name" and "value". + if (queryString.indexOf('?') === -1) { + return []; + } + const queryObjects = []; + + const queryPart = queryString.split('?')[1]; + + const queryParameters = queryPart.split('&'); + + queryParameters.forEach((queryParameter) => { + const keyValue = queryParameter.split('='); + const queryObject = {}; + // eslint-disable-next-line prefer-destructuring + queryObject.name = keyValue[0]; + queryObject.value = decodeURIComponent(keyValue[1]); + queryObjects.push(queryObject); + }); + + return queryObjects; +}; + +const allParameterValuePairsMatchExtreme = (queryStringObject, advancedExpectedParameterValuePairs) => { + // More advanced check if all request parameters match with the expectations + let littleReport = '\nQuery parameters:\n'; + let success = true; + + for (const expectedKey in advancedExpectedParameterValuePairs) { + if (!Object.prototype.hasOwnProperty.call(advancedExpectedParameterValuePairs, expectedKey)) { + continue; + } + let parameterFound = false; + const expectedValue = advancedExpectedParameterValuePairs[expectedKey]; + + for (const queryParameter of queryStringObject) { + if (queryParameter.name === expectedKey) { + parameterFound = true; + if (expectedValue === undefined) { + littleReport += ` ${expectedKey.padStart(10, ' ')}\n`; + } else if (typeof expectedValue === 'object' && expectedValue.base64) { + const decodedActualValue = Buffer.from(queryParameter.value, 'base64').toString('utf8'); + if (decodedActualValue === expectedValue.base64) { + littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`; + } else { + littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`; + success = false; + } + } else if (queryParameter.value === expectedValue) { + littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`; + } else { + littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${queryParameter.value}"\n`; + success = false; + } + } + } + + if (parameterFound === false) { + littleReport += ` ✖ ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> parameter not found in request\n`; + success = false; + } + } + + return success ? true : littleReport; +}; + +const allRequestPostDataValuePairsMatchExtreme = (RequestPostDataObject, advancedExpectedRequestPostValuePairs) => { + // More advanced check if all request post data match with the expectations + let littleReport = '\nRequest Post Data:\n'; + let success = true; + + for (const expectedKey in advancedExpectedRequestPostValuePairs) { + if (!Object.prototype.hasOwnProperty.call(advancedExpectedRequestPostValuePairs, expectedKey)) { + continue; + } + let keyFound = false; + const expectedValue = advancedExpectedRequestPostValuePairs[expectedKey]; + + for (const [key, value] of Object.entries(RequestPostDataObject)) { + if (key === expectedKey) { + keyFound = true; + if (expectedValue === undefined) { + littleReport += ` ${expectedKey.padStart(10, ' ')}\n`; + } else if (typeof expectedValue === 'object' && expectedValue.base64) { + const decodedActualValue = Buffer.from(value, 'base64').toString('utf8'); + if (decodedActualValue === expectedValue.base64) { + littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`; + } else { + littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`; + success = false; + } + } else if (value === expectedValue) { + littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`; + } else { + littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${value}"\n`; + success = false; + } + } + } + + if (keyFound === false) { + littleReport += ` ✖ ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> key not found in request\n`; + success = false; + } + } + + return success ? true : littleReport; +}; + +module.exports = { + createAdvancedTestResults, + extractQueryObjects, + allParameterValuePairsMatchExtreme, + allRequestPostDataValuePairsMatchExtreme, +}; diff --git a/test/helper/WebDriver_devtools_test.js b/test/helper/WebDriver_devtools_test.js index b0b4329bb..ed9bd4c36 100644 --- a/test/helper/WebDriver_devtools_test.js +++ b/test/helper/WebDriver_devtools_test.js @@ -1192,4 +1192,135 @@ describe('WebDriver - Devtools Protocol', function () { assert.equal('TestEd Beta 2.0', title); }); }); + + describe('#startRecordingTraffic, #seeTraffic, #stopRecordingTraffic, #dontSeeTraffic, #grabRecordedNetworkTraffics', () => { + it('should throw error when calling seeTraffic before recording traffics', async () => { + try { + wd.amOnPage('https://codecept.io/'); + await wd.seeTraffic({ name: 'traffics', url: 'https://codecept.io/img/companies/BC_LogoScreen_C.jpg' }); + } catch (e) { + expect(e.message).to.equal('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.'); + } + }); + + it('should throw error when calling seeTraffic but missing name', async () => { + try { + wd.amOnPage('https://codecept.io/'); + await wd.seeTraffic({ url: 'https://codecept.io/img/companies/BC_LogoScreen_C.jpg' }); + } catch (e) { + expect(e.message).to.equal('Missing required key "name" in object given to "I.seeTraffic".'); + } + }); + + it('should throw error when calling seeTraffic but missing url', async () => { + try { + wd.amOnPage('https://codecept.io/'); + await wd.seeTraffic({ name: 'https://codecept.io/img/companies/BC_LogoScreen_C.jpg' }); + } catch (e) { + expect(e.message).to.equal('Missing required key "url" in object given to "I.seeTraffic".'); + } + }); + + it('should flush the network traffics', async () => { + await wd.startRecordingTraffic(); + await wd.amOnPage('https://codecept.io/'); + await wd.flushNetworkTraffics(); + const traffics = await wd.grabRecordedNetworkTraffics(); + expect(traffics.length).to.equal(0); + }); + + it('should see recording traffics', async () => { + wd.startRecordingTraffic(); + wd.amOnPage('https://codecept.io/'); + await wd.seeTraffic({ name: 'traffics', url: 'https://codecept.io/img/companies/BC_LogoScreen_C.jpg' }); + }); + + it('should not see recording traffics', async () => { + wd.startRecordingTraffic(); + wd.amOnPage('https://codecept.io/'); + wd.stopRecordingTraffic(); + await wd.dontSeeTraffic({ name: 'traffics', url: 'https://codecept.io/img/companies/BC_LogoScreen_C.jpg' }); + }); + + it('should not see recording traffics using regex url', async () => { + wd.startRecordingTraffic(); + wd.amOnPage('https://codecept.io/'); + wd.stopRecordingTraffic(); + await wd.dontSeeTraffic({ name: 'traffics', url: /BC_LogoScreen_C.jpg/ }); + }); + + it('should throw error when calling dontSeeTraffic but missing name', async () => { + wd.startRecordingTraffic(); + wd.amOnPage('https://codecept.io/'); + wd.stopRecordingTraffic(); + try { + await wd.dontSeeTraffic({ url: 'https://codecept.io/img/companies/BC_LogoScreen_C.jpg' }); + } catch (e) { + expect(e.message).to.equal('Missing required key "name" in object given to "I.dontSeeTraffic".'); + } + }); + + it('should throw error when calling dontSeeTraffic but missing url', async () => { + wd.startRecordingTraffic(); + wd.amOnPage('https://codecept.io/'); + wd.stopRecordingTraffic(); + try { + await wd.dontSeeTraffic({ name: 'traffics' }); + } catch (e) { + expect(e.message).to.equal('Missing required key "url" in object given to "I.dontSeeTraffic".'); + } + }); + + it('should check traffics with more advanced params', async () => { + await wd.startRecordingTraffic(); + await wd.amOnPage('https://openawd.com/blog/chatgpt'); + const traffics = await wd.grabRecordedNetworkTraffics(); + + for (const traffic of traffics) { + if (traffic.url.includes('&width=')) { + // new URL object + const currentUrl = new URL(traffic.url); + + // get access to URLSearchParams object + const searchParams = currentUrl.searchParams; + + await wd.seeTraffic({ + name: 'sentry event', + url: currentUrl.origin + currentUrl.pathname, + parameters: searchParams, + }); + + break; + } + } + }); + + it.skip('should check traffics with more advanced post data', async () => { + await wd.amOnPage('https://openawd.com/blog/chatgpt'); + await wd.startRecordingTraffic(); + await wd.seeTraffic({ + name: 'event', + url: 'https://region1.google-analytics.com', + requestPostData: { + st: 2, + }, + }); + }); + + it.skip('should show error when advanced post data are not matching', async () => { + await wd.amOnPage('https://openawd.com/blog/chatgpt'); + await wd.startRecordingTraffic(); + try { + await wd.seeTraffic({ + name: 'event', + url: 'https://region1.google-analytics.com', + requestPostData: { + st: 3, + }, + }); + } catch (e) { + expect(e.message).to.contain('actual value: "2"'); + } + }); + }); }); diff --git a/typings/tests/helpers/Playwright.types.ts b/typings/tests/helpers/Playwright.types.ts index 981fd53fa..3f8c7154d 100644 --- a/typings/tests/helpers/Playwright.types.ts +++ b/typings/tests/helpers/Playwright.types.ts @@ -143,7 +143,7 @@ playwright.seeTraffic(); // $ExpectError playwright.seeTraffic(str); // $ExpectError playwright.seeTraffic({ name: str }); // $ExpectError playwright.seeTraffic({ url: str }); // $ExpectError -playwright.seeTraffic({ name: str, url: str}); // $ExpectType Promise +playwright.seeTraffic({ name: str, url: str}); // $ExpectType void playwright.dontSeeTraffic(); // $ExpectError playwright.dontSeeTraffic(str); // $ExpectError playwright.dontSeeTraffic({ name: str, url: str}); // $ExpectType void