Skip to content
This repository was archived by the owner on Oct 1, 2020. It is now read-only.

fix: The 'rerun' event is no longer emitted on the initial bundling of a file #103

Merged
merged 3 commits into from
Aug 12, 2020
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
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ examples/use-babelrc/cypress/integration/spec.js
dist
tsconfig.json
.vscode/
.history
32 changes: 22 additions & 10 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ const debug = require('debug')('cypress:webpack')
const debugStats = require('debug')('cypress:webpack:stats')

type FilePath = string
interface BundleObject {
promise: Promise<FilePath>
initial: boolean
}

// bundle promises from input spec filename to output bundled file paths
let bundles: {[key: string]: Promise<FilePath>} = {}
let bundles: {[key: string]: BundleObject} = {}

// we don't automatically load the rules, so that the babel dependencies are
// not required if a user passes in their own configuration
Expand Down Expand Up @@ -164,7 +168,7 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F
if (bundles[filePath]) {
debug(`already have bundle for ${filePath}`)

return bundles[filePath]
return bundles[filePath].promise
}

const defaultWebpackOptions = getDefaultWebpackOptions()
Expand Down Expand Up @@ -229,7 +233,10 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F

// cache the bundle promise, so it can be returned if this function
// is invoked again with the same filePath
bundles[filePath] = latestBundle.promise
bundles[filePath] = {
promise: latestBundle.promise,
initial: true,
}

