test/unit/index.spec.js
'use strict'
const expect = require('chai').expect
const sinon = require('sinon')
const del = require('del')
const path = require('path')
const CleanObsoleteChunks = require('./../../index')
describe('CleanObsoleteChunks', () => {
describe('instance', () => {
let inst
beforeEach(() => {
inst = new CleanObsoleteChunks()
})
describe('method _retrieveAllChunks(compilations)', () => {
describe('in order to retrieve all chunks', () => {
const compilation = {
chunks: [{id: 1}, {id: 2}, {id: 3}],
children: [{
name: 'child-compilation',
chunks: [{id: 1}, {id: 2}, {id: 3}],
children: []
}]
}
it('SHOULD return array with all chunks of INITIAL compilation only if {deep: false} option provided', () => {
const chunks = inst._retrieveAllChunks([compilation])
expect(chunks).to.have.length(compilation.chunks.length)
})
it('SHOULD return array with all chunks of all compilations if {deep: true} option provided', () => {
inst.options.deep = true
const chunks = inst._retrieveAllChunks([compilation])
expect(chunks).to.have.length(compilation.chunks.concat(compilation.children[0].chunks).length)
})
it('SHOULD add correct uniqueId to all chunks in all compilations', () => {
inst.options.deep = true
const chunks = inst._retrieveAllChunks([compilation])
chunks.forEach(chunk => {
expect('uniqueId' in chunk).to.be.true
})
})
})
})
describe('method _saveChunkConfig(chunk)', () => {
describe('in order to save chunks versions', () => {
it('SHOULD add or update its deep chunkVersions[chunk.name] property', () => {
let chunkId = 1
expect(inst.chunkVersions.get(chunkId)).to.be.equal(undefined)
let chunk = {
uniqueId: chunkId,
files: ['test-file-name1'],
hash: 'hash1'
}
inst._saveChunkConfig(chunk)
let oldValue = inst.chunkVersions.get(chunkId)
expect(oldValue).to.not.be.equal(undefined)
let updatedChunk = Object.assign(chunk, {files: ['test-file-name2'], hash: 'hash2'})
inst._saveChunkConfig(updatedChunk)
expect(oldValue).to.not.be.equal(inst.chunkVersions.get(chunkId))
})
it(
'SHOULD set its deep chunkVersions[chunk.name][\'files\'] property with chunk.files value',
() => {
let files = ['file1', 'file2', 'file3']
let chunkId = 1
let chunk = {
uniqueId: chunkId,
files: files,
hash: 'hash'
}
inst._saveChunkConfig(chunk)
expect(inst.chunkVersions.get(chunkId)['files']).to.be.equal(files)
}
)
})
})
describe('method _getObsoleteFiles(compilation)', () => {
describe('in order to get obsolete chunk files', () => {
it('SHOULD call _retrieveAllChunks() with [compilation] argument',
() => {
let compilation = Math.random()
let _retrieveAllChunks = sinon.stub(inst, '_retrieveAllChunks')
_retrieveAllChunks.returns([])
expect(_retrieveAllChunks.notCalled).to.be.true
inst._getObsoleteFiles(compilation)
expect(_retrieveAllChunks.calledOnce).to.be.true
expect(_retrieveAllChunks.args[0][0]).to.be.deep.equal([compilation])
})
it('SHOULD call _getChunkObsoleteFiles(chunk) for each chunk in compilation.chunks',
() => {
let compilation = {
chunks: [{id: 1}, {id: 2}, {id: 3}]
}
let _getChunkObsoleteFiles = sinon.stub(inst, '_getChunkObsoleteFiles')
expect(_getChunkObsoleteFiles.notCalled).to.be.true
inst._getObsoleteFiles(compilation)
expect(_getChunkObsoleteFiles.callCount).to.be.equal(3)
_getChunkObsoleteFiles.args.forEach((args, index) => {
expect(args[0].id).to.be.deep.equal(compilation.chunks[index].id)
})
})
it('SHOULD return concatenated array of obsolete files of all of the chunks (if any)',
() => {
let compilation = {
chunks: ['chunk1', 'chunk2', 'chunk3']
}
let _getChunkObsoleteFiles = sinon.stub(inst, '_getChunkObsoleteFiles')
_getChunkObsoleteFiles.onFirstCall().returns(['file1', 'file2'])
_getChunkObsoleteFiles.onSecondCall().returns(['file3', 'file4', 'file5'])
_getChunkObsoleteFiles.onThirdCall().returns(['file6'])
expect(inst._getObsoleteFiles(compilation)).to.be.deep.equal(
['file1', 'file2', 'file3', 'file4', 'file5', 'file6']
)
})
it('SHOULD return empty array if there are no chunks', () => {
let compilation = {
chunks: []
}
expect(inst._getObsoleteFiles(compilation)).to.be.deep.equal([])
})
it('SHOULD return empty array if there are no obsolete files in all of the chunks', () => {
let compilation = {
chunks: ['chunk1', 'chunk2', 'chunk3']
}
sinon.stub(inst, '_getChunkObsoleteFiles').returns([])
expect(inst._getObsoleteFiles(compilation)).to.be.deep.equal([])
})
})
})
describe('method _getChunkObsoleteFiles(chunk)', () => {
describe('in order to get obsolete chunk files', () => {
it('SHOULD return empty array if there is no chunkVersions[chunk.name]', () => {
let chunk = {
uniqueId: 1
}
expect(inst._getChunkObsoleteFiles(chunk)).to.be.deep.equal([])
})
it('SHOULD return only obsolete files', () => {
const chunkId = 1
let oldChunk = {
uniqueId: chunkId,
files: ['file1(old-name)', 'file2(old-name)', 'file3(old-name)']
}
inst.chunkVersions.set(chunkId, {
files: oldChunk.files
})
let newChunk = Object.assign(oldChunk, {
files: ['file1(old-name)', 'file2(old-name)', 'file3(new-name)']
})
expect(inst._getChunkObsoleteFiles(newChunk)).to.be.deep.equal(['file3(old-name)'])
})
it('SHOULD call _saveChunkConfig(chunk) method in any case',
() => {
let chunk = {
name: 'test',
files: ['file1', 'file2']
}
let _saveChunkConfig = sinon.stub(inst, '_saveChunkConfig')
// 1) if there is no chunkVersions[chunk.name]
expect(_saveChunkConfig.called).to.be.false
inst._getChunkObsoleteFiles(chunk)
expect(_saveChunkConfig.callCount).to.be.equal(1)
expect(_saveChunkConfig.args[0][0]).to.be.deep.equal(chunk)
_saveChunkConfig.reset()
// 2) otherwise
inst.chunkVersions[chunk.name] = {files: chunk.files}
expect(_saveChunkConfig.called).to.be.false
inst._getChunkObsoleteFiles(chunk)
expect(_saveChunkConfig.callCount).to.be.equal(1)
expect(_saveChunkConfig.args[0][0]).to.be.deep.equal(chunk)
})
})
})
describe('method apply(compiler)', () => {
describe('in order to remove obsolete chunks files in webpack watch mode', () => {
beforeEach(() => {
sinon.stub(inst._removeObsoleteFiles, 'bind').returns(inst._removeObsoleteFiles)
})
afterEach(() => {
inst._removeObsoleteFiles.bind.restore()
})
it(
`SHOULD get hooked on compiler 'after-emit' event and pass _removeObsoleteFiles method
as a callback`,
() => {
let hook = sinon.stub()
let compiler = {
hooks: {
afterEmit: {tapAsync: hook}
}
}
inst.apply(compiler)
expect(hook.callCount).to.be.equal(1)
expect(hook.args[0][0]).to.be.equal('webpack-clean-obsolete-chunks')
expect(hook.args[0][1]).to.be.equal(inst._removeObsoleteFiles)
expect(inst._removeObsoleteFiles.bind.called).to.be.true
expect(inst._removeObsoleteFiles.bind.args[0]).to.be.deep.equal([inst, compiler])
})
it(
'SHOULD support legacy compiler.plugin hooks',
() => {
let compiler = {
plugin: sinon.stub()
}
inst.apply(compiler)
expect(compiler.plugin.callCount).to.be.equal(1)
expect(compiler.plugin.args[0][0]).to.be.equal('after-emit')
expect(compiler.plugin.args[0][1]).to.be.equal(inst._removeObsoleteFiles)
expect(inst._removeObsoleteFiles.bind.called).to.be.true
expect(inst._removeObsoleteFiles.bind.args[0]).to.be.deep.equal([inst, compiler])
})
})
})
describe('method _removeObsoleteFiles(compiler, compilation, done)', () => {
describe('in order to remove obsolete chunks files in webpack watch mode', () => {
let done
let compilation
let compiler
let _getObsoleteFiles
beforeEach(() => {
compilation = Math.random()
_getObsoleteFiles = sinon.stub(inst, '_getObsoleteFiles').returns([])
done = sinon.stub()
sinon.stub(del, 'sync')
})
afterEach(() => {
del.sync.restore()
})
it('SHOULD call _getObsoleteFiles(compilation) method', () => {
expect(_getObsoleteFiles.notCalled).to.be.true
inst._removeObsoleteFiles(compiler, compilation, done)
expect(_getObsoleteFiles.callCount).to.be.equal(1)
expect(_getObsoleteFiles.args[0][0]).to.be.equal(compilation)
})
it('SHOULD call del.sync(filePath) for each obsolete file', () => {
compiler = {
outputPath: 'absolute/path/to/output/folder/'
}
let obsoleteFiles = ['obsolete-file1', 'obsolete-file2', 'obsolete-file3']
_getObsoleteFiles.returns(obsoleteFiles)
expect(del.sync.notCalled).to.be.true
inst._removeObsoleteFiles(compiler, compilation, done)
expect(del.sync.callCount).to.be.equal(obsoleteFiles.length)
del.sync.args.forEach((args, index) => {
expect(args[0]).to.be.deep.equal(path.join(compiler.outputPath, obsoleteFiles[index]))
})
})
it('SHOULD call done() callback at the end', () => {
expect(done.notCalled).to.be.true
inst._removeObsoleteFiles(compiler, compilation, done)
expect(done.callCount).to.be.equal(1)
})
})
})
describe('method _setOptions()', () => {
describe('in order to set default options correctly', () => {
beforeEach(() => {
inst._setOptions() // no options provided
})
it(
'SHOULD set verbose option to true',
() => {
expect(inst.options.verbose).to.be.true
})
it(
'SHOULD set deep option to false',
() => {
expect(inst.options.deep).to.be.false
})
})
describe('in order to act correctly with verbose option', () => {
beforeEach(() => {
sinon.stub(console, 'info') // no options provided
})
afterEach(() => {
console.info.restore() // eslint-disable-line no-console
})
it(
'SHOULD call console.info if verbose option === true',
() => {
inst._setOptions({verbose: true})
expect(console.info.notCalled).to.be.true // eslint-disable-line no-console
inst._consoleInfo('test')
expect(console.info.callCount).to.be.equal(1) // eslint-disable-line no-console
})
it(
'SHOULD NOT call console.info if verbose option === false',
() => {
inst._setOptions({verbose: false})
inst._consoleInfo('test')
expect(console.info.notCalled).to.be.true // eslint-disable-line no-console
})
})
})
})
})