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/codecept.js
CHANGED
|
@@ -130,7 +130,16 @@ class Codecept {
|
|
|
130
130
|
let patterns = [pattern];
|
|
131
131
|
if (!pattern) {
|
|
132
132
|
patterns = [];
|
|
133
|
-
|
|
133
|
+
|
|
134
|
+
// If the user wants to test a specific set of test files as an array or string.
|
|
135
|
+
if (this.config.tests && !this.opts.features) {
|
|
136
|
+
if (Array.isArray(this.config.tests)) {
|
|
137
|
+
patterns.push(...this.config.tests);
|
|
138
|
+
} else {
|
|
139
|
+
patterns.push(this.config.tests);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
134
143
|
if (this.config.gherkin.features && !this.opts.tests) {
|
|
135
144
|
if (Array.isArray(this.config.gherkin.features)) {
|
|
136
145
|
this.config.gherkin.features.forEach(feature => {
|
|
@@ -147,7 +156,9 @@ class Codecept {
|
|
|
147
156
|
if (!fsPath.isAbsolute(file)) {
|
|
148
157
|
file = fsPath.join(global.codecept_dir, file);
|
|
149
158
|
}
|
|
150
|
-
this.testFiles.
|
|
159
|
+
if (!this.testFiles.includes(fsPath.resolve(file))) {
|
|
160
|
+
this.testFiles.push(fsPath.resolve(file));
|
|
161
|
+
}
|
|
151
162
|
});
|
|
152
163
|
}
|
|
153
164
|
}
|
|
@@ -194,7 +194,14 @@ function getPath(originalPath, targetFolderPath, testsPath) {
|
|
|
194
194
|
else if (parsedPath.ext === '.ts') parsedPath.base = parsedPath.name;
|
|
195
195
|
|
|
196
196
|
if (!parsedPath.dir.startsWith('.')) return path.posix.join(parsedPath.dir, parsedPath.base);
|
|
197
|
-
const relativePath = path.posix.relative(
|
|
197
|
+
const relativePath = path.posix.relative(
|
|
198
|
+
targetFolderPath.split(path.sep).join(path.posix.sep),
|
|
199
|
+
path.posix.join(
|
|
200
|
+
testsPath.split(path.sep).join(path.posix.sep),
|
|
201
|
+
parsedPath.dir.split(path.sep).join(path.posix.sep),
|
|
202
|
+
parsedPath.base.split(path.sep).join(path.posix.sep),
|
|
203
|
+
),
|
|
204
|
+
);
|
|
198
205
|
|
|
199
206
|
return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
|
|
200
207
|
}
|
|
@@ -80,6 +80,10 @@ class Collection {
|
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
if (runConfig.gherkin && config.gherkin.features) {
|
|
84
|
+
patterns.push(runConfig.gherkin.features);
|
|
85
|
+
}
|
|
86
|
+
|
|
83
87
|
createChunks(runConfig, patterns).forEach((runChunkConfig, index) => {
|
|
84
88
|
const run = createRun(`${runName}:chunk${index + 1}`, runChunkConfig);
|
|
85
89
|
run.setOriginalName(runName);
|
package/lib/config.js
CHANGED
|
@@ -128,7 +128,7 @@ function loadConfigFile(configFile) {
|
|
|
128
128
|
const extensionName = path.extname(configFile);
|
|
129
129
|
|
|
130
130
|
// .conf.js config file
|
|
131
|
-
if (extensionName === '.js' || extensionName === '.ts') {
|
|
131
|
+
if (extensionName === '.js' || extensionName === '.ts' || extensionName === '.cjs') {
|
|
132
132
|
return Config.create(require(configFile).config);
|
|
133
133
|
}
|
|
134
134
|
|
package/lib/container.js
CHANGED
|
@@ -215,7 +215,7 @@ function createSupportObjects(config) {
|
|
|
215
215
|
newObj._init();
|
|
216
216
|
}
|
|
217
217
|
} catch (err) {
|
|
218
|
-
throw new Error(`Initialization failed for ${name}: ${newObj}\n${err.message}`);
|
|
218
|
+
throw new Error(`Initialization failed for ${name}: ${newObj}\n${err.message}\n${err.stack}`);
|
|
219
219
|
}
|
|
220
220
|
return newObj;
|
|
221
221
|
}
|
|
@@ -288,7 +288,7 @@ function createPlugins(config, options = {}) {
|
|
|
288
288
|
}
|
|
289
289
|
plugins[pluginName] = require(module)(config[pluginName]);
|
|
290
290
|
} catch (err) {
|
|
291
|
-
throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}`);
|
|
291
|
+
throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}\n${err.stack}`);
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
294
|
return plugins;
|
|
@@ -380,7 +380,7 @@ function loadSupportObject(modulePath, supportObjectName) {
|
|
|
380
380
|
|
|
381
381
|
return obj;
|
|
382
382
|
} catch (err) {
|
|
383
|
-
throw new Error(`Could not include object ${supportObjectName} from module '${modulePath}'\n${err.message}`);
|
|
383
|
+
throw new Error(`Could not include object ${supportObjectName} from module '${modulePath}'\n${err.message}\n${err.stack}`);
|
|
384
384
|
}
|
|
385
385
|
}
|
|
386
386
|
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataTableArgument class to store the Cucumber data table from
|
|
3
|
+
* a step as an object with methods that can be used to access the data.
|
|
4
|
+
*/
|
|
1
5
|
class DataTableArgument {
|
|
6
|
+
/** @param {*} gherkinDataTable */
|
|
2
7
|
constructor(gherkinDataTable) {
|
|
3
8
|
this.rawData = gherkinDataTable.rows.map((row) => {
|
|
4
9
|
return row.cells.map((cell) => {
|
|
@@ -7,16 +12,25 @@ class DataTableArgument {
|
|
|
7
12
|
});
|
|
8
13
|
}
|
|
9
14
|
|
|
15
|
+
/** Returns the table as a 2-D array
|
|
16
|
+
* @returns {string[][]}
|
|
17
|
+
*/
|
|
10
18
|
raw() {
|
|
11
19
|
return this.rawData.slice(0);
|
|
12
20
|
}
|
|
13
21
|
|
|
22
|
+
/** Returns the table as a 2-D array, without the first row
|
|
23
|
+
* @returns {string[][]}
|
|
24
|
+
*/
|
|
14
25
|
rows() {
|
|
15
26
|
const copy = this.raw();
|
|
16
27
|
copy.shift();
|
|
17
28
|
return copy;
|
|
18
29
|
}
|
|
19
30
|
|
|
31
|
+
/** Returns an array of objects where each row is converted to an object (column header is the key)
|
|
32
|
+
* @returns {any[]}
|
|
33
|
+
*/
|
|
20
34
|
hashes() {
|
|
21
35
|
const copy = this.raw();
|
|
22
36
|
const header = copy.shift();
|
|
@@ -26,6 +40,27 @@ class DataTableArgument {
|
|
|
26
40
|
return r;
|
|
27
41
|
});
|
|
28
42
|
}
|
|
43
|
+
|
|
44
|
+
/** Returns an object where each row corresponds to an entry
|
|
45
|
+
* (first column is the key, second column is the value)
|
|
46
|
+
* @returns {Record<string, string>}
|
|
47
|
+
*/
|
|
48
|
+
rowsHash() {
|
|
49
|
+
const rows = this.raw();
|
|
50
|
+
const everyRowHasTwoColumns = rows.every((row) => row.length === 2);
|
|
51
|
+
if (!everyRowHasTwoColumns) {
|
|
52
|
+
throw new Error('rowsHash can only be called on a data table where all rows have exactly two columns');
|
|
53
|
+
}
|
|
54
|
+
/** @type {Record<string, string>} */
|
|
55
|
+
const result = {};
|
|
56
|
+
rows.forEach((x) => (result[x[0]] = x[1]));
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Transposed the data */
|
|
61
|
+
transpose() {
|
|
62
|
+
this.rawData = this.rawData[0].map((x, i) => this.rawData.map((y) => y[i]));
|
|
63
|
+
}
|
|
29
64
|
}
|
|
30
65
|
|
|
31
66
|
module.exports = DataTableArgument;
|
package/lib/helper/Appium.js
CHANGED
|
@@ -481,10 +481,11 @@ class Appium extends Webdriver {
|
|
|
481
481
|
* ```js
|
|
482
482
|
* I.removeApp('appName', 'com.example.android.apis');
|
|
483
483
|
* ```
|
|
484
|
-
* @param {string} appId
|
|
485
|
-
* @param {string} bundleId String ID of bundle
|
|
486
484
|
*
|
|
487
485
|
* Appium: support only Android
|
|
486
|
+
*
|
|
487
|
+
* @param {string} appId
|
|
488
|
+
* @param {string} [bundleId] ID of bundle
|
|
488
489
|
*/
|
|
489
490
|
async removeApp(appId, bundleId) {
|
|
490
491
|
onlyForApps.call(this, 'Android');
|
|
@@ -820,9 +821,10 @@ class Appium extends Webdriver {
|
|
|
820
821
|
* I.hideDeviceKeyboard('pressKey', 'Done');
|
|
821
822
|
* ```
|
|
822
823
|
*
|
|
823
|
-
* @param {'tapOutside' | 'pressKey'} strategy desired strategy to close keyboard (‘tapOutside’ or ‘pressKey’)
|
|
824
|
-
*
|
|
825
824
|
* Appium: support Android and iOS
|
|
825
|
+
*
|
|
826
|
+
* @param {'tapOutside' | 'pressKey'} [strategy] Desired strategy to close keyboard (‘tapOutside’ or ‘pressKey’)
|
|
827
|
+
* @param {string} [key] Optional key
|
|
826
828
|
*/
|
|
827
829
|
async hideDeviceKeyboard(strategy, key) {
|
|
828
830
|
onlyForApps.call(this);
|
|
@@ -1162,6 +1164,8 @@ class Appium extends Webdriver {
|
|
|
1162
1164
|
* ```
|
|
1163
1165
|
*
|
|
1164
1166
|
* Appium: support Android and iOS
|
|
1167
|
+
*
|
|
1168
|
+
* @param {Array} actions Array of touch actions
|
|
1165
1169
|
*/
|
|
1166
1170
|
async touchPerform(actions) {
|
|
1167
1171
|
onlyForApps.call(this);
|
|
@@ -1352,6 +1356,33 @@ class Appium extends Webdriver {
|
|
|
1352
1356
|
return super.grabTextFrom(parseLocator.call(this, locator));
|
|
1353
1357
|
}
|
|
1354
1358
|
|
|
1359
|
+
/**
|
|
1360
|
+
* {{> grabNumberOfVisibleElements }}
|
|
1361
|
+
*/
|
|
1362
|
+
async grabNumberOfVisibleElements(locator) {
|
|
1363
|
+
if (this.isWeb) return super.grabNumberOfVisibleElements(locator);
|
|
1364
|
+
return super.grabNumberOfVisibleElements(parseLocator.call(this, locator));
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Can be used for apps only with several values ("contentDescription", "text", "className", "resourceId")
|
|
1369
|
+
*
|
|
1370
|
+
* {{> grabAttributeFrom }}
|
|
1371
|
+
*/
|
|
1372
|
+
async grabAttributeFrom(locator, attr) {
|
|
1373
|
+
if (this.isWeb) return super.grabAttributeFrom(locator, attr);
|
|
1374
|
+
return super.grabAttributeFrom(parseLocator.call(this, locator), attr);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Can be used for apps only with several values ("contentDescription", "text", "className", "resourceId")
|
|
1379
|
+
* {{> grabAttributeFromAll }}
|
|
1380
|
+
*/
|
|
1381
|
+
async grabAttributeFromAll(locator, attr) {
|
|
1382
|
+
if (this.isWeb) return super.grabAttributeFromAll(locator, attr);
|
|
1383
|
+
return super.grabAttributeFromAll(parseLocator.call(this, locator), attr);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1355
1386
|
/**
|
|
1356
1387
|
* {{> grabValueFromAll }}
|
|
1357
1388
|
*
|
|
@@ -1370,6 +1401,20 @@ class Appium extends Webdriver {
|
|
|
1370
1401
|
return super.grabValueFrom(parseLocator.call(this, locator));
|
|
1371
1402
|
}
|
|
1372
1403
|
|
|
1404
|
+
/**
|
|
1405
|
+
* Saves a screenshot to ouput folder (set in codecept.json or codecept.conf.js).
|
|
1406
|
+
* Filename is relative to output folder.
|
|
1407
|
+
*
|
|
1408
|
+
* ```js
|
|
1409
|
+
* I.saveScreenshot('debug.png');
|
|
1410
|
+
* ```
|
|
1411
|
+
*
|
|
1412
|
+
* @param {string} fileName file name to save.
|
|
1413
|
+
*/
|
|
1414
|
+
async saveScreenshot(fileName) {
|
|
1415
|
+
return super.saveScreenshot(fileName, false);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1373
1418
|
/**
|
|
1374
1419
|
* {{> scrollIntoView }}
|
|
1375
1420
|
*
|
package/lib/helper/Playwright.js
CHANGED
|
@@ -19,6 +19,7 @@ const {
|
|
|
19
19
|
screenshotOutputFolder,
|
|
20
20
|
getNormalizedKeyAttributeValue,
|
|
21
21
|
isModifierKey,
|
|
22
|
+
clearString,
|
|
22
23
|
} = require('../utils');
|
|
23
24
|
const {
|
|
24
25
|
isColorProperty,
|
|
@@ -28,6 +29,7 @@ const ElementNotFound = require('./errors/ElementNotFound');
|
|
|
28
29
|
const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnectionRefused');
|
|
29
30
|
const Popup = require('./extras/Popup');
|
|
30
31
|
const Console = require('./extras/Console');
|
|
32
|
+
const findReact = require('./extras/React');
|
|
31
33
|
|
|
32
34
|
let playwright;
|
|
33
35
|
let perfTiming;
|
|
@@ -64,6 +66,8 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
|
|
|
64
66
|
* * `restart`: (optional, default: true) - restart browser between tests.
|
|
65
67
|
* * `disableScreenshots`: (optional, default: false) - don't save screenshot on failure.
|
|
66
68
|
* * `emulate`: (optional, default: {}) launch browser in device emulation mode.
|
|
69
|
+
* * `video`: (optional, default: false) enables video recording for failed tests; videos are saved into `output/videos` folder
|
|
70
|
+
* * `trace`: (optional, default: false) record [tracing information](https://playwright.dev/docs/trace-viewer) with screenshots and snapshots.
|
|
67
71
|
* * `fullPageScreenshots` (optional, default: false) - make full page screenshots on failure.
|
|
68
72
|
* * `uniqueScreenshotNames`: (optional, default: false) - option to prevent screenshot override if you have scenarios with the same name in different suites.
|
|
69
73
|
* * `keepBrowserState`: (optional, default: false) - keep browser state between tests when `restart` is set to false.
|
|
@@ -80,6 +84,22 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
|
|
|
80
84
|
* * `chromium`: (optional) pass additional chromium options
|
|
81
85
|
* * `electron`: (optional) pass additional electron options
|
|
82
86
|
*
|
|
87
|
+
* #### Video Recording Customization
|
|
88
|
+
*
|
|
89
|
+
* By default, video is saved to `output/video` dir. You can customize this path by passing `dir` option to `recordVideo` option.
|
|
90
|
+
*
|
|
91
|
+
* * `video`: enables video recording for failed tests; videos are saved into `output/videos` folder
|
|
92
|
+
* * `keepVideoForPassedTests`: - save videos for passed tests
|
|
93
|
+
* * `recordVideo`: [additional options for videos customization](https://playwright.dev/docs/next/api/class-browser#browser-new-context)
|
|
94
|
+
*
|
|
95
|
+
* #### Trace Recording Customization
|
|
96
|
+
*
|
|
97
|
+
* Trace recording provides a complete information on test execution and includes DOM snapshots, screenshots, and network requests logged during run.
|
|
98
|
+
* Traces will be saved to `output/trace`
|
|
99
|
+
*
|
|
100
|
+
* * `trace`: enables trace recording for failed tests; trace are saved into `output/trace` folder
|
|
101
|
+
* * `keepTraceForPassedTests`: - save trace for passed tests
|
|
102
|
+
*
|
|
83
103
|
* #### Example #1: Wait for 0 network connections.
|
|
84
104
|
*
|
|
85
105
|
* ```js
|
|
@@ -131,7 +151,7 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
|
|
|
131
151
|
* Playwright: {
|
|
132
152
|
* url: "http://localhost",
|
|
133
153
|
* chromium: {
|
|
134
|
-
* browserWSEndpoint:
|
|
154
|
+
* browserWSEndpoint: 'ws://localhost:9222/devtools/browser/c5aa6160-b5bc-4d53-bb49-6ecb36cd2e0a'
|
|
135
155
|
* }
|
|
136
156
|
* }
|
|
137
157
|
* }
|
|
@@ -211,6 +231,7 @@ class Playwright extends Helper {
|
|
|
211
231
|
this.activeSessionName = '';
|
|
212
232
|
this.isElectron = false;
|
|
213
233
|
this.electronSessions = [];
|
|
234
|
+
this.storageState = null;
|
|
214
235
|
|
|
215
236
|
// override defaults with config
|
|
216
237
|
this._setConfig(config);
|
|
@@ -220,7 +241,6 @@ class Playwright extends Helper {
|
|
|
220
241
|
const defaults = {
|
|
221
242
|
// options to emulate context
|
|
222
243
|
emulate: {},
|
|
223
|
-
|
|
224
244
|
browser: 'chromium',
|
|
225
245
|
waitForAction: 100,
|
|
226
246
|
waitForTimeout: 1000,
|
|
@@ -249,10 +269,16 @@ class Playwright extends Helper {
|
|
|
249
269
|
}
|
|
250
270
|
|
|
251
271
|
_getOptionsForBrowser(config) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
272
|
+
if (config[config.browser]) {
|
|
273
|
+
if (config[config.browser].browserWSEndpoint && config[config.browser].browserWSEndpoint.wsEndpoint) {
|
|
274
|
+
config[config.browser].browserWSEndpoint = config[config.browser].browserWSEndpoint.wsEndpoint;
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
...config[config.browser],
|
|
278
|
+
wsEndpoint: config[config.browser].browserWSEndpoint,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return {};
|
|
256
282
|
}
|
|
257
283
|
|
|
258
284
|
_setConfig(config) {
|
|
@@ -261,6 +287,12 @@ class Playwright extends Helper {
|
|
|
261
287
|
headless: !this.options.show,
|
|
262
288
|
...this._getOptionsForBrowser(config),
|
|
263
289
|
};
|
|
290
|
+
if (this.options.video) {
|
|
291
|
+
this.options.recordVideo = { size: parseWindowSize(this.options.windowSize) };
|
|
292
|
+
}
|
|
293
|
+
if (this.options.recordVideo && !this.options.recordVideo.dir) {
|
|
294
|
+
this.options.recordVideo.dir = `${global.output_dir}/videos/`;
|
|
295
|
+
}
|
|
264
296
|
this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint;
|
|
265
297
|
this.isElectron = this.options.browser === 'electron';
|
|
266
298
|
this.userDataDir = this.playwrightOptions.userDataDir;
|
|
@@ -319,8 +351,42 @@ class Playwright extends Helper {
|
|
|
319
351
|
return err.message.includes('context');
|
|
320
352
|
},
|
|
321
353
|
});
|
|
322
|
-
if (this.options.restart && !this.options.manualStart)
|
|
323
|
-
if (!this.isRunning && !this.options.manualStart)
|
|
354
|
+
if (this.options.restart && !this.options.manualStart) await this._startBrowser();
|
|
355
|
+
if (!this.isRunning && !this.options.manualStart) await this._startBrowser();
|
|
356
|
+
|
|
357
|
+
this.isAuthenticated = false;
|
|
358
|
+
if (this.isElectron) {
|
|
359
|
+
this.browserContext = this.browser.context();
|
|
360
|
+
} else if (this.userDataDir) {
|
|
361
|
+
this.browserContext = this.browser;
|
|
362
|
+
} else {
|
|
363
|
+
const contextOptions = {
|
|
364
|
+
ignoreHTTPSErrors: this.options.ignoreHTTPSErrors,
|
|
365
|
+
acceptDownloads: true,
|
|
366
|
+
...this.options.emulate,
|
|
367
|
+
};
|
|
368
|
+
if (this.options.basicAuth) {
|
|
369
|
+
contextOptions.httpCredentials = this.options.basicAuth;
|
|
370
|
+
this.isAuthenticated = true;
|
|
371
|
+
}
|
|
372
|
+
if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo;
|
|
373
|
+
if (this.storageState) contextOptions.storageState = this.storageState;
|
|
374
|
+
this.browserContext = await this.browser.newContext(contextOptions); // Adding the HTTPSError ignore in the context so that we can ignore those errors
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
let mainPage;
|
|
378
|
+
if (this.isElectron) {
|
|
379
|
+
mainPage = await this.browser.firstWindow();
|
|
380
|
+
} else {
|
|
381
|
+
const existingPages = await this.browserContext.pages();
|
|
382
|
+
mainPage = existingPages[0] || await this.browserContext.newPage();
|
|
383
|
+
}
|
|
384
|
+
targetCreatedHandler.call(this, mainPage);
|
|
385
|
+
|
|
386
|
+
await this._setPage(mainPage);
|
|
387
|
+
|
|
388
|
+
if (this.options.trace) await this.browserContext.tracing.start({ screenshots: true, snapshots: true });
|
|
389
|
+
|
|
324
390
|
return this.browser;
|
|
325
391
|
}
|
|
326
392
|
|
|
@@ -333,43 +399,24 @@ class Playwright extends Helper {
|
|
|
333
399
|
return;
|
|
334
400
|
}
|
|
335
401
|
|
|
402
|
+
if (this.options.restart) {
|
|
403
|
+
this.isRunning = false;
|
|
404
|
+
return this._stopBrowser();
|
|
405
|
+
}
|
|
406
|
+
|
|
336
407
|
// close other sessions
|
|
337
408
|
try {
|
|
338
409
|
const contexts = await this.browser.contexts();
|
|
339
|
-
contexts
|
|
410
|
+
const currentContext = contexts[0];
|
|
411
|
+
if (currentContext && (this.options.keepCookies || this.options.keepBrowserState)) {
|
|
412
|
+
this.storageState = await currentContext.storageState();
|
|
413
|
+
}
|
|
340
414
|
|
|
341
415
|
await Promise.all(contexts.map(c => c.close()));
|
|
342
416
|
} catch (e) {
|
|
343
417
|
console.log(e);
|
|
344
418
|
}
|
|
345
419
|
|
|
346
|
-
if (this.options.restart) {
|
|
347
|
-
this.isRunning = false;
|
|
348
|
-
return this._stopBrowser();
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// ensure current page is in default context
|
|
352
|
-
if (this.page) {
|
|
353
|
-
const existingPages = await this.browserContext.pages();
|
|
354
|
-
await this._setPage(existingPages[0]);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (this.options.keepBrowserState) return;
|
|
358
|
-
|
|
359
|
-
if (!this.options.keepCookies) {
|
|
360
|
-
this.debugSection('Session', 'cleaning cookies and localStorage');
|
|
361
|
-
await this.clearCookie();
|
|
362
|
-
}
|
|
363
|
-
const currentUrl = await this.grabCurrentUrl();
|
|
364
|
-
|
|
365
|
-
if (currentUrl.startsWith('http')) {
|
|
366
|
-
await this.executeScript('localStorage.clear();').catch((err) => {
|
|
367
|
-
if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err;
|
|
368
|
-
});
|
|
369
|
-
await this.executeScript('sessionStorage.clear();').catch((err) => {
|
|
370
|
-
if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err;
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
420
|
// await this.closeOtherTabs();
|
|
374
421
|
return this.browser;
|
|
375
422
|
}
|
|
@@ -517,7 +564,7 @@ class Playwright extends Helper {
|
|
|
517
564
|
page.setDefaultNavigationTimeout(this.options.getPageTimeout);
|
|
518
565
|
this.context = await this.page;
|
|
519
566
|
this.contextLocator = null;
|
|
520
|
-
if (this.
|
|
567
|
+
if (this.options.browser === 'chrome') {
|
|
521
568
|
await page.bringToFront();
|
|
522
569
|
}
|
|
523
570
|
}
|
|
@@ -533,6 +580,7 @@ class Playwright extends Helper {
|
|
|
533
580
|
if (!page) {
|
|
534
581
|
return;
|
|
535
582
|
}
|
|
583
|
+
page.removeAllListeners('dialog');
|
|
536
584
|
page.on('dialog', async (dialog) => {
|
|
537
585
|
popupStore.popup = dialog;
|
|
538
586
|
const action = popupStore.actionType || this.options.defaultPopupAction;
|
|
@@ -579,7 +627,7 @@ class Playwright extends Helper {
|
|
|
579
627
|
this.browser = await playwright._electron.launch(this.playwrightOptions);
|
|
580
628
|
} else if (this.isRemoteBrowser) {
|
|
581
629
|
try {
|
|
582
|
-
this.browser = await playwright[this.options.browser].connect(this.playwrightOptions
|
|
630
|
+
this.browser = await playwright[this.options.browser].connect(this.playwrightOptions);
|
|
583
631
|
} catch (err) {
|
|
584
632
|
if (err.toString().indexOf('ECONNREFUSED')) {
|
|
585
633
|
throw new RemoteBrowserConnectionRefused(err);
|
|
@@ -597,26 +645,6 @@ class Playwright extends Helper {
|
|
|
597
645
|
this.debugSection('Url', target.url());
|
|
598
646
|
});
|
|
599
647
|
|
|
600
|
-
if (this.isElectron) {
|
|
601
|
-
this.browserContext = this.browser.context();
|
|
602
|
-
} else if (this.userDataDir) {
|
|
603
|
-
this.browserContext = this.browser;
|
|
604
|
-
} else {
|
|
605
|
-
this.browserContext = await this.browser.newContext({ ignoreHTTPSErrors: this.options.ignoreHTTPSErrors, acceptDownloads: true, ...this.options.emulate });// Adding the HTTPSError ignore in the context so that we can ignore those errors
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
let mainPage;
|
|
609
|
-
if (this.isElectron) {
|
|
610
|
-
mainPage = await this.browser.firstWindow();
|
|
611
|
-
} else {
|
|
612
|
-
const existingPages = await this.browserContext.pages();
|
|
613
|
-
mainPage = existingPages[0] || await this.browserContext.newPage();
|
|
614
|
-
}
|
|
615
|
-
targetCreatedHandler.call(this, mainPage);
|
|
616
|
-
|
|
617
|
-
await this._setPage(mainPage);
|
|
618
|
-
await this.closeOtherTabs();
|
|
619
|
-
|
|
620
648
|
this.isRunning = true;
|
|
621
649
|
}
|
|
622
650
|
|
|
@@ -691,9 +719,9 @@ class Playwright extends Helper {
|
|
|
691
719
|
url = this.options.url + url;
|
|
692
720
|
}
|
|
693
721
|
|
|
694
|
-
if (this.
|
|
722
|
+
if (this.options.basicAuth && (this.isAuthenticated !== true)) {
|
|
695
723
|
if (url.includes(this.options.url)) {
|
|
696
|
-
await this.browserContext.setHTTPCredentials(this.
|
|
724
|
+
await this.browserContext.setHTTPCredentials(this.options.basicAuth);
|
|
697
725
|
this.isAuthenticated = true;
|
|
698
726
|
}
|
|
699
727
|
}
|
|
@@ -752,7 +780,7 @@ class Playwright extends Helper {
|
|
|
752
780
|
if (!customHeaders) {
|
|
753
781
|
throw new Error('Cannot send empty headers.');
|
|
754
782
|
}
|
|
755
|
-
return this.
|
|
783
|
+
return this.browserContext.setExtraHTTPHeaders(customHeaders);
|
|
756
784
|
}
|
|
757
785
|
|
|
758
786
|
/**
|
|
@@ -1050,7 +1078,7 @@ class Playwright extends Helper {
|
|
|
1050
1078
|
*/
|
|
1051
1079
|
async seeElement(locator) {
|
|
1052
1080
|
let els = await this._locate(locator);
|
|
1053
|
-
els = await Promise.all(els.map(el => el.
|
|
1081
|
+
els = await Promise.all(els.map(el => el.isVisible()));
|
|
1054
1082
|
return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'));
|
|
1055
1083
|
}
|
|
1056
1084
|
|
|
@@ -1060,7 +1088,7 @@ class Playwright extends Helper {
|
|
|
1060
1088
|
*/
|
|
1061
1089
|
async dontSeeElement(locator) {
|
|
1062
1090
|
let els = await this._locate(locator);
|
|
1063
|
-
els = await Promise.all(els.map(el => el.
|
|
1091
|
+
els = await Promise.all(els.map(el => el.isVisible()));
|
|
1064
1092
|
return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'));
|
|
1065
1093
|
}
|
|
1066
1094
|
|
|
@@ -1364,7 +1392,7 @@ class Playwright extends Helper {
|
|
|
1364
1392
|
*/
|
|
1365
1393
|
async grabNumberOfVisibleElements(locator) {
|
|
1366
1394
|
let els = await this._locate(locator);
|
|
1367
|
-
els = await Promise.all(els.map(el => el.
|
|
1395
|
+
els = await Promise.all(els.map(el => el.isVisible()));
|
|
1368
1396
|
return els.filter(v => v).length;
|
|
1369
1397
|
}
|
|
1370
1398
|
|
|
@@ -1531,6 +1559,7 @@ class Playwright extends Helper {
|
|
|
1531
1559
|
async clearCookie() {
|
|
1532
1560
|
// Playwright currently doesn't support to delete a certain cookie
|
|
1533
1561
|
// https://github.com/microsoft/playwright/blob/master/docs/api.md#class-browsercontext
|
|
1562
|
+
if (!this.browserContext) return;
|
|
1534
1563
|
return this.browserContext.clearCookies();
|
|
1535
1564
|
}
|
|
1536
1565
|
|
|
@@ -1825,8 +1854,42 @@ class Playwright extends Helper {
|
|
|
1825
1854
|
return this.page.screenshot({ path: outputFile, fullPage: fullPageOption, type: 'png' });
|
|
1826
1855
|
}
|
|
1827
1856
|
|
|
1828
|
-
async _failed() {
|
|
1857
|
+
async _failed(test) {
|
|
1829
1858
|
await this._withinEnd();
|
|
1859
|
+
|
|
1860
|
+
if (!test.artifacts) {
|
|
1861
|
+
test.artifacts = {};
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
if (this.options.recordVideo && this.page.video()) {
|
|
1865
|
+
test.artifacts.video = await this.page.video().path();
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
if (this.options.trace) {
|
|
1869
|
+
const path = `${global.output_dir}/trace/${clearString(test.title).slice(0, 255)}.zip`;
|
|
1870
|
+
await this.browserContext.tracing.stop({ path });
|
|
1871
|
+
test.artifacts.trace = path;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
async _passed(test) {
|
|
1876
|
+
if (this.options.recordVideo && this.page.video()) {
|
|
1877
|
+
if (this.options.keepVideoForPassedTests) {
|
|
1878
|
+
test.artifacts.video = await this.page.video().path();
|
|
1879
|
+
} else {
|
|
1880
|
+
this.page.video().delete().catch(e => {});
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
if (this.options.trace) {
|
|
1885
|
+
if (this.options.keepTraceForPassedTests) {
|
|
1886
|
+
const path = `${global.output_dir}/trace/${clearString(test.title)}.zip`;
|
|
1887
|
+
await this.browserContext.tracing.stop({ path });
|
|
1888
|
+
test.artifacts.trace = path;
|
|
1889
|
+
} else {
|
|
1890
|
+
await this.browserContext.tracing.stop();
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1830
1893
|
}
|
|
1831
1894
|
|
|
1832
1895
|
/**
|
|
@@ -2234,6 +2297,39 @@ class Playwright extends Helper {
|
|
|
2234
2297
|
if (prop) return rect[prop];
|
|
2235
2298
|
return rect;
|
|
2236
2299
|
}
|
|
2300
|
+
|
|
2301
|
+
/**
|
|
2302
|
+
* Mocks network request using [`browserContext.route`](https://playwright.dev/docs/api/class-browsercontext#browser-context-route) of Playwright
|
|
2303
|
+
*
|
|
2304
|
+
* ```js
|
|
2305
|
+
* I.mockRoute(/(\.png$)|(\.jpg$)/, route => route.abort());
|
|
2306
|
+
* ```
|
|
2307
|
+
* This method allows intercepting and mocking requests & responses. [Learn more about it](https://playwright.dev/docs/network#handle-requests)
|
|
2308
|
+
*
|
|
2309
|
+
* @param {string} [url] URL, regex or pattern for to match URL
|
|
2310
|
+
* @param {function} [handler] a function to process request
|
|
2311
|
+
*
|
|
2312
|
+
*/
|
|
2313
|
+
async mockRoute(url, handler) {
|
|
2314
|
+
return this.browserContext.route(...arguments);
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
/**
|
|
2318
|
+
* Stops network mocking created by `mockRoute`.
|
|
2319
|
+
*
|
|
2320
|
+
* ```js
|
|
2321
|
+
* I.stopMockingRoute(/(\.png$)|(\.jpg$)/);
|
|
2322
|
+
* I.stopMockingRoute(/(\.png$)|(\.jpg$)/, previouslySetHandler);
|
|
2323
|
+
* ```
|
|
2324
|
+
* If no handler is passed, all mock requests for the rote are disabled.
|
|
2325
|
+
*
|
|
2326
|
+
* @param {string} [url] URL, regex or pattern for to match URL
|
|
2327
|
+
* @param {function} [handler] a function to process request
|
|
2328
|
+
*
|
|
2329
|
+
*/
|
|
2330
|
+
async stopMockingRoute(url, handler) {
|
|
2331
|
+
return this.browserContext.unroute(...arguments);
|
|
2332
|
+
}
|
|
2237
2333
|
}
|
|
2238
2334
|
|
|
2239
2335
|
module.exports = Playwright;
|
|
@@ -2249,6 +2345,7 @@ function buildLocatorString(locator) {
|
|
|
2249
2345
|
}
|
|
2250
2346
|
|
|
2251
2347
|
async function findElements(matcher, locator) {
|
|
2348
|
+
if (locator.react) return findReact(matcher, locator);
|
|
2252
2349
|
locator = new Locator(locator, 'css');
|
|
2253
2350
|
return matcher.$$(buildLocatorString(locator));
|
|
2254
2351
|
}
|
|
@@ -2297,6 +2394,8 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
2297
2394
|
}
|
|
2298
2395
|
|
|
2299
2396
|
async function findClickable(matcher, locator) {
|
|
2397
|
+
if (locator.react) return findReact(matcher, locator);
|
|
2398
|
+
|
|
2300
2399
|
locator = new Locator(locator);
|
|
2301
2400
|
if (!locator.isFuzzy()) return findElements.call(this, matcher, locator);
|
|
2302
2401
|
|
|
@@ -2403,6 +2502,15 @@ async function findFields(locator) {
|
|
|
2403
2502
|
}
|
|
2404
2503
|
|
|
2405
2504
|
async function proceedDragAndDrop(sourceLocator, destinationLocator) {
|
|
2505
|
+
// modern drag and drop in Playwright
|
|
2506
|
+
if (this.page.dragAndDrop) {
|
|
2507
|
+
const source = new Locator(sourceLocator);
|
|
2508
|
+
const dest = new Locator(destinationLocator);
|
|
2509
|
+
if (source.isBasic() && dest.isBasic()) {
|
|
2510
|
+
return this.page.dragAndDrop(source.simplify(), dest.simplify());
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2406
2514
|
const src = await this._locate(sourceLocator);
|
|
2407
2515
|
assertElementExists(src, sourceLocator, 'Source Element');
|
|
2408
2516
|
|
|
@@ -2562,11 +2670,20 @@ async function targetCreatedHandler(page) {
|
|
|
2562
2670
|
await page.setUserAgent(this.options.userAgent);
|
|
2563
2671
|
}
|
|
2564
2672
|
if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0 && this._getType() === 'Browser') {
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2673
|
+
await page.setViewportSize(parseWindowSize(this.options.windowSize));
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
function parseWindowSize(windowSize) {
|
|
2678
|
+
if (!windowSize) return { width: 800, height: 600 };
|
|
2679
|
+
const dimensions = windowSize.split('x');
|
|
2680
|
+
if (dimensions.length < 2 || windowSize === 'maximize') {
|
|
2681
|
+
console.log('Invalid window size, setting window to default values');
|
|
2682
|
+
return { width: 800, height: 600 }; // invalid size
|
|
2569
2683
|
}
|
|
2684
|
+
const width = parseInt(dimensions[0], 10);
|
|
2685
|
+
const height = parseInt(dimensions[1], 10);
|
|
2686
|
+
return { width, height };
|
|
2570
2687
|
}
|
|
2571
2688
|
|
|
2572
2689
|
// List of key values to key definitions
|