Skip to content

Commit 7bec5c7

Browse files
committed
[specs] Snapshot the DOM when a spec fails while cleaning up
1 parent d83f278 commit 7bec5c7

5 files changed

Lines changed: 78 additions & 5 deletions

File tree

‎runner/README.md‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,13 @@ so webpack's `require.context` picks it up.
7171
- `failedExpectations[]`: `{ message, stack }` — raw stack, Node remaps it. On `jasmineDone`
7272
these belong to no spec (a top-level `afterAll` that threw) and are reported as their own
7373
failure, so a red run cannot print a green summary.
74-
- `failure` (failed specs only): `{ log: string[], dom: DomNode[] }`. Log lines are
75-
already serialized strings; `dom` is a tree, not HTML.
74+
- `failure` (failed specs only): `{ log: string[], dom: DomNode[], domAt }`. Log lines are
75+
already serialized strings; `dom` is a tree, not HTML. `domAt` is `'failure'` for the
76+
usual snapshot taken at `specs:reset` — which, since specs stop at their first failed
77+
expectation, is the moment of the failure — or `'teardown'` for a spec that passed its
78+
expectations and then died while cleaning up (`Overlays survived reset!` and friends).
79+
A `'teardown'` snapshot is taken at `specDone`, after the other `afterEach` hooks have
80+
run, so `#fixtures` is already gone and the output says so.
7681
- `DomNode`: `{ tag, id, classes[], attrs{}, children: (DomNode | { text })[] }`.
7782
- `dom` is rooted at `<body>`, so it also captures elements a spec attached outside
7883
`#fixtures` and state Unpoly writes onto `body` itself. Jasmine's own

‎runner/terminal/formatter.mjs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,12 @@ export function failureBlock(failure, { verbose } = {}) {
7474
let outline = renderOutline(failure.dom)
7575
if (outline) {
7676
lines.push('')
77-
lines.push(indent('HTML state:', 2))
77+
// Two different moments, and they must not look alike: a teardown snapshot is
78+
// taken after the other afterEach hooks ran, so #fixtures is already destroyed.
79+
let title = failure.domAt === 'teardown'
80+
? 'HTML state (after teardown, since the spec failed while cleaning up):'
81+
: 'HTML state:'
82+
lines.push(indent(title, 2))
7883
lines.push(indent(outline, 4))
7984
}
8085
}

‎runner/terminal/poster.js‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,14 @@ function install() {
4848
capturing = false
4949
let failure
5050
if (result.status === 'failed') {
51-
failure = { log: currentLog.slice(), dom: pendingDom || [] }
51+
// A spec that passed its expectations and then died in teardown has no
52+
// snapshot yet — reset_up.js's checks ("Overlays survived reset!") are the
53+
// usual cause, and the element they complain about is still on the page. Grab
54+
// it now, flagged, because by this point the other afterEach hooks have run
55+
// and #fixtures is already gone.
56+
failure = pendingDom
57+
? { log: currentLog.slice(), dom: pendingDom, domAt: 'failure' }
58+
: { log: currentLog.slice(), dom: snapshotDom(), domAt: 'teardown' }
5259
}
5360
return post({
5461
type: 'specDone',

‎runner/terminal/receiver.mjs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,14 @@ export function createReceiver({ verbose = false, remapper, serverURL = '', vari
4242
// Normalizes and collects a failure record for the end-of-run report. Does NOT
4343
// touch counts — spec failures are already counted in onSpecDone, and afterAll
4444
// failures are counted by their caller (they aren't specs).
45-
function recordFailure({ path, fullName, expectations, log, dom }) {
45+
function recordFailure({ path, fullName, expectations, log, dom, domAt }) {
4646
failures.push({
4747
path,
4848
fullName,
4949
expectations: expectations || [],
5050
log: log || [],
5151
dom: dom || null,
52+
domAt,
5253
})
5354
}
5455

@@ -88,6 +89,7 @@ export function createReceiver({ verbose = false, remapper, serverURL = '', vari
8889
expectations: event.failedExpectations,
8990
log: event.failure && event.failure.log,
9091
dom: event.failure && event.failure.dom,
92+
domAt: event.failure && event.failure.domAt,
9193
})
9294
}
9395
}
@@ -123,6 +125,7 @@ export function createReceiver({ verbose = false, remapper, serverURL = '', vari
123125
extraFailures: Math.max(0, expectations.length - 1),
124126
log: raw.log,
125127
dom: raw.dom,
128+
domAt: raw.domAt,
126129
debugURL: specsURL(serverURL, { ...variant, spec: raw.fullName }),
127130
}, { verbose }))
128131
}

‎runner/test/receiver.test.mjs‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ async function run(events) {
1919
return { text: buffer, code }
2020
}
2121

22+
// Same as run(), with --verbose so the log and HTML state blocks are rendered.
23+
async function runVerbose(events) {
24+
let buffer = ''
25+
let receiver = createReceiver({
26+
verbose: true,
27+
remapper: noRemap,
28+
serverURL: 'http://localhost:4000',
29+
out: { write: (string) => { buffer += string } },
30+
})
31+
for (let event of events) await receiver.handle(event)
32+
return { text: buffer }
33+
}
34+
2235
test('prints live progress and a passing summary, exit 0', async () => {
2336
let { text, code } = await run([
2437
{ type: 'jasmineStarted', totalSpecs: 2 },
@@ -152,3 +165,43 @@ test('a filter that matched nothing fails instead of reporting success', async (
152165
assert.equal(code, 1)
153166
assert.match(text, /No specs ran/)
154167
})
168+
169+
test('a teardown snapshot is labelled so it is not mistaken for the failure-time DOM', async () => {
170+
// A spec that passes its expectations and then dies in teardown gets its snapshot late,
171+
// after #fixtures has been destroyed — so the heading has to say so.
172+
let { text } = await runVerbose([
173+
{ type: 'suiteStarted', description: 'up.layer' },
174+
{
175+
type: 'specDone',
176+
fullName: 'up.layer leaks an overlay',
177+
description: 'leaks an overlay',
178+
status: 'failed',
179+
failedExpectations: [{ message: 'Overlays survived reset!', stack: 'at reset_up.js:140' }],
180+
failure: { log: [], dom: [{ tag: 'up-modal', classes: [], attrs: {}, children: [] }], domAt: 'teardown' },
181+
},
182+
{ type: 'suiteDone', description: 'up.layer', failedExpectations: [] },
183+
{ type: 'jasmineDone', overallStatus: 'failed' },
184+
])
185+
186+
assert.match(text, /HTML state \(after teardown/)
187+
assert.match(text, /up-modal/)
188+
})
189+
190+
test('a failure-time snapshot keeps the plain heading', async () => {
191+
let { text } = await runVerbose([
192+
{ type: 'suiteStarted', description: 'up.layer' },
193+
{
194+
type: 'specDone',
195+
fullName: 'up.layer fails an expectation',
196+
description: 'fails an expectation',
197+
status: 'failed',
198+
failedExpectations: [{ message: 'Expected 1 to be 2.', stack: 'at spec/unpoly/layer_spec.js:5' }],
199+
failure: { log: [], dom: [{ tag: 'div', id: 'fixtures', classes: [], attrs: {}, children: [] }], domAt: 'failure' },
200+
},
201+
{ type: 'suiteDone', description: 'up.layer', failedExpectations: [] },
202+
{ type: 'jasmineDone', overallStatus: 'failed' },
203+
])
204+
205+
assert.match(text, /HTML state:/)
206+
assert.doesNotMatch(text, /after teardown/)
207+
})

0 commit comments

Comments
 (0)