Skip to content

Commit eaca22d

Browse files
DavertMikclaude
andcommitted
refactor(utils): unify captureSnapshot + trace into lib/utils/trace.js
Both files belonged to the same flow (capture artifacts -> render trace.md). Splitting them across two files made the import sites awkward, so they're now consolidated: - `lib/utils/captureSnapshot.js` deleted; its `captureSnapshot`, `normalizeBrowserLogs`, and `captureStorageState` moved into `lib/utils/trace.js`. - `writeTraceMarkdown` (was inlined in `bin/mcp-server.js`) moved into `lib/utils/trace.js` too — it's the same trace-record concern. - All three call sites (`bin/mcp-server.js`, `lib/plugin/aiTrace.js`, `lib/plugin/pageInfo.js`) now import from a single module. - Stale `writeFileSync` import dropped from `bin/mcp-server.js`. 72 unit tests still passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b7b5a33 commit eaca22d

5 files changed

Lines changed: 170 additions & 161 deletions

File tree

bin/mcp-server.js

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,21 @@ import Codecept from '../lib/codecept.js'
55
import container from '../lib/container.js'
66
import { getParamsToString } from '../lib/parser.js'
77
import { methodsOfObject, safeStringify, truncateString } from '../lib/utils.js'
8-
import { captureSnapshot } from '../lib/utils/captureSnapshot.js'
98
import {
9+
captureSnapshot,
1010
pickActingHelper,
1111
traceDirFor,
1212
snapshotDirFor,
13-
artifactLinks,
1413
artifactsToFileUrls,
14+
writeTraceMarkdown,
1515
} from '../lib/utils/trace.js'
1616
import event from '../lib/event.js'
1717
import { fileURLToPath } from 'url'
1818
import { dirname, resolve as resolvePath } from 'path'
1919
import path from 'path'
2020
import { spawn } from 'child_process'
2121
import { createRequire } from 'module'
22-
import { existsSync, readdirSync, writeFileSync } from 'fs'
22+
import { existsSync, readdirSync } from 'fs'
2323
import { mkdirp } from 'mkdirp'
2424

2525
const require = createRequire(import.meta.url)
@@ -235,30 +235,6 @@ function outputBaseDir() {
235235
return global.output_dir || resolvePath(process.cwd(), 'output')
236236
}
237237

