codeceptjs 2.2.0 → 2.2.1
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 +30 -1
- package/README.md +15 -22
- package/bin/codecept.js +3 -1
- package/docs/advanced.md +1 -1
- package/docs/angular.md +6 -9
- package/docs/basics.md +388 -86
- package/docs/bdd.md +4 -3
- package/docs/build/Nightmare.js +3 -0
- package/docs/build/Polly.js +26 -12
- package/docs/build/Puppeteer.js +14 -13
- package/docs/build/TestCafe.js +101 -2
- package/docs/build/WebDriver.js +53 -52
- package/docs/changelog.md +86 -57
- package/docs/detox.md +235 -0
- package/docs/helpers/Detox.md +579 -0
- package/docs/helpers/Polly.md +13 -3
- package/docs/helpers/Puppeteer.md +155 -156
- package/docs/helpers/TestCafe.md +53 -0
- package/docs/helpers/WebDriver.md +209 -204
- package/docs/locators.md +2 -0
- package/docs/mobile.md +5 -1
- package/docs/puppeteer.md +59 -13
- package/docs/quickstart.md +47 -12
- package/docs/testcafe.md +157 -0
- package/docs/webdriver.md +453 -0
- package/lib/command/definitions.js +152 -7
- package/lib/command/gherkin/snippets.js +19 -8
- package/lib/command/init.js +30 -22
- package/lib/command/utils.js +1 -1
- package/lib/container.js +36 -10
- package/lib/data/dataScenarioConfig.js +18 -0
- package/lib/helper/Nightmare.js +3 -0
- package/lib/helper/Polly.js +26 -12
- package/lib/helper/Puppeteer.js +14 -13
- package/lib/helper/TestCafe.js +72 -2
- package/lib/helper/WebDriver.js +53 -52
- package/lib/helper/testcafe/testcafe-utils.js +3 -2
- package/lib/interfaces/scenarioConfig.js +2 -2
- package/lib/listener/config.js +2 -2
- package/lib/plugin/allure.js +3 -0
- package/lib/step.js +5 -2
- package/lib/ui.js +1 -1
- package/lib/utils.js +13 -21
- package/package.json +14 -12
package/lib/helper/WebDriver.js
CHANGED
|
@@ -15,7 +15,6 @@ const {
|
|
|
15
15
|
chunkArray,
|
|
16
16
|
convertCssPropertiesToCamelCase,
|
|
17
17
|
screenshotOutputFolder,
|
|
18
|
-
fileToBase64Zip,
|
|
19
18
|
} = require('../utils');
|
|
20
19
|
const {
|
|
21
20
|
isColorProperty,
|
|
@@ -61,15 +60,15 @@ const webRoot = 'body';
|
|
|
61
60
|
*
|
|
62
61
|
* Example:
|
|
63
62
|
*
|
|
64
|
-
* ```
|
|
63
|
+
* ```js
|
|
65
64
|
* {
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
65
|
+
* helpers: {
|
|
66
|
+
* WebDriver : {
|
|
67
|
+
* smartWait: 5000,
|
|
68
|
+
* browser: "chrome",
|
|
69
|
+
* restart: false,
|
|
70
|
+
* windowSize: "maximize",
|
|
71
|
+
* timeouts: {
|
|
73
72
|
* "script": 60000,
|
|
74
73
|
* "page load": 10000
|
|
75
74
|
* }
|
|
@@ -83,15 +82,15 @@ const webRoot = 'body';
|
|
|
83
82
|
*
|
|
84
83
|
* ### Headless Chrome
|
|
85
84
|
*
|
|
86
|
-
* ```
|
|
85
|
+
* ```js
|
|
87
86
|
* {
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
87
|
+
* helpers: {
|
|
88
|
+
* WebDriver : {
|
|
89
|
+
* url: "http://localhost",
|
|
90
|
+
* browser: "chrome",
|
|
91
|
+
* desiredCapabilities: {
|
|
92
|
+
* chromeOptions: {
|
|
93
|
+
* args: [ "--headless", "--disable-gpu", "--window-size=800,600" ]
|
|
95
94
|
* }
|
|
96
95
|
* }
|
|
97
96
|
* }
|
|
@@ -103,14 +102,14 @@ const webRoot = 'body';
|
|
|
103
102
|
*
|
|
104
103
|
* Additional configuration params can be used from [IE options](https://seleniumhq.github.io/selenium/docs/api/rb/Selenium/WebDriver/IE/Options.html)
|
|
105
104
|
*
|
|
106
|
-
* ```
|
|
105
|
+
* ```js
|
|
107
106
|
* {
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
107
|
+
* helpers: {
|
|
108
|
+
* WebDriver : {
|
|
109
|
+
* url: "http://localhost",
|
|
110
|
+
* browser: "internet explorer",
|
|
111
|
+
* desiredCapabilities: {
|
|
112
|
+
* ieOptions: {
|
|
114
113
|
* "ie.browserCommandLineSwitches": "-private",
|
|
115
114
|
* "ie.usePerProcessProxy": true,
|
|
116
115
|
* "ie.ensureCleanSession": true,
|
|
@@ -123,15 +122,18 @@ const webRoot = 'body';
|
|
|
123
122
|
*
|
|
124
123
|
* ### Selenoid Options
|
|
125
124
|
*
|
|
126
|
-
*
|
|
125
|
+
* [Selenoid](https://aerokube.com/selenoid/latest/) is a modern way to run Selenium inside Docker containers.
|
|
126
|
+
* Selenoid is easy to set up and provides more features than original Selenium Server. Use `selenoidOptions` to set Selenoid capabilities
|
|
127
|
+
*
|
|
128
|
+
* ```js
|
|
127
129
|
* {
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
130
|
+
* helpers: {
|
|
131
|
+
* WebDriver : {
|
|
132
|
+
* url: "http://localhost",
|
|
133
|
+
* browser: "chrome",
|
|
134
|
+
* desiredCapabilities: {
|
|
135
|
+
* selenoidOptions: {
|
|
136
|
+
* enableVNC: true,
|
|
135
137
|
* }
|
|
136
138
|
* }
|
|
137
139
|
* }
|
|
@@ -139,17 +141,17 @@ const webRoot = 'body';
|
|
|
139
141
|
* }
|
|
140
142
|
* ```
|
|
141
143
|
*
|
|
142
|
-
* ### Connect
|
|
144
|
+
* ### Connect Through proxy
|
|
143
145
|
*
|
|
144
146
|
* CodeceptJS also provides flexible options when you want to execute tests to Selenium servers through proxy. You will
|
|
145
147
|
* need to update the `helpers.WebDriver.capabilities.proxy` key.
|
|
146
148
|
*
|
|
147
149
|
* ```js
|
|
148
150
|
* {
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
151
|
+
* helpers: {
|
|
152
|
+
* WebDriver: {
|
|
153
|
+
* capabilities: {
|
|
154
|
+
* proxy: {
|
|
153
155
|
* "proxyType": "manual|pac",
|
|
154
156
|
* "proxyAutoconfigUrl": "URL TO PAC FILE",
|
|
155
157
|
* "httpProxy": "PROXY SERVER",
|
|
@@ -169,10 +171,10 @@ const webRoot = 'body';
|
|
|
169
171
|
*
|
|
170
172
|
* ```js
|
|
171
173
|
* {
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
174
|
+
* helpers: {
|
|
175
|
+
* WebDriver: {
|
|
176
|
+
* capabilities: {
|
|
177
|
+
* proxy: {
|
|
176
178
|
* "proxyType": "manual",
|
|
177
179
|
* "httpProxy": "http://corporate.proxy:8080",
|
|
178
180
|
* "socksUsername": "codeceptjs",
|
|
@@ -199,12 +201,12 @@ const webRoot = 'body';
|
|
|
199
201
|
*
|
|
200
202
|
* ```js
|
|
201
203
|
* {
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
204
|
+
* helpers:{
|
|
205
|
+
* WebDriver: {
|
|
206
|
+
* url: "YOUR_DESIRED_HOST",
|
|
207
|
+
* user: "YOUR_BROWSERSTACK_USER",
|
|
208
|
+
* key: "YOUR_BROWSERSTACK_KEY",
|
|
209
|
+
* capabilities: {
|
|
208
210
|
* "browserName": "chrome",
|
|
209
211
|
*
|
|
210
212
|
* // only set this if you're using BrowserStackLocal to test a local domain
|
|
@@ -318,8 +320,8 @@ const webRoot = 'body';
|
|
|
318
320
|
*
|
|
319
321
|
* ```js
|
|
320
322
|
* {
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
+
* helpers: {
|
|
324
|
+
* WebDriver: {
|
|
323
325
|
* "multiremote": {
|
|
324
326
|
* "MyChrome": {
|
|
325
327
|
* "desiredCapabilities": {
|
|
@@ -852,12 +854,11 @@ class WebDriver extends Helper {
|
|
|
852
854
|
assertElementExists(res, locator, 'File field');
|
|
853
855
|
const el = usingFirstElement(res);
|
|
854
856
|
|
|
855
|
-
// Remote
|
|
857
|
+
// Remote Upload (when running Selenium Server)
|
|
856
858
|
if (this.options.remoteFileUpload) {
|
|
857
|
-
const fileCompressed = await fileToBase64Zip(file);
|
|
858
859
|
try {
|
|
859
860
|
this.debugSection('File', 'Uploading file to remote server');
|
|
860
|
-
file = await this.browser.uploadFile(
|
|
861
|
+
file = await this.browser.uploadFile(file);
|
|
861
862
|
} catch (err) {
|
|
862
863
|
throw new Error(`File can't be transferred to remote server. Set \`remoteFileUpload: false\` in config to upload file locally.\n${err.message}`);
|
|
863
864
|
}
|
|
@@ -9,7 +9,7 @@ const createTestFile = () => {
|
|
|
9
9
|
assert(global.output_dir, 'global.output_dir must be set');
|
|
10
10
|
|
|
11
11
|
const testFile = path.join(global.output_dir, `${Date.now()}_test.js`);
|
|
12
|
-
const testControllerHolderDir = __dirname;
|
|
12
|
+
const testControllerHolderDir = __dirname.replace(/\\/g, '/');
|
|
13
13
|
|
|
14
14
|
fs.writeFileSync(
|
|
15
15
|
testFile,
|
|
@@ -28,7 +28,8 @@ const mapError = (testcafeError) => {
|
|
|
28
28
|
if (testcafeError.errMsg) {
|
|
29
29
|
throw new Error(testcafeError.errMsg);
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
const errorInfo = `${testcafeError.callsite ? JSON.stringify(testcafeError.callsite) : ''} ${testcafeError.apiFnChain || JSON.stringify(testcafeError)}`;
|
|
32
|
+
throw new Error(`TestCafe Error: ${errorInfo}`);
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
|
|
@@ -58,13 +58,13 @@ class ScenarioConfig {
|
|
|
58
58
|
* Configures a helper.
|
|
59
59
|
* Helper name can be omitted and values will be applied to first helper.
|
|
60
60
|
*/
|
|
61
|
-
config(helper, obj) {
|
|
61
|
+
async config(helper, obj) {
|
|
62
62
|
if (!obj) {
|
|
63
63
|
obj = helper;
|
|
64
64
|
helper = 0;
|
|
65
65
|
}
|
|
66
66
|
if (typeof obj === 'function') {
|
|
67
|
-
obj = obj(this.test);
|
|
67
|
+
obj = await obj(this.test);
|
|
68
68
|
}
|
|
69
69
|
if (!this.test.config) {
|
|
70
70
|
this.test.config = {};
|
package/lib/listener/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const event = require('../event');
|
|
2
2
|
const container = require('../container');
|
|
3
3
|
const recorder = require('../recorder');
|
|
4
|
-
const { deepMerge, ucfirst } = require('../utils');
|
|
4
|
+
const { deepMerge, deepClone, ucfirst } = require('../utils');
|
|
5
5
|
const { debug } = require('../output');
|
|
6
6
|
/**
|
|
7
7
|
* Enable Helpers to listen to test events
|
|
@@ -17,7 +17,7 @@ module.exports = function () {
|
|
|
17
17
|
function updateHelperConfig(helper, config) {
|
|
18
18
|
const oldConfig = Object.assign({}, helper.options);
|
|
19
19
|
try {
|
|
20
|
-
helper._setConfig(deepMerge(
|
|
20
|
+
helper._setConfig(deepMerge(deepClone(oldConfig), config));
|
|
21
21
|
debug(`[${ucfirst(type)} Config] ${helper.constructor.name} ${JSON.stringify(config)}`);
|
|
22
22
|
} catch (err) {
|
|
23
23
|
recorder.throw(err);
|
package/lib/plugin/allure.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const event = require('../event');
|
|
2
2
|
const Allure = require('allure-js-commons');
|
|
3
3
|
const logger = require('../output');
|
|
4
|
+
const ansiRegExp = require('../utils').ansiRegExp;
|
|
4
5
|
|
|
5
6
|
const defaultConfig = {
|
|
6
7
|
outputDir: global.output_dir,
|
|
@@ -193,6 +194,8 @@ module.exports = (config) => {
|
|
|
193
194
|
currentMetaStep.forEach(() => reporter.endStep('failed'));
|
|
194
195
|
currentMetaStep = [];
|
|
195
196
|
}
|
|
197
|
+
|
|
198
|
+
err.message = err.message.replace(ansiRegExp(), '');
|
|
196
199
|
reporter.endCase('failed', err);
|
|
197
200
|
});
|
|
198
201
|
|
package/lib/step.js
CHANGED
|
@@ -154,8 +154,11 @@ function detectMetaStep(stack) {
|
|
|
154
154
|
if (isTest(line) || isBDD(line)) break;
|
|
155
155
|
const fnName = line.match(/^at (\w+)\.(\w+)\s\(/);
|
|
156
156
|
if (!fnName) continue;
|
|
157
|
-
if (fnName[1] === 'Generator'
|
|
158
|
-
|
|
157
|
+
if (fnName[1] === 'Generator'
|
|
158
|
+
|| fnName[1] === 'recorder'
|
|
159
|
+
|| fnName[1] === 'Runner'
|
|
160
|
+
) { return; } // don't track meta steps inside generators
|
|
161
|
+
|
|
159
162
|
if (fnName[1] === 'Object') {
|
|
160
163
|
// detect PO name from includes
|
|
161
164
|
for (const name in support) {
|
package/lib/ui.js
CHANGED
package/lib/utils.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const getFunctionArguments = require('fn-args');
|
|
4
|
+
const deepClone = require('lodash.clonedeep');
|
|
4
5
|
const { convertColorToRGBA, isColorProperty } = require('./colorUtils');
|
|
5
6
|
|
|
6
7
|
function isObject(item) {
|
|
@@ -26,6 +27,8 @@ function deepMerge(target, source) {
|
|
|
26
27
|
|
|
27
28
|
module.exports.deepMerge = deepMerge;
|
|
28
29
|
|
|
30
|
+
module.exports.deepClone = deepClone;
|
|
31
|
+
|
|
29
32
|
const isGenerator = module.exports.isGenerator = function (fn) {
|
|
30
33
|
return fn.constructor.name === 'GeneratorFunction';
|
|
31
34
|
};
|
|
@@ -281,26 +284,6 @@ module.exports.screenshotOutputFolder = function (fileName) {
|
|
|
281
284
|
return path.join(global.codecept_dir, fileName);
|
|
282
285
|
};
|
|
283
286
|
|
|
284
|
-
module.exports.fileToBase64Zip = async function (localPath) {
|
|
285
|
-
const archiver = require('archiver');
|
|
286
|
-
|
|
287
|
-
const zipData = [];
|
|
288
|
-
const source = fs.createReadStream(localPath);
|
|
289
|
-
|
|
290
|
-
return new Promise((resolve, reject) => {
|
|
291
|
-
archiver('zip')
|
|
292
|
-
.on('error', (e) => { throw new Error(e); })
|
|
293
|
-
.on('data', data => zipData.push(data))
|
|
294
|
-
.on('end', () => resolve(Buffer.concat(zipData).toString('base64')))
|
|
295
|
-
.append(source, { name: path.basename(localPath) })
|
|
296
|
-
.finalize((err) => {
|
|
297
|
-
if (err) {
|
|
298
|
-
reject(err);
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
};
|
|
303
|
-
|
|
304
287
|
module.exports.beautify = function (code) {
|
|
305
288
|
const format = require('js-beautify').js;
|
|
306
289
|
return format(code, { indent_size: 2, space_in_empty_paren: true });
|
|
@@ -322,7 +305,7 @@ function joinUrl(baseUrl, url) {
|
|
|
322
305
|
return shouldAppendBaseUrl(url) ? `${baseUrl}/${trimUrl(url)}` : url;
|
|
323
306
|
}
|
|
324
307
|
|
|
325
|
-
module.exports.appendBaseUrl = function (baseUrl, oneOrMoreUrls) {
|
|
308
|
+
module.exports.appendBaseUrl = function (baseUrl = '', oneOrMoreUrls) {
|
|
326
309
|
// Remove '/' if it's at the end of baseUrl
|
|
327
310
|
const lastChar = baseUrl.substr(-1);
|
|
328
311
|
if (lastChar === '/') {
|
|
@@ -363,3 +346,12 @@ module.exports.replaceValueDeep = function replaceValueDeep(obj, key, value) {
|
|
|
363
346
|
}
|
|
364
347
|
return obj;
|
|
365
348
|
};
|
|
349
|
+
|
|
350
|
+
module.exports.ansiRegExp = function ({ onlyFirst = false } = {}) {
|
|
351
|
+
const pattern = [
|
|
352
|
+
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
|
|
353
|
+
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))',
|
|
354
|
+
].join('|');
|
|
355
|
+
|
|
356
|
+
return new RegExp(pattern, onlyFirst ? undefined : 'g');
|
|
357
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeceptjs",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Modern Era Acceptance Testing Framework for NodeJS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"acceptance",
|
|
@@ -37,25 +37,27 @@
|
|
|
37
37
|
"test": "mocha test/unit --recursive && mocha test/runner --recursive"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
+
"@codeceptjs/detox-helper": "^1.0.1",
|
|
40
41
|
"allure-js-commons": "^1.3.2",
|
|
41
42
|
"archiver": "^3.0.0",
|
|
42
43
|
"axios": "^0.19.0",
|
|
43
44
|
"chalk": "^1.1.3",
|
|
44
45
|
"commander": "^2.20.0",
|
|
45
46
|
"css-to-xpath": "^0.1.0",
|
|
46
|
-
"cucumber-expressions": "^6.
|
|
47
|
+
"cucumber-expressions": "^6.6.2",
|
|
47
48
|
"escape-string-regexp": "^1.0.3",
|
|
48
49
|
"figures": "^2.0.0",
|
|
49
50
|
"fn-args": "^4.0.0",
|
|
50
51
|
"fs-extra": "^8.0.1",
|
|
51
52
|
"gherkin": "^5.1.0",
|
|
52
53
|
"glob": "^6.0.1",
|
|
53
|
-
"inquirer": "^6.
|
|
54
|
-
"js-beautify": "^1.
|
|
54
|
+
"inquirer": "^6.4.1",
|
|
55
|
+
"js-beautify": "^1.10.0",
|
|
56
|
+
"lodash.clonedeep": "^4.5.0",
|
|
55
57
|
"lodash.merge": "^4.6.1",
|
|
56
58
|
"mkdirp": "^0.5.1",
|
|
57
59
|
"mocha": "^4.1.0",
|
|
58
|
-
"mocha-junit-reporter": "^1.
|
|
60
|
+
"mocha-junit-reporter": "^1.23.0",
|
|
59
61
|
"parse-function": "^5.2.10",
|
|
60
62
|
"promise-retry": "^1.1.1",
|
|
61
63
|
"requireg": "^0.1.8",
|
|
@@ -66,31 +68,31 @@
|
|
|
66
68
|
"@pollyjs/adapter-puppeteer": "^2.5.0",
|
|
67
69
|
"@pollyjs/core": "^2.5.0",
|
|
68
70
|
"@types/inquirer": "^0.0.35",
|
|
69
|
-
"@types/node": "^8.10.
|
|
70
|
-
"@wdio/sauce-service": "^5.8
|
|
71
|
-
"@wdio/selenium-standalone-service": "^5.
|
|
72
|
-
"@wdio/utils": "^5.
|
|
71
|
+
"@types/node": "^8.10.49",
|
|
72
|
+
"@wdio/sauce-service": "^5.10.8",
|
|
73
|
+
"@wdio/selenium-standalone-service": "^5.9.3",
|
|
74
|
+
"@wdio/utils": "^5.9.3",
|
|
73
75
|
"chai": "^3.4.1",
|
|
74
76
|
"chai-as-promised": "^5.2.0",
|
|
75
77
|
"co-mocha": "^1.2",
|
|
76
78
|
"documentation": "^8.1.2",
|
|
77
79
|
"eslint": "^4.17.0",
|
|
78
80
|
"eslint-config-airbnb-base": "^12.1.0",
|
|
79
|
-
"eslint-plugin-import": "^2.
|
|
81
|
+
"eslint-plugin-import": "^2.18.0",
|
|
80
82
|
"eslint-plugin-mocha": "^5.3.0",
|
|
81
83
|
"faker": "^4.1.0",
|
|
82
84
|
"husky": "^1.2.1",
|
|
83
85
|
"json-server": "^0.10.1",
|
|
84
86
|
"nightmare": "^3.0.2",
|
|
85
87
|
"protractor": "^5.4.1",
|
|
86
|
-
"puppeteer": "^1.
|
|
88
|
+
"puppeteer": "^1.18.1",
|
|
87
89
|
"rosie": "^1.6.0",
|
|
88
90
|
"sinon": "^1.17.2",
|
|
89
91
|
"sinon-chai": "^2.14.0",
|
|
90
92
|
"testcafe": "^1.2.1",
|
|
91
93
|
"typescript": "^2.9.2",
|
|
92
94
|
"wdio-docker-service": "^1.5.0",
|
|
93
|
-
"webdriverio": "^5.
|
|
95
|
+
"webdriverio": "^5.10.9",
|
|
94
96
|
"xmldom": "^0.1.27",
|
|
95
97
|
"xpath": "0.0.27"
|
|
96
98
|
},
|