Skip to content

Commit adbbcd8

Browse files
committed
fix bug and add test support to testomatio
1 parent ca575c9 commit adbbcd8

File tree

5 files changed

+288
-47
lines changed

5 files changed

+288
-47
lines changed

docs/aitrace.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,18 @@ For each test, a `trace_<sha256>` directory is created with the following files:
5151

5252
**trace.md** - AI-friendly markdown file with test execution history
5353

54-
**0000_screenshot.png** - Screenshot for each step
54+
**0000_step_name_screenshot.png** - Screenshot for each step (file names include step names)
5555

56-
**0000_page.html** - Full HTML of the page at each step
56+
**0000_step_name_page.html** - Full HTML of the page at each step
5757

58-
**0000_aria.txt** - ARIA accessibility snapshot (AI-readable structure without HTML noise)
58+
**0000_step_name_aria.txt** - ARIA accessibility snapshot (AI-readable structure without HTML noise)
5959

60-
**0000_console.json** - Browser console logs
60+
**0000_step_name_console.json** - Browser console logs
6161

6262
When HAR or trace recording is enabled in your helper config, links to those files are also included.
6363

64+
**Note:** Artifact files are named using step names for easier identification (e.g., `0000_I_see_Product_screenshot.png` instead of just `0000_screenshot.png`).
65+
6466
## Trace File Format
6567

6668
The `trace.md` file contains a structured execution log with links to all artifacts:
@@ -73,20 +75,22 @@ time: 3.45s
7375

7476
I am on page "https://example.com"
7577
> navigated to https://example.com/
76-
> [HTML](./0000_page.html)
77-
> [ARIA Snapshot](./0000_aria.txt)
78-
> [Screenshot](./0000_screenshot.png)
79-
> [Browser Logs](0000_console.json) (7 entries)
78+
> [HTML](./0000_I_am_on_page_https_example.com_page.html)
79+
> [ARIA Snapshot](./0000_I_am_on_page_https_example.com_aria.txt)
80+
> [Screenshot](./0000_I_am_on_page_https_example.com_screenshot.png)
81+
> [Browser Logs](./0000_I_am_on_page_https_example.com_console.json) (7 entries)
8082
> HTTP: see [HAR file](../har/...) for network requests
8183
8284
I see "Welcome"
8385
> navigated to https://example.com/
84-
> [HTML](./0001_page.html)
85-
> [ARIA Snapshot](./0001_aria.txt)
86-
> [Screenshot](./0001_screenshot.png)
87-
> [Browser Logs](0001_console.json) (0 entries)
86+
> [HTML](./0001_I_see_Welcome_page.html)
87+
> [ARIA Snapshot](./0001_I_see_Welcome_aria.txt)
88+
> [Screenshot](./0001_I_see_Welcome_screenshot.png)
89+
> [Browser Logs](./0001_I_see_Welcome_console.json) (0 entries)
8890
```
8991

92+
Files are named with step descriptions for easier navigation and debugging.
93+
9094
## Configuration
9195

9296
### Basic Configuration

docs/plugins.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,12 @@ exports.config = {
103103
For each test, a `trace_<sha256>` directory is created with:
104104

105105
* **trace.md** - AI-friendly markdown file with test execution history
106-
* **0000_screenshot.png** - screenshot for each step
107-
* **0000_page.html** - full HTML of the page at each step
108-
* **0000_aria.txt** - ARIA accessibility snapshot (AI-readable structure)
109-
* **0000_console.json** - browser console logs
106+
* **0000_step_name_screenshot.png** - screenshot for each step (named with step description)
107+
* **0000_step_name_page.html** - full HTML of the page at each step
108+
* **0000_step_name_aria.txt** - ARIA accessibility snapshot (AI-readable structure)
109+
* **0000_step_name_console.json** - browser console logs
110+
111+
Artifact files include step names for easier identification (e.g., `0000_I_see_Product_screenshot.png`).
110112

111113
When HAR or trace recording is enabled in your helper config, links to those files are also included.
112114

@@ -120,20 +122,22 @@ time: 3.45s
120122

121123
I am on page "https://example.com"
122124
> navigated to https://example.com/
123-
> [HTML](./0000_page.html)
124-
> [ARIA Snapshot](./0000_aria.txt)
125-
> [Screenshot](./0000_screenshot.png)
126-
> [Browser Logs](0000_console.json) (7 entries)
125+
> [HTML](./0000_I_am_on_page_https_example.com_page.html)
126+
> [ARIA Snapshot](./0000_I_am_on_page_https_example.com_aria.txt)
127+
> [Screenshot](./0000_I_am_on_page_https_example.com_screenshot.png)
128+
> [Browser Logs](./0000_I_am_on_page_https_example.com_console.json) (7 entries)
127129
> HTTP: see [HAR file](../har/...) for network requests
128130
129131
I see "Welcome"
130132
> navigated to https://example.com/
131-
> [HTML](./0001_page.html)
132-
> [ARIA Snapshot](./0001_aria.txt)
133-
> [Screenshot](./0001_screenshot.png)
134-
> [Browser Logs](0001_console.json) (0 entries)
133+
> [HTML](./0001_I_see_Welcome_page.html)
134+
> [ARIA Snapshot](./0001_I_see_Welcome_aria.txt)
135+
> [Screenshot](./0001_I_see_Welcome_screenshot.png)
136+
> [Browser Logs](./0001_I_see_Welcome_console.json) (0 entries)
135137
```
136138

