Skip to content

Commit 4364bb7

Browse files
committed
fix(pause): only pause on fail when the test fails
The fail mode of the pause plugin (and the pauseOnFail alias) set its flag on step.failed, so a step that failed inside tryTo, hopeThat or retryTo opened the interactive shell even though the test passed. Track test.failed instead. Hook failures still pause because they are reported through test.failed before test.after. Fixes #4516
1 parent 17c52c1 commit 4364bb7

3 files changed

Lines changed: 143 additions & 3 deletions

File tree

‎docs/debugging.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ For automated debugging without modifying test code, use the `pause` plugin. It
120120

121121
### Pause on Failure
122122

123-
Automatically enters interactive pause when a step fails:
123+
Automatically enters interactive pause when a test fails. A step that fails inside `tryTo`, `hopeThat` or `retryTo` does not trigger it while the test keeps passing:
124124

125125
```bash
126126
npx codeceptjs run -p pause

‎lib/plugin/pause.js‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ import {
2727
*
2828
* #### `on=` modes
2929
*
30-
* * **fail** — pause when a step fails (default)
30+
* * **fail** — pause after a test fails (default). Steps that fail inside
31+
* `tryTo`, `hopeThat` or `retryTo` without failing the test do not trigger a pause
3132
* * **test** — pause after each test
3233
* * **step** — pause before the first step (interactive walk-through)
3334
* * **file** — pause when execution reaches `path=...[;line=...]`
@@ -68,7 +69,7 @@ function initFailMode() {
6869
failed = false
6970
})
7071

71-
event.dispatcher.on(event.step.failed, () => {
72+
event.dispatcher.on(event.test.failed, () => {
7273
failed = true
7374
})
7475

‎test/unit/plugin/pause_test.js‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { expect } from 'chai'
2+
import pausePlugin from '../../../lib/plugin/pause.js'
3+
import { setPauseHandler } from '../../../lib/pause.js'
4+
import { hopeThat, retryTo, tryTo } from '../../../lib/effects.js'
5+
import recordStep from '../../../lib/step/record.js'
6+
import Step from '../../../lib/step.js'
7+
import event from '../../../lib/event.js'
8+
import recorder from '../../../lib/recorder.js'
9+
import store from '../../../lib/store.js'
10+
import { createTest } from '../../../lib/mocha/test.js'
11+
12+
const trackedEvents = [event.test.before, event.test.started, event.test.failed, event.test.after, event.step.failed]
13+
14+
const failingStep = () => recordStep(new Step({ fail: () => Promise.reject(new Error('Ups')) }, 'fail'), [])
15+
16+
describe('pause plugin on=fail', () => {
17+
let pauses
18+
let listenersBefore
19+
20+
beforeEach(() => {
21+
store.dryRun = false
22+
pauses = 0
23+
setPauseHandler(() => {
24+
pauses++
25+
return Promise.resolve()
26+
})
27+
listenersBefore = Object.fromEntries(trackedEvents.map(name => [name, event.dispatcher.listeners(name)]))
28+
recorder.reset()
29+
recorder.start()
30+
pausePlugin({})
31+
})
32+
33+
afterEach(() => {
34+
for (const name of trackedEvents) {
35+
for (const listener of event.dispatcher.listeners(name)) {
36+
if (!listenersBefore[name].includes(listener)) event.dispatcher.removeListener(name, listener)
37+
}
38+
}
39+
event.dispatcher.emit(event.test.finished, createTest('cleanup'))
40+
setPauseHandler(null)
41+
recorder.reset()
42+
})
43+
44+
const finishTest = async test => {
45+
event.dispatcher.emit(event.test.finished, test)
46+
event.dispatcher.emit(event.test.after, test)
47+
await recorder.promise()
48+
}
49+
50+
it('does not pause when a step failure is swallowed by tryTo (#4516)', async () => {
51+
const test = createTest('tryTo')
52+
event.dispatcher.emit(event.test.started, test)
53+
const result = await tryTo(failingStep)
54+
await recorder.promise()
55+
expect(result).to.equal(false)
56+
event.dispatcher.emit(event.test.passed, test)
57+
await finishTest(test)
58+
expect(pauses).to.equal(0)
59+
})
60+
61+
it('does not pause when a step failure is swallowed by hopeThat', async () => {
62+
const test = createTest('hopeThat')
63+
event.dispatcher.emit(event.test.started, test)
64+
const result = await hopeThat(failingStep)
65+
await recorder.promise()
66+
expect(result).to.equal(false)
67+
event.dispatcher.emit(event.test.passed, test)
68+
await finishTest(test)
69+
expect(pauses).to.equal(0)
70+
})
71+
72+
it('does not pause when retryTo succeeds after a failed attempt', async () => {
73+
const test = createTest('retryTo')
74+
event.dispatcher.emit(event.test.started, test)
75+
await retryTo(tries => (tries === 1 ? failingStep() : recorder.add(() => 'ok')), 2, 0)
76+
await recorder.promise()
77+
event.dispatcher.emit(event.test.passed, test)
78+
await finishTest(test)
79+
expect(pauses).to.equal(0)
80+
})
81+
82+
it('pauses when a failed step fails the test', async () => {
83+
const test = createTest('failing step')
84+
event.dispatcher.emit(event.test.started, test)
85+
const err = await failingStep().catch(e => e)
86+
recorder.reset()
87+
recorder.start()
88+
event.dispatcher.emit(event.test.failed, test, err)
89+
await finishTest(test)
90+
expect(pauses).to.equal(1)
91+
})
92+
93+
it('pauses when a Before hook fails', async () => {
94+
const test = createTest('before hook')
95+
event.dispatcher.emit(event.test.before, test)
96+
const err = await failingStep().catch(e => e)
97+
recorder.reset()
98+
recorder.start()
99+
event.dispatcher.emit(event.test.failed, test, err, 'Before')
100+
await finishTest(test)
101+
expect(pauses).to.equal(1)
102+
})
103+
104+
it('pauses when an After hook fails a passed test', async () => {
105+
const test = createTest('after hook')
106+
event.dispatcher.emit(event.test.started, test)
107+
event.dispatcher.emit(event.test.passed, test)
108+
const err = await failingStep().catch(e => e)
109+
recorder.reset()
110+
recorder.start()
111+
event.dispatcher.emit(event.test.failed, test, err, 'After')
112+
await finishTest(test)
113+
expect(pauses).to.equal(1)
114+
})
115+
116+
it('pauses when the test fails without a failed step', async () => {
117+
const test = createTest('plain error')
118+
event.dispatcher.emit(event.test.started, test)
119+
event.dispatcher.emit(event.test.failed, test, new Error('Ups'))
120+
await finishTest(test)
121+
expect(pauses).to.equal(1)
122+
})
123+
124+
it('does not carry a failure over to the next test', async () => {
125+
const failed = createTest('failed')
126+
event.dispatcher.emit(event.test.started, failed)
127+
const err = await failingStep().catch(e => e)
128+
recorder.reset()
129+
recorder.start()
130+
event.dispatcher.emit(event.test.failed, failed, err)
131+
await finishTest(failed)
132+
133+
const passed = createTest('passed')
134+
event.dispatcher.emit(event.test.started, passed)
135+
event.dispatcher.emit(event.test.passed, passed)
136+
await finishTest(passed)
137+
expect(pauses).to.equal(1)
138+
})
139+
})

0 commit comments

Comments
 (0)