Skip to content

Commit 571cafa

Browse files
authored
fix: fix file watching on compilation error (cypress-io/cypress-webpack-preprocessor#66)
fix issue where compilation error does not cause cypress to refetch the spec and display the error without a manual page reload.
1 parent 1d6e229 commit 571cafa

File tree

8 files changed

+483
-201
lines changed

8 files changed

+483
-201
lines changed

.mocharc.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"watch-ignore": ["./test/_test-output", "node_modules"]
3+
}

.vscode/settings.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
{
22
"standard.enable": false,
3+
"eslint.enable": true,
4+
"eslint.validate": ["json"],
35
"git.ignoreLimitWarning": true
4-
}
6+
}

circle.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@ jobs:
1313

1414
steps:
1515
- checkout
16-
16+
1717
- restore_cache:
1818
keys:
19-
- cache-{{ arch }}-{{ .Branch }}-{{ checksum "package.json" }}
19+
- cache-{{ arch }}-{{ .Branch }}-{{ checksum "package.json" }}
2020
- run:
2121
name: Yarn install
2222
command: yarn install --frozen-lockfile
2323
- save_cache:
2424
key: cache-{{ arch }}-{{ .Branch }}-{{ checksum "package.json" }}
2525
paths:
26-
- ~/.cache
26+
- ~/.cache
2727
- run:
2828
name: Lint code
2929
command: npm run lint
@@ -33,7 +33,10 @@ jobs:
3333
- run:
3434
name: Run tests
3535
command: npm test
36-
36+
- run: yarn add -D webpack@latest webpack-cli@latest
37+
- run:
38+
name: Run tests w/ webpack@latest
39+
command: npm test
3740
- run:
3841
name: Semantic release
3942
command: npm run semantic-release || true

