codeceptjs 4.0.2-beta.8 → 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 +91 -37
- 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
package/lib/mocha/factory.js
CHANGED
|
@@ -7,6 +7,8 @@ import gherkinParser, { loadTranslations } from './gherkin.js'
|
|
|
7
7
|
import output from '../output.js'
|
|
8
8
|
import scenarioUiFunction from './ui.js'
|
|
9
9
|
import { initMochaGlobals } from '../globals.js'
|
|
10
|
+
import { fixErrorStack } from '../utils/typescript.js'
|
|
11
|
+
import container from '../container.js'
|
|
10
12
|
|
|
11
13
|
const __filename = fileURLToPath(import.meta.url)
|
|
12
14
|
const __dirname = fsPath.dirname(__filename)
|
|
@@ -15,7 +17,11 @@ let mocha
|
|
|
15
17
|
|
|
16
18
|
class MochaFactory {
|
|
17
19
|
static create(config, opts) {
|
|
18
|
-
|
|
20
|
+
const merged = Object.assign({}, config, opts)
|
|
21
|
+
mocha = new Mocha(merged)
|
|
22
|
+
if (merged.cleanReferencesAfterRun !== true) {
|
|
23
|
+
mocha.cleanReferencesAfterRun(false)
|
|
24
|
+
}
|
|
19
25
|
output.process(opts.child)
|
|
20
26
|
mocha.ui(scenarioUiFunction)
|
|
21
27
|
|
|
@@ -34,6 +40,10 @@ class MochaFactory {
|
|
|
34
40
|
// Handle ECONNREFUSED without dynamic import for now
|
|
35
41
|
err = new Error('Connection refused: ' + err.toString())
|
|
36
42
|
}
|
|
43
|
+
const fileMapping = container?.tsFileMapping?.()
|
|
44
|
+
if (fileMapping) {
|
|
45
|
+
fixErrorStack(err, fileMapping)
|
|
46
|
+
}
|
|
37
47
|
output.error(err)
|
|
38
48
|
output.print(err.stack)
|
|
39
49
|
process.exit(1)
|
package/lib/mocha/inject.js
CHANGED
|
@@ -5,7 +5,7 @@ const getInjectedArguments = async (fn, test, suite) => {
|
|
|
5
5
|
const container = containerModule.default || containerModule
|
|
6
6
|
|
|
7
7
|
const testArgs = {}
|
|
8
|
-
const params = getParams(fn) || []
|
|
8
|
+
const params = getParams(fn, { warnOnLegacyFormat: true }) || []
|
|
9
9
|
const objects = container.support()
|
|
10
10
|
|
|
11
11
|
for (const key of params) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isAsyncFunction } from '../utils.js'
|
|
2
|
+
import store from '../store.js'
|
|
2
3
|
|
|
3
4
|
/** @class */
|
|
4
5
|
class ScenarioConfig {
|
|
@@ -40,7 +41,7 @@ class ScenarioConfig {
|
|
|
40
41
|
* @returns {this}
|
|
41
42
|
*/
|
|
42
43
|
retry(retries) {
|
|
43
|
-
if (
|
|
44
|
+
if (store.scenarioOnly) retries = -retries
|
|
44
45
|
this.test.retries(retries)
|
|
45
46
|
return this
|
|
46
47
|
}
|
package/lib/mocha/ui.js
CHANGED
|
@@ -9,13 +9,12 @@ import { HookConfig, AfterSuiteHook, AfterHook, BeforeSuiteHook, BeforeHook } fr
|
|
|
9
9
|
import { initMochaGlobals } from '../globals.js'
|
|
10
10
|
import common from 'mocha/lib/interfaces/common.js'
|
|
11
11
|
import container from '../container.js'
|
|
12
|
+
import store from '../store.js'
|
|
12
13
|
|
|
13
14
|
const setContextTranslation = context => {
|
|
14
|
-
|
|
15
|
-
const containerToUse = global.container || container
|
|
16
|
-
if (!containerToUse) return
|
|
15
|
+
if (!container) return
|
|
17
16
|
|
|
18
|
-
const translation =
|
|
17
|
+
const translation = container.translation?.() || container.translation
|
|
19
18
|
const contexts = translation?.value?.('contexts')
|
|
20
19
|
|
|
21
20
|
if (contexts) {
|
|
@@ -119,7 +118,7 @@ export default function (suite) {
|
|
|
119
118
|
context.Feature.only = function (title, opts) {
|
|
120
119
|
const reString = `^${escapeRe(`${title}:`)}`
|
|
121
120
|
mocha.grep(new RegExp(reString))
|
|
122
|
-
|
|
121
|
+
store.featureOnly = true
|
|
123
122
|
return context.Feature(title, opts)
|
|
124
123
|
}
|
|
125
124
|
|
|
@@ -171,7 +170,7 @@ export default function (suite) {
|
|
|
171
170
|
context.Scenario.only = function (title, opts, fn) {
|
|
172
171
|
const reString = `^${escapeRe(`${suites[0].title}: ${title}`.replace(/( \| {.+})?$/g, ''))}`
|
|
173
172
|
mocha.grep(new RegExp(reString))
|
|
174
|
-
|
|
173
|
+
store.scenarioOnly = true
|
|
175
174
|
return addScenario(title, opts, fn)
|
|
176
175
|
}
|
|
177
176
|
|
package/lib/parser.js
CHANGED
|
@@ -14,11 +14,11 @@ export const getParamsToString = function (fn) {
|
|
|
14
14
|
return getParams(newFn).join(', ')
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function getParams(fn) {
|
|
17
|
+
function getParams(fn, { warnOnLegacyFormat = false } = {}) {
|
|
18
18
|
if (fn.isSinonProxy) return []
|
|
19
19
|
try {
|
|
20
20
|
const reflected = parser.parse(fn)
|
|
21
|
-
if (reflected.args.length > 1 || reflected.args[0] === 'I') {
|
|
21
|
+
if (warnOnLegacyFormat && (reflected.args.length > 1 || reflected.args[0] === 'I')) {
|
|
22
22
|
output.error('Error: old CodeceptJS v2 format detected. Upgrade your project to the new format -> https://bit.ly/codecept3Up')
|
|
23
23
|
}
|
|
24
24
|
if (reflected.destructuredArgs.length > 0) reflected.args = [...reflected.destructuredArgs]
|
package/lib/pause.js
CHANGED
|
@@ -18,6 +18,8 @@ let nextStep
|
|
|
18
18
|
let finish
|
|
19
19
|
let next
|
|
20
20
|
let registeredVariables = {}
|
|
21
|
+
let externalHandler = null
|
|
22
|
+
|
|
21
23
|
/**
|
|
22
24
|
* Pauses test execution and starts interactive shell
|
|
23
25
|
* @param {Object<string, *>} [passedObject]
|
|
@@ -37,10 +39,10 @@ const pause = function (passedObject = {}) {
|
|
|
37
39
|
})
|
|
38
40
|
|
|
39
41
|
event.dispatcher.on(event.test.finished, () => {
|
|
40
|
-
finish()
|
|
42
|
+
if (typeof finish === 'function') finish()
|
|
41
43
|
recorder.session.restore('pause')
|
|
42
|
-
rl.close()
|
|
43
|
-
history.save()
|
|
44
|
+
if (rl) rl.close()
|
|
45
|
+
if (!externalHandler) history.save()
|
|
44
46
|
})
|
|
45
47
|
|
|
46
48
|
recorder.add('Start new session', () => pauseSession(passedObject))
|
|
@@ -49,6 +51,15 @@ const pause = function (passedObject = {}) {
|
|
|
49
51
|
function pauseSession(passedObject = {}) {
|
|
50
52
|
registeredVariables = passedObject
|
|
51
53
|
recorder.session.start('pause')
|
|
54
|
+
|
|
55
|
+
if (externalHandler) {
|
|
56
|
+
store.onPause = true
|
|
57
|
+
return externalHandler({ registeredVariables }).then(() => {
|
|
58
|
+
store.onPause = false
|
|
59
|
+
recorder.session.restore('pause')
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
if (!next) {
|
|
53
64
|
let vars = Object.keys(registeredVariables).join(', ')
|
|
54
65
|
if (vars) vars = `(vars: ${vars})`
|
|
@@ -234,5 +245,28 @@ function registerVariable(name, value) {
|
|
|
234
245
|
registeredVariables[name] = value
|
|
235
246
|
}
|
|
236
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Hook for external pause drivers (e.g. the MCP server). When set, pauseSession
|
|
250
|
+
* delegates to the handler instead of opening a readline REPL. The handler
|
|
251
|
+
* receives `{ registeredVariables }` and returns a Promise that resolves when
|
|
252
|
+
* the driver decides to continue (resume) or step.
|
|
253
|
+
*
|
|
254
|
+
* The driver controls step-vs-resume by mutating `next` via setNextStep before
|
|
255
|
+
* resolving its Promise.
|
|
256
|
+
*/
|
|
257
|
+
function setPauseHandler(handler) {
|
|
258
|
+
externalHandler = handler
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Trigger a one-shot pause from outside the test (e.g. the MCP server,
|
|
263
|
+
* pausing the test at a specific step index without modifying the test).
|
|
264
|
+
* Schedules pauseSession through the recorder so it slots between steps.
|
|
265
|
+
*/
|
|
266
|
+
function pauseNow(passedObject = {}) {
|
|
267
|
+
if (store.dryRun) return
|
|
268
|
+
recorder.add('Triggered pause', () => pauseSession(passedObject))
|
|
269
|
+
}
|
|
270
|
+
|
|
237
271
|
export default pause
|
|
238
|
-
export { registerVariable }
|
|
272
|
+
export { registerVariable, setPauseHandler, pauseNow }
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import { mkdirp } from 'mkdirp'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
import store from '../store.js'
|
|
6
|
+
import Container from '../container.js'
|
|
7
|
+
import recorder from '../recorder.js'
|
|
8
|
+
import event from '../event.js'
|
|
9
|
+
import output from '../output.js'
|
|
10
|
+
import { deleteDir, clearString } from '../utils.js'
|
|
11
|
+
import { captureSnapshot, pickActingHelper, traceDirFor, artifactLinks } from '../utils/trace.js'
|
|
12
|
+
import {
|
|
13
|
+
parsePluginArgs,
|
|
14
|
+
resolveTrigger,
|
|
15
|
+
matchStepFile,
|
|
16
|
+
matchUrl,
|
|
17
|
+
} from '../utils/pluginParser.js'
|
|
18
|
+
|
|
19
|
+
const defaultConfig = {
|
|
20
|
+
on: 'step',
|
|
21
|
+
deleteSuccessful: false,
|
|
22
|
+
fullPageScreenshots: false,
|
|
23
|
+
output: store.outputDir,
|
|
24
|
+
captureHTML: true,
|
|
25
|
+
captureARIA: true,
|
|
26
|
+
captureBrowserLogs: true,
|
|
27
|
+
captureHTTP: true,
|
|
28
|
+
captureDebugOutput: true,
|
|
29
|
+
ignoreSteps: [],
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
*
|
|
34
|
+
* Generates AI-friendly trace files for debugging with AI agents.
|
|
35
|
+
* This plugin creates a markdown file with test execution logs and links to all artifacts
|
|
36
|
+
* (screenshots, HTML, ARIA snapshots, browser logs, HTTP requests) for each step.
|
|
37
|
+
*
|
|
38
|
+
* #### Configuration
|
|
39
|
+
*
|
|
40
|
+
* ```js
|
|
41
|
+
* "plugins": {
|
|
42
|
+
* "aiTrace": {
|
|
43
|
+
* "enabled": true
|
|
44
|
+
* }
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* Possible config options:
|
|
49
|
+
*
|
|
50
|
+
* * `deleteSuccessful`: delete traces for successfully executed tests. Default: false.
|
|
51
|
+
* * `fullPageScreenshots`: should full page screenshots be used. Default: false.
|
|
52
|
+
* * `output`: a directory where traces should be stored. Default: `output`.
|
|
53
|
+
* * `captureHTML`: capture HTML for each step. Default: true.
|
|
54
|
+
* * `captureARIA`: capture ARIA snapshot for each step. Default: true.
|
|
55
|
+
* * `captureBrowserLogs`: capture browser console logs. Default: true.
|
|
56
|
+
* * `captureHTTP`: capture HTTP requests (requires `trace` or `recordHar` enabled in helper config). Default: true.
|
|
57
|
+
* * `captureDebugOutput`: capture CodeceptJS debug output. Default: true.
|
|
58
|
+
* * `ignoreSteps`: steps to ignore in trace. Array of RegExps is expected.
|
|
59
|
+
* * `on`: trigger mode — `step` (default), `fail`, `test`, `file`, `url`.
|
|
60
|
+
*
|
|
61
|
+
* #### `on=` modes
|
|
62
|
+
*
|
|
63
|
+
* * **step** — persist every step (default)
|
|
64
|
+
* * **fail** — persist only the failed step
|
|
65
|
+
* * **test** — persist only the last step of each test
|
|
66
|
+
* * **file** — persist steps from `path=...[;line=...]`
|
|
67
|
+
* * **url** — persist when the current URL matches `pattern=...`
|
|
68
|
+
*
|
|
69
|
+
* @param {*} config
|
|
70
|
+
*/
|
|
71
|
+
export default function (config = {}) {
|
|
72
|
+
const cliArgs = parsePluginArgs(config._args)
|
|
73
|
+
const trigger = resolveTrigger(cliArgs, config, { on: defaultConfig.on }, { name: 'aiTrace' })
|
|
74
|
+
if (!trigger) return
|
|
75
|
+
|
|
76
|
+
config = Object.assign(defaultConfig, config)
|
|
77
|
+
|
|
78
|
+
const helper = pickActingHelper(Container.helpers())
|
|
79
|
+
|
|
80
|
+
if (!helper) {
|
|
81
|
+
output.warn('aiTrace plugin: No supported helper found (Playwright, Puppeteer, WebDriver). Plugin disabled.')
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let dir
|
|
86
|
+
let stepNum
|
|
87
|
+
let steps = []
|
|
88
|
+
let debugOutput = []
|
|
89
|
+
let error
|
|
90
|
+
let savedSteps = new Set()
|
|
91
|
+
let currentTest = null
|
|
92
|
+
let testStartTime
|
|
93
|
+
let currentUrl = null
|
|
94
|
+
let testFailed = false
|
|
95
|
+
let pendingArtifactCapture = null
|
|
96
|
+
let firstFailedStepSaved = false
|
|
97
|
+
|
|
98
|
+
const reportDir = config.output ? path.resolve(store.codeceptDir, config.output) : defaultConfig.output
|
|
99
|
+
|
|
100
|
+
if (config.captureDebugOutput) {
|
|
101
|
+
const originalDebug = output.debug
|
|
102
|
+
output.debug = function (...args) {
|
|
103
|
+
debugOutput.push(args.join(' '))
|
|
104
|
+
originalDebug.apply(output, args)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
event.dispatcher.on(event.suite.before, suite => {
|
|
109
|
+
stepNum = -1
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
event.dispatcher.on(event.test.before, test => {
|
|
113
|
+
let title
|
|
114
|
+
try {
|
|
115
|
+
title = test.fullTitle ? test.fullTitle() : test.title
|
|
116
|
+
} catch (err) {
|
|
117
|
+
title = test.title
|
|
118
|
+
}
|
|
119
|
+
dir = traceDirFor(test.file, title, reportDir)
|
|
120
|
+
mkdirp.sync(dir)
|
|
121
|
+
deleteDir(dir)
|
|
122
|
+
mkdirp.sync(dir)
|
|
123
|
+
stepNum = 0
|
|
124
|
+
error = null
|
|
125
|
+
steps = []
|
|
126
|
+
debugOutput = []
|
|
127
|
+
savedSteps.clear()
|
|
128
|
+
currentTest = test
|
|
129
|
+
testStartTime = Date.now()
|
|
130
|
+
currentUrl = null
|
|
131
|
+
testFailed = false
|
|
132
|
+
firstFailedStepSaved = false
|
|
133
|
+
pendingArtifactCapture = null
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
event.dispatcher.on(event.step.after, step => {
|
|
137
|
+
if (!currentTest) return
|
|
138
|
+
if (step.status === 'failed') {
|
|
139
|
+
testFailed = true
|
|
140
|
+
}
|
|
141
|
+
if (step.status === 'queued' && testFailed) {
|
|
142
|
+
output.debug(`aiTrace: Skipping queued step "${step.toString()}" - testFailed: ${testFailed}`)
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
if (step.status === 'failed' && firstFailedStepSaved) {
|
|
146
|
+
output.debug(`aiTrace: Skipping failed step "${step.toString()}" - already handled by step.failed event`)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// on= filtering
|
|
151
|
+
if (trigger.on === 'fail') return // failed steps handled by step.failed
|
|
152
|
+
if (trigger.on === 'file' && !matchStepFile(step, trigger.path, trigger.line)) return
|
|
153
|
+
if (trigger.on === 'url') {
|
|
154
|
+
recorder.add('aiTrace:url check', async () => {
|
|
155
|
+
try {
|
|
156
|
+
if (!helper.grabCurrentUrl) return
|
|
157
|
+
const url = await helper.grabCurrentUrl()
|
|
158
|
+
if (!matchUrl(url, trigger.pattern)) return
|
|
159
|
+
await persistStep(step)
|
|
160
|
+
} catch (err) {
|
|
161
|
+
output.debug(`aiTrace: Error in url-mode step persistence: ${err.message}`)
|
|
162
|
+
}
|
|
163
|
+
}, true)
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
recorder.add(`aiTrace step persistence: ${step.toString()}`, () => persistStep(step).catch(err => {
|
|
168
|
+
output.debug(`aiTrace: Error saving step: ${err.message}`)
|
|
169
|
+
}), true)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
event.dispatcher.on(event.step.failed, step => {
|
|
173
|
+
if (!currentTest) return
|
|
174
|
+
if (step.status === 'queued' && testFailed) {
|
|
175
|
+
output.debug(`aiTrace: Skipping queued failed step "${step.toString()}" - testFailed: ${testFailed}`)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
if (firstFailedStepSaved) {
|
|
179
|
+
output.debug(`aiTrace: Skipping subsequent failed step "${step.toString()}" - already saved first failed step`)
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const stepKey = step.toString()
|
|
184
|
+
if (savedSteps.has(stepKey)) {
|
|
185
|
+
const existingStep = steps.find(s => s.step === stepKey)
|
|
186
|
+
if (!existingStep) {
|
|
187
|
+
output.debug(`aiTrace: Step "${stepKey}" marked as saved but not found in steps array`)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
existingStep.status = 'failed'
|
|
191
|
+
|
|
192
|
+
pendingArtifactCapture = captureArtifactsForStep(step, existingStep, existingStep.prefix).catch(err => {
|
|
193
|
+
output.debug(`aiTrace: Error updating failed step: ${err.message}`)
|
|
194
|
+
})
|
|
195
|
+
} else {
|
|
196
|
+
if (stepNum === -1) return
|
|
197
|
+
if (isStepIgnored(step)) return
|
|
198
|
+
if (step.metaStep && step.metaStep.title === 'BeforeSuite') return
|
|
199
|
+
|
|
200
|
+
const stepPrefix = generateStepPrefix(step, stepNum)
|
|
201
|
+
stepNum++
|
|
202
|
+
|
|
203
|
+
const stepData = {
|
|
204
|
+
step: stepKey,
|
|
205
|
+
status: 'failed',
|
|
206
|
+
prefix: stepPrefix,
|
|
207
|
+
artifacts: {},
|
|
208
|
+
meta: {},
|
|
209
|
+
debugOutput: [],
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (step.startTime && step.endTime) {
|
|
213
|
+
stepData.meta.duration = ((step.endTime - step.startTime) / 1000).toFixed(2) + 's'
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
savedSteps.add(stepKey)
|
|
217
|
+
steps.push(stepData)
|
|
218
|
+
firstFailedStepSaved = true
|
|
219
|
+
|
|
220
|
+
pendingArtifactCapture = captureArtifactsForStep(step, stepData, stepPrefix).catch(err => {
|
|
221
|
+
output.debug(`aiTrace: Error capturing failed step artifacts: ${err.message}`)
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
event.dispatcher.on(event.test.passed, test => {
|
|
227
|
+
if (config.deleteSuccessful) {
|
|
228
|
+
deleteDir(dir)
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
persist(test, 'passed')
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
event.dispatcher.on(event.test.failed, (test, _err, hookName) => {
|
|
235
|
+
if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') {
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
recorder.add('aiTrace:persist failed', async () => {
|
|
239
|
+
if (pendingArtifactCapture) {
|
|
240
|
+
await pendingArtifactCapture
|
|
241
|
+
pendingArtifactCapture = null
|
|
242
|
+
}
|
|
243
|
+
persist(test, 'failed')
|
|
244
|
+
}, true)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
async function persistStep(step) {
|
|
248
|
+
if (stepNum === -1) return
|
|
249
|
+
if (isStepIgnored(step)) return
|
|
250
|
+
if (step.metaStep && step.metaStep.title === 'BeforeSuite') return
|
|
251
|
+
|
|
252
|
+
const stepKey = step.toString()
|
|
253
|
+
|
|
254
|
+
if (savedSteps.has(stepKey)) {
|
|
255
|
+
const existingStep = steps.find(s => s.step === stepKey)
|
|
256
|
+
if (existingStep && step.status === 'failed') {
|
|
257
|
+
existingStep.status = 'failed'
|
|
258
|
+
step.artifacts = {}
|
|
259
|
+
await captureArtifactsForStep(step, existingStep, existingStep.prefix)
|
|
260
|
+
}
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
savedSteps.add(stepKey)
|
|
264
|
+
|
|
265
|
+
const stepPrefix = generateStepPrefix(step, stepNum)
|
|
266
|
+
stepNum++
|
|
267
|
+
|
|
268
|
+
const stepData = {
|
|
269
|
+
step: step.toString(),
|
|
270
|
+
status: step.status,
|
|
271
|
+
prefix: stepPrefix,
|
|
272
|
+
artifacts: {},
|
|
273
|
+
meta: {},
|
|
274
|
+
debugOutput: [],
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (step.startTime && step.endTime) {
|
|
278
|
+
stepData.meta.duration = ((step.endTime - step.startTime) / 1000).toFixed(2) + 's'
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (config.captureDebugOutput && debugOutput.length > 0) {
|
|
282
|
+
stepData.debugOutput = [...debugOutput]
|
|
283
|
+
debugOutput = []
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
await captureArtifactsForStep(step, stepData, stepPrefix)
|
|
287
|
+
steps.push(stepData)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function captureArtifactsForStep(step, stepData, stepPrefix) {
|
|
291
|
+
if (!step.artifacts) {
|
|
292
|
+
step.artifacts = {}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let browserAvailable = true
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
try {
|
|
299
|
+
if (helper.grabCurrentUrl) {
|
|
300
|
+
const url = await helper.grabCurrentUrl()
|
|
301
|
+
stepData.meta.url = url
|
|
302
|
+
currentUrl = url
|
|
303
|
+
}
|
|
304
|
+
} catch (err) {
|
|
305
|
+
browserAvailable = false
|
|
306
|
+
output.debug(`aiTrace: Browser unavailable, partial artifact capture: ${err.message}`)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let preExistingScreenshot = false
|
|
310
|
+
if (step.artifacts?.screenshot) {
|
|
311
|
+
const screenshotPath = path.isAbsolute(step.artifacts.screenshot)
|
|
312
|
+
? step.artifacts.screenshot
|
|
313
|
+
: path.resolve(dir, step.artifacts.screenshot)
|
|
314
|
+
const screenshotFile = path.basename(screenshotPath)
|
|
315
|
+
stepData.artifacts.screenshot = screenshotFile
|
|
316
|
+
step.artifacts.screenshot = screenshotPath
|
|
317
|
+
preExistingScreenshot = true
|
|
318
|
+
|
|
319
|
+
if (!fs.existsSync(screenshotPath)) {
|
|
320
|
+
try {
|
|
321
|
+
await helper.saveScreenshot(screenshotPath, config.fullPageScreenshots)
|
|
322
|
+
} catch (err) {
|
|
323
|
+
output.debug(`aiTrace: Could not save screenshot: ${err.message}`)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const captured = await captureSnapshot(helper, {
|
|
329
|
+
dir,
|
|
330
|
+
prefix: stepPrefix,
|
|
331
|
+
fullPage: config.fullPageScreenshots,
|
|
332
|
+
captureHTML: config.captureHTML && browserAvailable,
|
|
333
|
+
captureARIA: config.captureARIA && browserAvailable,
|
|
334
|
+
captureBrowserLogs: config.captureBrowserLogs && browserAvailable,
|
|
335
|
+
captureStorage: false,
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
if (!preExistingScreenshot && captured.screenshot) {
|
|
339
|
+
stepData.artifacts.screenshot = captured.screenshot
|
|
340
|
+
step.artifacts.screenshot = path.join(dir, captured.screenshot)
|
|
341
|
+
}
|
|
342
|
+
if (step.artifacts?.html) {
|
|
343
|
+
stepData.artifacts.html = step.artifacts.html
|
|
344
|
+
} else if (captured.html) {
|
|
345
|
+
stepData.artifacts.html = captured.html
|
|
346
|
+
}
|
|
347
|
+
if (captured.aria) stepData.artifacts.aria = captured.aria
|
|
348
|
+
if (captured.console) {
|
|
349
|
+
stepData.artifacts.console = captured.console
|
|
350
|
+
stepData.meta.consoleCount = captured.consoleCount
|
|
351
|
+
}
|
|
352
|
+
} catch (err) {
|
|
353
|
+
output.plugin(`aiTrace: Can't save step artifacts: ${err}`)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function persist(test, status) {
|
|
358
|
+
if (!steps.length) {
|
|
359
|
+
output.debug('aiTrace: No steps to save in trace')
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// on=test: only render the last step in markdown; artifacts of earlier steps
|
|
364
|
+
// remain on disk unreferenced.
|
|
365
|
+
if (trigger.on === 'test') {
|
|
366
|
+
steps = steps.slice(-1)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const testDuration = ((Date.now() - testStartTime) / 1000).toFixed(2)
|
|
370
|
+
|
|
371
|
+
let markdown = `file: ${test.file || 'unknown'}\n`
|
|
372
|
+
markdown += `name: ${test.title}\n`
|
|
373
|
+
markdown += `time: ${testDuration}s\n`
|
|
374
|
+
markdown += `---\n\n`
|
|
375
|
+
|
|
376
|
+
if (status === 'failed') {
|
|
377
|
+
if (test.art && test.art.message) {
|
|
378
|
+
markdown += `Error: ${test.art.message}\n\n`
|
|
379
|
+
}
|
|
380
|
+
if (test.art && test.art.stack) {
|
|
381
|
+
markdown += `${test.art.stack}\n\n`
|
|
382
|
+
}
|
|
383
|
+
markdown += `---\n\n`
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (config.captureDebugOutput && debugOutput.length > 0) {
|
|
387
|
+
markdown += `CodeceptJS Debug Output:\n\n`
|
|
388
|
+
debugOutput.forEach(line => {
|
|
389
|
+
markdown += `> ${line}\n`
|
|
390
|
+
})
|
|
391
|
+
markdown += `\n---\n\n`
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
steps.forEach((stepData, index) => {
|
|
395
|
+
const stepAnchor = clearString(stepData.step).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 50)
|
|
396
|
+
markdown += `### Step ${index + 1}: ${stepData.step}\n`
|
|
397
|
+
markdown += `<a id="${stepAnchor}"></a>\n`
|
|
398
|
+
|
|
399
|
+
if (stepData.meta.duration) {
|
|
400
|
+
markdown += ` > duration: ${stepData.meta.duration}\n`
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (stepData.meta.url) {
|
|
404
|
+
markdown += ` > navigated to ${stepData.meta.url}\n`
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (config.captureDebugOutput && stepData.debugOutput && stepData.debugOutput.length > 0) {
|
|
408
|
+
stepData.debugOutput.forEach(line => {
|
|
409
|
+
markdown += ` > ${line}\n`
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const links = artifactLinks(stepData.artifacts, { consoleCount: stepData.meta.consoleCount })
|
|
414
|
+
if (links) markdown += links + '\n'
|
|
415
|
+
|
|
416
|
+
if (config.captureHTTP) {
|
|
417
|
+
if (test.artifacts && test.artifacts.har) {
|
|
418
|
+
const harPath = path.relative(reportDir, test.artifacts.har)
|
|
419
|
+
markdown += ` > HTTP: see [HAR file](../${harPath}) for network requests\n`
|
|
420
|
+
} else if (test.artifacts && test.artifacts.trace) {
|
|
421
|
+
const tracePath = path.relative(reportDir, test.artifacts.trace)
|
|
422
|
+
markdown += ` > HTTP: see [Playwright trace](../${tracePath}) for network requests\n`
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
markdown += `\n`
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
const traceFile = path.join(dir, 'trace.md')
|
|
430
|
+
fs.writeFileSync(traceFile, markdown)
|
|
431
|
+
|
|
432
|
+
output.print(`Trace Saved: file://${traceFile}`)
|
|
433
|
+
|
|
434
|
+
if (!test.artifacts) test.artifacts = {}
|
|
435
|
+
test.artifacts.aiTrace = traceFile
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function isStepIgnored(step) {
|
|
439
|
+
if (!config.ignoreSteps) return false
|
|
440
|
+
if (!step.title) return false
|
|
441
|
+
for (const pattern of config.ignoreSteps || []) {
|
|
442
|
+
if (step.title.match(pattern)) return true
|
|
443
|
+
}
|
|
444
|
+
return false
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function generateStepPrefix(step, index) {
|
|
448
|
+
const stepName = step.toString()
|
|
449
|
+
const cleanedName = clearString(stepName)
|
|
450
|
+
.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
451
|
+
.replace(/_{2,}/g, '_')
|
|
452
|
+
.slice(0, 80)
|
|
453
|
+
.trim()
|
|
454
|
+
|
|
455
|
+
return `${String(index).padStart(4, '0')}_${cleanedName}`
|
|
456
|
+
}
|
|
457
|
+
}
|
package/lib/plugin/analyze.js
CHANGED
|
@@ -12,6 +12,7 @@ const ai = aiModule.default || aiModule
|
|
|
12
12
|
import colors from 'chalk'
|
|
13
13
|
import ora from 'ora'
|
|
14
14
|
import event from '../event.js'
|
|
15
|
+
import recorder from '../recorder.js'
|
|
15
16
|
|
|
16
17
|
import output from '../output.js'
|
|
17
18
|
|
|
@@ -154,10 +155,9 @@ const defaultConfig = {
|
|
|
154
155
|
if (config.vision && test.artifacts.screenshot) {
|
|
155
156
|
debug('Adding screenshot to prompt')
|
|
156
157
|
messages[0].content.push({
|
|
157
|
-
type: '
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
},
|
|
158
|
+
type: 'image',
|
|
159
|
+
image: base64EncodeFile(test.artifacts.screenshot),
|
|
160
|
+
mediaType: 'image/png',
|
|
161
161
|
})
|
|
162
162
|
}
|
|
163
163
|
|
|
@@ -227,14 +227,14 @@ export default function (config = {}) {
|
|
|
227
227
|
console.log('Enabled AI analysis')
|
|
228
228
|
})
|
|
229
229
|
|
|
230
|
-
event.dispatcher.on(event.all.result,
|
|
230
|
+
event.dispatcher.on(event.all.result, result => {
|
|
231
231
|
if (!isMainThread) return // run only on main thread
|
|
232
232
|
if (!ai.isEnabled) {
|
|
233
233
|
console.log('AI is disabled, no analysis will be performed. Run tests with --ai flag to enable it.')
|
|
234
234
|
return
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
printReport(result)
|
|
237
|
+
recorder.add('analyze:print-ai-report', () => printReport(result), true)
|
|
238
238
|
})
|
|
239
239
|
|
|
240
240
|
event.dispatcher.on(event.workers.result, async result => {
|
|
@@ -248,7 +248,7 @@ export default function (config = {}) {
|
|
|
248
248
|
return
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
printReport(result)
|
|
251
|
+
await printReport(result)
|
|
252
252
|
})
|
|
253
253
|
|
|
254
254
|
async function printReport(result) {
|
|
@@ -294,7 +294,7 @@ export default function (config = {}) {
|
|
|
294
294
|
console.error('Error analyzing failed tests', err)
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
if (!Object.keys(
|
|
297
|
+
if (!Object.keys(Container.plugins()).includes('pageInfo')) {
|
|
298
298
|
console.log('To improve analysis, enable pageInfo plugin to get more context for failed tests.')
|
|
299
299
|
}
|
|
300
300
|
}
|
|
@@ -353,7 +353,7 @@ function serializeError(error) {
|
|
|
353
353
|
errorMessage +=
|
|
354
354
|
'\n' +
|
|
355
355
|
error.stack
|
|
356
|
-
.replace(
|
|
356
|
+
.replace(store.codeceptDir || '', '.')
|
|
357
357
|
.split('\n')
|
|
358
358
|
.map(line => line.replace(ansiRegExp(), ''))
|
|
359
359
|
.slice(0, 5)
|