codeceptjs 4.0.0-rc.9 → 4.0.0
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 +9 -10
- package/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +751 -172
- package/docs/advanced.md +201 -0
- package/docs/agents.md +181 -0
- package/docs/ai.md +489 -0
- package/docs/aitrace.md +266 -0
- package/docs/api.md +332 -0
- package/docs/architecture.md +235 -0
- package/docs/assertions.md +415 -0
- package/docs/auth.md +318 -0
- package/docs/basics.md +424 -0
- package/docs/bdd.md +539 -0
- package/docs/best.md +240 -0
- package/docs/bootstrap.md +132 -0
- package/docs/commands.md +352 -0
- package/docs/community-helpers.md +63 -0
- package/docs/configuration.md +185 -0
- package/docs/continuous-integration.md +431 -0
- package/docs/custom-helpers.md +297 -0
- package/docs/data.md +448 -0
- package/docs/debugging.md +332 -0
- package/docs/detox.md +235 -0
- package/docs/docker.md +107 -0
- package/docs/effects.md +179 -0
- package/docs/element-based-testing.md +295 -0
- package/docs/element-selection.md +125 -0
- package/docs/els.md +328 -0
- package/docs/environment-variables.md +131 -0
- package/docs/examples.md +160 -0
- package/docs/heal.md +213 -0
- package/docs/helpers/ApiDataFactory.md +267 -0
- package/docs/helpers/Appium.md +1419 -0
- package/docs/helpers/Detox.md +665 -0
- package/docs/helpers/ExpectHelper.md +275 -0
- package/docs/helpers/FileSystem.md +152 -0
- package/docs/helpers/GraphQL.md +152 -0
- package/docs/helpers/GraphQLDataFactory.md +226 -0
- package/docs/helpers/JSONResponse.md +255 -0
- package/docs/helpers/MockRequest.md +377 -0
- package/docs/helpers/Playwright.md +2970 -0
- package/docs/helpers/Puppeteer-firefox.md +86 -0
- package/docs/helpers/Puppeteer.md +2583 -0
- package/docs/helpers/REST.md +289 -0
- package/docs/helpers/WebDriver.md +2639 -0
- package/docs/hooks.md +148 -0
- package/docs/index.md +111 -0
- package/docs/installation.md +121 -0
- package/docs/internal-test-server.md +89 -0
- package/docs/locators.md +355 -0
- package/docs/mcp.md +485 -0
- package/docs/migrate-from-cypress.md +98 -0
- package/docs/migrate-from-java.md +108 -0
- package/docs/migrate-from-protractor.md +101 -0
- package/docs/migrate-from-testcafe.md +99 -0
- package/docs/migration-4.md +743 -0
- package/docs/mobile.md +338 -0
- package/docs/pageobjects.md +399 -0
- package/docs/parallel.md +187 -0
- package/docs/playwright.md +714 -0
- package/docs/plugins/aiTrace.md +49 -0
- package/docs/plugins/analyze.md +66 -0
- package/docs/plugins/auth.md +241 -0
- package/docs/plugins/autoDelay.md +48 -0
- package/docs/plugins/browser.md +41 -0
- package/docs/plugins/coverage.md +39 -0
- package/docs/plugins/customLocator.md +119 -0
- package/docs/plugins/customReporter.md +16 -0
- package/docs/plugins/expose.md +75 -0
- package/docs/plugins/heal.md +44 -0
- package/docs/plugins/junitReporter.md +51 -0
- package/docs/plugins/pageInfo.md +34 -0
- package/docs/plugins/pause.md +43 -0
- package/docs/plugins/pauseOnFail.md +18 -0
- package/docs/plugins/retryFailedStep.md +75 -0
- package/docs/plugins/screencast.md +55 -0
- package/docs/plugins/screenshot.md +58 -0
- package/docs/plugins/screenshotOnFail.md +18 -0
- package/docs/plugins/stepTimeout.md +65 -0
- package/docs/plugins.md +87 -0
- package/docs/puppeteer.md +314 -0
- package/docs/quickstart.md +120 -0
- package/docs/reports.md +198 -0
- package/docs/retry.md +311 -0
- package/docs/secrets.md +150 -0
- package/docs/sessions.md +80 -0
- package/docs/shadow.md +68 -0
- package/docs/store.md +94 -0
- package/docs/test-structure.md +275 -0
- package/docs/timeouts.md +183 -0
- package/docs/translation.md +247 -0
- package/docs/tutorial.md +323 -0
- package/docs/typescript.md +159 -0
- package/docs/web-element.md +251 -0
- package/docs/webdriver.md +641 -0
- package/docs/within.md +55 -0
- package/lib/actor.js +1 -36
- package/lib/ai.js +3 -2
- package/lib/aria.js +260 -0
- package/lib/assertions.js +18 -0
- package/lib/codecept.js +7 -7
- package/lib/command/check.js +2 -1
- package/lib/command/dryRun.js +24 -5
- package/lib/command/generate.js +2 -0
- package/lib/command/gherkin/snippets.js +5 -4
- package/lib/command/init.js +248 -266
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/command/run-multiple.js +3 -2
- package/lib/command/run-workers.js +1 -14
- package/lib/command/run.js +3 -17
- package/lib/command/utils.js +14 -0
- package/lib/command/workers/runTests.js +11 -15
- package/lib/config.js +77 -4
- package/lib/container.js +97 -15
- package/lib/effects.js +17 -0
- package/lib/element/WebElement.js +194 -2
- package/lib/els.js +12 -6
- package/lib/globals.js +32 -19
- package/lib/heal.js +7 -4
- package/lib/helper/ApiDataFactory.js +2 -1
- package/lib/helper/FileSystem.js +3 -2
- package/lib/helper/GraphQLDataFactory.js +2 -1
- package/lib/helper/Playwright.js +63 -70
- package/lib/helper/Puppeteer.js +20 -109
- package/lib/helper/WebDriver.js +13 -30
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/PlaywrightLocator.js +10 -0
- package/lib/helper/extras/elementSelection.js +10 -3
- package/lib/helper/extras/focusCheck.js +43 -0
- package/lib/helper/extras/richTextEditor.js +178 -0
- package/lib/history.js +3 -2
- package/lib/html.js +90 -16
- package/lib/index.js +9 -1
- package/lib/listener/config.js +6 -4
- package/lib/listener/emptyRun.js +2 -1
- package/lib/listener/helpers.js +4 -1
- package/lib/listener/mocha.js +2 -1
- package/lib/listener/pageobjects.js +43 -0
- package/lib/listener/result.js +3 -2
- package/lib/locator.js +126 -16
- package/lib/mocha/cli.js +4 -2
- package/lib/mocha/factory.js +7 -2
- package/lib/mocha/inject.js +1 -1
- package/lib/mocha/scenarioConfig.js +2 -1
- package/lib/mocha/ui.js +5 -6
- package/lib/parser.js +2 -2
- package/lib/pause.js +38 -4
- package/lib/plugin/aiTrace.js +96 -103
- package/lib/plugin/analyze.js +9 -9
- package/lib/plugin/auth.js +3 -3
- package/lib/plugin/browser.js +77 -0
- package/lib/plugin/expose.js +159 -0
- package/lib/plugin/heal.js +47 -3
- package/lib/plugin/junitReporter.js +303 -0
- package/lib/plugin/pageInfo.js +54 -52
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +11 -33
- package/lib/plugin/retryFailedStep.js +15 -13
- package/lib/plugin/screencast.js +289 -0
- package/lib/plugin/screenshot.js +558 -0
- package/lib/plugin/screenshotOnFail.js +9 -170
- package/lib/plugin/stepTimeout.js +3 -2
- package/lib/recorder.js +1 -1
- package/lib/rerun.js +2 -1
- package/lib/result.js +2 -1
- package/lib/step/base.js +10 -9
- package/lib/step/comment.js +2 -2
- package/lib/step/config.js +7 -0
- package/lib/step/helper.js +4 -4
- package/lib/step/meta.js +3 -3
- package/lib/step/record.js +5 -5
- package/lib/store.js +72 -3
- package/lib/translation.js +2 -1
- package/lib/utils/mask_data.js +2 -1
- package/lib/utils/pluginParser.js +151 -0
- package/lib/utils/trace.js +297 -0
- package/lib/utils.js +29 -3
- package/lib/workers.js +14 -22
- package/package.json +17 -14
- package/typings/index.d.ts +0 -5
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -16
- package/docs/webapi/attachFile.mustache +0 -24
- package/docs/webapi/blur.mustache +0 -18
- package/docs/webapi/checkOption.mustache +0 -13
- package/docs/webapi/clearCookie.mustache +0 -9
- package/docs/webapi/clearField.mustache +0 -14
- package/docs/webapi/click.mustache +0 -29
- 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/dontSeeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
- package/docs/webapi/dontSeeElement.mustache +0 -12
- package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
- package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
- package/docs/webapi/dontSeeInField.mustache +0 -16
- package/docs/webapi/dontSeeInSource.mustache +0 -8
- package/docs/webapi/dontSeeInTitle.mustache +0 -8
- package/docs/webapi/dontSeeTraffic.mustache +0 -13
- 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 -21
- package/docs/webapi/flushNetworkTraffics.mustache +0 -5
- package/docs/webapi/focus.mustache +0 -13
- 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/grabRecordedNetworkTraffics.mustache +0 -10
- 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/grabWebElement.mustache +0 -9
- package/docs/webapi/grabWebElements.mustache +0 -9
- package/docs/webapi/moveCursorTo.mustache +0 -16
- 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/seeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
- package/docs/webapi/seeElement.mustache +0 -12
- package/docs/webapi/seeElementInDOM.mustache +0 -8
- package/docs/webapi/seeInCurrentUrl.mustache +0 -8
- package/docs/webapi/seeInField.mustache +0 -17
- 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/seeTraffic.mustache +0 -36
- package/docs/webapi/selectOption.mustache +0 -26
- package/docs/webapi/setCookie.mustache +0 -16
- package/docs/webapi/setGeoLocation.mustache +0 -12
- package/docs/webapi/startRecordingTraffic.mustache +0 -8
- package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
- package/docs/webapi/stopRecordingTraffic.mustache +0 -5
- package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
- 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 -21
- package/docs/webapi/uncheckOption.mustache +0 -13
- package/docs/webapi/wait.mustache +0 -8
- package/docs/webapi/waitForClickable.mustache +0 -11
- package/docs/webapi/waitForCookie.mustache +0 -9
- package/docs/webapi/waitForDetached.mustache +0 -10
- package/docs/webapi/waitForDisabled.mustache +0 -6
- 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/waitForNumberOfTabs.mustache +0 -9
- 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/lib/helper/AI.js +0 -214
- package/lib/helper/Mochawesome.js +0 -96
- package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
- package/lib/helper/extras/React.js +0 -65
- package/lib/plugin/stepByStepReport.js +0 -431
- package/lib/plugin/subtitles.js +0 -89
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import store from '../../store.js'
|
|
2
|
+
import NonFocusedType from '../errors/NonFocusedType.js'
|
|
3
|
+
|
|
4
|
+
const MODIFIER_PATTERN = /^(control|ctrl|meta|cmd|command|commandorcontrol|ctrlorcommand)/i
|
|
5
|
+
const EDITING_KEYS = new Set(['a', 'c', 'x', 'v', 'z', 'y'])
|
|
6
|
+
|
|
7
|
+
async function isNoElementFocused(helper) {
|
|
8
|
+
return helper.executeScript(() => {
|
|
9
|
+
const ae = document.activeElement
|
|
10
|
+
return !ae || ae === document.documentElement || (ae === document.body && !ae.isContentEditable)
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function checkFocusBeforeType(helper) {
|
|
15
|
+
if (!helper.options.strict && !store.debugMode) return
|
|
16
|
+
if (!await isNoElementFocused(helper)) return
|
|
17
|
+
|
|
18
|
+
const message = 'No element is in focus. Use I.click() or I.focus() to activate an element before typing.'
|
|
19
|
+
if (helper.options.strict) throw new NonFocusedType(message)
|
|
20
|
+
helper.debugSection('Warning', message)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function checkFocusBeforePressKey(helper, originalKey) {
|
|
24
|
+
if (!helper.options.strict && !store.debugMode) return
|
|
25
|
+
if (!Array.isArray(originalKey)) return
|
|
26
|
+
|
|
27
|
+
let hasCtrlOrMeta = false
|
|
28
|
+
let actionKey = null
|
|
29
|
+
for (const k of originalKey) {
|
|
30
|
+
if (MODIFIER_PATTERN.test(k)) {
|
|
31
|
+
hasCtrlOrMeta = true
|
|
32
|
+
} else {
|
|
33
|
+
actionKey = k
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!hasCtrlOrMeta || !actionKey || !EDITING_KEYS.has(actionKey.toLowerCase())) return
|
|
37
|
+
|
|
38
|
+
if (!await isNoElementFocused(helper)) return
|
|
39
|
+
|
|
40
|
+
const message = `No element is in focus. Key combination with "${originalKey.join('+')}" may not work as expected. Use I.click() or I.focus() first.`
|
|
41
|
+
if (helper.options.strict) throw new NonFocusedType(message)
|
|
42
|
+
helper.debugSection('Warning', message)
|
|
43
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import WebElement from '../../element/WebElement.js'
|
|
2
|
+
|
|
3
|
+
const MARKER = 'data-codeceptjs-rte-target'
|
|
4
|
+
|
|
5
|
+
const EDITOR = {
|
|
6
|
+
STANDARD: 'standard',
|
|
7
|
+
IFRAME: 'iframe',
|
|
8
|
+
CONTENTEDITABLE: 'contenteditable',
|
|
9
|
+
HIDDEN_TEXTAREA: 'hidden-textarea',
|
|
10
|
+
UNREACHABLE: 'unreachable',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function detectAndMark(el, opts) {
|
|
14
|
+
const marker = opts.marker
|
|
15
|
+
const kinds = opts.kinds
|
|
16
|
+
const CE = '[contenteditable="true"], [contenteditable=""]'
|
|
17
|
+
|
|
18
|
+
function mark(kind, target) {
|
|
19
|
+
document.querySelectorAll('[' + marker + ']').forEach(n => n.removeAttribute(marker))
|
|
20
|
+
if (target && target.nodeType === 1) target.setAttribute(marker, '1')
|
|
21
|
+
return kind
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!el || el.nodeType !== 1) return mark(kinds.STANDARD, el)
|
|
25
|
+
|
|
26
|
+
const tag = el.tagName
|
|
27
|
+
if (tag === 'IFRAME') return mark(kinds.IFRAME, el)
|
|
28
|
+
if (el.isContentEditable) return mark(kinds.CONTENTEDITABLE, el)
|
|
29
|
+
|
|
30
|
+
const isFormHidden = tag === 'INPUT' && el.type === 'hidden'
|
|
31
|
+
if ((tag === 'INPUT' || tag === 'TEXTAREA') && !isFormHidden) {
|
|
32
|
+
const style = window.getComputedStyle(el)
|
|
33
|
+
if (style.display === 'none') return mark(kinds.UNREACHABLE, el)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const canSearchDescendants = tag !== 'INPUT' && tag !== 'TEXTAREA'
|
|
37
|
+
if (canSearchDescendants) {
|
|
38
|
+
const iframe = el.querySelector('iframe')
|
|
39
|
+
if (iframe) return mark(kinds.IFRAME, iframe)
|
|
40
|
+
const ce = el.querySelector(CE)
|
|
41
|
+
if (ce) return mark(kinds.CONTENTEDITABLE, ce)
|
|
42
|
+
const textareas = [...el.querySelectorAll('textarea')]
|
|
43
|
+
const focusable = textareas.find(t => window.getComputedStyle(t).display !== 'none')
|
|
44
|
+
const textarea = focusable || textareas[0]
|
|
45
|
+
if (textarea) return mark(kinds.HIDDEN_TEXTAREA, textarea)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return mark(kinds.STANDARD, el)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function detectInsideFrame() {
|
|
52
|
+
const MARKER = 'data-codeceptjs-rte-target'
|
|
53
|
+
const CE = '[contenteditable="true"], [contenteditable=""]'
|
|
54
|
+
const CONTENTEDITABLE = 'contenteditable'
|
|
55
|
+
const HIDDEN_TEXTAREA = 'hidden-textarea'
|
|
56
|
+
const body = document.body
|
|
57
|
+
document.querySelectorAll('[' + MARKER + ']').forEach(n => n.removeAttribute(MARKER))
|
|
58
|
+
|
|
59
|
+
if (body.isContentEditable) return CONTENTEDITABLE
|
|
60
|
+
|
|
61
|
+
const ce = body.querySelector(CE)
|
|
62
|
+
if (ce) {
|
|
63
|
+
ce.setAttribute(MARKER, '1')
|
|
64
|
+
return CONTENTEDITABLE
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const textareas = [...body.querySelectorAll('textarea')]
|
|
68
|
+
const focusable = textareas.find(t => window.getComputedStyle(t).display !== 'none')
|
|
69
|
+
const textarea = focusable || textareas[0]
|
|
70
|
+
if (textarea) {
|
|
71
|
+
textarea.setAttribute(MARKER, '1')
|
|
72
|
+
return HIDDEN_TEXTAREA
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return CONTENTEDITABLE
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function evaluateInFrame(helper, body, fn) {
|
|
79
|
+
if (body.helperType === 'webdriver') {
|
|
80
|
+
return helper.executeScript(fn)
|
|
81
|
+
}
|
|
82
|
+
return body.element.evaluate(fn)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function focusMarkedInFrameScript() {
|
|
86
|
+
const el = document.querySelector('[data-codeceptjs-rte-target]') || document.body
|
|
87
|
+
el.focus()
|
|
88
|
+
return document.activeElement === el
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function selectAllInFrameScript() {
|
|
92
|
+
const el = document.querySelector('[data-codeceptjs-rte-target]') || document.body
|
|
93
|
+
el.focus()
|
|
94
|
+
const range = document.createRange()
|
|
95
|
+
range.selectNodeContents(el)
|
|
96
|
+
const sel = window.getSelection()
|
|
97
|
+
sel.removeAllRanges()
|
|
98
|
+
sel.addRange(range)
|
|
99
|
+
return document.activeElement === el
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function selectAllInEditable(el) {
|
|
103
|
+
const doc = el.ownerDocument
|
|
104
|
+
const win = doc.defaultView
|
|
105
|
+
el.focus()
|
|
106
|
+
const range = doc.createRange()
|
|
107
|
+
range.selectNodeContents(el)
|
|
108
|
+
const sel = win.getSelection()
|
|
109
|
+
sel.removeAllRanges()
|
|
110
|
+
sel.addRange(range)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function unmarkAll(marker) {
|
|
114
|
+
document.querySelectorAll('[' + marker + ']').forEach(n => n.removeAttribute(marker))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isActive(el) {
|
|
118
|
+
return el.ownerDocument.activeElement === el
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function assertFocused(target) {
|
|
122
|
+
const focused = await target.evaluate(isActive)
|
|
123
|
+
if (!focused) {
|
|
124
|
+
throw new Error('fillField: rich editor target did not accept focus. Locator must point at the visible editor surface (a wrapper, iframe, or contenteditable) — not a hidden backing element.')
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function findMarked(helper) {
|
|
129
|
+
const root = helper.page || helper.browser
|
|
130
|
+
const raw = await root.$('[' + MARKER + ']')
|
|
131
|
+
return new WebElement(raw, helper)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function clearMarker(helper) {
|
|
135
|
+
if (helper.page) return helper.page.evaluate(unmarkAll, MARKER)
|
|
136
|
+
return helper.executeScript(unmarkAll, MARKER)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function fillRichEditor(helper, el, value) {
|
|
140
|
+
const source = el instanceof WebElement ? el : new WebElement(el, helper)
|
|
141
|
+
const kind = await source.evaluate(detectAndMark, { marker: MARKER, kinds: EDITOR })
|
|
142
|
+
if (kind === EDITOR.STANDARD) return false
|
|
143
|
+
if (kind === EDITOR.UNREACHABLE) {
|
|
144
|
+
throw new Error('fillField: cannot fill a display:none form control. Locator must point at the visible editor surface (a wrapper, iframe, or contenteditable).')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const target = await findMarked(helper)
|
|
148
|
+
const delay = helper.options.pressKeyDelay
|
|
149
|
+
|
|
150
|
+
if (kind === EDITOR.IFRAME) {
|
|
151
|
+
await target.inIframe(async body => {
|
|
152
|
+
const innerKind = await evaluateInFrame(helper, body, detectInsideFrame)
|
|
153
|
+
if (innerKind === EDITOR.HIDDEN_TEXTAREA) {
|
|
154
|
+
const focused = await evaluateInFrame(helper, body, focusMarkedInFrameScript)
|
|
155
|
+
if (!focused) throw new Error('fillField: rich editor target inside iframe did not accept focus.')
|
|
156
|
+
await body.selectAllAndDelete()
|
|
157
|
+
await body.typeText(value, { delay })
|
|
158
|
+
} else {
|
|
159
|
+
const focused = await evaluateInFrame(helper, body, selectAllInFrameScript)
|
|
160
|
+
if (!focused) throw new Error('fillField: rich editor target inside iframe did not accept focus.')
|
|
161
|
+
await body.typeText(value, { delay })
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
} else if (kind === EDITOR.HIDDEN_TEXTAREA) {
|
|
165
|
+
await target.focus()
|
|
166
|
+
await assertFocused(target)
|
|
167
|
+
await target.selectAllAndDelete()
|
|
168
|
+
await target.typeText(value, { delay })
|
|
169
|
+
} else if (kind === EDITOR.CONTENTEDITABLE) {
|
|
170
|
+
await target.click()
|
|
171
|
+
await target.evaluate(selectAllInEditable)
|
|
172
|
+
await assertFocused(target)
|
|
173
|
+
await target.typeText(value, { delay })
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
await clearMarker(helper)
|
|
177
|
+
return true
|
|
178
|
+
}
|
package/lib/history.js
CHANGED
|
@@ -2,6 +2,7 @@ import colors from 'chalk'
|
|
|
2
2
|
import fs from 'fs'
|
|
3
3
|
import path from 'path'
|
|
4
4
|
import output from './output.js'
|
|
5
|
+
import store from './store.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* REPL history records REPL commands and stores them in
|
|
@@ -9,8 +10,8 @@ import output from './output.js'
|
|
|
9
10
|
*/
|
|
10
11
|
class ReplHistory {
|
|
11
12
|
constructor() {
|
|
12
|
-
if (
|
|
13
|
-
this.historyFile = path.join(
|
|
13
|
+
if (store.outputDir) {
|
|
14
|
+
this.historyFile = path.join(store.outputDir, 'cli-history')
|
|
14
15
|
}
|
|
15
16
|
this.commands = []
|
|
16
17
|
}
|
package/lib/html.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { parse, serialize } from 'parse5'
|
|
2
2
|
import { minify } from 'html-minifier-terser'
|
|
3
|
+
import beautify from 'js-beautify'
|
|
4
|
+
|
|
5
|
+
const { html: html_beautify } = beautify
|
|
3
6
|
|
|
4
7
|
async function minifyHtml(html) {
|
|
5
8
|
return minify(html, {
|
|
@@ -14,6 +17,62 @@ async function minifyHtml(html) {
|
|
|
14
17
|
})
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
const TRASH_HTML_CLASSES = /^(text-|color-|flex-|float-|v-|ember-|d-|border-)/
|
|
21
|
+
|
|
22
|
+
function isTrashClass(className) {
|
|
23
|
+
if (!className) return true
|
|
24
|
+
if (/\d/.test(className)) return true
|
|
25
|
+
if (TRASH_HTML_CLASSES.test(className)) return true
|
|
26
|
+
if (/(:|__)/.test(className)) return true
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function filterClassValue(value) {
|
|
31
|
+
return (value || '')
|
|
32
|
+
.split(/\s+/)
|
|
33
|
+
.filter(c => c && !isTrashClass(c))
|
|
34
|
+
.join(' ')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DROP_TAGS = new Set(['style', 'noscript'])
|
|
38
|
+
const DROP_ATTRS = new Set(['style'])
|
|
39
|
+
|
|
40
|
+
function cleanHtml(html) {
|
|
41
|
+
const document = parse(html)
|
|
42
|
+
|
|
43
|
+
function walk(node) {
|
|
44
|
+
if (!node) return false
|
|
45
|
+
|
|
46
|
+
if (DROP_TAGS.has(node.nodeName) || (node.nodeName === 'script' && !(node.attrs || []).some(a => a.name === 'src'))) {
|
|
47
|
+
const parent = node.parentNode
|
|
48
|
+
const idx = parent.childNodes.indexOf(node)
|
|
49
|
+
if (idx >= 0) parent.childNodes.splice(idx, 1)
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (node.attrs) {
|
|
54
|
+
node.attrs = node.attrs.filter(attr => {
|
|
55
|
+
if (DROP_ATTRS.has(attr.name)) return false
|
|
56
|
+
if (attr.name === 'class') {
|
|
57
|
+
attr.value = filterClassValue(attr.value)
|
|
58
|
+
if (!attr.value) return false
|
|
59
|
+
}
|
|
60
|
+
return true
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (node.childNodes) {
|
|
65
|
+
for (let i = node.childNodes.length - 1; i >= 0; i--) {
|
|
66
|
+
walk(node.childNodes[i])
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
walk(document)
|
|
73
|
+
return serialize(document)
|
|
74
|
+
}
|
|
75
|
+
|
|
17
76
|
const defaultHtmlOpts = {
|
|
18
77
|
interactiveElements: ['a', 'input', 'button', 'select', 'textarea', 'option'],
|
|
19
78
|
textElements: ['label', 'h1', 'h2'],
|
|
@@ -28,7 +87,6 @@ function removeNonInteractiveElements(html, opts = {}) {
|
|
|
28
87
|
// Parse the HTML into a document tree
|
|
29
88
|
const document = parse(html)
|
|
30
89
|
|
|
31
|
-
const trashHtmlClasses = /^(text-|color-|flex-|float-|v-|ember-|d-|border-)/
|
|
32
90
|
// Array to store interactive elements
|
|
33
91
|
const removeElements = ['path', 'script']
|
|
34
92
|
|
|
@@ -103,21 +161,10 @@ function removeNonInteractiveElements(html, opts = {}) {
|
|
|
103
161
|
if (node.attrs) {
|
|
104
162
|
// Filter and keep allowed attributes, accessibility attributes
|
|
105
163
|
node.attrs = node.attrs.filter(attr => {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// Remove classes containing digits
|
|
109
|
-
attr.value = value
|
|
110
|
-
.split(' ')
|
|
111
|
-
// remove classes containing digits/
|
|
112
|
-
.filter(className => !/\d/.test(className))
|
|
113
|
-
// remove popular trash classes
|
|
114
|
-
.filter(className => !className.match(trashHtmlClasses))
|
|
115
|
-
// remove classes with : and __ in them
|
|
116
|
-
.filter(className => !className.match(/(:|__)/))
|
|
117
|
-
.join(' ')
|
|
164
|
+
if (attr.name === 'class') {
|
|
165
|
+
attr.value = filterClassValue(attr.value)
|
|
118
166
|
}
|
|
119
|
-
|
|
120
|
-
return allowedAttrs.includes(name)
|
|
167
|
+
return allowedAttrs.includes(attr.name)
|
|
121
168
|
})
|
|
122
169
|
}
|
|
123
170
|
|
|
@@ -258,4 +305,31 @@ function simplifyHtmlElement(html, maxLength = 300) {
|
|
|
258
305
|
return html
|
|
259
306
|
}
|
|
260
307
|
|
|
261
|
-
|
|
308
|
+
async function formatHtml(html) {
|
|
309
|
+
let processed = html
|
|
310
|
+
try {
|
|
311
|
+
processed = await minifyHtml(processed)
|
|
312
|
+
} catch (e) {
|
|
313
|
+
// keep raw html if minification fails
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
processed = cleanHtml(processed)
|
|
317
|
+
} catch (e) {
|
|
318
|
+
// keep minified html if cleaning fails
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
return html_beautify(processed, {
|
|
322
|
+
indent_size: 2,
|
|
323
|
+
wrap_line_length: 0,
|
|
324
|
+
preserve_newlines: false,
|
|
325
|
+
end_with_newline: false,
|
|
326
|
+
// Force every element onto its own line so line numbers in trace HTML
|
|
327
|
+
// map 1:1 to elements (consumed by codeceptq for AI/agent debugging).
|
|
328
|
+
inline: [],
|
|
329
|
+
})
|
|
330
|
+
} catch (e) {
|
|
331
|
+
return processed
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export { scanForErrorMessages, removeNonInteractiveElements, splitByChunks, minifyHtml, simplifyHtmlElement, formatHtml, cleanHtml, isTrashClass }
|
package/lib/index.js
CHANGED
|
@@ -23,6 +23,10 @@ import heal from './heal.js'
|
|
|
23
23
|
import ai from './ai.js'
|
|
24
24
|
import Workers from './workers.js'
|
|
25
25
|
import Secret, { secret } from './secret.js'
|
|
26
|
+
import session from './session.js'
|
|
27
|
+
|
|
28
|
+
const inject = (name) => container.support(name)
|
|
29
|
+
const locate = (query) => locator.build(query)
|
|
26
30
|
|
|
27
31
|
export default {
|
|
28
32
|
/** @type {typeof CodeceptJS.Codecept} */
|
|
@@ -67,7 +71,11 @@ export default {
|
|
|
67
71
|
Secret,
|
|
68
72
|
/** @type {typeof CodeceptJS.secret} */
|
|
69
73
|
secret,
|
|
74
|
+
|
|
75
|
+
session,
|
|
76
|
+
inject,
|
|
77
|
+
locate,
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
// Named exports for ESM compatibility
|
|
73
|
-
export { codecept, output, container, event, recorder, config, actor, helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret }
|
|
81
|
+
export { codecept, output, container, event, recorder, config, actor, helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret, session, inject, locate }
|
package/lib/listener/config.js
CHANGED
|
@@ -2,16 +2,18 @@ import event from '../event.js'
|
|
|
2
2
|
import recorder from '../recorder.js'
|
|
3
3
|
import { deepMerge, deepClone, ucfirst } from '../utils.js'
|
|
4
4
|
import output from '../output.js'
|
|
5
|
+
import container from '../container.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Enable Helpers to listen to test events
|
|
8
9
|
*/
|
|
10
|
+
let initialized = false
|
|
11
|
+
|
|
9
12
|
export default function () {
|
|
10
|
-
|
|
11
|
-
if (global.__codeceptConfigListenerInitialized) {
|
|
13
|
+
if (initialized) {
|
|
12
14
|
return
|
|
13
15
|
}
|
|
14
|
-
|
|
16
|
+
initialized = true
|
|
15
17
|
|
|
16
18
|
enableDynamicConfigFor('suite')
|
|
17
19
|
enableDynamicConfigFor('test')
|
|
@@ -20,7 +22,7 @@ export default function () {
|
|
|
20
22
|
event.dispatcher.on(event[type].before, (context = {}) => {
|
|
21
23
|
// Get helpers dynamically at runtime, not at initialization time
|
|
22
24
|
// This ensures we get the actual helper instances, not placeholders
|
|
23
|
-
const helpers =
|
|
25
|
+
const helpers = container.helpers()
|
|
24
26
|
|
|
25
27
|
function updateHelperConfig(helper, config) {
|
|
26
28
|
// Guard against undefined or invalid helpers
|
package/lib/listener/emptyRun.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import figures from 'figures'
|
|
2
2
|
import event from '../event.js'
|
|
3
3
|
import output from '../output.js'
|
|
4
|
+
import container from '../container.js'
|
|
4
5
|
import { searchWithFusejs } from '../utils.js'
|
|
5
6
|
|
|
6
7
|
export default function () {
|
|
@@ -12,7 +13,7 @@ export default function () {
|
|
|
12
13
|
|
|
13
14
|
event.dispatcher.on(event.all.result, () => {
|
|
14
15
|
if (isEmptyRun) {
|
|
15
|
-
const mocha =
|
|
16
|
+
const mocha = container.mocha()
|
|
16
17
|
|
|
17
18
|
if (mocha.options.grep) {
|
|
18
19
|
output.print()
|
package/lib/listener/helpers.js
CHANGED
|
@@ -3,11 +3,12 @@ import event from '../event.js'
|
|
|
3
3
|
import recorder from '../recorder.js'
|
|
4
4
|
import store from '../store.js'
|
|
5
5
|
import output from '../output.js'
|
|
6
|
+
import container from '../container.js'
|
|
6
7
|
/**
|
|
7
8
|
* Enable Helpers to listen to test events
|
|
8
9
|
*/
|
|
9
10
|
export default function () {
|
|
10
|
-
const helpers =
|
|
11
|
+
const helpers = container.helpers()
|
|
11
12
|
|
|
12
13
|
const runHelpersHook = (hook, param) => {
|
|
13
14
|
if (store.dryRun) return
|
|
@@ -29,11 +30,13 @@ export default function () {
|
|
|
29
30
|
event.dispatcher.on(event.suite.before, suite => {
|
|
30
31
|
// if (suite.parent) return; // only for root suite
|
|
31
32
|
runAsyncHelpersHook('_beforeSuite', suite, true)
|
|
33
|
+
recorder.catch()
|
|
32
34
|
})
|
|
33
35
|
|
|
34
36
|
event.dispatcher.on(event.suite.after, suite => {
|
|
35
37
|
// if (suite.parent) return; // only for root suite
|
|
36
38
|
runAsyncHelpersHook('_afterSuite', suite, true)
|
|
39
|
+
recorder.catch()
|
|
37
40
|
})
|
|
38
41
|
|
|
39
42
|
event.dispatcher.on(event.test.started, test => {
|
package/lib/listener/mocha.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import event from '../event.js'
|
|
2
|
+
import container from '../container.js'
|
|
2
3
|
|
|
3
4
|
export default function () {
|
|
4
5
|
let mocha
|
|
5
6
|
|
|
6
7
|
event.dispatcher.on(event.all.before, () => {
|
|
7
|
-
mocha =
|
|
8
|
+
mocha = container.mocha()
|
|
8
9
|
})
|
|
9
10
|
|
|
10
11
|
event.dispatcher.on(event.test.passed, test => {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import event from '../event.js'
|
|
2
|
+
import recorder from '../recorder.js'
|
|
3
|
+
import store from '../store.js'
|
|
4
|
+
import container from '../container.js'
|
|
5
|
+
import { resetBeforeCalledSet, getBeforeCalledSet } from '../container.js'
|
|
6
|
+
|
|
7
|
+
export default function () {
|
|
8
|
+
const runAsyncSupportHook = (hook, param, force) => {
|
|
9
|
+
if (store.dryRun) return
|
|
10
|
+
const support = container.supportObjects()
|
|
11
|
+
Object.keys(support).forEach(key => {
|
|
12
|
+
if (key === 'I') return
|
|
13
|
+
const obj = support[key]
|
|
14
|
+
if (!obj || typeof obj !== 'object' || !obj[hook]) return
|
|
15
|
+
recorder.add(`pageobject ${key}.${hook}()`, () => obj[hook](param), force, false)
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
event.dispatcher.on(event.test.started, () => {
|
|
20
|
+
resetBeforeCalledSet()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
event.dispatcher.on(event.test.after, () => {
|
|
24
|
+
if (store.dryRun) return
|
|
25
|
+
const support = container.supportObjects()
|
|
26
|
+
const called = getBeforeCalledSet()
|
|
27
|
+
called.forEach(name => {
|
|
28
|
+
const obj = support[name]
|
|
29
|
+
if (obj && obj._after) {
|
|
30
|
+
recorder.add(`pageobject ${name}._after()`, () => obj._after(), true, false)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
recorder.catchWithoutStop(() => {})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
event.dispatcher.on(event.suite.after, suite => {
|
|
37
|
+
runAsyncSupportHook('_afterSuite', suite, true)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
event.dispatcher.on(event.suite.before, suite => {
|
|
41
|
+
runAsyncSupportHook('_beforeSuite', suite, true)
|
|
42
|
+
})
|
|
43
|
+
}
|
package/lib/listener/result.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import event from '../event.js'
|
|
2
|
+
import container from '../container.js'
|
|
2
3
|
|
|
3
4
|
export default function () {
|
|
4
5
|
event.dispatcher.on(event.hook.failed, err => {
|
|
5
|
-
|
|
6
|
+
container.result().addStats({ failedHooks: 1 })
|
|
6
7
|
})
|
|
7
8
|
|
|
8
9
|
event.dispatcher.on(event.test.before, test => {
|
|
9
|
-
|
|
10
|
+
container.result().addTest(test)
|
|
10
11
|
})
|
|
11
12
|
}
|