139+
Files are named with step descriptions for easier identification.
140+
137141
#### Best Practices
138142

139143
**Save disk space** - Only keep traces for failed tests:

lib/plugin/aiTrace.js

Lines changed: 127 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ export default function (config) {
8181
let currentTest = null
8282
let testStartTime
8383
let currentUrl = null
84+
let testFailed = false
85+
let firstFailedStepSaved = false
8486

8587
const reportDir = config.output ? path.resolve(global.codecept_dir, config.output) : defaultConfig.output
8688

@@ -111,6 +113,8 @@ export default function (config) {
111113
.slice(0, 8)
112114
dir = path.join(reportDir, `trace_${testTitle}_${uniqueHash}`)
113115
mkdirp.sync(dir)
116+
deleteDir(dir)
117+
mkdirp.sync(dir)
114118
stepNum = 0
115119
error = null
116120
steps = []
@@ -119,16 +123,88 @@ export default function (config) {
119123
currentTest = test
120124
testStartTime = Date.now()
121125
currentUrl = null
126+
testFailed = false
127+
firstFailedStepSaved = false
122128
})
123129

124-
event.dispatcher.on(event.step.after, step => {
130+
event.dispatcher.on(event.step.after, async step => {
125131
if (!currentTest) return
126-
recorder.add('save ai trace step', async () => persistStep(step), true)
132+
if (step.status === 'failed') {
133+
testFailed = true
134+
}
135+
if (step.status === 'queued' && testFailed) {
136+
output.debug(`aiTrace: Skipping queued step "${step.toString()}" - testFailed: ${testFailed}`)
137+
return
138+
}
139+
if (step.status === 'failed' && firstFailedStepSaved) {
140+
output.debug(`aiTrace: Skipping failed step "${step.toString()}" - already handled by step.failed event`)
141+
return
142+
}
143+
recorder.add(`save artifacts for step ${step.toString()}`, async () => {
144+
try {
145+
await persistStep(step)
146+
} catch (err) {
147+
output.debug(`aiTrace: Error saving step: ${err.message}`)
148+
}
149+
}, true)
127150
})
128151

129-
event.dispatcher.on(event.step.failed, step => {
152+
event.dispatcher.on(event.step.failed, async step => {
130153
if (!currentTest) return
131-
recorder.add('save ai trace failed step', async () => persistStep(step), true)
154+
if (step.status === 'queued' && testFailed) {
155+
output.debug(`aiTrace: Skipping queued failed step "${step.toString()}" - testFailed: ${testFailed}`)
156+
return
157+
}
158+
if (firstFailedStepSaved) {
159+
output.debug(`aiTrace: Skipping subsequent failed step "${step.toString()}" - already saved first failed step`)
160+
return
161+
}
162+
163+
const stepKey = step.toString()
164+
if (savedSteps.has(stepKey)) {
165+
const existingStep = steps.find(s => s.step === stepKey)
166+
if (!existingStep) {
167+
output.debug(`aiTrace: Step "${stepKey}" marked as saved but not found in steps array`)
168+
return
169+
}
170+
existingStep.status = 'failed'
171+
172+
try {
173+
await captureArtifactsForStep(step, existingStep, existingStep.prefix)
174+
} catch (err) {
175+
output.debug(`aiTrace: Error updating failed step: ${err.message}`)
176+
}
177+
} else {
178+
if (stepNum === -1) return
179+
if (isStepIgnored(step)) return
180+
if (step.metaStep && step.metaStep.name === 'BeforeSuite') return
181+
182+
const stepPrefix = generateStepPrefix(step, stepNum)
183+
stepNum++
184+
185+
const stepData = {
186+
step: stepKey,
187+
status: 'failed',
188+
prefix: stepPrefix,
189+
artifacts: {},
190+
meta: {},
191+
debugOutput: [],
192+
}
193+
194+
if (step.startTime && step.endTime) {
195+
stepData.meta.duration = ((step.endTime - step.startTime) / 1000).toFixed(2) + 's'
196+
}
197+
198+
savedSteps.add(stepKey)
199+
steps.push(stepData)
200+
firstFailedStepSaved = true
201+
202+
try {
203+
await captureArtifactsForStep(step, stepData, stepPrefix)
204+
} catch (err) {
205+
output.debug(`aiTrace: Error capturing failed step artifacts: ${err.message}`)
206+
}
207+
}
132208
})
133209

134210
event.dispatcher.on(event.test.passed, test => {
@@ -152,16 +228,19 @@ export default function (config) {
152228
if (step.metaStep && step.metaStep.name === 'BeforeSuite') return
153229

154230
const stepKey = step.toString()
231+
155232
if (savedSteps.has(stepKey)) {
156233
const existingStep = steps.find(s => s.step === stepKey)
157234
if (existingStep && step.status === 'failed') {
158235
existingStep.status = 'failed'
236+
step.artifacts = {}
237+
await captureArtifactsForStep(step, existingStep, existingStep.prefix)
159238
}
160239
return
161240
}
162241
savedSteps.add(stepKey)
163242

164-
const stepPrefix = `${String(stepNum).padStart(4, '0')}`
243+
const stepPrefix = generateStepPrefix(step, stepNum)
165244
stepNum++
166245

167246
const stepData = {
@@ -182,28 +261,44 @@ export default function (config) {
182261
debugOutput = []
183262
}
184263

264+
await captureArtifactsForStep(step, stepData, stepPrefix)
265+
steps.push(stepData)
266+
}
267+
268+
async function captureArtifactsForStep(step, stepData, stepPrefix) {
269+
if (!step.artifacts) {
270+
step.artifacts = {}
271+
}
272+
273+
let browserAvailable = true
274+
185275
try {
186-
if (helper.grabCurrentUrl) {
187-
try {
276+
try {
277+
if (helper.grabCurrentUrl) {
188278
const url = await helper.grabCurrentUrl()
189279
stepData.meta.url = url
190280
currentUrl = url
191-
} catch (err) {
192-
// Ignore URL capture errors
193281
}
282+
} catch (err) {
283+
browserAvailable = false
284+
output.debug(`aiTrace: Browser unavailable, partial artifact capture: ${err.message}`)
194285
}
195286

196-
// Save screenshot
197287
if (!step.artifacts?.screenshot) {
198-
const screenshotFile = `${stepPrefix}_screenshot.png`
199-
await helper.saveScreenshot(path.join(dir, screenshotFile), config.fullPageScreenshots)
200-
stepData.artifacts.screenshot = screenshotFile
201-
} else {
202-
stepData.artifacts.screenshot = step.artifacts.screenshot
288+
try {
289+
const screenshotFile = `${stepPrefix}_screenshot.png`
290+
const screenshotPath = path.join(dir, screenshotFile)
291+
await helper.saveScreenshot(screenshotPath, config.fullPageScreenshots)
292+
293+
stepData.artifacts.screenshot = screenshotFile
294+
step.artifacts.screenshot = screenshotPath
295+
} catch (err) {
296+
output.debug(`aiTrace: Could not save screenshot: ${err.message}`)
297+
}
203298
}
204299

205300
// Save HTML
206-
if (config.captureHTML && helper.grabSource) {
301+
if (config.captureHTML && helper.grabSource && browserAvailable) {
207302
if (!step.artifacts?.html) {
208303
try {
209304
const html = await helper.grabSource()
@@ -219,7 +314,7 @@ export default function (config) {
219314
}
220315

221316
// Save ARIA snapshot
222-
if (config.captureARIA && helper.grabAriaSnapshot) {
317+
if (config.captureARIA && helper.grabAriaSnapshot && browserAvailable) {
223318
try {
224319
const aria = await helper.grabAriaSnapshot()
225320
const ariaFile = `${stepPrefix}_aria.txt`
@@ -231,7 +326,7 @@ export default function (config) {
231326
}
232327

233328
// Save browser logs
234-
if (config.captureBrowserLogs && helper.grabBrowserLogs) {
329+
if (config.captureBrowserLogs && helper.grabBrowserLogs && browserAvailable) {
235330
try {
236331
const logs = await helper.grabBrowserLogs()
237332
const logsFile = `${stepPrefix}_console.json`
@@ -245,8 +340,6 @@ export default function (config) {
245340
} catch (err) {
246341
output.plugin(`aiTrace: Can't save step artifacts: ${err}`)
247342
}
248-
249-
steps.push(stepData)
250343
}
251344

252345
function persist(test, status) {
@@ -281,7 +374,9 @@ export default function (config) {
281374
}
282375

283376
steps.forEach((stepData, index) => {
284-
markdown += `${stepData.step}\n`
377+
const stepAnchor = clearString(stepData.step).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 50)
378+
markdown += `### Step ${index + 1}: ${stepData.step}\n`
379+
markdown += `<a id="${stepAnchor}"></a>\n`
285380

286381
if (stepData.meta.duration) {
287382
markdown += ` > duration: ${stepData.meta.duration}\n`
@@ -343,5 +438,15 @@ export default function (config) {
343438
}
344439
return false
345440
}
346-
}
347441

442+
function generateStepPrefix(step, index) {
443+
const stepName = step.toString()
444+
const cleanedName = clearString(stepName)
445+
.replace(/[^a-zA-Z0-9_-]/g, '_')
446+
.replace(/_{2,}/g, '_')
447+
.slice(0, 80)
448+
.trim()
449+
450+
return `${String(index).padStart(4, '0')}_${cleanedName}`
451+
}
452+
}

lib/plugin/stepByStepReport.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,11 @@ export default function (config) {
207207
stepNum++
208208
slides[fileName] = step
209209
try {
210-
await helper.saveScreenshot(path.join(dir, fileName), config.fullPageScreenshots)
210+
const screenshotPath = path.join(dir, fileName)
211+
await helper.saveScreenshot(screenshotPath, config.fullPageScreenshots)
212+
213+
step.artifacts = step.artifacts || {}
214+
step.artifacts.screenshot = screenshotPath
211215
} catch (err) {
212216
output.plugin(`Can't save step screenshot: ${err}`)
213217
error = err

0 commit comments

Comments
 (0)