index.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,17 @@ const preprocessor = (options = {}) => {
108108

109109
const rejectWithErr = (err) => {
110110
err.filePath = filePath
111-
debug(`errored bundling ${outputPath}`, err)
111+
debug(`errored bundling ${outputPath}`, err.message)
112+
112113
latestBundle.reject(err)
113114
}
114115

115116
// this function is called when bundling is finished, once at the start
116117
// and, if watching, each time watching triggers a re-bundle
117118
const handle = (err, stats) => {
118119
if (err) {
120+
debug('handle - had error', err.message)
121+
119122
return rejectWithErr(err)
120123
}
121124

@@ -126,12 +129,14 @@ const preprocessor = (options = {}) => {
126129

127130
const errorsToAppend = jsonStats.errors
128131
// remove stack trace lines since they're useless for debugging
129-
.map((err) => err.replace(/\n\s*at.*/g, '').replace(/From previous event:\n?/g, ''))
132+
.map(cleanseError)
130133
// multiple errors separated by newline
131134
.join('\n\n')
132135

133136
err.message += `\n${errorsToAppend}`
134137

138+
debug('stats had error(s)')
139+
135140
return rejectWithErr(err)
136141
}
137142

@@ -157,7 +162,7 @@ const preprocessor = (options = {}) => {
157162
latestBundle = createDeferred()
158163
bundles[filePath] = latestBundle.promise
159164

160-
bundles[filePath].tap(() => {
165+
bundles[filePath].finally(() => {
161166
debug('- compile finished for', filePath)
162167
// when the bundling is finished, emit 'rerun' to let Cypress
163168
// know to rerun the spec
@@ -187,12 +192,12 @@ const preprocessor = (options = {}) => {
187192

188193
// when the spec or project is closed, we need to clean up the cached
189194
// bundle promise and stop the watcher via `bundler.close()`
190-
file.on('close', () => {
195+
file.on('close', (cb = function () {}) => {
191196
debug('close', filePath)
192197
delete bundles[filePath]
193198

194199
if (file.shouldWatch) {
195-
bundler.close()
200+
bundler.close(cb)
196201
}
197202
})
198203

@@ -220,4 +225,8 @@ preprocessor.__reset = () => {
220225
bundles = {}
221226
}
222227

228+
function cleanseError (err) {
229+
return err.replace(/\n\s*at.*/g, '').replace(/From previous event:\n?/g, '')
230+
}
231+
223232
module.exports = preprocessor

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,14 @@
4444
"fs-extra": "8.1.0",
4545
"github-post-release": "1.13.1",
4646
"license-checker": "13.0.3",
47-
"mocha": "3.5.0",
47+
"mocha": "^7.1.0",
4848
"mockery": "2.1.0",
4949
"nsp": "2.7.0",
5050
"prettier-eslint-cli": "4.4.0",
5151
"semantic-release": "8.2.0",
5252
"simple-commit-message": "3.3.1",
53-
"sinon": "3.2.1",
54-
"sinon-chai": "2.13.0",
53+
"sinon": "^9.0.0",
54+
"sinon-chai": "^3.5.0",
5555
"snap-shot-it": "7.9.2",
5656
"webpack": "^4.18.1"
5757
},

test/e2e/compilation.spec.js

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
const EventEmitter = require('events').EventEmitter
2-
const expect = require('chai').expect
2+
const chai = require('chai')
33
const fs = require('fs-extra')
44
const path = require('path')
55
const snapshot = require('snap-shot-it')
6+
const sinon = require('sinon')
7+
const Bluebird = require('bluebird')
8+
const sinonChai = require('sinon-chai')
9+
10+
chai.use(sinonChai)
11+
const { expect } = chai
612

713
const preprocessor = require('../../index')
814

@@ -12,48 +18,91 @@ describe('webpack preprocessor - e2e', () => {
1218
let file
1319
let filePath
1420

15-
beforeEach(() => {
21+
beforeEach(async () => {
1622
const originalFilePath = path.join(__dirname, '..', 'fixtures', 'example_spec.js')
1723

1824
filePath = path.join(__dirname, '..', '_test-output', 'example_spec.js')
1925

2026
preprocessor.__reset()
21-
fs.removeSync(path.join(__dirname, '_test-output'))
22-
fs.outputFileSync(filePath, '')
23-
fs.copyFileSync(originalFilePath, filePath)
27+
await fs.remove(path.join(__dirname, '_test-output'))
28+
await fs.outputFile(filePath, '')
29+
await fs.copyFile(originalFilePath, filePath)
2430

2531
file = Object.assign(new EventEmitter(), {
2632
filePath,
2733
outputPath,
2834
})
2935
})
3036

37+
afterEach(async () => {
38+
if (file.shouldWatch) {
39+
await new Promise((resolve) => {
40+
file.emit('close', resolve)
41+
})
42+
}
43+
})
44+
3145
it('correctly preprocesses the file', () => {
3246
return preprocessor()(file).then(() => {
3347
snapshot(fs.readFileSync(outputPath).toString())
3448
})
3549
})
3650

37-
it('allows attaching catch later on syntax error without triggering unhandled rejection', (done) => {
51+
it('allows attaching catch later on syntax error without triggering unhandled rejection', async () => {
3852
process.on('unhandledRejection', (err) => {
3953
// eslint-disable-next-line no-console
4054
console.error('Unhandled Rejection:', err.stack)
41-
done('Should not have trigger unhandled rejection')
55+
throw new Error('Should not have trigger unhandled rejection')
4256
})
4357

4458
file.shouldWatch = true
4559

46-
preprocessor()(file).then(() => {
47-
fs.outputFileSync(filePath, '{')
60+
await preprocessor()(file)
61+
await fs.outputFile(filePath, '{')
4862

63+
await new Promise((resolve) => {
4964
setTimeout(() => {
5065
preprocessor()(file)
5166
.catch((err) => {
5267
expect(err.stack).to.include('Unexpected token')
53-
file.emit('close')
54-
done()
68+
resolve()
5569
})
5670
}, 1000)
5771
})
5872
})
73+
74+
it('triggers rerun on syntax error', async () => {
75+
const _emit = sinon.spy(file, 'emit')
76+
77+
file.shouldWatch = true
78+
79+
await preprocessor()(file)
80+
81+
_emit.resetHistory()
82+
83+
await fs.outputFile(filePath, '{')
84+
85+
await retry(() => expect(_emit).calledWith('rerun'))
86+
})
5987
})
88+
89+
function retry (fn, timeout = 1000) {
90+
let timedOut = false
91+
92+
setTimeout(() => timedOut = true, timeout)
93+
const tryFn = () => {
94+
return Bluebird.try(() => {
95+
return fn()
96+
})
97+
98+
.catch((err) => {
99+
if (timedOut) {
100+
throw err
101+
}
102+
103+
return Bluebird.delay(100).then(() => tryFn())
104+
})
105+
}
106+
107+
return tryFn()
108+
}

test/unit/index.spec.js

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ const expect = chai.expect
88

99
chai.use(require('sinon-chai'))
1010

11-
const sandbox = sinon.sandbox.create()
12-
const webpack = sandbox.stub()
11+
const webpack = sinon.stub()
1312

1413
mockery.enable({
1514
warnOnUnregistered: false,
@@ -22,16 +21,16 @@ const stubbableRequire = require('../../stubbable-require')
2221

2322
describe('webpack preprocessor', function () {
2423
beforeEach(function () {
25-
sandbox.restore()
24+
sinon.restore()
2625

2726
this.watchApi = {
28-
close: sandbox.spy(),
27+
close: sinon.spy(),
2928
}
3029

3130
this.compilerApi = {
32-
run: sandbox.stub(),
33-
watch: sandbox.stub().returns(this.watchApi),
34-
plugin: sandbox.stub(),
31+
run: sinon.stub(),
32+
watch: sinon.stub().returns(this.watchApi),
33+
plugin: sinon.stub(),
3534
}
3635

3736
webpack.returns(this.compilerApi)
@@ -49,14 +48,14 @@ describe('webpack preprocessor', function () {
4948
filePath: 'path/to/file.js',
5049
outputPath: 'output/output.js',
5150
shouldWatch: false,
52-
on: sandbox.stub(),
53-
emit: sandbox.spy(),
51+
on: sinon.stub(),
52+
emit: sinon.spy(),
5453
}
5554

5655
this.util = {
57-
getOutputPath: sandbox.stub().returns(this.outputPath),
58-
fileUpdated: sandbox.spy(),
59-
onClose: sandbox.stub(),
56+
getOutputPath: sinon.stub().returns(this.outputPath),
57+
fileUpdated: sinon.spy(),
58+
onClose: sinon.stub(),
6059
}
6160

6261
this.run = (options, file = this.file) => {
@@ -239,7 +238,7 @@ describe('webpack preprocessor', function () {
239238
})
240239

241240
it('requires babel dependencies when default options are used', function () {
242-
sandbox.spy(stubbableRequire, 'resolve')
241+
sinon.spy(stubbableRequire, 'resolve')
243242

244243
return this.run().then(() => {
245244
expect(stubbableRequire.resolve).to.be.calledWith('babel-loader')
@@ -248,7 +247,7 @@ describe('webpack preprocessor', function () {
248247
})
249248

250249
it('does not requires babel dependencies when user options are non-default', function () {
251-
sandbox.spy(stubbableRequire, 'resolve')
250+
sinon.spy(stubbableRequire, 'resolve')
252251
const options = { webpackOptions: { module: { rules: [] } } }
253252

254253
return this.run(options).then(() => {

0 commit comments

Comments
 (0)