From c44ce740ce5819e05a9f4c69428023693297d13e Mon Sep 17 00:00:00 2001 From: KobeN <7845001+kobenguyent@users.noreply.github.com> Date: Sat, 23 Mar 2024 07:36:55 +0100 Subject: [PATCH] feat(puppeteer): network traffics manipulation (#4263) --- docs/helpers/Puppeteer.md | 337 ++++++++++++------ lib/helper/Playwright.js | 159 +-------- lib/helper/Puppeteer.js | 85 ++++- lib/helper/WebDriver.js | 150 +------- lib/helper/network/actions.js | 123 +++++++ .../{networkTraffics => network}/utils.js | 50 +++ test/helper/Playwright_test.js | 169 --------- test/helper/WebDriver_devtools_test.js | 131 ------- test/helper/WebDriver_test.js | 1 + test/helper/webapi.js | 123 ++++++- 10 files changed, 622 insertions(+), 706 deletions(-) create mode 100644 lib/helper/network/actions.js rename lib/helper/{networkTraffics => network}/utils.js (80%) diff --git a/docs/helpers/Puppeteer.md b/docs/helpers/Puppeteer.md index 6daed6c2e..3bf4cf9a0 100644 --- a/docs/helpers/Puppeteer.md +++ b/docs/helpers/Puppeteer.md @@ -39,26 +39,26 @@ Type: [object][4] - `url` **[string][6]** base url of website to be tested - `basicAuth` **[object][4]?** (optional) the basic authentication to pass to base url. Example: {username: 'username', password: 'password'} -- `show` **[boolean][22]?** show Google Chrome window for debug. -- `restart` **[boolean][22]?** restart browser between tests. -- `disableScreenshots` **[boolean][22]?** don't save screenshot on failure. -- `fullPageScreenshots` **[boolean][22]?** make full page screenshots on failure. -- `uniqueScreenshotNames` **[boolean][22]?** option to prevent screenshot override if you have scenarios with the same name in different suites. -- `trace` **[boolean][22]?** record [tracing information][26] with screenshots. -- `keepTraceForPassedTests` **[boolean][22]?** save trace for passed tests. -- `keepBrowserState` **[boolean][22]?** keep browser state between tests when `restart` is set to false. -- `keepCookies` **[boolean][22]?** keep cookies between tests when `restart` is set to false. -- `waitForAction` **[number][10]?** how long to wait after click, doubleClick or PressKey actions in ms. Default: 100. -- `waitForNavigation` **[string][6]?** when to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `networkidle0`, `networkidle2`. See [Puppeteer API][25]. Array values are accepted as well. -- `pressKeyDelay` **[number][10]?** delay between key presses in ms. Used when calling Puppeteers page.type(...) in fillField/appendField -- `getPageTimeout` **[number][10]?** config option to set maximum navigation time in milliseconds. If the timeout is set to 0, then timeout will be disabled. -- `waitForTimeout` **[number][10]?** default wait* timeout in ms. +- `show` **[boolean][23]?** show Google Chrome window for debug. +- `restart` **[boolean][23]?** restart browser between tests. +- `disableScreenshots` **[boolean][23]?** don't save screenshot on failure. +- `fullPageScreenshots` **[boolean][23]?** make full page screenshots on failure. +- `uniqueScreenshotNames` **[boolean][23]?** option to prevent screenshot override if you have scenarios with the same name in different suites. +- `trace` **[boolean][23]?** record [tracing information][27] with screenshots. +- `keepTraceForPassedTests` **[boolean][23]?** save trace for passed tests. +- `keepBrowserState` **[boolean][23]?** keep browser state between tests when `restart` is set to false. +- `keepCookies` **[boolean][23]?** keep cookies between tests when `restart` is set to false. +- `waitForAction` **[number][11]?** how long to wait after click, doubleClick or PressKey actions in ms. Default: 100. +- `waitForNavigation` **[string][6]?** when to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `networkidle0`, `networkidle2`. See [Puppeteer API][26]. Array values are accepted as well. +- `pressKeyDelay` **[number][11]?** delay between key presses in ms. Used when calling Puppeteers page.type(...) in fillField/appendField +- `getPageTimeout` **[number][11]?** config option to set maximum navigation time in milliseconds. If the timeout is set to 0, then timeout will be disabled. +- `waitForTimeout` **[number][11]?** default wait* timeout in ms. - `windowSize` **[string][6]?** default window size. Set a dimension in format WIDTHxHEIGHT like `640x480`. - `userAgent` **[string][6]?** user-agent string. -- `manualStart` **[boolean][22]?** do not start browser before a test, start it manually inside a helper with `this.helpers["Puppeteer"]._startBrowser()`. +- `manualStart` **[boolean][23]?** do not start browser before a test, start it manually inside a helper with `this.helpers["Puppeteer"]._startBrowser()`. - `browser` **[string][6]?** can be changed to `firefox` when using [puppeteer-firefox][2]. -- `chrome` **[object][4]?** pass additional [Puppeteer run options][27]. -- `highlightElement` **[boolean][22]?** highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose). +- `chrome` **[object][4]?** pass additional [Puppeteer run options][28]. +- `highlightElement` **[boolean][23]?** highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose). @@ -648,6 +648,25 @@ I.dontSeeInTitle('Error'); Returns **void** automatically synchronized promise through #recorder +### dontSeeTraffic + +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][4]** options when checking the traffic network. + - `opts.name` **[string][6]** 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][6] | [RegExp][10])** 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. @@ -710,7 +729,7 @@ I.dragSlider('#slider', -70); #### Parameters - `locator` **([string][6] | [object][4])** located by label|name|CSS|XPath|strict locator. -- `offsetX` **[number][10]** position to drag. +- `offsetX` **[number][11]** position to drag. Returns **void** automatically synchronized promise through #recorder @@ -724,7 +743,7 @@ Asynchronous scripts can also be executed with `executeScript` if a function ret 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][11]. +Example: In Vue.js to make components completely rendered we are waiting for [nextTick][12]. ```js I.executeAsyncScript(function(done) { @@ -745,9 +764,9 @@ let val = await I.executeAsyncScript(function(url, done) { #### Parameters - `args` **...any** to be passed to function. -- `fn` **([string][6] | [function][12])** function to be executed in browser context. +- `fn` **([string][6] | [function][13])** function to be executed in browser context. -Returns **[Promise][13]<any>** script return value +Returns **[Promise][14]<any>** script return value ### executeScript @@ -780,9 +799,9 @@ let date = await I.executeScript(function(el) { #### Parameters - `args` **...any** to be passed to function. -- `fn` **([string][6] | [function][12])** function to be executed in browser context. +- `fn` **([string][6] | [function][13])** function to be executed in browser context. -Returns **[Promise][13]<any>** script return value +Returns **[Promise][14]<any>** script return value ### fillField @@ -811,6 +830,14 @@ Returns **void** automatically synchronized promise through #recorder This action supports [React locators](https://codecept.io/react#locators) +### flushNetworkTraffics + +Resets all recorded network requests. + +```js +I.flushNetworkTraffics(); +``` + ### flushWebSocketMessages Resets all recorded WS messages. @@ -830,7 +857,7 @@ I.see('#add-to-cart-bnt'); #### Parameters - `locator` **([string][6] | [object][4])** field located by label|name|CSS|XPath|strict locator. -- `options` **any?** Playwright only: [Additional options][14] for available options object as 2nd argument. +- `options` **any?** Playwright only: [Additional options][15] for available options object as 2nd argument. Returns **void** automatically synchronized promise through #recorder @@ -887,7 +914,7 @@ let hint = await I.grabAttributeFrom('#tooltip', 'title'); - `locator` **([string][6] | [object][4])** element located by CSS|XPath|strict locator. - `attr` **[string][6]** attribute name. -Returns **[Promise][13]<[string][6]>** attribute value +Returns **[Promise][14]<[string][6]>** attribute value This action supports [React locators](https://codecept.io/react#locators) @@ -907,7 +934,7 @@ let hints = await I.grabAttributeFromAll('.tooltip', 'title'); - `locator` **([string][6] | [object][4])** element located by CSS|XPath|strict locator. - `attr` **[string][6]** attribute name. -Returns **[Promise][13]<[Array][15]<[string][6]>>** attribute value +Returns **[Promise][14]<[Array][16]<[string][6]>>** attribute value This action supports [React locators](https://codecept.io/react#locators) @@ -922,7 +949,7 @@ let logs = await I.grabBrowserLogs(); console.log(JSON.stringify(logs)) ``` -Returns **[Promise][13]<[Array][15]<any>>** +Returns **[Promise][14]<[Array][16]<any>>** ### grabCookie @@ -956,7 +983,7 @@ const value = await I.grabCssPropertyFrom('h3', 'font-weight'); - `locator` **([string][6] | [object][4])** element located by CSS|XPath|strict locator. - `cssProperty` **[string][6]** CSS property name. -Returns **[Promise][13]<[string][6]>** CSS value +Returns **[Promise][14]<[string][6]>** CSS value This action supports [React locators](https://codecept.io/react#locators) @@ -976,7 +1003,7 @@ const values = await I.grabCssPropertyFromAll('h3', 'font-weight'); - `locator` **([string][6] | [object][4])** element located by CSS|XPath|strict locator. - `cssProperty` **[string][6]** CSS property name. -Returns **[Promise][13]<[Array][15]<[string][6]>>** CSS value +Returns **[Promise][14]<[Array][16]<[string][6]>>** CSS value This action supports [React locators](https://codecept.io/react#locators) @@ -992,7 +1019,7 @@ let url = await I.grabCurrentUrl(); console.log(`Current URL is [${url}]`); ``` -Returns **[Promise][13]<[string][6]>** current URL +Returns **[Promise][14]<[string][6]>** current URL ### grabDataFromPerformanceTiming @@ -1045,7 +1072,7 @@ const width = await I.grabElementBoundingRect('h3', 'width'); - `prop` - `elementSize` **[string][6]?** x, y, width or height of the given element. -Returns **([Promise][13]<DOMRect> | [Promise][13]<[number][10]>)** Element bounding rectangle +Returns **([Promise][14]<DOMRect> | [Promise][14]<[number][11]>)** Element bounding rectangle ### grabHTMLFrom @@ -1062,7 +1089,7 @@ let postHTML = await I.grabHTMLFrom('#post'); - `locator` - `element` **([string][6] | [object][4])** located by CSS|XPath|strict locator. -Returns **[Promise][13]<[string][6]>** HTML code for an element +Returns **[Promise][14]<[string][6]>** HTML code for an element ### grabHTMLFromAll @@ -1078,7 +1105,7 @@ let postHTMLs = await I.grabHTMLFromAll('.post'); - `locator` - `element` **([string][6] | [object][4])** located by CSS|XPath|strict locator. -Returns **[Promise][13]<[Array][15]<[string][6]>>** HTML code for an element +Returns **[Promise][14]<[Array][16]<[string][6]>>** HTML code for an element ### grabNumberOfOpenTabs @@ -1089,7 +1116,7 @@ Resumes test execution, so **should be used inside async function with `await`** let tabs = await I.grabNumberOfOpenTabs(); ``` -Returns **[Promise][13]<[number][10]>** number of open tabs +Returns **[Promise][14]<[number][11]>** number of open tabs ### grabNumberOfVisibleElements @@ -1104,7 +1131,7 @@ let numOfElements = await I.grabNumberOfVisibleElements('p'); - `locator` **([string][6] | [object][4])** located by CSS|XPath|strict locator. -Returns **[Promise][13]<[number][10]>** number of visible elements +Returns **[Promise][14]<[number][11]>** number of visible elements @@ -1120,7 +1147,7 @@ Resumes test execution, so **should be used inside an async function with `await let { x, y } = await I.grabPageScrollPosition(); ``` -Returns **[Promise][13]<PageScrollPosition>** scroll position +Returns **[Promise][14]<PageScrollPosition>** scroll position ### grabPopupText @@ -1130,7 +1157,20 @@ Grab the text within the popup. If no popup is visible then it will return null await I.grabPopupText(); ``` -Returns **[Promise][13]<([string][6] | null)>** +Returns **[Promise][14]<([string][6] | null)>** + +### grabRecordedNetworkTraffics + +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][16]** recorded network traffics ### grabSource @@ -1141,7 +1181,7 @@ Resumes test execution, so **should be used inside async function with `await`** let pageSource = await I.grabSource(); ``` -Returns **[Promise][13]<[string][6]>** source code +Returns **[Promise][14]<[string][6]>** source code ### grabTextFrom @@ -1158,7 +1198,7 @@ If multiple elements found returns first element. - `locator` **([string][6] | [object][4])** element located by CSS|XPath|strict locator. -Returns **[Promise][13]<[string][6]>** attribute value +Returns **[Promise][14]<[string][6]>** attribute value This action supports [React locators](https://codecept.io/react#locators) @@ -1177,7 +1217,7 @@ let pins = await I.grabTextFromAll('#pin li'); - `locator` **([string][6] | [object][4])** element located by CSS|XPath|strict locator. -Returns **[Promise][13]<[Array][15]<[string][6]>>** attribute value +Returns **[Promise][14]<[Array][16]<[string][6]>>** attribute value This action supports [React locators](https://codecept.io/react#locators) @@ -1192,7 +1232,7 @@ Resumes test execution, so **should be used inside async with `await`** operator let title = await I.grabTitle(); ``` -Returns **[Promise][13]<[string][6]>** title +Returns **[Promise][14]<[string][6]>** title ### grabValueFrom @@ -1208,7 +1248,7 @@ let email = await I.grabValueFrom('input[name=email]'); - `locator` **([string][6] | [object][4])** field located by label|name|CSS|XPath|strict locator. -Returns **[Promise][13]<[string][6]>** attribute value +Returns **[Promise][14]<[string][6]>** attribute value ### grabValueFromAll @@ -1223,7 +1263,7 @@ let inputs = await I.grabValueFromAll('//form/input'); - `locator` **([string][6] | [object][4])** field located by label|name|CSS|XPath|strict locator. -Returns **[Promise][13]<[Array][15]<[string][6]>>** attribute value +Returns **[Promise][14]<[Array][16]<[string][6]>>** attribute value ### grabWebElements @@ -1238,18 +1278,18 @@ const webElements = await I.grabWebElements('#button'); - `locator` **([string][6] | [object][4])** element located by CSS|XPath|strict locator. -Returns **[Promise][13]<any>** WebElement of being used Web helper +Returns **[Promise][14]<any>** WebElement of being used Web helper ### grabWebSocketMessages Grab the recording WS messages -Returns **[Array][15]<any>** +Returns **([Array][16]<any> | [undefined][17])** ### handleDownloads Sets a directory to where save files. Allows to test file downloads. -Should be used with [FileSystem helper][16] to check that file were downloaded correctly. +Should be used with [FileSystem helper][18] to check that file were downloaded correctly. By default, files are saved to `output/downloads`. This directory is cleaned on every `handleDownloads` call, to ensure no old files are kept. @@ -1267,18 +1307,18 @@ I.seeFile('avatar.jpg'); ### mockRoute -Mocks network request using [`Request Interception`][17] +Mocks network request using [`Request Interception`][19] ```js I.mockRoute(/(.png$)|(.jpg$)/, route => route.abort()); ``` -This method allows intercepting and mocking requests & responses. [Learn more about it][17] +This method allows intercepting and mocking requests & responses. [Learn more about it][19] #### Parameters -- `url` **([string][6] | [RegExp][18])?** URL, regex or pattern for to match URL -- `handler` **[function][12]?** a function to process request +- `url` **([string][6] | [RegExp][10])?** URL, regex or pattern for to match URL +- `handler` **[function][13]?** a function to process request ### moveCursorTo @@ -1293,8 +1333,8 @@ I.moveCursorTo('#submit', 5,5); #### Parameters - `locator` **([string][6] | [object][4])** located by CSS|XPath|strict locator. -- `offsetX` **[number][10]** (optional, `0` by default) X-axis offset. -- `offsetY` **[number][10]** (optional, `0` by default) Y-axis offset. +- `offsetX` **[number][11]** (optional, `0` by default) X-axis offset. +- `offsetY` **[number][11]** (optional, `0` by default) Y-axis offset. Returns **void** automatically synchronized promise through #recorder @@ -1312,11 +1352,11 @@ I.openNewTab(); ### pressKey -_Note:_ Shortcuts like `'Meta'` + `'A'` do not work on macOS ([GoogleChrome/puppeteer#1313][19]). +_Note:_ Shortcuts like `'Meta'` + `'A'` do not work on macOS ([GoogleChrome/puppeteer#1313][20]). Presses a key in the browser (on a focused element). -_Hint:_ For populating text field or textarea, it is recommended to use [`fillField`][20]. +_Hint:_ For populating text field or textarea, it is recommended to use [`fillField`][21]. ```js I.pressKey('Backspace'); @@ -1375,7 +1415,7 @@ Some of the supported key names are: #### Parameters -- `key` **([string][6] | [Array][15]<[string][6]>)** key or array of keys to press. +- `key` **([string][6] | [Array][16]<[string][6]>)** key or array of keys to press. Returns **void** automatically synchronized promise through #recorder @@ -1383,7 +1423,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`][21]). +To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][22]). ```js I.pressKeyDown('Control'); @@ -1401,7 +1441,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`][21]). +To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][22]). ```js I.pressKeyDown('Control'); @@ -1436,8 +1476,8 @@ First parameter can be set to `maximize`. #### Parameters -- `width` **[number][10]** width in pixels or `maximize`. -- `height` **[number][10]** height in pixels. +- `width` **[number][11]** width in pixels or `maximize`. +- `height` **[number][11]** height in pixels. Returns **void** automatically synchronized promise through #recorder @@ -1495,7 +1535,7 @@ I.saveScreenshot('debug.png', true) //resizes to available scrollHeight and scro #### Parameters - `fileName` **[string][6]** file name to save. -- `fullPage` **[boolean][22]** (optional, `false` by default) flag to enable fullscreen screenshot mode. +- `fullPage` **[boolean][23]** (optional, `false` by default) flag to enable fullscreen screenshot mode. Returns **void** automatically synchronized promise through #recorder @@ -1532,8 +1572,8 @@ I.scrollTo('#submit', 5, 5); #### Parameters - `locator` **([string][6] | [object][4])** located by CSS|XPath|strict locator. -- `offsetX` **[number][10]** (optional, `0` by default) X-axis offset. -- `offsetY` **[number][10]** (optional, `0` by default) Y-axis offset. +- `offsetX` **[number][11]** (optional, `0` by default) X-axis offset. +- `offsetY` **[number][11]** (optional, `0` by default) Y-axis offset. Returns **void** automatically synchronized promise through #recorder @@ -1766,7 +1806,7 @@ I.seeNumberOfElements('#submitBtn', 1); #### Parameters - `locator` **([string][6] | [object][4])** element located by CSS|XPath|strict locator. -- `num` **[number][10]** number of elements. +- `num` **[number][11]** number of elements. Returns **void** automatically synchronized promise through #recorder @@ -1786,7 +1826,7 @@ I.seeNumberOfVisibleElements('.buttons', 3); #### Parameters - `locator` **([string][6] | [object][4])** element located by CSS|XPath|strict locator. -- `num` **[number][10]** number of elements. +- `num` **[number][11]** number of elements. Returns **void** automatically synchronized promise through #recorder @@ -1823,6 +1863,48 @@ I.seeTitleEquals('Test title.'); Returns **void** automatically synchronized promise through #recorder +### seeTraffic + +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][4]** options when checking the traffic network. + - `opts.name` **[string][6]** 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][6]** Expected URL of request in network traffic + - `opts.parameters` **[Object][4]?** Expected parameters of that request in network traffic + - `opts.requestPostData` **[Object][4]?** Expected that request contains post data in network traffic + - `opts.timeout` **[number][11]?** 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. @@ -1847,7 +1929,7 @@ I.selectOption('Which OS do you use?', ['Android', 'iOS']); #### Parameters - `select` **([string][6] | [object][4])** field located by label|name|CSS|XPath|strict locator. -- `option` **([string][6] | [Array][15]<any>)** visible text or value of option. +- `option` **([string][6] | [Array][16]<any>)** visible text or value of option. Returns **void** automatically synchronized promise through #recorder @@ -1869,7 +1951,7 @@ I.setCookie([ #### Parameters -- `cookie` **(Cookie | [Array][15]<Cookie>)** a cookie object or array of cookie objects. +- `cookie` **(Cookie | [Array][16]<Cookie>)** a cookie object or array of cookie objects. Returns **void** automatically synchronized promise through #recorder @@ -1887,6 +1969,17 @@ I.setPuppeteerRequestHeaders({ - `customHeaders` **[object][4]** headers to set +### startRecordingTraffic + +Starts recording the network traffics. +This also resets recorded network requests. + +```js +I.startRecordingTraffic(); +``` + +Returns **void** automatically synchronized promise through #recorder + ### startRecordingWebSocketMessages Starts recording of websocket messages. @@ -1908,7 +2001,15 @@ I.stopMockingRoute(/(.png$)|(.jpg$)/); #### Parameters -- `url` **([string][6] | [RegExp][18])?** URL, regex or pattern for to match URL +- `url` **([string][6] | [RegExp][10])?** URL, regex or pattern for to match URL + +### stopRecordingTraffic + +Stops recording of network traffic. Recorded traffic is not flashed. + +```js +I.stopRecordingTraffic(); +``` ### stopRecordingWebSocketMessages @@ -1946,7 +2047,7 @@ I.switchToNextTab(2); #### Parameters -- `num` **[number][10]** +- `num` **[number][11]** ### switchToPreviousTab @@ -1959,13 +2060,13 @@ I.switchToPreviousTab(2); #### Parameters -- `num` **[number][10]** +- `num` **[number][11]** ### type 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`][20] is not an option. +_Note:_ Should be used when [`fillField`][21] is not an option. ```js // passing in a string @@ -1984,8 +2085,8 @@ I.type(secret('123456')); #### Parameters - `keys` -- `delay` **[number][10]?** (optional) delay in ms between key presses -- `key` **([string][6] | [Array][15]<[string][6]>)** or array of keys to type. +- `delay` **[number][11]?** (optional) delay in ms between key presses +- `key` **([string][6] | [Array][16]<[string][6]>)** or array of keys to type. Returns **void** automatically synchronized promise through #recorder @@ -2016,7 +2117,7 @@ Use Puppeteer API inside a test. First argument is a description of an action. Second argument is async function that gets this helper as parameter. -{ [`page`][23], [`browser`][24] } from Puppeteer API are available. +{ [`page`][24], [`browser`][25] } from Puppeteer API are available. ```js I.usePuppeteerTo('emulate offline mode', async ({ page }) { @@ -2027,7 +2128,7 @@ I.usePuppeteerTo('emulate offline mode', async ({ page }) { #### Parameters - `description` **[string][6]** used to show in logs. -- `fn` **[function][12]** async function that is executed with Puppeteer as argument +- `fn` **[function][13]** async function that is executed with Puppeteer as argument ### wait @@ -2039,7 +2140,7 @@ I.wait(2); // wait 2 secs #### Parameters -- `sec` **[number][10]** number of second to wait. +- `sec` **[number][11]** number of second to wait. Returns **void** automatically synchronized promise through #recorder @@ -2057,7 +2158,7 @@ I.waitForClickable('.btn.continue', 5); // wait for 5 secs - `locator` **([string][6] | [object][4])** element located by CSS|XPath|strict locator. - `waitTimeout` -- `sec` **[number][10]?** (optional, `1` by default) time in seconds to wait +- `sec` **[number][11]?** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2072,7 +2173,7 @@ I.waitForCookie("token"); #### Parameters - `name` **[string][6]** expected cookie name. -- `sec` **[number][10]** (optional, `3` by default) time in seconds to wait +- `sec` **[number][11]** (optional, `3` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2088,7 +2189,7 @@ I.waitForDetached('#popup'); #### Parameters - `locator` **([string][6] | [object][4])** element located by CSS|XPath|strict locator. -- `sec` **[number][10]** (optional, `1` by default) time in seconds to wait +- `sec` **[number][11]** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2105,7 +2206,7 @@ I.waitForElement('.btn.continue', 5); // wait for 5 secs #### Parameters - `locator` **([string][6] | [object][4])** element located by CSS|XPath|strict locator. -- `sec` **[number][10]?** (optional, `1` by default) time in seconds to wait +- `sec` **[number][11]?** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2121,7 +2222,7 @@ Element can be located by CSS or XPath. #### Parameters - `locator` **([string][6] | [object][4])** element located by CSS|XPath|strict locator. -- `sec` **[number][10]** (optional) time in seconds to wait, 1 by default. +- `sec` **[number][11]** (optional) time in seconds to wait, 1 by default. Returns **void** automatically synchronized promise through #recorder @@ -2142,9 +2243,9 @@ I.waitForFunction((count) => window.requests == count, [3], 5) // pass args and #### Parameters -- `fn` **([string][6] | [function][12])** to be executed in browser context. -- `argsOrSec` **([Array][15]<any> | [number][10])?** (optional, `1` by default) arguments for function or seconds. -- `sec` **[number][10]?** (optional, `1` by default) time in seconds to wait +- `fn` **([string][6] | [function][13])** to be executed in browser context. +- `argsOrSec` **([Array][16]<any> | [number][11])?** (optional, `1` by default) arguments for function or seconds. +- `sec` **[number][11]?** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2160,7 +2261,7 @@ I.waitForInvisible('#popup'); #### Parameters - `locator` **([string][6] | [object][4])** element located by CSS|XPath|strict locator. -- `sec` **[number][10]** (optional, `1` by default) time in seconds to wait +- `sec` **[number][11]** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2168,7 +2269,7 @@ Returns **void** automatically synchronized promise through #recorder Waits for navigation to finish. By default, takes configured `waitForNavigation` option. -See [Puppeteer's reference][25] +See [Puppeteer's reference][26] #### Parameters @@ -2184,8 +2285,8 @@ I.waitForNumberOfTabs(2); #### Parameters -- `expectedTabs` **[number][10]** expecting the number of tabs. -- `sec` **[number][10]** number of secs to wait. +- `expectedTabs` **[number][11]** expecting the number of tabs. +- `sec` **[number][11]** number of secs to wait. Returns **void** automatically synchronized promise through #recorder @@ -2200,8 +2301,8 @@ I.waitForRequest(request => request.url() === 'http://example.com' && request.me #### Parameters -- `urlOrPredicate` **([string][6] | [function][12])** -- `sec` **[number][10]?** seconds to wait +- `urlOrPredicate` **([string][6] | [function][13])** +- `sec` **[number][11]?** seconds to wait ### waitForResponse @@ -2214,8 +2315,8 @@ I.waitForResponse(response => response.url() === 'http://example.com' && respons #### Parameters -- `urlOrPredicate` **([string][6] | [function][12])** -- `sec` **[number][10]?** number of seconds to wait +- `urlOrPredicate` **([string][6] | [function][13])** +- `sec` **[number][11]?** number of seconds to wait ### waitForText @@ -2231,7 +2332,7 @@ I.waitForText('Thank you, form has been submitted', 5, '#modal'); #### Parameters - `text` **[string][6]** to wait for. -- `sec` **[number][10]** (optional, `1` by default) time in seconds to wait +- `sec` **[number][11]** (optional, `1` by default) time in seconds to wait - `context` **([string][6] | [object][4])?** (optional) element located by CSS|XPath|strict locator. Returns **void** automatically synchronized promise through #recorder @@ -2248,7 +2349,7 @@ I.waitForValue('//input', "GoodValue"); - `field` **([string][6] | [object][4])** input field. - `value` **[string][6]** expected value. -- `sec` **[number][10]** (optional, `1` by default) time in seconds to wait +- `sec` **[number][11]** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2264,7 +2365,7 @@ I.waitForVisible('#popup'); #### Parameters - `locator` **([string][6] | [object][4])** element located by CSS|XPath|strict locator. -- `sec` **[number][10]** (optional, `1` by default) time in seconds to wait +- `sec` **[number][11]** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2283,7 +2384,7 @@ I.waitInUrl('/info', 2); #### Parameters - `urlPart` **[string][6]** value to check. -- `sec` **[number][10]** (optional, `1` by default) time in seconds to wait +- `sec` **[number][11]** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2298,8 +2399,8 @@ I.waitNumberOfVisibleElements('a', 3); #### Parameters - `locator` **([string][6] | [object][4])** element located by CSS|XPath|strict locator. -- `num` **[number][10]** number of elements. -- `sec` **[number][10]** (optional, `1` by default) time in seconds to wait +- `num` **[number][11]** number of elements. +- `sec` **[number][11]** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2319,7 +2420,7 @@ I.waitToHide('#popup'); #### Parameters - `locator` **([string][6] | [object][4])** element located by CSS|XPath|strict locator. -- `sec` **[number][10]** (optional, `1` by default) time in seconds to wait +- `sec` **[number][11]** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2335,7 +2436,7 @@ I.waitUrlEquals('http://127.0.0.1:8000/info'); #### Parameters - `urlPart` **[string][6]** value to check. -- `sec` **[number][10]** (optional, `1` by default) time in seconds to wait +- `sec` **[number][11]** (optional, `1` by default) time in seconds to wait Returns **void** automatically synchronized promise through #recorder @@ -2357,38 +2458,40 @@ Returns **void** automatically synchronized promise through #recorder [9]: https://playwright.dev/docs/api/class-locator#locator-blur -[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number +[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp + +[11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number -[11]: https://vuejs.org/v2/api/#Vue-nextTick +[12]: https://vuejs.org/v2/api/#Vue-nextTick -[12]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function +[13]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function -[13]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise +[14]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise -[14]: https://playwright.dev/docs/api/class-locator#locator-focus +[15]: https://playwright.dev/docs/api/class-locator#locator-focus -[15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array +[16]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array -[16]: https://codecept.io/helpers/FileSystem +[17]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined -[17]: https://pptr.dev/next/guides/request-interception +[18]: https://codecept.io/helpers/FileSystem -[18]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp +[19]: https://pptr.dev/next/guides/request-interception -[19]: https://github.com/GoogleChrome/puppeteer/issues/1313 +[20]: https://github.com/GoogleChrome/puppeteer/issues/1313 -[20]: #fillfield +[21]: #fillfield -[21]: #click +[22]: #click -[22]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean +[23]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean -[23]: https://github.com/puppeteer/puppeteer/blob/master/docs/api.md#class-page +[24]: https://github.com/puppeteer/puppeteer/blob/master/docs/api.md#class-page -[24]: https://github.com/puppeteer/puppeteer/blob/master/docs/api.md#class-browser +[25]: https://github.com/puppeteer/puppeteer/blob/master/docs/api.md#class-browser -[25]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagewaitfornavigationoptions +[26]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagewaitfornavigationoptions -[26]: https://pptr.dev/api/puppeteer.tracing +[27]: https://pptr.dev/api/puppeteer.tracing -[27]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteerlaunchoptions +[28]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteerlaunchoptions diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 8587be57d..15f2aab0a 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -50,8 +50,9 @@ 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 { + dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics, +} = require('./network/actions'); const pathSeparator = path.sep; @@ -3010,37 +3011,6 @@ class Playwright extends Helper { }); } - /** - * {{> 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.'); - } - - const promises = this.requests.map(async (request) => { - const resp = await request.response; - 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); - } - /** * Blocks traffic of a given URL or a list of URLs. * @@ -3120,67 +3090,19 @@ class Playwright extends Helper { } /** + * * {{> flushNetworkTraffics }} */ flushNetworkTraffics() { - this.requests = []; + flushNetworkTraffics.call(this); } /** + * * {{> stopRecordingTraffic }} */ stopRecordingTraffic() { - this.page.removeAllListeners('request'); - this.recording = false; - } - - /** - * {{> seeTraffic }} - */ - async seeTraffic({ - name, url, parameters, requestPostData, timeout = 10, - }) { - 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()}`, - ); - } + stopRecordingTraffic.call(this); } /** @@ -3217,73 +3139,30 @@ class Playwright extends Helper { } /** - * {{> dontSeeTraffic }} * + * {{> grabRecordedNetworkTraffics }} */ - dontSeeTraffic({ name, url }) { - 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.`); - } + async grabRecordedNetworkTraffics() { + return grabRecordedNetworkTraffics.call(this); } /** - * 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 + * {{> seeTraffic }} */ - _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; + async seeTraffic({ + name, url, parameters, requestPostData, timeout = 10, + }) { + await seeTraffic.call(this, ...arguments); } /** - * 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 + * {{> dontSeeTraffic }} + * */ - _getTrafficDump() { - let dumpedTraffic = ''; - this.requests.forEach((request) => { - dumpedTraffic += `${request.method} - ${request.url}\n`; - }); - return dumpedTraffic; + dontSeeTraffic({ name, url }) { + dontSeeTraffic.call(this, ...arguments); } /** diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index 8a6caa5c6..8ee9e0561 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -36,13 +36,14 @@ const ElementNotFound = require('./errors/ElementNotFound'); const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnectionRefused'); const Popup = require('./extras/Popup'); const Console = require('./extras/Console'); -const findReact = require('./extras/React'); const { highlightElement } = require('./scripts/highlightElement'); const { blurElement } = require('./scripts/blurElement'); -const { focusElement } = require('./scripts/focusElement'); const { dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElementInDOMError, } = require('./errors/ElementAssertion'); +const { + dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics, +} = require('./network/actions'); let puppeteer; let perfTiming; @@ -226,6 +227,11 @@ class Puppeteer extends Helper { this.sessionPages = {}; this.activeSessionName = ''; + // for network stuff + this.requests = []; + this.recording = false; + this.recordedAtLeastOnce = false; + // for websocket messages this.webSocketMessages = []; this.recordingWebSocketMessages = false; @@ -2514,6 +2520,79 @@ class Puppeteer extends Helper { }); } + /** + * + * {{> flushNetworkTraffics }} + */ + flushNetworkTraffics() { + flushNetworkTraffics.call(this); + } + + /** + * + * {{> stopRecordingTraffic }} + */ + stopRecordingTraffic() { + stopRecordingTraffic.call(this); + } + + /** + * {{> startRecordingTraffic }} + * + */ + async startRecordingTraffic() { + this.flushNetworkTraffics(); + this.recording = true; + this.recordedAtLeastOnce = true; + + 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); + }); + } + + /** + * + * {{> grabRecordedNetworkTraffics }} + */ + async grabRecordedNetworkTraffics() { + return grabRecordedNetworkTraffics.call(this); + } + + /** + * + * {{> seeTraffic }} + */ + async seeTraffic({ + name, url, parameters, requestPostData, timeout = 10, + }) { + await seeTraffic.call(this, ...arguments); + } + + /** + * + * {{> dontSeeTraffic }} + * + */ + dontSeeTraffic({ name, url }) { + dontSeeTraffic.call(this, ...arguments); + } + async getNewCDPSession() { const client = await this.page.target().createCDPSession(); return client; @@ -2566,7 +2645,7 @@ class Puppeteer extends Helper { /** * Grab the recording WS messages * - * @return { Array } + * @return { Array|undefined } * */ grabWebSocketMessages() { diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index 62c122bee..359fb16ea 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -2,10 +2,8 @@ let webdriverio; const assert = require('assert'); const path = require('path'); -const fs = require('fs'); const Helper = require('@codeceptjs/helper'); -const crypto = require('crypto'); const promiseRetry = require('promise-retry'); const stringIncludes = require('../assert/include').includes; const { urlEquals, equals } = require('../assert/equal'); @@ -30,13 +28,14 @@ const ElementNotFound = require('./errors/ElementNotFound'); const ConnectionRefused = require('./errors/ConnectionRefused'); const Locator = require('../locator'); const { highlightElement } = require('./scripts/highlightElement'); -const store = require('../store'); const { focusElement } = require('./scripts/focusElement'); const { blurElement } = require('./scripts/blurElement'); const { dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElementInDOMError, } = require('./errors/ElementAssertion'); -const { allParameterValuePairsMatchExtreme, extractQueryObjects, createAdvancedTestResults } = require('./networkTraffics/utils'); +const { + dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics, +} = require('./network/actions'); const SHADOW = 'shadow'; const webRoot = 'body'; @@ -2758,42 +2757,7 @@ class WebDriver extends Helper { 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); + return grabRecordedNetworkTraffics.call(this); } /** @@ -2809,47 +2773,7 @@ class WebDriver extends Helper { 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()}`, - ); - } + await seeTraffic.call(this, ...arguments); } /** @@ -2864,69 +2788,7 @@ class WebDriver extends Helper { 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; + dontSeeTraffic.call(this, ...arguments); } } diff --git a/lib/helper/network/actions.js b/lib/helper/network/actions.js new file mode 100644 index 000000000..e89ba7ed2 --- /dev/null +++ b/lib/helper/network/actions.js @@ -0,0 +1,123 @@ +const assert = require('assert'); +const { isInTraffic, createAdvancedTestResults, getTrafficDump } = require('./utils'); + +function dontSeeTraffic({ name, url }) { + 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 (isInTraffic.call(this, url)) { + assert.fail(`Traffic with name "${name}" (URL: "${url}') found, but was not expected to be found.`); + } +} + +async function seeTraffic({ + name, url, parameters, requestPostData, timeout = 10, +}) { + 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 = isInTraffic.call(this, url, parameters); + if (found) { + return true; + } + await new Promise((done) => { + setTimeout(done, 1000); + }); + } + + // check request post data + if (requestPostData && isInTraffic.call(this, 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 && isInTraffic.call(this, 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${getTrafficDump.call(this)}`, + ); + } +} + +async function grabRecordedNetworkTraffics() { + 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); +} + +function stopRecordingTraffic() { + // @ts-ignore + this.page.removeAllListeners('request'); + this.recording = false; +} + +function flushNetworkTraffics() { + this.requests = []; +} + +module.exports = { + dontSeeTraffic, + seeTraffic, + grabRecordedNetworkTraffics, + stopRecordingTraffic, + flushNetworkTraffics, +}; diff --git a/lib/helper/networkTraffics/utils.js b/lib/helper/network/utils.js similarity index 80% rename from lib/helper/networkTraffics/utils.js rename to lib/helper/network/utils.js index dfb771c46..7b34ec342 100644 --- a/lib/helper/networkTraffics/utils.js +++ b/lib/helper/network/utils.js @@ -129,9 +129,59 @@ const allRequestPostDataValuePairsMatchExtreme = (RequestPostDataObject, advance return success ? true : littleReport; }; +/** + * 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 + */ +function getTrafficDump() { + let dumpedTraffic = ''; + this.requests.forEach((request) => { + dumpedTraffic += `${request.method} - ${request.url}\n`; + }); + return dumpedTraffic; +} + +/** + * 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 + */ +function 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; +} + module.exports = { createAdvancedTestResults, extractQueryObjects, allParameterValuePairsMatchExtreme, allRequestPostDataValuePairsMatchExtreme, + getTrafficDump, + isInTraffic, }; diff --git a/test/helper/Playwright_test.js b/test/helper/Playwright_test.js index 81ac807b6..f2680b9b8 100644 --- a/test/helper/Playwright_test.js +++ b/test/helper/Playwright_test.js @@ -898,175 +898,6 @@ describe('Playwright', function () { }); }); - describe('#startRecordingTraffic, #seeTraffic, #stopRecordingTraffic, #dontSeeTraffic, #grabRecordedNetworkTraffics', () => { - it('should throw error when calling seeTraffic before recording traffics', async () => { - try { - I.amOnPage('https://codecept.io/'); - await I.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 { - I.amOnPage('https://codecept.io/'); - await I.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 { - I.amOnPage('https://codecept.io/'); - await I.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 () => { - I.startRecordingTraffic(); - I.amOnPage('https://codecept.io/'); - I.flushNetworkTraffics(); - const traffics = await I.grabRecordedNetworkTraffics(); - expect(traffics.length).to.equal(0); - }); - - it('should see recording traffics', async () => { - I.startRecordingTraffic(); - I.amOnPage('https://codecept.io/'); - await I.seeTraffic({ name: 'traffics', url: 'https://codecept.io/img/companies/BC_LogoScreen_C.jpg' }); - }); - - it('should not see recording traffics', async () => { - I.startRecordingTraffic(); - I.amOnPage('https://codecept.io/'); - I.stopRecordingTraffic(); - await I.dontSeeTraffic({ name: 'traffics', url: 'https://codecept.io/img/companies/BC_LogoScreen_C.jpg' }); - }); - - it('should not see recording traffics using regex url', async () => { - I.startRecordingTraffic(); - I.amOnPage('https://codecept.io/'); - I.stopRecordingTraffic(); - await I.dontSeeTraffic({ name: 'traffics', url: /BC_LogoScreen_C.jpg/ }); - }); - - it('should throw error when calling dontSeeTraffic but missing name', async () => { - I.startRecordingTraffic(); - I.amOnPage('https://codecept.io/'); - I.stopRecordingTraffic(); - try { - await I.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 () => { - I.startRecordingTraffic(); - I.amOnPage('https://codecept.io/'); - I.stopRecordingTraffic(); - try { - await I.dontSeeTraffic({ name: 'traffics' }); - } catch (e) { - expect(e.message).to.equal('Missing required key "url" in object given to "I.dontSeeTraffic".'); - } - }); - - it('should mock traffics', async () => { - await I.amOnPage('/form/fetch_call'); - await I.mockTraffic('https://reqres.in/api/comments/1', '{"name": "this was mocked" }'); - await I.startRecordingTraffic(); - await I.click('GET COMMENTS'); - await I.see('this was mocked'); - - await I.mockTraffic('https://reqres.in/api/comments/1', '{"name": "this was another mocked" }'); - await I.click('GET COMMENTS'); - await I.see('this was another mocked'); - - const traffics = await I.grabRecordedNetworkTraffics(); - 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' }); - - expect(traffics[1].url).to.equal('https://reqres.in/api/comments/1'); - expect(traffics[1].response.status).to.equal(200); - expect(traffics[1].response.body).to.contain({ name: 'this was another mocked' }); - }); - - it('should block traffics using a list of urls', async () => { - I.blockTraffic(['https://reqres.in/api/*', 'https://reqres.in/api/comments/*']); - I.amOnPage('/form/fetch_call'); - I.startRecordingTraffic(); - I.click('GET COMMENTS'); - I.see('Can not load data!'); - }); - - it('should block traffics of a given url', async () => { - I.blockTraffic('https://reqres.in/api/comments/*'); - I.amOnPage('/form/fetch_call'); - I.startRecordingTraffic(); - I.click('GET COMMENTS'); - I.see('Can not load data!'); - }); - - it('should check traffics with more advanced params', async () => { - await I.startRecordingTraffic(); - await I.amOnPage('https://openai.com/blog/chatgpt'); - const traffics = await I.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 I.seeTraffic({ - name: 'sentry event', - url: currentUrl.origin + currentUrl.pathname, - parameters: searchParams, - }); - - break; - } - } - }); - - it('should check traffics with more advanced post data', async () => { - I.amOnPage('https://openai.com/blog/chatgpt'); - I.startRecordingTraffic(); - await I.seeTraffic({ - name: 'event', - url: 'https://cloudflareinsights.com/cdn-cgi/rum', - requestPostData: { - st: 2, - }, - }); - }); - - it('should show error when advanced post data are not matching', async () => { - I.amOnPage('https://openai.com/blog/chatgpt'); - I.startRecordingTraffic(); - try { - await I.seeTraffic({ - name: 'event', - url: 'https://cloudflareinsights.com/cdn-cgi/rum', - requestPostData: { - st: 3, - }, - }); - } catch (e) { - expect(e.message).to.contain('actual value: "2"'); - } - }); - }); - describe('#makeApiRequest', () => { it('should make 3rd party API request', async () => { const response = await I.makeApiRequest('get', 'https://reqres.in/api/users?page=2'); diff --git a/test/helper/WebDriver_devtools_test.js b/test/helper/WebDriver_devtools_test.js index ed9bd4c36..b0b4329bb 100644 --- a/test/helper/WebDriver_devtools_test.js +++ b/test/helper/WebDriver_devtools_test.js @@ -1192,135 +1192,4 @@ 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/test/helper/WebDriver_test.js b/test/helper/WebDriver_test.js index c542dc9d8..c8581938b 100644 --- a/test/helper/WebDriver_test.js +++ b/test/helper/WebDriver_test.js @@ -18,6 +18,7 @@ const siteUrl = TestHelper.siteUrl(); let wd; console.log('Connecting to Selenium Server', TestHelper.seleniumAddress()); +process.env.isSelenium = 'true'; describe('WebDriver', function () { this.retries(1); diff --git a/test/helper/webapi.js b/test/helper/webapi.js index fc3853034..14688fda4 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -1,8 +1,8 @@ -let assert; let expect; +let assert; import('chai').then(chai => { - assert = chai.assert; expect = chai.expect; + assert = chai.assert; }); const path = require('path'); @@ -1612,6 +1612,125 @@ module.exports.tests = function () { }); }); + describe('#startRecordingTraffic, #seeTraffic, #stopRecordingTraffic, #dontSeeTraffic, #grabRecordedNetworkTraffics', () => { + beforeEach(function () { + if (isHelper('TestCafe') || process.env.isSelenium === 'true') this.skip(); + }); + + it('should throw error when calling seeTraffic before recording traffics', async () => { + try { + I.amOnPage('https://codecept.io/'); + await I.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 { + I.amOnPage('https://codecept.io/'); + await I.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 { + I.amOnPage('https://codecept.io/'); + await I.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 I.startRecordingTraffic(); + await I.amOnPage('https://codecept.io/'); + await I.flushNetworkTraffics(); + const traffics = await I.grabRecordedNetworkTraffics(); + expect(traffics.length).to.equal(0); + }); + + it('should see recording traffics', async () => { + I.startRecordingTraffic(); + I.amOnPage('https://codecept.io/'); + await I.seeTraffic({ name: 'traffics', url: 'https://codecept.io/img/companies/BC_LogoScreen_C.jpg' }); + }); + + it('should not see recording traffics', async () => { + I.startRecordingTraffic(); + I.amOnPage('https://codecept.io/'); + I.stopRecordingTraffic(); + await I.dontSeeTraffic({ name: 'traffics', url: 'https://codecept.io/img/companies/BC_LogoScreen_C.jpg' }); + }); + + it('should not see recording traffics using regex url', async () => { + I.startRecordingTraffic(); + I.amOnPage('https://codecept.io/'); + I.stopRecordingTraffic(); + await I.dontSeeTraffic({ name: 'traffics', url: /BC_LogoScreen_C.jpg/ }); + }); + + it('should throw error when calling dontSeeTraffic but missing name', async () => { + I.startRecordingTraffic(); + I.amOnPage('https://codecept.io/'); + I.stopRecordingTraffic(); + try { + await I.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 () => { + I.startRecordingTraffic(); + I.amOnPage('https://codecept.io/'); + I.stopRecordingTraffic(); + try { + await I.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 I.startRecordingTraffic(); + await I.amOnPage('https://openaI.com/blog/chatgpt'); + const traffics = await I.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 I.seeTraffic({ + name: 'sentry event', + url: currentUrl.origin + currentUrl.pathname, + parameters: searchParams, + }); + + break; + } + } + }); + + it.skip('should check traffics with more advanced post data', async () => { + await I.amOnPage('https://openaI.com/blog/chatgpt'); + await I.startRecordingTraffic(); + await I.seeTraffic({ + name: 'event', + url: 'https://region1.google-analytics.com', + requestPostData: { + st: 2, + }, + }); + }); + }); + describe('#startRecordingWebSocketMessages, #grabWebSocketMessages, #stopRecordingWebSocketMessages', () => { beforeEach(function () { if (isHelper('TestCafe') || isHelper('WebDriver') || process.env.BROWSER === 'firefox') this.skip();