├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── README.md ├── build ├── build.js ├── check-versions.js ├── dev-client.js ├── dev-server.js ├── utils.js ├── vue-loader.conf.js ├── webpack.base.conf.js ├── webpack.dev.conf.js ├── webpack.prod.conf.js └── webpack.test.conf.js ├── config ├── dev.env.js ├── index.js ├── prod.env.js └── test.env.js ├── index.html ├── package.json ├── src ├── app │ ├── App.vue │ ├── accounts │ │ ├── api.js │ │ ├── components │ │ │ ├── AccountsList.vue │ │ │ ├── CreateUpdateAccount.vue │ │ │ └── index.js │ │ ├── index.js │ │ ├── routes.js │ │ └── vuex │ │ │ ├── actions.js │ │ │ ├── getters.js │ │ │ ├── index.js │ │ │ └── mutations.js │ ├── budgets │ │ ├── api.js │ │ ├── components │ │ │ ├── BudgetCategory.vue │ │ │ ├── BudgetsList.vue │ │ │ ├── CreateUpdateBudget.vue │ │ │ ├── CreateUpdateBudgetCategory.vue │ │ │ └── index.js │ │ ├── index.js │ │ ├── routes.js │ │ └── vuex │ │ │ ├── actions.js │ │ │ ├── getters.js │ │ │ ├── index.js │ │ │ └── mutations.js │ ├── index.js │ ├── navigation │ │ ├── components │ │ │ ├── Navigation.vue │ │ │ └── index.js │ │ └── index.js │ ├── routes.js │ ├── transactions │ │ ├── api.js │ │ ├── components │ │ │ ├── CreateUpdateTransaction.vue │ │ │ ├── Transaction.vue │ │ │ ├── TransactionsList.vue │ │ │ └── index.js │ │ ├── index.js │ │ ├── routes.js │ │ └── vuex │ │ │ ├── actions.js │ │ │ ├── getters.js │ │ │ ├── index.js │ │ │ └── mutations.js │ └── vuex.js ├── assets │ └── logo.png ├── consts.js ├── filters.js ├── main.js ├── router │ └── index.js ├── store │ └── index.js └── utils.js ├── static └── .gitkeep └── test ├── e2e ├── custom-assertions │ └── elementCount.js ├── nightwatch.conf.js ├── runner.js └── specs │ └── test.js └── unit ├── .eslintrc ├── index.js ├── karma.conf.js └── specs └── Hello.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015", { "modules": false }], 4 | "stage-2" 5 | ], 6 | "plugins": ["transform-runtime"], 7 | "comments": false, 8 | "env": { 9 | "test": { 10 | "plugins": [ "istanbul" ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 13 | extends: 'standard', 14 | // required to lint *.vue files 15 | plugins: [ 16 | 'html' 17 | ], 18 | // add your custom rules here 19 | 'rules': { 20 | // allow paren-less arrow functions 21 | 'arrow-parens': 0, 22 | // allow async-await 23 | 'generator-star-spacing': 0, 24 | // allow debugger during development 25 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 26 | // require semicolons 27 | "semi": [2, "always"] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | test/unit/coverage 6 | test/e2e/reports 7 | selenium-debug.log 8 | debug.log 9 | .idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # budgeterbium 2 | 3 | > A simple budgeting app 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | 17 | # build for production and view the bundle analyzer report 18 | npm run build --report 19 | 20 | # run unit tests 21 | npm run unit 22 | 23 | # run e2e tests 24 | npm run e2e 25 | 26 | # run all tests 27 | npm test 28 | ``` 29 | 30 | For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 31 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | // https://github.com/shelljs/shelljs 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | var ora = require('ora') 7 | var path = require('path') 8 | var chalk = require('chalk') 9 | var shell = require('shelljs') 10 | var webpack = require('webpack') 11 | var config = require('../config') 12 | var webpackConfig = require('./webpack.prod.conf') 13 | 14 | var spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory) 18 | shell.rm('-rf', assetsPath) 19 | shell.mkdir('-p', assetsPath) 20 | shell.config.silent = true 21 | shell.cp('-R', 'static/*', assetsPath) 22 | shell.config.silent = false 23 | 24 | webpack(webpackConfig, function (err, stats) { 25 | spinner.stop() 26 | if (err) throw err 27 | process.stdout.write(stats.toString({ 28 | colors: true, 29 | modules: false, 30 | children: false, 31 | chunks: false, 32 | chunkModules: false 33 | }) + '\n\n') 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var semver = require('semver') 3 | var packageConfig = require('../package.json') 4 | 5 | function exec (cmd) { 6 | return require('child_process').execSync(cmd).toString().trim() 7 | } 8 | 9 | var versionRequirements = [ 10 | { 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node 14 | }, 15 | { 16 | name: 'npm', 17 | currentVersion: exec('npm --version'), 18 | versionRequirement: packageConfig.engines.npm 19 | } 20 | ] 21 | 22 | module.exports = function () { 23 | var warnings = [] 24 | for (var i = 0; i < versionRequirements.length; i++) { 25 | var mod = versionRequirements[i] 26 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 27 | warnings.push(mod.name + ': ' + 28 | chalk.red(mod.currentVersion) + ' should be ' + 29 | chalk.green(mod.versionRequirement) 30 | ) 31 | } 32 | } 33 | 34 | if (warnings.length) { 35 | console.log('') 36 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 37 | console.log() 38 | for (var i = 0; i < warnings.length; i++) { 39 | var warning = warnings[i] 40 | console.log(' ' + warning) 41 | } 42 | console.log() 43 | process.exit(1) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | var config = require('../config') 4 | if (!process.env.NODE_ENV) { 5 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 6 | } 7 | 8 | var opn = require('opn') 9 | var path = require('path') 10 | var express = require('express') 11 | var webpack = require('webpack') 12 | var proxyMiddleware = require('http-proxy-middleware') 13 | var webpackConfig = process.env.NODE_ENV === 'testing' 14 | ? require('./webpack.prod.conf') 15 | : require('./webpack.dev.conf') 16 | 17 | // default port where dev server listens for incoming traffic 18 | var port = process.env.PORT || config.dev.port 19 | // automatically open browser, if not set will be false 20 | var autoOpenBrowser = !!config.dev.autoOpenBrowser 21 | // Define HTTP proxies to your custom API backend 22 | // https://github.com/chimurai/http-proxy-middleware 23 | var proxyTable = config.dev.proxyTable 24 | 25 | var app = express() 26 | var compiler = webpack(webpackConfig) 27 | 28 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 29 | publicPath: webpackConfig.output.publicPath, 30 | quiet: true 31 | }) 32 | 33 | var hotMiddleware = require('webpack-hot-middleware')(compiler, { 34 | log: () => {} 35 | }) 36 | // force page reload when html-webpack-plugin template changes 37 | compiler.plugin('compilation', function (compilation) { 38 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 39 | hotMiddleware.publish({ action: 'reload' }) 40 | cb() 41 | }) 42 | }) 43 | 44 | // proxy api requests 45 | Object.keys(proxyTable).forEach(function (context) { 46 | var options = proxyTable[context] 47 | if (typeof options === 'string') { 48 | options = { target: options } 49 | } 50 | app.use(proxyMiddleware(options.filter || context, options)) 51 | }) 52 | 53 | // handle fallback for HTML5 history API 54 | app.use(require('connect-history-api-fallback')()) 55 | 56 | // serve webpack bundle output 57 | app.use(devMiddleware) 58 | 59 | // enable hot-reload and state-preserving 60 | // compilation error display 61 | app.use(hotMiddleware) 62 | 63 | // serve pure static assets 64 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 65 | app.use(staticPath, express.static('./static')) 66 | 67 | var uri = 'http://localhost:' + port 68 | 69 | devMiddleware.waitUntilValid(function () { 70 | console.log('> Listening at ' + uri + '\n') 71 | }) 72 | 73 | module.exports = app.listen(port, function (err) { 74 | if (err) { 75 | console.log(err) 76 | return 77 | } 78 | 79 | // when env is testing, don't need open it 80 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 81 | opn(uri) 82 | } 83 | }) 84 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | // generate loader string to be used with extract text plugin 15 | function generateLoaders (loaders) { 16 | var sourceLoader = loaders.map(function (loader) { 17 | var extraParamChar 18 | if (/\?/.test(loader)) { 19 | loader = loader.replace(/\?/, '-loader?') 20 | extraParamChar = '&' 21 | } else { 22 | loader = loader + '-loader' 23 | extraParamChar = '?' 24 | } 25 | return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '') 26 | }).join('!') 27 | 28 | // Extract CSS when that option is specified 29 | // (which is the case during production build) 30 | if (options.extract) { 31 | return ExtractTextPlugin.extract({ 32 | use: sourceLoader, 33 | fallback: 'vue-style-loader' 34 | }) 35 | } else { 36 | return ['vue-style-loader', sourceLoader].join('!') 37 | } 38 | } 39 | 40 | // http://vuejs.github.io/vue-loader/en/configurations/extract-css.html 41 | return { 42 | css: generateLoaders(['css']), 43 | postcss: generateLoaders(['css']), 44 | less: generateLoaders(['css', 'less']), 45 | sass: generateLoaders(['css', 'sass?indentedSyntax']), 46 | scss: generateLoaders(['css', 'sass']), 47 | stylus: generateLoaders(['css', 'stylus']), 48 | styl: generateLoaders(['css', 'stylus']) 49 | } 50 | } 51 | 52 | // Generate loaders for standalone style files (outside of .vue) 53 | exports.styleLoaders = function (options) { 54 | var output = [] 55 | var loaders = exports.cssLoaders(options) 56 | for (var extension in loaders) { 57 | var loader = loaders[extension] 58 | output.push({ 59 | test: new RegExp('\\.' + extension + '$'), 60 | loader: loader 61 | }) 62 | } 63 | return output 64 | } 65 | -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var config = require('../config') 3 | var isProduction = process.env.NODE_ENV === 'production' 4 | 5 | module.exports = { 6 | loaders: utils.cssLoaders({ 7 | sourceMap: isProduction 8 | ? config.build.productionSourceMap 9 | : config.dev.cssSourceMap, 10 | extract: isProduction 11 | }), 12 | postcss: [ 13 | require('autoprefixer')({ 14 | browsers: ['last 2 versions'] 15 | }) 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var config = require('../config') 4 | var vueLoaderConfig = require('./vue-loader.conf') 5 | 6 | function resolve (dir) { 7 | return path.join(__dirname, '..', dir) 8 | } 9 | 10 | module.exports = { 11 | entry: { 12 | app: './src/main.js' 13 | }, 14 | output: { 15 | path: config.build.assetsRoot, 16 | filename: '[name].js', 17 | publicPath: process.env.NODE_ENV === 'production' 18 | ? config.build.assetsPublicPath 19 | : config.dev.assetsPublicPath 20 | }, 21 | resolve: { 22 | extensions: ['.js', '.vue', '.json'], 23 | modules: [ 24 | resolve('src'), 25 | resolve('node_modules') 26 | ], 27 | alias: { 28 | 'vue$': 'vue/dist/vue.common.js', 29 | 'src': resolve('src'), 30 | 'assets': resolve('src/assets'), 31 | 'components': resolve('src/components') 32 | } 33 | }, 34 | module: { 35 | rules: [ 36 | { 37 | test: /\.(js|vue)$/, 38 | loader: 'eslint-loader', 39 | enforce: "pre", 40 | include: [resolve('src'), resolve('test')], 41 | options: { 42 | formatter: require('eslint-friendly-formatter') 43 | } 44 | }, 45 | { 46 | test: /\.vue$/, 47 | loader: 'vue-loader', 48 | options: vueLoaderConfig 49 | }, 50 | { 51 | test: /\.js$/, 52 | loader: 'babel-loader', 53 | include: [resolve('src'), resolve('test')] 54 | }, 55 | { 56 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 57 | loader: 'url-loader', 58 | query: { 59 | limit: 10000, 60 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 61 | } 62 | }, 63 | { 64 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 65 | loader: 'url-loader', 66 | query: { 67 | limit: 10000, 68 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 69 | } 70 | } 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var webpack = require('webpack') 3 | var config = require('../config') 4 | var merge = require('webpack-merge') 5 | var baseWebpackConfig = require('./webpack.base.conf') 6 | var HtmlWebpackPlugin = require('html-webpack-plugin') 7 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 8 | 9 | // add hot-reload related code to entry chunks 10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 11 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 12 | }) 13 | 14 | module.exports = merge(baseWebpackConfig, { 15 | module: { 16 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 17 | }, 18 | // cheap-module-eval-source-map is faster for development 19 | devtool: '#cheap-module-eval-source-map', 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | 'process.env': config.dev.env 23 | }), 24 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 25 | new webpack.HotModuleReplacementPlugin(), 26 | new webpack.NoEmitOnErrorsPlugin(), 27 | // https://github.com/ampedandwired/html-webpack-plugin 28 | new HtmlWebpackPlugin({ 29 | filename: 'index.html', 30 | template: 'index.html', 31 | inject: true 32 | }), 33 | new FriendlyErrorsPlugin() 34 | ] 35 | }) 36 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var webpack = require('webpack') 4 | var config = require('../config') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var HtmlWebpackPlugin = require('html-webpack-plugin') 8 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 9 | var env = process.env.NODE_ENV === 'testing' 10 | ? require('../config/test.env') 11 | : config.build.env 12 | 13 | var webpackConfig = merge(baseWebpackConfig, { 14 | module: { 15 | rules: utils.styleLoaders({ 16 | sourceMap: config.build.productionSourceMap, 17 | extract: true 18 | }) 19 | }, 20 | devtool: config.build.productionSourceMap ? '#source-map' : false, 21 | output: { 22 | path: config.build.assetsRoot, 23 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 24 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 25 | }, 26 | plugins: [ 27 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 28 | new webpack.DefinePlugin({ 29 | 'process.env': env 30 | }), 31 | new webpack.optimize.UglifyJsPlugin({ 32 | compress: { 33 | warnings: false 34 | } 35 | }), 36 | // extract css into its own file 37 | new ExtractTextPlugin({ 38 | filename: utils.assetsPath('css/[name].[contenthash].css') 39 | }), 40 | // generate dist index.html with correct asset hash for caching. 41 | // you can customize output by editing /index.html 42 | // see https://github.com/ampedandwired/html-webpack-plugin 43 | new HtmlWebpackPlugin({ 44 | filename: process.env.NODE_ENV === 'testing' 45 | ? 'index.html' 46 | : config.build.index, 47 | template: 'index.html', 48 | inject: true, 49 | minify: { 50 | removeComments: true, 51 | collapseWhitespace: true, 52 | removeAttributeQuotes: true 53 | // more options: 54 | // https://github.com/kangax/html-minifier#options-quick-reference 55 | }, 56 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 57 | chunksSortMode: 'dependency' 58 | }), 59 | // split vendor js into its own file 60 | new webpack.optimize.CommonsChunkPlugin({ 61 | name: 'vendor', 62 | minChunks: function (module, count) { 63 | // any required modules inside node_modules are extracted to vendor 64 | return ( 65 | module.resource && 66 | /\.js$/.test(module.resource) && 67 | module.resource.indexOf( 68 | path.join(__dirname, '../node_modules') 69 | ) === 0 70 | ) 71 | } 72 | }), 73 | // extract webpack runtime and module manifest to its own file in order to 74 | // prevent vendor hash from being updated whenever app bundle is updated 75 | new webpack.optimize.CommonsChunkPlugin({ 76 | name: 'manifest', 77 | chunks: ['vendor'] 78 | }) 79 | ] 80 | }) 81 | 82 | if (config.build.productionGzip) { 83 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 84 | 85 | webpackConfig.plugins.push( 86 | new CompressionWebpackPlugin({ 87 | asset: '[path].gz[query]', 88 | algorithm: 'gzip', 89 | test: new RegExp( 90 | '\\.(' + 91 | config.build.productionGzipExtensions.join('|') + 92 | ')$' 93 | ), 94 | threshold: 10240, 95 | minRatio: 0.8 96 | }) 97 | ) 98 | } 99 | 100 | if (config.build.bundleAnalyzerReport) { 101 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 102 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 103 | } 104 | 105 | module.exports = webpackConfig 106 | -------------------------------------------------------------------------------- /build/webpack.test.conf.js: -------------------------------------------------------------------------------- 1 | // This is the webpack config used for unit tests. 2 | 3 | var utils = require('./utils') 4 | var webpack = require('webpack') 5 | var merge = require('webpack-merge') 6 | var baseConfig = require('./webpack.base.conf') 7 | 8 | var webpackConfig = merge(baseConfig, { 9 | // use inline sourcemap for karma-sourcemap-loader 10 | module: { 11 | rules: utils.styleLoaders() 12 | }, 13 | devtool: '#inline-source-map', 14 | plugins: [ 15 | new webpack.DefinePlugin({ 16 | 'process.env': require('../config/test.env') 17 | }) 18 | ] 19 | }) 20 | 21 | // no need for app entry during tests 22 | delete webpackConfig.entry 23 | 24 | module.exports = webpackConfig 25 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../dist/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../dist'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: '/budgeterbium/', 11 | productionSourceMap: true, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'], 18 | // Run the build command with an extra argument to 19 | // View the bundle analyzer report after build finishes: 20 | // `npm run build --report` 21 | // Set to `true` or `false` to always turn it on or off 22 | bundleAnalyzerReport: process.env.npm_config_report 23 | }, 24 | dev: { 25 | env: require('./dev.env'), 26 | port: 8080, 27 | autoOpenBrowser: true, 28 | assetsSubDirectory: 'static', 29 | assetsPublicPath: '/', 30 | proxyTable: {}, 31 | // CSS Sourcemaps off by default because relative paths are "buggy" 32 | // with this option, according to the CSS-Loader README 33 | // (https://github.com/webpack/css-loader#sourcemaps) 34 | // In our experience, they generally work as expected, 35 | // just be aware of this issue when enabling this option. 36 | cssSourceMap: false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var devEnv = require('./dev.env') 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"testing"' 6 | }) 7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | budgeterbium 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "budgeterbium", 3 | "version": "1.0.0", 4 | "description": "A simple budgeting app", 5 | "author": "Matthias Hager ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "build": "node build/build.js", 10 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", 11 | "e2e": "node test/e2e/runner.js", 12 | "test": "npm run unit && npm run e2e", 13 | "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs" 14 | }, 15 | "dependencies": { 16 | "bulma": "^0.3.1", 17 | "localforage": "^1.4.3", 18 | "localforage-startswith": "^1.2.0", 19 | "moment": "^2.17.1", 20 | "vue": "^2.1.10", 21 | "vue-multiselect": "^2.0.0-beta.14", 22 | "vue-router": "^2.2.0", 23 | "vuejs-datepicker": "^0.6.2", 24 | "vuex": "^2.1.2" 25 | }, 26 | "devDependencies": { 27 | "autoprefixer": "^6.7.2", 28 | "babel-core": "^6.22.1", 29 | "babel-eslint": "^7.1.1", 30 | "babel-loader": "^6.2.10", 31 | "babel-plugin-istanbul": "^3.1.2", 32 | "babel-plugin-transform-runtime": "^6.22.0", 33 | "babel-preset-es2015": "^6.22.0", 34 | "babel-preset-stage-2": "^6.22.0", 35 | "babel-register": "^6.22.0", 36 | "chai": "^3.5.0", 37 | "chalk": "^1.1.3", 38 | "chromedriver": "^2.27.2", 39 | "connect-history-api-fallback": "^1.3.0", 40 | "cross-env": "^3.1.4", 41 | "cross-spawn": "^5.0.1", 42 | "css-loader": "^0.26.1", 43 | "eslint": "^3.14.1", 44 | "eslint-config-standard": "^6.2.1", 45 | "eslint-friendly-formatter": "^2.0.7", 46 | "eslint-loader": "^1.6.1", 47 | "eslint-plugin-html": "^2.0.0", 48 | "eslint-plugin-promise": "^3.4.0", 49 | "eslint-plugin-standard": "^2.0.1", 50 | "eventsource-polyfill": "^0.9.6", 51 | "express": "^4.14.1", 52 | "extract-text-webpack-plugin": "^2.0.0-rc.2", 53 | "file-loader": "^0.10.0", 54 | "friendly-errors-webpack-plugin": "^1.1.3", 55 | "function-bind": "^1.1.0", 56 | "html-webpack-plugin": "^2.28.0", 57 | "http-proxy-middleware": "^0.17.3", 58 | "inject-loader": "^2.0.1", 59 | "karma": "^1.4.1", 60 | "karma-coverage": "^1.1.1", 61 | "karma-mocha": "^1.3.0", 62 | "karma-phantomjs-launcher": "^1.0.2", 63 | "karma-sinon-chai": "^1.2.4", 64 | "karma-sourcemap-loader": "^0.3.7", 65 | "karma-spec-reporter": "0.0.26", 66 | "karma-webpack": "^2.0.2", 67 | "lolex": "^1.5.2", 68 | "mocha": "^3.2.0", 69 | "nightwatch": "^0.9.12", 70 | "node-sass": "^4.5.0", 71 | "opn": "^4.0.2", 72 | "ora": "^1.1.0", 73 | "phantomjs-prebuilt": "^2.1.14", 74 | "sass-loader": "^5.0.1", 75 | "scss-loader": "0.0.1", 76 | "selenium-server": "^3.0.1", 77 | "semver": "^5.3.0", 78 | "shelljs": "^0.7.6", 79 | "sinon": "^1.17.7", 80 | "sinon-chai": "^2.8.0", 81 | "url-loader": "^0.5.7", 82 | "vue-loader": "^10.3.0", 83 | "vue-style-loader": "^2.0.0", 84 | "vue-template-compiler": "^2.1.10", 85 | "webpack": "^2.2.1", 86 | "webpack-bundle-analyzer": "^2.2.1", 87 | "webpack-dev-middleware": "^1.10.0", 88 | "webpack-hot-middleware": "^2.16.1", 89 | "webpack-merge": "^2.6.1" 90 | }, 91 | "engines": { 92 | "node": ">= 4.0.0", 93 | "npm": ">= 3.0.0" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/app/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | 26 | 79 | -------------------------------------------------------------------------------- /src/app/accounts/api.js: -------------------------------------------------------------------------------- 1 | import localforage from 'localforage'; 2 | import { processAPIData } from '../../utils'; 3 | 4 | const ACCOUNT_NAMESPACE = 'ACCOUNT-'; 5 | 6 | export const fetchAccounts = () => { 7 | return localforage.startsWith(ACCOUNT_NAMESPACE).then((res) => { 8 | return processAPIData(res); 9 | }); 10 | }; 11 | 12 | export const saveAccount = (account) => { 13 | return localforage.setItem( 14 | ACCOUNT_NAMESPACE + account.id, 15 | account 16 | ).then((value) => { 17 | return value; 18 | }).catch((err) => { 19 | console.log('oops! the account was too far gone, there was nothing we could do to save him ', err); 20 | }); 21 | }; 22 | 23 | export const deleteAccount = (account) => { 24 | return localforage.removeItem( 25 | ACCOUNT_NAMESPACE + account.id 26 | ).then(() => { 27 | return true; 28 | }).catch((err) => { 29 | console.log(err); 30 | return false; 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/accounts/components/AccountsList.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 81 | 82 | 85 | -------------------------------------------------------------------------------- /src/app/accounts/components/CreateUpdateAccount.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 137 | 138 | 142 | -------------------------------------------------------------------------------- /src/app/accounts/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as AccountsList } from './AccountsList'; 2 | export { default as CreateUpdateAccount } from './CreateUpdateAccount'; 3 | -------------------------------------------------------------------------------- /src/app/accounts/index.js: -------------------------------------------------------------------------------- 1 | export { default as routes } from './routes'; 2 | export { default as vuex } from './vuex'; 3 | -------------------------------------------------------------------------------- /src/app/accounts/routes.js: -------------------------------------------------------------------------------- 1 | import * as components from './components'; 2 | 3 | export default [ 4 | { 5 | path: '/', 6 | component: components.AccountsList, 7 | name: 'accountsList' 8 | }, 9 | { 10 | path: '/accounts/create', 11 | component: components.CreateUpdateAccount, 12 | name: 'createAccount' 13 | }, 14 | { 15 | path: '/accounts/:accountId/update', 16 | component: components.CreateUpdateAccount, 17 | name: 'updateAccount', 18 | props: true 19 | } 20 | ]; 21 | -------------------------------------------------------------------------------- /src/app/accounts/vuex/actions.js: -------------------------------------------------------------------------------- 1 | import { guid } from '../../../utils'; 2 | import { deleteAccount as deleteAccountFromAPI, saveAccount, fetchAccounts } from '../api'; 3 | 4 | export const createAccount = ({ commit }, data) => { 5 | let id = guid(); 6 | let account = Object.assign({ id: id }, data); 7 | commit('CREATE_ACCOUNT', {account: account}); 8 | saveAccount(account).then((value) => { 9 | // we've saved the account, what now 10 | }); 11 | }; 12 | 13 | export const updateAccount = ({ commit }, data) => { 14 | commit('UPDATE_ACCOUNT', {account: data}); 15 | saveAccount(data); 16 | }; 17 | 18 | export const deleteAccount = ({ commit }, data) => { 19 | commit('DELETE_ACCOUNT', { account: data }); 20 | deleteAccountFromAPI(data); 21 | }; 22 | 23 | export const loadAccounts = ({ state, commit }) => { 24 | // loads accounts only if they are not already loaded 25 | if (!state.accounts || Object.keys(state.accounts).length === 0) { 26 | return fetchAccounts().then((res) => { 27 | commit('LOAD_ACCOUNTS', res); 28 | }); 29 | } 30 | }; 31 | 32 | export const updateAccountBalance = ({ commit, getters }, data) => { 33 | /* 34 | Accepts a transaction amount and sums that with the current balance 35 | account: account 36 | amount: num 37 | */ 38 | commit('UPDATE_ACCOUNT_BALANCE', data); 39 | saveAccount(getters.getAccountById(data.account.id)); // save the updated account 40 | }; 41 | -------------------------------------------------------------------------------- /src/app/accounts/vuex/getters.js: -------------------------------------------------------------------------------- 1 | export default { 2 | getAccountById: (state, getters) => (accountId) => { 3 | return state.accounts && accountId in state.accounts ? state.accounts[accountId] : false; 4 | }, 5 | 6 | getAccountSelectList: (state, getters) => { 7 | return state.accounts && Object.keys(state.accounts).length > 0 ? Object.values(state.accounts) : []; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/app/accounts/vuex/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions'; 2 | import getters from './getters'; 3 | import mutations from './mutations'; 4 | 5 | const state = { 6 | accounts: {} 7 | }; 8 | 9 | export default { 10 | state, 11 | actions, 12 | mutations, 13 | getters 14 | }; 15 | -------------------------------------------------------------------------------- /src/app/accounts/vuex/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | export default { 4 | CREATE_ACCOUNT (state, payload) { 5 | payload.account.balance = parseFloat(payload.account.balance); 6 | state.accounts[payload.account.id] = payload.account; 7 | }, 8 | 9 | UPDATE_ACCOUNT (state, payload) { 10 | payload.account.balance = parseFloat(payload.account.balance); 11 | state.accounts[payload.account.id] = payload.account; 12 | }, 13 | 14 | UPDATE_ACCOUNT_BALANCE (state, payload) { 15 | state.accounts[payload.account.id].balance += parseFloat(payload.amount); 16 | }, 17 | 18 | DELETE_ACCOUNT (state, payload) { 19 | Vue.delete(state.accounts, payload.account.id); 20 | }, 21 | 22 | LOAD_ACCOUNTS (state, payload) { 23 | state.accounts = payload; 24 | 25 | Object.values(state.accounts).forEach((o) => { o.balance = parseFloat(o.balance); }); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/app/budgets/api.js: -------------------------------------------------------------------------------- 1 | import localforage from 'localforage'; 2 | import { processAPIData } from '../../utils'; 3 | 4 | const BUDGET_NAMESPACE = 'BUDGET-'; 5 | const CATEGORY_NAMESPACE = 'CATEGORY-'; 6 | 7 | export const saveBudget = (budget) => { 8 | budget = Object.assign({}, budget); // clone our object so we can manipulate it before saving 9 | budget.month = budget.month.toJSON(); 10 | 11 | return localforage.setItem( 12 | BUDGET_NAMESPACE + budget.id, 13 | budget 14 | ).then((value) => { 15 | return value; 16 | }).catch((err) => { 17 | console.log('had a little trouble saving that budget', err); 18 | }); 19 | }; 20 | 21 | export const fetchBudgets = () => { 22 | return localforage.startsWith(BUDGET_NAMESPACE).then((res) => { 23 | let budgets = processAPIData(res); 24 | Object.keys(budgets).forEach((o) => { 25 | budgets[o].month = new Date(budgets[o].month); 26 | }); 27 | 28 | return budgets; 29 | }); 30 | }; 31 | 32 | export const saveCategory = (category) => { 33 | return localforage.setItem( 34 | CATEGORY_NAMESPACE + category.id, 35 | category 36 | ).then((value) => { 37 | return value; 38 | }).catch((err) => { 39 | console.log('category problems abound! ', err); 40 | }); 41 | }; 42 | 43 | export const fetchCategories = () => { 44 | return localforage.startsWith(CATEGORY_NAMESPACE).then((res) => { 45 | return processAPIData(res); 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /src/app/budgets/components/BudgetCategory.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | -------------------------------------------------------------------------------- /src/app/budgets/components/BudgetsList.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 82 | 83 | 87 | -------------------------------------------------------------------------------- /src/app/budgets/components/CreateUpdateBudget.vue: -------------------------------------------------------------------------------- 1 | 106 | 107 | 243 | -------------------------------------------------------------------------------- /src/app/budgets/components/CreateUpdateBudgetCategory.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 116 | -------------------------------------------------------------------------------- /src/app/budgets/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as BudgetsList } from './BudgetsList'; 2 | export { default as CreateUpdateBudget } from './CreateUpdateBudget'; 3 | -------------------------------------------------------------------------------- /src/app/budgets/index.js: -------------------------------------------------------------------------------- 1 | export { default as routes } from './routes'; 2 | export { default as vuex } from './vuex'; 3 | -------------------------------------------------------------------------------- /src/app/budgets/routes.js: -------------------------------------------------------------------------------- 1 | import * as components from './components'; 2 | 3 | export default [ 4 | { 5 | path: '/budgets', 6 | component: components.BudgetsList, 7 | name: 'budgetsList' 8 | }, 9 | { 10 | path: '/budgets/create', 11 | component: components.CreateUpdateBudget, 12 | name: 'createBudget' 13 | }, 14 | { 15 | path: '/budgets/:budgetId/update', 16 | component: components.CreateUpdateBudget, 17 | name: 'updateBudget' 18 | } 19 | ]; 20 | -------------------------------------------------------------------------------- /src/app/budgets/vuex/actions.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { guid } from '../../../utils'; 3 | import { saveBudget, fetchBudgets, saveCategory, fetchCategories } from '../api'; 4 | 5 | const verifyUniqueMonth = (budgets, budget) => { 6 | // accepts a list of budgets, and the budget being updated 7 | // returns true if there is no date collision 8 | // returns false if a budget already exists in budgets with the same month as budget 9 | let month = moment(budget.month); 10 | return !Object.values(budgets).find((o) => { 11 | if (o.id === budget.id) return false; // it's the budget we're examining, let's not check if the months are the same 12 | return month.isSame(o.month, 'month'); 13 | }); 14 | }; 15 | 16 | export const createBudget = ({ commit, state }, data) => { 17 | if (!verifyUniqueMonth(state.budgets, data)) { 18 | return Promise.reject(new Error('A budget already exists for this month.')); 19 | } 20 | 21 | let id = guid(); 22 | let budget = Object.assign({ id: id }, data); 23 | 24 | budget.budgeted = 0; 25 | budget.spent = 0; 26 | budget.income = 0; 27 | 28 | commit('CREATE_BUDGET', { budget: budget }); 29 | saveBudget(budget).then((value) => { 30 | // we saved the budget, what's next? 31 | }); 32 | }; 33 | 34 | export const updateBudget = ({ commit, state }, data) => { 35 | if (!verifyUniqueMonth(state.budgets, data)) { 36 | return Promise.reject(new Error('A budget already exists for this month.')); 37 | } 38 | 39 | commit('UPDATE_BUDGET', { budget: data }); 40 | saveBudget(data); 41 | }; 42 | 43 | export const duplicateBudget = ({ commit, dispatch, getters }, data) => { 44 | /* 45 | * Expects an existing budget object, budget, and an budget to be copied, baseBudget 46 | * Duplicates all budget categories and budgeted amounts to the new budget 47 | */ 48 | if (!(data.budget && data.baseBudget)) return Promise.reject(new Error('Incorrect data sent to duplicateBudget')); 49 | 50 | // clone our object in case we received something from the store 51 | let budget = Object.assign({}, data.budget); 52 | 53 | // let's reset some information first 54 | budget.budgeted = 0; 55 | budget.budgetCategories = null; 56 | // note that we don't reset the spent or income because we aren't 57 | // changing any transactions, which are what determine those values 58 | 59 | commit('UPDATE_BUDGET', { budget: budget }); 60 | 61 | budget = getters.getBudgetById(budget.id); 62 | 63 | if ('budgetCategories' in data.baseBudget) { 64 | Object.keys(data.baseBudget.budgetCategories).forEach((key) => { 65 | dispatch('createBudgetCategory', { 66 | budget: budget, 67 | budgetCategory: { 68 | category: data.baseBudget.budgetCategories[key].category, 69 | budgeted: data.baseBudget.budgetCategories[key].budgeted, 70 | spent: 0 // TODO: grab this value when we have transactions! 71 | } 72 | }); 73 | }); 74 | } 75 | 76 | saveBudget(budget); 77 | 78 | return budget; 79 | }; 80 | 81 | export const loadBudgets = ({ state, commit }) => { 82 | if (!state.budgets || Object.keys(state.budgets).length === 0) { 83 | return fetchBudgets().then((res) => { 84 | commit('LOAD_BUDGETS', res); 85 | }); 86 | } 87 | }; 88 | 89 | export const updateBudgetBalance = ({ commit, getters }, data) => { 90 | /* 91 | Accepts a budget and a parameter-value to be updated 92 | param: budgeted|spent 93 | value: num 94 | */ 95 | 96 | commit('UPDATE_BUDGET_BALANCE', data); 97 | saveBudget(getters.getBudgetById(data.budget.id)); 98 | }; 99 | 100 | export const createCategory = ({ commit, state }, data) => { 101 | let id = guid(); 102 | let category = Object.assign({ id: id }, data); 103 | commit('CREATE_CATEGORY', { category: category }); 104 | saveCategory(category); 105 | 106 | return category; 107 | }; 108 | 109 | export const loadCategories = ({ state, commit }) => { 110 | if (!state.categories || Object.keys(state.categories).length === 0) { 111 | return fetchCategories().then((res) => { 112 | commit('LOAD_CATEGORIES', res); 113 | }); 114 | } 115 | }; 116 | 117 | export const createBudgetCategory = ({ commit, dispatch, getters }, data) => { 118 | // create an empty budget categories object if it doesn't exist 119 | if (!data.budget.budgetCategories || Object.keys(data.budget.budgetCategories).length === 0) { 120 | commit('CREATE_EMPTY_BUDGET_CATEGORY_OBJECT', data.budget); 121 | } 122 | 123 | let id = guid(); 124 | let budgetCategory = Object.assign({ id: id }, data.budgetCategory); 125 | 126 | commit('CREATE_BUDGET_CATEGORY', { budget: data.budget, budgetCategory: budgetCategory }); 127 | 128 | // save using the budget in our store 129 | saveBudget(getters.getBudgetById(data.budget.id)); 130 | 131 | dispatch('updateBudgetBalance', { 132 | budget: data.budget, 133 | param: 'budgeted', 134 | value: budgetCategory.budgeted 135 | }); 136 | }; 137 | 138 | export const updateBudgetCategory = ({ commit, dispatch, getters }, data) => { 139 | let newBudget = data.budgetCategory.budgeted; 140 | let oldBudget = getters.getBudgetCategoryById(data.budget.id, data.budgetCategory.id).budgeted; 141 | 142 | if (newBudget !== oldBudget) { 143 | dispatch('updateBudgetBalance', { 144 | budget: data.budget, 145 | param: 'budgeted', 146 | value: newBudget - oldBudget 147 | }); 148 | } 149 | 150 | commit('UPDATE_BUDGET_CATEGORY', data); 151 | 152 | // save using the budget in our store 153 | saveBudget(getters.getBudgetById(data.budget.id)); 154 | }; 155 | 156 | export const updateBudgetCategorySpent = ({ commit, dispatch, getters }, data) => { 157 | // expects data.budget, data.budgetCategory, and data.spent 158 | // spent should always be the amount spent on a transaction, not a total amount 159 | commit('UPDATE_BUDGET_CATEGORY_BALANCE', { budget: data.budget, value: data.amount, budgetCategory: data.budgetCategory, param: 'spent' }); 160 | 161 | dispatch('updateBudgetBalance', { 162 | budget: data.budget, 163 | param: 'spent', 164 | value: data.budget.spent + data.amount 165 | }); 166 | 167 | // save using the budget in our store 168 | saveBudget(getters.getBudgetById(data.budget.id)); 169 | }; 170 | -------------------------------------------------------------------------------- /src/app/budgets/vuex/getters.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | export default { 4 | getBudgetById: (state, getters) => (budgetId) => { 5 | return state.budgets && budgetId in state.budgets ? state.budgets[budgetId] : false; 6 | }, 7 | 8 | getCategoryById: (state, getters) => (categoryId) => { 9 | return state.categories && categoryId in state.categories ? state.categories[categoryId] : false; 10 | }, 11 | 12 | getCategorySelectList: (state, getters) => { 13 | return state.categories && Object.keys(state.categories).length > 0 ? Object.values(state.categories) : []; 14 | }, 15 | 16 | getBudgetCategoryById: (state, getters) => (budgetId, budgetCategoryId) => { 17 | return state.budgets && budgetId in state.budgets 18 | ? state.budgets[budgetId].budgetCategories && budgetCategoryId in state.budgets[budgetId].budgetCategories 19 | ? state.budgets[budgetId].budgetCategories[budgetCategoryId] 20 | : false 21 | : false; 22 | }, 23 | 24 | getBudgetCategoryByBudgetAndCategory: (state, getters) => (budgetId, categoryId) => { 25 | let budget = getters.getBudgetById(budgetId); 26 | if (!budget) return false; 27 | 28 | return budget.budgetCategories ? Object.values(budget.budgetCategories).find((o) => { return o.category === categoryId; }) : false; 29 | }, 30 | 31 | getBudgetByDate: (state, getters) => (date) => { 32 | if (!state.budgets) return false; 33 | 34 | let month = moment(date); 35 | return Object.values(state.budgets).find((o) => { 36 | return month.isSame(o.month, 'month'); // remember this checks month and year are the same https://momentjs.com/docs/#/query/is-same/ 37 | }); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/app/budgets/vuex/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions'; 2 | import mutations from './mutations'; 3 | import getters from './getters'; 4 | 5 | const state = { 6 | budgets: {}, 7 | categories: {} 8 | }; 9 | 10 | export default { 11 | state, 12 | actions, 13 | mutations, 14 | getters 15 | }; 16 | -------------------------------------------------------------------------------- /src/app/budgets/vuex/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | const forceBudgetFloats = (o) => { 4 | o.budgeted = parseFloat(o.budgeted); 5 | o.income = parseFloat(o.income); 6 | o.spent = parseFloat(o.spent); 7 | 8 | if (o.budgetCategories && Object.keys(o.budgetCategories).length > 0) { 9 | Object.values(o.budgetCategories).forEach((bc) => { forceBudgetCategoryFloats(bc); }); 10 | } 11 | }; 12 | 13 | const forceBudgetCategoryFloats = (o) => { 14 | o.budgeted = parseFloat(o.budgeted); 15 | o.spent = parseFloat(o.spent); 16 | }; 17 | 18 | export default { 19 | CREATE_BUDGET (state, payload) { 20 | forceBudgetFloats(payload.budget); 21 | state.budgets[payload.budget.id] = payload.budget; 22 | }, 23 | 24 | UPDATE_BUDGET (state, payload) { 25 | forceBudgetFloats(payload.budget); 26 | state.budgets[payload.budget.id] = payload.budget; 27 | }, 28 | 29 | LOAD_BUDGETS (state, payload) { 30 | state.budgets = payload; 31 | 32 | Object.values(state.budgets).forEach((o) => { 33 | forceBudgetFloats(o); 34 | }); 35 | }, 36 | 37 | UPDATE_BUDGET_BALANCE (state, payload) { 38 | if (!(payload['param'] === 'budgeted' || payload['param'] === 'spent') || payload['param'] === 'income') { 39 | throw new Error('UPDATE_BUDGET_BALANCE expects either { param: "budgeted" } or { param: "spent" } or { param: "income" }'); 40 | } 41 | 42 | state.budgets[payload.budget.id][payload.param] += parseFloat(payload.value); 43 | }, 44 | 45 | CREATE_CATEGORY (state, payload) { 46 | Vue.set(state.categories, payload.category.id, payload.category); 47 | }, 48 | 49 | UPDATE_CATEGORY (state, payload) { 50 | state.categories[payload.category.id] = payload.category; 51 | }, 52 | 53 | LOAD_CATEGORIES (state, payload) { 54 | state.categories = payload; 55 | }, 56 | 57 | CREATE_EMPTY_BUDGET_CATEGORY_OBJECT (state, payload) { 58 | Vue.set(state.budgets[payload.id], 'budgetCategories', {}); 59 | }, 60 | 61 | CREATE_BUDGET_CATEGORY (state, payload) { 62 | forceBudgetCategoryFloats(payload.budgetCategory); 63 | Vue.set(state.budgets[payload.budget.id].budgetCategories, payload.budgetCategory.id, payload.budgetCategory); 64 | }, 65 | 66 | UPDATE_BUDGET_CATEGORY (state, payload) { 67 | forceBudgetCategoryFloats(payload.budgetCategory); 68 | state.budgets[payload.budget.id].budgetCategories[payload.budgetCategory.id] = payload.budgetCategory; 69 | }, 70 | 71 | UPDATE_BUDGET_CATEGORY_BALANCE (state, payload) { 72 | if (!(payload['param'] === 'budgeted' || payload['param'] === 'spent')) { 73 | throw new Error('UPDATE_BUDGET_BALANCE expects either { param: "budgeted" } or { param: "spent" }'); 74 | } 75 | 76 | state.budgets[payload.budget.id].budgetCategories[payload.budgetCategory.id][payload.param] += parseFloat(payload.value); 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /src/app/index.js: -------------------------------------------------------------------------------- 1 | export { default as routes } from './routes'; 2 | export { default as vuex } from './vuex'; 3 | export { default as App } from './App'; 4 | -------------------------------------------------------------------------------- /src/app/navigation/components/Navigation.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 43 | 44 | 73 | -------------------------------------------------------------------------------- /src/app/navigation/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navigation } from './Navigation'; 2 | -------------------------------------------------------------------------------- /src/app/navigation/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiaswh/budgeterbium/572a573f3a437ebde8e2c73e7debd9bb3b75ad53/src/app/navigation/index.js -------------------------------------------------------------------------------- /src/app/routes.js: -------------------------------------------------------------------------------- 1 | import { routes as accounts } from './accounts'; 2 | import { routes as transactions } from './transactions'; 3 | import { routes as budgets } from './budgets'; 4 | 5 | export default [ ...accounts, ...transactions, ...budgets ]; 6 | -------------------------------------------------------------------------------- /src/app/transactions/api.js: -------------------------------------------------------------------------------- 1 | import localforage from 'localforage'; 2 | import { processAPIData } from '../../utils'; 3 | 4 | const TRANSACTION_NAMESPACE = 'TRANSACTION-'; 5 | const BUSINESS_NAMESPACE = 'BUSINESS-'; 6 | 7 | export const fetchTransactions = () => { 8 | return localforage.startsWith(TRANSACTION_NAMESPACE).then((res) => { 9 | return processAPIData(res); 10 | }); 11 | }; 12 | 13 | export const saveTransaction = (transaction) => { 14 | return localforage.setItem( 15 | TRANSACTION_NAMESPACE + transaction.id, 16 | transaction 17 | ).then((value) => { 18 | return value; 19 | }).catch((err) => { 20 | console.log('he\'s dead, jim, the transaction is dead', err); 21 | }); 22 | }; 23 | 24 | export const deleteTransaction = (transaction) => { 25 | return localforage.removeItem( 26 | TRANSACTION_NAMESPACE + transaction.id 27 | ).then(() => { 28 | return true; 29 | }).catch((err) => { 30 | console.log(err); 31 | return false; 32 | }); 33 | }; 34 | 35 | export const fetchBusinesses = () => { 36 | return localforage.startsWith(BUSINESS_NAMESPACE).then((res) => { 37 | return processAPIData(res); 38 | }); 39 | }; 40 | 41 | export const saveBusiness = (business) => { 42 | return localforage.setItem( 43 | BUSINESS_NAMESPACE + business.id, 44 | business 45 | ).then((value) => { 46 | return value; 47 | }); 48 | }; 49 | 50 | export const deleteBusiness = (business) => { 51 | return localforage.removeItem( 52 | BUSINESS_NAMESPACE + business.id 53 | ).then(() => { 54 | return true; 55 | }).catch((err) => { 56 | console.log(err); 57 | return false; 58 | }); 59 | }; 60 | -------------------------------------------------------------------------------- /src/app/transactions/components/CreateUpdateTransaction.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 208 | -------------------------------------------------------------------------------- /src/app/transactions/components/Transaction.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 38 | -------------------------------------------------------------------------------- /src/app/transactions/components/TransactionsList.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 91 | 92 | 96 | -------------------------------------------------------------------------------- /src/app/transactions/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as TransactionsListView } from './TransactionsList'; 2 | -------------------------------------------------------------------------------- /src/app/transactions/index.js: -------------------------------------------------------------------------------- 1 | export { default as routes } from './routes'; 2 | export { default as vuex } from './vuex'; 3 | -------------------------------------------------------------------------------- /src/app/transactions/routes.js: -------------------------------------------------------------------------------- 1 | import * as components from './components'; 2 | 3 | export default [ 4 | { 5 | path: '/transactions', 6 | name: 'transactionsList', 7 | component: components.TransactionsListView 8 | } 9 | ]; 10 | -------------------------------------------------------------------------------- /src/app/transactions/vuex/actions.js: -------------------------------------------------------------------------------- 1 | import { guid } from '../../../utils'; 2 | import { deleteTransaction as deleteTransactionFromAPI, deleteBusiness as deleteBusinessFromAPI, fetchBusinesses, fetchTransactions, saveBusiness, saveTransaction } from '../api'; 3 | 4 | const prepareTransaction = (getters, data) => { 5 | // code shared by createTransaction and updateTransaction 6 | 7 | // find the budget based on the date 8 | // we'll go ahead and do this for existing transactions in case the user changes the date 9 | // in the future we may want to only do it on a date change 10 | let budget = getters.getBudgetByDate(data.date); 11 | if (!budget) throw new Error('Could not find a budget for the date ' + data.date); 12 | data.budget = budget.id; 13 | console.log(data.budget); 14 | 15 | // tell the budget category that the transaction is occurring so it can update its amount 16 | let budgetCategory = getters.getBudgetCategoryByBudgetAndCategory(budget.id, data.category); 17 | if (!budgetCategory) throw new Error('Could not find a budget category for ' + data.category); 18 | // don't dispatch yet, we are just preparing data here 19 | 20 | return { preparedData: data, budgetCategory: budgetCategory, budget: budget }; 21 | }; 22 | 23 | export const createTransaction = ({ commit, dispatch, getters }, data) => { 24 | let { preparedData, budgetCategory, budget } = prepareTransaction(getters, data); 25 | 26 | let id = guid(); 27 | let transaction = Object.assign({ id: id }, preparedData); 28 | 29 | // update the budget category, which updates the budget spend total 30 | dispatch('updateBudgetCategorySpent', { 31 | budgetCategory: budgetCategory, 32 | budget: budget, 33 | amount: transaction.amount 34 | }); 35 | 36 | // update the account balance 37 | dispatch('updateAccountBalance', { 38 | account: getters.getAccountById(data.account), 39 | amount: transaction.amount 40 | }); 41 | 42 | commit('CREATE_TRANSACTION', { transaction: transaction }); 43 | saveTransaction(transaction); 44 | }; 45 | 46 | export const updateTransaction = ({ commit, getters }, data) => { 47 | // TODO: handle any change the user could make here! Including 48 | // updating budgets or account balances 49 | 50 | let { preparedData } = prepareTransaction(getters, data); 51 | 52 | commit('UPDATE_TRANSACTION', { transaction: preparedData }); 53 | saveTransaction(preparedData); 54 | }; 55 | 56 | export const deleteTransaction = ({ commit }, data) => { 57 | commit('DELETE_TRANSACTION', { transaction: data }); 58 | deleteTransactionFromAPI(data); 59 | }; 60 | 61 | export const loadTransactions = ({ state, commit }) => { 62 | // loads transactions if they're not already loaded 63 | if (!state.transactions || Object.keys(state.transactions).length === 0) { 64 | return fetchTransactions().then((res) => { 65 | commit('LOAD_TRANSACTIONS', res); 66 | }); 67 | } 68 | }; 69 | 70 | export const createBusiness = ({ commit, state }, data) => { 71 | let id = guid(); 72 | let business = Object.assign({ id: id }, data); 73 | commit('CREATE_BUSINESS', { business: business }); 74 | saveBusiness(business); 75 | 76 | return business; 77 | }; 78 | 79 | export const loadBusinesses = ({ state, commit }) => { 80 | // loads businesses if they're not already loaded 81 | if (!state.businesses || Object.keys(state.businesses).length === 0) { 82 | return fetchBusinesses().then((res) => { 83 | commit('LOAD_BUSINESSES', res); 84 | }); 85 | } 86 | }; 87 | 88 | export const deleteBusiness = ({ commit }, data) => { 89 | commit('DELETE_BUSINESS', { business: data }); 90 | deleteBusinessFromAPI(data); 91 | }; 92 | -------------------------------------------------------------------------------- /src/app/transactions/vuex/getters.js: -------------------------------------------------------------------------------- 1 | export default { 2 | getBusinessSelectList: (state, getters) => { 3 | return state.businesses && Object.keys(state.businesses).length > 0 ? Object.values(state.businesses) : []; 4 | }, 5 | 6 | getBusinessById: (state, getters) => (businessId) => { 7 | return state.businesses && businessId in state.businesses ? state.businesses[businessId] : false; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/app/transactions/vuex/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions'; 2 | import mutations from './mutations'; 3 | import getters from './getters'; 4 | 5 | const state = { 6 | transactions: [], 7 | businesses: [] 8 | }; 9 | 10 | export default { 11 | state, 12 | actions, 13 | mutations, 14 | getters 15 | }; 16 | -------------------------------------------------------------------------------- /src/app/transactions/vuex/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | const forceFloats = (o) => { 4 | o.amount = parseFloat(o.amount); 5 | }; 6 | 7 | export default { 8 | CREATE_TRANSACTION (state, payload) { 9 | forceFloats(payload.transaction); 10 | Vue.set(state.transactions, payload.transaction.id, payload.transaction); 11 | }, 12 | 13 | UPDATE_TRANSACTION (state, payload) { 14 | forceFloats(payload.transaction); 15 | state.transactions[payload.transaction.id] = payload.transaction; 16 | }, 17 | 18 | DELETE_TRANSACTION (state, payload) { 19 | Vue.delete(state.transactions, payload.transaction.id); 20 | }, 21 | 22 | LOAD_TRANSACTIONS (state, payload) { 23 | state.transactions = payload; 24 | 25 | Object.values(state.transactions).forEach((o) => { forceFloats(o); }); 26 | }, 27 | 28 | LOAD_BUSINESSES (state, payload) { 29 | state.businesses = payload; 30 | }, 31 | 32 | CREATE_BUSINESS (state, payload) { 33 | state.businesses[payload.business.id] = payload.business; 34 | }, 35 | 36 | DELETE_BUSINESS (state, payload) { 37 | Vue.delete(state.businesses, payload.business.id); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/app/vuex.js: -------------------------------------------------------------------------------- 1 | import { vuex as accounts } from './accounts'; 2 | import { vuex as transactions } from './transactions'; 3 | import { vuex as budgets } from './budgets'; 4 | 5 | export default { accounts, transactions, budgets }; 6 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiaswh/budgeterbium/572a573f3a437ebde8e2c73e7debd9bb3b75ad53/src/assets/logo.png -------------------------------------------------------------------------------- /src/consts.js: -------------------------------------------------------------------------------- 1 | export const CATEGORIES = { 2 | 'CREDIT_CARD': 'Credit Card', 3 | 'CHECKING': 'Checking', 4 | 'SAVINGS': 'Savings' 5 | }; 6 | -------------------------------------------------------------------------------- /src/filters.js: -------------------------------------------------------------------------------- 1 | import momentjs from 'moment'; 2 | 3 | export const moment = (date, format) => { 4 | format = format || 'MMMM YYYY'; 5 | return momentjs(date).format(format); 6 | }; 7 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue'; 4 | import localforage from 'localforage'; 5 | require('localforage-startswith'); 6 | import 'bulma/css/bulma.css'; 7 | 8 | import { App } from './app'; 9 | import router from './router'; 10 | import store from './store'; 11 | 12 | localforage.config({ 13 | name: 'budgeterbium' 14 | }); 15 | 16 | /* eslint-disable no-new */ 17 | new Vue({ 18 | el: '#app', 19 | store, 20 | router, 21 | template: '', 22 | components: { App } 23 | }); 24 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import { routes } from '../app'; 4 | 5 | Vue.use(Router); 6 | 7 | export default new Router({ 8 | mode: 'hash', 9 | routes: routes 10 | }); 11 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import { vuex } from '../app'; 4 | 5 | Vue.use(Vuex); 6 | 7 | const debug = process.env.NODE_ENV !== 'production'; 8 | 9 | export default new Vuex.Store({ 10 | modules: vuex, 11 | strict: debug 12 | }); 13 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | // thanks to http://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript 2 | export const guid = function () { 3 | function s4 () { 4 | return Math.floor((1 + Math.random()) * 0x10000) 5 | .toString(16) 6 | .substring(1); 7 | } 8 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + 9 | s4() + '-' + s4() + s4() + s4(); 10 | }; 11 | 12 | export const processAPIData = function (data) { 13 | /* 14 | Converts the data formatted for IndexedDB / API into the format 15 | our application uses. 16 | */ 17 | let res = {}; 18 | Object.keys(data).forEach((key) => { res[data[key].id] = data[key]; }); 19 | return res; 20 | }; 21 | 22 | export const sortObjects = (objects, key, reverse = false) => { 23 | /* 24 | Sorts a list of objects based on the value of their key property 25 | */ 26 | let sortedKeys = Object.keys(objects).sort((a, b) => { 27 | if (reverse) return objects[b][key] - objects[a][key]; 28 | return objects[a][key] - objects[b][key]; 29 | }); 30 | 31 | return sortedKeys.map((k) => { 32 | return objects[k]; 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiaswh/budgeterbium/572a573f3a437ebde8e2c73e7debd9bb3b75ad53/static/.gitkeep -------------------------------------------------------------------------------- /test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // the name of the method is the filename. 3 | // can be used in tests like this: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // for how to write custom assertions see 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | exports.assertion = function (selector, count) { 10 | this.message = 'Testing if element <' + selector + '> has count: ' + count 11 | this.expected = count 12 | this.pass = function (val) { 13 | return val === this.expected 14 | } 15 | this.value = function (res) { 16 | return res.value 17 | } 18 | this.command = function (cb) { 19 | var self = this 20 | return this.api.execute(function (selector) { 21 | return document.querySelectorAll(selector).length 22 | }, [selector], function (res) { 23 | cb.call(self, res) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | var config = require('../../config') 3 | 4 | // http://nightwatchjs.org/guide#settings-file 5 | module.exports = { 6 | src_folders: ['test/e2e/specs'], 7 | output_folder: 'test/e2e/reports', 8 | custom_assertions_path: ['test/e2e/custom-assertions'], 9 | 10 | selenium: { 11 | start_process: true, 12 | server_path: require('selenium-server').path, 13 | host: '127.0.0.1', 14 | port: 4444, 15 | cli_args: { 16 | 'webdriver.chrome.driver': require('chromedriver').path 17 | } 18 | }, 19 | 20 | test_settings: { 21 | default: { 22 | selenium_port: 4444, 23 | selenium_host: 'localhost', 24 | silent: true, 25 | globals: { 26 | devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port) 27 | } 28 | }, 29 | 30 | chrome: { 31 | desiredCapabilities: { 32 | browserName: 'chrome', 33 | javascriptEnabled: true, 34 | acceptSslCerts: true 35 | } 36 | }, 37 | 38 | firefox: { 39 | desiredCapabilities: { 40 | browserName: 'firefox', 41 | javascriptEnabled: true, 42 | acceptSslCerts: true 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server using production config 2 | process.env.NODE_ENV = 'testing' 3 | var server = require('../../build/dev-server.js') 4 | 5 | // 2. run the nightwatch test suite against it 6 | // to run in additional browsers: 7 | // 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings" 8 | // 2. add it to the --env flag below 9 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 10 | // For more information on Nightwatch's config file, see 11 | // http://nightwatchjs.org/guide#settings-file 12 | var opts = process.argv.slice(2) 13 | if (opts.indexOf('--config') === -1) { 14 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']) 15 | } 16 | if (opts.indexOf('--env') === -1) { 17 | opts = opts.concat(['--env', 'chrome']) 18 | } 19 | 20 | var spawn = require('cross-spawn') 21 | var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }) 22 | 23 | runner.on('exit', function (code) { 24 | server.close() 25 | process.exit(code) 26 | }) 27 | 28 | runner.on('error', function (err) { 29 | server.close() 30 | throw err 31 | }) 32 | -------------------------------------------------------------------------------- /test/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': function (browser) { 6 | // automatically uses dev Server port from /config.index.js 7 | // default: http://localhost:8080 8 | // see nightwatch.conf.js 9 | const devServer = browser.globals.devServerURL 10 | 11 | browser 12 | .url(devServer) 13 | .waitForElementVisible('#app', 5000) 14 | .assert.elementPresent('.hello') 15 | .assert.containsText('h1', 'Welcome to Your Vue.js App') 16 | .assert.elementCount('img', 1) 17 | .end() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | // Polyfill fn.bind() for PhantomJS 2 | /* eslint-disable no-extend-native */ 3 | Function.prototype.bind = require('function-bind') 4 | 5 | // require all test files (files that ends with .spec.js) 6 | const testsContext = require.context('./specs', true, /\.spec$/) 7 | testsContext.keys().forEach(testsContext) 8 | 9 | // require all src files except main.js for coverage. 10 | // you can also change this to match only the subset of files that 11 | // you want coverage for. 12 | const srcContext = require.context('src', true, /^\.\/(?!main(\.js)?$)/) 13 | srcContext.keys().forEach(srcContext) 14 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This is a karma config file. For more details see 2 | // http://karma-runner.github.io/0.13/config/configuration-file.html 3 | // we are also using it with karma-webpack 4 | // https://github.com/webpack/karma-webpack 5 | 6 | var webpackConfig = require('../../build/webpack.test.conf') 7 | 8 | module.exports = function (config) { 9 | config.set({ 10 | // to run in additional browsers: 11 | // 1. install corresponding karma launcher 12 | // http://karma-runner.github.io/0.13/config/browsers.html 13 | // 2. add it to the `browsers` array below. 14 | browsers: ['PhantomJS'], 15 | frameworks: ['mocha', 'sinon-chai'], 16 | reporters: ['spec', 'coverage'], 17 | files: ['./index.js'], 18 | preprocessors: { 19 | './index.js': ['webpack', 'sourcemap'] 20 | }, 21 | webpack: webpackConfig, 22 | webpackMiddleware: { 23 | noInfo: true 24 | }, 25 | coverageReporter: { 26 | dir: './coverage', 27 | reporters: [ 28 | { type: 'lcov', subdir: '.' }, 29 | { type: 'text-summary' } 30 | ] 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /test/unit/specs/Hello.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Hello from 'src/components/Hello' 3 | 4 | describe('Hello.vue', () => { 5 | it('should render correct contents', () => { 6 | const Constructor = Vue.extend(Hello) 7 | const vm = new Constructor().$mount() 8 | expect(vm.$el.querySelector('.hello h1').textContent) 9 | .to.equal('Welcome to Your Vue.js App') 10 | }) 11 | }) 12 | --------------------------------------------------------------------------------