diff --git a/src/cleancss.spec.ts b/src/cleancss.spec.ts new file mode 100644 index 00000000..7e4700a4 --- /dev/null +++ b/src/cleancss.spec.ts @@ -0,0 +1,182 @@ +import { join } from 'path'; +import * as rewire from 'rewire'; + +const cleanCss = rewire('./cleancss'); + +import * as cleanCssFactory from './util/clean-css-factory'; +import * as config from './util/config'; +import * as helpers from './util/helpers'; +import * as workerClient from './worker-client'; + + +describe('clean css task', () => { + + describe('cleancss', () => { + it('should return when the worker returns', async () => { + // arrange + const context = { }; + const configFile: any = null; + const spy = spyOn(workerClient, workerClient.runWorker.name).and.returnValue(Promise.resolve()); + // act + await (cleanCss as any).cleancss(context, null); + // assert + expect(spy).toHaveBeenCalledWith('cleancss', 'cleancssWorker', context, configFile); + }); + + it('should throw when the worker throws', async () => { + // arrange + const context = { }; + const errorMessage = 'Simulating an error'; + spyOn(workerClient, workerClient.runWorker.name).and.throwError(errorMessage); + try { + // act + await (cleanCss as any).cleancss(context, null); + throw new Error('Should never get here'); + } catch (ex) { + // assert + expect(ex.message).toEqual(errorMessage, `Expected ex.message ${ex.message} to equal ${errorMessage}`); + } + }); + }); + + describe('cleancssworker', () => { + it('should throw when reading the file throws', async (done) => { + const errorMessage = 'simulating an error'; + try { + // arrange + const context = { buildDir: 'www'}; + const cleanCssConfig = { sourceFileName: 'sourceFileName', destFileName: 'destFileName'}; + spyOn(config, config.generateContext.name).and.returnValue(context); + spyOn(config, config.fillConfigDefaults.name).and.returnValue(cleanCssConfig); + spyOn(helpers, helpers.readFileAsync.name).and.throwError(errorMessage); + + // act + await (cleanCss as any).cleancssWorker(context, null); + + throw new Error('Should never get here'); + } catch (ex) { + expect(ex.message).toEqual(errorMessage); + done(); + } + }); + + it('should return what writeFileAsync returns', async (done) => { + // arrange + const context = { buildDir: 'www'}; + const cleanCssConfig = { sourceFileName: 'sourceFileName', destFileName: 'destFileName'}; + const fileContent = 'content'; + const minifiedContent = 'someContent'; + spyOn(config, config.generateContext.name).and.returnValue(context); + spyOn(config, config.fillConfigDefaults.name).and.returnValue(cleanCssConfig); + spyOn(helpers, helpers.readFileAsync.name).and.returnValue(Promise.resolve(fileContent)); + spyOn(helpers, helpers.writeFileAsync.name).and.returnValue(Promise.resolve()); + + // use rewire to stub this since jasmine is insufficient + const spy = jasmine.createSpy('mySpy').and.returnValue(Promise.resolve(minifiedContent)); + cleanCss.__set__('runCleanCss', spy); + + // act + await (cleanCss as any).cleancssWorker(context, null); + + // assert + expect(config.generateContext).toHaveBeenCalledWith(context); + expect(config.fillConfigDefaults).toHaveBeenCalledWith(null, (cleanCss as any).taskInfo.defaultConfigFile); + expect(helpers.readFileAsync).toHaveBeenCalledWith(join(context.buildDir, cleanCssConfig.sourceFileName)); + expect(helpers.writeFileAsync).toHaveBeenCalledWith(join(context.buildDir, cleanCssConfig.destFileName), minifiedContent); + expect(spy).toHaveBeenCalledWith(cleanCssConfig, fileContent); + done(); + }); + }); + + describe('runCleanCss', () => { + it('should reject when minification errors out', async (done) => { + // arrange + const errorMessage = 'simulating an error'; + const configFile = { options: {} }; + const fileContent = 'fileContent'; + let minifySpy: jasmine.Spy = null; + try { + const destinationFilePath = 'filePath'; + const mockMinifier = { + minify: () => {} + }; + minifySpy = spyOn(mockMinifier, mockMinifier.minify.name); + spyOn(cleanCssFactory, cleanCssFactory.getCleanCssInstance.name).and.returnValue(mockMinifier); + + // act + const promise = (cleanCss as any).runCleanCss(configFile, fileContent, destinationFilePath); + // call the callback from the spy's args + const callback = minifySpy.calls.mostRecent().args[1]; + callback(new Error(errorMessage), null); + + await promise; + + throw new Error('Should never get here'); + } catch (ex) { + // assert + expect(ex.message).toEqual(errorMessage); + done(); + } + }); + + it('should reject when minification has one or more errors', async (done) => { + // arrange + const configFile = { options: {} }; + const fileContent = 'fileContent'; + let minifySpy: jasmine.Spy = null; + const minificationResponse = { + errors: ['some error'] + }; + try { + const destinationFilePath = 'filePath'; + const mockMinifier = { + minify: () => {} + }; + minifySpy = spyOn(mockMinifier, mockMinifier.minify.name); + spyOn(cleanCssFactory, cleanCssFactory.getCleanCssInstance.name).and.returnValue(mockMinifier); + + // act + const promise = (cleanCss as any).runCleanCss(configFile, fileContent, destinationFilePath); + // call the callback from the spy's args + const callback = minifySpy.calls.mostRecent().args[1]; + callback(null, minificationResponse); + + await promise; + + throw new Error('Should never get here'); + } catch (ex) { + // assert + expect(ex.message).toEqual(minificationResponse.errors[0]); + done(); + } + }); + + it('should return minified content', async (done) => { + const configFile = { options: {} }; + const fileContent = 'fileContent'; + let minifySpy: jasmine.Spy = null; + const minificationResponse = { + styles: 'minifiedContent' + }; + const destinationFilePath = 'filePath'; + const mockMinifier = { + minify: () => {} + }; + minifySpy = spyOn(mockMinifier, mockMinifier.minify.name); + spyOn(cleanCssFactory, cleanCssFactory.getCleanCssInstance.name).and.returnValue(mockMinifier); + + // act + const promise = (cleanCss as any).runCleanCss(configFile, fileContent, destinationFilePath); + // call the callback from the spy's args + const callback = minifySpy.calls.mostRecent().args[1]; + callback(null, minificationResponse); + + const result = await promise; + expect(result).toEqual(minificationResponse.styles); + expect(cleanCssFactory.getCleanCssInstance).toHaveBeenCalledWith(configFile.options); + expect(minifySpy.calls.mostRecent().args[0]).toEqual(fileContent); + done(); + }); + }); +}); + diff --git a/src/cleancss.ts b/src/cleancss.ts index afe743c4..2f4534a0 100644 --- a/src/cleancss.ts +++ b/src/cleancss.ts @@ -4,74 +4,59 @@ import { BuildError } from './util/errors'; import { fillConfigDefaults, generateContext, getUserConfigFile } from './util/config'; import { Logger } from './logger/logger'; import { readFileAsync, writeFileAsync } from './util/helpers'; -import { runWorker } from './worker-client'; -import * as cleanCss from 'clean-css'; +import * as workerClient from './worker-client'; +import { CleanCssConfig, getCleanCssInstance } from './util/clean-css-factory'; -export function cleancss(context: BuildContext, configFile?: string) { - configFile = getUserConfigFile(context, taskInfo, configFile); - +export async function cleancss(context: BuildContext, configFile?: string) { const logger = new Logger('cleancss'); - - return runWorker('cleancss', 'cleancssWorker', context, configFile) - .then(() => { - logger.finish(); - }) - .catch(err => { - throw logger.fail(err); - }); + try { + configFile = getUserConfigFile(context, taskInfo, configFile); + await workerClient.runWorker('cleancss', 'cleancssWorker', context, configFile); + logger.finish(); + } catch (ex) { + throw logger.fail(ex); + } } -export function cleancssWorker(context: BuildContext, configFile: string): Promise { - return new Promise((resolve, reject) => { - context = generateContext(context); - const cleanCssConfig: CleanCssConfig = fillConfigDefaults(configFile, taskInfo.defaultConfigFile); - const srcFile = join(context.buildDir, cleanCssConfig.sourceFileName); - const destFile = join(context.buildDir, cleanCssConfig.destFileName); - - Logger.debug(`cleancss read: ${srcFile}`); - - readFileAsync(srcFile).then(fileContent => { - const minifier = new cleanCss(cleanCssConfig.options); - minifier.minify(fileContent, (err, minified) => { - if (err) { - reject(new BuildError(err)); - - } else if (minified.errors && minified.errors.length > 0) { - // just return the first error for now I guess - minified.errors.forEach(e => { - Logger.error(e); - }); - reject(new BuildError()); +export async function cleancssWorker(context: BuildContext, configFile: string): Promise { + context = generateContext(context); + const config: CleanCssConfig = fillConfigDefaults(configFile, taskInfo.defaultConfigFile); + const srcFile = join(context.buildDir, config.sourceFileName); + const destFilePath = join(context.buildDir, config.destFileName); + Logger.debug(`[Clean CSS] cleancssWorker: reading source file ${srcFile}`); + const fileContent = await readFileAsync(srcFile); + const minifiedContent = await runCleanCss(config, fileContent); + Logger.debug(`[Clean CSS] runCleanCss: writing file to disk ${destFilePath}`); + await writeFileAsync(destFilePath, minifiedContent); +} - } else { - Logger.debug(`cleancss write: ${destFile}`); - writeFileAsync(destFile, minified.styles).then(() => { - resolve(); - }); - } - }); +// exporting for easier unit testing +export function runCleanCss(cleanCssConfig: CleanCssConfig, fileContent: string): Promise { + return new Promise((resolve, reject) => { + const minifier = getCleanCssInstance(cleanCssConfig.options); + minifier.minify(fileContent, (err, minified) => { + if (err) { + reject(new BuildError(err)); + } else if (minified.errors && minified.errors.length > 0) { + // just return the first error for now I guess + minified.errors.forEach(e => { + Logger.error(e); + }); + reject(new BuildError(minified.errors[0])); + } else { + resolve(minified.styles); + } }); - }); } - -const taskInfo: TaskInfo = { +// export for testing only +export const taskInfo: TaskInfo = { fullArg: '--cleancss', shortArg: '-e', envVar: 'IONIC_CLEANCSS', packageConfig: 'ionic_cleancss', defaultConfigFile: 'cleancss.config' }; - - -export interface CleanCssConfig { - // https://www.npmjs.com/package/clean-css - sourceFileName: string; - // sourceSourceMapName: string; - destFileName: string; - // options: cleanCss Options; - options?: cleanCss.Options; -}