codeceptjs 3.4.1 → 3.5.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 +85 -0
- package/README.md +11 -9
- package/bin/codecept.js +1 -1
- package/docs/ai.md +248 -0
- package/docs/build/Appium.js +47 -7
- package/docs/build/JSONResponse.js +4 -4
- package/docs/build/Nightmare.js +3 -1
- package/docs/build/OpenAI.js +122 -0
- package/docs/build/Playwright.js +234 -54
- package/docs/build/Protractor.js +3 -1
- package/docs/build/Puppeteer.js +101 -12
- package/docs/build/REST.js +15 -5
- package/docs/build/TestCafe.js +61 -2
- package/docs/build/WebDriver.js +85 -5
- package/docs/changelog.md +85 -0
- package/docs/helpers/Appium.md +152 -147
- package/docs/helpers/JSONResponse.md +4 -4
- package/docs/helpers/Nightmare.md +2 -0
- package/docs/helpers/OpenAI.md +70 -0
- package/docs/helpers/Playwright.md +228 -151
- package/docs/helpers/Puppeteer.md +153 -101
- package/docs/helpers/REST.md +6 -5
- package/docs/helpers/TestCafe.md +97 -49
- package/docs/helpers/WebDriver.md +159 -107
- package/docs/mobile.md +49 -2
- package/docs/parallel.md +56 -0
- package/docs/plugins.md +87 -33
- package/docs/secrets.md +6 -0
- package/docs/tutorial.md +2 -2
- package/docs/webapi/appendField.mustache +2 -0
- package/docs/webapi/blur.mustache +17 -0
- package/docs/webapi/focus.mustache +12 -0
- package/docs/webapi/type.mustache +3 -0
- package/lib/ai.js +171 -0
- package/lib/cli.js +10 -2
- package/lib/codecept.js +4 -0
- package/lib/command/dryRun.js +9 -1
- package/lib/command/generate.js +46 -3
- package/lib/command/init.js +23 -1
- package/lib/command/interactive.js +15 -1
- package/lib/command/run-workers.js +2 -1
- package/lib/container.js +13 -3
- package/lib/event.js +2 -0
- package/lib/helper/Appium.js +45 -7
- package/lib/helper/JSONResponse.js +4 -4
- package/lib/helper/Nightmare.js +1 -1
- package/lib/helper/OpenAI.js +122 -0
- package/lib/helper/Playwright.js +200 -45
- package/lib/helper/Protractor.js +1 -1
- package/lib/helper/Puppeteer.js +67 -12
- package/lib/helper/REST.js +15 -5
- package/lib/helper/TestCafe.js +30 -2
- package/lib/helper/WebDriver.js +51 -5
- package/lib/helper/scripts/blurElement.js +17 -0
- package/lib/helper/scripts/focusElement.js +17 -0
- package/lib/helper/scripts/highlightElement.js +20 -0
- package/lib/html.js +258 -0
- package/lib/interfaces/gherkin.js +8 -0
- package/lib/listener/retry.js +2 -1
- package/lib/pause.js +73 -17
- package/lib/plugin/debugErrors.js +67 -0
- package/lib/plugin/fakerTransform.js +4 -6
- package/lib/plugin/heal.js +177 -0
- package/lib/plugin/screenshotOnFail.js +11 -2
- package/lib/recorder.js +11 -8
- package/lib/secret.js +5 -4
- package/lib/step.js +6 -1
- package/lib/ui.js +4 -3
- package/lib/utils.js +17 -0
- package/lib/workers.js +57 -9
- package/package.json +25 -16
- package/translations/ja-JP.js +9 -9
- package/typings/index.d.ts +43 -9
- package/typings/promiseBasedTypes.d.ts +242 -25
- package/typings/types.d.ts +260 -35
package/lib/helper/TestCafe.js
CHANGED
|
@@ -346,6 +346,34 @@ class TestCafe extends Helper {
|
|
|
346
346
|
return this.t.resizeWindow(width, height).catch(mapError);
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
/**
|
|
350
|
+
* {{> focus }}
|
|
351
|
+
*
|
|
352
|
+
*/
|
|
353
|
+
async focus(locator) {
|
|
354
|
+
const els = await this._locate(locator);
|
|
355
|
+
await assertElementExists(els, locator, 'Element to focus');
|
|
356
|
+
const element = await els.nth(0);
|
|
357
|
+
|
|
358
|
+
const focusElement = ClientFunction(() => element().focus(), { boundTestRun: this.t, dependencies: { element } });
|
|
359
|
+
|
|
360
|
+
return focusElement();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* {{> blur }}
|
|
365
|
+
*
|
|
366
|
+
*/
|
|
367
|
+
async blur(locator) {
|
|
368
|
+
const els = await this._locate(locator);
|
|
369
|
+
await assertElementExists(els, locator, 'Element to blur');
|
|
370
|
+
const element = await els.nth(0);
|
|
371
|
+
|
|
372
|
+
const blurElement = ClientFunction(() => element().blur(), { boundTestRun: this.t, dependencies: { element } });
|
|
373
|
+
|
|
374
|
+
return blurElement();
|
|
375
|
+
}
|
|
376
|
+
|
|
349
377
|
/**
|
|
350
378
|
* {{> click }}
|
|
351
379
|
*
|
|
@@ -410,7 +438,7 @@ class TestCafe extends Helper {
|
|
|
410
438
|
const el = await els.nth(0);
|
|
411
439
|
|
|
412
440
|
return this.t
|
|
413
|
-
.typeText(el, value, { replace: false })
|
|
441
|
+
.typeText(el, value.toString(), { replace: false })
|
|
414
442
|
.catch(mapError);
|
|
415
443
|
}
|
|
416
444
|
|
|
@@ -792,7 +820,7 @@ class TestCafe extends Helper {
|
|
|
792
820
|
/**
|
|
793
821
|
* {{> executeScript }}
|
|
794
822
|
*
|
|
795
|
-
* If a function returns a Promise It will wait for
|
|
823
|
+
* If a function returns a Promise It will wait for its resolution.
|
|
796
824
|
*/
|
|
797
825
|
async executeScript(fn, ...args) {
|
|
798
826
|
const browserFn = createClientFunction(fn, args).with({ boundTestRun: this.t });
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
|
|
7
7
|
const Helper = require('@codeceptjs/helper');
|
|
8
|
+
const crypto = require('crypto');
|
|
8
9
|
const stringIncludes = require('../assert/include').includes;
|
|
9
10
|
const { urlEquals, equals } = require('../assert/equal');
|
|
10
11
|
const { debug } = require('../output');
|
|
@@ -27,6 +28,10 @@ const {
|
|
|
27
28
|
const ElementNotFound = require('./errors/ElementNotFound');
|
|
28
29
|
const ConnectionRefused = require('./errors/ConnectionRefused');
|
|
29
30
|
const Locator = require('../locator');
|
|
31
|
+
const { highlightElement } = require('./scripts/highlightElement');
|
|
32
|
+
const store = require('../store');
|
|
33
|
+
const { focusElement } = require('./scripts/focusElement');
|
|
34
|
+
const { blurElement } = require('./scripts/blurElement');
|
|
30
35
|
|
|
31
36
|
const SHADOW = 'shadow';
|
|
32
37
|
const webRoot = 'body';
|
|
@@ -39,7 +44,7 @@ const webRoot = 'body';
|
|
|
39
44
|
* @typedef WebDriverConfig
|
|
40
45
|
* @type {object}
|
|
41
46
|
* @prop {string} url - base url of website to be tested.
|
|
42
|
-
* @prop {string} browser
|
|
47
|
+
* @prop {string} browser - Browser in which to perform testing.
|
|
43
48
|
* @prop {string} [basicAuth] - (optional) the basic authentication to pass to base url. Example: {username: 'username', password: 'password'}
|
|
44
49
|
* @prop {string} [host=localhost] - WebDriver host to connect.
|
|
45
50
|
* @prop {number} [port=4444] - WebDriver port to connect.
|
|
@@ -57,6 +62,7 @@ const webRoot = 'body';
|
|
|
57
62
|
* @prop {object} [desiredCapabilities] Selenium's [desired capabilities](https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities).
|
|
58
63
|
* @prop {boolean} [manualStart=false] - do not start browser before a test, start it manually inside a helper with `this.helpers["WebDriver"]._startBrowser()`.
|
|
59
64
|
* @prop {object} [timeouts] [WebDriver timeouts](http://webdriver.io/docs/timeouts.html) defined as hash.
|
|
65
|
+
* @prop {boolean} [highlightElement] - highlight the interacting elements
|
|
60
66
|
*/
|
|
61
67
|
const config = {};
|
|
62
68
|
|
|
@@ -822,7 +828,7 @@ class WebDriver extends Helper {
|
|
|
822
828
|
}
|
|
823
829
|
|
|
824
830
|
/**
|
|
825
|
-
* Find a checkbox by providing human
|
|
831
|
+
* Find a checkbox by providing human-readable text:
|
|
826
832
|
*
|
|
827
833
|
* ```js
|
|
828
834
|
* this.helpers['WebDriver']._locateCheckable('I agree with terms and conditions').then // ...
|
|
@@ -835,7 +841,7 @@ class WebDriver extends Helper {
|
|
|
835
841
|
}
|
|
836
842
|
|
|
837
843
|
/**
|
|
838
|
-
* Find a clickable element by providing human
|
|
844
|
+
* Find a clickable element by providing human-readable text:
|
|
839
845
|
*
|
|
840
846
|
* ```js
|
|
841
847
|
* const els = await this.helpers.WebDriver._locateClickable('Next page');
|
|
@@ -850,7 +856,7 @@ class WebDriver extends Helper {
|
|
|
850
856
|
}
|
|
851
857
|
|
|
852
858
|
/**
|
|
853
|
-
* Find field elements by providing human
|
|
859
|
+
* Find field elements by providing human-readable text:
|
|
854
860
|
*
|
|
855
861
|
* ```js
|
|
856
862
|
* this.helpers['WebDriver']._locateFields('Your email').then // ...
|
|
@@ -914,6 +920,7 @@ class WebDriver extends Helper {
|
|
|
914
920
|
assertElementExists(res, locator, 'Clickable element');
|
|
915
921
|
}
|
|
916
922
|
const elem = usingFirstElement(res);
|
|
923
|
+
highlightActiveElement.call(this, elem);
|
|
917
924
|
return this.browser[clickMethod](getElementId(elem));
|
|
918
925
|
}
|
|
919
926
|
|
|
@@ -932,6 +939,7 @@ class WebDriver extends Helper {
|
|
|
932
939
|
assertElementExists(res, locator, 'Clickable element');
|
|
933
940
|
}
|
|
934
941
|
const elem = usingFirstElement(res);
|
|
942
|
+
highlightActiveElement.call(this, elem);
|
|
935
943
|
|
|
936
944
|
return this.executeScript((el) => {
|
|
937
945
|
if (document.activeElement instanceof HTMLElement) {
|
|
@@ -959,6 +967,7 @@ class WebDriver extends Helper {
|
|
|
959
967
|
}
|
|
960
968
|
|
|
961
969
|
const elem = usingFirstElement(res);
|
|
970
|
+
highlightActiveElement.call(this, elem);
|
|
962
971
|
return elem.doubleClick();
|
|
963
972
|
}
|
|
964
973
|
|
|
@@ -1024,6 +1033,7 @@ class WebDriver extends Helper {
|
|
|
1024
1033
|
const res = await findFields.call(this, field);
|
|
1025
1034
|
assertElementExists(res, field, 'Field');
|
|
1026
1035
|
const elem = usingFirstElement(res);
|
|
1036
|
+
highlightActiveElement.call(this, elem);
|
|
1027
1037
|
return elem.setValue(value.toString());
|
|
1028
1038
|
}
|
|
1029
1039
|
|
|
@@ -1035,7 +1045,8 @@ class WebDriver extends Helper {
|
|
|
1035
1045
|
const res = await findFields.call(this, field);
|
|
1036
1046
|
assertElementExists(res, field, 'Field');
|
|
1037
1047
|
const elem = usingFirstElement(res);
|
|
1038
|
-
|
|
1048
|
+
highlightActiveElement.call(this, elem);
|
|
1049
|
+
return elem.addValue(value.toString());
|
|
1039
1050
|
}
|
|
1040
1051
|
|
|
1041
1052
|
/**
|
|
@@ -1046,6 +1057,7 @@ class WebDriver extends Helper {
|
|
|
1046
1057
|
const res = await findFields.call(this, field);
|
|
1047
1058
|
assertElementExists(res, field, 'Field');
|
|
1048
1059
|
const elem = usingFirstElement(res);
|
|
1060
|
+
highlightActiveElement.call(this, elem);
|
|
1049
1061
|
return elem.clearValue(getElementId(elem));
|
|
1050
1062
|
}
|
|
1051
1063
|
|
|
@@ -1056,6 +1068,7 @@ class WebDriver extends Helper {
|
|
|
1056
1068
|
const res = await findFields.call(this, select);
|
|
1057
1069
|
assertElementExists(res, select, 'Selectable field');
|
|
1058
1070
|
const elem = usingFirstElement(res);
|
|
1071
|
+
highlightActiveElement.call(this, elem);
|
|
1059
1072
|
|
|
1060
1073
|
if (!Array.isArray(option)) {
|
|
1061
1074
|
option = [option];
|
|
@@ -1122,6 +1135,7 @@ class WebDriver extends Helper {
|
|
|
1122
1135
|
assertElementExists(res, field, 'Checkable');
|
|
1123
1136
|
const elem = usingFirstElement(res);
|
|
1124
1137
|
const elementId = getElementId(elem);
|
|
1138
|
+
highlightActiveElement.call(this, elem);
|
|
1125
1139
|
|
|
1126
1140
|
const isSelected = await this.browser.isElementSelected(elementId);
|
|
1127
1141
|
if (isSelected) return Promise.resolve(true);
|
|
@@ -1141,6 +1155,7 @@ class WebDriver extends Helper {
|
|
|
1141
1155
|
assertElementExists(res, field, 'Checkable');
|
|
1142
1156
|
const elem = usingFirstElement(res);
|
|
1143
1157
|
const elementId = getElementId(elem);
|
|
1158
|
+
highlightActiveElement.call(this, elem);
|
|
1144
1159
|
|
|
1145
1160
|
const isSelected = await this.browser.isElementSelected(elementId);
|
|
1146
1161
|
if (!isSelected) return Promise.resolve(true);
|
|
@@ -1879,6 +1894,7 @@ class WebDriver extends Helper {
|
|
|
1879
1894
|
*/
|
|
1880
1895
|
async type(keys, delay = null) {
|
|
1881
1896
|
if (!Array.isArray(keys)) {
|
|
1897
|
+
keys = keys.toString();
|
|
1882
1898
|
keys = keys.split('');
|
|
1883
1899
|
}
|
|
1884
1900
|
if (delay) {
|
|
@@ -1920,6 +1936,30 @@ class WebDriver extends Helper {
|
|
|
1920
1936
|
}
|
|
1921
1937
|
}
|
|
1922
1938
|
|
|
1939
|
+
/**
|
|
1940
|
+
* {{> focus }}
|
|
1941
|
+
*
|
|
1942
|
+
*/
|
|
1943
|
+
async focus(locator) {
|
|
1944
|
+
const els = await this._locate(locator);
|
|
1945
|
+
assertElementExists(els, locator, 'Element to focus');
|
|
1946
|
+
const el = usingFirstElement(els);
|
|
1947
|
+
|
|
1948
|
+
await focusElement(el, this.browser);
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
/**
|
|
1952
|
+
* {{> blur }}
|
|
1953
|
+
*
|
|
1954
|
+
*/
|
|
1955
|
+
async blur(locator) {
|
|
1956
|
+
const els = await this._locate(locator);
|
|
1957
|
+
assertElementExists(els, locator, 'Element to blur');
|
|
1958
|
+
const el = usingFirstElement(els);
|
|
1959
|
+
|
|
1960
|
+
await blurElement(el, this.browser);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1923
1963
|
/**
|
|
1924
1964
|
* {{> dragAndDrop }}
|
|
1925
1965
|
* Appium: not tested
|
|
@@ -2873,6 +2913,12 @@ function isModifierKey(key) {
|
|
|
2873
2913
|
return unicodeModifierKeys.includes(key);
|
|
2874
2914
|
}
|
|
2875
2915
|
|
|
2916
|
+
function highlightActiveElement(element) {
|
|
2917
|
+
if (!this.options.enableHighlight && !store.debugMode) return;
|
|
2918
|
+
|
|
2919
|
+
highlightElement(element, this.browser);
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2876
2922
|
function prepareLocateFn(context) {
|
|
2877
2923
|
if (!context) return this._locate.bind(this);
|
|
2878
2924
|
return (l) => {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module.exports.blurElement = (element, context) => {
|
|
2
|
+
const clientSideBlurFn = el => {
|
|
3
|
+
el.blur();
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
// Puppeteer
|
|
8
|
+
context.evaluate(clientSideBlurFn, element);
|
|
9
|
+
} catch (e) {
|
|
10
|
+
// WebDriver
|
|
11
|
+
try {
|
|
12
|
+
context.execute(clientSideBlurFn, element);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
// ignore
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module.exports.focusElement = (element, context) => {
|
|
2
|
+
const clientSideFn = el => {
|
|
3
|
+
el.focus();
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
// Puppeteer
|
|
8
|
+
context.evaluate(clientSideFn, element);
|
|
9
|
+
} catch (e) {
|
|
10
|
+
// WebDriver
|
|
11
|
+
try {
|
|
12
|
+
context.execute(clientSideFn, element);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
// ignore
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module.exports.highlightElement = (element, context) => {
|
|
2
|
+
const clientSideHighlightFn = el => {
|
|
3
|
+
const style = '0px 0px 4px 3px rgba(255, 0, 0, 0.7)';
|
|
4
|
+
const prevStyle = el.style.boxShadow;
|
|
5
|
+
el.style.boxShadow = style;
|
|
6
|
+
setTimeout(() => el.style.boxShadow = prevStyle, 2000);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
// Playwright, Puppeteer
|
|
11
|
+
context.evaluate(clientSideHighlightFn, element);
|
|
12
|
+
} catch (e) {
|
|
13
|
+
// WebDriver
|
|
14
|
+
try {
|
|
15
|
+
context.execute(clientSideHighlightFn, element);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
// ignore
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
package/lib/html.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
const { parse, serialize } = require('parse5');
|
|
2
|
+
const { minify } = require('html-minifier');
|
|
3
|
+
|
|
4
|
+
function minifyHtml(html) {
|
|
5
|
+
return minify(html, {
|
|
6
|
+
collapseWhitespace: true,
|
|
7
|
+
removeComments: true,
|
|
8
|
+
removeEmptyAttributes: true,
|
|
9
|
+
removeRedundantAttributes: true,
|
|
10
|
+
removeScriptTypeAttributes: true,
|
|
11
|
+
removeStyleLinkTypeAttributes: true,
|
|
12
|
+
collapseBooleanAttributes: true,
|
|
13
|
+
useShortDoctype: true,
|
|
14
|
+
}).toString();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const defaultHtmlOpts = {
|
|
18
|
+
interactiveElements: ['a', 'input', 'button', 'select', 'textarea', 'option'],
|
|
19
|
+
textElements: ['label', 'h1', 'h2'],
|
|
20
|
+
allowedAttrs: ['id', 'for', 'class', 'name', 'type', 'value', 'tabindex', 'aria-labelledby', 'aria-label', 'label', 'placeholder', 'title', 'alt', 'src', 'role'],
|
|
21
|
+
allowedRoles: ['button', 'checkbox', 'search', 'textbox', 'tab'],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function removeNonInteractiveElements(html, opts = {}) {
|
|
25
|
+
opts = { ...defaultHtmlOpts, ...opts };
|
|
26
|
+
const {
|
|
27
|
+
interactiveElements,
|
|
28
|
+
textElements,
|
|
29
|
+
allowedAttrs,
|
|
30
|
+
allowedRoles,
|
|
31
|
+
} = opts;
|
|
32
|
+
|
|
33
|
+
// Parse the HTML into a document tree
|
|
34
|
+
const document = parse(html);
|
|
35
|
+
|
|
36
|
+
const trashHtmlClasses = /^(text-|color-|flex-|float-|v-|ember-|d-|border-)/;
|
|
37
|
+
// Array to store interactive elements
|
|
38
|
+
const removeElements = ['path', 'script'];
|
|
39
|
+
|
|
40
|
+
function isFilteredOut(node) {
|
|
41
|
+
if (removeElements.includes(node.nodeName)) return true;
|
|
42
|
+
if (node.attrs) {
|
|
43
|
+
if (node.attrs.find(attr => attr.name === 'role' && attr.value === 'tooltip')) return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Function to check if an element is interactive
|
|
49
|
+
function isInteractive(element) {
|
|
50
|
+
if (element.nodeName === 'input' && element.attrs.find(attr => attr.name === 'type' && attr.value === 'hidden')) return false;
|
|
51
|
+
if (interactiveElements.includes(element.nodeName)) return true;
|
|
52
|
+
if (element.attrs) {
|
|
53
|
+
if (element.attrs.find(attr => attr.name === 'contenteditable')) return true;
|
|
54
|
+
if (element.attrs.find(attr => attr.name === 'tabindex')) return true;
|
|
55
|
+
const role = element.attrs.find(attr => attr.name === 'role');
|
|
56
|
+
if (role && allowedRoles.includes(role.value)) return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hasMeaningfulText(node) {
|
|
62
|
+
if (textElements.includes(node.nodeName)) return true;
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function hasInteractiveDescendant(node) {
|
|
67
|
+
if (!node.childNodes) return false;
|
|
68
|
+
let result = false;
|
|
69
|
+
|
|
70
|
+
for (const childNode of node.childNodes) {
|
|
71
|
+
if (isInteractive(childNode) || hasMeaningfulText(childNode)) return true;
|
|
72
|
+
result = result || hasInteractiveDescendant(childNode);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Function to remove non-interactive elements recursively
|
|
79
|
+
function removeNonInteractive(node) {
|
|
80
|
+
if (node.nodeName !== '#document') {
|
|
81
|
+
const parent = node.parentNode;
|
|
82
|
+
const index = parent.childNodes.indexOf(node);
|
|
83
|
+
|
|
84
|
+
if (isFilteredOut(node)) {
|
|
85
|
+
parent.childNodes.splice(index, 1);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// keep texts for interactive elements
|
|
90
|
+
if ((isInteractive(parent) || hasMeaningfulText(parent)) && node.nodeName === '#text') {
|
|
91
|
+
node.value = node.value.trim().slice(0, 200);
|
|
92
|
+
if (!node.value) return false;
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
// if parent is interactive, we may need child element to match
|
|
98
|
+
!isInteractive(parent)
|
|
99
|
+
&& !isInteractive(node)
|
|
100
|
+
&& !hasInteractiveDescendant(node)
|
|
101
|
+
&& !hasMeaningfulText(node)) {
|
|
102
|
+
parent.childNodes.splice(index, 1);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (node.attrs) {
|
|
108
|
+
// Filter and keep allowed attributes, accessibility attributes
|
|
109
|
+
node.attrs = node.attrs.filter(attr => {
|
|
110
|
+
const { name, value } = attr;
|
|
111
|
+
if (name === 'class') {
|
|
112
|
+
// Remove classes containing digits
|
|
113
|
+
attr.value = value.split(' ')
|
|
114
|
+
// remove classes containing digits/
|
|
115
|
+
.filter(className => !/\d/.test(className))
|
|
116
|
+
// remove popular trash classes
|
|
117
|
+
.filter(className => !className.match(trashHtmlClasses))
|
|
118
|
+
// remove classes with : and __ in them
|
|
119
|
+
.filter(className => !className.match(/(:|__)/))
|
|
120
|
+
.join(' ');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return allowedAttrs.includes(name);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (node.childNodes) {
|
|
128
|
+
for (let i = node.childNodes.length - 1; i >= 0; i--) {
|
|
129
|
+
const childNode = node.childNodes[i];
|
|
130
|
+
removeNonInteractive(childNode);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Remove non-interactive elements starting from the root element
|
|
137
|
+
removeNonInteractive(document);
|
|
138
|
+
|
|
139
|
+
// Serialize the modified document tree back to HTML
|
|
140
|
+
const serializedHTML = serialize(document);
|
|
141
|
+
|
|
142
|
+
return serializedHTML;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function scanForErrorMessages(html, errorClasses = []) {
|
|
146
|
+
// Parse the HTML into a document tree
|
|
147
|
+
const document = parse(html);
|
|
148
|
+
|
|
149
|
+
// Array to store error messages
|
|
150
|
+
const errorMessages = [];
|
|
151
|
+
|
|
152
|
+
// Function to recursively scan for error classes and messages
|
|
153
|
+
function scanErrors(node) {
|
|
154
|
+
if (node.attrs) {
|
|
155
|
+
const classAttr = node.attrs.find(attr => attr.name === 'class');
|
|
156
|
+
if (classAttr && classAttr.value) {
|
|
157
|
+
const classNameChunks = classAttr.value.split(' ');
|
|
158
|
+
const errorClassFound = errorClasses.some(errorClass => classNameChunks.includes(errorClass));
|
|
159
|
+
if (errorClassFound && node.childNodes) {
|
|
160
|
+
const errorMessage = sanitizeTextContent(node);
|
|
161
|
+
errorMessages.push(errorMessage);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (node.childNodes) {
|
|
167
|
+
for (const childNode of node.childNodes) {
|
|
168
|
+
scanErrors(childNode);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Start scanning for error classes and messages from the root element
|
|
174
|
+
scanErrors(document);
|
|
175
|
+
|
|
176
|
+
return errorMessages;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function sanitizeTextContent(node) {
|
|
180
|
+
if (node.nodeName === '#text') {
|
|
181
|
+
return node.value.trim();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let sanitizedText = '';
|
|
185
|
+
|
|
186
|
+
if (node.childNodes) {
|
|
187
|
+
for (const childNode of node.childNodes) {
|
|
188
|
+
sanitizedText += sanitizeTextContent(childNode);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return sanitizedText;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function buildPath(node, path = '') {
|
|
196
|
+
const tag = node.nodeName;
|
|
197
|
+
let attributes = '';
|
|
198
|
+
|
|
199
|
+
if (node.attrs) {
|
|
200
|
+
attributes = node.attrs
|
|
201
|
+
.map(attr => `${attr.name}="${attr.value}"`)
|
|
202
|
+
.join(' ');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!tag.startsWith('#') && tag !== 'body' && tag !== 'html') {
|
|
206
|
+
path += `<${node.nodeName}${node.attrs ? ` ${attributes}` : ''}>`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!node.childNodes) return path;
|
|
210
|
+
|
|
211
|
+
const children = node.childNodes.filter(child => !child.nodeName.startsWith('#'));
|
|
212
|
+
|
|
213
|
+
if (children.length) {
|
|
214
|
+
return buildPath(children[children.length - 1], path);
|
|
215
|
+
}
|
|
216
|
+
return path;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function splitByChunks(text, chunkSize) {
|
|
220
|
+
chunkSize -= 20;
|
|
221
|
+
const chunks = [];
|
|
222
|
+
for (let i = 0; i < text.length; i += chunkSize) {
|
|
223
|
+
chunks.push(text.slice(i, i + chunkSize));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const regex = /<\s*\w+(?:\s+\w+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^>\s]+)))*\s*$/;
|
|
227
|
+
|
|
228
|
+
// append tag to chunk if it was split out
|
|
229
|
+
for (const index in chunks) {
|
|
230
|
+
const nextIndex = parseInt(index, 10) + 1;
|
|
231
|
+
if (!chunks[nextIndex]) break;
|
|
232
|
+
|
|
233
|
+
const currentChunk = chunks[index];
|
|
234
|
+
const nextChunk = chunks[nextIndex];
|
|
235
|
+
|
|
236
|
+
const lastTag = currentChunk.match(regex);
|
|
237
|
+
if (lastTag) {
|
|
238
|
+
chunks[nextIndex] = lastTag[0] + nextChunk;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const path = buildPath(parse(currentChunk));
|
|
242
|
+
if (path) {
|
|
243
|
+
chunks[nextIndex] = path + chunks[nextIndex];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (chunks[nextIndex].includes('<html')) continue;
|
|
247
|
+
chunks[nextIndex] = `<html><body>${chunks[nextIndex]}</body></html>`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return chunks.map(chunk => chunk.trim());
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = {
|
|
254
|
+
scanForErrorMessages,
|
|
255
|
+
removeNonInteractiveElements,
|
|
256
|
+
splitByChunks,
|
|
257
|
+
minifyHtml,
|
|
258
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const Gherkin = require('@cucumber/gherkin');
|
|
2
2
|
const Messages = require('@cucumber/messages');
|
|
3
3
|
const { Context, Suite, Test } = require('mocha');
|
|
4
|
+
const debug = require('debug')('codeceptjs:bdd');
|
|
4
5
|
|
|
5
6
|
const { matchStep } = require('./bdd');
|
|
6
7
|
const event = require('../event');
|
|
@@ -39,7 +40,9 @@ module.exports = (text, file) => {
|
|
|
39
40
|
for (const step of steps) {
|
|
40
41
|
const metaStep = new Step.MetaStep(null, step.text);
|
|
41
42
|
metaStep.actor = step.keyword.trim();
|
|
43
|
+
let helperStep;
|
|
42
44
|
const setMetaStep = (step) => {
|
|
45
|
+
helperStep = step;
|
|
43
46
|
if (step.metaStep) {
|
|
44
47
|
if (step.metaStep === metaStep) {
|
|
45
48
|
return;
|
|
@@ -67,11 +70,15 @@ module.exports = (text, file) => {
|
|
|
67
70
|
step.startTime = Date.now();
|
|
68
71
|
step.match = fn.line;
|
|
69
72
|
event.emit(event.bddStep.before, step);
|
|
73
|
+
event.emit(event.bddStep.started, metaStep);
|
|
70
74
|
event.dispatcher.prependListener(event.step.before, setMetaStep);
|
|
71
75
|
try {
|
|
76
|
+
debug(`Step '${step.text}' started...`);
|
|
72
77
|
await fn(...fn.params);
|
|
78
|
+
debug('Step passed');
|
|
73
79
|
step.status = 'passed';
|
|
74
80
|
} catch (err) {
|
|
81
|
+
debug(`Step failed: ${err?.message}`);
|
|
75
82
|
step.status = 'failed';
|
|
76
83
|
step.err = err;
|
|
77
84
|
throw err;
|
|
@@ -79,6 +86,7 @@ module.exports = (text, file) => {
|
|
|
79
86
|
step.endTime = Date.now();
|
|
80
87
|
event.dispatcher.removeListener(event.step.before, setMetaStep);
|
|
81
88
|
}
|
|
89
|
+
event.emit(event.bddStep.finished, metaStep);
|
|
82
90
|
event.emit(event.bddStep.after, step);
|
|
83
91
|
}
|
|
84
92
|
};
|
package/lib/listener/retry.js
CHANGED
|
@@ -44,6 +44,7 @@ module.exports = function () {
|
|
|
44
44
|
if (!retryConfig) return;
|
|
45
45
|
|
|
46
46
|
if (Number.isInteger(+retryConfig)) {
|
|
47
|
+
if (test.retries() === -1) test.retries(retryConfig);
|
|
47
48
|
return;
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -59,7 +60,7 @@ module.exports = function () {
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
if (config.Scenario) {
|
|
62
|
-
if (
|
|
63
|
+
if (test.retries() === -1) test.retries(config.Scenario);
|
|
63
64
|
output.log(`Retries: ${config.Scenario}`);
|
|
64
65
|
}
|
|
65
66
|
}
|