codeceptjs 3.3.3 → 3.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,41 @@
1
+ ## 3.3.4
2
+
3
+ * Added support for masking fields in objects via `secret` function:
4
+
5
+ ```js
6
+ I.sendPostRequest('/auth', secret({ name: 'jon', password: '123456' }, 'password'));
7
+ ```
8
+ * Added [a guide about using of `secret`](/secrets) function
9
+ * [Appium] Use `touchClick` when interacting with elements in iOS. See #3317 by @mikk150
10
+ * [Playwright] Added `cdpConnection` option to connect over CDP. See #3309 by @Hmihaly
11
+ * [customLocator plugin] Allowed to specify multiple attributes for custom locator. Thanks to @aruiz-caritsqa
12
+
13
+ ```js
14
+ plugins: {
15
+ customLocator: {
16
+ enabled: true,
17
+ prefix: '$',
18
+ attribute: ['data-qa', 'data-test'],
19
+ }
20
+ }
21
+ ```
22
+ * [retryTo plugin] Fixed #3147 using `pollInterval` option. See #3351 by @cyonkee
23
+ * [Playwright] Fixed grabbing of browser console messages and window resize in new tab. Thanks to @mirao
24
+ * [REST] Added `prettyPrintJson` option to print JSON in nice way by @PeterNgTr
25
+ * [JSONResponse] Updated response validation to iterate over array items if response is array. Thanks to @PeterNgTr
26
+
27
+ ```js
28
+ // response.data == [
29
+ // { user: { name: 'jon', email: 'jon@doe.com' } },
30
+ // { user: { name: 'matt', email: 'matt@doe.com' } },
31
+ //]
32
+
33
+ I.seeResponseContainsKeys(['user']);
34
+ I.seeResponseContainsJson({ user: { email: 'jon@doe.com' } });
35
+ I.seeResponseContainsJson({ user: { email: 'matt@doe.com' } });
36
+ I.dontSeeResponseContainsJson({ user: 2 });
37
+ ```
38
+
1
39
  ## 3.3.3
2
40
 
3
41
  * Fixed `DataCloneError: () => could not be cloned` when running data tests in run-workers
package/docs/api.md CHANGED
@@ -263,6 +263,8 @@ The most basic thing to check in response is existence of keys in JSON object. U
263
263
  I.seeResponseContainsKeys(['name', 'email']);
264
264
  ```
265
265
 
266
+ > ℹ️ If response is an array, it will check that every element in array have provided keys
267
+
266
268
  However, this is a very naive approach. It won't work for arrays or nested objects.
267
269
  To check complex JSON structures `JSONResponse` helper uses [`joi`](https://joi.dev) library.
268
270
  It has rich API to validate JSON by the schema defined using JavaScript.
@@ -296,6 +298,8 @@ I.seeResponseContainsJson({
296
298
  })
297
299
  ```
298
300
 
301
+ > ℹ️ If response is an array, it will check that at least one element in array matches JSON
302
+
299
303
  To perform arbitrary assertions on a response object use `seeResponseValidByCallback`.
