codeceptjs 4.0.0-rc.8 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -10
- package/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +751 -172
- package/docs/advanced.md +201 -0
- package/docs/agents.md +181 -0
- package/docs/ai.md +489 -0
- package/docs/aitrace.md +266 -0
- package/docs/api.md +332 -0
- package/docs/architecture.md +235 -0
- package/docs/assertions.md +415 -0
- package/docs/auth.md +318 -0
- package/docs/basics.md +424 -0
- package/docs/bdd.md +539 -0
- package/docs/best.md +240 -0
- package/docs/bootstrap.md +132 -0
- package/docs/commands.md +352 -0
- package/docs/community-helpers.md +63 -0
- package/docs/configuration.md +185 -0
- package/docs/continuous-integration.md +431 -0
- package/docs/custom-helpers.md +297 -0
- package/docs/data.md +448 -0
- package/docs/debugging.md +332 -0
- package/docs/detox.md +235 -0
- package/docs/docker.md +107 -0
- package/docs/effects.md +179 -0
- package/docs/element-based-testing.md +295 -0
- package/docs/element-selection.md +125 -0
- package/docs/els.md +328 -0
- package/docs/environment-variables.md +131 -0
- package/docs/examples.md +160 -0
- package/docs/heal.md +213 -0
- package/docs/helpers/ApiDataFactory.md +267 -0
- package/docs/helpers/Appium.md +1419 -0
- package/docs/helpers/Detox.md +665 -0
- package/docs/helpers/ExpectHelper.md +275 -0
- package/docs/helpers/FileSystem.md +152 -0
- package/docs/helpers/GraphQL.md +152 -0
- package/docs/helpers/GraphQLDataFactory.md +226 -0
- package/docs/helpers/JSONResponse.md +255 -0
- package/docs/helpers/MockRequest.md +377 -0
- package/docs/helpers/Playwright.md +2970 -0
- package/docs/helpers/Puppeteer-firefox.md +86 -0
- package/docs/helpers/Puppeteer.md +2583 -0
- package/docs/helpers/REST.md +289 -0
- package/docs/helpers/WebDriver.md +2639 -0
- package/docs/hooks.md +148 -0
- package/docs/index.md +111 -0
- package/docs/installation.md +121 -0
- package/docs/internal-test-server.md +89 -0
- package/docs/locators.md +355 -0
- package/docs/mcp.md +485 -0
- package/docs/migrate-from-cypress.md +98 -0
- package/docs/migrate-from-java.md +108 -0
- package/docs/migrate-from-protractor.md +101 -0
- package/docs/migrate-from-testcafe.md +99 -0
- package/docs/migration-4.md +743 -0
- package/docs/mobile.md +338 -0
- package/docs/pageobjects.md +399 -0
- package/docs/parallel.md +187 -0
- package/docs/playwright.md +714 -0
- package/docs/plugins/aiTrace.md +49 -0
- package/docs/plugins/analyze.md +66 -0
- package/docs/plugins/auth.md +241 -0
- package/docs/plugins/autoDelay.md +48 -0
- package/docs/plugins/browser.md +41 -0
- package/docs/plugins/coverage.md +39 -0
- package/docs/plugins/customLocator.md +119 -0
- package/docs/plugins/customReporter.md +16 -0
- package/docs/plugins/expose.md +75 -0
- package/docs/plugins/heal.md +44 -0
- package/docs/plugins/junitReporter.md +51 -0
- package/docs/plugins/pageInfo.md +34 -0
- package/docs/plugins/pause.md +43 -0
- package/docs/plugins/pauseOnFail.md +18 -0
- package/docs/plugins/retryFailedStep.md +75 -0
- package/docs/plugins/screencast.md +55 -0
- package/docs/plugins/screenshot.md +58 -0
- package/docs/plugins/screenshotOnFail.md +18 -0
- package/docs/plugins/stepTimeout.md +65 -0
- package/docs/plugins.md +87 -0
- package/docs/puppeteer.md +314 -0
- package/docs/quickstart.md +120 -0
- package/docs/reports.md +198 -0
- package/docs/retry.md +311 -0
- package/docs/secrets.md +150 -0
- package/docs/sessions.md +80 -0
- package/docs/shadow.md +68 -0
- package/docs/store.md +94 -0
- package/docs/test-structure.md +275 -0
- package/docs/timeouts.md +183 -0
- package/docs/translation.md +247 -0
- package/docs/tutorial.md +323 -0
- package/docs/typescript.md +159 -0
- package/docs/web-element.md +251 -0
- package/docs/webdriver.md +641 -0
- package/docs/within.md +55 -0
- package/lib/actor.js +1 -36
- package/lib/ai.js +3 -2
- package/lib/aria.js +260 -0
- package/lib/assertions.js +18 -0
- package/lib/codecept.js +7 -7
- package/lib/command/check.js +2 -1
- package/lib/command/dryRun.js +24 -5
- package/lib/command/generate.js +2 -0
- package/lib/command/gherkin/snippets.js +5 -4
- package/lib/command/init.js +248 -266
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/command/run-multiple.js +3 -2
- package/lib/command/run-workers.js +1 -14
- package/lib/command/run.js +3 -17
- package/lib/command/utils.js +14 -0
- package/lib/command/workers/runTests.js +11 -15
- package/lib/config.js +77 -4
- package/lib/container.js +97 -15
- package/lib/effects.js +17 -0
- package/lib/element/WebElement.js +195 -3
- package/lib/els.js +12 -6
- package/lib/globals.js +32 -19
- package/lib/heal.js +7 -4
- package/lib/helper/ApiDataFactory.js +2 -1
- package/lib/helper/FileSystem.js +3 -2
- package/lib/helper/GraphQLDataFactory.js +2 -1
- package/lib/helper/Playwright.js +96 -115
- package/lib/helper/Puppeteer.js +43 -131
- package/lib/helper/WebDriver.js +42 -52
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/PlaywrightLocator.js +10 -0
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/extras/focusCheck.js +43 -0
- package/lib/helper/extras/richTextEditor.js +178 -0
- package/lib/history.js +3 -2
- package/lib/html.js +90 -16
- package/lib/index.js +9 -1
- package/lib/listener/config.js +6 -4
- package/lib/listener/emptyRun.js +2 -1
- package/lib/listener/helpers.js +4 -1
- package/lib/listener/mocha.js +2 -1
- package/lib/listener/pageobjects.js +43 -0
- package/lib/listener/result.js +3 -2
- package/lib/locator.js +126 -16
- package/lib/mocha/cli.js +4 -2
- package/lib/mocha/factory.js +7 -2
- package/lib/mocha/inject.js +1 -1
- package/lib/mocha/scenarioConfig.js +2 -1
- package/lib/mocha/ui.js +5 -6
- package/lib/parser.js +2 -2
- package/lib/pause.js +38 -4
- package/lib/plugin/aiTrace.js +96 -103
- package/lib/plugin/analyze.js +9 -9
- package/lib/plugin/auth.js +3 -3
- package/lib/plugin/browser.js +77 -0
- package/lib/plugin/expose.js +159 -0
- package/lib/plugin/heal.js +47 -3
- package/lib/plugin/junitReporter.js +303 -0
- package/lib/plugin/pageInfo.js +54 -52
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +11 -33
- package/lib/plugin/retryFailedStep.js +15 -13
- package/lib/plugin/screencast.js +289 -0
- package/lib/plugin/screenshot.js +558 -0
- package/lib/plugin/screenshotOnFail.js +9 -170
- package/lib/plugin/stepTimeout.js +3 -2
- package/lib/recorder.js +1 -1
- package/lib/rerun.js +2 -1
- package/lib/result.js +2 -1
- package/lib/step/base.js +10 -9
- package/lib/step/comment.js +2 -2
- package/lib/step/config.js +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/mask_data.js +2 -1
- package/lib/utils/pluginParser.js +151 -0
- package/lib/utils/trace.js +297 -0
- package/lib/utils.js +29 -3
- package/lib/workers.js +14 -22
- package/package.json +17 -14
- package/typings/index.d.ts +19 -5
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -16
- package/docs/webapi/attachFile.mustache +0 -24
- package/docs/webapi/blur.mustache +0 -18
- package/docs/webapi/checkOption.mustache +0 -13
- package/docs/webapi/clearCookie.mustache +0 -9
- package/docs/webapi/clearField.mustache +0 -14
- package/docs/webapi/click.mustache +0 -29
- package/docs/webapi/clickLink.mustache +0 -8
- package/docs/webapi/closeCurrentTab.mustache +0 -7
- package/docs/webapi/closeOtherTabs.mustache +0 -8
- package/docs/webapi/dontSee.mustache +0 -11
- package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/dontSeeCookie.mustache +0 -8
- package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
- package/docs/webapi/dontSeeElement.mustache +0 -12
- package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
- package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
- package/docs/webapi/dontSeeInField.mustache +0 -16
- package/docs/webapi/dontSeeInSource.mustache +0 -8
- package/docs/webapi/dontSeeInTitle.mustache +0 -8
- package/docs/webapi/dontSeeTraffic.mustache +0 -13
- package/docs/webapi/doubleClick.mustache +0 -13
- package/docs/webapi/downloadFile.mustache +0 -12
- package/docs/webapi/dragAndDrop.mustache +0 -9
- package/docs/webapi/dragSlider.mustache +0 -11
- package/docs/webapi/executeAsyncScript.mustache +0 -24
- package/docs/webapi/executeScript.mustache +0 -26
- package/docs/webapi/fillField.mustache +0 -21
- package/docs/webapi/flushNetworkTraffics.mustache +0 -5
- package/docs/webapi/focus.mustache +0 -13
- package/docs/webapi/forceClick.mustache +0 -28
- package/docs/webapi/forceRightClick.mustache +0 -18
- package/docs/webapi/grabAllWindowHandles.mustache +0 -7
- package/docs/webapi/grabAttributeFrom.mustache +0 -10
- package/docs/webapi/grabAttributeFromAll.mustache +0 -9
- package/docs/webapi/grabBrowserLogs.mustache +0 -9
- package/docs/webapi/grabCookie.mustache +0 -11
- package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
- package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
- package/docs/webapi/grabCurrentUrl.mustache +0 -9
- package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
- package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
- package/docs/webapi/grabElementBoundingRect.mustache +0 -20
- package/docs/webapi/grabGeoLocation.mustache +0 -8
- package/docs/webapi/grabHTMLFrom.mustache +0 -10
- package/docs/webapi/grabHTMLFromAll.mustache +0 -9
- package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
- package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
- package/docs/webapi/grabPageScrollPosition.mustache +0 -8
- package/docs/webapi/grabPopupText.mustache +0 -5
- package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
- package/docs/webapi/grabSource.mustache +0 -8
- package/docs/webapi/grabTextFrom.mustache +0 -10
- package/docs/webapi/grabTextFromAll.mustache +0 -9
- package/docs/webapi/grabTitle.mustache +0 -8
- package/docs/webapi/grabValueFrom.mustache +0 -9
- package/docs/webapi/grabValueFromAll.mustache +0 -8
- package/docs/webapi/grabWebElement.mustache +0 -9
- package/docs/webapi/grabWebElements.mustache +0 -9
- package/docs/webapi/moveCursorTo.mustache +0 -16
- package/docs/webapi/openNewTab.mustache +0 -7
- package/docs/webapi/pressKey.mustache +0 -12
- package/docs/webapi/pressKeyDown.mustache +0 -12
- package/docs/webapi/pressKeyUp.mustache +0 -12
- package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
- package/docs/webapi/refreshPage.mustache +0 -6
- package/docs/webapi/resizeWindow.mustache +0 -6
- package/docs/webapi/rightClick.mustache +0 -14
- package/docs/webapi/saveElementScreenshot.mustache +0 -10
- package/docs/webapi/saveScreenshot.mustache +0 -12
- package/docs/webapi/say.mustache +0 -10
- package/docs/webapi/scrollIntoView.mustache +0 -11
- package/docs/webapi/scrollPageToBottom.mustache +0 -6
- package/docs/webapi/scrollPageToTop.mustache +0 -6
- package/docs/webapi/scrollTo.mustache +0 -12
- package/docs/webapi/see.mustache +0 -11
- package/docs/webapi/seeAttributesOnElements.mustache +0 -9
- package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/seeCookie.mustache +0 -8
- package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
- package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
- package/docs/webapi/seeElement.mustache +0 -12
- package/docs/webapi/seeElementInDOM.mustache +0 -8
- package/docs/webapi/seeInCurrentUrl.mustache +0 -8
- package/docs/webapi/seeInField.mustache +0 -17
- package/docs/webapi/seeInPopup.mustache +0 -8
- package/docs/webapi/seeInSource.mustache +0 -7
- package/docs/webapi/seeInTitle.mustache +0 -8
- package/docs/webapi/seeNumberOfElements.mustache +0 -11
- package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/seeTextEquals.mustache +0 -9
- package/docs/webapi/seeTitleEquals.mustache +0 -8
- package/docs/webapi/seeTraffic.mustache +0 -36
- package/docs/webapi/selectOption.mustache +0 -26
- package/docs/webapi/setCookie.mustache +0 -16
- package/docs/webapi/setGeoLocation.mustache +0 -12
- package/docs/webapi/startRecordingTraffic.mustache +0 -8
- package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
- package/docs/webapi/stopRecordingTraffic.mustache +0 -5
- package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
- package/docs/webapi/switchTo.mustache +0 -9
- package/docs/webapi/switchToNextTab.mustache +0 -10
- package/docs/webapi/switchToPreviousTab.mustache +0 -10
- package/docs/webapi/type.mustache +0 -21
- package/docs/webapi/uncheckOption.mustache +0 -13
- package/docs/webapi/wait.mustache +0 -8
- package/docs/webapi/waitForClickable.mustache +0 -11
- package/docs/webapi/waitForCookie.mustache +0 -9
- package/docs/webapi/waitForDetached.mustache +0 -10
- package/docs/webapi/waitForDisabled.mustache +0 -6
- package/docs/webapi/waitForElement.mustache +0 -11
- package/docs/webapi/waitForEnabled.mustache +0 -6
- package/docs/webapi/waitForFunction.mustache +0 -17
- package/docs/webapi/waitForInvisible.mustache +0 -10
- package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
- package/docs/webapi/waitForText.mustache +0 -13
- package/docs/webapi/waitForValue.mustache +0 -10
- package/docs/webapi/waitForVisible.mustache +0 -10
- package/docs/webapi/waitInUrl.mustache +0 -9
- package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/waitToHide.mustache +0 -10
- package/docs/webapi/waitUrlEquals.mustache +0 -10
- package/lib/helper/AI.js +0 -214
- package/lib/helper/Mochawesome.js +0 -96
- package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
- package/lib/helper/extras/React.js +0 -65
- package/lib/plugin/stepByStepReport.js +0 -431
- package/lib/plugin/subtitles.js +0 -89
package/bin/mcp-server.js
CHANGED
|
@@ -1,18 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
2
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
3
4
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
4
5
|
import Codecept from '../lib/codecept.js'
|
|
5
6
|
import container from '../lib/container.js'
|
|
6
7
|
import { getParamsToString } from '../lib/parser.js'
|
|
7
|
-
import { methodsOfObject } from '../lib/utils.js'
|
|
8
|
+
import { methodsOfObject, safeStringify, truncateString } from '../lib/utils.js'
|
|
9
|
+
import {
|
|
10
|
+
captureSnapshot,
|
|
11
|
+
pickActingHelper,
|
|
12
|
+
traceDirFor,
|
|
13
|
+
snapshotDirFor,
|
|
14
|
+
artifactsToFileUrls,
|
|
15
|
+
writeTraceMarkdown,
|
|
16
|
+
TraceReader,
|
|
17
|
+
ariaDiff,
|
|
18
|
+
} from '../lib/utils/trace.js'
|
|
8
19
|
import event from '../lib/event.js'
|
|
9
|
-
import
|
|
20
|
+
import recorder from '../lib/recorder.js'
|
|
21
|
+
import WebElement from '../lib/element/WebElement.js'
|
|
22
|
+
import { locate, within, session, secret, inject, pause } from '../lib/index.js'
|
|
23
|
+
import { tryTo, retryTo, hopeThat } from '../lib/effects.js'
|
|
24
|
+
import step from '../lib/steps.js'
|
|
25
|
+
import { element, eachElement, expectElement, expectAnyElement, expectAllElements } from '../lib/els.js'
|
|
26
|
+
import { setPauseHandler, pauseNow } from '../lib/pause.js'
|
|
27
|
+
import { EventEmitter } from 'events'
|
|
28
|
+
import { fileURLToPath, pathToFileURL } from 'url'
|
|
10
29
|
import { dirname, resolve as resolvePath } from 'path'
|
|
11
30
|
import path from 'path'
|
|
12
|
-
import crypto from 'crypto'
|
|
13
31
|
import { spawn } from 'child_process'
|
|
14
32
|
import { createRequire } from 'module'
|
|
15
33
|
import { existsSync, readdirSync } from 'fs'
|
|
34
|
+
import { mkdirp } from 'mkdirp'
|
|
16
35
|
|
|
17
36
|
const require = createRequire(import.meta.url)
|
|
18
37
|
|
|
@@ -22,6 +41,127 @@ const __dirname = dirname(__filename)
|
|
|
22
41
|
let codecept = null
|
|
23
42
|
let containerInitialized = false
|
|
24
43
|
let browserStarted = false
|
|
44
|
+
let shellSessionActive = false
|
|
45
|
+
let bootstrapDone = false
|
|
46
|
+
let currentPluginsSig = ''
|
|
47
|
+
let currentAiTraceDir = null // mirrors the dir aiTrace plugin computes per test/session
|
|
48
|
+
let aiTraceEnabled = false // tracked across the session so tool responses can surface a hint when off
|
|
49
|
+
|
|
50
|
+
event.dispatcher.on(event.test.before, test => {
|
|
51
|
+
try {
|
|
52
|
+
const title = (test && (test.fullTitle ? test.fullTitle() : test.title)) || 'MCP Session'
|
|
53
|
+
currentAiTraceDir = traceDirFor(test?.file, title, outputBaseDir())
|
|
54
|
+
} catch {}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
function aiTraceHint() {
|
|
58
|
+
if (aiTraceEnabled) return undefined
|
|
59
|
+
return 'aiTrace plugin is disabled — re-run start_browser with plugins={ aiTrace: { enabled: true } } to capture per-step DOM/ARIA/console traces for debugging.'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function applyMochaGrep(grep) {
|
|
63
|
+
if (grep) container.mocha().grep(grep)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function pauseAtMatcher(pauseAt) {
|
|
67
|
+
if (pauseAt == null) return () => false
|
|
68
|
+
if (typeof pauseAt === 'number') return (idx) => idx === pauseAt
|
|
69
|
+
if (typeof pauseAt === 'string') {
|
|
70
|
+
const m = pauseAt.match(/^\/(.+)\/([gimsuy]*)$/)
|
|
71
|
+
const re = m ? new RegExp(m[1], m[2]) : new RegExp(pauseAt.replace(/[.+?^${}()|[\]\\]/g, '\\$&'), 'i')
|
|
72
|
+
return (_idx, name) => re.test(name)
|
|
73
|
+
}
|
|
74
|
+
return () => false
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function ensureBootstrap() {
|
|
78
|
+
if (bootstrapDone) return
|
|
79
|
+
await codecept.bootstrap()
|
|
80
|
+
bootstrapDone = true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function startShellSession() {
|
|
84
|
+
if (shellSessionActive) return
|
|
85
|
+
await ensureBootstrap()
|
|
86
|
+
recorder.start()
|
|
87
|
+
event.emit(event.suite.before, {
|
|
88
|
+
fullTitle: () => 'MCP Session',
|
|
89
|
+
tests: [],
|
|
90
|
+
retries: () => {},
|
|
91
|
+
})
|
|
92
|
+
event.emit(event.test.before, {
|
|
93
|
+
title: 'MCP Session',
|
|
94
|
+
artifacts: {},
|
|
95
|
+
retries: () => {},
|
|
96
|
+
})
|
|
97
|
+
shellSessionActive = true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function endShellSession() {
|
|
101
|
+
if (!shellSessionActive) return
|
|
102
|
+
try { event.emit(event.test.after, {}) } catch {}
|
|
103
|
+
try { event.emit(event.suite.after, {}) } catch {}
|
|
104
|
+
try { event.emit(event.all.result, {}) } catch {}
|
|
105
|
+
shellSessionActive = false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function ensureSession() {
|
|
109
|
+
if (shellSessionActive || pausedController) return
|
|
110
|
+
await startShellSession()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalizePluginOverrides(plugins) {
|
|
114
|
+
if (!plugins || typeof plugins !== 'object') return {}
|
|
115
|
+
const out = {}
|
|
116
|
+
for (const [name, opts] of Object.entries(plugins)) {
|
|
117
|
+
if (opts === false) continue
|
|
118
|
+
out[name] = (opts === true || opts == null) ? {} : opts
|
|
119
|
+
}
|
|
120
|
+
return out
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function applyPluginOverrides(config, plugins) {
|
|
124
|
+
config.plugins = config.plugins || {}
|
|
125
|
+
for (const [name, opts] of Object.entries(plugins)) {
|
|
126
|
+
config.plugins[name] = { ...(config.plugins[name] || {}), ...opts, enabled: true }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function pluginsSignature(plugins) {
|
|
131
|
+
const keys = Object.keys(plugins).sort()
|
|
132
|
+
return JSON.stringify(keys.map(k => [k, plugins[k]]))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function teardownContainer() {
|
|
136
|
+
if (!containerInitialized) return
|
|
137
|
+
try {
|
|
138
|
+
await closeBrowser()
|
|
139
|
+
try { if (codecept?.teardown) await codecept.teardown() } catch {}
|
|
140
|
+
} finally {
|
|
141
|
+
containerInitialized = false
|
|
142
|
+
browserStarted = false
|
|
143
|
+
bootstrapDone = false
|
|
144
|
+
aiTraceEnabled = false
|
|
145
|
+
codecept = null
|
|
146
|
+
currentPluginsSig = ''
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let shutdownStarted = false
|
|
151
|
+
function installShutdownHooks() {
|
|
152
|
+
const onSignal = (signal) => {
|
|
153
|
+
if (shutdownStarted) return
|
|
154
|
+
shutdownStarted = true
|
|
155
|
+
teardownContainer().finally(() => process.exit(signal === 'SIGINT' ? 130 : 0))
|
|
156
|
+
}
|
|
157
|
+
process.on('SIGTERM', () => onSignal('SIGTERM'))
|
|
158
|
+
process.on('SIGINT', () => onSignal('SIGINT'))
|
|
159
|
+
process.on('beforeExit', () => {
|
|
160
|
+
if (shutdownStarted) return
|
|
161
|
+
shutdownStarted = true
|
|
162
|
+
teardownContainer().catch(() => {})
|
|
163
|
+
})
|
|
164
|
+
}
|
|
25
165
|
|
|
26
166
|
let runLock = Promise.resolve()
|
|
27
167
|
async function withLock(fn) {
|
|
@@ -223,19 +363,155 @@ async function resolveTestToFile({ cli, root, configPath, test }) {
|
|
|
223
363
|
return fsFound ? normalizePath(fsFound) : null
|
|
224
364
|
}
|
|
225
365
|
|
|
226
|
-
function
|
|
227
|
-
return
|
|
366
|
+
function outputBaseDir() {
|
|
367
|
+
return global.output_dir || resolvePath(process.cwd(), 'output')
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// In-process pause coordination. When a test running through run_test calls
|
|
371
|
+
// pause(), the handler registered via setPauseHandler resolves a "paused"
|
|
372
|
+
// promise that run_test is racing against test completion. The "pause" tool
|
|
373
|
+
// then drives the REPL by mutating next/abort and resolving the controller.
|
|
374
|
+
let pausedController = null
|
|
375
|
+
let pendingRunPromise = null
|
|
376
|
+
let pendingRunResults = null
|
|
377
|
+
let pendingRunCleanup = null
|
|
378
|
+
let pendingTestFile = null
|
|
379
|
+
let pendingStepInfo = null
|
|
380
|
+
let abortRun = false
|
|
381
|
+
const pauseEvents = new EventEmitter()
|
|
382
|
+
|
|
383
|
+
setPauseHandler(({ registeredVariables }) => {
|
|
384
|
+
if (abortRun) return Promise.reject(new Error('MCP session aborted'))
|
|
385
|
+
return new Promise(resolve => {
|
|
386
|
+
pausedController = {
|
|
387
|
+
registeredVariables,
|
|
388
|
+
resolveContinue: () => {
|
|
389
|
+
pausedController = null
|
|
390
|
+
resolve()
|
|
391
|
+
},
|
|
392
|
+
}
|
|
393
|
+
pauseEvents.emit('paused')
|
|
394
|
+
})
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
async function cancelRun() {
|
|
398
|
+
if (!pendingRunPromise && !pausedController) return false
|
|
399
|
+
abortRun = true
|
|
400
|
+
if (typeof pendingRunCleanup === 'function') { try { pendingRunCleanup() } catch {} }
|
|
401
|
+
if (pausedController) { try { pausedController.resolveContinue() } catch {} ; pausedController = null }
|
|
402
|
+
|
|
403
|
+
try { container.mocha().runner?.abort() } catch {}
|
|
404
|
+
|
|
405
|
+
if (pendingRunPromise) {
|
|
406
|
+
try { await pendingRunPromise.catch(() => {}) } catch {}
|
|
407
|
+
}
|
|
408
|
+
pendingRunPromise = null
|
|
409
|
+
pendingRunResults = null
|
|
410
|
+
pendingTestFile = null
|
|
411
|
+
pendingStepInfo = null
|
|
412
|
+
return true
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function waitForTestResult(runPromise, timeout) {
|
|
416
|
+
const pausedPromise = new Promise(resolve => pauseEvents.once('paused', () => resolve('paused')))
|
|
417
|
+
const completedPromise = runPromise.then(() => 'completed', () => 'completed')
|
|
418
|
+
let timeoutId
|
|
419
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
420
|
+
timeoutId = setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout)
|
|
421
|
+
})
|
|
422
|
+
try {
|
|
423
|
+
return { status: await Promise.race([completedPromise, pausedPromise, timeoutPromise]) }
|
|
424
|
+
} catch (err) {
|
|
425
|
+
await cancelRun()
|
|
426
|
+
return { status: 'aborted', error: err.message }
|
|
427
|
+
} finally {
|
|
428
|
+
clearTimeout(timeoutId)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function closeBrowser() {
|
|
433
|
+
if (!containerInitialized) return
|
|
434
|
+
await cancelRun()
|
|
435
|
+
await endShellSession()
|
|
436
|
+
for (const helper of Object.values(container.helpers() || {})) {
|
|
437
|
+
try { if (helper._cleanup) await helper._cleanup() } catch {}
|
|
438
|
+
try { if (helper._finishTest) await helper._finishTest() } catch {}
|
|
439
|
+
}
|
|
440
|
+
browserStarted = false
|
|
228
441
|
}
|
|
229
442
|
|
|
230
|
-
function
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
443
|
+
async function captureLiveArtifacts(prefix = 'pause') {
|
|
444
|
+
const helper = pickActingHelper(container.helpers())
|
|
445
|
+
if (!helper) return {}
|
|
446
|
+
const dir = snapshotDirFor(outputBaseDir())
|
|
447
|
+
mkdirp.sync(dir)
|
|
448
|
+
const captured = await captureSnapshot(helper, { dir, prefix })
|
|
449
|
+
return artifactsToFileUrls(captured, dir)
|
|
235
450
|
}
|
|
236
451
|
|
|
237
|
-
async function
|
|
238
|
-
|
|
452
|
+
async function gatherPageBrief() {
|
|
453
|
+
const helper = pickActingHelper(container.helpers())
|
|
454
|
+
if (!helper) return {}
|
|
455
|
+
const out = {}
|
|
456
|
+
try { if (helper.grabCurrentUrl) out.url = await helper.grabCurrentUrl() } catch {}
|
|
457
|
+
try { if (helper.grabTitle) out.title = await helper.grabTitle() } catch {}
|
|
458
|
+
try {
|
|
459
|
+
if (helper.grabSource) {
|
|
460
|
+
const html = await helper.grabSource()
|
|
461
|
+
out.contentSize = typeof html === 'string' ? html.length : null
|
|
462
|
+
}
|
|
463
|
+
} catch {}
|
|
464
|
+
return out
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function collectRunCompletion(errorMessage) {
|
|
468
|
+
const results = pendingRunResults || []
|
|
469
|
+
const stats = {
|
|
470
|
+
tests: results.length,
|
|
471
|
+
passes: results.filter(r => r.status === 'passed').length,
|
|
472
|
+
failures: results.filter(r => r.status === 'failed').length,
|
|
473
|
+
}
|
|
474
|
+
if (typeof pendingRunCleanup === 'function') pendingRunCleanup()
|
|
475
|
+
pendingRunPromise = null
|
|
476
|
+
pendingRunResults = null
|
|
477
|
+
pendingTestFile = null
|
|
478
|
+
pendingStepInfo = null
|
|
479
|
+
let error = errorMessage || null
|
|
480
|
+
if (!error && results.length === 0) {
|
|
481
|
+
error = 'No tests ran and no error was reported. The Mocha instance may have been disposed (set mocha.cleanReferencesAfterRun=false in config) or the test file matched no scenarios.'
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
status: error ? 'failed' : 'completed',
|
|
485
|
+
aiTraceDir: currentAiTraceDir,
|
|
486
|
+
reporterJson: { stats, tests: results },
|
|
487
|
+
error,
|
|
488
|
+
aiTraceHint: aiTraceHint(),
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function pausedPayload() {
|
|
493
|
+
return {
|
|
494
|
+
status: 'paused',
|
|
495
|
+
file: pendingTestFile,
|
|
496
|
+
aiTraceDir: currentAiTraceDir,
|
|
497
|
+
pausedAfter: pendingStepInfo,
|
|
498
|
+
suggestions: [
|
|
499
|
+
'Call snapshot to capture URL/HTML/ARIA/screenshot/console/storage at this point',
|
|
500
|
+
'Call run_code to inspect or manipulate state (e.g. return await I.grabText("h1"))',
|
|
501
|
+
'Call continue to release the pause and let the test run the next step (or finish)',
|
|
502
|
+
'Query a saved step snapshot offline: codeceptq <locator> --file <aiTraceDir>/<NNNN>_<step>_page.html',
|
|
503
|
+
],
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function initCodecept(configPath, pluginOverrides) {
|
|
508
|
+
const plugins = normalizePluginOverrides(pluginOverrides)
|
|
509
|
+
const sig = pluginsSignature(plugins)
|
|
510
|
+
|
|
511
|
+
if (containerInitialized) {
|
|
512
|
+
if (!Object.keys(plugins).length || sig === currentPluginsSig) return
|
|
513
|
+
await teardownContainer()
|
|
514
|
+
}
|
|
239
515
|
|
|
240
516
|
const testRoot = process.env.CODECEPTJS_PROJECT_DIR || process.cwd()
|
|
241
517
|
|
|
@@ -260,13 +536,27 @@ async function initCodecept(configPath) {
|
|
|
260
536
|
const { getConfig } = await import('../lib/command/utils.js')
|
|
261
537
|
const config = await getConfig(configPath)
|
|
262
538
|
|
|
539
|
+
// aiTrace is the canonical per-step ARIA/HTML/screenshot capture for MCP.
|
|
540
|
+
// Always on so run_code / continue can read the latest snapshot from disk
|
|
541
|
+
// instead of double-capturing through grabAriaSnapshot etc.
|
|
542
|
+
applyPluginOverrides(config, { aiTrace: { on: 'step' }, browser: { show: false }, ...plugins })
|
|
543
|
+
|
|
263
544
|
codecept = new Codecept(config, {})
|
|
264
545
|
await codecept.init(testRoot)
|
|
265
|
-
await container.create(config, {})
|
|
266
546
|
await container.started()
|
|
267
547
|
|
|
268
548
|
containerInitialized = true
|
|
269
549
|
browserStarted = true
|
|
550
|
+
aiTraceEnabled = config.plugins?.aiTrace?.enabled === true
|
|
551
|
+
currentPluginsSig = sig
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function formatReturnValue(value) {
|
|
555
|
+
if (value instanceof WebElement) return await value.describe()
|
|
556
|
+
if (Array.isArray(value) && value.length && value.every(v => v instanceof WebElement)) {
|
|
557
|
+
return await Promise.all(value.map(v => v.describe()))
|
|
558
|
+
}
|
|
559
|
+
return value
|
|
270
560
|
}
|
|
271
561
|
|
|
272
562
|
const server = new Server(
|
|
@@ -274,66 +564,124 @@ const server = new Server(
|
|
|
274
564
|
{ capabilities: { tools: {} } }
|
|
275
565
|
)
|
|
276
566
|
|
|
567
|
+
const PLUGINS_PROP = {
|
|
568
|
+
type: 'object',
|
|
569
|
+
description: 'Plugin configs to enable for this session, keyed by plugin name. Same shape as `plugins` in codecept.conf.js — each value is the plugin\'s config object (`enabled: true` is added automatically). Common entries:\n' +
|
|
570
|
+
' • { browser: { show: true } } — visible browser (headed)\n' +
|
|
571
|
+
' • { browser: { show: false } } — headless\n' +
|
|
572
|
+
' • { browser: { browser: "firefox", windowSize: "1280x720" } } — switch browser + viewport\n' +
|
|
573
|
+
' • { pause: { on: "fail" } } / { screenshot: { on: "step" } } / { aiTrace: {} }\n' +
|
|
574
|
+
'Override or add to whatever the project config already enables.',
|
|
575
|
+
additionalProperties: { type: 'object' },
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const CONFIG_PROP = {
|
|
579
|
+
type: 'string',
|
|
580
|
+
description: 'Path to codecept.conf.js (or .cjs). Defaults to $CODECEPTJS_CONFIG, then ./codecept.conf.js in $CODECEPTJS_PROJECT_DIR or cwd. Only needed for projects with a non-standard config location.',
|
|
581
|
+
}
|
|
582
|
+
|
|
277
583
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
278
584
|
tools: [
|
|
279
585
|
{
|
|
280
586
|
name: 'list_tests',
|
|
281
|
-
description: 'List all tests in the CodeceptJS project',
|
|
282
|
-
inputSchema: { type: 'object', properties: {
|
|
587
|
+
description: 'List all tests in the CodeceptJS project. Uses the active session if start_browser was called, otherwise auto-inits with project defaults.',
|
|
588
|
+
inputSchema: { type: 'object', properties: {} },
|
|
283
589
|
},
|
|
284
590
|
{
|
|
285
591
|
name: 'list_actions',
|
|
286
|
-
description: 'List all available CodeceptJS actions (I.* methods)',
|
|
287
|
-
inputSchema: { type: 'object', properties: {
|
|
592
|
+
description: 'List all available CodeceptJS actions (I.* methods). Uses the active session if start_browser was called, otherwise auto-inits with project defaults.',
|
|
593
|
+
inputSchema: { type: 'object', properties: {} },
|
|
288
594
|
},
|
|
289
595
|
{
|
|
290
596
|
name: 'run_code',
|
|
291
|
-
description: 'Run arbitrary CodeceptJS code.',
|
|
597
|
+
description: 'Run arbitrary CodeceptJS code. Response includes `availableObjects` listing every symbol in scope (I, helpers, container, step, tryTo, within, etc.).',
|
|
292
598
|
inputSchema: {
|
|
293
599
|
type: 'object',
|
|
294
600
|
properties: {
|
|
295
601
|
code: { type: 'string' },
|
|
296
602
|
timeout: { type: 'number' },
|
|
297
|
-
config: { type: 'string' },
|
|
298
603
|
saveArtifacts: { type: 'boolean' },
|
|
604
|
+
settleMs: { type: 'number', description: 'Wait N ms after the code finishes before capturing artifacts. Default 300. Set higher (1000+) when actions trigger slow re-renders, or 0 to skip.' },
|
|
299
605
|
},
|
|
300
606
|
required: ['code'],
|
|
301
607
|
},
|
|
302
608
|
},
|
|
303
609
|
{
|
|
304
610
|
name: 'run_test',
|
|
305
|
-
description: 'Run a specific test.',
|
|
611
|
+
description: 'Run a specific test. Returns reporter JSON with one entry per scenario; each entry has a `traceFile` (file:// URL) pointing to the aiTrace markdown for that scenario — Read it on failures to see the failing step\'s DOM/ARIA/screenshot. If aiTrace is disabled the response includes an `aiTraceHint`. If the test calls pause() — or if pauseAt is set and reached — returns early with status "paused" so the agent can inspect via run_code and release with continue. To learn step indices for pauseAt, call run_step_by_step first. Auto-inits with project defaults if no session is active — call start_browser first to customize launch (e.g. plugins={ browser: { show: true } } to watch the run).',
|
|
306
612
|
inputSchema: {
|
|
307
613
|
type: 'object',
|
|
308
614
|
properties: {
|
|
309
615
|
test: { type: 'string' },
|
|
310
616
|
timeout: { type: 'number' },
|
|
311
|
-
|
|
617
|
+
grep: { type: 'string', description: 'Filter scenarios by title (passed to mocha.grep). Mirrors --grep on the CLI.' },
|
|
618
|
+
pauseAt: {
|
|
619
|
+
description: 'Programmatic breakpoint. Either a 1-based step index (number) or a step-name match (string — substring case-insensitive, or `/regex/i` literal). Examples: 5 / "fill field" / "/grab.*url/i".',
|
|
620
|
+
oneOf: [{ type: 'number' }, { type: 'string' }],
|
|
621
|
+
},
|
|
622
|
+
plugins: PLUGINS_PROP,
|
|
312
623
|
},
|
|
313
624
|
required: ['test'],
|
|
314
625
|
},
|
|
315
626
|
},
|
|
316
627
|
{
|
|
317
628
|
name: 'run_step_by_step',
|
|
318
|
-
description: 'Run a test step
|
|
629
|
+
description: 'Run a test interactively, pausing after every step. Returns paused payload after the first step (URL/title/contentSize, last step info, suggestions). Call continue to advance one step (and re-pause), or run_code/snapshot to inspect state. On completion each scenario in `reporterJson.tests[]` has a `traceFile` (file:// URL) for the per-step aiTrace markdown — Read it for the full execution log. Much more useful when start_browser was called with plugins={ browser: { show: true } } so you can watch what happens between pauses.',
|
|
319
630
|
inputSchema: {
|
|
320
631
|
type: 'object',
|
|
321
632
|
properties: {
|
|
322
633
|
test: { type: 'string' },
|
|
323
634
|
timeout: { type: 'number' },
|
|
324
|
-
|
|
635
|
+
grep: { type: 'string', description: 'Filter scenarios by title (passed to mocha.grep). Mirrors --grep on the CLI.' },
|
|
636
|
+
plugins: PLUGINS_PROP,
|
|
325
637
|
},
|
|
326
638
|
required: ['test'],
|
|
327
639
|
},
|
|
328
640
|
},
|
|
329
641
|
{
|
|
330
642
|
name: 'start_browser',
|
|
331
|
-
description: 'Start the
|
|
332
|
-
|
|
643
|
+
description: 'Start the session — initializes the codeceptjs container, loads helpers, and applies any plugin overrides. This is the only tool that customizes initialization; every other tool either uses the active session or auto-inits with project defaults.\n\n' +
|
|
644
|
+
'MCP enforces two plugin defaults so the agent gets useful telemetry:\n' +
|
|
645
|
+
' • aiTrace: { on: "step", enabled: true } — per-step DOM/ARIA/console/screenshot traces for debugging\n' +
|
|
646
|
+
' • browser: { show: false, enabled: true } — headless by default\n' +
|
|
647
|
+
'Both can be overridden via the `plugins` arg. To watch the run live: plugins={ browser: { show: true } }. To skip per-step trace overhead on a re-run: plugins={ aiTrace: { enabled: false } } (or { on: "fail" } to only capture failures). To switch config or plugins mid-session, call stop_browser first.',
|
|
648
|
+
inputSchema: {
|
|
649
|
+
type: 'object',
|
|
650
|
+
properties: {
|
|
651
|
+
config: CONFIG_PROP,
|
|
652
|
+
plugins: PLUGINS_PROP,
|
|
653
|
+
},
|
|
654
|
+
},
|
|
333
655
|
},
|
|
334
656
|
{
|
|
335
657
|
name: 'stop_browser',
|
|
336
|
-
description: 'Stop the
|
|
658
|
+
description: 'Stop the session, close browsers, and tear down the container. Required before re-initing with different config or plugins.',
|
|
659
|
+
inputSchema: { type: 'object', properties: {} },
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
name: 'snapshot',
|
|
663
|
+
description: 'Capture current browser state (HTML, ARIA, screenshot, console, URL) without performing any action. Returns `traceFile` (file:// URL) to a markdown trace bundling the captured artifacts — Read it for full context. Auto-inits with project defaults if no session is active.',
|
|
664
|
+
inputSchema: {
|
|
665
|
+
type: 'object',
|
|
666
|
+
properties: {
|
|
667
|
+
fullPage: { type: 'boolean' },
|
|
668
|
+
settleMs: { type: 'number', description: 'Wait N ms before capturing. Default 300. Set higher when the previous action is still re-rendering, or 0 to skip.' },
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
name: 'continue',
|
|
674
|
+
description: 'Release a paused test (one that called pause() during run_test) and let it run to completion. Returns the final reporter result. Use run_code to inspect or manipulate state while the test is paused — both tools share the same container.',
|
|
675
|
+
inputSchema: {
|
|
676
|
+
type: 'object',
|
|
677
|
+
properties: {
|
|
678
|
+
timeout: { type: 'number' },
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
name: 'cancel',
|
|
684
|
+
description: 'Abort the currently paused or in-progress test run without closing the browser. Use when you want to bail out of a paused test and start something else without going through stop_browser/start_browser. The browser session and Mocha state stay alive.',
|
|
337
685
|
inputSchema: { type: 'object', properties: {} },
|
|
338
686
|
},
|
|
339
687
|
],
|
|
@@ -345,8 +693,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
345
693
|
try {
|
|
346
694
|
switch (name) {
|
|
347
695
|
case 'list_tests': {
|
|
348
|
-
|
|
349
|
-
await initCodecept(configPath)
|
|
696
|
+
await initCodecept()
|
|
350
697
|
|
|
351
698
|
codecept.loadTests()
|
|
352
699
|
const tests = codecept.testFiles.map(testFile => {
|
|
@@ -361,8 +708,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
361
708
|
}
|
|
362
709
|
|
|
363
710
|
case 'list_actions': {
|
|
364
|
-
|
|
365
|
-
await initCodecept(configPath)
|
|
711
|
+
await initCodecept()
|
|
366
712
|
|
|
367
713
|
const helpers = container.helpers()
|
|
368
714
|
const supportI = container.support('I')
|
|
@@ -390,199 +736,431 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
390
736
|
}
|
|
391
737
|
|
|
392
738
|
case 'start_browser': {
|
|
393
|
-
const configPath = args
|
|
394
|
-
if (browserStarted) {
|
|
395
|
-
return { content: [{ type: 'text', text: JSON.stringify({ status: '
|
|
739
|
+
const { config: configPath, plugins } = args || {}
|
|
740
|
+
if (browserStarted && shellSessionActive) {
|
|
741
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'Session already active', plugins: plugins ?? null }, null, 2) }] }
|
|
396
742
|
}
|
|
397
|
-
await initCodecept(configPath)
|
|
398
|
-
|
|
743
|
+
await initCodecept(configPath, plugins)
|
|
744
|
+
if (containerInitialized && !browserStarted) {
|
|
745
|
+
for (const helper of Object.values(container.helpers() || {})) {
|
|
746
|
+
try { if (helper._beforeSuite) await helper._beforeSuite() } catch {}
|
|
747
|
+
}
|
|
748
|
+
browserStarted = true
|
|
749
|
+
}
|
|
750
|
+
await startShellSession()
|
|
751
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'Session started — run_code and snapshot are now available', plugins: plugins ?? null }, null, 2) }] }
|
|
399
752
|
}
|
|
400
753
|
|
|
401
754
|
case 'stop_browser': {
|
|
402
755
|
if (!containerInitialized) {
|
|
403
756
|
return { content: [{ type: 'text', text: JSON.stringify({ status: 'Browser not initialized' }, null, 2) }] }
|
|
404
757
|
}
|
|
758
|
+
await closeBrowser()
|
|
759
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'Browser stopped — Mocha and config preserved; call start_browser to reopen' }, null, 2) }] }
|
|
760
|
+
}
|
|
405
761
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
762
|
+
case 'snapshot': {
|
|
763
|
+
const { fullPage = false, settleMs = 300 } = args || {}
|
|
764
|
+
await initCodecept()
|
|
765
|
+
await ensureSession()
|
|
766
|
+
|
|
767
|
+
const helper = pickActingHelper(container.helpers())
|
|
768
|
+
if (!helper) throw new Error('No supported acting helper available (Playwright, Puppeteer, WebDriver).')
|
|
769
|
+
|
|
770
|
+
const dir = snapshotDirFor(outputBaseDir())
|
|
771
|
+
mkdirp.sync(dir)
|
|
772
|
+
|
|
773
|
+
if (settleMs > 0) await new Promise(r => setTimeout(r, settleMs))
|
|
774
|
+
const captured = await captureSnapshot(helper, { dir, prefix: 'snapshot', fullPage })
|
|
775
|
+
const traceFile = writeTraceMarkdown({
|
|
776
|
+
dir,
|
|
777
|
+
title: 'snapshot',
|
|
778
|
+
file: 'mcp',
|
|
779
|
+
durationMs: 0,
|
|
780
|
+
commands: [],
|
|
781
|
+
captured,
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
return {
|
|
785
|
+
content: [{
|
|
786
|
+
type: 'text',
|
|
787
|
+
text: JSON.stringify({
|
|
788
|
+
status: 'success',
|
|
789
|
+
dir,
|
|
790
|
+
traceFile: pathToFileURL(traceFile).href,
|
|
791
|
+
artifacts: artifactsToFileUrls(captured, dir),
|
|
792
|
+
aiTraceHint: aiTraceHint(),
|
|
793
|
+
}, null, 2),
|
|
794
|
+
}],
|
|
410
795
|
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
case 'continue': {
|
|
799
|
+
if (!pausedController) throw new Error('No paused test. Run a test first via run_test or run_step_by_step; this tool becomes available if the test pauses.')
|
|
800
|
+
const { timeout = 60000 } = args || {}
|
|
801
|
+
return await withSilencedIO(async () => {
|
|
802
|
+
pausedController.resolveContinue()
|
|
803
|
+
if (!pendingRunPromise) {
|
|
804
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'continued' }, null, 2) }] }
|
|
805
|
+
}
|
|
411
806
|
|
|
412
|
-
|
|
413
|
-
|
|
807
|
+
// Race: test pauses again (step-by-step or another pause()) vs test finishes.
|
|
808
|
+
const pausedAgain = new Promise(resolve => pauseEvents.once('paused', () => resolve('paused')))
|
|
809
|
+
const completed = pendingRunPromise.then(() => 'completed', () => 'completed')
|
|
810
|
+
const which = await Promise.race([
|
|
811
|
+
pausedAgain,
|
|
812
|
+
completed,
|
|
813
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout)),
|
|
814
|
+
])
|
|
815
|
+
|
|
816
|
+
if (which === 'paused') {
|
|
817
|
+
const page = await gatherPageBrief()
|
|
818
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ...pausedPayload(), page }, null, 2) }] }
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
let runError = null
|
|
822
|
+
try { await pendingRunPromise } catch (err) { runError = err }
|
|
823
|
+
const file = pendingTestFile
|
|
824
|
+
const final = collectRunCompletion(runError?.message)
|
|
825
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ...final, file }, null, 2) }] }
|
|
826
|
+
})
|
|
827
|
+
}
|
|
414
828
|
|
|
415
|
-
|
|
829
|
+
case 'cancel': {
|
|
830
|
+
const cancelled = await cancelRun()
|
|
831
|
+
await ensureSession()
|
|
832
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: cancelled ? 'Run cancelled — browser kept open' : 'No run in progress' }, null, 2) }] }
|
|
416
833
|
}
|
|
417
834
|
|
|
418
835
|
case 'run_code': {
|
|
419
|
-
const { code, timeout = 60000,
|
|
420
|
-
await initCodecept(
|
|
836
|
+
const { code, timeout = 60000, saveArtifacts = true, settleMs = 300 } = args
|
|
837
|
+
await initCodecept()
|
|
838
|
+
await ensureSession()
|
|
839
|
+
|
|
840
|
+
const support = container.supportObjects() || {}
|
|
841
|
+
if (!support.I) throw new Error('I object not available. Make sure helpers are configured.')
|
|
421
842
|
|
|
422
|
-
const
|
|
423
|
-
if (!I) throw new Error('I object not available. Make sure helpers are configured.')
|
|
843
|
+
const result = { status: 'unknown', output: '', error: null, commands: [], artifacts: {} }
|
|
424
844
|
|
|
425
|
-
const
|
|
845
|
+
const commands = []
|
|
846
|
+
let lastStepValue
|
|
847
|
+
const onStepAfter = step => {
|
|
848
|
+
try { commands.push(step.toString()) } catch {}
|
|
849
|
+
}
|
|
850
|
+
const onStepPassed = (step, val) => {
|
|
851
|
+
if (val !== undefined) lastStepValue = val
|
|
852
|
+
}
|
|
853
|
+
event.dispatcher.on(event.step.after, onStepAfter)
|
|
854
|
+
event.dispatcher.on(event.step.passed, onStepPassed)
|
|
855
|
+
|
|
856
|
+
const traceDir = traceDirFor(`mcp_${Date.now()}`, 'run_code', outputBaseDir())
|
|
857
|
+
mkdirp.sync(traceDir)
|
|
858
|
+
const startedAt = Date.now()
|
|
859
|
+
|
|
860
|
+
// Pin the latest aiTrace ARIA file before running the code, so we
|
|
861
|
+
// can diff after. aiTrace owns per-step capture; we just read it.
|
|
862
|
+
const reader = new TraceReader(currentAiTraceDir)
|
|
863
|
+
const ariaBefore = reader.last('aria')
|
|
864
|
+
|
|
865
|
+
const MAX_LOG_ENTRIES = 100
|
|
866
|
+
const MAX_LOG_MSG_BYTES = 2000
|
|
867
|
+
const MAX_RETURN_BYTES = 20000
|
|
868
|
+
const consoleLogs = []
|
|
869
|
+
const consoleMethods = ['log', 'info', 'warn', 'error', 'debug']
|
|
870
|
+
const origConsoleMethods = {}
|
|
871
|
+
const captureLog = level => (...args) => {
|
|
872
|
+
if (consoleLogs.length >= MAX_LOG_ENTRIES) return
|
|
873
|
+
const message = args.map(a => {
|
|
874
|
+
if (typeof a === 'string') return a
|
|
875
|
+
return truncateString(safeStringify(a, [], 2), MAX_LOG_MSG_BYTES).value
|
|
876
|
+
}).join(' ')
|
|
877
|
+
consoleLogs.push({ level, message, t: Date.now() - startedAt })
|
|
878
|
+
}
|
|
879
|
+
for (const m of consoleMethods) {
|
|
880
|
+
origConsoleMethods[m] = console[m]
|
|
881
|
+
console[m] = captureLog(m)
|
|
882
|
+
}
|
|
426
883
|
|
|
884
|
+
const scope = {
|
|
885
|
+
locate, within, session, secret, inject, pause, share: container.share,
|
|
886
|
+
tryTo, retryTo, hopeThat,
|
|
887
|
+
step, element, eachElement, expectElement, expectAnyElement, expectAllElements,
|
|
888
|
+
container, helpers: container.helpers(),
|
|
889
|
+
...support,
|
|
890
|
+
}
|
|
891
|
+
const paramNames = ['I', ...Object.keys(scope).filter(k => k !== 'I').sort()]
|
|
892
|
+
const paramValues = paramNames.map(k => scope[k])
|
|
893
|
+
|
|
894
|
+
const wasPaused = !!pausedController
|
|
895
|
+
if (wasPaused) recorder.session.start('mcp_run_code')
|
|
896
|
+
|
|
897
|
+
let returnValue
|
|
427
898
|
try {
|
|
428
|
-
const asyncFn = new Function(
|
|
429
|
-
await Promise.race([
|
|
430
|
-
asyncFn(
|
|
899
|
+
const asyncFn = new Function(...paramNames, `return (async () => { ${code} })()`)
|
|
900
|
+
returnValue = await Promise.race([
|
|
901
|
+
asyncFn(...paramValues),
|
|
431
902
|
new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout)),
|
|
432
903
|
])
|
|
904
|
+
await recorder.promise()
|
|
433
905
|
|
|
434
906
|
result.status = 'success'
|
|
435
907
|
result.output = 'Code executed successfully'
|
|
436
|
-
|
|
437
|
-
if (saveArtifacts) {
|
|
438
|
-
const helpers = container.helpers()
|
|
439
|
-
const helper = Object.values(helpers)[0]
|
|
440
|
-
if (helper) {
|
|
441
|
-
try {
|
|
442
|
-
if (helper.grabAriaSnapshot) result.artifacts.aria = await helper.grabAriaSnapshot()
|
|
443
|
-
if (helper.grabCurrentUrl) result.artifacts.url = await helper.grabCurrentUrl()
|
|
444
|
-
if (helper.grabBrowserLogs) result.artifacts.consoleLogs = (await helper.grabBrowserLogs()) || []
|
|
445
|
-
if (helper.grabSource) {
|
|
446
|
-
const html = await helper.grabSource()
|
|
447
|
-
result.artifacts.html = html.substring(0, 10000) + '...'
|
|
448
|
-
}
|
|
449
|
-
} catch (e) {
|
|
450
|
-
result.output += ` (Warning: ${e.message})`
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
908
|
} catch (error) {
|
|
455
909
|
result.status = 'failed'
|
|
456
910
|
result.error = error.message
|
|
457
911
|
result.output = error.stack || error.message
|
|
912
|
+
} finally {
|
|
913
|
+
for (const m of consoleMethods) console[m] = origConsoleMethods[m]
|
|
914
|
+
try { event.dispatcher.removeListener(event.step.after, onStepAfter) } catch {}
|
|
915
|
+
try { event.dispatcher.removeListener(event.step.passed, onStepPassed) } catch {}
|
|
916
|
+
if (wasPaused) {
|
|
917
|
+
try { recorder.session.restore('mcp_run_code') } catch {}
|
|
918
|
+
} else {
|
|
919
|
+
try { recorder.reset() } catch {}
|
|
920
|
+
}
|
|
458
921
|
}
|
|
459
922
|
|
|
923
|
+
result.commands = commands
|
|
924
|
+
result.logs = consoleLogs
|
|
925
|
+
if (consoleLogs.length === MAX_LOG_ENTRIES) result.logsTruncated = true
|
|
926
|
+
result.availableObjects = paramNames
|
|
927
|
+
|
|
928
|
+
if (returnValue === undefined) returnValue = await Promise.resolve(lastStepValue)
|
|
929
|
+
returnValue = await formatReturnValue(returnValue)
|
|
930
|
+
|
|
931
|
+
if (returnValue !== undefined) {
|
|
932
|
+
const json = typeof returnValue === 'string' ? returnValue : safeStringify(returnValue, [], 2)
|
|
933
|
+
const stringified = truncateString(json, MAX_RETURN_BYTES)
|
|
934
|
+
result.returnValue = stringified.value
|
|
935
|
+
if (stringified.truncated) result.returnValueTruncated = true
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
let captured = {}
|
|
939
|
+
if (saveArtifacts) {
|
|
940
|
+
const helper = pickActingHelper(container.helpers())
|
|
941
|
+
if (helper) {
|
|
942
|
+
try {
|
|
943
|
+
if (settleMs > 0) await new Promise(r => setTimeout(r, settleMs))
|
|
944
|
+
captured = await captureSnapshot(helper, { dir: traceDir, prefix: 'mcp' })
|
|
945
|
+
result.artifacts = artifactsToFileUrls(captured, traceDir)
|
|
946
|
+
} catch (e) {
|
|
947
|
+
result.output += ` (Warning: ${e.message})`
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Diff against the latest aiTrace ARIA file produced by the steps
|
|
953
|
+
// that just ran inside this run_code call.
|
|
954
|
+
const ariaAfter = reader.last('aria')
|
|
955
|
+
if (ariaBefore && ariaAfter && ariaBefore !== ariaAfter) {
|
|
956
|
+
const diff = ariaDiff(ariaBefore, ariaAfter)
|
|
957
|
+
if (diff) result.ariaDiff = diff
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const traceFile = writeTraceMarkdown({
|
|
961
|
+
dir: traceDir,
|
|
962
|
+
title: 'run_code',
|
|
963
|
+
file: 'mcp',
|
|
964
|
+
durationMs: Date.now() - startedAt,
|
|
965
|
+
commands,
|
|
966
|
+
captured,
|
|
967
|
+
error: result.error,
|
|
968
|
+
})
|
|
969
|
+
result.dir = traceDir
|
|
970
|
+
result.traceFile = pathToFileURL(traceFile).href
|
|
971
|
+
result.aiTraceHint = aiTraceHint()
|
|
972
|
+
|
|
460
973
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
461
974
|
}
|
|
462
975
|
|
|
463
976
|
case 'run_test': {
|
|
464
977
|
return await withLock(async () => {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
978
|
+
if (pausedController) {
|
|
979
|
+
throw new Error('A previous run_test is still paused. Call "continue" first.')
|
|
980
|
+
}
|
|
981
|
+
const { test, timeout = 60000, pauseAt, grep, plugins } = args || {}
|
|
982
|
+
await initCodecept(undefined, plugins)
|
|
983
|
+
await endShellSession()
|
|
984
|
+
applyMochaGrep(grep)
|
|
985
|
+
|
|
986
|
+
return await withSilencedIO(async () => {
|
|
987
|
+
codecept.loadTests()
|
|
988
|
+
|
|
989
|
+
let testFiles = codecept.testFiles
|
|
990
|
+
if (test) {
|
|
991
|
+
const testName = normalizePath(test).toLowerCase()
|
|
992
|
+
testFiles = codecept.testFiles.filter(f => {
|
|
993
|
+
const filePath = normalizePath(f).toLowerCase()
|
|
994
|
+
return filePath.includes(testName) || filePath.endsWith(testName)
|
|
995
|
+
})
|
|
996
|
+
}
|
|
473
997
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
998
|
+
if (!testFiles.length) throw new Error(`No tests found matching: ${test}`)
|
|
999
|
+
const testFile = testFiles[0]
|
|
1000
|
+
|
|
1001
|
+
pendingRunResults = []
|
|
1002
|
+
pendingTestFile = testFile
|
|
1003
|
+
pendingStepInfo = null
|
|
1004
|
+
let stepIndex = 0
|
|
1005
|
+
const matchPauseAt = pauseAtMatcher(pauseAt)
|
|
1006
|
+
|
|
1007
|
+
const onAfter = t => {
|
|
1008
|
+
const aiTrace = t.artifacts?.aiTrace
|
|
1009
|
+
pendingRunResults.push({
|
|
1010
|
+
title: t.title,
|
|
1011
|
+
file: t.file,
|
|
1012
|
+
status: t.err ? 'failed' : 'passed',
|
|
1013
|
+
error: t.err?.message,
|
|
1014
|
+
duration: t.duration,
|
|
1015
|
+
traceFile: aiTrace ? pathToFileURL(aiTrace).href : null,
|
|
1016
|
+
})
|
|
1017
|
+
}
|
|
1018
|
+
const onStepAfter = step => {
|
|
1019
|
+
stepIndex += 1
|
|
1020
|
+
const idx = stepIndex
|
|
1021
|
+
const name = (() => { try { return step.toString() } catch { return '' } })()
|
|
1022
|
+
recorder.add('mcp pause info', () => {
|
|
1023
|
+
pendingStepInfo = { index: idx, name, status: step.status }
|
|
1024
|
+
})
|
|
1025
|
+
if (matchPauseAt(idx, name)) pauseNow()
|
|
1026
|
+
}
|
|
1027
|
+
event.dispatcher.on(event.test.after, onAfter)
|
|
1028
|
+
event.dispatcher.on(event.step.after, onStepAfter)
|
|
1029
|
+
pendingRunCleanup = () => {
|
|
1030
|
+
try { event.dispatcher.removeListener(event.test.after, onAfter) } catch {}
|
|
1031
|
+
try { event.dispatcher.removeListener(event.step.after, onStepAfter) } catch {}
|
|
1032
|
+
pendingRunCleanup = null
|
|
1033
|
+
}
|
|
477
1034
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
1035
|
+
abortRun = false
|
|
1036
|
+
let runError = null
|
|
1037
|
+
const runPromise = (async () => {
|
|
1038
|
+
try {
|
|
1039
|
+
await ensureBootstrap()
|
|
1040
|
+
await codecept.run(testFile)
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
runError = err
|
|
1043
|
+
throw err
|
|
1044
|
+
}
|
|
1045
|
+
})()
|
|
1046
|
+
pendingRunPromise = runPromise
|
|
481
1047
|
|
|
482
|
-
|
|
1048
|
+
const result = await waitForTestResult(runPromise, timeout)
|
|
1049
|
+
if (result.status === 'aborted') {
|
|
1050
|
+
await startShellSession()
|
|
1051
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'failed', file: testFile, error: result.error }, null, 2) }] }
|
|
1052
|
+
}
|
|
483
1053
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
1054
|
+
if (result.status === 'paused') {
|
|
1055
|
+
const page = await gatherPageBrief()
|
|
1056
|
+
return {
|
|
1057
|
+
content: [{
|
|
1058
|
+
type: 'text',
|
|
1059
|
+
text: JSON.stringify({ ...pausedPayload(), page }, null, 2),
|
|
1060
|
+
}],
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
490
1063
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
reporterJson: parsed,
|
|
497
|
-
stderr: err ? err.slice(0, 20000) : '',
|
|
498
|
-
rawStdout: parsed ? '' : out.slice(0, 20000),
|
|
499
|
-
}, null, 2),
|
|
500
|
-
}],
|
|
501
|
-
}
|
|
1064
|
+
pendingRunPromise = null
|
|
1065
|
+
const final = collectRunCompletion(runError?.message)
|
|
1066
|
+
await startShellSession()
|
|
1067
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ...final, file: testFile }, null, 2) }] }
|
|
1068
|
+
})
|
|
502
1069
|
})
|
|
503
1070
|
}
|
|
504
1071
|
|
|
505
1072
|
case 'run_step_by_step': {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
return await withSilencedIO(async () => {
|
|
510
|
-
codecept.loadTests()
|
|
511
|
-
|
|
512
|
-
let testFiles = codecept.testFiles
|
|
513
|
-
if (test) {
|
|
514
|
-
const testName = normalizePath(test).toLowerCase()
|
|
515
|
-
testFiles = codecept.testFiles.filter(f => {
|
|
516
|
-
const filePath = normalizePath(f).toLowerCase()
|
|
517
|
-
return filePath.includes(testName) || filePath.endsWith(testName)
|
|
518
|
-
})
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if (!testFiles.length) throw new Error(`No tests found matching: ${test}`)
|
|
522
|
-
|
|
523
|
-
const results = []
|
|
524
|
-
const currentSteps = {}
|
|
525
|
-
let currentTestTitle = null
|
|
526
|
-
const testFile = testFiles[0]
|
|
527
|
-
|
|
528
|
-
const onBefore = (t) => {
|
|
529
|
-
const traceDir = getTraceDir(t.title, t.file)
|
|
530
|
-
currentTestTitle = t.title
|
|
531
|
-
currentSteps[t.title] = []
|
|
532
|
-
results.push({
|
|
533
|
-
test: t.title,
|
|
534
|
-
file: t.file,
|
|
535
|
-
traceFile: `file://${resolvePath(traceDir, 'trace.md')}`,
|
|
536
|
-
status: 'running',
|
|
537
|
-
steps: [],
|
|
538
|
-
})
|
|
1073
|
+
return await withLock(async () => {
|
|
1074
|
+
if (pausedController) {
|
|
1075
|
+
throw new Error('A previous run is still paused. Call "continue" first.')
|
|
539
1076
|
}
|
|
1077
|
+
const { test, timeout = 60000, grep, plugins } = args || {}
|
|
1078
|
+
await initCodecept(undefined, plugins)
|
|
1079
|
+
await endShellSession()
|
|
1080
|
+
applyMochaGrep(grep)
|
|
1081
|
+
|
|
1082
|
+
return await withSilencedIO(async () => {
|
|
1083
|
+
codecept.loadTests()
|
|
1084
|
+
|
|
1085
|
+
let testFiles = codecept.testFiles
|
|
1086
|
+
if (test) {
|
|
1087
|
+
const testName = normalizePath(test).toLowerCase()
|
|
1088
|
+
testFiles = codecept.testFiles.filter(f => {
|
|
1089
|
+
const filePath = normalizePath(f).toLowerCase()
|
|
1090
|
+
return filePath.includes(testName) || filePath.endsWith(testName)
|
|
1091
|
+
})
|
|
1092
|
+
}
|
|
540
1093
|
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
1094
|
+
if (!testFiles.length) throw new Error(`No tests found matching: ${test}`)
|
|
1095
|
+
const testFile = testFiles[0]
|
|
1096
|
+
|
|
1097
|
+
pendingRunResults = []
|
|
1098
|
+
pendingTestFile = testFile
|
|
1099
|
+
pendingStepInfo = null
|
|
1100
|
+
let stepIndex = 0
|
|
1101
|
+
|
|
1102
|
+
const onAfter = t => {
|
|
1103
|
+
const aiTrace = t.artifacts?.aiTrace
|
|
1104
|
+
pendingRunResults.push({
|
|
1105
|
+
title: t.title,
|
|
1106
|
+
file: t.file,
|
|
1107
|
+
status: t.err ? 'failed' : 'passed',
|
|
1108
|
+
error: t.err?.message,
|
|
1109
|
+
duration: t.duration,
|
|
1110
|
+
traceFile: aiTrace ? pathToFileURL(aiTrace).href : null,
|
|
1111
|
+
})
|
|
1112
|
+
}
|
|
1113
|
+
const onStepAfter = step => {
|
|
1114
|
+
stepIndex += 1
|
|
1115
|
+
const idx = stepIndex
|
|
1116
|
+
const name = (() => { try { return step.toString() } catch { return '' } })()
|
|
1117
|
+
recorder.add('mcp pause info', () => {
|
|
1118
|
+
pendingStepInfo = { index: idx, name, status: step.status }
|
|
1119
|
+
})
|
|
1120
|
+
pauseNow()
|
|
1121
|
+
}
|
|
1122
|
+
event.dispatcher.on(event.test.after, onAfter)
|
|
1123
|
+
event.dispatcher.on(event.step.after, onStepAfter)
|
|
1124
|
+
pendingRunCleanup = () => {
|
|
1125
|
+
try { event.dispatcher.removeListener(event.test.after, onAfter) } catch {}
|
|
1126
|
+
try { event.dispatcher.removeListener(event.step.after, onStepAfter) } catch {}
|
|
1127
|
+
pendingRunCleanup = null
|
|
546
1128
|
}
|
|
547
|
-
currentTestTitle = null
|
|
548
|
-
}
|
|
549
1129
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
1130
|
+
abortRun = false
|
|
1131
|
+
let runError = null
|
|
1132
|
+
const runPromise = (async () => {
|
|
1133
|
+
try {
|
|
1134
|
+
await ensureBootstrap()
|
|
1135
|
+
await codecept.run(testFile)
|
|
1136
|
+
} catch (err) {
|
|
1137
|
+
runError = err
|
|
1138
|
+
throw err
|
|
1139
|
+
}
|
|
1140
|
+
})()
|
|
1141
|
+
pendingRunPromise = runPromise
|
|
560
1142
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
1143
|
+
const result = await waitForTestResult(runPromise, timeout)
|
|
1144
|
+
if (result.status === 'aborted') {
|
|
1145
|
+
await startShellSession()
|
|
1146
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'failed', file: testFile, error: result.error }, null, 2) }] }
|
|
1147
|
+
}
|
|
564
1148
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
} catch (error) {
|
|
574
|
-
const lastRunning = results.filter(r => r.status === 'running').pop()
|
|
575
|
-
if (lastRunning) {
|
|
576
|
-
lastRunning.status = 'failed'
|
|
577
|
-
lastRunning.error = error.message
|
|
1149
|
+
if (result.status === 'paused') {
|
|
1150
|
+
const page = await gatherPageBrief()
|
|
1151
|
+
return {
|
|
1152
|
+
content: [{
|
|
1153
|
+
type: 'text',
|
|
1154
|
+
text: JSON.stringify({ ...pausedPayload(), page }, null, 2),
|
|
1155
|
+
}],
|
|
1156
|
+
}
|
|
578
1157
|
}
|
|
579
|
-
} finally {
|
|
580
|
-
try { event.dispatcher.removeListener(event.test.before, onBefore) } catch {}
|
|
581
|
-
try { event.dispatcher.removeListener(event.test.after, onAfter) } catch {}
|
|
582
|
-
try { event.dispatcher.removeListener(event.step.after, onStepAfter) } catch {}
|
|
583
|
-
}
|
|
584
1158
|
|
|
585
|
-
|
|
1159
|
+
pendingRunPromise = null
|
|
1160
|
+
const final = collectRunCompletion(runError?.message)
|
|
1161
|
+
await startShellSession()
|
|
1162
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ...final, file: testFile }, null, 2) }] }
|
|
1163
|
+
})
|
|
586
1164
|
})
|
|
587
1165
|
}
|
|
588
1166
|
|
|
@@ -598,6 +1176,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
598
1176
|
})
|
|
599
1177
|
|
|
600
1178
|
async function main() {
|
|
1179
|
+
installShutdownHooks()
|
|
601
1180
|
const transport = new StdioServerTransport()
|
|
602
1181
|
await server.connect(transport)
|
|
603
1182
|
}
|