238-
function writeTraceMarkdown({ dir, title, file, durationMs, commands, captured, error }) {
239-
let md = `file: ${file || 'mcp'}\n`
240-
md += `name: ${title}\n`
241-
md += `time: ${(durationMs / 1000).toFixed(2)}s\n`
242-
md += `---\n\n`
243-
244-
if (error) md += `Error: ${error}\n\n---\n\n`
245-
246-
if (commands && commands.length) {
247-
md += `### Commands\n`
248-
for (const c of commands) md += `- ${c}\n`
249-
md += `\n`
250-
}
251-
252-
md += `### Final State\n`
253-
if (captured.url) md += ` > URL: ${captured.url}\n`
254-
const links = artifactLinks(captured)
255-
if (links) md += links + '\n'
256-
257-
const traceFile = path.join(dir, 'trace.md')
258-
writeFileSync(traceFile, md)
259-
return traceFile
260-
}
261-
262238
async function initCodecept(configPath) {
263239
if (containerInitialized) return
264240

lib/plugin/aiTrace.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import recorder from '../recorder.js'
88
import event from '../event.js'
99
import output from '../output.js'
1010
import { deleteDir, clearString } from '../utils.js'
11-
import { captureSnapshot } from '../utils/captureSnapshot.js'
12-
import { pickActingHelper, traceDirFor, artifactLinks } from '../utils/trace.js'
11+
import { captureSnapshot, pickActingHelper, traceDirFor, artifactLinks } from '../utils/trace.js'
1312

1413
const defaultConfig = {
1514
deleteSuccessful: false,

lib/plugin/pageInfo.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import Container from '../container.js'
44
import recorder from '../recorder.js'
55
import event from '../event.js'
66
import { scanForErrorMessages } from '../html.js'
7-
import { captureSnapshot } from '../utils/captureSnapshot.js'
8-
import { pickActingHelper } from '../utils/trace.js'
7+
import { captureSnapshot, pickActingHelper } from '../utils/trace.js'
98
import { output } from '../index.js'
109
import store from '../store.js'
1110
import { humanizeString, ucfirst } from '../utils.js'

lib/utils/captureSnapshot.js

Lines changed: 0 additions & 130 deletions
This file was deleted.

lib/utils/trace.js

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import crypto from 'crypto'
2+
import fs from 'fs'
23
import path from 'path'
34
import Container from '../container.js'
45
import { clearString } from '../utils.js'
6+
import { formatHtml } from '../html.js'
7+
8+
// ---------------------------------------------------------------------------
9+
// Helper / directory naming
10+
// ---------------------------------------------------------------------------
511

612
export function pickActingHelper(helpers) {
713
for (const name of Container.STANDARD_ACTING_HELPERS) {
@@ -21,6 +27,10 @@ export function snapshotDirFor(baseDir) {
2127
return path.resolve(baseDir, `snapshot_${Date.now()}_${hash}`)
2228
}
2329

30+
// ---------------------------------------------------------------------------
31+
// Artifact link rendering (trace.md)
32+
// ---------------------------------------------------------------------------
33+
2434
const ARTIFACT_LABELS = {
2535
html: 'HTML',
2636
aria: 'ARIA',
@@ -56,6 +66,30 @@ export function fileToUrl(dir, basename) {
5666
return `file://${path.join(dir, basename)}`
5767
}
5868

69+
export function writeTraceMarkdown({ dir, title, file, durationMs, commands, captured, error }) {
70+
let md = `file: ${file || 'mcp'}\n`
71+
md += `name: ${title}\n`
72+
md += `time: ${(durationMs / 1000).toFixed(2)}s\n`
73+
md += `---\n\n`
74+
75+
if (error) md += `Error: ${error}\n\n---\n\n`
76+
77+
if (commands && commands.length) {
78+
md += `### Commands\n`
79+
for (const c of commands) md += `- ${c}\n`
80+
md += `\n`
81+
}
82+
83+
md += `### Final State\n`
84+
if (captured.url) md += ` > URL: ${captured.url}\n`
85+
const links = artifactLinks(captured)
86+
if (links) md += links + '\n'
87+
88+
const traceFile = path.join(dir, 'trace.md')
89+
fs.writeFileSync(traceFile, md)
90+
return traceFile
91+
}
92+
5993
export function artifactsToFileUrls(captured, dir) {
6094
const out = {}
6195
if (captured.url) out.url = captured.url
@@ -69,3 +103,134 @@ export function artifactsToFileUrls(captured, dir) {
69103
if (typeof captured.localStorageCount === 'number') out.localStorageCount = captured.localStorageCount
70104
return out
71105
}
106+
107+
// ---------------------------------------------------------------------------
108+
// Snapshot capture (HTML / ARIA / screenshot / console / storage)
109+
// ---------------------------------------------------------------------------
110+
111+
function normalizeBrowserLogs(logs) {
112+
return (logs || []).map(l => {
113+
if (typeof l === 'string') return l
114+
if (l && typeof l.type === 'function' && typeof l.text === 'function') {
115+
return { type: l.type(), text: l.text() }
116+
}
117+
return l
118+
})
119+
}
120+
121+
async function captureStorageState(helper) {
122+
if (typeof helper.grabStorageState === 'function') {
123+
try {
124+
const state = await helper.grabStorageState()
125+
if (state) return state
126+
} catch {}
127+
}
128+
129+
const state = { cookies: [], origins: [] }
130+
131+
if (typeof helper.grabCookie === 'function') {
132+
try {
133+
const cookies = await helper.grabCookie()
134+
if (Array.isArray(cookies)) state.cookies = cookies
135+
} catch {}
136+
}
137+
138+
if (typeof helper.executeScript === 'function') {
139+
try {
140+
const result = await helper.executeScript(() => {
141+
const out = { origin: location.origin, items: [] }
142+
for (let i = 0; i < localStorage.length; i++) {
143+
const name = localStorage.key(i)
144+
out.items.push({ name, value: localStorage.getItem(name) })
145+
}
146+
return out
147+
})
148+
if (result?.items?.length) {
149+
state.origins.push({ origin: result.origin, localStorage: result.items })
150+
}
151+
} catch {}
152+
}
153+
154+
return state
155+
}
156+
157+
export async function captureSnapshot(helper, {
158+
dir,
159+
prefix = 'snapshot',
160+
fullPage = false,
161+
captureURL = true,
162+
captureScreenshot = true,
163+
captureHTML = true,
164+
captureARIA = true,
165+
captureBrowserLogs = true,
166+
captureStorage = true,
167+
} = {}) {
168+
if (!helper) return {}
169+
const out = {}
170+
171+
if (captureURL) {
172+
try {
173+
if (helper.grabCurrentUrl) out.url = await helper.grabCurrentUrl()
174+
} catch {}
175+
}
176+
177+
if (captureScreenshot && helper.saveScreenshot) {
178+
try {
179+
const file = `${prefix}_screenshot.png`
180+
await helper.saveScreenshot(path.join(dir, file), fullPage)
181+
out.screenshot = file
182+
} catch {}
183+
}
184+
185+
if (captureHTML && helper.grabSource) {
186+
try {
187+
const html = await helper.grabSource()
188+
// Universal funnel: every captured HTML snapshot flows through formatHtml
189+
// (minify -> cleanHtml -> beautify). Don't add direct grabSource->writeFile
190+
// paths elsewhere; route through this util so trash-class cleanup stays
191+
// consistent across aiTrace, pageInfo, and MCP tools.
192+
const formatted = await formatHtml(html)
193+
const file = `${prefix}_page.html`
194+
fs.writeFileSync(path.join(dir, file), formatted)
195+
out.html = file
196+
} catch {}
197+
}
198+
199+
if (captureARIA && helper.grabAriaSnapshot) {
200+
try {
201+
const aria = await helper.grabAriaSnapshot()
202+
const file = `${prefix}_aria.txt`
203+
fs.writeFileSync(path.join(dir, file), aria)
204+
out.aria = file
205+
} catch {}
206+
}
207+
208+
if (captureBrowserLogs && helper.grabBrowserLogs) {
209+
try {
210+
const logs = await helper.grabBrowserLogs()
211+
const normalized = normalizeBrowserLogs(logs)
212+
const file = `${prefix}_console.json`
213+
fs.writeFileSync(path.join(dir, file), JSON.stringify(normalized, null, 2))
214+
out.console = file
215+
out.consoleCount = normalized.length
216+
} catch {}
217+
}
218+
219+
if (captureStorage) {
220+
try {
221+
const state = await captureStorageState(helper)
222+
const cookieCount = state.cookies?.length || 0
223+
const localStorageCount = (state.origins || [])
224+
.reduce((sum, o) => sum + (o.localStorage?.length || 0), 0)
225+
if (cookieCount || localStorageCount) {
226+
const file = `${prefix}_storage.json`
227+
fs.writeFileSync(path.join(dir, file), JSON.stringify(state, null, 2))
228+
out.storage = file
229+
out.cookieCount = cookieCount
230+
out.localStorageCount = localStorageCount
231+
}
232+
} catch {}
233+
}
234+
235+
return out
236+
}

0 commit comments

Comments
 (0)