From 8270be84345b5bd57293c02f8326ce51f4a1248b Mon Sep 17 00:00:00 2001 From: Ricardo Machado Date: Thu, 2 Feb 2017 10:43:02 +0100 Subject: [PATCH] Adds commander as a dependency to create a CLI structure Refactors code to have a CLI (registering the binary in the package.json) and watcher embedded Moves the saving of the theme file to the getStyles.js, renaming it to 'getStylesAndSaveTheme.js' Updates the jest.config.json to be compliant with the latest Jest's config format Updates the package.json to use the builder for building and watching purposes Updates the README.md with the usage of the CLI Updates the development section Adds comments to the code and fixes the 'usage name' of a module Changes the jest config to include non-covered scripts Adds tests for the builder/* files and creates a new module to specifically handle the generation of the theme file Improves the linting and jest's config to work properly Adds wallaby.js (a config file for wallabyjs.com's tool) Adds the files with a '100%' coverage on statements Updates the package.json to run the babel command in parallel with the build:*/build process Fixes the missing commands --- .eslintrc | 6 +- .gitignore | 1 + README.md | 69 ++++++++- bin/build-theme.js | 22 --- builder/builder.js | 28 ++++ builder/copyToFinalDestination.js | 34 +++++ builder/generateThemeFile.js | 24 +++ builder/getStylesAndSaveTheme.js | 25 +++ builder/index.js | 18 +++ ...unWebpackAndCopyFilesToFinalDestination.js | 38 +++++ builder/saveThemeScssFile.js | 18 +++ .../webpack.config.js | 11 +- package.json | 12 +- scripts/getStyles.js | 20 --- tests/jest.config.json | 10 +- tests/unit/builder/builder.spec.js | 35 +++++ .../builder/copyToFinalDestination.spec.js | 64 ++++++++ tests/unit/builder/generateThemeFile.spec.js | 53 +++++++ .../builder/getStylesAndSaveTheme.spec.js | 60 ++++++++ tests/unit/builder/index.spec.js | 48 ++++++ ...packAndCopyFilesToFinalDestination.spec.js | 142 ++++++++++++++++++ tests/unit/builder/saveThemeScssFile.spec.js | 36 +++++ 22 files changed, 716 insertions(+), 58 deletions(-) delete mode 100644 bin/build-theme.js create mode 100644 builder/builder.js create mode 100644 builder/copyToFinalDestination.js create mode 100644 builder/generateThemeFile.js create mode 100644 builder/getStylesAndSaveTheme.js create mode 100755 builder/index.js create mode 100644 builder/runWebpackAndCopyFilesToFinalDestination.js create mode 100644 builder/saveThemeScssFile.js rename webpack.config.js => builder/webpack.config.js (78%) delete mode 100644 scripts/getStyles.js create mode 100644 tests/unit/builder/builder.spec.js create mode 100644 tests/unit/builder/copyToFinalDestination.spec.js create mode 100644 tests/unit/builder/generateThemeFile.spec.js create mode 100644 tests/unit/builder/getStylesAndSaveTheme.spec.js create mode 100644 tests/unit/builder/index.spec.js create mode 100644 tests/unit/builder/runWebpackAndCopyFilesToFinalDestination.spec.js create mode 100644 tests/unit/builder/saveThemeScssFile.spec.js diff --git a/.eslintrc b/.eslintrc index 529424c1..9cbf6a62 100644 --- a/.eslintrc +++ b/.eslintrc @@ -13,11 +13,13 @@ ], "globals": { "__base": true, - "describe": true, - "it": true, "assert": true, + "beforeEach": true, + "describe": true, "enzyme": true, "expect": true, + "it": true, + "jest": true, "React": true, "ReactDOM": true }, diff --git a/.gitignore b/.gitignore index fbb70717..6f0bdb41 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ node_modules/ styleguide/ themes/theme.scss .DS_Store +wallaby.js diff --git a/README.md b/README.md index cbcd0e75..62eb745a 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,66 @@ Travix UI Components' repository. ## UI-Kit ### How to install and setup - `npm i travix-ui-kit -S` install as a dependency -- add npm script `build` with value `cd node_modules/travix-ui-kit/ && npm run build && cd ../..` to your `package.json` -### How to use -#### JS +### Usage + +#### CLI + +The UI Kit comes with a CLI tool to help you build your UI bundles (JS and CSS). + +To see the options available: + +```bash +$ node_modules/.bin/travix-ui-kit -h + + Usage: travix-ui-kit [options] + + Options: + + -h, --help output usage information + -V, --version output the version number + -c, --css-dir Destination directory of the ui-kit.css + -j, --js-dir Destination directory of the ui-kit.js + -t, --theme-file Path to a theme file to override default UI Kit styles + -w, --watch Enables file-watcher functionality +``` + +For example, if we want to generate our UI Bundles, with the default styling, on `./js/` and `./css/` folders, +we do: + +```bash +$ node_modules/.bin/travix-ui-kit -j ./js/ -c ./css/ +``` + +If we want to pass our own YAML file for styling, we also can do it: + +```bash +$ node_modules/.bin/travix-ui-kit -j ./js/ -c ./css/ -t ./myDefaultStyle.yml +``` + +And for development purposes, we tend to want to watch for changes on the files. +That's possible too: + +```bash +$ node_modules/.bin/travix-ui-kit -j ./js/ -c ./css/ -t ./myDefaultStyle.yml -w +``` + + +For simplicity purposes we suggest to add a task/script to your `package.json`, +which simplifies the usage of the CLI. E.g.: + +```js +{ + "scripts": { + "build:ui": "travix-ui-kit -j ./js/ -c ./css/ -t ./myDefaultStyle.yml", + "build:ui-watch": "travix-ui-kit -j ./js/ -c ./css/ -t ./myDefaultStyle.yml -w", + } +} +``` + +#### The components + +##### JS ```javascript const Button = require('travix-ui-kit').Button; // or @@ -22,7 +78,7 @@ Travix UI Components' repository. ); } ``` -#### CSS +##### CSS use file `node_modules/travix-ui-kit/dist/bundle.css` - you can create an alias in your webpack plugin - or inject it in your page current styles bundle @@ -30,6 +86,8 @@ use file `node_modules/travix-ui-kit/dist/bundle.css` **Warning**: Directly using file `components/index.scss` not recommended. We're not promising that we will use SCSS in future or will keep file's structure +--- + ## Living style guide ### Before installation @@ -57,8 +115,7 @@ use file `node_modules/travix-ui-kit/dist/bundle.css` #### Start developing - `npm run build:watch` to build the themes, styles and javascript on each file change -- `npm run build-theme:watch` to build the themes on each theme change -- `THEME_PATH=/some/path/to/theme.yaml npm run build` to pass other than default theme. Theme must be a valid yaml file +- `npm run build:watch -- -t "./path/to/my/theme.yml"` to build using a custom theme (also can use the other options as well). - `npm run styleguide-server` to run web service with livingstyle guide and review changes #### Testing diff --git a/bin/build-theme.js b/bin/build-theme.js deleted file mode 100644 index 3d8772d1..00000000 --- a/bin/build-theme.js +++ /dev/null @@ -1,22 +0,0 @@ -const path = require('path'); -const fs = require('fs'); -const root = path.join(__dirname, '/../'); -const getStyles = require('../scripts/getStyles.js').getStyles; -const themeFile = process.env.THEME_PATH || path.join(root, 'themes/_default.yaml'); -const appName = 'Build Theme'; - -function buildTheme() { - console.log(`[${appName}]:`, 'Using theme ' + themeFile); - getStyles(process.env.THEME_PATH || path.join(root, 'themes/_default.yaml')); - console.log(`[${appName}]:`, 'Building styles done'); -} - -if (~process.argv.indexOf('--watch')) { - console.log(`[${appName}]:`, 'Watching theme file changes'); - fs.watchFile(themeFile, (curr, prev) => { - console.log(`[${appName}]:`, themeFile, 'was changed. Rebuilding...'); - buildTheme(); - }); -} - -buildTheme(); diff --git a/builder/builder.js b/builder/builder.js new file mode 100644 index 00000000..43f36dbe --- /dev/null +++ b/builder/builder.js @@ -0,0 +1,28 @@ +const getStylesAndSaveTheme = require('./getStylesAndSaveTheme'); +const runWebpackAndCopyFilesToFinalDestination = require('./runWebpackAndCopyFilesToFinalDestination'); +const webpackConfig = require('./webpack.config'); + +const webpackNodeEnv = { + 'process.env.NODE_ENV': process.env.NODE_ENV || 'development', +}; + +/** + * Triggers the build process. + * + * @module builder + * @param {String} cssDir Destination folder for the ui-bundle.css + * @param {String} jsDir Destination folder for the ui-bundle.js + * @param {String} themeFile Path where a custom YAML w/ styles' definitions + * @param {Boolean} watch Flag to determine if it should run in 'watch' mode + * @return {Promise} + */ +module.exports = ({ cssDir, jsDir, themeFile, watch }) => { + return getStylesAndSaveTheme(themeFile, watch) + .then(runWebpackAndCopyFilesToFinalDestination({ + webpackConfig, + webpackNodeEnv, + cssDir, + jsDir, + watch, + })); +}; diff --git a/builder/copyToFinalDestination.js b/builder/copyToFinalDestination.js new file mode 100644 index 00000000..cc4355ed --- /dev/null +++ b/builder/copyToFinalDestination.js @@ -0,0 +1,34 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * Copies a given file from one path to another. + * + * @module copyToFinalDestination + * @param {String} originalPath Source path to be copied + * @param {String} finalPath Destination folder to copy the originalPath to. + * @return {Promise} + */ +module.exports = ({ originalPath, finalPath }) => new Promise((resolve, reject) => { + if (!finalPath) { + resolve(); + return; + } + + const copyTo = path.join(finalPath, path.basename(originalPath)); + fs.readFile(originalPath, (readErr, content) => { + if (readErr) { + reject(readErr); + return; + } + + fs.writeFile(copyTo, content, (writeErr) => { + if (writeErr) { + reject(writeErr); + return; + } + + resolve(); + }); + }); +}); diff --git a/builder/generateThemeFile.js b/builder/generateThemeFile.js new file mode 100644 index 00000000..675282ee --- /dev/null +++ b/builder/generateThemeFile.js @@ -0,0 +1,24 @@ +const fs = require('fs'); +const saveThemeScssFile = require('./saveThemeScssFile'); +const themeBuilder = require('theme-builder'); + +/** + * Generates the themes/theme.scss file, based on a given YAML file. + * + * @module generateThemeFile + * @param {String} yamlFile File path to a YAML file with the styles' definitions. + * @return {Promise} + */ +module.exports = (yamlFile) => { + return new Promise((resolve, reject) => { + fs.readFile(yamlFile, { encoding: 'utf-8' }, (err, content) => { + if (err) { + reject(err); + return; + } + + const themeChunks = themeBuilder(content, 'scss', { prefix: 'tx' }); + resolve(themeChunks.join('\n')); + }); + }).then(saveThemeScssFile); +}; diff --git a/builder/getStylesAndSaveTheme.js b/builder/getStylesAndSaveTheme.js new file mode 100644 index 00000000..1d122869 --- /dev/null +++ b/builder/getStylesAndSaveTheme.js @@ -0,0 +1,25 @@ +const fs = require('fs'); +const path = require('path'); +const generateThemeFile = require('./generateThemeFile'); + +/** + * Triggers the generation of the theme file (theme.scss) + * and handles the watch mode. + * + * @module getStylesAndSaveTheme + * @param {String} [themeFile] Path to a custom YAML file with styles' definitions. + * @param {Boolean} [isWatchEnabled] Flag that enables the 'watch mode' when true. Default: false. + * @return {Promise} + */ +module.exports = (themeFile, isWatchEnabled) => new Promise((resolve, reject) => { + const yamlFile = themeFile || path.join(__dirname, '..', 'themes', '_default.yaml'); + + if (isWatchEnabled) { + fs.watch(yamlFile, { persistent: true }, () => { + /** TODO: Do proper error handling on watch mode */ + generateThemeFile(yamlFile).catch(reject); + }); + } + + generateThemeFile(yamlFile).then(resolve).catch(reject); +}); diff --git a/builder/index.js b/builder/index.js new file mode 100755 index 00000000..8e939502 --- /dev/null +++ b/builder/index.js @@ -0,0 +1,18 @@ +#!/usr/bin/env node + +const program = require('commander'); +const pkg = require('../package.json'); +const builder = require('./builder'); + +program + .version(pkg.version) + .option('-c, --css-dir ', 'Destination directory of the ui-kit.css') + .option('-j, --js-dir ', 'Destination directory of the ui-kit.js') + .option('-t, --theme-file ', 'Path to a theme file to override default UI Kit styles') + .option('-w, --watch', 'Enables file-watcher functionality', false); + +program.parse(process.argv); + +builder(program) + .then(() => console.log('Done!')) + .catch(console.error); diff --git a/builder/runWebpackAndCopyFilesToFinalDestination.js b/builder/runWebpackAndCopyFilesToFinalDestination.js new file mode 100644 index 00000000..ed5c0a91 --- /dev/null +++ b/builder/runWebpackAndCopyFilesToFinalDestination.js @@ -0,0 +1,38 @@ +const copyToFinalDestination = require('./copyToFinalDestination'); +const path = require('path'); +const webpack = require('webpack'); + +/** + * @module runWebpackAndCopyFilesToFinaDestination + * @param {Object} options Object containing the configuration props. + * @param {String} options.cssDir Folder where to place the ui-bundle.css + * @param {String} options.jsDir Folder where to place the ui-bundle.js + * @param {String} options.watch Flag that enables the 'watch mode' on Webpack. Default: false + * @param {Object} options.webpackConfig Webpack configuration object + * @param {Object} options.webpackNodeEnv Webpack NODE_ENV configuration + * @return {Promise} + */ +module.exports = ({ cssDir, jsDir, watch, webpackConfig, webpackNodeEnv }) => new Promise((resolve, reject) => { + webpackConfig.plugins.push(new webpack.DefinePlugin(webpackNodeEnv)); + webpackConfig.context = __dirname; + + const runner = webpack(webpackConfig); + const runnerFn = watch ? runner.watch.bind(runner, {}) : runner.run.bind(runner); + + runnerFn((err, stats) => { + if (err) { + reject(err); + return; + } + + copyToFinalDestination({ + finalPath: jsDir, + originalPath: path.join(webpackConfig.output.path, 'ui-bundle.js'), + }).then(() => copyToFinalDestination({ + finalPath: cssDir, + originalPath: path.join(webpackConfig.output.path, 'ui-bundle.css'), + })).then(() => { + resolve(stats); + }).catch(reject); + }); +}); diff --git a/builder/saveThemeScssFile.js b/builder/saveThemeScssFile.js new file mode 100644 index 00000000..c2b41b98 --- /dev/null +++ b/builder/saveThemeScssFile.js @@ -0,0 +1,18 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * @module saveThemeScssFile + * @param {String} themeScss SCSS content to be stored as themes/theme.scss + * @return {Promise} + */ +module.exports = themeScss => new Promise((resolve, reject) => { + fs.writeFile(path.join(__dirname, '..', 'themes', 'theme.scss'), themeScss, (err) => { + if (err) { + reject(err); + return; + } + + resolve(); + }); +}); diff --git a/webpack.config.js b/builder/webpack.config.js similarity index 78% rename from webpack.config.js rename to builder/webpack.config.js index 92bdbae7..8a170671 100644 --- a/webpack.config.js +++ b/builder/webpack.config.js @@ -1,13 +1,18 @@ const autoprefixer = require('autoprefixer'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); +const path = require('path'); -const outputDir = './dist/'; +const outputDir = path.join(__dirname, '..', 'dist'); +/** + * @module webpack.config + * @type {Object} + */ module.exports = { entry: { dist: [ - './components/index.scss', - './components/index.js', + '../components/index.scss', + '../components/index.js', ], }, diff --git a/package.json b/package.json index f197f13a..cb8547cc 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,10 @@ "description": "Travix UI kit", "main": "lib/index.js", "scripts": { - "build": "npm run build-theme && webpack && babel --copy-files ./components --out-dir lib --ignore *.scss,*.md", - "build:watch": "npm run build-theme:watch & webpack --watch& babel --copy-files ./components --out-dir lib --ignore *.scss,*.md -w", - "build-theme": "node bin/build-theme.js", - "build-theme:watch": "node bin/build-theme.js --watch", + "prebuild": "babel --copy-files ./components --out-dir lib --ignore *.scss,*.md &", + "build": "node ./builder", + "build:watch": "node ./builder -w", + "prebuild:watch": "babel --copy-files ./components --out-dir lib --ignore *.scss,*.md -w &", "styleguide-server": "styleguidist server", "styleguide-build": "styleguidist build", "test": "jest -c ./tests/jest.config.json", @@ -17,6 +17,9 @@ "lint": "eslint --color '{components,tests,utils,scripts}/**/*.js'", "transpile": "npm run build" }, + "bin": { + "travix-ui-kit": "./builder/index.js" + }, "repository": { "type": "git", "url": "git@github.com:Travix-International/travix-ui-kit.git" @@ -54,6 +57,7 @@ "babel-loader": "^6.2.5", "babel-preset-travix": "^1.1.0", "babel-register": "^6.16.3", + "commander": "^2.9.0", "css-loader": "^0.26.1", "extract-text-webpack-plugin": "^1.0.1", "node-sass": "^4.3.0", diff --git a/scripts/getStyles.js b/scripts/getStyles.js deleted file mode 100644 index 2611c278..00000000 --- a/scripts/getStyles.js +++ /dev/null @@ -1,20 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const themeBuilder = require('theme-builder'); - -const root = path.join(__dirname, '/../'); -const themeScssFile = path.join(root, 'themes/theme.scss'); - -function getStyles(currentThemePath) { - currentThemePath = currentThemePath || path.join(root, 'themes/_default.yaml'); - - const themeYaml = fs.readFileSync(currentThemePath); - const themeScss = themeBuilder(themeYaml, 'scss', { prefix: 'tx' }); - - // Could be removed in future if pass it in data for sass render func - fs.writeFileSync(themeScssFile, themeScss.join('\n')); -} - -module.exports = { - getStyles: getStyles, -}; diff --git a/tests/jest.config.json b/tests/jest.config.json index 1d508482..e3f08110 100644 --- a/tests/jest.config.json +++ b/tests/jest.config.json @@ -1,8 +1,16 @@ { "collectCoverage": true, + "collectCoverageFrom": [ + "builder/**/*.js", + "!builder/webpack.config.js", + "components/**/*.js", + "!components/index.js" + ], "moduleFileExtensions": [ "js", "json" ], - "scriptPreprocessor": "/node_modules/babel-jest" + "transform": { + ".*": "/node_modules/babel-jest" + } } diff --git a/tests/unit/builder/builder.spec.js b/tests/unit/builder/builder.spec.js new file mode 100644 index 00000000..fe7ea492 --- /dev/null +++ b/tests/unit/builder/builder.spec.js @@ -0,0 +1,35 @@ +jest.mock('../../../builder/getStylesAndSaveTheme', () => jest.fn(() => Promise.resolve())); +jest.mock('../../../builder/runWebpackAndCopyFilesToFinalDestination', () => jest.fn(() => Promise.resolve())); + +const builder = require('../../../builder/builder'); +const getStylesAndSaveTheme = require('../../../builder/getStylesAndSaveTheme'); +const runWebpackAndCopyFilesToFinalDestination = require('../../../builder/runWebpackAndCopyFilesToFinalDestination'); +const webpackConfig = require('../../../builder/webpack.config'); + +describe('Builder › builder.js', () => { + it('should call the dependencies\' functions with the proper args', () => { + const args = { + cssDir: 'myCssDir', + jsDir: 'myJsDir', + themeFile: 'myThemeFile', + watch: true, + }; + + return builder(args) + .then(() => { + expect(getStylesAndSaveTheme).toHaveBeenCalled(); + expect(getStylesAndSaveTheme).toHaveBeenCalledWith(args.themeFile, args.watch); + + expect(runWebpackAndCopyFilesToFinalDestination).toHaveBeenCalled(); + expect(runWebpackAndCopyFilesToFinalDestination).toHaveBeenCalledWith({ + cssDir: args.cssDir, + jsDir: args.jsDir, + watch: args.watch, + webpackConfig: webpackConfig, + webpackNodeEnv: { + 'process.env.NODE_ENV': 'test', // When running jest, the env changes to 'test' + }, + }); + }); + }); +}); diff --git a/tests/unit/builder/copyToFinalDestination.spec.js b/tests/unit/builder/copyToFinalDestination.spec.js new file mode 100644 index 00000000..45e36cd9 --- /dev/null +++ b/tests/unit/builder/copyToFinalDestination.spec.js @@ -0,0 +1,64 @@ +jest.mock('fs'); + +const fs = require('fs'); +const copyToFinalDestination = require('../../../builder/copyToFinalDestination'); + +describe('Builder › copyToFinalDestination.js', () => { + beforeEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + + fs.readFile = jest.fn(); + fs.writeFile = jest.fn(); + }); + + it('resolves immediately when no finalPath is provided', () => { + return copyToFinalDestination({ originalPath: 'fake/original/path.ext' }) + .then(() => { + expect(fs.readFile).not.toHaveBeenCalled(); + expect(fs.writeFile).not.toHaveBeenCalled(); + }); + }); + + it('reads the original file and writes it into the final path w/ the same basename', () => { + fs.readFile.mockImplementation((filePath, cb) => cb(null, 'fake content')); + fs.writeFile.mockImplementation((filePath, content, cb) => cb(null)); + + return copyToFinalDestination({ + originalPath: 'fake/original/path.ext', + finalPath: 'fake/final/path/', + }).then(() => { + expect(fs.readFile).toHaveBeenCalled(); + expect(fs.writeFile).toHaveBeenCalled(); + }); + }); + + it('rejects promise when it is unable to read file', () => { + fs.readFile.mockImplementation((filePath, cb) => cb(new Error('Read error'))); + + return copyToFinalDestination({ + originalPath: 'fake/original/path.ext', + finalPath: 'fake/final/path/', + }).catch((err) => { + expect(fs.readFile).toHaveBeenCalled(); + expect(fs.writeFile).not.toHaveBeenCalled(); + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe('Read error'); + }); + }); + + it('rejects promise when it is unable to write file', () => { + fs.readFile.mockImplementation((filePath, cb) => cb(null, 'fake content')); + fs.writeFile.mockImplementation((filePath, content, cb) => cb(new Error('Write error'))); + + return copyToFinalDestination({ + originalPath: 'fake/original/path.ext', + finalPath: 'fake/final/path/', + }).catch((err) => { + expect(fs.readFile).toHaveBeenCalled(); + expect(fs.writeFile).toHaveBeenCalled(); + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe('Write error'); + }); + }); +}); diff --git a/tests/unit/builder/generateThemeFile.spec.js b/tests/unit/builder/generateThemeFile.spec.js new file mode 100644 index 00000000..39e3f9c1 --- /dev/null +++ b/tests/unit/builder/generateThemeFile.spec.js @@ -0,0 +1,53 @@ +jest.mock('fs'); +jest.mock('../../../builder/saveThemeScssFile', () => jest.fn(() => Promise.resolve())); +jest.mock('theme-builder', () => jest.fn(() => ['scss', 'content'])); + +const fs = require('fs'); +const generateThemeFile = require('../../../builder/generateThemeFile'); +const saveThemeScssFile = require('../../../builder/saveThemeScssFile'); +const themeBuilder = require('theme-builder'); + +describe('Builder › generateThemeFile.js', () => { + beforeEach(() => { + jest.resetModules(); + fs.readFile = jest.fn(); + themeBuilder.mockClear(); + saveThemeScssFile.mockClear(); + }); + + it('reads the file, calls the theme-builder fand saveThemeScssFile functions', () => { + const fakeFile = 'fake/file.yml'; + fs.readFile.mockImplementation((filePath, options, cb) => cb(null, 'fake file content')); + return generateThemeFile(fakeFile) + .then(() => { + expect(fs.readFile).toHaveBeenCalled(); + expect(fs.readFile.mock.calls[0][0]).toBe(fakeFile); + expect(fs.readFile.mock.calls[0][1]).toEqual({ encoding: 'utf-8' }); + expect(fs.readFile.mock.calls[0][2]).toBeInstanceOf(Function); + + expect(themeBuilder).toHaveBeenCalled(); + expect(themeBuilder).toHaveBeenCalledWith('fake file content', 'scss', { prefix: 'tx' }); + + expect(saveThemeScssFile).toHaveBeenCalled(); + expect(saveThemeScssFile).toHaveBeenCalledWith('scss\ncontent'); + }); + }); + + it('rejects when it has a read error', () => { + const fakeFile = 'fake/file.yml'; + fs.readFile.mockImplementation((filePath, options, cb) => cb(new Error('Read error'))); + return generateThemeFile(fakeFile) + .catch((err) => { + expect(fs.readFile).toHaveBeenCalled(); + expect(fs.readFile.mock.calls[0][0]).toBe(fakeFile); + expect(fs.readFile.mock.calls[0][1]).toEqual({ encoding: 'utf-8' }); + expect(fs.readFile.mock.calls[0][2]).toBeInstanceOf(Function); + + expect(themeBuilder).not.toHaveBeenCalled(); + expect(saveThemeScssFile).not.toHaveBeenCalled(); + + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe('Read error'); + }); + }); +}); diff --git a/tests/unit/builder/getStylesAndSaveTheme.spec.js b/tests/unit/builder/getStylesAndSaveTheme.spec.js new file mode 100644 index 00000000..d10a9f8e --- /dev/null +++ b/tests/unit/builder/getStylesAndSaveTheme.spec.js @@ -0,0 +1,60 @@ +jest.mock('fs'); +jest.mock('../../../builder/generateThemeFile', () => jest.fn(() => Promise.resolve())); +const fs = require('fs'); +const generateThemeFile = require('../../../builder/generateThemeFile'); +const getStylesAndSaveTheme = require('../../../builder/getStylesAndSaveTheme'); +const path = require('path'); + +describe('Builder › getStylesAndSaveTheme.js', () => { + beforeEach(() => { + jest.resetModules(); + fs.watch = jest.fn(); + generateThemeFile.mockClear(); + }); + + it('sets the yamlFile\'s basename to "_default.yaml" when no themeFile is provided', () => { + return getStylesAndSaveTheme() + .then(() => { + expect(fs.watch).not.toHaveBeenCalled(); + expect(generateThemeFile).toHaveBeenCalled(); + expect(path.basename(generateThemeFile.mock.calls[0][0])).toBe('_default.yaml'); + }); + }); + + it('calls generateThemeFile and resolves without watching files (when watch = false)', () => { + return getStylesAndSaveTheme('fake/yaml/file.yml', false) + .then(() => { + expect(fs.watch).not.toHaveBeenCalled(); + expect(generateThemeFile).toHaveBeenCalled(); + expect(generateThemeFile).toHaveBeenCalledWith('fake/yaml/file.yml'); + }); + }); + + it('calls generateThemeFile and watches the yaml file (when watch = true)', () => { + return getStylesAndSaveTheme('fake/yaml/file.yml', true) + .then(() => { + expect(fs.watch).toHaveBeenCalled(); + expect(fs.watch.mock.calls[0][0]).toBe('fake/yaml/file.yml'); + expect(fs.watch.mock.calls[0][1]).toEqual({ persistent: true }); + expect(fs.watch.mock.calls[0][2]).toBeInstanceOf(Function); + + expect(generateThemeFile).toHaveBeenCalled(); + expect(generateThemeFile).toHaveBeenCalledWith('fake/yaml/file.yml'); + }); + }); + + it('calls generateThemeFile when executing the watcher\'s callback (when watch = true)', () => { + fs.watch.mockImplementation((filePath, opts, cb) => cb()); // Automatically yields + + return getStylesAndSaveTheme('fake/yaml/file.yml', true) + .then(() => { + expect(fs.watch).toHaveBeenCalled(); + expect(fs.watch.mock.calls[0][0]).toBe('fake/yaml/file.yml'); + expect(fs.watch.mock.calls[0][1]).toEqual({ persistent: true }); + expect(fs.watch.mock.calls[0][2]).toBeInstanceOf(Function); + + expect(generateThemeFile).toHaveBeenCalledTimes(2); + expect(generateThemeFile).toHaveBeenCalledWith('fake/yaml/file.yml'); + }); + }); +}); diff --git a/tests/unit/builder/index.spec.js b/tests/unit/builder/index.spec.js new file mode 100644 index 00000000..5e5a7e53 --- /dev/null +++ b/tests/unit/builder/index.spec.js @@ -0,0 +1,48 @@ +jest.mock('commander'); +jest.mock('../../../builder/builder', () => jest.fn(() => Promise.resolve())); + +const builder = require('../../../builder/builder'); +const commander = require('commander'); +const pkg = require('../../../package.json'); + +commander.version = jest.fn().mockReturnValue(commander); +commander.option = jest.fn().mockReturnValue(commander); +commander.parse = jest.fn().mockReturnValue(commander); + +require('../../../builder/index'); + +describe('Builder › generateThemeFile.js', () => { + beforeEach(() => { + jest.resetModules(); + }); + + it('sets the program version, attributes, parses the process.argv and calls the builder', () => { + expect(commander.version).toHaveBeenCalled(); + expect(commander.version).toHaveBeenCalledWith(pkg.version); + + expect(commander.option).toHaveBeenCalledTimes(4); + expect(commander.option).toHaveBeenCalledWith( + '-c, --css-dir ', + 'Destination directory of the ui-kit.css' + ); + expect(commander.option).toHaveBeenCalledWith( + '-j, --js-dir ', + 'Destination directory of the ui-kit.js' + ); + expect(commander.option).toHaveBeenCalledWith( + '-t, --theme-file ', + 'Path to a theme file to override default UI Kit styles' + ); + expect(commander.option).toHaveBeenCalledWith( + '-w, --watch', + 'Enables file-watcher functionality', + false + ); + + expect(commander.parse).toHaveBeenCalled(); + expect(commander.parse).toHaveBeenCalledWith(process.argv); + + expect(builder).toHaveBeenCalled(); + expect(builder).toHaveBeenCalledWith(commander); + }); +}); diff --git a/tests/unit/builder/runWebpackAndCopyFilesToFinalDestination.spec.js b/tests/unit/builder/runWebpackAndCopyFilesToFinalDestination.spec.js new file mode 100644 index 00000000..05eda08c --- /dev/null +++ b/tests/unit/builder/runWebpackAndCopyFilesToFinalDestination.spec.js @@ -0,0 +1,142 @@ +const runnerStub = { + run: jest.fn(cb => cb(null, 'fakeStats')), + watch: jest.fn((opts, cb) => cb(null, 'fakeStats')), +}; +const mockWebpack = jest.fn(() => { + return runnerStub; +}); +mockWebpack.DefinePlugin = jest.fn(() => ({})); + +jest.mock('webpack', () => mockWebpack); +jest.mock('../../../builder/copyToFinalDestination', () => jest.fn(() => Promise.resolve())); + +const copyToFinalDestination = require('../../../builder/copyToFinalDestination'); +const path = require('path'); +const runWebpackAndCopyFilesToFinalDestination = require('../../../builder/runWebpackAndCopyFilesToFinalDestination'); + +describe('Builder › runWebpackAndCopyFilesToFinalDestination.js', () => { + beforeEach(() => { + jest.resetModules(); + copyToFinalDestination.mockClear(); + }); + + it('reads the file, calls the theme-builder fand saveThemeScssFile functions', () => { + const fakeArgs = { + cssDir: 'fake/css/dir/', + jsDir: 'fake/js/dir/', + watch: false, + webpackConfig: { + output: { + path: 'fakePath', + }, + plugins: [], + }, + webpackNodeEnv: 'fakeWebpackNodeEnv', + }; + + return runWebpackAndCopyFilesToFinalDestination(fakeArgs) + .then((stats) => { + expect(runnerStub.run).toHaveBeenCalled(); + expect(runnerStub.run.mock.calls[0][0]).toBeInstanceOf(Function); + + expect(copyToFinalDestination).toHaveBeenCalledTimes(2); + expect(copyToFinalDestination).toHaveBeenCalledWith({ + finalPath: fakeArgs.cssDir, + originalPath: path.join(fakeArgs.webpackConfig.output.path, 'ui-bundle.css'), + }); + expect(copyToFinalDestination).toHaveBeenCalledWith({ + finalPath: fakeArgs.jsDir, + originalPath: path.join(fakeArgs.webpackConfig.output.path, 'ui-bundle.js'), + }); + expect(stats).toBe('fakeStats'); + }); + }); + + it('sets the watcher', () => { + const fakeArgs = { + cssDir: 'fake/css/dir/', + jsDir: 'fake/js/dir/', + watch: true, + webpackConfig: { + output: { + path: 'fakePath', + }, + plugins: [], + }, + webpackNodeEnv: 'fakeWebpackNodeEnv', + }; + + return runWebpackAndCopyFilesToFinalDestination(fakeArgs) + .then((stats) => { + expect(runnerStub.watch).toHaveBeenCalled(); + expect(runnerStub.watch.mock.calls[0][0]).toEqual({}); + expect(runnerStub.watch.mock.calls[0][1]).toBeInstanceOf(Function); + + expect(copyToFinalDestination).toHaveBeenCalledTimes(2); + expect(copyToFinalDestination).toHaveBeenCalledWith({ + finalPath: fakeArgs.cssDir, + originalPath: path.join(fakeArgs.webpackConfig.output.path, 'ui-bundle.css'), + }); + expect(copyToFinalDestination).toHaveBeenCalledWith({ + finalPath: fakeArgs.jsDir, + originalPath: path.join(fakeArgs.webpackConfig.output.path, 'ui-bundle.js'), + }); + expect(stats).toBe('fakeStats'); + }); + }); + + it('rejects when it has an error executing the runnerFn', () => { + const fakeArgs = { + cssDir: 'fake/css/dir/', + jsDir: 'fake/js/dir/', + watch: false, + webpackConfig: { + output: { + path: 'fakePath', + }, + plugins: [], + }, + webpackNodeEnv: 'fakeWebpackNodeEnv', + }; + + runnerStub.run.mockImplementation(cb => cb(new Error('Runner error'))); + + return runWebpackAndCopyFilesToFinalDestination(fakeArgs) + .catch((err) => { + expect(runnerStub.run).toHaveBeenCalled(); + expect(runnerStub.run.mock.calls[0][0]).toBeInstanceOf(Function); + + expect(copyToFinalDestination).not.toHaveBeenCalled(); + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe('Runner error'); + }); + }); + + it('rejects when it has an error executing the runnerFn (watch = true)', () => { + const fakeArgs = { + cssDir: 'fake/css/dir/', + jsDir: 'fake/js/dir/', + watch: true, + webpackConfig: { + output: { + path: 'fakePath', + }, + plugins: [], + }, + webpackNodeEnv: 'fakeWebpackNodeEnv', + }; + + runnerStub.watch.mockImplementation((opts, cb) => cb(new Error('Runner error'))); + + return runWebpackAndCopyFilesToFinalDestination(fakeArgs) + .catch((err) => { + expect(runnerStub.watch).toHaveBeenCalled(); + expect(runnerStub.watch.mock.calls[0][0]).toEqual({}); + expect(runnerStub.watch.mock.calls[0][1]).toBeInstanceOf(Function); + + expect(copyToFinalDestination).not.toHaveBeenCalled(); + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe('Runner error'); + }); + }); +}); diff --git a/tests/unit/builder/saveThemeScssFile.spec.js b/tests/unit/builder/saveThemeScssFile.spec.js new file mode 100644 index 00000000..7501195f --- /dev/null +++ b/tests/unit/builder/saveThemeScssFile.spec.js @@ -0,0 +1,36 @@ +jest.mock('fs'); +const fs = require('fs'); +const path = require('path'); +const saveThemeScssFile = require('../../../builder/saveThemeScssFile'); + +describe('Builder › saveThemeScssFile.js', () => { + beforeEach(() => { + jest.resetModules(); + fs.writeFile = jest.fn(); + }); + + it('saves the SCSS provided as themes/theme.scss', () => { + fs.writeFile.mockImplementation((filepath, content, cb) => cb(null)); + return saveThemeScssFile('fakeScssContent') + .then(() => { + expect(fs.writeFile).toHaveBeenCalled(); + expect(fs.writeFile.mock.calls[0][0]).toBe(path.join(__dirname, '..', '..', '..', 'themes', 'theme.scss')); + expect(fs.writeFile.mock.calls[0][1]).toBe('fakeScssContent'); + expect(fs.writeFile.mock.calls[0][2]).toBeInstanceOf(Function); + }); + }); + + it('rejects the Promise in case of write error', () => { + fs.writeFile.mockImplementation((filepath, content, cb) => cb(new Error('Write error'))); + return saveThemeScssFile('fakeScssContent') + .catch((err) => { + expect(fs.writeFile).toHaveBeenCalled(); + expect(fs.writeFile.mock.calls[0][0]).toBe(path.join(__dirname, '..', '..', '..', 'themes', 'theme.scss')); + expect(fs.writeFile.mock.calls[0][1]).toBe('fakeScssContent'); + expect(fs.writeFile.mock.calls[0][2]).toBeInstanceOf(Function); + + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe('Write error'); + }); + }); +});