codeceptjs 3.4.1 → 3.5.1-2.beta.7
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/README.md +31 -30
- package/bin/codecept.js +1 -1
- package/lib/actor.js +6 -3
- package/lib/ai.js +180 -0
- package/lib/cli.js +13 -3
- package/lib/codecept.js +8 -0
- package/lib/colorUtils.js +10 -0
- package/lib/command/definitions.js +2 -7
- package/lib/command/dryRun.js +11 -2
- package/lib/command/generate.js +46 -3
- package/lib/command/info.js +24 -0
- package/lib/command/init.js +64 -6
- package/lib/command/interactive.js +15 -1
- package/lib/command/run-multiple/collection.js +17 -5
- package/lib/command/run-multiple.js +4 -2
- package/lib/command/run-workers.js +68 -5
- package/lib/command/run.js +7 -0
- package/lib/command/workers/runTests.js +39 -0
- package/lib/container.js +13 -3
- package/lib/data/context.js +14 -6
- package/lib/event.js +4 -0
- package/lib/helper/ApiDataFactory.js +2 -1
- package/lib/helper/Appium.js +116 -29
- package/lib/helper/Expect.js +422 -0
- package/lib/helper/FileSystem.js +1 -1
- package/lib/helper/GraphQL.js +25 -0
- package/lib/helper/JSONResponse.js +4 -4
- package/lib/helper/Nightmare.js +10 -5
- package/lib/helper/OpenAI.js +126 -0
- package/lib/helper/Playwright.js +1298 -229
- package/lib/helper/Protractor.js +12 -7
- package/lib/helper/Puppeteer.js +204 -64
- package/lib/helper/REST.js +15 -5
- package/lib/helper/TestCafe.js +45 -10
- package/lib/helper/WebDriver.js +252 -83
- package/lib/helper/errors/ElementNotFound.js +2 -1
- package/lib/helper/extras/PlaywrightReactVueLocator.js +38 -0
- 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/bdd.js +1 -1
- package/lib/interfaces/gherkin.js +37 -3
- package/lib/interfaces/scenarioConfig.js +1 -0
- package/lib/listener/retry.js +2 -1
- package/lib/locator.js +17 -4
- package/lib/mochaFactory.js +2 -1
- package/lib/output.js +1 -1
- package/lib/pause.js +78 -19
- package/lib/plugin/autoLogin.js +45 -10
- package/lib/plugin/debugErrors.js +67 -0
- package/lib/plugin/fakerTransform.js +4 -6
- package/lib/plugin/heal.js +209 -0
- package/lib/plugin/retryFailedStep.js +10 -1
- package/lib/plugin/retryTo.js +2 -4
- package/lib/plugin/screenshotOnFail.js +11 -2
- package/lib/plugin/selenoid.js +6 -1
- package/lib/plugin/standardActingHelpers.js +0 -2
- package/lib/plugin/stepByStepReport.js +2 -2
- package/lib/plugin/tryTo.js +5 -7
- package/lib/plugin/wdio.js +0 -1
- package/lib/recorder.js +22 -11
- package/lib/secret.js +5 -4
- package/lib/session.js +1 -1
- package/lib/step.js +36 -12
- package/lib/ui.js +5 -3
- package/lib/utils.js +22 -1
- package/lib/workers.js +83 -10
- package/package.json +117 -95
- package/translations/de-DE.js +5 -0
- package/translations/fr-FR.js +14 -1
- package/translations/it-IT.js +1 -0
- package/translations/ja-JP.js +14 -9
- package/translations/pl-PL.js +5 -0
- package/translations/pt-BR.js +1 -0
- package/translations/ru-RU.js +1 -0
- package/translations/zh-CN.js +5 -0
- package/translations/zh-TW.js +5 -0
- package/typings/index.d.ts +51 -15
- package/typings/promiseBasedTypes.d.ts +864 -802
- package/typings/types.d.ts +1339 -744
- package/CHANGELOG.md +0 -2427
- package/docs/advanced.md +0 -351
- package/docs/api.md +0 -323
- package/docs/basics.md +0 -980
- package/docs/bdd.md +0 -535
- package/docs/best.md +0 -237
- package/docs/books.md +0 -37
- package/docs/bootstrap.md +0 -135
- package/docs/build/ApiDataFactory.js +0 -409
- package/docs/build/Appium.js +0 -1938
- package/docs/build/FileSystem.js +0 -228
- package/docs/build/GraphQL.js +0 -204
- package/docs/build/GraphQLDataFactory.js +0 -309
- package/docs/build/JSONResponse.js +0 -338
- package/docs/build/Mochawesome.js +0 -71
- package/docs/build/Nightmare.js +0 -2145
- package/docs/build/Playwright.js +0 -3986
- package/docs/build/Polly.js +0 -42
- package/docs/build/Protractor.js +0 -2699
- package/docs/build/Puppeteer.js +0 -3710
- package/docs/build/REST.js +0 -334
- package/docs/build/SeleniumWebdriver.js +0 -76
- package/docs/build/TestCafe.js +0 -2057
- package/docs/build/WebDriver.js +0 -4017
- package/docs/changelog.md +0 -2436
- package/docs/commands.md +0 -254
- package/docs/community-helpers.md +0 -58
- package/docs/configuration.md +0 -157
- package/docs/continuous-integration.md +0 -22
- package/docs/custom-helpers.md +0 -306
- package/docs/data.md +0 -375
- package/docs/detox.md +0 -235
- package/docs/docker.md +0 -137
- package/docs/email.md +0 -183
- package/docs/examples.md +0 -149
- package/docs/helpers/ApiDataFactory.md +0 -266
- package/docs/helpers/Appium.md +0 -1312
- package/docs/helpers/Detox.md +0 -586
- package/docs/helpers/FileSystem.md +0 -152
- package/docs/helpers/GraphQL.md +0 -130
- package/docs/helpers/GraphQLDataFactory.md +0 -226
- package/docs/helpers/JSONResponse.md +0 -254
- package/docs/helpers/Mochawesome.md +0 -8
- package/docs/helpers/MockRequest.md +0 -377
- package/docs/helpers/Nightmare.md +0 -1256
- package/docs/helpers/Playwright.md +0 -2208
- package/docs/helpers/Polly.md +0 -44
- package/docs/helpers/Puppeteer-firefox.md +0 -86
- package/docs/helpers/Puppeteer.md +0 -2141
- package/docs/helpers/REST.md +0 -217
- package/docs/helpers/TestCafe.md +0 -1222
- package/docs/helpers/WebDriver.md +0 -2319
- package/docs/hooks.md +0 -340
- package/docs/index.md +0 -111
- package/docs/installation.md +0 -75
- package/docs/internal-api.md +0 -265
- package/docs/locators.md +0 -331
- package/docs/mobile-react-native-locators.md +0 -67
- package/docs/mobile.md +0 -297
- package/docs/nightmare.md +0 -223
- package/docs/pageobjects.md +0 -291
- package/docs/parallel.md +0 -232
- package/docs/playwright.md +0 -609
- package/docs/plugins.md +0 -1171
- package/docs/puppeteer.md +0 -316
- package/docs/quickstart.md +0 -163
- package/docs/react.md +0 -69
- package/docs/reports.md +0 -392
- package/docs/secrets.md +0 -30
- package/docs/shadow.md +0 -68
- package/docs/shared/keys.mustache +0 -31
- package/docs/shared/react.mustache +0 -1
- package/docs/testcafe.md +0 -174
- package/docs/translation.md +0 -247
- package/docs/tutorial.md +0 -271
- package/docs/typescript.md +0 -180
- package/docs/ui.md +0 -59
- package/docs/videos.md +0 -28
- package/docs/visual.md +0 -202
- package/docs/vue.md +0 -121
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -9
- package/docs/webapi/attachFile.mustache +0 -12
- package/docs/webapi/checkOption.mustache +0 -13
- package/docs/webapi/clearCookie.mustache +0 -10
- package/docs/webapi/clearField.mustache +0 -9
- package/docs/webapi/click.mustache +0 -25
- package/docs/webapi/clickLink.mustache +0 -8
- package/docs/webapi/closeCurrentTab.mustache +0 -7
- package/docs/webapi/closeOtherTabs.mustache +0 -8
- package/docs/webapi/dontSee.mustache +0 -11
- package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/dontSeeCookie.mustache +0 -8
- package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
- package/docs/webapi/dontSeeElement.mustache +0 -8
- package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
- package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
- package/docs/webapi/dontSeeInField.mustache +0 -11
- package/docs/webapi/dontSeeInSource.mustache +0 -8
- package/docs/webapi/dontSeeInTitle.mustache +0 -8
- package/docs/webapi/doubleClick.mustache +0 -13
- package/docs/webapi/downloadFile.mustache +0 -12
- package/docs/webapi/dragAndDrop.mustache +0 -9
- package/docs/webapi/dragSlider.mustache +0 -11
- package/docs/webapi/executeAsyncScript.mustache +0 -24
- package/docs/webapi/executeScript.mustache +0 -26
- package/docs/webapi/fillField.mustache +0 -16
- package/docs/webapi/forceClick.mustache +0 -28
- package/docs/webapi/forceRightClick.mustache +0 -18
- package/docs/webapi/grabAllWindowHandles.mustache +0 -7
- package/docs/webapi/grabAttributeFrom.mustache +0 -10
- package/docs/webapi/grabAttributeFromAll.mustache +0 -9
- package/docs/webapi/grabBrowserLogs.mustache +0 -9
- package/docs/webapi/grabCookie.mustache +0 -11
- package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
- package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
- package/docs/webapi/grabCurrentUrl.mustache +0 -9
- package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
- package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
- package/docs/webapi/grabElementBoundingRect.mustache +0 -20
- package/docs/webapi/grabGeoLocation.mustache +0 -8
- package/docs/webapi/grabHTMLFrom.mustache +0 -10
- package/docs/webapi/grabHTMLFromAll.mustache +0 -9
- package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
- package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
- package/docs/webapi/grabPageScrollPosition.mustache +0 -8
- package/docs/webapi/grabPopupText.mustache +0 -5
- package/docs/webapi/grabSource.mustache +0 -8
- package/docs/webapi/grabTextFrom.mustache +0 -10
- package/docs/webapi/grabTextFromAll.mustache +0 -9
- package/docs/webapi/grabTitle.mustache +0 -8
- package/docs/webapi/grabValueFrom.mustache +0 -9
- package/docs/webapi/grabValueFromAll.mustache +0 -8
- package/docs/webapi/moveCursorTo.mustache +0 -12
- package/docs/webapi/openNewTab.mustache +0 -7
- package/docs/webapi/pressKey.mustache +0 -12
- package/docs/webapi/pressKeyDown.mustache +0 -12
- package/docs/webapi/pressKeyUp.mustache +0 -12
- package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
- package/docs/webapi/refreshPage.mustache +0 -6
- package/docs/webapi/resizeWindow.mustache +0 -6
- package/docs/webapi/rightClick.mustache +0 -14
- package/docs/webapi/saveElementScreenshot.mustache +0 -10
- package/docs/webapi/saveScreenshot.mustache +0 -12
- package/docs/webapi/say.mustache +0 -10
- package/docs/webapi/scrollIntoView.mustache +0 -11
- package/docs/webapi/scrollPageToBottom.mustache +0 -6
- package/docs/webapi/scrollPageToTop.mustache +0 -6
- package/docs/webapi/scrollTo.mustache +0 -12
- package/docs/webapi/see.mustache +0 -11
- package/docs/webapi/seeAttributesOnElements.mustache +0 -9
- package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/seeCookie.mustache +0 -8
- package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
- package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
- package/docs/webapi/seeElement.mustache +0 -8
- package/docs/webapi/seeElementInDOM.mustache +0 -8
- package/docs/webapi/seeInCurrentUrl.mustache +0 -8
- package/docs/webapi/seeInField.mustache +0 -12
- package/docs/webapi/seeInPopup.mustache +0 -8
- package/docs/webapi/seeInSource.mustache +0 -7
- package/docs/webapi/seeInTitle.mustache +0 -8
- package/docs/webapi/seeNumberOfElements.mustache +0 -11
- package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/seeTextEquals.mustache +0 -9
- package/docs/webapi/seeTitleEquals.mustache +0 -8
- package/docs/webapi/selectOption.mustache +0 -21
- package/docs/webapi/setCookie.mustache +0 -16
- package/docs/webapi/setGeoLocation.mustache +0 -12
- package/docs/webapi/switchTo.mustache +0 -9
- package/docs/webapi/switchToNextTab.mustache +0 -10
- package/docs/webapi/switchToPreviousTab.mustache +0 -10
- package/docs/webapi/type.mustache +0 -18
- package/docs/webapi/uncheckOption.mustache +0 -13
- package/docs/webapi/wait.mustache +0 -8
- package/docs/webapi/waitForClickable.mustache +0 -11
- package/docs/webapi/waitForDetached.mustache +0 -10
- package/docs/webapi/waitForElement.mustache +0 -11
- package/docs/webapi/waitForEnabled.mustache +0 -6
- package/docs/webapi/waitForFunction.mustache +0 -17
- package/docs/webapi/waitForInvisible.mustache +0 -10
- package/docs/webapi/waitForText.mustache +0 -13
- package/docs/webapi/waitForValue.mustache +0 -10
- package/docs/webapi/waitForVisible.mustache +0 -10
- package/docs/webapi/waitInUrl.mustache +0 -9
- package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/waitToHide.mustache +0 -10
- package/docs/webapi/waitUrlEquals.mustache +0 -10
- package/docs/webdriver.md +0 -657
- package/docs/wiki/Books-&-Posts.md +0 -27
- package/docs/wiki/Community-Helpers-&-Plugins.md +0 -49
- package/docs/wiki/Converting-Playwright-to-Istanbul-Coverage.md +0 -29
- package/docs/wiki/Examples.md +0 -139
- package/docs/wiki/Google-Summer-of-Code-(GSoC)-2020.md +0 -68
- package/docs/wiki/Home.md +0 -16
- package/docs/wiki/Release-Process.md +0 -24
- package/docs/wiki/Roadmap.md +0 -23
- package/docs/wiki/Tests.md +0 -1393
- package/docs/wiki/Upgrading-to-CodeceptJS-3.md +0 -153
- package/docs/wiki/Videos.md +0 -19
|
@@ -5,7 +5,8 @@ const Locator = require('../../locator');
|
|
|
5
5
|
*/
|
|
6
6
|
class ElementNotFound {
|
|
7
7
|
constructor(
|
|
8
|
-
locator,
|
|
8
|
+
locator,
|
|
9
|
+
prefixMessage = 'Element',
|
|
9
10
|
postfixMessage = 'was not found by text|CSS|XPath',
|
|
10
11
|
) {
|
|
11
12
|
if (typeof locator === 'object') {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
async function findReact(matcher, locator) {
|
|
2
|
+
let _locator = `_react=${locator.react}`;
|
|
3
|
+
let props = '';
|
|
4
|
+
|
|
5
|
+
if (locator.props) {
|
|
6
|
+
props += propBuilder(locator.props);
|
|
7
|
+
_locator += props;
|
|
8
|
+
}
|
|
9
|
+
return matcher.locator(_locator).all();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function findVue(matcher, locator) {
|
|
13
|
+
let _locator = `_vue=${locator.vue}`;
|
|
14
|
+
let props = '';
|
|
15
|
+
|
|
16
|
+
if (locator.props) {
|
|
17
|
+
props += propBuilder(locator.props);
|
|
18
|
+
_locator += props;
|
|
19
|
+
}
|
|
20
|
+
return matcher.locator(_locator).all();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function propBuilder(props) {
|
|
24
|
+
let _props = '';
|
|
25
|
+
|
|
26
|
+
for (const [key, value] of Object.entries(props)) {
|
|
27
|
+
if (typeof value === 'object') {
|
|
28
|
+
for (const [k, v] of Object.entries(value)) {
|
|
29
|
+
_props += `[${key}.${k} = "${v}"]`;
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
_props += `[${key} = "${value}"]`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return _props;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { findReact, findVue };
|
|
@@ -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
|
+
// Puppeteer
|
|
11
|
+
context.evaluate(clientSideHighlightFn, element).catch(err => console.error(err));
|
|
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-terser');
|
|
3
|
+
|
|
4
|
+
async 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
|
+
});
|
|
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
|
+
};
|
package/lib/interfaces/bdd.js
CHANGED
|
@@ -30,7 +30,7 @@ const parameterTypeRegistry = new ParameterTypeRegistry();
|
|
|
30
30
|
const matchStep = (step) => {
|
|
31
31
|
for (const stepName in steps) {
|
|
32
32
|
if (stepName.indexOf('/') === 0) {
|
|
33
|
-
const regExpArr = stepName.match(
|
|
33
|
+
const regExpArr = stepName.match(/^\/(.*?)\/([gimy]*)$/) || [];
|
|
34
34
|
const res = step.match(new RegExp(regExpArr[1], regExpArr[2]));
|
|
35
35
|
if (res) {
|
|
36
36
|
const fn = steps[stepName];
|
|
@@ -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');
|
|
@@ -17,6 +18,11 @@ parser.stopAtFirstError = false;
|
|
|
17
18
|
|
|
18
19
|
module.exports = (text, file) => {
|
|
19
20
|
const ast = parser.parse(text);
|
|
21
|
+
let currentLanguage;
|
|
22
|
+
|
|
23
|
+
if (ast.feature) {
|
|
24
|
+
currentLanguage = getTranslation(ast.feature.language);
|
|
25
|
+
}
|
|
20
26
|
|
|
21
27
|
if (!ast.feature) {
|
|
22
28
|
throw new Error(`No 'Features' available in Gherkin '${file}' provided!`);
|
|
@@ -39,7 +45,9 @@ module.exports = (text, file) => {
|
|
|
39
45
|
for (const step of steps) {
|
|
40
46
|
const metaStep = new Step.MetaStep(null, step.text);
|
|
41
47
|
metaStep.actor = step.keyword.trim();
|
|
48
|
+
let helperStep;
|
|
42
49
|
const setMetaStep = (step) => {
|
|
50
|
+
helperStep = step;
|
|
43
51
|
if (step.metaStep) {
|
|
44
52
|
if (step.metaStep === metaStep) {
|
|
45
53
|
return;
|
|
@@ -67,11 +75,15 @@ module.exports = (text, file) => {
|
|
|
67
75
|
step.startTime = Date.now();
|
|
68
76
|
step.match = fn.line;
|
|
69
77
|
event.emit(event.bddStep.before, step);
|
|
78
|
+
event.emit(event.bddStep.started, metaStep);
|
|
70
79
|
event.dispatcher.prependListener(event.step.before, setMetaStep);
|
|
71
80
|
try {
|
|
81
|
+
debug(`Step '${step.text}' started...`);
|
|
72
82
|
await fn(...fn.params);
|
|
83
|
+
debug('Step passed');
|
|
73
84
|
step.status = 'passed';
|
|
74
85
|
} catch (err) {
|
|
86
|
+
debug(`Step failed: ${err?.message}`);
|
|
75
87
|
step.status = 'failed';
|
|
76
88
|
step.err = err;
|
|
77
89
|
throw err;
|
|
@@ -79,6 +91,7 @@ module.exports = (text, file) => {
|
|
|
79
91
|
step.endTime = Date.now();
|
|
80
92
|
event.dispatcher.removeListener(event.step.before, setMetaStep);
|
|
81
93
|
}
|
|
94
|
+
event.emit(event.bddStep.finished, metaStep);
|
|
82
95
|
event.emit(event.bddStep.after, step);
|
|
83
96
|
}
|
|
84
97
|
};
|
|
@@ -88,7 +101,7 @@ module.exports = (text, file) => {
|
|
|
88
101
|
suite.beforeEach('Before', scenario.injected(async () => runSteps(child.background.steps), suite, 'before'));
|
|
89
102
|
continue;
|
|
90
103
|
}
|
|
91
|
-
if (child.scenario && child.scenario.keyword === 'Scenario Outline') {
|
|
104
|
+
if (child.scenario && (currentLanguage ? child.scenario.keyword === currentLanguage.contexts.ScenarioOutline : child.scenario.keyword === 'Scenario Outline')) {
|
|
92
105
|
for (const examples of child.scenario.examples) {
|
|
93
106
|
const fields = examples.tableHeader.cells.map(c => c.value);
|
|
94
107
|
for (const example of examples.tableBody) {
|
|
@@ -106,7 +119,14 @@ module.exports = (text, file) => {
|
|
|
106
119
|
});
|
|
107
120
|
}
|
|
108
121
|
const tags = child.scenario.tags.map(t => t.name).concat(examples.tags.map(t => t.name));
|
|
109
|
-
|
|
122
|
+
let title = `${child.scenario.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim();
|
|
123
|
+
|
|
124
|
+
for (const [key, value] of Object.entries(current)) {
|
|
125
|
+
if (title.includes(`<${key}>`)) {
|
|
126
|
+
title = title.replace(JSON.stringify(current), '').replace(`<${key}>`, value);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
110
130
|
const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current)));
|
|
111
131
|
test.tags = suite.tags.concat(tags);
|
|
112
132
|
test.file = file;
|
|
@@ -133,7 +153,7 @@ function transformTable(table) {
|
|
|
133
153
|
let str = '';
|
|
134
154
|
for (const id in table.rows) {
|
|
135
155
|
const cells = table.rows[id].cells;
|
|
136
|
-
str += cells.map(c => c.value).map(c => c.
|
|
156
|
+
str += cells.map(c => c.value).map(c => c.padEnd(15)).join(' | ');
|
|
137
157
|
str += '\n';
|
|
138
158
|
}
|
|
139
159
|
return str;
|
|
@@ -154,3 +174,17 @@ function addExampleInTable(exampleSteps, placeholders) {
|
|
|
154
174
|
}
|
|
155
175
|
return steps;
|
|
156
176
|
}
|
|
177
|
+
|
|
178
|
+
function getTranslation(language) {
|
|
179
|
+
const translations = Object.keys(require('../../translations'));
|
|
180
|
+
|
|
181
|
+
for (const availableTranslation of translations) {
|
|
182
|
+
if (!language) {
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (availableTranslation.includes(language)) {
|
|
187
|
+
return require('../../translations')[availableTranslation];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
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
|
}
|
package/lib/locator.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const cssToXPath = require('
|
|
1
|
+
const cssToXPath = require('csstoxpath');
|
|
2
2
|
const { sprintf } = require('sprintf-js');
|
|
3
3
|
|
|
4
4
|
const { xpathLocator } = require('./utils');
|
|
@@ -158,11 +158,12 @@ class Locator {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
/**
|
|
161
|
+
* @param {string} [pseudoSelector] CSS to XPath extension pseudo: https://www.npmjs.com/package/csstoxpath?activeTab=explore#extension-pseudos
|
|
161
162
|
* @returns {string}
|
|
162
163
|
*/
|
|
163
|
-
toXPath() {
|
|
164
|
+
toXPath(pseudoSelector = '') {
|
|
164
165
|
if (this.isXPath()) return this.value;
|
|
165
|
-
if (this.isCSS()) return cssToXPath(this.value);
|
|
166
|
+
if (this.isCSS()) return cssToXPath(`${this.value}${pseudoSelector}`);
|
|
166
167
|
|
|
167
168
|
throw new Error('Can\'t be converted to XPath');
|
|
168
169
|
}
|
|
@@ -243,12 +244,24 @@ class Locator {
|
|
|
243
244
|
}
|
|
244
245
|
|
|
245
246
|
/**
|
|
247
|
+
* Find an element containing a text
|
|
246
248
|
* @param {string} text
|
|
247
249
|
* @returns {Locator}
|
|
248
250
|
*/
|
|
249
251
|
withText(text) {
|
|
250
252
|
text = xpathLocator.literal(text);
|
|
251
|
-
const xpath =
|
|
253
|
+
const xpath = this.toXPath(`:text-contains-case(${text})`);
|
|
254
|
+
return new Locator({ xpath });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Find an element with exact text
|
|
259
|
+
* @param {string} text
|
|
260
|
+
* @returns {Locator}
|
|
261
|
+
*/
|
|
262
|
+
withTextEquals(text) {
|
|
263
|
+
text = xpathLocator.literal(text);
|
|
264
|
+
const xpath = this.toXPath(`:text-case(${text})`);
|
|
252
265
|
return new Locator({ xpath });
|
|
253
266
|
}
|
|
254
267
|
|
package/lib/mochaFactory.js
CHANGED
|
@@ -97,7 +97,8 @@ class MochaFactory {
|
|
|
97
97
|
const attributes = Object.getOwnPropertyDescriptor(reporterOptions, 'codeceptjs-cli-reporter');
|
|
98
98
|
if (reporterOptions['codeceptjs-cli-reporter'] && attributes) {
|
|
99
99
|
Object.defineProperty(
|
|
100
|
-
reporterOptions,
|
|
100
|
+
reporterOptions,
|
|
101
|
+
'codeceptjs/lib/cli',
|
|
101
102
|
attributes,
|
|
102
103
|
);
|
|
103
104
|
delete reporterOptions['codeceptjs-cli-reporter'];
|
package/lib/output.js
CHANGED
|
@@ -106,7 +106,7 @@ module.exports = {
|
|
|
106
106
|
if (!step) return;
|
|
107
107
|
// Avoid to print non-gherkin steps, when gherkin is running for --steps mode
|
|
108
108
|
if (outputLevel === 1) {
|
|
109
|
-
if (step.hasBDDAncestor()) {
|
|
109
|
+
if (typeof step === 'object' && step.hasBDDAncestor()) {
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
112
|
}
|