codeceptjs 4.0.2-beta.9 → 4.0.2
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 -28
- 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 +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 +745 -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 +195 -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 +34 -25
- package/lib/command/check.js +2 -1
- package/lib/command/definitions.js +6 -7
- package/lib/command/dryRun.js +24 -5
- package/lib/command/generate.js +3 -1
- package/lib/command/gherkin/snippets.js +5 -4
- package/lib/command/init.js +249 -270
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/command/run-multiple.js +3 -1
- 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 +84 -41
- package/lib/config.js +96 -18
- package/lib/container.js +115 -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 +7 -4
- 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 +358 -467
- package/lib/helper/Puppeteer.js +335 -192
- package/lib/helper/WebDriver.js +324 -111
- package/lib/helper/errors/ElementNotFound.js +5 -2
- package/lib/helper/errors/MultipleElementsFound.js +52 -0
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/PlaywrightLocator.js +7 -107
- 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 +158 -16
- package/lib/mocha/cli.js +19 -1
- package/lib/mocha/factory.js +11 -1
- 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 +457 -0
- package/lib/plugin/analyze.js +9 -9
- package/lib/plugin/auth.js +5 -4
- 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 +43 -32
- 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 +15 -2
- 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/loaderCheck.js +28 -0
- 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/typescript.js +188 -23
- package/lib/utils.js +77 -3
- package/lib/workers.js +65 -40
- package/package.json +35 -30
- package/typings/index.d.ts +119 -8
- package/typings/promiseBasedTypes.d.ts +3158 -6065
- package/typings/types.d.ts +3453 -6494
- 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/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/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/helper/Mochawesome.js +0 -96
- package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
- package/lib/helper/extras/React.js +0 -65
- 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
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import Container from '../container.js'
|
|
2
|
+
import output from '../output.js'
|
|
3
|
+
|
|
4
|
+
const supportedHelpers = Container.STANDARD_ACTING_HELPERS
|
|
5
|
+
|
|
6
|
+
const RESERVED_KEYS = new Set(['on', 'path', 'line', 'pattern'])
|
|
7
|
+
const ALL_MODES = ['fail', 'test', 'step', 'file', 'url']
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parse a plugin's _args (from CLI `-p plugin:key=value:key=value`) into a flat dict.
|
|
11
|
+
* Each entry is split on `;` then on the first `=`. Bare segments become `{ key: true }`.
|
|
12
|
+
*
|
|
13
|
+
* Examples:
|
|
14
|
+
* parsePluginArgs(['on=fail'])
|
|
15
|
+
* → { on: 'fail' }
|
|
16
|
+
* parsePluginArgs(['on=file', 'path=tests/foo.js;line=43'])
|
|
17
|
+
* → { on: 'file', path: 'tests/foo.js', line: '43' }
|
|
18
|
+
* parsePluginArgs(['on=file', 'path=tests/foo.js', 'line=43'])
|
|
19
|
+
* → { on: 'file', path: 'tests/foo.js', line: '43' }
|
|
20
|
+
* parsePluginArgs(['show'])
|
|
21
|
+
* → { show: true }
|
|
22
|
+
*/
|
|
23
|
+
export function parsePluginArgs(args = []) {
|
|
24
|
+
const opts = {}
|
|
25
|
+
for (const arg of args) {
|
|
26
|
+
if (!arg) continue
|
|
27
|
+
for (const segment of arg.split(';')) {
|
|
28
|
+
if (!segment) continue
|
|
29
|
+
if (segment.includes('=')) {
|
|
30
|
+
const eq = segment.indexOf('=')
|
|
31
|
+
const key = segment.slice(0, eq)
|
|
32
|
+
const value = segment.slice(eq + 1)
|
|
33
|
+
opts[key] = coerce(value)
|
|
34
|
+
} else {
|
|
35
|
+
opts[segment] = true
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return opts
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function coerce(v) {
|
|
43
|
+
if (v === 'true') return true
|
|
44
|
+
if (v === 'false') return false
|
|
45
|
+
return v
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compose CLI args > config > defaults into a normalized trigger spec, then
|
|
50
|
+
* validate it. Returns `{ on, path, line, pattern, ...rest }` with `line`
|
|
51
|
+
* coerced to a number, or `null` if validation failed (an error is printed).
|
|
52
|
+
*
|
|
53
|
+
* @param {object} cliArgs — output of parsePluginArgs(config._args)
|
|
54
|
+
* @param {object} config — full plugin config object
|
|
55
|
+
* @param {object} defaults — fallback values, e.g. `{ on: 'fail' }`
|
|
56
|
+
* @param {object} options
|
|
57
|
+
* @param {string} options.name — plugin name, used in error messages
|
|
58
|
+
* @param {string[]} [options.validModes] — accepted values for `on`
|
|
59
|
+
* (default: fail, test, step, file, url)
|
|
60
|
+
*/
|
|
61
|
+
export function resolveTrigger(cliArgs = {}, config = {}, defaults = {}, options = {}) {
|
|
62
|
+
const { name = 'plugin', validModes = ALL_MODES } = options
|
|
63
|
+
const merged = { ...defaults, ...pickKnown(config), ...cliArgs }
|
|
64
|
+
if (merged.line != null) merged.line = parseInt(merged.line, 10)
|
|
65
|
+
|
|
66
|
+
const valid = new Set(validModes)
|
|
67
|
+
if (!valid.has(merged.on)) {
|
|
68
|
+
output.error(`${name}: unknown on="${merged.on}". Valid: ${validModes.join(', ')}`)
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
if (merged.on === 'file' && !merged.path) {
|
|
72
|
+
output.error(`${name}:on=file requires path=. Example: -p ${name}:on=file:path=tests/foo.js`)
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
if (merged.on === 'url' && !merged.pattern) {
|
|
76
|
+
output.error(`${name}:on=url requires pattern=. Example: -p ${name}:on=url:pattern=/users/*`)
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return merged
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function pickKnown(config) {
|
|
84
|
+
const out = {}
|
|
85
|
+
for (const key of Object.keys(config || {})) {
|
|
86
|
+
if (RESERVED_KEYS.has(key)) out[key] = config[key]
|
|
87
|
+
}
|
|
88
|
+
return out
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Match a step's source location against a `path` (substring/suffix) and optional `line`.
|
|
93
|
+
* Reads the step's stack via `step.line()` to get `file:row:col`.
|
|
94
|
+
*/
|
|
95
|
+
export function matchStepFile(step, targetPath, targetLine) {
|
|
96
|
+
if (!targetPath) return false
|
|
97
|
+
const stepLine = step.line && step.line()
|
|
98
|
+
if (!stepLine) return false
|
|
99
|
+
|
|
100
|
+
const parsed = parseStepLine(stepLine)
|
|
101
|
+
if (!parsed) return false
|
|
102
|
+
|
|
103
|
+
const fileMatches = parsed.file.includes(targetPath) || parsed.file.endsWith(targetPath)
|
|
104
|
+
if (!fileMatches) return false
|
|
105
|
+
|
|
106
|
+
if (targetLine != null && !Number.isNaN(targetLine) && parsed.line !== targetLine) return false
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseStepLine(stepLine) {
|
|
111
|
+
let line = stepLine.trim()
|
|
112
|
+
if (line.startsWith('at ')) line = line.substring(3).trim()
|
|
113
|
+
|
|
114
|
+
const lastColon = line.lastIndexOf(':')
|
|
115
|
+
if (lastColon < 0) return null
|
|
116
|
+
const secondLastColon = line.lastIndexOf(':', lastColon - 1)
|
|
117
|
+
if (secondLastColon < 0) return null
|
|
118
|
+
|
|
119
|
+
const file = line.substring(0, secondLastColon)
|
|
120
|
+
const lineNum = parseInt(line.substring(secondLastColon + 1, lastColon), 10)
|
|
121
|
+
|
|
122
|
+
if (Number.isNaN(lineNum)) return null
|
|
123
|
+
return { file, line: lineNum }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Match a URL string against a glob-style pattern (supports `*` wildcards).
|
|
128
|
+
*/
|
|
129
|
+
export function matchUrl(currentUrl, pattern) {
|
|
130
|
+
if (!pattern || !currentUrl) return false
|
|
131
|
+
return patternToRegex(pattern).test(currentUrl)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function patternToRegex(pattern) {
|
|
135
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
136
|
+
const regexStr = escaped.replace(/\*/g, '.*')
|
|
137
|
+
return new RegExp(regexStr)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Return the first available standard browser helper, or null.
|
|
142
|
+
*/
|
|
143
|
+
export function getBrowserHelper() {
|
|
144
|
+
const helpers = Container.helpers()
|
|
145
|
+
for (const name of supportedHelpers) {
|
|
146
|
+
if (Object.keys(helpers).indexOf(name) > -1) {
|
|
147
|
+
return helpers[name]
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
@@ -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
|