const rejectWithErr = (err: Error) => {
err = quietErrorMessage(err)
Expand Down Expand Up @@ -294,19 +301,24 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F
// we overwrite the latest bundle, so that a new call to this function
// returns a promise that resolves when the bundling is finished
latestBundle = createDeferred<string>()
bundles[filePath] = latestBundle.promise
bundles[filePath].promise = latestBundle.promise

bundles[filePath].finally(() => {
debug('- compile finished for', filePath)
bundles[filePath].promise.finally(() => {
debug('- compile finished for %s, initial? %s', filePath, bundles[filePath].initial)
// when the bundling is finished, emit 'rerun' to let Cypress
// know to rerun the spec
file.emit('rerun')
// know to rerun the spec, but NOT when it is the initial
// bundling of the file
if (!bundles[filePath].initial) {
file.emit('rerun')
}

bundles[filePath].initial = false
})
// we suppress unhandled rejections so they don't bubble up to the
// unhandledRejection handler and crash the process. Cypress will
// eventually take care of the rejection when the file is requested.
// note that this does not work if attached to latestBundle.promise
// for some reason. it only works when attached after .tap ¯\_(ツ)_/¯
// for some reason. it only works when attached after .finally ¯\_(ツ)_/¯
.suppressUnhandledRejections()
}

Expand Down Expand Up @@ -341,7 +353,7 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F

// return the promise, which will resolve with the outputPath or reject
// with any error encountered
return bundles[filePath]
return bundles[filePath].promise
}
}

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
"secure": "nsp check",
"semantic-release": "semantic-release",
"size": "npm pack --dry",
"pretest": "yarn lint && yarn build",
"test": "yarn test-unit && yarn test-e2e",
"test-debug": "node --inspect --debug-brk ./node_modules/.bin/_mocha",
"test-e2e": "mocha test/e2e/*.spec.*",
"test-unit": "mocha test/unit/*.spec.*",
"test-watch": "yarn test-unit & chokidar '**/*.(js|ts)' 'test/unit/*.(js|ts)' -c 'yarn test-unit'",
"types": "tsc --noEmit"
"types": "tsc --noEmit",
"watch": "yarn build --watch"
},
"husky": {
"hooks": {
Expand All @@ -37,6 +37,7 @@
"@babel/plugin-proposal-nullish-coalescing-operator": "7.8.3",
"@babel/preset-env": "^7.0.0",
"@cypress/eslint-plugin-dev": "5.0.0",
"@fellow/eslint-plugin-coffee": "0.4.13",
"@types/webpack": "4.41.12",
"@typescript-eslint/eslint-plugin": "2.31.0",
"@typescript-eslint/parser": "2.31.0",
Expand Down
55 changes: 36 additions & 19 deletions test/e2e/compilation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,20 @@ const normalizeErrMessage = (message) => {
const fixturesDir = path.join(__dirname, '..', 'fixtures')
const outputDir = path.join(__dirname, '..', '_test-output')

const createFile = ({ name = 'example_spec.js', shouldWatch = false } = {}) => {
return Object.assign(new EventEmitter(), {
filePath: path.join(outputDir, name),
outputPath: path.join(outputDir, name.replace('.', '_output.')),
shouldWatch,
})
}

describe('webpack preprocessor - e2e', () => {
let run
let file

beforeEach(async () => {
preprocessor.__reset()

run = ({ options, keepFile, shouldWatch = false, fileName = 'example_spec.js' } = {}) => {
if (!keepFile) {
file = Object.assign(new EventEmitter(), {
filePath: path.join(outputDir, fileName),
outputPath: path.join(outputDir, fileName.replace('.', '_output.')),
shouldWatch,
})
}

return preprocessor(options)(file)
}

await fs.remove(outputDir)
await fs.copy(fixturesDir, outputDir)
})
Expand All @@ -54,14 +49,17 @@ describe('webpack preprocessor - e2e', () => {
const options = preprocessor.defaultOptions

options.webpackOptions.mode = 'production' // snapshot will be minified
file = createFile()

return run({ options }).then((outputPath) => {
return preprocessor(options)(file).then((outputPath) => {
snapshot(fs.readFileSync(outputPath).toString())
})
})

it('has less verbose "Module not found" error', () => {
return run({ fileName: 'imports_nonexistent_file_spec.js' })
file = createFile({ name: 'imports_nonexistent_file_spec.js' })

return preprocessor()(file)
.then(() => {
throw new Error('Should not resolve')
})
Expand All @@ -71,7 +69,9 @@ describe('webpack preprocessor - e2e', () => {
})

it('has less verbose syntax error', () => {
return run({ fileName: 'syntax_error_spec.js' })
file = createFile({ name: 'syntax_error_spec.js' })

return preprocessor()(file)
.then(() => {
throw new Error('Should not resolve')
})
Expand All @@ -87,12 +87,14 @@ describe('webpack preprocessor - e2e', () => {
throw new Error('Should not have trigger unhandled rejection')
})

await run({ shouldWatch: true })
file = createFile({ shouldWatch: true })

await preprocessor()(file)
await fs.outputFile(file.filePath, '{')

await new Promise((resolve) => {
setTimeout(() => {
run({ keepFile: true, shouldWatch: true })
preprocessor()(file)
.catch((err) => {
expect(err.stack).to.include('Unexpected token')
resolve()
Expand All @@ -102,14 +104,29 @@ describe('webpack preprocessor - e2e', () => {
})

it('triggers rerun on syntax error', async () => {
await run({ shouldWatch: true })
file = createFile({ shouldWatch: true })

await preprocessor()(file)

const _emit = sinon.spy(file, 'emit')

await fs.outputFile(file.filePath, '{')

await retry(() => expect(_emit).calledWith('rerun'))
})

it('does not call rerun on initial build, but on subsequent builds', async () => {
file = createFile({ shouldWatch: true })
const _emit = sinon.spy(file, 'emit')

await preprocessor()(file)

expect(_emit).not.to.be.calledWith('rerun')

await fs.outputFile(file.filePath, 'console.log()')

await retry(() => expect(_emit).calledWith('rerun'))
})
})

function retry (fn, timeout = 1000) {
Expand Down
14 changes: 12 additions & 2 deletions test/unit/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const chai = require('chai')
const mockery = require('mockery')
const Promise = require('bluebird')
const sinon = require('sinon')

const expect = chai.expect
Expand Down Expand Up @@ -247,12 +248,21 @@ describe('webpack preprocessor', function () {
})
})

it('emits "rerun" when shouldWatch is true and there is an update', function () {
it('emits "rerun" when shouldWatch is true after there is an update', function () {
this.file.shouldWatch = true
this.compilerApi.watch.yields(null, this.statsApi)
this.compilerApi.plugin.withArgs('compile').yields()

return this.run().then(() => {
return this.run()
.then(() => {
expect(this.file.emit).not.to.be.calledWith('rerun')

this.compilerApi.plugin.withArgs('compile').yield()
this.compilerApi.watch.yield(null, this.statsApi)

return Promise.delay(10) // give assertion time till next tick
})
.then(() => {
expect(this.file.emit).to.be.calledWith('rerun')
})
})
Expand Down
Loading