300
304
  It allows you to do any kind of assertions by using `expect` from [`chai`](https://www.chaijs.com) library.
301
305
 
package/docs/basics.md CHANGED
@@ -212,6 +212,8 @@ To fill in sensitive data use the `secret` function, it won't expose actual valu
212
212
  I.fillField('password', secret('123456'));
213
213
  ```
214
214
 
215
+ > ℹ️ Learn more about [masking secret](/secrets/) output
216
+
215
217
  ### Assertions
216
218
 
217
219
  In order to verify the expected behavior of a web application, its content should be checked.
@@ -1,3 +1,4 @@
1
+ const assert = require('assert');
1
2
  const chai = require('chai');
2
3
  const joi = require('joi');
3
4
  const chaiDeepMatch = require('chai-deep-match');
@@ -173,12 +174,30 @@ class JSONResponse extends Helper {
173
174
  *
174
175
  * I.seeResponseContainsJson({ user: { email: 'jon@doe.com' } });
175
176
  * ```
177
+ * If an array is received, checks that at least one element contains JSON
178
+ * ```js
179
+ * // response.data == [{ user: { name: 'jon', email: 'jon@doe.com' } }]
180
+ *
181
+ * I.seeResponseContainsJson({ user: { email: 'jon@doe.com' } });
182
+ * ```
176
183
  *
177
184
  * @param {object} json
178
185
  */
179
186
  seeResponseContainsJson(json = {}) {
180
187
  this._checkResponseReady();
181
- expect(this.response.data).to.deep.match(json);
188
+ if (Array.isArray(this.response.data)) {
189
+ let fails = 0;
190
+ for (const el of this.response.data) {
191
+ try {
192
+ expect(el).to.deep.match(json);
193
+ } catch (err) {
194
+ fails++;
195
+ }
196
+ }
197
+ assert.ok(fails < this.response.data.length, `No elements in array matched ${JSON.stringify(json)}`);
198
+ } else {
199
+ expect(this.response.data).to.deep.match(json);
200
+ }
182
201
  }
183
202
 
184
203
  /**
@@ -189,12 +208,22 @@ class JSONResponse extends Helper {
189
208
  *
190
209
  * I.dontSeeResponseContainsJson({ user: 2 });
191
210
  * ```
211
+ * If an array is received, checks that no element of array contains json:
212
+ * ```js
213
+ * // response.data == [{ user: 1 }, { user: 3 }]
214
+ *
215
+ * I.dontSeeResponseContainsJson({ user: 2 });
216
+ * ```
192
217
  *
193
218
  * @param {object} json
194
219
  */
195
220
  dontSeeResponseContainsJson(json = {}) {
196
221
  this._checkResponseReady();
197
- expect(this.response.data).not.to.deep.match(json);
222
+ if (Array.isArray(this.response.data)) {
223
+ this.response.data.forEach(data => expect(data).not.to.deep.match(json));
224
+ } else {
225
+ expect(this.response.data).not.to.deep.match(json);
226
+ }
198
227
  }
199
228
 
200
229
  /**
@@ -206,11 +235,23 @@ class JSONResponse extends Helper {
206
235
  * I.seeResponseContainsKeys(['user']);
207
236
  * ```
208
237
  *
238
+ * If an array is received, check is performed for each element of array:
239
+ *
240
+ * ```js
241
+ * // response.data == [{ user: 'jon' }, { user: 'matt'}]
242
+ *
243
+ * I.seeResponseContainsKeys(['user']);
244
+ * ```
245
+ *
209
246
  * @param {array} keys
210
247
  */
211
248
  seeResponseContainsKeys(keys = []) {
212
249
  this._checkResponseReady();
213
- expect(this.response.data).to.include.keys(keys);
250
+ if (Array.isArray(this.response.data)) {
251
+ this.response.data.forEach(data => expect(data).to.include.keys(keys));
252
+ } else {
253
+ expect(this.response.data).to.include.keys(keys);
254
+ }
214
255
  }
215
256
 
216
257
  /**
@@ -167,7 +167,8 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
167
167
  * Playwright: {
168
168
  * url: "http://localhost",
169
169
  * chromium: {
170
- * browserWSEndpoint: 'ws://localhost:9222/devtools/browser/c5aa6160-b5bc-4d53-bb49-6ecb36cd2e0a'
170
+ * browserWSEndpoint: 'ws://localhost:9222/devtools/browser/c5aa6160-b5bc-4d53-bb49-6ecb36cd2e0a',
171
+ * cdpConnection: false // default is false
171
172
  * }
172
173
  * }
173
174
  * }
@@ -272,6 +273,7 @@ class Playwright extends Helper {
272
273
  this.sessionPages = {};
273
274
  this.activeSessionName = '';
274
275
  this.isElectron = false;
276
+ this.isCDPConnection = false;
275
277
  this.electronSessions = [];
276
278
  this.storageState = null;
277
279
 
@@ -347,6 +349,7 @@ class Playwright extends Helper {
347
349
  this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint;
348
350
  this.isElectron = this.options.browser === 'electron';
349
351
  this.userDataDir = this.playwrightOptions.userDataDir;
352
+ this.isCDPConnection = this.playwrightOptions.cdpConnection;
350
353
  popupStore.defaultAction = this.options.defaultPopupAction;
351
354
  }
352
355
 
@@ -699,6 +702,15 @@ class Playwright extends Helper {
699
702
  async _startBrowser() {
700
703
  if (this.isElectron) {
701
704
  this.browser = await playwright._electron.launch(this.playwrightOptions);
705
+ } else if (this.isRemoteBrowser && this.isCDPConnection) {
706
+ try {
707
+ this.browser = await playwright[this.options.browser].connectOverCDP(this.playwrightOptions);
708
+ } catch (err) {
709
+ if (err.toString().indexOf('ECONNREFUSED')) {
710
+ throw new RemoteBrowserConnectionRefused(err);
711
+ }
712
+ throw err;
713
+ }
702
714
  } else if (this.isRemoteBrowser) {
703
715
  try {
704
716
  this.browser = await playwright[this.options.browser].connect(this.playwrightOptions);
@@ -1157,6 +1169,7 @@ class Playwright extends Helper {
1157
1169
  if (!page) {
1158
1170
  throw new Error(`There is no ability to switch to next tab with offset ${num}`);
1159
1171
  }
1172
+ targetCreatedHandler.call(this, page);
1160
1173
  await this._setPage(page);
1161
1174
  return this._waitForAction();
1162
1175
  }
@@ -1239,7 +1252,9 @@ class Playwright extends Helper {
1239
1252
  if (this.isElectron) {
1240
1253
  throw new Error('Cannot open new tabs inside an Electron container');
1241
1254
  }
1242
- await this._setPage(await this.browserContext.newPage(options));
1255
+ const page = await this.browserContext.newPage(options);
1256
+ targetCreatedHandler.call(this, page);
1257
+ await this._setPage(page);
1243
1258
  return this._waitForAction();
1244
1259
  }
1245
1260
 
@@ -2071,9 +2086,11 @@ class Playwright extends Helper {
2071
2086
  * Get JS log from browser.
2072
2087
  *
2073
2088
  * ```js
2074
- * let logs = await I.grabBrowserLogs();
2075
- * console.log(JSON.stringify(logs))
2089
+ * const logs = await I.grabBrowserLogs();
2090
+ * const errors = logs.map(l => ({ type: l.type(), text: l.text() })).filter(l => l.type === 'error');
2091
+ * console.log(JSON.stringify(errors));
2076
2092
  * ```
2093
+ * [Learn more about console messages](https://playwright.dev/docs/api/class-consolemessage)
2077
2094
  * @return {Promise<any[]>}
2078
2095
  */
2079
2096
  async grabBrowserLogs() {
@@ -2,6 +2,7 @@ const axios = require('axios').default;
2
2
  const Secret = require('../secret');
3
3
 
4
4
  const Helper = require('../helper');
5
+ const { beautify } = require('../utils');
5
6
 
6
7
  /**
7
8
  * REST helper allows to send additional requests to the REST API during acceptance tests.
@@ -10,6 +11,7 @@ const Helper = require('../helper');
10
11
  * ## Configuration
11
12
  *
12
13
  * * endpoint: API base URL
14
+ * * prettyPrintJson: pretty print json for response/request on console logs
13
15
  * * timeout: timeout for requests in milliseconds. 10000ms by default
14
16
  * * defaultHeaders: a list of default headers
15
17
  * * onRequest: a async function which can update request object.
@@ -22,6 +24,7 @@ const Helper = require('../helper');
22
24
  * helpers: {
23
25
  * REST: {
24
26
  * endpoint: 'http://site.com/api',
27
+ * prettyPrintJson: true,
25
28
  * onRequest: (request) => {
26
29
  * request.headers.auth = '123';
27
30
  * }
@@ -49,6 +52,7 @@ class REST extends Helper {
49
52
  timeout: 10000,
50
53
  defaultHeaders: {},
51
54
  endpoint: '',
55
+ prettyPrintJson: false,
52
56
  };
53
57
 
54
58
  if (this.options.maxContentLength) {
@@ -137,7 +141,7 @@ class REST extends Helper {
137
141
  await this.config.onRequest(request);
138
142
  }
139
143
 
140
- this.debugSection('Request', JSON.stringify(_debugRequest));
144
+ this.options.prettyPrintJson ? this.debugSection('Request', beautify(JSON.stringify(_debugRequest))) : this.debugSection('Request', JSON.stringify(_debugRequest));
141
145
 
142
146
  let response;
143
147
  try {
@@ -150,7 +154,7 @@ class REST extends Helper {
150
154
  if (this.config.onResponse) {
151
155
  await this.config.onResponse(response);
152
156
  }
153
- this.debugSection('Response', JSON.stringify(response.data));
157
+ this.options.prettyPrintJson ? this.debugSection('Response', beautify(JSON.stringify(response.data))) : this.debugSection('Response', JSON.stringify(response.data));
154
158
  return response;
155
159
  }
156
160
 
@@ -942,7 +942,7 @@ class WebDriver extends Helper {
942
942
  * {{ react }}
943
943
  */
944
944
  async click(locator, context = null) {
945
- const clickMethod = this.browser.isMobile ? 'touchClick' : 'elementClick';
945
+ const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick';
946
946
  const locateFn = prepareLocateFn.call(this, context);
947
947
 
948
948
  const res = await findClickable.call(this, locator, locateFn);
@@ -1296,7 +1296,7 @@ class WebDriver extends Helper {
1296
1296
  * Appium: not tested
1297
1297
  */
1298
1298
  async checkOption(field, context = null) {
1299
- const clickMethod = this.browser.isMobile ? 'touchClick' : 'elementClick';
1299
+ const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick';
1300
1300
  const locateFn = prepareLocateFn.call(this, context);
1301
1301
 
1302
1302
  const res = await findCheckable.call(this, field, locateFn);
@@ -1327,7 +1327,7 @@ class WebDriver extends Helper {
1327
1327
  * Appium: not tested
1328
1328
  */
1329
1329
  async uncheckOption(field, context = null) {
1330
- const clickMethod = this.browser.isMobile ? 'touchClick' : 'elementClick';
1330
+ const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick';
1331
1331
  const locateFn = prepareLocateFn.call(this, context);
1332
1332
 
1333
1333
  const res = await findCheckable.call(this, field, locateFn);
@@ -2212,7 +2212,7 @@ class WebDriver extends Helper {
2212
2212
  assertElementExists(res);
2213
2213
  const elem = usingFirstElement(res);
2214
2214
  const elementId = getElementId(elem);
2215
- if (this.browser.isMobile) return this.browser.touchScroll(offsetX, offsetY, elementId);
2215
+ if (this.browser.isMobile && this.browser.capabilities.platformName !== 'android') return this.browser.touchScroll(offsetX, offsetY, elementId);
2216
2216
  const location = await elem.getLocation();
2217
2217
  assertElementExists(location, 'Failed to receive', 'location');
2218
2218
  /* eslint-disable prefer-arrow-callback */
@@ -2220,7 +2220,7 @@ class WebDriver extends Helper {
2220
2220
  /* eslint-enable */
2221
2221
  }
2222
2222
 
2223
- if (this.browser.isMobile) return this.browser.touchScroll(locator, offsetX, offsetY);
2223
+ if (this.browser.isMobile && this.browser.capabilities.platformName !== 'android') return this.browser.touchScroll(locator, offsetX, offsetY);
2224
2224
 
2225
2225
  /* eslint-disable prefer-arrow-callback, comma-dangle */
2226
2226
  return this.browser.execute(function (x, y) { return window.scrollTo(x, y); }, offsetX, offsetY);
package/docs/changelog.md CHANGED
@@ -7,6 +7,44 @@ layout: Section
7
7
 
8
8
  # Releases
9
9
 
10
+ ## 3.3.4
11
+
12
+ * Added support for masking fields in objects via `secret` function:
13
+
14
+ ```js
15
+ I.sendPostRequest('/auth', secret({ name: 'jon', password: '123456' }, 'password'));
16
+ ```
17
+ * Added [a guide about using of `secret`](/secrets) function
18
+ * **[Appium]** Use `touchClick` when interacting with elements in iOS. See [#3317](https://github.com/codeceptjs/CodeceptJS/issues/3317) by **[mikk150](https://github.com/mikk150)**
19
+ * **[Playwright]** Added `cdpConnection` option to connect over CDP. See [#3309](https://github.com/codeceptjs/CodeceptJS/issues/3309) by **[Hmihaly](https://github.com/Hmihaly)**
20
+ * [customLocator plugin] Allowed to specify multiple attributes for custom locator. Thanks to **[aruiz-caritsqa](https://github.com/aruiz-caritsqa)**
21
+
22
+ ```js
23
+ plugins: {
24
+ customLocator: {
25
+ enabled: true,
26
+ prefix: '$',
27
+ attribute: ['data-qa', 'data-test'],
28
+ }
29
+ }
30
+ ```
31
+ * [retryTo plugin] Fixed [#3147](https://github.com/codeceptjs/CodeceptJS/issues/3147) using `pollInterval` option. See [#3351](https://github.com/codeceptjs/CodeceptJS/issues/3351) by **[cyonkee](https://github.com/cyonkee)**
32
+ * **[Playwright]** Fixed grabbing of browser console messages and window resize in new tab. Thanks to **[mirao](https://github.com/mirao)**
33
+ * **[REST]** Added `prettyPrintJson` option to print JSON in nice way by **[PeterNgTr](https://github.com/PeterNgTr)**
34
+ * **[JSONResponse]** Updated response validation to iterate over array items if response is array. Thanks to **[PeterNgTr](https://github.com/PeterNgTr)**
35
+
36
+ ```js
37
+ // response.data == [
38
+ // { user: { name: 'jon', email: 'jon@doe.com' } },
39
+ // { user: { name: 'matt', email: 'matt@doe.com' } },
40
+ //]
41
+
42
+ I.seeResponseContainsKeys(['user']);
43
+ I.seeResponseContainsJson({ user: { email: 'jon@doe.com' } });
44
+ I.seeResponseContainsJson({ user: { email: 'matt@doe.com' } });
45
+ I.dontSeeResponseContainsJson({ user: 2 });
46
+ ```
47
+
10
48
  ## 3.3.3
11
49
 
12
50
  * Fixed `DataCloneError: () => could not be cloned` when running data tests in run-workers
@@ -92,6 +92,14 @@ Checks for deep inclusion of a provided json in a response data.
92
92
  I.dontSeeResponseContainsJson({ user: 2 });
93
93
  ```
94
94
 
95
+ If an array is received, checks that no element of array contains json:
96
+
97
+ ```js
98
+ // response.data == [{ user: 1 }, { user: 3 }]
99
+
100
+ I.dontSeeResponseContainsJson({ user: 2 });
101
+ ```
102
+
95
103
  #### Parameters
96
104
 
97
105
  - `json` **[object][2]**
@@ -139,6 +147,14 @@ Checks for deep inclusion of a provided json in a response data.
139
147
  I.seeResponseContainsJson({ user: { email: 'jon@doe.com' } });
140
148
  ```
141
149
 
150
+ If an array is received, checks that at least one element contains JSON
151
+
152
+ ```js
153
+ // response.data == [{ user: { name: 'jon', email: 'jon@doe.com' } }]
154
+
155
+ I.seeResponseContainsJson({ user: { email: 'jon@doe.com' } });
156
+ ```
157
+
142
158
  #### Parameters
143
159
 
144
160
  - `json` **[object][2]**
@@ -153,6 +169,14 @@ Checks for deep inclusion of a provided json in a response data.
153
169
  I.seeResponseContainsKeys(['user']);
154
170
  ```
155
171
 
172
+ If an array is received, check is performed for each element of array:
173
+
174
+ ```js
175
+ // response.data == [{ user: 'jon' }, { user: 'matt'}]
176
+
177
+ I.seeResponseContainsKeys(['user']);
178
+ ```
179
+
156
180
  #### Parameters
157
181
 
158
182
  - `keys` **[array][3]**
@@ -131,7 +131,8 @@ Traces will be saved to `output/trace`
131
131
  Playwright: {
132
132
  url: "http://localhost",
133
133
  chromium: {
134
- browserWSEndpoint: 'ws://localhost:9222/devtools/browser/c5aa6160-b5bc-4d53-bb49-6ecb36cd2e0a'
134
+ browserWSEndpoint: 'ws://localhost:9222/devtools/browser/c5aa6160-b5bc-4d53-bb49-6ecb36cd2e0a',
135
+ cdpConnection: false // default is false
135
136
  }
136
137
  }
137
138
  }
@@ -811,10 +812,13 @@ Returns **[Promise][18]&lt;[Array][19]&lt;[string][12]>>** attribute value
811
812
  Get JS log from browser.
812
813
 
813
814
  ```js
814
- let logs = await I.grabBrowserLogs();
815
- console.log(JSON.stringify(logs))
815
+ const logs = await I.grabBrowserLogs();
816
+ const errors = logs.map(l => ({ type: l.type(), text: l.text() })).filter(l => l.type === 'error');
817
+ console.log(JSON.stringify(errors));
816
818
  ```
817
819
 
820
+ [Learn more about console messages][20]
821
+
818
822
  Returns **[Promise][18]&lt;[Array][19]&lt;any>>**
819
823
 
820
824
  ### grabCookie
@@ -1102,7 +1106,7 @@ Returns **[Promise][18]&lt;[Array][19]&lt;[string][12]>>** attribute value
1102
1106
  Handles a file download.Aa file name is required to save the file on disk.
1103
1107
  Files are saved to "output" directory.
1104
1108
 
1105
- Should be used with [FileSystem helper][20] to check that file were downloaded correctly.
1109
+ Should be used with [FileSystem helper][21] to check that file were downloaded correctly.
1106
1110
 
1107
1111
  ```js
1108
1112
  I.handleDownloads('downloads/avatar.jpg');
@@ -1133,7 +1137,7 @@ I.haveRequestHeaders({
1133
1137
 
1134
1138
  ### makeApiRequest
1135
1139
 
1136
- Performs [api request][21] using
1140
+ Performs [api request][22] using
1137
1141
  the cookies from the current browser session.
1138
1142
 
1139
1143
  ```js
@@ -1154,17 +1158,17 @@ Returns **[Promise][18]&lt;[object][10]>** response
1154
1158
 
1155
1159
  ### mockRoute
1156
1160
 
1157
- Mocks network request using [`browserContext.route`][22] of Playwright
1161
+ Mocks network request using [`browserContext.route`][23] of Playwright
1158
1162
 
1159
1163
  ```js
1160
1164
  I.mockRoute(/(.png$)|(.jpg$)/, route => route.abort());
1161
1165
  ```
1162
1166
 
1163
- This method allows intercepting and mocking requests & responses. [Learn more about it][23]
1167
+ This method allows intercepting and mocking requests & responses. [Learn more about it][24]
1164
1168
 
1165
1169
  #### Parameters
1166
1170
 
1167
- - `url` **([string][12] | [RegExp][24])?** URL, regex or pattern for to match URL
1171
+ - `url` **([string][12] | [RegExp][25])?** URL, regex or pattern for to match URL
1168
1172
  - `handler` **[function][17]?** a function to process reques
1169
1173
 
1170
1174
  ### moveCursorTo
@@ -1192,7 +1196,7 @@ Open new tab and automatically switched to new tab
1192
1196
  I.openNewTab();
1193
1197
  ```
1194
1198
 
1195
- You can pass in [page options][25] to emulate device on this page
1199
+ You can pass in [page options][26] to emulate device on this page
1196
1200
 
1197
1201
  ```js
1198
1202
  // enable mobile
@@ -1207,7 +1211,7 @@ I.openNewTab({ isMobile: true });
1207
1211
 
1208
1212
  Presses a key in the browser (on a focused element).
1209
1213
 
1210
- _Hint:_ For populating text field or textarea, it is recommended to use [`fillField`][26].
1214
+ _Hint:_ For populating text field or textarea, it is recommended to use [`fillField`][27].
1211
1215
 
1212
1216
  ```js
1213
1217
  I.pressKey('Backspace');
@@ -1267,13 +1271,13 @@ Some of the supported key names are:
1267
1271
  #### Parameters
1268
1272
 
1269
1273
  - `key` **([string][12] | [Array][19]&lt;[string][12]>)** key or array of keys to press.
1270
- [!] returns a _promise_ which is synchronized internally by recorder_Note:_ Shortcuts like `'Meta'` + `'A'` do not work on macOS ([GoogleChrome/Puppeteer#1313][27]).
1274
+ [!] returns a _promise_ which is synchronized internally by recorder_Note:_ Shortcuts like `'Meta'` + `'A'` do not work on macOS ([GoogleChrome/Puppeteer#1313][28]).
1271
1275
 
1272
1276
  ### pressKeyDown
1273
1277
 
1274
1278
  Presses a key in the browser and leaves it in a down state.
1275
1279
 
1276
- To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][28]).
1280
+ To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][29]).
1277
1281
 
1278
1282
  ```js
1279
1283
  I.pressKeyDown('Control');
@@ -1290,7 +1294,7 @@ I.pressKeyUp('Control');
1290
1294
 
1291
1295
  Releases a key in the browser which was previously set to a down state.
1292
1296
 
1293
- To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][28]).
1297
+ To make combinations with modifier key and user operation (e.g. `'Control'` + [`click`][29]).
1294
1298
 
1295
1299
  ```js
1296
1300
  I.pressKeyDown('Control');
@@ -1378,7 +1382,7 @@ I.saveScreenshot('debug.png', true) //resizes to available scrollHeight and scro
1378
1382
  #### Parameters
1379
1383
 
1380
1384
  - `fileName` **[string][12]** file name to save.
1381
- - `fullPage` **[boolean][29]** (optional, `false` by default) flag to enable fullscreen screenshot mode.
1385
+ - `fullPage` **[boolean][30]** (optional, `false` by default) flag to enable fullscreen screenshot mode.
1382
1386
  [!] returns a _promise_ which is synchronized internally by recorder
1383
1387
 
1384
1388
  ### scrollPageToBottom
@@ -1724,7 +1728,7 @@ If no handler is passed, all mock requests for the rote are disabled.
1724
1728
 
1725
1729
  #### Parameters
1726
1730
 
1727
- - `url` **([string][12] | [RegExp][24])?** URL, regex or pattern for to match URL
1731
+ - `url` **([string][12] | [RegExp][25])?** URL, regex or pattern for to match URL
1728
1732
  - `handler` **[function][17]?** a function to process reques
1729
1733
 
1730
1734
  ### switchTo
@@ -1771,7 +1775,7 @@ I.switchToPreviousTab(2);
1771
1775
 
1772
1776
  Types out the given text into an active field.
1773
1777
  To slow down typing use a second parameter, to set interval between key presses.
1774
- _Note:_ Should be used when [`fillField`][26] is not an option.
1778
+ _Note:_ Should be used when [`fillField`][27] is not an option.
1775
1779
 
1776
1780
  ```js
1777
1781
  // passing in a string
@@ -1808,7 +1812,7 @@ I.uncheckOption('agree', '//form');
1808
1812
 
1809
1813
  - `field` **([string][12] | [object][10])** checkbox located by label | name | CSS | XPath | strict locator.
1810
1814
  - `context` **([string][12]? | [object][10])** (optional, `null` by default) element located by CSS | XPath | strict locator.
1811
- [!] returns a _promise_ which is synchronized internally by recorder[Additional options][30] for uncheck available as 3rd argument.Examples:```js
1815
+ [!] returns a _promise_ which is synchronized internally by recorder[Additional options][31] for uncheck available as 3rd argument.Examples:```js
1812
1816
  // click on element at position
1813
1817
  I.uncheckOption('Agree', '.signup', { position: { x: 5, y: 5 } })
1814
1818
  ```> ⚠️ To avoid flakiness, option `force: true` is set by default
@@ -1821,7 +1825,7 @@ Use Playwright API inside a test.
1821
1825
  First argument is a description of an action.
1822
1826
  Second argument is async function that gets this helper as parameter.
1823
1827
 
1824
- { [`page`][31], [`browserContext`][32] [`browser`][33] } objects from Playwright API are available.
1828
+ { [`page`][32], [`browserContext`][33] [`browser`][34] } objects from Playwright API are available.
1825
1829
 
1826
1830
  ```js
1827
1831
  I.usePlaywrightTo('emulate offline mode', async ({ browserContext }) => {
@@ -1947,7 +1951,7 @@ I.waitForInvisible('#popup');
1947
1951
 
1948
1952
  Waits for navigation to finish. By default takes configured `waitForNavigation` option.
1949
1953
 
1950
- See [Playwright's reference][34]
1954
+ See [Playwright's reference][35]
1951
1955
 
1952
1956
  #### Parameters
1953
1957
 
@@ -2027,7 +2031,7 @@ I.waitForVisible('#popup');
2027
2031
 
2028
2032
  - `locator` **([string][12] | [object][10])** element located by CSS|XPath|strict locator.
2029
2033
  - `sec` **[number][16]** (optional, `1` by default) time in seconds to wait
2030
- [!] returns a _promise_ which is synchronized internally by recorderThis method accepts [React selectors][35].
2034
+ [!] returns a _promise_ which is synchronized internally by recorderThis method accepts [React selectors][36].
2031
2035
 
2032
2036
  ### waitInUrl
2033
2037
 
@@ -2126,34 +2130,36 @@ I.waitUrlEquals('http://127.0.0.1:8000/info');
2126
2130
 
2127
2131
  [19]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
2128
2132
 
2129
- [20]: https://codecept.io/helpers/FileSystem
2133
+ [20]: https://playwright.dev/docs/api/class-consolemessage
2134
+
2135
+ [21]: https://codecept.io/helpers/FileSystem
2130
2136
 
2131
- [21]: https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-get
2137
+ [22]: https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-get
2132
2138
 
2133
- [22]: https://playwright.dev/docs/api/class-browsercontext#browser-context-route
2139
+ [23]: https://playwright.dev/docs/api/class-browsercontext#browser-context-route
2134
2140
 
2135
- [23]: https://playwright.dev/docs/network#handle-requests
2141
+ [24]: https://playwright.dev/docs/network#handle-requests
2136
2142
 
2137
- [24]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp
2143
+ [25]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp
2138
2144
 
2139
- [25]: https://github.com/microsoft/playwright/blob/main/docs/api.md#browsernewpageoptions
2145
+ [26]: https://github.com/microsoft/playwright/blob/main/docs/api.md#browsernewpageoptions
2140
2146
 
2141
- [26]: #fillfield
2147
+ [27]: #fillfield
2142
2148
 
2143
- [27]: https://github.com/GoogleChrome/puppeteer/issues/1313
2149
+ [28]: https://github.com/GoogleChrome/puppeteer/issues/1313
2144
2150
 
2145
- [28]: #click
2151
+ [29]: #click
2146
2152
 
2147
- [29]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
2153
+ [30]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
2148
2154
 
2149
- [30]: https://playwright.dev/docs/api/class-elementhandle#element-handle-uncheck
2155
+ [31]: https://playwright.dev/docs/api/class-elementhandle#element-handle-uncheck
2150
2156
 
2151
- [31]: https://github.com/microsoft/playwright/blob/main/docs/src/api/class-page.md
2157
+ [32]: https://github.com/microsoft/playwright/blob/main/docs/src/api/class-page.md
2152
2158
 
2153
- [32]: https://github.com/microsoft/playwright/blob/main/docs/src/api/class-browsercontext.md
2159
+ [33]: https://github.com/microsoft/playwright/blob/main/docs/src/api/class-browsercontext.md
2154
2160
 
2155
- [33]: https://github.com/microsoft/playwright/blob/main/docs/src/api/class-browser.md
2161
+ [34]: https://github.com/microsoft/playwright/blob/main/docs/src/api/class-browser.md
2156
2162
 
2157
- [34]: https://playwright.dev/docs/api/class-page?_highlight=waitfornavi#pagewaitfornavigationoptions
2163
+ [35]: https://playwright.dev/docs/api/class-page?_highlight=waitfornavi#pagewaitfornavigationoptions
2158
2164
 
2159
- [35]: https://codecept.io/react
2165
+ [36]: https://codecept.io/react
@@ -17,6 +17,7 @@ REST helper allows to send additional requests to the REST API during acceptance
17
17
  ## Configuration
18
18
 
19
19
  - endpoint: API base URL
20
+ - prettyPrintJson: pretty print json for response/request on console logs
20
21
  - timeout: timeout for requests in milliseconds. 10000ms by default
21
22
  - defaultHeaders: a list of default headers
22
23
  - onRequest: a async function which can update request object.
@@ -29,6 +30,7 @@ REST helper allows to send additional requests to the REST API during acceptance
29
30
  helpers: {
30
31
  REST: {
31
32
  endpoint: 'http://site.com/api',
33
+ prettyPrintJson: true,
32
34
  onRequest: (request) => {
33
35
  request.headers.auth = '123';
34
36
  }
package/docs/plugins.md CHANGED
@@ -512,6 +512,46 @@ I.seeElement('=user'); // matches => [data-qa=user]
512
512
  I.click('=sign-up'); // matches => [data-qa=sign-up]
513
513
  ```
514
514
 
515
+ Using `data-qa` OR `data-test` attribute with `=` prefix:
516
+
517
+ ```js
518
+ // in codecept.conf.js
519
+ plugins: {
520
+ customLocator: {
521
+ enabled: true,
522
+ prefix: '=',
523
+ attribute: ['data-qa', 'data-test'],
524
+ strategy: 'xpath'
525
+ }
526
+ }
527
+ ```
528
+
529
+ In a test:
530
+
531
+ ```js
532
+ I.seeElement('=user'); // matches => //*[@data-qa=user or @data-test=user]
533
+ I.click('=sign-up'); // matches => //*[data-qa=sign-up or @data-test=sign-up]
534
+ ```
535
+
536
+ ```js
537
+ // in codecept.conf.js
538
+ plugins: {
539
+ customLocator: {
540
+ enabled: true,
541
+ prefix: '=',
542
+ attribute: ['data-qa', 'data-test'],
543
+ strategy: 'css'
544
+ }
545
+ }
546
+ ```
547
+
548
+ In a test:
549
+
550
+ ```js
551
+ I.seeElement('=user'); // matches => [data-qa=user],[data-test=user]
552
+ I.click('=sign-up'); // matches => [data-qa=sign-up],[data-test=sign-up]
553
+ ```
554
+
515
555
  ### Parameters
516
556
 
517
557
  - `config`
@@ -682,7 +722,7 @@ Run tests with plugin enabled:
682
722
  plugins: {
683
723
  retryFailedStep: {
684
724
  enabled: true,
685
- ignoreSteps: [
725
+ ignoredSteps: [
686
726
  'scroll*', // ignore all scroll steps
687
727
  /Cookie/, // ignore all steps with a Cookie in it (by regexp)
688
728
  ]
@@ -0,0 +1,30 @@
1
+ # Secrets
2
+
3
+ It is possible to mask out sensitive data when passing it to steps. This is important when filling password fields, or sending secure keys to API endpoint.
4
+
5
+ Wrap data in `secret` function to mask sensitive values in output and logs.
6
+
7
+ For basic string `secret` just wrap a value into a string:
8
+
9
+ ```js
10
+ I.fillField('password', secret('123456'));
11
+ ```
12
+
13
+ When executed it will be printed like this:
14
+
15
+ ```
16
+ I fill field "password" "*****"
17
+ ```
18
+
19
+ For an object, which can be a payload to POST request, specify which fields should be masked:
20
+
21
+ ```js
22
+ I.sendPostRequest('/login', secret({
23
+ name: 'davert',
24
+ password: '123456'
25
+ }, 'password'))
26
+ ```
27
+
28
+ The object created from `secret` is as Proxy to the object passed in. When printed password will be replaced with ****.
29
+
30
+ > ⚠️ Only direct properties of the object can be masked via `secret`
@@ -21,7 +21,7 @@ module.exports = async function (path, options) {
21
21
 
22
22
  if (options.verbose) output.level(3);
23
23
 
24
- output.print('String interactive shell for current suite...');
24
+ output.print('Starting interactive shell for current suite...');
25
25
  recorder.start();
26
26
  event.emit(event.suite.before, {
27
27
  fullTitle: () => 'Interactive Shell',
@@ -1,3 +1,4 @@
1
+ const assert = require('assert');
1
2
  const chai = require('chai');
2
3
  const joi = require('joi');
3
4
  const chaiDeepMatch = require('chai-deep-match');
@@ -173,12 +174,30 @@ class JSONResponse extends Helper {
173
174
  *
174
175
  * I.seeResponseContainsJson({ user: { email: 'jon@doe.com' } });
175
176
  * ```
177
+ * If an array is received, checks that at least one element contains JSON
178
+ * ```js
179
+ * // response.data == [{ user: { name: 'jon', email: 'jon@doe.com' } }]
180
+ *
181
+ * I.seeResponseContainsJson({ user: { email: 'jon@doe.com' } });
182
+ * ```
176
183
  *
177
184
  * @param {object} json
178
185
  */
179
186
  seeResponseContainsJson(json = {}) {
180
187
  this._checkResponseReady();
181
- expect(this.response.data).to.deep.match(json);
188
+ if (Array.isArray(this.response.data)) {
189
+ let fails = 0;
190
+ for (const el of this.response.data) {
191
+ try {
192
+ expect(el).to.deep.match(json);
193
+ } catch (err) {
194
+ fails++;
195
+ }
196
+ }
197
+ assert.ok(fails < this.response.data.length, `No elements in array matched ${JSON.stringify(json)}`);
198
+ } else {
199
+ expect(this.response.data).to.deep.match(json);
200
+ }
182
201
  }
183
202
 
184
203
  /**
@@ -189,12 +208,22 @@ class JSONResponse extends Helper {
189
208
  *
190
209
  * I.dontSeeResponseContainsJson({ user: 2 });
191
210
  * ```
211
+ * If an array is received, checks that no element of array contains json:
212
+ * ```js
213
+ * // response.data == [{ user: 1 }, { user: 3 }]
214
+ *
215
+ * I.dontSeeResponseContainsJson({ user: 2 });
216
+ * ```
192
217
  *
193
218
  * @param {object} json
194
219
  */
195
220
  dontSeeResponseContainsJson(json = {}) {
196
221
  this._checkResponseReady();
197
- expect(this.response.data).not.to.deep.match(json);
222
+ if (Array.isArray(this.response.data)) {
223
+ this.response.data.forEach(data => expect(data).not.to.deep.match(json));
224
+ } else {
225
+ expect(this.response.data).not.to.deep.match(json);
226
+ }
198
227
  }
199
228
 
200
229
  /**
@@ -206,11 +235,23 @@ class JSONResponse extends Helper {
206
235
  * I.seeResponseContainsKeys(['user']);
207
236
  * ```
208
237
  *
238
+ * If an array is received, check is performed for each element of array:
239
+ *
240
+ * ```js
241
+ * // response.data == [{ user: 'jon' }, { user: 'matt'}]
242
+ *
243
+ * I.seeResponseContainsKeys(['user']);
244
+ * ```
245
+ *
209
246
  * @param {array} keys
210
247
  */
211
248
  seeResponseContainsKeys(keys = []) {
212
249
  this._checkResponseReady();
213
- expect(this.response.data).to.include.keys(keys);
250
+ if (Array.isArray(this.response.data)) {
251
+ this.response.data.forEach(data => expect(data).to.include.keys(keys));
252
+ } else {
253
+ expect(this.response.data).to.include.keys(keys);
254
+ }
214
255
  }
215
256
 
216
257
  /**
@@ -167,7 +167,8 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
167
167
  * Playwright: {
168
168
  * url: "http://localhost",
169
169
  * chromium: {
170
- * browserWSEndpoint: 'ws://localhost:9222/devtools/browser/c5aa6160-b5bc-4d53-bb49-6ecb36cd2e0a'
170
+ * browserWSEndpoint: 'ws://localhost:9222/devtools/browser/c5aa6160-b5bc-4d53-bb49-6ecb36cd2e0a',
171
+ * cdpConnection: false // default is false
171
172
  * }
172
173
  * }
173
174
  * }
@@ -272,6 +273,7 @@ class Playwright extends Helper {
272
273
  this.sessionPages = {};
273
274
  this.activeSessionName = '';
274
275
  this.isElectron = false;
276
+ this.isCDPConnection = false;
275
277
  this.electronSessions = [];
276
278
  this.storageState = null;
277
279
 
@@ -347,6 +349,7 @@ class Playwright extends Helper {
347
349
  this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint;
348
350
  this.isElectron = this.options.browser === 'electron';
349
351
  this.userDataDir = this.playwrightOptions.userDataDir;
352
+ this.isCDPConnection = this.playwrightOptions.cdpConnection;
350
353
  popupStore.defaultAction = this.options.defaultPopupAction;
351
354
  }
352
355
 
@@ -692,6 +695,15 @@ class Playwright extends Helper {
692
695
  async _startBrowser() {
693
696
  if (this.isElectron) {
694
697
  this.browser = await playwright._electron.launch(this.playwrightOptions);
698
+ } else if (this.isRemoteBrowser && this.isCDPConnection) {
699
+ try {
700
+ this.browser = await playwright[this.options.browser].connectOverCDP(this.playwrightOptions);
701
+ } catch (err) {
702
+ if (err.toString().indexOf('ECONNREFUSED')) {
703
+ throw new RemoteBrowserConnectionRefused(err);
704
+ }
705
+ throw err;
706
+ }
695
707
  } else if (this.isRemoteBrowser) {
696
708
  try {
697
709
  this.browser = await playwright[this.options.browser].connect(this.playwrightOptions);
@@ -1054,6 +1066,7 @@ class Playwright extends Helper {
1054
1066
  if (!page) {
1055
1067
  throw new Error(`There is no ability to switch to next tab with offset ${num}`);
1056
1068
  }
1069
+ targetCreatedHandler.call(this, page);
1057
1070
  await this._setPage(page);
1058
1071
  return this._waitForAction();
1059
1072
  }
@@ -1136,7 +1149,9 @@ class Playwright extends Helper {
1136
1149
  if (this.isElectron) {
1137
1150
  throw new Error('Cannot open new tabs inside an Electron container');
1138
1151
  }
1139
- await this._setPage(await this.browserContext.newPage(options));
1152
+ const page = await this.browserContext.newPage(options);
1153
+ targetCreatedHandler.call(this, page);
1154
+ await this._setPage(page);
1140
1155
  return this._waitForAction();
1141
1156
  }
1142
1157
 
@@ -1558,9 +1573,11 @@ class Playwright extends Helper {
1558
1573
  * Get JS log from browser.
1559
1574
  *
1560
1575
  * ```js
1561
- * let logs = await I.grabBrowserLogs();
1562
- * console.log(JSON.stringify(logs))
1576
+ * const logs = await I.grabBrowserLogs();
1577
+ * const errors = logs.map(l => ({ type: l.type(), text: l.text() })).filter(l => l.type === 'error');
1578
+ * console.log(JSON.stringify(errors));
1563
1579
  * ```
1580
+ * [Learn more about console messages](https://playwright.dev/docs/api/class-consolemessage)
1564
1581
  * @return {Promise<any[]>}
1565
1582
  */
1566
1583
  async grabBrowserLogs() {
@@ -2,6 +2,7 @@ const axios = require('axios').default;
2
2
  const Secret = require('../secret');
3
3
 
4
4
  const Helper = require('../helper');
5
+ const { beautify } = require('../utils');
5
6
 
6
7
  /**
7
8
  * REST helper allows to send additional requests to the REST API during acceptance tests.
@@ -10,6 +11,7 @@ const Helper = require('../helper');
10
11
  * ## Configuration
11
12
  *
12
13
  * * endpoint: API base URL
14
+ * * prettyPrintJson: pretty print json for response/request on console logs
13
15
  * * timeout: timeout for requests in milliseconds. 10000ms by default
14
16
  * * defaultHeaders: a list of default headers
15
17
  * * onRequest: a async function which can update request object.
@@ -22,6 +24,7 @@ const Helper = require('../helper');
22
24
  * helpers: {
23
25
  * REST: {
24
26
  * endpoint: 'http://site.com/api',
27
+ * prettyPrintJson: true,
25
28
  * onRequest: (request) => {
26
29
  * request.headers.auth = '123';
27
30
  * }
@@ -49,6 +52,7 @@ class REST extends Helper {
49
52
  timeout: 10000,
50
53
  defaultHeaders: {},
51
54
  endpoint: '',
55
+ prettyPrintJson: false,
52
56
  };
53
57
 
54
58
  if (this.options.maxContentLength) {
@@ -137,7 +141,7 @@ class REST extends Helper {
137
141
  await this.config.onRequest(request);
138
142
  }
139
143
 
140
- this.debugSection('Request', JSON.stringify(_debugRequest));
144
+ this.options.prettyPrintJson ? this.debugSection('Request', beautify(JSON.stringify(_debugRequest))) : this.debugSection('Request', JSON.stringify(_debugRequest));
141
145
 
142
146
  let response;
143
147
  try {
@@ -150,7 +154,7 @@ class REST extends Helper {
150
154
  if (this.config.onResponse) {
151
155
  await this.config.onResponse(response);
152
156
  }
153
- this.debugSection('Response', JSON.stringify(response.data));
157
+ this.options.prettyPrintJson ? this.debugSection('Response', beautify(JSON.stringify(response.data))) : this.debugSection('Response', JSON.stringify(response.data));
154
158
  return response;
155
159
  }
156
160
 
@@ -907,7 +907,7 @@ class WebDriver extends Helper {
907
907
  * {{ react }}
908
908
  */
909
909
  async click(locator, context = null) {
910
- const clickMethod = this.browser.isMobile ? 'touchClick' : 'elementClick';
910
+ const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick';
911
911
  const locateFn = prepareLocateFn.call(this, context);
912
912
 
913
913
  const res = await findClickable.call(this, locator, locateFn);
@@ -1117,7 +1117,7 @@ class WebDriver extends Helper {
1117
1117
  * Appium: not tested
1118
1118
  */
1119
1119
  async checkOption(field, context = null) {
1120
- const clickMethod = this.browser.isMobile ? 'touchClick' : 'elementClick';
1120
+ const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick';
1121
1121
  const locateFn = prepareLocateFn.call(this, context);
1122
1122
 
1123
1123
  const res = await findCheckable.call(this, field, locateFn);
@@ -1136,7 +1136,7 @@ class WebDriver extends Helper {
1136
1136
  * Appium: not tested
1137
1137
  */
1138
1138
  async uncheckOption(field, context = null) {
1139
- const clickMethod = this.browser.isMobile ? 'touchClick' : 'elementClick';
1139
+ const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick';
1140
1140
  const locateFn = prepareLocateFn.call(this, context);
1141
1141
 
1142
1142
  const res = await findCheckable.call(this, field, locateFn);
@@ -1623,7 +1623,7 @@ class WebDriver extends Helper {
1623
1623
  assertElementExists(res);
1624
1624
  const elem = usingFirstElement(res);
1625
1625
  const elementId = getElementId(elem);
1626
- if (this.browser.isMobile) return this.browser.touchScroll(offsetX, offsetY, elementId);
1626
+ if (this.browser.isMobile && this.browser.capabilities.platformName !== 'android') return this.browser.touchScroll(offsetX, offsetY, elementId);
1627
1627
  const location = await elem.getLocation();
1628
1628
  assertElementExists(location, 'Failed to receive', 'location');
1629
1629
  /* eslint-disable prefer-arrow-callback */
@@ -1631,7 +1631,7 @@ class WebDriver extends Helper {
1631
1631
  /* eslint-enable */
1632
1632
  }
1633
1633
 
1634
- if (this.browser.isMobile) return this.browser.touchScroll(locator, offsetX, offsetY);
1634
+ if (this.browser.isMobile && this.browser.capabilities.platformName !== 'android') return this.browser.touchScroll(locator, offsetX, offsetY);
1635
1635
 
1636
1636
  /* eslint-disable prefer-arrow-callback, comma-dangle */
1637
1637
  return this.browser.execute(function (x, y) { return window.scrollTo(x, y); }, offsetX, offsetY);
@@ -70,23 +70,70 @@ const defaultConfig = {
70
70
  * I.seeElement('=user'); // matches => [data-qa=user]
71
71
  * I.click('=sign-up'); // matches => [data-qa=sign-up]
72
72
  * ```
73
+ *
74
+ * Using `data-qa` OR `data-test` attribute with `=` prefix:
75
+ *
76
+ * ```js
77
+ * // in codecept.conf.js
78
+ * plugins: {
79
+ * customLocator: {
80
+ * enabled: true,
81
+ * prefix: '=',
82
+ * attribute: ['data-qa', 'data-test'],
83
+ * strategy: 'xpath'
84
+ * }
85
+ * }
86
+ * ```
87
+ *
88
+ * In a test:
89
+ *
90
+ * ```js
91
+ * I.seeElement('=user'); // matches => //*[@data-qa=user or @data-test=user]
92
+ * I.click('=sign-up'); // matches => //*[data-qa=sign-up or @data-test=sign-up]
93
+ * ```
94
+ *
95
+ * ```js
96
+ * // in codecept.conf.js
97
+ * plugins: {
98
+ * customLocator: {
99
+ * enabled: true,
100
+ * prefix: '=',
101
+ * attribute: ['data-qa', 'data-test'],
102
+ * strategy: 'css'
103
+ * }
104
+ * }
105
+ * ```
106
+ *
107
+ * In a test:
108
+ *
109
+ * ```js
110
+ * I.seeElement('=user'); // matches => [data-qa=user],[data-test=user]
111
+ * I.click('=sign-up'); // matches => [data-qa=sign-up],[data-test=sign-up]
112
+ * ```
73
113
  */
74
114
  module.exports = (config) => {
75
- config = Object.assign(defaultConfig, config);
115
+ config = { ...defaultConfig, ...config };
76
116
 
77
117
  Locator.addFilter((value, locatorObj) => {
78
118
  if (typeof value !== 'string') return;
79
119
  if (!value.startsWith(config.prefix)) return;
80
120
 
121
+ if (!['String', 'Array'].includes(config.attribute.constructor.name)) return;
122
+
81
123
  const val = value.substr(config.prefix.length);
82
124
 
83
125
  if (config.strategy.toLowerCase() === 'xpath') {
84
- locatorObj.value = `.//*[@${config.attribute}=${xpathLocator.literal(val)}]`;
126
+ locatorObj.value = `.//*[${
127
+ [].concat(config.attribute)
128
+ .map((attr) => `@${attr}=${xpathLocator.literal(val)}`)
129
+ .join(' or ')}]`;
85
130
  locatorObj.type = 'xpath';
86
131
  }
87
132
 
88
133
  if (config.strategy.toLowerCase() === 'css') {
89
- locatorObj.value = `[${config.attribute}=${val}]`;
134
+ locatorObj.value = [].concat(config.attribute)
135
+ .map((attr) => `[${attr}=${val}]`)
136
+ .join(',');
90
137
  locatorObj.type = 'css';
91
138
  }
92
139
 
@@ -60,7 +60,7 @@ const defaultConfig = {
60
60
  * plugins: {
61
61
  * retryFailedStep: {
62
62
  * enabled: true,
63
- * ignoreSteps: [
63
+ * ignoredSteps: [
64
64
  * 'scroll*', // ignore all scroll steps
65
65
  * /Cookie/, // ignore all steps with a Cookie in it (by regexp)
66
66
  * ]
@@ -101,27 +101,20 @@ module.exports = function (config) {
101
101
  err = e;
102
102
  recorder.session.restore(`retryTo ${tries}`);
103
103
  tries++;
104
- // recorder.session.restore(`retryTo`);
105
104
  if (tries <= maxTries) {
106
105
  debug(`Error ${err}... Retrying`);
107
106
  err = null;
108
107
 
109
- recorder.add(`retryTo ${tries}`, () => {
110
- tryBlock();
111
- // recorder.add(() => new Promise(done => setTimeout(done, pollInterval)));
112
- });
108
+ recorder.add(`retryTo ${tries}`, () => setTimeout(tryBlock, pollInterval));
113
109
  } else {
114
- // recorder.throw(err);
115
110
  done(null);
116
111
  }
117
112
  });
118
- // return recorder.promise();
119
113
  };
120
114
 
121
115
  recorder.add('retryTo', async () => {
122
116
  store.debugMode = true;
123
117
  tryBlock();
124
- // recorder.add(() => recorder.session.restore(`retryTo ${tries-1}`));
125
118
  });
126
119
  }).then(() => {
127
120
  if (err) recorder.throw(err);
package/lib/secret.js CHANGED
@@ -1,3 +1,6 @@
1
+ /* eslint-disable max-classes-per-file */
2
+ const { deepClone } = require('./utils');
3
+
1
4
  /** @param {string} secret */
2
5
  class Secret {
3
6
  constructor(secret) {
@@ -9,13 +12,40 @@ class Secret {
9
12
  return this._secret;
10
13
  }
11
14
 
15
+ getMasked() {
16
+ return '*****';
17
+ }
18
+
12
19
  /**
13
20
  * @param {*} secret
14
21
  * @returns {Secret}
15
22
  */
16
23
  static secret(secret) {
24
+ if (typeof secret === 'object') {
25
+ const fields = Array.from(arguments);
26
+ fields.shift();
27
+ return secretObject(secret, fields);
28
+ }
17
29
  return new Secret(secret);
18
30
  }
19
31
  }
20
32
 
33
+ function secretObject(obj, fieldsToHide = []) {
34
+ const handler = {
35
+ get(obj, prop) {
36
+ if (prop === 'toString') {
37
+ return function () {
38
+ const maskedObject = deepClone(obj);
39
+ fieldsToHide.forEach(f => maskedObject[f] = '****');
40
+ return JSON.stringify(maskedObject);
41
+ };
42
+ }
43
+
44
+ return obj[prop];
45
+ },
46
+ };
47
+
48
+ return new Proxy(obj, handler);
49
+ }
50
+
21
51
  module.exports = Secret;
package/lib/step.js CHANGED
@@ -168,7 +168,7 @@ class Step {
168
168
  } else if (typeof arg === 'undefined') {
169
169
  return `${arg}`;
170
170
  } else if (arg instanceof Secret) {
171
- return '*****';
171
+ return arg.getMasked();
172
172
  } else if (arg.toString && arg.toString() !== '[object Object]') {
173
173
  return arg.toString();
174
174
  } else if (typeof arg === 'object') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "3.3.3",
3
+ "version": "3.3.4",
4
4
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
5
5
  "keywords": [
6
6
  "acceptance",
@@ -90,7 +90,7 @@
90
90
  "uuid": "^8.3.2"
91
91
  },
92
92
  "devDependencies": {
93
- "@codeceptjs/detox-helper": "^1.0.2",
93
+ "@codeceptjs/detox-helper": "^1.0.2",
94
94
  "@codeceptjs/mock-request": "^0.3.1",
95
95
  "@faker-js/faker": "^5.5.3",
96
96
  "@pollyjs/adapter-puppeteer": "^5.1.0",
@@ -101,7 +101,7 @@
101
101
  "@wdio/selenium-standalone-service": "^5.16.10",
102
102
  "@wdio/utils": "^5.23.0",
103
103
  "apollo-server-express": "^2.25.3",
104
- "chai-as-promised": "^5.2.0",
104
+ "chai-as-promised": "^7.1.1",
105
105
  "chai-subset": "^1.6.0",
106
106
  "contributor-faces": "^1.0.3",
107
107
  "documentation": "^12.3.0",
@@ -122,7 +122,7 @@
122
122
  "mocha-parallel-tests": "^2.3.0",
123
123
  "nightmare": "^3.0.2",
124
124
  "nodemon": "^1.19.4",
125
- "playwright": "^1.18.1",
125
+ "playwright": "^1.23.2",
126
126
  "puppeteer": "^10.4.0",
127
127
  "qrcode-terminal": "^0.12.0",
128
128
  "rosie": "^1.6.0",
@@ -1562,6 +1562,12 @@ declare namespace CodeceptJS {
1562
1562
  *
1563
1563
  * I.seeResponseContainsJson({ user: { email: 'jon@doe.com' } });
1564
1564
  * ```
1565
+ * If an array is received, checks that at least one element contains JSON
1566
+ * ```js
1567
+ * // response.data == [{ user: { name: 'jon', email: 'jon@doe.com' } }]
1568
+ *
1569
+ * I.seeResponseContainsJson({ user: { email: 'jon@doe.com' } });
1570
+ * ```
1565
1571
  */
1566
1572
  seeResponseContainsJson(json: any): void;
1567
1573
  /**
@@ -1572,6 +1578,12 @@ declare namespace CodeceptJS {
1572
1578
  *
1573
1579
  * I.dontSeeResponseContainsJson({ user: 2 });
1574
1580
  * ```
1581
+ * If an array is received, checks that no element of array contains json:
1582
+ * ```js
1583
+ * // response.data == [{ user: 1 }, { user: 3 }]
1584
+ *
1585
+ * I.dontSeeResponseContainsJson({ user: 2 });
1586
+ * ```
1575
1587
  */
1576
1588
  dontSeeResponseContainsJson(json: any): void;
1577
1589
  /**
@@ -1582,6 +1594,14 @@ declare namespace CodeceptJS {
1582
1594
  *
1583
1595
  * I.seeResponseContainsKeys(['user']);
1584
1596
  * ```
1597
+ *
1598
+ * If an array is received, check is performed for each element of array:
1599
+ *
1600
+ * ```js
1601
+ * // response.data == [{ user: 'jon' }, { user: 'matt'}]
1602
+ *
1603
+ * I.seeResponseContainsKeys(['user']);
1604
+ * ```
1585
1605
  */
1586
1606
  seeResponseContainsKeys(keys: any[]): void;
1587
1607
  /**
@@ -2755,7 +2775,8 @@ declare namespace CodeceptJS {
2755
2775
  * Playwright: {
2756
2776
  * url: "http://localhost",
2757
2777
  * chromium: {
2758
- * browserWSEndpoint: 'ws://localhost:9222/devtools/browser/c5aa6160-b5bc-4d53-bb49-6ecb36cd2e0a'
2778
+ * browserWSEndpoint: 'ws://localhost:9222/devtools/browser/c5aa6160-b5bc-4d53-bb49-6ecb36cd2e0a',
2779
+ * cdpConnection: false // default is false
2759
2780
  * }
2760
2781
  * }
2761
2782
  * }
@@ -3753,9 +3774,11 @@ declare namespace CodeceptJS {
3753
3774
  * Get JS log from browser.
3754
3775
  *
3755
3776
  * ```js
3756
- * let logs = await I.grabBrowserLogs();
3757
- * console.log(JSON.stringify(logs))
3777
+ * const logs = await I.grabBrowserLogs();
3778
+ * const errors = logs.map(l => ({ type: l.type(), text: l.text() })).filter(l => l.type === 'error');
3779
+ * console.log(JSON.stringify(errors));
3758
3780
  * ```
3781
+ * [Learn more about console messages](https://playwright.dev/docs/api/class-consolemessage)
3759
3782
  */
3760
3783
  grabBrowserLogs(): Promise<any[]>;
3761
3784
  /**
@@ -7436,6 +7459,7 @@ declare namespace CodeceptJS {
7436
7459
  * ## Configuration
7437
7460
  *
7438
7461
  * * endpoint: API base URL
7462
+ * * prettyPrintJson: pretty print json for response/request on console logs
7439
7463
  * * timeout: timeout for requests in milliseconds. 10000ms by default
7440
7464
  * * defaultHeaders: a list of default headers
7441
7465
  * * onRequest: a async function which can update request object.
@@ -7448,6 +7472,7 @@ declare namespace CodeceptJS {
7448
7472
  * helpers: {
7449
7473
  * REST: {
7450
7474
  * endpoint: 'http://site.com/api',
7475
+ * prettyPrintJson: true,
7451
7476
  * onRequest: (request) => {
7452
7477
  * request.headers.auth = '123';
7453
7478
  * }