codeceptjs 4.0.0-rc.2 → 4.0.0-rc.21
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 +39 -27
- package/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +1189 -0
- package/docs/advanced.md +201 -0
- package/docs/agents.md +181 -0
- package/docs/ai.md +537 -0
- package/docs/aitrace.md +266 -0
- package/docs/api.md +332 -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 +230 -0
- package/docs/continuous-integration.md +497 -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 +136 -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 +161 -0
- package/docs/heal.md +213 -0
- package/docs/helpers/ApiDataFactory.md +267 -0
- package/docs/helpers/Appium.md +1405 -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/Mochawesome.md +8 -0
- package/docs/helpers/MockRequest.md +377 -0
- package/docs/helpers/MockServer.md +212 -0
- package/docs/helpers/Playwright.md +2969 -0
- package/docs/helpers/Polly.md +44 -0
- package/docs/helpers/Protractor.md +1769 -0
- package/docs/helpers/Puppeteer-firefox.md +86 -0
- package/docs/helpers/Puppeteer.md +2690 -0
- package/docs/helpers/REST.md +289 -0
- package/docs/helpers/SoftExpectHelper.md +352 -0
- package/docs/helpers/WebDriver.md +2682 -0
- package/docs/hooks.md +339 -0
- package/docs/index.md +111 -0
- package/docs/installation.md +83 -0
- package/docs/internal-api.md +265 -0
- package/docs/internal-test-server.md +89 -0
- package/docs/locators.md +355 -0
- package/docs/mcp.md +485 -0
- package/docs/migration-4.md +556 -0
- package/docs/mobile.md +338 -0
- package/docs/pageobjects.md +399 -0
- package/docs/parallel.md +585 -0
- package/docs/playwright.md +714 -0
- package/docs/plugins.md +866 -0
- package/docs/puppeteer.md +314 -0
- package/docs/quickstart.md +120 -0
- package/docs/react.md +70 -0
- package/docs/reports.md +483 -0
- package/docs/retry.md +274 -0
- package/docs/secrets.md +150 -0
- package/docs/sessions.md +80 -0
- package/docs/shadow.md +68 -0
- package/docs/test-structure.md +275 -0
- package/docs/timeouts.md +183 -0
- package/docs/translation.md +247 -0
- package/docs/tutorial.md +271 -0
- package/docs/typescript.md +374 -0
- package/docs/web-element.md +251 -0
- package/docs/webdriver.md +708 -0
- package/docs/within.md +55 -0
- package/lib/ai.js +3 -2
- package/lib/aria.js +260 -0
- package/lib/assertions.js +18 -0
- package/lib/codecept.js +27 -24
- 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 -269
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/command/run-multiple.js +2 -0
- package/lib/command/run-workers.js +2 -14
- package/lib/command/run.js +3 -17
- package/lib/command/utils.js +14 -0
- package/lib/command/workers/runTests.js +10 -10
- package/lib/config.js +77 -4
- package/lib/container.js +114 -17
- package/lib/effects.js +17 -0
- package/lib/element/WebElement.js +246 -2
- package/lib/els.js +12 -6
- package/lib/globals.js +32 -19
- package/lib/heal.js +6 -3
- package/lib/helper/ApiDataFactory.js +2 -1
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/FileSystem.js +3 -2
- package/lib/helper/GraphQLDataFactory.js +2 -1
- package/lib/helper/Playwright.js +233 -162
- package/lib/helper/Puppeteer.js +208 -76
- package/lib/helper/WebDriver.js +173 -68
- package/lib/helper/errors/MultipleElementsFound.js +27 -110
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/extras/focusCheck.js +43 -0
- package/lib/helper/extras/richTextEditor.js +178 -0
- package/lib/helper/scripts/dropFile.js +11 -0
- package/lib/history.js +3 -2
- package/lib/html.js +103 -16
- package/lib/index.js +9 -1
- package/lib/listener/config.js +6 -4
- package/lib/listener/emptyRun.js +2 -1
- package/lib/listener/globalRetry.js +32 -6
- 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 -3
- package/lib/mocha/cli.js +14 -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 +456 -0
- package/lib/plugin/analyze.js +6 -5
- 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/pageInfo.js +54 -52
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +10 -34
- package/lib/plugin/retryFailedStep.js +32 -22
- package/lib/plugin/screencast.js +289 -0
- package/lib/plugin/screenshot.js +563 -0
- package/lib/plugin/screenshotOnFail.js +8 -171
- package/lib/rerun.js +2 -1
- package/lib/result.js +2 -1
- package/lib/step/base.js +3 -2
- package/lib/step/config.js +15 -2
- package/lib/step/record.js +2 -2
- 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 +77 -3
- package/lib/workers.js +63 -25
- package/package.json +19 -13
- package/typings/index.d.ts +19 -5
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -11
- package/docs/webapi/attachFile.mustache +0 -12
- 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 -9
- 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 -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/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 -16
- 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 -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/seeCurrentPathEquals.mustache +0 -10
- 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/seeTraffic.mustache +0 -36
- package/docs/webapi/selectOption.mustache +0 -21
- 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/listener/enhancedGlobalRetry.js +0 -110
- package/lib/plugin/enhancedRetryFailedStep.js +0 -99
- package/lib/plugin/htmlReporter.js +0 -3648
- package/lib/plugin/stepByStepReport.js +0 -427
- package/lib/plugin/subtitles.js +0 -89
- package/lib/retryCoordinator.js +0 -207
- package/typings/promiseBasedTypes.d.ts +0 -9469
- package/typings/types.d.ts +0 -11402
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { pathToFileURL } from 'url'
|
|
5
|
+
import Container from '../container.js'
|
|
6
|
+
import { clearString } from '../utils.js'
|
|
7
|
+
import { formatHtml } from '../html.js'
|
|
8
|
+
import { diffAriaSnapshots } from '../aria.js'
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Helper / directory naming
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export function pickActingHelper(helpers) {
|
|
15
|
+
for (const name of Container.STANDARD_ACTING_HELPERS) {
|
|
16
|
+
if (helpers[name]) return helpers[name]
|
|
17
|
+
}
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function traceDirFor(testFile, testTitle, baseDir) {
|
|
22
|
+
const hash = crypto.createHash('sha256').update((testFile || '') + (testTitle || '')).digest('hex').slice(0, 8)
|
|
23
|
+
const cleanTitle = clearString(testTitle || '').slice(0, 200)
|
|
24
|
+
return path.resolve(baseDir, `trace_${cleanTitle}_${hash}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function snapshotDirFor(baseDir) {
|
|
28
|
+
const hash = crypto.randomBytes(4).toString('hex')
|
|
29
|
+
return path.resolve(baseDir, `snapshot_${Date.now()}_${hash}`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Artifact link rendering (trace.md)
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const ARTIFACT_LABELS = {
|
|
37
|
+
html: 'HTML',
|
|
38
|
+
aria: 'ARIA',
|
|
39
|
+
screenshot: 'Screenshot',
|
|
40
|
+
console: 'Browser Logs',
|
|
41
|
+
storage: 'Storage',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function artifactLinks(artifacts, { indent = ' ', consoleCount } = {}) {
|
|
45
|
+
const lines = []
|
|
46
|
+
const order = ['html', 'aria', 'screenshot', 'console', 'storage']
|
|
47
|
+
|
|
48
|
+
for (const key of order) {
|
|
49
|
+
const file = artifacts[key]
|
|
50
|
+
if (!file) continue
|
|
51
|
+
const label = ARTIFACT_LABELS[key]
|
|
52
|
+
let line = `${indent}> [${label}](./${file})`
|
|
53
|
+
if (key === 'console') {
|
|
54
|
+
const count = consoleCount ?? artifacts.consoleCount ?? 0
|
|
55
|
+
line += ` (${count} entries)`
|
|
56
|
+
} else if (key === 'storage') {
|
|
57
|
+
const cookies = artifacts.cookieCount ?? 0
|
|
58
|
+
const ls = artifacts.localStorageCount ?? 0
|
|
59
|
+
line += ` (${cookies} cookies, ${ls} localStorage)`
|
|
60
|
+
}
|
|
61
|
+
lines.push(line)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return lines.join('\n')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function fileToUrl(dir, basename) {
|
|
68
|
+
return pathToFileURL(path.join(dir, basename)).href
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function writeTraceMarkdown({ dir, title, file, durationMs, commands, captured, error }) {
|
|
72
|
+
let md = `file: ${file || 'mcp'}\n`
|
|
73
|
+
md += `name: ${title}\n`
|
|
74
|
+
md += `time: ${(durationMs / 1000).toFixed(2)}s\n`
|
|
75
|
+
md += `---\n\n`
|
|
76
|
+
|
|
77
|
+
if (error) md += `Error: ${error}\n\n---\n\n`
|
|
78
|
+
|
|
79
|
+
if (commands && commands.length) {
|
|
80
|
+
md += `### Commands\n`
|
|
81
|
+
for (const c of commands) md += `- ${c}\n`
|
|
82
|
+
md += `\n`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
md += `### Final State\n`
|
|
86
|
+
if (captured.url) md += ` > URL: ${captured.url}\n`
|
|
87
|
+
const links = artifactLinks(captured)
|
|
88
|
+
if (links) md += links + '\n'
|
|
89
|
+
|
|
90
|
+
const traceFile = path.join(dir, 'trace.md')
|
|
91
|
+
fs.writeFileSync(traceFile, md)
|
|
92
|
+
return traceFile
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function artifactsToFileUrls(captured, dir) {
|
|
96
|
+
const out = {}
|
|
97
|
+
if (captured.url) out.url = captured.url
|
|
98
|
+
if (captured.screenshot) out.screenshot = fileToUrl(dir, captured.screenshot)
|
|
99
|
+
if (captured.html) out.html = fileToUrl(dir, captured.html)
|
|
100
|
+
if (captured.aria) out.aria = fileToUrl(dir, captured.aria)
|
|
101
|
+
if (captured.console) out.console = fileToUrl(dir, captured.console)
|
|
102
|
+
if (captured.storage) out.storage = fileToUrl(dir, captured.storage)
|
|
103
|
+
if (typeof captured.consoleCount === 'number') out.consoleCount = captured.consoleCount
|
|
104
|
+
if (typeof captured.cookieCount === 'number') out.cookieCount = captured.cookieCount
|
|
105
|
+
if (typeof captured.localStorageCount === 'number') out.localStorageCount = captured.localStorageCount
|
|
106
|
+
return out
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Snapshot capture (HTML / ARIA / screenshot / console / storage)
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
function normalizeBrowserLogs(logs) {
|
|
114
|
+
return (logs || []).map(l => {
|
|
115
|
+
if (typeof l === 'string') return l
|
|
116
|
+
if (l && typeof l.type === 'function' && typeof l.text === 'function') {
|
|
117
|
+
return { type: l.type(), text: l.text() }
|
|
118
|
+
}
|
|
119
|
+
return l
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function captureStorageState(helper) {
|
|
124
|
+
if (typeof helper.grabStorageState === 'function') {
|
|
125
|
+
try {
|
|
126
|
+
const state = await helper.grabStorageState()
|
|
127
|
+
if (state) return state
|
|
128
|
+
} catch {}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const state = { cookies: [], origins: [] }
|
|
132
|
+
|
|
133
|
+
if (typeof helper.grabCookie === 'function') {
|
|
134
|
+
try {
|
|
135
|
+
const cookies = await helper.grabCookie()
|
|
136
|
+
if (Array.isArray(cookies)) state.cookies = cookies
|
|
137
|
+
} catch {}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (typeof helper.executeScript === 'function') {
|
|
141
|
+
try {
|
|
142
|
+
const result = await helper.executeScript(() => {
|
|
143
|
+
const out = { origin: location.origin, items: [] }
|
|
144
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
145
|
+
const name = localStorage.key(i)
|
|
146
|
+
out.items.push({ name, value: localStorage.getItem(name) })
|
|
147
|
+
}
|
|
148
|
+
return out
|
|
149
|
+
})
|
|
150
|
+
if (result?.items?.length) {
|
|
151
|
+
state.origins.push({ origin: result.origin, localStorage: result.items })
|
|
152
|
+
}
|
|
153
|
+
} catch {}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return state
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function captureSnapshot(helper, {
|
|
160
|
+
dir,
|
|
161
|
+
prefix = 'snapshot',
|
|
162
|
+
fullPage = false,
|
|
163
|
+
captureURL = true,
|
|
164
|
+
captureScreenshot = true,
|
|
165
|
+
captureHTML = true,
|
|
166
|
+
captureARIA = true,
|
|
167
|
+
captureBrowserLogs = true,
|
|
168
|
+
captureStorage = true,
|
|
169
|
+
} = {}) {
|
|
170
|
+
if (!helper) return {}
|
|
171
|
+
const out = {}
|
|
172
|
+
|
|
173
|
+
if (captureURL) {
|
|
174
|
+
try {
|
|
175
|
+
if (helper.grabCurrentUrl) out.url = await helper.grabCurrentUrl()
|
|
176
|
+
} catch {}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (captureScreenshot && helper.saveScreenshot) {
|
|
180
|
+
try {
|
|
181
|
+
const file = `${prefix}_screenshot.png`
|
|
182
|
+
await helper.saveScreenshot(path.join(dir, file), fullPage)
|
|
183
|
+
out.screenshot = file
|
|
184
|
+
} catch {}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (captureHTML && helper.grabSource) {
|
|
188
|
+
try {
|
|
189
|
+
const html = await helper.grabSource()
|
|
190
|
+
// Universal funnel: every captured HTML snapshot flows through formatHtml
|
|
191
|
+
// (minify -> cleanHtml -> beautify). Don't add direct grabSource->writeFile
|
|
192
|
+
// paths elsewhere; route through this util so trash-class cleanup stays
|
|
193
|
+
// consistent across aiTrace, pageInfo, and MCP tools.
|
|
194
|
+
const formatted = await formatHtml(html)
|
|
195
|
+
const file = `${prefix}_page.html`
|
|
196
|
+
fs.writeFileSync(path.join(dir, file), formatted)
|
|
197
|
+
out.html = file
|
|
198
|
+
// Expose pre-cleanup HTML for consumers that need to inspect classes
|
|
199
|
+
// stripped by cleanHtml (e.g. pageInfo's error-class scan).
|
|
200
|
+
out.htmlRaw = html
|
|
201
|
+
} catch {}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (captureARIA && helper.grabAriaSnapshot) {
|
|
205
|
+
try {
|
|
206
|
+
const aria = await helper.grabAriaSnapshot()
|
|
207
|
+
const file = `${prefix}_aria.txt`
|
|
208
|
+
fs.writeFileSync(path.join(dir, file), aria)
|
|
209
|
+
out.aria = file
|
|
210
|
+
} catch {}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (captureBrowserLogs && helper.grabBrowserLogs) {
|
|
214
|
+
try {
|
|
215
|
+
const logs = await helper.grabBrowserLogs()
|
|
216
|
+
const normalized = normalizeBrowserLogs(logs)
|
|
217
|
+
const file = `${prefix}_console.json`
|
|
218
|
+
fs.writeFileSync(path.join(dir, file), JSON.stringify(normalized, null, 2))
|
|
219
|
+
out.console = file
|
|
220
|
+
out.consoleCount = normalized.length
|
|
221
|
+
} catch {}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (captureStorage) {
|
|
225
|
+
try {
|
|
226
|
+
const state = await captureStorageState(helper)
|
|
227
|
+
const cookieCount = state.cookies?.length || 0
|
|
228
|
+
const localStorageCount = (state.origins || [])
|
|
229
|
+
.reduce((sum, o) => sum + (o.localStorage?.length || 0), 0)
|
|
230
|
+
if (cookieCount || localStorageCount) {
|
|
231
|
+
const file = `${prefix}_storage.json`
|
|
232
|
+
fs.writeFileSync(path.join(dir, file), JSON.stringify(state, null, 2))
|
|
233
|
+
out.storage = file
|
|
234
|
+
out.cookieCount = cookieCount
|
|
235
|
+
out.localStorageCount = localStorageCount
|
|
236
|
+
}
|
|
237
|
+
} catch {}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return out
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// TraceReader — read artifacts already on disk (written by aiTrace, MCP, etc.)
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
const KIND_SUFFIX = {
|
|
248
|
+
aria: '_aria.txt',
|
|
249
|
+
html: '_page.html',
|
|
250
|
+
screenshot: '_screenshot.png',
|
|
251
|
+
console: '_console.json',
|
|
252
|
+
storage: '_storage.json',
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export class TraceReader {
|
|
256
|
+
constructor(dir) {
|
|
257
|
+
this.dir = dir
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Filenames of a given kind, sorted in capture order. aiTrace prefixes with
|
|
261
|
+
// a zero-padded step index (`0000_`, `0001_`...), so a lexical sort is
|
|
262
|
+
// chronological.
|
|
263
|
+
list(kind) {
|
|
264
|
+
const suffix = KIND_SUFFIX[kind]
|
|
265
|
+
if (!suffix || !this.dir || !fs.existsSync(this.dir)) return []
|
|
266
|
+
let entries
|
|
267
|
+
try { entries = fs.readdirSync(this.dir) } catch { return [] }
|
|
268
|
+
return entries.filter(f => f.endsWith(suffix)).sort()
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Path of the n-th file of `kind`, or null. Python-style indexing:
|
|
272
|
+
// 0..N-1 from the start, -1..-N from the end.
|
|
273
|
+
pathAt(n, kind) {
|
|
274
|
+
const files = this.list(kind)
|
|
275
|
+
if (!files.length) return null
|
|
276
|
+
const i = n < 0 ? files.length + n : n
|
|
277
|
+
if (i < 0 || i >= files.length) return null
|
|
278
|
+
return path.join(this.dir, files[i])
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Read content of the n-th file of `kind`. Binary kinds (screenshot) are
|
|
282
|
+
// returned as Buffer; text kinds as utf8 string.
|
|
283
|
+
nth(n, kind) {
|
|
284
|
+
const p = this.pathAt(n, kind)
|
|
285
|
+
if (!p) return null
|
|
286
|
+
try {
|
|
287
|
+
if (kind === 'screenshot') return fs.readFileSync(p)
|
|
288
|
+
return fs.readFileSync(p, 'utf8')
|
|
289
|
+
} catch { return null }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
first(kind) { return this.nth(0, kind) }
|
|
293
|
+
last(kind) { return this.nth(-1, kind) }
|
|
294
|
+
count(kind) { return this.list(kind).length }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export const ariaDiff = diffAriaSnapshots
|
package/lib/utils.js
CHANGED
|
@@ -7,6 +7,7 @@ import getFunctionArguments from 'fn-args'
|
|
|
7
7
|
import deepClone from 'lodash.clonedeep'
|
|
8
8
|
import merge from 'lodash.merge'
|
|
9
9
|
import { convertColorToRGBA, isColorProperty } from './colorUtils.js'
|
|
10
|
+
import store from './store.js'
|
|
10
11
|
import Fuse from 'fuse.js'
|
|
11
12
|
import crypto from 'crypto'
|
|
12
13
|
import jsBeautify from 'js-beautify'
|
|
@@ -150,6 +151,24 @@ export const decodeUrl = function (url) {
|
|
|
150
151
|
return decodeURIComponent(decodeURIComponent(decodeURIComponent(url)))
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
export const normalizePath = function (path) {
|
|
155
|
+
if (path === '' || path === '/') return '/'
|
|
156
|
+
return path
|
|
157
|
+
.replace(/\/+/g, '/')
|
|
158
|
+
.replace(/\/$/, '') || '/'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const resolveUrl = function (url, baseUrl) {
|
|
162
|
+
if (!url) return url
|
|
163
|
+
if (url.indexOf('http') === 0) return url
|
|
164
|
+
if (!baseUrl) return url
|
|
165
|
+
try {
|
|
166
|
+
return new URL(url, baseUrl).href
|
|
167
|
+
} catch (e) {
|
|
168
|
+
return url
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
153
172
|
export const xpathLocator = {
|
|
154
173
|
/**
|
|
155
174
|
* @param {string} string
|
|
@@ -317,13 +336,13 @@ export const screenshotOutputFolder = function (fileName) {
|
|
|
317
336
|
const fileSep = path.sep
|
|
318
337
|
|
|
319
338
|
if (!fileName.includes(fileSep) || fileName.includes('record_')) {
|
|
320
|
-
return path.resolve(
|
|
339
|
+
return path.resolve(store.outputDir, fileName)
|
|
321
340
|
}
|
|
322
|
-
return path.resolve(
|
|
341
|
+
return path.resolve(store.codeceptDir, fileName)
|
|
323
342
|
}
|
|
324
343
|
|
|
325
344
|
export const relativeDir = function (fileName) {
|
|
326
|
-
return fileName.replace(
|
|
345
|
+
return fileName.replace(store.codeceptDir, '').replace(/^\//, '')
|
|
327
346
|
}
|
|
328
347
|
|
|
329
348
|
export const beautify = function (code) {
|
|
@@ -598,6 +617,12 @@ function createCircularSafeReplacer(keysToSkip = []) {
|
|
|
598
617
|
return undefined
|
|
599
618
|
}
|
|
600
619
|
|
|
620
|
+
// Coerce types that JSON.stringify can't handle natively
|
|
621
|
+
if (typeof value === 'function') return `[Function: ${value.name || 'anonymous'}]`
|
|
622
|
+
if (typeof value === 'bigint') return `${value.toString()}n`
|
|
623
|
+
if (typeof value === 'symbol') return value.toString()
|
|
624
|
+
if (value instanceof Error) return { name: value.name, message: value.message, stack: value.stack }
|
|
625
|
+
|
|
601
626
|
if (value === null || typeof value !== 'object') {
|
|
602
627
|
return value
|
|
603
628
|
}
|
|
@@ -628,6 +653,25 @@ export const safeStringify = function (obj, keysToSkip = [], space = 0) {
|
|
|
628
653
|
}
|
|
629
654
|
}
|
|
630
655
|
|
|
656
|
+
/**
|
|
657
|
+
* Truncate a string at a byte cap, returning structured info.
|
|
658
|
+
* @param {string} str
|
|
659
|
+
* @param {number} maxBytes
|
|
660
|
+
* @returns {{ value: string, truncated: boolean, fullLength: number }}
|
|
661
|
+
*/
|
|
662
|
+
export const truncateString = function (str, maxBytes) {
|
|
663
|
+
if (typeof str !== 'string') str = String(str)
|
|
664
|
+
if (str.length <= maxBytes) {
|
|
665
|
+
return { value: str, truncated: false, fullLength: str.length }
|
|
666
|
+
}
|
|
667
|
+
const dropped = str.length - maxBytes
|
|
668
|
+
return {
|
|
669
|
+
value: `${str.slice(0, maxBytes)}\n...[truncated ${dropped} more chars]`,
|
|
670
|
+
truncated: true,
|
|
671
|
+
fullLength: str.length,
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
631
675
|
export const serializeError = function (error) {
|
|
632
676
|
if (error) {
|
|
633
677
|
const { stack, uncaught, message, actual, expected } = error
|
|
@@ -640,6 +684,36 @@ export const base64EncodeFile = function (filePath) {
|
|
|
640
684
|
return Buffer.from(fs.readFileSync(filePath)).toString('base64')
|
|
641
685
|
}
|
|
642
686
|
|
|
687
|
+
export const getMimeType = function (fileName) {
|
|
688
|
+
const ext = path.extname(fileName).toLowerCase()
|
|
689
|
+
const mimeTypes = {
|
|
690
|
+
'.jpg': 'image/jpeg',
|
|
691
|
+
'.jpeg': 'image/jpeg',
|
|
692
|
+
'.png': 'image/png',
|
|
693
|
+
'.gif': 'image/gif',
|
|
694
|
+
'.bmp': 'image/bmp',
|
|
695
|
+
'.svg': 'image/svg+xml',
|
|
696
|
+
'.webp': 'image/webp',
|
|
697
|
+
'.pdf': 'application/pdf',
|
|
698
|
+
'.txt': 'text/plain',
|
|
699
|
+
'.html': 'text/html',
|
|
700
|
+
'.css': 'text/css',
|
|
701
|
+
'.js': 'application/javascript',
|
|
702
|
+
'.json': 'application/json',
|
|
703
|
+
'.xml': 'application/xml',
|
|
704
|
+
'.zip': 'application/zip',
|
|
705
|
+
'.csv': 'text/csv',
|
|
706
|
+
'.doc': 'application/msword',
|
|
707
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
708
|
+
'.xls': 'application/vnd.ms-excel',
|
|
709
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
710
|
+
'.mp3': 'audio/mpeg',
|
|
711
|
+
'.mp4': 'video/mp4',
|
|
712
|
+
'.wav': 'audio/wav',
|
|
713
|
+
}
|
|
714
|
+
return mimeTypes[ext] || 'application/octet-stream'
|
|
715
|
+
}
|
|
716
|
+
|
|
643
717
|
export const markdownToAnsi = function (markdown) {
|
|
644
718
|
return (
|
|
645
719
|
markdown
|
package/lib/workers.js
CHANGED
|
@@ -20,6 +20,7 @@ import event from './event.js'
|
|
|
20
20
|
import { deserializeTest } from './mocha/test.js'
|
|
21
21
|
import { deserializeSuite } from './mocha/suite.js'
|
|
22
22
|
import recorder from './recorder.js'
|
|
23
|
+
import store from './store.js'
|
|
23
24
|
import runHook from './hooks.js'
|
|
24
25
|
import WorkerStorage from './workerStorage.js'
|
|
25
26
|
import { createRuns } from './command/run-multiple/collection.js'
|
|
@@ -28,7 +29,7 @@ const pathToWorker = path.join(__dirname, 'command', 'workers', 'runTests.js')
|
|
|
28
29
|
|
|
29
30
|
const initializeCodecept = async (configPath, options = {}) => {
|
|
30
31
|
const config = await mainConfig.load(configPath || '.')
|
|
31
|
-
const codecept = new Codecept(config, options)
|
|
32
|
+
const codecept = new Codecept(config, { ...options, skipDefaultListeners: true })
|
|
32
33
|
await codecept.init(getTestRoot(configPath))
|
|
33
34
|
codecept.loadTests()
|
|
34
35
|
|
|
@@ -504,6 +505,7 @@ class Workers extends EventEmitter {
|
|
|
504
505
|
await this._ensureInitialized()
|
|
505
506
|
recorder.startUnlessRunning()
|
|
506
507
|
event.dispatcher.emit(event.workers.before)
|
|
508
|
+
store.workerMode = true
|
|
507
509
|
process.env.RUNS_WITH_WORKERS = 'true'
|
|
508
510
|
|
|
509
511
|
// Create workers and set up message handlers immediately (not in recorder queue)
|
|
@@ -519,22 +521,8 @@ class Workers extends EventEmitter {
|
|
|
519
521
|
// Workers are already running, this is just a placeholder step
|
|
520
522
|
})
|
|
521
523
|
|
|
522
|
-
// Add overall timeout to prevent infinite hanging
|
|
523
|
-
const overallTimeout = setTimeout(() => {
|
|
524
|
-
console.error('[Main] Overall timeout reached (10 minutes). Force terminating remaining workers...')
|
|
525
|
-
workerThreads.forEach(w => {
|
|
526
|
-
try {
|
|
527
|
-
w.terminate()
|
|
528
|
-
} catch (e) {
|
|
529
|
-
// ignore
|
|
530
|
-
}
|
|
531
|
-
})
|
|
532
|
-
this._finishRun()
|
|
533
|
-
}, 600000) // 10 minutes
|
|
534
|
-
|
|
535
524
|
return new Promise(resolve => {
|
|
536
525
|
this.on('end', () => {
|
|
537
|
-
clearTimeout(overallTimeout)
|
|
538
526
|
resolve()
|
|
539
527
|
})
|
|
540
528
|
})
|
|
@@ -559,11 +547,12 @@ class Workers extends EventEmitter {
|
|
|
559
547
|
if (this.isPoolMode) {
|
|
560
548
|
this.activeWorkers.set(worker, { available: true, workerIndex: null })
|
|
561
549
|
}
|
|
562
|
-
|
|
550
|
+
|
|
563
551
|
// Track last activity time to detect hanging workers
|
|
564
552
|
let lastActivity = Date.now()
|
|
565
553
|
let currentTest = null
|
|
566
|
-
|
|
554
|
+
let autoTerminated = false
|
|
555
|
+
const workerTimeout = process.env.CODECEPT_WORKER_TIMEOUT ? ms(process.env.CODECEPT_WORKER_TIMEOUT) : ms('5m')
|
|
567
556
|
|
|
568
557
|
const timeoutChecker = setInterval(() => {
|
|
569
558
|
const elapsed = Date.now() - lastActivity
|
|
@@ -623,15 +612,41 @@ class Workers extends EventEmitter {
|
|
|
623
612
|
})
|
|
624
613
|
}
|
|
625
614
|
|
|
615
|
+
const exitTimeout = parseInt(process.env.CODECEPT_AUTO_EXIT_TIMEOUT, 10)
|
|
616
|
+
if (exitTimeout === 0) break
|
|
617
|
+
setTimeout(() => {
|
|
618
|
+
autoTerminated = true
|
|
619
|
+
worker.terminate()
|
|
620
|
+
}, exitTimeout || 2000)
|
|
621
|
+
|
|
626
622
|
break
|
|
627
623
|
case event.suite.before:
|
|
628
|
-
|
|
624
|
+
{
|
|
625
|
+
const suite = deserializeSuite(message.data)
|
|
626
|
+
this.emit(event.suite.before, suite)
|
|
627
|
+
event.dispatcher.emit(event.suite.before, suite)
|
|
628
|
+
}
|
|
629
|
+
break
|
|
630
|
+
case event.suite.after:
|
|
631
|
+
{
|
|
632
|
+
const suite = deserializeSuite(message.data)
|
|
633
|
+
this.emit(event.suite.after, suite)
|
|
634
|
+
event.dispatcher.emit(event.suite.after, suite)
|
|
635
|
+
}
|
|
629
636
|
break
|
|
630
637
|
case event.test.before:
|
|
631
|
-
|
|
638
|
+
{
|
|
639
|
+
const test = deserializeTest(message.data)
|
|
640
|
+
this.emit(event.test.before, test)
|
|
641
|
+
event.dispatcher.emit(event.test.before, test)
|
|
642
|
+
}
|
|
632
643
|
break
|
|
633
644
|
case event.test.started:
|
|
634
|
-
|
|
645
|
+
{
|
|
646
|
+
const test = deserializeTest(message.data)
|
|
647
|
+
this.emit(event.test.started, test)
|
|
648
|
+
event.dispatcher.emit(event.test.started, test)
|
|
649
|
+
}
|
|
635
650
|
break
|
|
636
651
|
case event.test.failed:
|
|
637
652
|
// For hook failures, emit immediately as there won't be a test.finished event
|
|
@@ -645,7 +660,11 @@ class Workers extends EventEmitter {
|
|
|
645
660
|
// Skip individual passed events - we'll emit based on finished state
|
|
646
661
|
break
|
|
647
662
|
case event.test.skipped:
|
|
648
|
-
|
|
663
|
+
{
|
|
664
|
+
const test = deserializeTest(message.data)
|
|
665
|
+
this.emit(event.test.skipped, test)
|
|
666
|
+
event.dispatcher.emit(event.test.skipped, test)
|
|
667
|
+
}
|
|
649
668
|
break
|
|
650
669
|
case event.test.finished:
|
|
651
670
|
// Handle different types of test completion properly
|
|
@@ -674,28 +693,47 @@ class Workers extends EventEmitter {
|
|
|
674
693
|
}
|
|
675
694
|
}
|
|
676
695
|
|
|
677
|
-
|
|
696
|
+
const test = deserializeTest(data)
|
|
697
|
+
this.emit(event.test.finished, test)
|
|
698
|
+
event.dispatcher.emit(event.test.finished, test)
|
|
678
699
|
}
|
|
679
700
|
break
|
|
680
701
|
case event.test.after:
|
|
681
|
-
|
|
702
|
+
{
|
|
703
|
+
const test = deserializeTest(message.data)
|
|
704
|
+
this.emit(event.test.after, test)
|
|
705
|
+
event.dispatcher.emit(event.test.after, test)
|
|
706
|
+
}
|
|
682
707
|
break
|
|
683
708
|
case event.step.finished:
|
|
684
709
|
this.emit(event.step.finished, message.data)
|
|
710
|
+
event.dispatcher.emit(event.step.finished, message.data)
|
|
685
711
|
break
|
|
686
712
|
case event.step.started:
|
|
687
713
|
this.emit(event.step.started, message.data)
|
|
714
|
+
event.dispatcher.emit(event.step.started, message.data)
|
|
688
715
|
break
|
|
689
716
|
case event.step.passed:
|
|
690
717
|
this.emit(event.step.passed, message.data)
|
|
718
|
+
event.dispatcher.emit(event.step.passed, message.data)
|
|
691
719
|
break
|
|
692
720
|
case event.step.failed:
|
|
693
721
|
this.emit(event.step.failed, message.data, message.data.error)
|
|
722
|
+
event.dispatcher.emit(event.step.failed, message.data, message.data.error)
|
|
694
723
|
break
|
|
695
724
|
case event.hook.failed:
|
|
696
725
|
// Hook failures are already reported as test failures by the worker
|
|
697
726
|
// Just emit the hook.failed event for listeners
|
|
698
727
|
this.emit(event.hook.failed, message.data)
|
|
728
|
+
event.dispatcher.emit(event.hook.failed, message.data)
|
|
729
|
+
break
|
|
730
|
+
case event.hook.passed:
|
|
731
|
+
this.emit(event.hook.passed, message.data)
|
|
732
|
+
event.dispatcher.emit(event.hook.passed, message.data)
|
|
733
|
+
break
|
|
734
|
+
case event.hook.finished:
|
|
735
|
+
this.emit(event.hook.finished, message.data)
|
|
736
|
+
event.dispatcher.emit(event.hook.finished, message.data)
|
|
699
737
|
break
|
|
700
738
|
}
|
|
701
739
|
})
|
|
@@ -711,8 +749,8 @@ class Workers extends EventEmitter {
|
|
|
711
749
|
worker.on('exit', (code) => {
|
|
712
750
|
clearInterval(timeoutChecker)
|
|
713
751
|
this.closedWorkers += 1
|
|
714
|
-
|
|
715
|
-
if (code !== 0) {
|
|
752
|
+
|
|
753
|
+
if (code !== 0 && !autoTerminated) {
|
|
716
754
|
console.error(`[Main] Worker exited with code ${code}`)
|
|
717
755
|
if (currentTest) {
|
|
718
756
|
console.error(`[Main] Last test running: ${currentTest}`)
|