codeceptjs 3.0.7 → 3.1.3
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 +96 -2
- package/README.md +9 -1
- package/bin/codecept.js +27 -17
- package/docs/bdd.md +55 -1
- package/docs/build/Appium.js +76 -4
- package/docs/build/Playwright.js +186 -69
- package/docs/build/Protractor.js +2 -0
- package/docs/build/Puppeteer.js +56 -18
- package/docs/build/REST.js +12 -0
- package/docs/build/WebDriver.js +1 -3
- package/docs/changelog.md +96 -2
- package/docs/commands.md +21 -7
- package/docs/configuration.md +15 -2
- package/docs/helpers/Appium.md +96 -94
- package/docs/helpers/Playwright.md +259 -202
- package/docs/helpers/Puppeteer.md +17 -1
- package/docs/helpers/REST.md +23 -9
- package/docs/helpers/WebDriver.md +2 -2
- package/docs/mobile.md +2 -1
- package/docs/playwright.md +156 -6
- package/docs/plugins.md +61 -69
- package/docs/react.md +1 -1
- package/docs/reports.md +21 -3
- package/lib/actor.js +2 -3
- package/lib/codecept.js +13 -2
- package/lib/command/definitions.js +8 -1
- package/lib/command/run-multiple/collection.js +4 -0
- package/lib/config.js +1 -1
- package/lib/container.js +3 -3
- package/lib/data/dataTableArgument.js +35 -0
- package/lib/helper/Appium.js +49 -4
- package/lib/helper/Playwright.js +186 -69
- package/lib/helper/Protractor.js +2 -0
- package/lib/helper/Puppeteer.js +56 -18
- package/lib/helper/REST.js +12 -0
- package/lib/helper/WebDriver.js +1 -3
- package/lib/helper/errors/ConnectionRefused.js +1 -1
- package/lib/helper/extras/Popup.js +1 -1
- package/lib/helper/extras/React.js +44 -32
- package/lib/index.js +2 -0
- package/lib/interfaces/gherkin.js +8 -1
- package/lib/listener/exit.js +2 -4
- package/lib/listener/helpers.js +4 -4
- package/lib/locator.js +7 -0
- package/lib/mochaFactory.js +13 -9
- package/lib/output.js +2 -2
- package/lib/plugin/allure.js +7 -18
- package/lib/plugin/commentStep.js +1 -1
- package/lib/plugin/{puppeteerCoverage.js → coverage.js} +10 -22
- package/lib/plugin/customLocator.js +2 -2
- package/lib/plugin/subtitles.js +88 -0
- package/lib/plugin/tryTo.js +1 -1
- package/lib/recorder.js +5 -3
- package/lib/step.js +4 -2
- package/package.json +4 -3
- package/typings/index.d.ts +2 -0
- package/typings/types.d.ts +158 -18
package/lib/helper/Protractor.js
CHANGED
|
@@ -124,6 +124,8 @@ class Protractor extends Helper {
|
|
|
124
124
|
|
|
125
125
|
this.isRunning = false;
|
|
126
126
|
this._setConfig(config);
|
|
127
|
+
|
|
128
|
+
console.log('Protractor helper is deprecated as well as Protractor itself.\nThis helper will be removed in next major release');
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
_validateConfig(config) {
|
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -129,8 +129,9 @@ const consoleLogStore = new Console();
|
|
|
129
129
|
* }
|
|
130
130
|
* }
|
|
131
131
|
* ```
|
|
132
|
+
* > Note: When connecting to remote browser `show` and specific `chrome` options (e.g. `headless` or `devtools`) are ignored.
|
|
132
133
|
*
|
|
133
|
-
* #### Example #5: Target URL with provided basic authentication
|
|
134
|
+
* #### Example #5: Target URL with provided basic authentication
|
|
134
135
|
*
|
|
135
136
|
* ```js
|
|
136
137
|
* {
|
|
@@ -143,10 +144,25 @@ const consoleLogStore = new Console();
|
|
|
143
144
|
* }
|
|
144
145
|
* }
|
|
145
146
|
* ```
|
|
147
|
+
* #### Troubleshooting
|
|
146
148
|
*
|
|
149
|
+
* Error Message: `No usable sandbox!`
|
|
150
|
+
*
|
|
151
|
+
* When running Puppeteer on CI try to disable sandbox if you see that message
|
|
152
|
+
*
|
|
153
|
+
* ```
|
|
154
|
+
* helpers: {
|
|
155
|
+
* Puppeteer: {
|
|
156
|
+
* url: 'http://localhost',
|
|
157
|
+
* show: false,
|
|
158
|
+
* chrome: {
|
|
159
|
+
* args: ['--no-sandbox', '--disable-setuid-sandbox']
|
|
160
|
+
* }
|
|
161
|
+
* },
|
|
162
|
+
* }
|
|
163
|
+
* ```
|
|
147
164
|
*
|
|
148
165
|
*
|
|
149
|
-
* Note: When connecting to remote browser `show` and specific `chrome` options (e.g. `headless` or `devtools`) are ignored.
|
|
150
166
|
*
|
|
151
167
|
* ## Access From Helpers
|
|
152
168
|
*
|
|
@@ -670,7 +686,7 @@ class Puppeteer extends Helper {
|
|
|
670
686
|
assertElementExists(els);
|
|
671
687
|
|
|
672
688
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
673
|
-
const { x, y } = await els[0]
|
|
689
|
+
const { x, y } = await getClickablePoint(els[0]);
|
|
674
690
|
await this.page.mouse.move(x + offsetX, y + offsetY);
|
|
675
691
|
return this._waitForAction();
|
|
676
692
|
}
|
|
@@ -726,7 +742,7 @@ class Puppeteer extends Helper {
|
|
|
726
742
|
const els = await this._locate(locator);
|
|
727
743
|
assertElementExists(els, locator, 'Element');
|
|
728
744
|
await els[0]._scrollIntoViewIfNeeded();
|
|
729
|
-
const elementCoordinates = await els[0]
|
|
745
|
+
const elementCoordinates = await getClickablePoint(els[0]);
|
|
730
746
|
await this.executeScript((x, y) => window.scrollBy(x, y), elementCoordinates.x + offsetX, elementCoordinates.y + offsetY);
|
|
731
747
|
} else {
|
|
732
748
|
await this.executeScript((x, y) => window.scrollTo(x, y), offsetX, offsetY);
|
|
@@ -942,7 +958,10 @@ class Puppeteer extends Helper {
|
|
|
942
958
|
*/
|
|
943
959
|
async seeElement(locator) {
|
|
944
960
|
let els = await this._locate(locator);
|
|
945
|
-
els = await Promise.all(els.map(el => el.boundingBox()));
|
|
961
|
+
els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v);
|
|
962
|
+
// Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
|
|
963
|
+
els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el));
|
|
964
|
+
|
|
946
965
|
return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'));
|
|
947
966
|
}
|
|
948
967
|
|
|
@@ -952,7 +971,10 @@ class Puppeteer extends Helper {
|
|
|
952
971
|
*/
|
|
953
972
|
async dontSeeElement(locator) {
|
|
954
973
|
let els = await this._locate(locator);
|
|
955
|
-
els = await Promise.all(els.map(el => el.boundingBox()));
|
|
974
|
+
els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v);
|
|
975
|
+
// Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
|
|
976
|
+
els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el));
|
|
977
|
+
|
|
956
978
|
return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'));
|
|
957
979
|
}
|
|
958
980
|
|
|
@@ -1239,7 +1261,7 @@ class Puppeteer extends Helper {
|
|
|
1239
1261
|
* {{ react }}
|
|
1240
1262
|
*/
|
|
1241
1263
|
async fillField(field, value) {
|
|
1242
|
-
const els = await
|
|
1264
|
+
const els = await findVisibleFields.call(this, field);
|
|
1243
1265
|
assertElementExists(els, field, 'Field');
|
|
1244
1266
|
const el = els[0];
|
|
1245
1267
|
const tag = await el.getProperty('tagName').then(el => el.jsonValue());
|
|
@@ -1266,7 +1288,7 @@ class Puppeteer extends Helper {
|
|
|
1266
1288
|
* {{ react }}
|
|
1267
1289
|
*/
|
|
1268
1290
|
async appendField(field, value) {
|
|
1269
|
-
const els = await
|
|
1291
|
+
const els = await findVisibleFields.call(this, field);
|
|
1270
1292
|
assertElementExists(els, field, 'Field');
|
|
1271
1293
|
await els[0].press('End');
|
|
1272
1294
|
await els[0].type(value, { delay: this.options.pressKeyDelay });
|
|
@@ -1308,7 +1330,7 @@ class Puppeteer extends Helper {
|
|
|
1308
1330
|
* {{> selectOption }}
|
|
1309
1331
|
*/
|
|
1310
1332
|
async selectOption(select, option) {
|
|
1311
|
-
const els = await
|
|
1333
|
+
const els = await findVisibleFields.call(this, select);
|
|
1312
1334
|
assertElementExists(els, select, 'Selectable field');
|
|
1313
1335
|
const el = els[0];
|
|
1314
1336
|
if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
|
|
@@ -1342,7 +1364,10 @@ class Puppeteer extends Helper {
|
|
|
1342
1364
|
*/
|
|
1343
1365
|
async grabNumberOfVisibleElements(locator) {
|
|
1344
1366
|
let els = await this._locate(locator);
|
|
1345
|
-
els = await Promise.all(els.map(el => el.boundingBox()));
|
|
1367
|
+
els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v);
|
|
1368
|
+
// Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
|
|
1369
|
+
els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el));
|
|
1370
|
+
|
|
1346
1371
|
return els.filter(v => v).length;
|
|
1347
1372
|
}
|
|
1348
1373
|
|
|
@@ -1727,8 +1752,8 @@ class Puppeteer extends Helper {
|
|
|
1727
1752
|
const src = await this._locate(locator);
|
|
1728
1753
|
assertElementExists(src, locator, 'Slider Element');
|
|
1729
1754
|
|
|
1730
|
-
// Note: Using
|
|
1731
|
-
const sliderSource = await src[0]
|
|
1755
|
+
// Note: Using public api .getClickablePoint because the .BoundingBox does not take into account iframe offsets
|
|
1756
|
+
const sliderSource = await getClickablePoint(src[0]);
|
|
1732
1757
|
|
|
1733
1758
|
// Drag start point
|
|
1734
1759
|
await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 });
|
|
@@ -2264,7 +2289,7 @@ class Puppeteer extends Helper {
|
|
|
2264
2289
|
module.exports = Puppeteer;
|
|
2265
2290
|
|
|
2266
2291
|
async function findElements(matcher, locator) {
|
|
2267
|
-
if (locator.react) return findReact(matcher, locator);
|
|
2292
|
+
if (locator.react) return findReact(matcher.executionContext(), locator);
|
|
2268
2293
|
locator = new Locator(locator, 'css');
|
|
2269
2294
|
if (!locator.isXPath()) return matcher.$$(locator.simplify());
|
|
2270
2295
|
return matcher.$x(locator.value);
|
|
@@ -2293,7 +2318,7 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
2293
2318
|
}
|
|
2294
2319
|
|
|
2295
2320
|
async function findClickable(matcher, locator) {
|
|
2296
|
-
if (locator.react) return findReact(matcher, locator);
|
|
2321
|
+
if (locator.react) return findReact(matcher.executionContext(), locator);
|
|
2297
2322
|
locator = new Locator(locator);
|
|
2298
2323
|
if (!locator.isFuzzy()) return findElements.call(this, matcher, locator);
|
|
2299
2324
|
|
|
@@ -2376,6 +2401,12 @@ async function proceedIsChecked(assertType, option) {
|
|
|
2376
2401
|
return truth(`checkable ${option}`, 'to be checked')[assertType](selected);
|
|
2377
2402
|
}
|
|
2378
2403
|
|
|
2404
|
+
async function findVisibleFields(locator) {
|
|
2405
|
+
const els = await findFields.call(this, locator);
|
|
2406
|
+
const visible = await Promise.all(els.map(el => el.boundingBox()));
|
|
2407
|
+
return els.filter((el, index) => visible[index]);
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2379
2410
|
async function findFields(locator) {
|
|
2380
2411
|
const matchedLocator = new Locator(locator);
|
|
2381
2412
|
if (!matchedLocator.isFuzzy()) {
|
|
@@ -2406,9 +2437,9 @@ async function proceedDragAndDrop(sourceLocator, destinationLocator) {
|
|
|
2406
2437
|
const dst = await this._locate(destinationLocator);
|
|
2407
2438
|
assertElementExists(dst, destinationLocator, 'Destination Element');
|
|
2408
2439
|
|
|
2409
|
-
// Note: Using
|
|
2410
|
-
const dragSource = await src[0]
|
|
2411
|
-
const dragDestination = await dst[0]
|
|
2440
|
+
// Note: Using public api .getClickablePoint becaues the .BoundingBox does not take into account iframe offsets
|
|
2441
|
+
const dragSource = await getClickablePoint(src[0]);
|
|
2442
|
+
const dragDestination = await getClickablePoint(dst[0]);
|
|
2412
2443
|
|
|
2413
2444
|
// Drag start point
|
|
2414
2445
|
await this.page.mouse.move(dragSource.x, dragSource.y, { steps: 5 });
|
|
@@ -2422,7 +2453,7 @@ async function proceedDragAndDrop(sourceLocator, destinationLocator) {
|
|
|
2422
2453
|
}
|
|
2423
2454
|
|
|
2424
2455
|
async function proceedSeeInField(assertType, field, value) {
|
|
2425
|
-
const els = await
|
|
2456
|
+
const els = await findVisibleFields.call(this, field);
|
|
2426
2457
|
assertElementExists(els, field, 'Field');
|
|
2427
2458
|
const el = els[0];
|
|
2428
2459
|
const tag = await el.getProperty('tagName').then(el => el.jsonValue());
|
|
@@ -2555,6 +2586,13 @@ async function targetCreatedHandler(page) {
|
|
|
2555
2586
|
}
|
|
2556
2587
|
}
|
|
2557
2588
|
|
|
2589
|
+
// BC compatibility for Puppeteer < 10
|
|
2590
|
+
async function getClickablePoint(el) {
|
|
2591
|
+
if (el.clickablePoint) return el.clickablePoint();
|
|
2592
|
+
if (el._clickablePoint) return el._clickablePoint();
|
|
2593
|
+
return null;
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2558
2596
|
// List of key values to key definitions
|
|
2559
2597
|
// https://github.com/GoogleChrome/puppeteer/blob/v1.20.0/lib/USKeyboardLayout.js
|
|
2560
2598
|
const keyDefinitionMap = {
|
package/lib/helper/REST.js
CHANGED
|
@@ -75,6 +75,8 @@ class REST extends Helper {
|
|
|
75
75
|
* Executes axios request
|
|
76
76
|
*
|
|
77
77
|
* @param {*} request
|
|
78
|
+
*
|
|
79
|
+
* @returns {Promise<*>} response
|
|
78
80
|
*/
|
|
79
81
|
async _executeRequest(request) {
|
|
80
82
|
const _debugRequest = { ...request };
|
|
@@ -143,6 +145,8 @@ class REST extends Helper {
|
|
|
143
145
|
*
|
|
144
146
|
* @param {*} url
|
|
145
147
|
* @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
|
|
148
|
+
*
|
|
149
|
+
* @returns {Promise<*>} response
|
|
146
150
|
*/
|
|
147
151
|
async sendGetRequest(url, headers = {}) {
|
|
148
152
|
const request = {
|
|
@@ -166,6 +170,8 @@ class REST extends Helper {
|
|
|
166
170
|
* @param {*} url
|
|
167
171
|
* @param {*} [payload={}] - the payload to be sent. By default it is sent as an empty object
|
|
168
172
|
* @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
|
|
173
|
+
*
|
|
174
|
+
* @returns {Promise<*>} response
|
|
169
175
|
*/
|
|
170
176
|
async sendPostRequest(url, payload = {}, headers = {}) {
|
|
171
177
|
const request = {
|
|
@@ -197,6 +203,8 @@ class REST extends Helper {
|
|
|
197
203
|
* @param {string} url
|
|
198
204
|
* @param {*} [payload={}] - the payload to be sent. By default it is sent as an empty object
|
|
199
205
|
* @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
|
|
206
|
+
*
|
|
207
|
+
* @returns {Promise<*>} response
|
|
200
208
|
*/
|
|
201
209
|
async sendPatchRequest(url, payload = {}, headers = {}) {
|
|
202
210
|
const request = {
|
|
@@ -228,6 +236,8 @@ class REST extends Helper {
|
|
|
228
236
|
* @param {string} url
|
|
229
237
|
* @param {*} [payload={}] - the payload to be sent. By default it is sent as an empty object
|
|
230
238
|
* @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
|
|
239
|
+
*
|
|
240
|
+
* @returns {Promise<*>} response
|
|
231
241
|
*/
|
|
232
242
|
async sendPutRequest(url, payload = {}, headers = {}) {
|
|
233
243
|
const request = {
|
|
@@ -254,6 +264,8 @@ class REST extends Helper {
|
|
|
254
264
|
*
|
|
255
265
|
* @param {*} url
|
|
256
266
|
* @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
|
|
267
|
+
*
|
|
268
|
+
* @returns {Promise<*>} response
|
|
257
269
|
*/
|
|
258
270
|
async sendDeleteRequest(url, headers = {}) {
|
|
259
271
|
const request = {
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -481,7 +481,7 @@ class WebDriver extends Helper {
|
|
|
481
481
|
try {
|
|
482
482
|
require('webdriverio');
|
|
483
483
|
} catch (e) {
|
|
484
|
-
return ['webdriverio@^
|
|
484
|
+
return ['webdriverio@^6.12.1'];
|
|
485
485
|
}
|
|
486
486
|
}
|
|
487
487
|
|
|
@@ -1252,7 +1252,6 @@ class WebDriver extends Helper {
|
|
|
1252
1252
|
|
|
1253
1253
|
/**
|
|
1254
1254
|
* {{> grabAttributeFromAll }}
|
|
1255
|
-
* Appium: can be used for apps only with several values ("contentDescription", "text", "className", "resourceId")
|
|
1256
1255
|
*/
|
|
1257
1256
|
async grabAttributeFromAll(locator, attr) {
|
|
1258
1257
|
const res = await this._locate(locator, true);
|
|
@@ -1263,7 +1262,6 @@ class WebDriver extends Helper {
|
|
|
1263
1262
|
|
|
1264
1263
|
/**
|
|
1265
1264
|
* {{> grabAttributeFrom }}
|
|
1266
|
-
* Appium: can be used for apps only with several values ("contentDescription", "text", "className", "resourceId")
|
|
1267
1265
|
*/
|
|
1268
1266
|
async grabAttributeFrom(locator, attr) {
|
|
1269
1267
|
const attrs = await this.grabAttributeFromAll(locator, attr);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
function ConnectionRefused(err) {
|
|
2
2
|
this.message = "Can't connect to WebDriver.\n";
|
|
3
3
|
this.message += `${err}\n\n`;
|
|
4
|
-
this.message += 'Please make sure Selenium Server
|
|
4
|
+
this.message += 'Please make sure Selenium Server is running and accessible';
|
|
5
5
|
this.stack = err.stack;
|
|
6
6
|
}
|
|
7
7
|
|
|
@@ -4,50 +4,62 @@ let resqScript;
|
|
|
4
4
|
|
|
5
5
|
module.exports = async function findReact(matcher, locator) {
|
|
6
6
|
if (!resqScript) resqScript = fs.readFileSync(require.resolve('resq'));
|
|
7
|
-
await matcher.
|
|
8
|
-
await matcher
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
if (Object.keys(state).length) {
|
|
15
|
-
elements = elements.byState(state);
|
|
16
|
-
}
|
|
7
|
+
await matcher.evaluate(resqScript.toString());
|
|
8
|
+
await matcher
|
|
9
|
+
.evaluate(() => window.resq.waitToLoadReact());
|
|
10
|
+
const arrayHandle = await matcher.evaluateHandle(
|
|
11
|
+
(obj) => {
|
|
12
|
+
const { selector, props, state } = obj;
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
let elements = window.resq.resq$$(selector);
|
|
15
|
+
if (Object.keys(props).length) {
|
|
16
|
+
elements = elements.byProps(props);
|
|
17
|
+
}
|
|
18
|
+
if (Object.keys(state).length) {
|
|
19
|
+
elements = elements.byState(state);
|
|
20
|
+
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
let nodes = [];
|
|
22
|
+
if (!elements.length) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
// resq returns an array of HTMLElements if the React component is a fragment
|
|
27
|
+
// this avoids having nested arrays of nodes which the driver does not understand
|
|
28
|
+
// [[div, div], [div, div]] => [div, div, div, div]
|
|
29
|
+
let nodes = [];
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
isFragment =
|
|
32
|
-
node = element.children;
|
|
33
|
-
}
|
|
31
|
+
elements.forEach((element) => {
|
|
32
|
+
let { node, isFragment } = element;
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
});
|
|
34
|
+
if (!node) {
|
|
35
|
+
isFragment = true;
|
|
36
|
+
node = element.children;
|
|
37
|
+
}
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
if (isFragment) {
|
|
40
|
+
nodes = nodes.concat(node);
|
|
41
|
+
} else {
|
|
42
|
+
nodes.push(node);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return [...nodes];
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
selector: locator.react,
|
|
50
|
+
props: locator.props || {},
|
|
51
|
+
state: locator.state || {},
|
|
52
|
+
},
|
|
53
|
+
);
|
|
44
54
|
|
|
45
55
|
const properties = await arrayHandle.getProperties();
|
|
46
56
|
await arrayHandle.dispose();
|
|
47
57
|
const result = [];
|
|
48
58
|
for (const property of properties.values()) {
|
|
49
59
|
const elementHandle = property.asElement();
|
|
50
|
-
if (elementHandle) {
|
|
60
|
+
if (elementHandle) {
|
|
61
|
+
result.push(elementHandle);
|
|
62
|
+
}
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
return result;
|
package/lib/index.js
CHANGED
|
@@ -32,6 +32,8 @@ module.exports = {
|
|
|
32
32
|
within: require('./within'),
|
|
33
33
|
/** @type {typeof CodeceptJS.DataTable} */
|
|
34
34
|
dataTable: require('./data/table'),
|
|
35
|
+
/** @type {typeof CodeceptJS.DataTableArgument} */
|
|
36
|
+
dataTableArgument: require('./data/dataTableArgument'),
|
|
35
37
|
/** @type {typeof CodeceptJS.store} */
|
|
36
38
|
store: require('./store'),
|
|
37
39
|
/** @type {typeof CodeceptJS.Locator} */
|
|
@@ -11,15 +11,19 @@ const transform = require('../transform');
|
|
|
11
11
|
const parser = new Parser();
|
|
12
12
|
parser.stopAtFirstError = false;
|
|
13
13
|
|
|
14
|
-
module.exports = (text) => {
|
|
14
|
+
module.exports = (text, file) => {
|
|
15
15
|
const ast = parser.parse(text);
|
|
16
16
|
|
|
17
|
+
if (!ast.feature) {
|
|
18
|
+
throw new Error(`No 'Features' available in Gherkin '${file}' provided!`);
|
|
19
|
+
}
|
|
17
20
|
const suite = new Suite(ast.feature.name, new Context());
|
|
18
21
|
const tags = ast.feature.tags.map(t => t.name);
|
|
19
22
|
suite.title = `${suite.title} ${tags.join(' ')}`.trim();
|
|
20
23
|
suite.tags = tags || [];
|
|
21
24
|
suite.comment = ast.feature.description;
|
|
22
25
|
suite.feature = ast.feature;
|
|
26
|
+
suite.file = file;
|
|
23
27
|
suite.timeout(0);
|
|
24
28
|
|
|
25
29
|
suite.beforeEach('codeceptjs.before', () => scenario.setup(suite));
|
|
@@ -83,6 +87,7 @@ module.exports = (text) => {
|
|
|
83
87
|
for (const index in example.cells) {
|
|
84
88
|
const placeholder = fields[index];
|
|
85
89
|
const value = transform('gherkin.examples', example.cells[index].value);
|
|
90
|
+
example.cells[index].value = value;
|
|
86
91
|
current[placeholder] = value;
|
|
87
92
|
exampleSteps = exampleSteps.map((step) => {
|
|
88
93
|
step = { ...step };
|
|
@@ -94,6 +99,7 @@ module.exports = (text) => {
|
|
|
94
99
|
const title = `${child.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim();
|
|
95
100
|
const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current)));
|
|
96
101
|
test.tags = suite.tags.concat(tags);
|
|
102
|
+
test.file = file;
|
|
97
103
|
suite.addTest(scenario.test(test));
|
|
98
104
|
}
|
|
99
105
|
}
|
|
@@ -103,6 +109,7 @@ module.exports = (text) => {
|
|
|
103
109
|
const title = `${child.name} ${tags.join(' ')}`.trim();
|
|
104
110
|
const test = new Test(title, async () => runSteps(child.steps));
|
|
105
111
|
test.tags = suite.tags.concat(tags);
|
|
112
|
+
test.file = file;
|
|
106
113
|
suite.addTest(scenario.test(test));
|
|
107
114
|
}
|
|
108
115
|
|
package/lib/listener/exit.js
CHANGED
|
@@ -18,13 +18,11 @@ module.exports = function () {
|
|
|
18
18
|
failedTests = failedTests.filter(failed => id !== failed);
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
process.on('beforeExit', (code) => {
|
|
22
22
|
if (failedTests.length) {
|
|
23
|
-
|
|
23
|
+
code = 1;
|
|
24
24
|
}
|
|
25
|
-
});
|
|
26
25
|
|
|
27
|
-
process.on('beforeExit', (code) => {
|
|
28
26
|
if (code) {
|
|
29
27
|
process.exit(code);
|
|
30
28
|
}
|
package/lib/listener/helpers.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const path = require('path');
|
|
1
2
|
const event = require('../event');
|
|
2
3
|
const container = require('../container');
|
|
3
4
|
const recorder = require('../recorder');
|
|
@@ -11,11 +12,10 @@ module.exports = function () {
|
|
|
11
12
|
|
|
12
13
|
const runHelpersHook = (hook, param) => {
|
|
13
14
|
if (store.dryRun) return;
|
|
14
|
-
Object.
|
|
15
|
-
if (
|
|
16
|
-
|
|
15
|
+
Object.values(helpers).forEach((helper) => {
|
|
16
|
+
if (helper[hook]) {
|
|
17
|
+
helper[hook](param);
|
|
17
18
|
}
|
|
18
|
-
helpers[key][hook](param);
|
|
19
19
|
});
|
|
20
20
|
};
|
|
21
21
|
|
package/lib/locator.js
CHANGED
package/lib/mochaFactory.js
CHANGED
|
@@ -2,7 +2,7 @@ const Mocha = require('mocha');
|
|
|
2
2
|
const fsPath = require('path');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const reporter = require('./cli');
|
|
5
|
-
const gherkinParser = require('./interfaces/gherkin
|
|
5
|
+
const gherkinParser = require('./interfaces/gherkin');
|
|
6
6
|
const output = require('./output');
|
|
7
7
|
const { genTestId } = require('./utils');
|
|
8
8
|
const ConnectionRefused = require('./helper/errors/ConnectionRefused');
|
|
@@ -17,12 +17,6 @@ class MochaFactory {
|
|
|
17
17
|
output.process(opts.child);
|
|
18
18
|
mocha.ui(scenarioUi);
|
|
19
19
|
|
|
20
|
-
// process.on('unhandledRejection', (reason) => {
|
|
21
|
-
// output.error('Unhandled rejection');
|
|
22
|
-
// console.log(Error.captureStackTrace(reason));
|
|
23
|
-
// output.error(reason);
|
|
24
|
-
// });
|
|
25
|
-
|
|
26
20
|
Mocha.Runner.prototype.uncaught = function (err) {
|
|
27
21
|
if (err) {
|
|
28
22
|
if (err.toString().indexOf('ECONNREFUSED') >= 0) {
|
|
@@ -41,8 +35,7 @@ class MochaFactory {
|
|
|
41
35
|
if (mocha.suite.suites.length === 0) {
|
|
42
36
|
mocha.files
|
|
43
37
|
.filter(file => file.match(/\.feature$/))
|
|
44
|
-
.
|
|
45
|
-
.forEach(content => mocha.suite.addSuite(gherkinParser(content)));
|
|
38
|
+
.forEach(file => mocha.suite.addSuite(gherkinParser(fs.readFileSync(file, 'utf8'), file)));
|
|
46
39
|
|
|
47
40
|
// remove feature files
|
|
48
41
|
mocha.files = mocha.files.filter(file => !file.match(/\.feature$/));
|
|
@@ -51,19 +44,30 @@ class MochaFactory {
|
|
|
51
44
|
|
|
52
45
|
// add ids for each test and check uniqueness
|
|
53
46
|
const dupes = [];
|
|
47
|
+
let missingFeatureInFile = [];
|
|
54
48
|
const seenTests = [];
|
|
55
49
|
mocha.suite.eachTest(test => {
|
|
56
50
|
test.id = genTestId(test);
|
|
51
|
+
|
|
57
52
|
const name = test.fullTitle();
|
|
58
53
|
if (seenTests.includes(test.id)) {
|
|
59
54
|
dupes.push(name);
|
|
60
55
|
}
|
|
61
56
|
seenTests.push(test.id);
|
|
57
|
+
|
|
58
|
+
if (name.slice(0, name.indexOf(':')) === '') {
|
|
59
|
+
missingFeatureInFile.push(test.file);
|
|
60
|
+
}
|
|
62
61
|
});
|
|
63
62
|
if (dupes.length) {
|
|
64
63
|
// ideally this should be no-op and throw (breaking change)...
|
|
65
64
|
output.error(`Duplicate test names detected - Feature + Scenario name should be unique:\n${dupes.join('\n')}`);
|
|
66
65
|
}
|
|
66
|
+
|
|
67
|
+
if (missingFeatureInFile.length) {
|
|
68
|
+
missingFeatureInFile = [...new Set(missingFeatureInFile)];
|
|
69
|
+
output.error(`Missing Feature section in:\n${missingFeatureInFile.join('\n')}`);
|
|
70
|
+
}
|
|
67
71
|
}
|
|
68
72
|
};
|
|
69
73
|
|
package/lib/output.js
CHANGED
|
@@ -235,8 +235,8 @@ function print(...msg) {
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
function truncate(msg, gap = 0) {
|
|
238
|
-
if (msg.indexOf('\n') > 0) {
|
|
239
|
-
return msg; // don't cut multi line steps
|
|
238
|
+
if (msg.indexOf('\n') > 0 || outputLevel >= 3) {
|
|
239
|
+
return msg; // don't cut multi line steps or on verbose log level
|
|
240
240
|
}
|
|
241
241
|
const width = (process.stdout.columns || 200) - gap - 4;
|
|
242
242
|
if (msg.length > width) {
|
package/lib/plugin/allure.js
CHANGED
|
@@ -83,7 +83,6 @@ module.exports = (config) => {
|
|
|
83
83
|
|
|
84
84
|
let currentMetaStep = [];
|
|
85
85
|
let currentStep;
|
|
86
|
-
let isHookSteps = false;
|
|
87
86
|
|
|
88
87
|
reporter.pendingCase = function (testName, timestamp, opts = {}) {
|
|
89
88
|
reporter.startCase(testName, timestamp);
|
|
@@ -191,14 +190,6 @@ module.exports = (config) => {
|
|
|
191
190
|
}
|
|
192
191
|
});
|
|
193
192
|
|
|
194
|
-
event.dispatcher.on(event.hook.started, () => {
|
|
195
|
-
isHookSteps = true;
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
event.dispatcher.on(event.hook.passed, () => {
|
|
199
|
-
isHookSteps = false;
|
|
200
|
-
});
|
|
201
|
-
|
|
202
193
|
event.dispatcher.on(event.suite.after, () => {
|
|
203
194
|
reporter.endSuite();
|
|
204
195
|
});
|
|
@@ -258,15 +249,13 @@ module.exports = (config) => {
|
|
|
258
249
|
});
|
|
259
250
|
|
|
260
251
|
event.dispatcher.on(event.step.started, (step) => {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
currentStep = step;
|
|
269
|
-
}
|
|
252
|
+
startMetaStep(step.metaStep);
|
|
253
|
+
if (currentStep !== step) {
|
|
254
|
+
// In multi-session scenarios, actors' names will be highlighted with ANSI
|
|
255
|
+
// escape sequences which are invalid XML values
|
|
256
|
+
step.actor = step.actor.replace(ansiRegExp(), '');
|
|
257
|
+
reporter.startStep(step.toString());
|
|
258
|
+
currentStep = step;
|
|
270
259
|
}
|
|
271
260
|
});
|
|
272
261
|
|
|
@@ -41,7 +41,7 @@ const defaultGlobalName = '__';
|
|
|
41
41
|
* ### Config
|
|
42
42
|
*
|
|
43
43
|
* * `enabled` - (default: false) enable a plugin
|
|
44
|
-
* * `
|
|
44
|
+
* * `registerGlobal` - (default: false) register `__` template literal function globally. You can override function global name by providing a name as a value.
|
|
45
45
|
*
|
|
46
46
|
* ### Examples
|
|
47
47
|
*
|