Skip to content

Commit 5b2517c

Browse files
DavertMikDavertMikclaude
authored
fix(junit): report suite hook failures via hook.failed event (#5657)
* fix(junit): report suite hook failures via hook.failed event (#5645) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): vendor CodeMirror 6 bundle so richtext tests don't depend on esm.sh The CodeMirror 6 rich-text fixture imported codemirror@6.0.1 from esm.sh, which fans out to the entire @codemirror/* + @lezer/* module graph over many serial requests. In CI this reliably exceeded the 30s waitForFunction timeout, so window.__editorReady never became true and all four CodeMirror 6 fillField tests timed out across Playwright, Puppeteer and WebDriver. Pre-existing on 4.x and unrelated to the junit change on this branch. Vendor the self-contained esm.sh bundle locally and import it same-origin, making the test network-independent and deterministic. Exclude the vendored blob from eslint and prettier. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: DavertMik <davert@testomat.io> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 5e07553 commit 5b2517c

6 files changed

Lines changed: 104 additions & 9 deletions

File tree

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Vendored third-party bundles used by rich-text editor test fixtures
2+
test/data/app/js/

eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const compat = new FlatCompat({
1414

1515
export default [
1616
{
17-
ignores: ['test/data/output', 'lib/css2xpath/*'],
17+
ignores: ['test/data/output', 'test/data/app/js/*', 'lib/css2xpath/*'],
1818
},
1919
{
2020
languageOptions: {

lib/plugin/junitReporter.js

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,26 @@ export default function (config = {}) {
6666
config = Object.assign({}, defaultConfig, config)
6767

6868
let written = false
69+
const hookFailures = []
70+
71+
event.dispatcher.on(event.hook.failed, hook => {
72+
if (!hook || !['BeforeSuite', 'AfterSuite'].includes(hook.hookName)) return
73+
const err = hook.err || hook.error
74+
if (!err) return
75+
const runnable = hook.ctx && hook.ctx.test
76+
const suite = runnable && runnable.parent
77+
hookFailures.push({
78+
title: hook.title || `${hook.hookName} hook failed`,
79+
state: 'failed',
80+
err,
81+
parent: suite,
82+
file: (runnable && runnable.file) || (suite && suite.file),
83+
tags: (suite && suite.tags) || [],
84+
meta: {},
85+
steps: [],
86+
duration: (runnable && runnable.duration) || 0,
87+
})
88+
})
6989

7090
const writeReport = result => {
7191
if (written) return
@@ -76,25 +96,26 @@ export default function (config = {}) {
7696
mkdirp.sync(dir)
7797
const file = path.join(dir, config.outputName)
7898

79-
fs.writeFileSync(file, buildXml(result, config))
99+
fs.writeFileSync(file, buildXml(result, config, hookFailures))
80100
output.plugin('junitReporter', `JUnit report saved to ${file}`)
81101
}
82102

83103
event.dispatcher.on(event.all.result, writeReport)
84104
event.dispatcher.on(event.workers.result, writeReport)
85105
}
86106

87-
function buildXml(result, config) {
107+
function buildXml(result, config, hookFailures = []) {
88108
const doc = new DOMImplementation().createDocument(null, null, null)
89-
const suites = groupBySuite(result.tests)
109+
const allTests = result.tests.concat(hookFailures)
110+
const suites = groupBySuite(allTests)
90111

91112
const root = doc.createElement('testsuites')
92113
setAttr(root, 'name', config.testGroupName)
93-
setAttr(root, 'tests', result.tests.length)
94-
setAttr(root, 'failures', countState(result.tests, 'failed'))
95-
setAttr(root, 'skipped', countSkipped(result.tests))
114+
setAttr(root, 'tests', allTests.length)
115+
setAttr(root, 'failures', countState(allTests, 'failed'))
116+
setAttr(root, 'skipped', countSkipped(allTests))
96117
setAttr(root, 'errors', 0)
97-
setAttr(root, 'time', toSeconds(sumDuration(result.tests)))
118+
setAttr(root, 'time', toSeconds(sumDuration(allTests)))
98119
setAttr(root, 'timestamp', toIso(result.stats && result.stats.start))
99120
doc.appendChild(root)
100121

test/data/app/js/codemirror6.js

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/data/app/view/form/richtext/codemirror6.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<button type="submit" id="submit">Submit</button>
1515
</form>
1616
<script type="module">
17-
import { EditorView, minimalSetup } from 'https://esm.sh/codemirror@6.0.1'
17+
import { EditorView, minimalSetup } from '/js/codemirror6.js'
1818
const initial = <?php echo json_encode($initial, JSON_UNESCAPED_UNICODE); ?>;
1919
const view = new EditorView({
2020
doc: initial,

test/unit/junitReporter_test.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import xml2js from 'xml2js'
88
import junitReporter from '../../lib/plugin/junitReporter.js'
99
import event from '../../lib/event.js'
1010
import store from '../../lib/store.js'
11+
import { BeforeSuiteHook, BeforeHook } from '../../lib/mocha/hooks.js'
1112
import Step from '../../lib/step/base.js'
1213
import MetaStep from '../../lib/step/meta.js'
1314

@@ -93,11 +94,13 @@ describe('JUnit Reporter Plugin', () => {
9394
store.outputDir = tmpDir
9495
event.dispatcher.removeAllListeners(event.all.result)
9596
event.dispatcher.removeAllListeners(event.workers.result)
97+
event.dispatcher.removeAllListeners(event.hook.failed)
9698
})
9799

98100
afterEach(() => {
99101
event.dispatcher.removeAllListeners(event.all.result)
100102
event.dispatcher.removeAllListeners(event.workers.result)
103+
event.dispatcher.removeAllListeners(event.hook.failed)
101104
store._outputDir = prevOutputDir
102105
fs.rmSync(tmpDir, { recursive: true, force: true })
103106
})
@@ -198,4 +201,56 @@ describe('JUnit Reporter Plugin', () => {
198201
const secondMtime = fs.statSync(path.join(tmpDir, 'report.xml')).mtimeMs
199202
expect(secondMtime).to.equal(firstMtime)
200203
})
204+
205+
it('adds suite hook failures as failed testcases', async () => {
206+
junitReporter({})
207+
208+
const suite = { title: 'Suite hooks @smoke', file: '/tests/hooks_test.js', tags: ['@smoke'], startedAt: Date.now() }
209+
const beforeSuiteRunnable = { title: '"before all" hook: BeforeSuite for "test something"', parent: suite, file: suite.file, duration: 42 }
210+
event.dispatcher.emit(event.hook.failed, new BeforeSuiteHook({ ctx: { test: beforeSuiteRunnable } }, new Error('BeforeSuite failed')))
211+
212+
const beforeRunnable = { title: '"before each" hook: Before for "test something"', parent: suite, file: suite.file, duration: 7 }
213+
event.dispatcher.emit(event.hook.failed, new BeforeHook({ ctx: { test: beforeRunnable } }, new Error('Before failed')))
214+
215+
event.dispatcher.emit(event.all.result, {
216+
tests: [],
217+
stats: { start: new Date(), passes: 0, failures: 0, pending: 0, tests: 0 },
218+
})
219+
220+
const parsed = await parseReport(tmpDir)
221+
expect(parsed.testsuites.$.tests).to.equal('1')
222+
expect(parsed.testsuites.$.failures).to.equal('1')
223+
224+
const suiteEl = parsed.testsuites.testsuite[0]
225+
expect(suiteEl.$.name).to.equal('Suite hooks @smoke')
226+
expect(suiteEl.testcase).to.have.length(1)
227+
expect(suiteEl.testcase[0].$.name).to.contain('"before all" hook: BeforeSuite')
228+
expect(suiteEl.testcase[0].failure[0].$.message).to.contain('BeforeSuite failed')
229+
230+
const tagsProp = suiteEl.testcase[0].properties[0].property.find(p => p.$.name === 'tags')
231+
expect(tagsProp.$.value).to.equal('@smoke')
232+
})
233+
234+
it('adds suite hook failures forwarded from workers as failed testcases', async () => {
235+
junitReporter({})
236+
237+
event.dispatcher.emit(event.hook.failed, {
238+
hookName: 'AfterSuite',
239+
title: '"after all" hook: AfterSuite for "test something"',
240+
error: { message: 'AfterSuite failed', stack: 'Error: AfterSuite failed\n at suite' },
241+
})
242+
243+
event.dispatcher.emit(event.workers.result, {
244+
tests: [],
245+
stats: { start: new Date(), passes: 0, failures: 0, pending: 0, tests: 0 },
246+
})
247+
248+
const parsed = await parseReport(tmpDir)
249+
expect(parsed.testsuites.$.tests).to.equal('1')
250+
expect(parsed.testsuites.$.failures).to.equal('1')
251+
252+
const suiteEl = parsed.testsuites.testsuite[0]
253+
expect(suiteEl.testcase[0].$.name).to.contain('"after all" hook: AfterSuite')
254+
expect(suiteEl.testcase[0].failure[0].$.message).to.contain('AfterSuite failed')
255+
})
201256
})

0 commit comments

Comments
 (0)