Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/heal.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ class Heal {

async getCodeSuggestions(context) {
const suggestions = []
const stepName = context.step?.name
const recipes = matchRecipes(this.recipes, this.contextName)
.filter(r => !r.steps || !stepName || r.steps.includes(stepName))

debug('Recipes', recipes)

Expand Down
29 changes: 16 additions & 13 deletions lib/plugin/aiTrace.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export default function (config = {}) {
let testStartTime
let currentUrl = null
let testFailed = false
let pendingArtifactCapture = null
let firstFailedStepSaved = false

const reportDir = config.output ? path.resolve(store.codeceptDir, config.output) : defaultConfig.output
Expand Down Expand Up @@ -129,6 +130,7 @@ export default function (config = {}) {
currentUrl = null
testFailed = false
firstFailedStepSaved = false
pendingArtifactCapture = null
})

event.dispatcher.on(event.step.after, step => {
Expand Down Expand Up @@ -162,13 +164,12 @@ export default function (config = {}) {
return
}

const stepPersistPromise = persistStep(step).catch(err => {
recorder.add(`aiTrace step persistence: ${step.toString()}`, () => persistStep(step).catch(err => {
output.debug(`aiTrace: Error saving step: ${err.message}`)
})
recorder.add(`wait aiTrace step persistence: ${step.toString()}`, () => stepPersistPromise, true)
}), true)
})

event.dispatcher.on(event.step.failed, async step => {
event.dispatcher.on(event.step.failed, step => {
if (!currentTest) return
if (step.status === 'queued' && testFailed) {
output.debug(`aiTrace: Skipping queued failed step "${step.toString()}" - testFailed: ${testFailed}`)
Expand All @@ -188,11 +189,9 @@ export default function (config = {}) {
}
existingStep.status = 'failed'

try {
await captureArtifactsForStep(step, existingStep, existingStep.prefix)
} catch (err) {
pendingArtifactCapture = captureArtifactsForStep(step, existingStep, existingStep.prefix).catch(err => {
output.debug(`aiTrace: Error updating failed step: ${err.message}`)
}
})
} else {
if (stepNum === -1) return
if (isStepIgnored(step)) return
Expand All @@ -218,11 +217,9 @@ export default function (config = {}) {
steps.push(stepData)
firstFailedStepSaved = true

try {
await captureArtifactsForStep(step, stepData, stepPrefix)
} catch (err) {
pendingArtifactCapture = captureArtifactsForStep(step, stepData, stepPrefix).catch(err => {
output.debug(`aiTrace: Error capturing failed step artifacts: ${err.message}`)
}
})
}
})

Expand All @@ -238,7 +235,13 @@ export default function (config = {}) {
if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') {
return
}
persist(test, 'failed')
recorder.add('aiTrace:persist failed', async () => {
if (pendingArtifactCapture) {
await pendingArtifactCapture
pendingArtifactCapture = null
}
persist(test, 'failed')
}, true)
})

async function persistStep(step) {
Expand Down
9 changes: 5 additions & 4 deletions lib/plugin/analyze.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const ai = aiModule.default || aiModule
import colors from 'chalk'
import ora from 'ora'
import event from '../event.js'
import recorder from '../recorder.js'

import output from '../output.js'

Expand Down Expand Up @@ -227,14 +228,14 @@ export default function (config = {}) {
console.log('Enabled AI analysis')
})

event.dispatcher.on(event.all.result, async result => {
event.dispatcher.on(event.all.result, result => {
if (!isMainThread) return // run only on main thread
if (!ai.isEnabled) {
console.log('AI is disabled, no analysis will be performed. Run tests with --ai flag to enable it.')
return
}

printReport(result)
recorder.add('analyze:print-ai-report', () => printReport(result), true)
})

event.dispatcher.on(event.workers.result, async result => {
Expand All @@ -248,7 +249,7 @@ export default function (config = {}) {
return
}

printReport(result)
await printReport(result)
})

async function printReport(result) {
Expand Down Expand Up @@ -294,7 +295,7 @@ export default function (config = {}) {
console.error('Error analyzing failed tests', err)
}

if (!Object.keys(container.plugins()).includes('pageInfo')) {
if (!Object.keys(Container.plugins()).includes('pageInfo')) {
console.log('To improve analysis, enable pageInfo plugin to get more context for failed tests.')
}
}
Expand Down
5 changes: 3 additions & 2 deletions lib/plugin/heal.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export default function (config = {}) {
event.dispatcher.on(event.test.before, test => {
currentTest = test
healedSteps = 0
healTries = 0
caughtError = null
})

Expand All @@ -94,7 +95,9 @@ export default function (config = {}) {
if (trigger.on === 'file' && !matchStepFile(step, trigger.path, trigger.line)) return

recorder.catchWithoutStop(async err => {
if (healTries >= config.healLimit) throw err
isHealing = true
healTries++
if (caughtError === err) throw err // avoid double handling
caughtError = err

Expand All @@ -121,8 +124,6 @@ export default function (config = {}) {

await heal.healStep(step, err, { test })

healTries++

recorder.add('close healing session', () => {
recorder.reset()
recorder.session.restore('heal')
Expand Down
12 changes: 5 additions & 7 deletions lib/plugin/pageInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ const defaultConfig = {
export default function (config = {}) {
config = Object.assign(defaultConfig, config)

const helper = pickActingHelper(Container.helpers())
if (!helper) return

event.dispatcher.on(event.test.failed, test => {
const helper = pickActingHelper(Container.helpers())
if (!helper) return

const pageState = {}

recorder.add('pageInfo capture', async () => {
Expand All @@ -60,8 +60,6 @@ export default function (config = {}) {
if (captured.html) {
const htmlPath = path.join(store.outputDir, captured.html)
pageState.htmlSnapshot = htmlPath
// Scan raw HTML (pre-cleanHtml) so error classes containing digits
// or trash-class prefixes aren't stripped before detection.
const htmlForScan = captured.htmlRaw || (() => {
try { return fs.readFileSync(htmlPath, 'utf8') } catch { return '' }
})()
Expand Down Expand Up @@ -90,7 +88,7 @@ export default function (config = {}) {
} catch {}
}
} catch {}
})
}, true)

recorder.add('Save page info', () => {
test.addNote('pageInfo', pageStateToMarkdown(pageState))
Expand All @@ -99,7 +97,7 @@ export default function (config = {}) {
fs.writeFileSync(pageStateFileName, pageStateToMarkdown(pageState))
test.artifacts.pageInfo = pageStateFileName
return pageState
})
}, true)
})
}

Expand Down
10 changes: 6 additions & 4 deletions lib/plugin/screencast.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ function wireScreencast(mode, options) {
state.startedAt = options.subtitles ? Date.now() : null
})

event.dispatcher.on(event.test.started, test => {
if (!options.video || state.startQueued) return
state.startQueued = true
recorder.add('screencast:start', async () => startScreencast(state.test, options, state), true)
})

event.dispatcher.on(event.step.started, step => {
if (state.steps) {
const at = Date.now()
Expand All @@ -116,10 +122,6 @@ function wireScreencast(mode, options) {
title: stepTitle(step),
}
}
if (!options.video || state.startQueued || !state.test) return
state.startQueued = true
const test = state.test
recorder.add('screencast:start', async () => startScreencast(test, options, state), true)
})

if (options.subtitles) {
Expand Down
10 changes: 9 additions & 1 deletion test/unit/plugin/screencast_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function fakeHelper(screencastApi) {

function detachAll() {
for (const evt of [
event.test.before, event.test.after, event.test.failed,
event.test.before, event.test.started, event.test.after, event.test.failed,
event.step.started, event.step.finished,
]) {
event.dispatcher.removeAllListeners(evt)
Expand Down Expand Up @@ -73,6 +73,7 @@ describe('screencast', () => {
const test = createTest('keep-on-pass')

event.dispatcher.emit(event.test.before, test)
event.dispatcher.emit(event.test.started, test)
event.dispatcher.emit(event.step.started, aStep())
await recorder.promise()

Expand All @@ -93,6 +94,7 @@ describe('screencast', () => {
const test = createTest('delete-on-pass')

event.dispatcher.emit(event.test.before, test)
event.dispatcher.emit(event.test.started, test)
event.dispatcher.emit(event.step.started, aStep())
await recorder.promise()

Expand All @@ -114,6 +116,7 @@ describe('screencast', () => {
const test = createTest('keep-on-fail')

event.dispatcher.emit(event.test.before, test)
event.dispatcher.emit(event.test.started, test)
event.dispatcher.emit(event.step.started, aStep())
await recorder.promise()

Expand All @@ -132,6 +135,7 @@ describe('screencast', () => {
screencast({ on: 'test', captions: true })
let test = createTest('with-captions')
event.dispatcher.emit(event.test.before, test)
event.dispatcher.emit(event.test.started, test)
event.dispatcher.emit(event.step.started, aStep())
await recorder.promise()
event.dispatcher.emit(event.test.after, test)
Expand All @@ -144,6 +148,7 @@ describe('screencast', () => {
screencast({ on: 'test', captions: false })
test = createTest('no-captions')
event.dispatcher.emit(event.test.before, test)
event.dispatcher.emit(event.test.started, test)
event.dispatcher.emit(event.step.started, aStep())
await recorder.promise()
event.dispatcher.emit(event.test.after, test)
Expand All @@ -159,6 +164,7 @@ describe('screencast', () => {
const test = createTest('with-srt')

event.dispatcher.emit(event.test.before, test)
event.dispatcher.emit(event.test.started, test)
await recorder.promise()

const step = { name: 'click', actor: 'I', args: ['Continue'] }
Expand All @@ -183,6 +189,7 @@ describe('screencast', () => {
test.artifacts.video = '/tmp/some-video-dir/myrun.webm'

event.dispatcher.emit(event.test.before, test)
event.dispatcher.emit(event.test.started, test)
const step = { name: 'see', actor: 'I', args: ['Github'] }
event.dispatcher.emit(event.step.started, step)
event.dispatcher.emit(event.step.finished, step)
Expand All @@ -202,6 +209,7 @@ describe('screencast', () => {
const test = createTest('no-api')

event.dispatcher.emit(event.test.before, test)
event.dispatcher.emit(event.test.started, test)
await recorder.promise()
event.dispatcher.emit(event.test.after, test)
await recorder.promise()
Expand Down