Compare commits

1 Commits
Author SHA1 Message Date
Jenkins 5402e448fd Deploy by Jenkins 2021-05-15 16:06:16 +00:00
41 changed files with 125 additions and 17625 deletions
-18
View File
@@ -1,18 +0,0 @@
{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}],
"stage-2"
],
"plugins": ["transform-vue-jsx", "transform-runtime"],
"env": {
"test": {
"presets": ["env", "stage-2"],
"plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"]
}
}
}
-9
View File
@@ -1,9 +0,0 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
-5
View File
@@ -1,5 +0,0 @@
/build/
/config/
/dist/
/*.js
/test/unit/coverage/
-29
View File
@@ -1,29 +0,0 @@
// https://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint'
},
env: {
browser: true,
},
extends: [
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
'plugin:vue/essential',
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
'standard'
],
// required to lint *.vue files
plugins: [
'vue'
],
// add your custom rules here
rules: {
// allow async-await
'generator-star-spacing': 'off',
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
}
-15
View File
@@ -1,15 +0,0 @@
.DS_Store
node_modules/
/dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/test/unit/coverage/
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
-10
View File
@@ -1,10 +0,0 @@
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
"plugins": {
"postcss-import": {},
"postcss-url": {},
// to edit target browsers: use "browserslist" field in package.json
"autoprefixer": {}
}
}
Vendored
-46
View File
@@ -1,46 +0,0 @@
pipeline {
agent any
tools {
nodejs '15.14.0'
}
stages {
stage ('Initialize') {
steps {
echo 'Initializing..'
}
}
stage('Build') {
steps {
sh 'npm install'
sh 'npm run build'
}
}
stage('Archive') {
when {
branch 'master'
expression {
currentBuild.result == null || currentBuild.result == 'SUCCESS'
}
}
steps {
echo 'Archiving....'
dir("dist") {
sh 'git init'
sh 'git add .'
sh 'git config --global user.email "jenkins@bosym.de"'
sh 'git config --global user.name "Jenkins"'
sh 'git commit -m "Deploy by Jenkins"'
withCredentials([usernamePassword(credentialsId: 'f9aaa85c-5f26-4e5c-82fa-1c05127a6648', usernameVariable: 'username', passwordVariable: 'password')])
{
sh "git push --force --quiet \"https://$username:$password@git.syma.dev/Pascal/expense.git\" master:gh-pages"
}
}
deleteDir()
}
}
}
}
-64
View File
@@ -1,64 +0,0 @@
# expense WIP
> Basic Expense Tracker to analyse yearly/monthly/daily transactions.
>
> Completely cloudless, cookieless and trackingless. All data is stored in LocalStorage.
>
> Build with Vue, Webpack, Bootstrap(Vue) and JUI Charts
Hosted here: [expense.syma.dev](https://expense.syma.dev).
![Input fields][input]
![Generated charts][charts]
## Roadmap
- [x] Data handling
- [x] Save/Read from LocalStorage
- [x] Import/Export in UI
- [ ] Input
- [x] Name and Frequency
- [x] Tagging
- [ ] Effective date (of month)
- [ ] Analysis
- [x] Calculate average per day/month/year
- [ ] Charts
- [x] Basic Pie of every entry
- [x] Basic Pie of every tag
- [ ] Line
- [ ] General
- [ ] Branding
- [ ] Logo
- [ ] Name
- [ ] Domain
- [ ] Rethink UI
- [ ] Consider more analysis
- [ ] Refactor, clean codebase and upgrade
- [ ] Localize
## Build Setup
``` bash
# install dependencies
npm install
# serve with hot reload at localhost:8080
npm run dev
# build for production with minification
npm run build
# build for production and view the bundle analyzer report
npm run build --report
# run unit tests
npm run unit
# run all tests
npm test
```
Based on [vuejs-templates/webpack](https://github.com/vuejs-templates/webpack) old template.
[input]: https://expense.syma.dev/static/input.png "Input fields"
[charts]: https://expense.syma.dev/static/charts.png "Generated charts"
-41
View File
@@ -1,41 +0,0 @@
'use strict'
require('./check-versions')()
process.env.NODE_ENV = 'production'
const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')
const spinner = ora('building for production...')
spinner.start()
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, (err, stats) => {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
chunks: false,
chunkModules: false
}) + '\n\n')
if (stats.hasErrors()) {
console.log(chalk.red(' Build failed with errors.\n'))
process.exit(1)
}
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})
-54
View File
@@ -1,54 +0,0 @@
'use strict'
const chalk = require('chalk')
const semver = require('semver')
const packageConfig = require('../package.json')
const shell = require('shelljs')
function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim()
}
const versionRequirements = [
{
name: 'node',
currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node
}
]
if (shell.which('npm')) {
versionRequirements.push({
name: 'npm',
currentVersion: exec('npm --version'),
versionRequirement: packageConfig.engines.npm
})
}
module.exports = function () {
const warnings = []
for (let i = 0; i < versionRequirements.length; i++) {
const mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' +
chalk.red(mod.currentVersion) + ' should be ' +
chalk.green(mod.versionRequirement)
)
}
}
if (warnings.length) {
console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:'))
console.log()
for (let i = 0; i < warnings.length; i++) {
const warning = warnings[i]
console.log(' ' + warning)
}
console.log()
process.exit(1)
}
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

-101
View File
@@ -1,101 +0,0 @@
'use strict'
const path = require('path')
const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const packageConfig = require('../package.json')
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap
}
}
const postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) {
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
const output = []
const loaders = exports.cssLoaders(options)
for (const extension in loaders) {
const loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
exports.createNotifierCallback = () => {
const notifier = require('node-notifier')
return (severity, errors) => {
if (severity !== 'error') return
const error = errors[0]
const filename = error.file && error.file.split('!').pop()
notifier.notify({
title: packageConfig.name,
message: severity + ': ' + error.name,
subtitle: filename || '',
icon: path.join(__dirname, 'logo.png')
})
}
}
-22
View File
@@ -1,22 +0,0 @@
'use strict'
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap
module.exports = {
loaders: utils.cssLoaders({
sourceMap: sourceMapEnabled,
extract: isProduction
}),
cssSourceMap: sourceMapEnabled,
cacheBusting: config.dev.cacheBusting,
transformToRequire: {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: 'xlink:href'
}
}
-92
View File
@@ -1,92 +0,0 @@
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
const createLintingRule = () => ({
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter'),
emitWarning: !config.dev.showEslintErrorsInOverlay
}
})
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: './src/main.js'
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
module: {
rules: [
...(config.dev.useEslint ? [createLintingRule()] : []),
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
node: {
// prevent webpack from injecting useless setImmediate polyfill because Vue
// source contains it (although only uses it if it's native).
setImmediate: false,
// prevent webpack from injecting mocks to Node native modules
// that does not make sense for the client
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
}
}
-95
View File
@@ -1,95 +0,0 @@
'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')
const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)
const devWebpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
},
// cheap-module-eval-source-map is faster for development
devtool: config.dev.devtool,
// these devServer options should be customized in /config/index.js
devServer: {
clientLogLevel: 'warning',
historyApiFallback: {
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
],
},
hot: true,
contentBase: false, // since we use CopyWebpackPlugin.
compress: true,
host: HOST || config.dev.host,
port: PORT || config.dev.port,
open: config.dev.autoOpenBrowser,
overlay: config.dev.errorOverlay
? { warnings: false, errors: true }
: false,
publicPath: config.dev.assetsPublicPath,
proxy: config.dev.proxyTable,
quiet: true, // necessary for FriendlyErrorsPlugin
watchOptions: {
poll: config.dev.poll,
}
},
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
module.exports = new Promise((resolve, reject) => {
portfinder.basePort = process.env.PORT || config.dev.port
portfinder.getPort((err, port) => {
if (err) {
reject(err)
} else {
// publish the new Port, necessary for e2e tests
process.env.PORT = port
// add port to devServer config
devWebpackConfig.devServer.port = port
// Add FriendlyErrorsPlugin
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
compilationSuccessInfo: {
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
},
onErrors: config.dev.notifyOnErrors
? utils.createNotifierCallback()
: undefined
}))
resolve(devWebpackConfig)
}
})
})
-149
View File
@@ -1,149 +0,0 @@
'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const env = process.env.NODE_ENV === 'testing'
? require('../config/test.env')
: require('../config/prod.env')
const webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
usePostCSS: true
})
},
devtool: config.build.productionSourceMap ? config.build.devtool : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
'process.env': env
}),
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
}),
// extract css into its own file
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
// Setting the following option to `false` will not extract CSS from codesplit chunks.
// Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
// It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
// increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
allChunks: true,
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } }
: { safe: true }
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: process.env.NODE_ENV === 'testing'
? 'index.html'
: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}),
// keep module.id stable when vendor modules does not change
new webpack.HashedModuleIdsPlugin(),
// enable scope hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks (module) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
// This instance extracts shared chunks from code splitted chunks and bundles them
// in a separate chunk, similar to the vendor chunk
// see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
if (config.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
}
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig
-7
View File
@@ -1,7 +0,0 @@
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})
-76
View File
@@ -1,76 +0,0 @@
'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.
const path = require('path')
module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
// Various Dev Server settings
host: 'localhost', // can be overwritten by process.env.HOST
port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
autoOpenBrowser: false,
errorOverlay: true,
notifyOnErrors: true,
poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
// Use Eslint Loader?
// If true, your code will be linted during bundling and
// linting errors and warnings will be shown in the console.
useEslint: true,
// If true, eslint errors and warnings will also be shown in the error overlay
// in the browser.
showEslintErrorsInOverlay: false,
/**
* Source Maps
*/
// https://webpack.js.org/configuration/devtool/#development
devtool: 'cheap-module-eval-source-map',
// If you have problems debugging vue-files in devtools,
// set this to false - it *may* help
// https://vue-loader.vuejs.org/en/options.html#cachebusting
cacheBusting: true,
cssSourceMap: true
},
build: {
// Template for index.html
index: path.resolve(__dirname, '../dist/index.html'),
// Paths
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
/**
* Source Maps
*/
productionSourceMap: true,
// https://webpack.js.org/configuration/devtool/#production
devtool: '#source-map',
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report
}
}
-4
View File
@@ -1,4 +0,0 @@
'use strict'
module.exports = {
NODE_ENV: '"production"'
}
-7
View File
@@ -1,7 +0,0 @@
'use strict'
const merge = require('webpack-merge')
const devEnv = require('./dev.env')
module.exports = merge(devEnv, {
NODE_ENV: '"testing"'
})
+1 -12
View File
@@ -1,12 +1 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Expense Tracker</title>
</head>
<body class="py-4">
<div id="app" class="container"></div>
<!-- built files will be auto injected -->
</body>
</html>
<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,shrink-to-fit=no"><title>Expense Tracker</title><link href=/static/css/app.347d7846e7fa2ba5a314cb5302db9d3f.css rel=stylesheet></head><body class=py-4><div id=app class=container></div><script type=text/javascript src=/static/js/manifest.6e32a8920e40b3fc2d09.js></script><script type=text/javascript src=/static/js/vendor.24cf67262cb4a0d61451.js></script><script type=text/javascript src=/static/js/app.e99c1f67565345fb7d54.js></script></body></html>
-15887
View File
File diff suppressed because it is too large Load Diff
-89
View File
@@ -1,89 +0,0 @@
{
"name": "expense",
"version": "1.0.0",
"description": "Expense Tracker",
"author": "Pascal Syma <pascal@syma.dev>",
"private": true,
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"unit": "jest --config test/unit/jest.conf.js --coverage",
"test": "npm run unit",
"lint": "eslint --ext .js,.vue src test/unit",
"build": "node build/build.js"
},
"dependencies": {
"@popperjs/core": "^2.9.2",
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.21.2",
"bootswatch": "^4.6.0",
"jquery": "^3.6.0",
"popper.js": "^1.16.1",
"vue": "^2.5.2",
"vue-enums": "^1.0.0",
"vue-graph": "^0.8.7"
},
"devDependencies": {
"autoprefixer": "^7.1.2",
"babel-core": "^6.22.1",
"babel-eslint": "^8.2.1",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-jest": "^21.0.2",
"babel-loader": "^7.1.1",
"babel-plugin-dynamic-import-node": "^1.2.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-plugin-transform-vue-jsx": "^3.5.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"chalk": "^2.0.1",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.0",
"eslint": "^4.15.0",
"eslint-config-standard": "^10.2.1",
"eslint-friendly-formatter": "^3.0.0",
"eslint-loader": "^1.7.1",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^5.2.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^4.0.0",
"exports-loader": "^2.0.0",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1",
"jest": "^22.0.4",
"jest-serializer-vue": "^0.3.0",
"node-notifier": "^5.1.2",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"portfinder": "^1.0.13",
"postcss-import": "^11.0.0",
"postcss-loader": "^2.0.8",
"postcss-url": "^7.2.1",
"rimraf": "^2.6.0",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"uglifyjs-webpack-plugin": "^1.1.1",
"url-loader": "^0.5.8",
"vue-jest": "^1.0.2",
"vue-loader": "^13.3.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.5.2",
"webpack": "^3.6.0",
"webpack-bundle-analyzer": "^2.9.0",
"webpack-dev-server": "^2.9.1",
"webpack-merge": "^4.1.0"
},
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
-443
View File
@@ -1,443 +0,0 @@
<template>
<div id="app" class="container">
<b-row class="results">
<b-col>
<b-card title="Analysis" no-body>
<b-card-header header-text-variant="white" class="container-fluid">
<b-row style="display: table">
<b-col md="8" lg="10" style="display: table-cell; vertical-align: middle">Result</b-col>
<b-col md="2" class="float-right align-self-center" style="display: inline-table">
<b-button-group size="sm">
<b-button v-b-modal.modalImport variant="info"><b-icon-upload /> Import</b-button>
<b-button id="exportButton" @click="exportData" variant="success"><b-icon-download /> Export</b-button>
<b-tooltip ref="exportTooltip" target="exportButton" disabled>Copied to clipboard!</b-tooltip>
<b-button @click="askReset" variant="danger"><b-icon-trash /> Reset</b-button>
</b-button-group>
</b-col>
</b-row>
</b-card-header>
<b-card-body>
<b-row v-if="calculated.in.data.length + calculated.out.data.length > 1">
<b-col md="6"
v-for="key in ['in', 'out']"
:key="key">
<b-card
:header="calculated[key].title"
bg-variant="secondary">
<graph-pie
:width="NaN"
:height="400"
:padding-top="100"
:padding-bottom="100"
:padding-left="100"
:padding-right="100"
:names="calculated[key].labels"
:values="calculated[key].data"
:show-text-type="'outside'"
:active-event="'click'"
:data-format="dataFormat"
:styles="pieStyle">
<legends :names="calculated[key].labels"></legends>
<tooltip :names="calculated[key].labels"></tooltip></graph-pie>
</b-card>
</b-col>
</b-row>
<b-row v-if="calculated.inTag.data.length + calculated.outTag.data.length > 1">
<b-col md="6"
v-for="key in ['inTag', 'outTag']"
:key="key">
<b-card
:header="calculated[key].title"
bg-variant="secondary">
<graph-pie
:width="NaN"
:height="400"
:padding-top="100"
:padding-bottom="100"
:padding-left="100"
:padding-right="100"
:names="calculated[key].labels"
:values="calculated[key].data"
:show-text-type="'outside'"
:active-event="'click'"
:data-format="dataFormat"
:styles="pieStyle">
<legends :names="calculated[key].labels"></legends>
<tooltip :names="calculated[key].labels"></tooltip></graph-pie>
</b-card>
</b-col>
</b-row>
<b-card-group class="text-center">
<b-card header="Yearly">
<p>Income: {{ calculated.yearly.in | currency }}</p>
<p>Expenses: {{ calculated.yearly.out | currency }}</p>
<p>Profit: <span :class="calculated.yearly.sum < 0 ? 'text-danger' : 'text-success'">{{ calculated.yearly.sum | currency }}</span></p>
</b-card>
<b-card header="Monthly">
<p>Income: {{ calculated.yearly.in/12 | currency }}</p>
<p>Expenses: {{ calculated.yearly.out/12 | currency }}</p>
<p>Profit: <span :class="calculated.yearly.sum/12 < 0 ? 'text-danger' : 'text-success'">{{ calculated.yearly.sum/12 | currency }}</span></p>
</b-card>
<b-card header="Daily">
<p>Income: {{ calculated.yearly.in/365 | currency }}</p>
<p>Expenses: {{ calculated.yearly.out/365 | currency }}</p>
<p>Profit: <span :class="calculated.yearly.sum/365 < 0 ? 'text-danger' : 'text-success'">{{ calculated.yearly.sum/365 | currency }}</span></p>
</b-card>
</b-card-group>
</b-card-body>
</b-card>
</b-col>
</b-row>
<b-row class="inputs">
<Input title="Income" color="success" :entries="income" />
<Input title="Expenses" color="danger" :entries="expenses" />
</b-row>
<b-modal id="modalImport" title="Import" @ok="importData" @show="resetImport" @hidden="resetImport">
<form ref="import" @submit.stop.prevent="importData">
<b-form-group
label="Data: "
label-for="data-input"
invalid-feedback="Correct data is required"
:state="importDataState"
>
<b-form-textarea
id="data-input"
v-model="importdata"
:state="importDataState"
rows="5"
no-resize
required
></b-form-textarea>
</b-form-group>
</form>
</b-modal>
</div>
</template>
<script>
import Input from './components/Input'
import Frequency from './enums/Frequency'
export default {
name: 'App',
components: {
Input
},
data () {
return {
income: [],
expenses: [],
calculated: {
yearly: {
in: 0,
out: 0,
sum: 0
},
in: {
title: 'Income',
labels: [],
data: []
},
out: {
title: 'Expenses',
labels: [],
data: []
},
inTag: {
title: 'Income Tags',
labels: [],
data: []
},
outTag: {
title: 'Expenses Tags',
labels: [],
data: []
}
},
pieStyle: {
backgroundColor: '#ffffff00',
pieOuterFontColor: '#ccc',
legendFontColor: '#ccc'
},
importdata: '',
importDataState: null
}
},
mounted () {
const storage = localStorage.getItem('expenses')
if (storage) {
try {
const storageObj = JSON.parse(storage)
if (storageObj.expenses && storageObj.income) {
this.$set(this, 'expenses', storageObj.expenses)
this.$set(this, 'income', storageObj.income)
this.income.forEach(i => this.$set(i, '_showDetails', false))
this.expenses.forEach(i => this.$set(i, '_showDetails', false))
}
} catch (e) {
}
}
this.calculate()
},
watch: {
income: {
deep: true,
handler: 'calculate'
},
expenses: {
deep: true,
handler: 'calculate'
}
},
methods: {
calculate () {
const storage = JSON.stringify({expenses: this.expenses, income: this.income})
localStorage.setItem('expenses', storage)
let calc = {
in: 0,
out: 0,
sum: 0
}
let inLabel = []
let inData = []
let outLabel = []
let outData = []
let inTagLabel = ['Without Tags']
let inTagData = [0]
let outTagLabel = ['Without Tags']
let outTagData = [0]
const inList = [...this.income.map(i => {
return {
name: i.name,
tags: i.tags,
amount: Frequency.byId(i.frequency).interval * parseFloat(i.amount)
}
}), ...this.income.filter(i => i.frequency === 0).map(i => {
return {
name: i.name,
tags: i.tags,
amount: parseFloat(i.amount)
}
})]
const outList = [...this.expenses.map(i => {
return {
name: i.name,
tags: i.tags,
amount: Frequency.byId(i.frequency).interval * parseFloat(i.amount)
}
}), ...this.expenses.filter(i => i.frequency === 0).map(i => {
return {
name: i.name,
tags: i.tags,
amount: parseFloat(i.amount)
}
})]
calc.in = inList.reduce((c, v) => c + v.amount, 0)
calc.out = outList.reduce((c, v) => c + v.amount, 0)
calc.sum = calc.in - calc.out
if (calc.sum < 0) {
inList.push({
name: 'Loss',
amount: -calc.sum,
tags: ['Loss']
})
} else {
outList.push({
name: 'Profit',
amount: calc.sum,
tags: ['Profit']
})
}
inList.forEach(e => {
inLabel.push(e.name)
inData.push(e.amount)
if (!e.tags) {
inTagData[0] += e.amount
return
}
// TODO: maybe consider using only the first tag, else the total will change because elements get included multiple times
e.tags.forEach(t => {
let index = inTagLabel.indexOf(t)
if (index === -1) {
inTagLabel.push(t)
inTagData.push(e.amount)
} else {
inTagData[index] += e.amount
}
})
})
outList.forEach(e => {
outLabel.push(e.name)
outData.push(e.amount)
if (!e.tags) {
outTagData[0] += e.amount
return
}
e.tags.forEach(t => {
let index = outTagLabel.indexOf(t)
if (index === -1) {
outTagLabel.push(t)
outTagData.push(e.amount)
} else {
outTagData[index] += e.amount
}
})
})
if (outTagData[0] === 0) {
outTagData.shift()
outTagLabel.shift()
}
if (inTagData[0] === 0) {
inTagData.shift()
inTagLabel.shift()
}
// sort asc
const sort = (label, data) => {
let merge = label.map((e, i) => {
return {
label: e,
data: data[i]
}
}).sort((a, b) => b.data - a.data)
label = merge.map(e => e.label)
data = merge.map(e => e.data)
return [label, data]
}
[inTagLabel, inTagData] = sort(inTagLabel, inTagData);
[outTagLabel, outTagData] = sort(outTagLabel, outTagData);
[inLabel, inData] = sort(inLabel, inData);
[outLabel, outData] = sort(outLabel, outData)
this.$set(this.calculated, 'yearly', calc)
this.$set(this.calculated.inTag, 'labels', inTagLabel)
this.$set(this.calculated.inTag, 'data', inTagData)
this.$set(this.calculated.outTag, 'labels', outTagLabel)
this.$set(this.calculated.outTag, 'data', outTagData)
this.$set(this.calculated.in, 'labels', inLabel)
this.$set(this.calculated.in, 'data', inData)
this.$set(this.calculated.out, 'labels', outLabel)
this.$set(this.calculated.out, 'data', outData)
},
dataFormat (a, b) {
if (b) return this.$options.filters.currency(b)
return a
},
askReset () {
// ask before deletion
this.$bvModal.msgBoxConfirm(['Please confirm that you want to reset.', 'All data will be lost forever! (A long time!)'].map(e => this.$createElement('p', [e])), {
title: 'Are you sure?',
size: 'md',
buttonSize: 'sm',
okVariant: 'danger',
okTitle: 'Yes',
cancelTitle: 'No',
footerClass: 'p-2',
hideHeaderClose: false,
centered: true
})
.then(confirmedDelete => {
if (!confirmedDelete) return
this.$set(this, 'expenses', [])
this.$set(this, 'income', [])
this.calculate()
})
},
resetImport () {
this.importdata = ''
this.importDataState = null
},
importData (bvModalEvt) {
let valid = this.$refs.import.checkValidity()
let obj
if (valid) {
try {
obj = JSON.parse(atob(this.importdata))
if (!obj.expenses || !obj.income) {
valid = false
}
} catch (e) {
valid = false
}
}
this.importDataState = valid
if (bvModalEvt) bvModalEvt.preventDefault()
if (!valid) return
this.$set(this, 'expenses', obj.expenses)
this.$set(this, 'income', obj.income)
this.income.forEach(i => this.$set(i, '_showDetails', false))
this.expenses.forEach(i => this.$set(i, '_showDetails', false))
this.calculate()
this.$nextTick(() => {
this.$bvModal.hide('modalImport')
})
},
exportData () {
const storage = localStorage.getItem('expenses')
if (!storage) return
const el = document.createElement('textarea')
el.value = btoa(storage)
el.setAttribute('readonly', '')
el.style.all = 'position: absolute; left: -9999px'
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
this.$refs.exportTooltip.$emit('open')
setTimeout(() => this.$refs.exportTooltip.$emit('close'), 1000)
}
}
}
</script>
<style>
body {
overflow-y: scroll;
}
.inputs {
margin-top: 20px;
}
/* width */
::-webkit-scrollbar {
width: 12px;
}
/* Track */
::-webkit-scrollbar-track {
background: var(--gray);
}
/* Handle */
::-webkit-scrollbar-thumb {
background: var(--dark);
border-radius: 6px 6px 6px 6px / 12px 12px 12px 12px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: #555;
}
</style>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

-225
View File
@@ -1,225 +0,0 @@
<template>
<b-col md="6">
<b-card no-body>
<b-card-header :header-bg-variant="color" header-text-variant="white" class="container-fluid">
<b-row>
<b-col md="10">
<h3 class="w-75 p-3">{{ title }}</h3>
</b-col>
<b-col md="2" class="float-right align-self-center">
<b-button @click="add()" variant="primary" size="lg"><b-icon-plus /></b-button>
</b-col>
</b-row>
</b-card-header>
<b-card-body>
<b-table :fields="fields" :items="data" responsive="sm" v-model="currentItems">
<template #cell(index)="data">
{{ data.index + 1 }}
</template>
<template #cell(options)="data">
<b-button-group size="sm">
<b-button @click="toggleDetails(data)" v-b-tooltip.hover :title="data.detailsShowing ? 'Cancel' : 'Edit'" :variant="data.detailsShowing ? 'outline-danger' : 'primary'">
<b-icon-x-square v-if="data.detailsShowing" />
<b-icon-pencil-square v-else />
</b-button>
<b-button @click="askDelete(data)" v-b-tooltip.hover title="Remove" variant="danger"><b-icon-trash /></b-button>
</b-button-group>
</template>
<template #cell()="data">
{{ data.value }}
</template>
<template #cell(amount)="data">
{{ data.item.amount | currency }}
</template>
<template #cell(frequency)="data">
{{ $enums.Frequency.byId(data.item.frequency).name }}
</template>
<template #cell(tags)="data">
{{ (data.item.tags ? data.item.tags.length : 0) }}
</template>
<template #row-details="row">
<b-card>
<b-form @submit.prevent="save(row)">
<b-form-group
id="name-group"
label="Name:"
label-for="name">
<b-form-input
id="name"
v-model="editingCache.name"
type="text"
placeholder="Name"
required></b-form-input>
</b-form-group>
<b-form-group
id="amount-group"
label="Amount:"
label-for="amount">
<b-input-group append="€" class="mb-2 mr-sm-2 mb-sm-0">
<b-form-input
id="amount"
v-model="editingCache.amount"
type="number"
placeholder="0,00"
step="0.01"
min="0.01"
required></b-form-input>
</b-input-group>
</b-form-group>
<b-form-group
id="frequency-group"
label="Frequency:"
label-for="frequency">
<b-form-select
id="frequency"
v-model="editingCache.frequency"
type="text"
:options="$enums.Frequency.options()"
required></b-form-select>
</b-form-group>
<b-form-group
id="tags-group"
description="Enter new tags separated by comma or semicolon"
label="Tags:"
label-for="tags">
<b-form-tags
id="tags"
separator=",;"
v-model="editingCache.tags"></b-form-tags>
</b-form-group>
<b-button type="submit" variant="success"><b-icon-check /> Save</b-button>
</b-form>
</b-card>
</template>
</b-table>
</b-card-body>
</b-card>
</b-col>
</template>
<script>
import Vue from 'vue'
import Frequency from '../enums/Frequency'
export default {
name: 'Input',
enums: {
Frequency
},
props: [
'title',
'color',
'entries'
],
data () {
return {
fields: [
{
key: 'index',
label: '#'
},
'name',
'amount',
'frequency',
'tags',
{
key: 'options',
label: ''
}
],
data: [],
currentItems: [],
editingCache: undefined,
editing: undefined
}
},
mounted () {
this.$set(this, 'data', this.$props.entries)
},
methods: {
add () {
this.data.push({
name: '',
amount: 1.00,
frequency: 0
})
this.toggleDetails({index: this.data.length - 1, toggleDetails: () => {}})
},
askDelete (row) {
// ask before deletion
this.$bvModal.msgBoxConfirm(`Please confirm that you want to delete '${row.item.name}'?`, {
title: 'Are you sure?',
size: 'sm',
buttonSize: 'sm',
okVariant: 'danger',
okTitle: 'Yes',
cancelTitle: 'No',
footerClass: 'p-2',
hideHeaderClose: false,
centered: true
})
.then(confirmedDelete => {
if (!confirmedDelete) return
this.data.splice(row.index, 1)
})
},
save (row) {
// override active data with from cache
this.$set(this.data, row.index, this.editingCache)
this.toggleDetails(row)
},
toggleDetails (row) {
// close all open details
this.currentItems.forEach((item, i) => {
if (i === row.index) return
this.$set(item, '_showDetails', false)
})
// create (or reset) local copy for form data
if (row.detailsShowing) {
this.editingCache = undefined
} else {
this.editingCache = Vue.util.extend({}, row.item)
}
row.toggleDetails()
}
},
watch: {
data: {
deep: true,
handler () {
this.$emit('input', this.data)
}
},
entries: {
deep: true,
handler () {
this.$set(this, 'data', this.$props.entries)
}
}
}
}
</script>
<style>
/* highlight selected row */
.b-table-has-details {
background: var(--gray);
color: var(--white);
}
.b-table-has-details + tr {
background: var(--gray);
}
</style>
-36
View File
@@ -1,36 +0,0 @@
class Frequency {
constructor (name, interval) {
this.name = name
this.interval = interval
}
}
Frequency.ONCE = new Frequency('Once', 0)
Frequency.YEARLY = new Frequency('Yearly', 1)
Frequency.BIMONTHLY = new Frequency('Bi-Monthly', 6)
Frequency.MONTHLY = new Frequency('Monthly', 12)
Frequency.BIWEEKLY = new Frequency('Bi-Weekly', 26)
Frequency.WEEKLY = new Frequency('Weekly', 52)
Frequency.DAILY = new Frequency('Daily', 52 * 7)
Frequency.byId = id => {
try { return Frequency[Object.keys(Frequency)[id]] } catch (e) { return Frequency.ONCE }
}
Frequency.values = () => {
let keys = Object.keys(Frequency)
keys.splice(-3, 3)
return keys.map(k => Frequency[k])
}
Frequency.options = () => {
return Frequency.values().map((f, i) => {
return {
value: i,
text: f.name
}
})
}
export default Frequency
-42
View File
@@ -1,42 +0,0 @@
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import enums from 'vue-enums'
import VueGraph from 'vue-graph'
import App from './App'
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import 'bootstrap/dist/js/bootstrap'
import 'bootswatch/dist/darkly/bootstrap.min.css'
// import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
Vue.use(VueGraph)
Vue.use(enums, {namespace: '$enums'})
// Make BootstrapVue available throughout your project
Vue.use(BootstrapVue)
// Optionally install the BootstrapVue icon components plugin
Vue.use(IconsPlugin)
Vue.filter('currency', function (value) {
if (typeof value !== 'number') {
try {
value = parseFloat(value)
} catch (e) {
return value
}
}
return `${value.toFixed(2)}`
})
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
components: { App },
template: '<App/>'
})
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
!function(e){var n=window.webpackJsonp;window.webpackJsonp=function(r,c,a){for(var i,u,f,s=0,l=[];s<r.length;s++)u=r[s],t[u]&&l.push(t[u][0]),t[u]=0;for(i in c)Object.prototype.hasOwnProperty.call(c,i)&&(e[i]=c[i]);for(n&&n(r,c,a);l.length;)l.shift()();if(a)for(s=0;s<a.length;s++)f=o(o.s=a[s]);return f};var r={},t={2:0};function o(n){if(r[n])return r[n].exports;var t=r[n]={i:n,l:!1,exports:{}};return e[n].call(t.exports,t,t.exports,o),t.l=!0,t.exports}o.e=function(e){var n=t[e];if(0===n)return new Promise(function(e){e()});if(n)return n[2];var r=new Promise(function(r,o){n=t[e]=[r,o]});n[2]=r;var c=document.getElementsByTagName("head")[0],a=document.createElement("script");a.type="text/javascript",a.charset="utf-8",a.async=!0,a.timeout=12e4,o.nc&&a.setAttribute("nonce",o.nc),a.src=o.p+"static/js/"+e+"."+{0:"24cf67262cb4a0d61451",1:"e99c1f67565345fb7d54"}[e]+".js";var i=setTimeout(u,12e4);function u(){a.onerror=a.onload=null,clearTimeout(i);var n=t[e];0!==n&&(n&&n[1](new Error("Loading chunk "+e+" failed.")),t[e]=void 0)}return a.onerror=a.onload=u,c.appendChild(a),r},o.m=e,o.c=r,o.d=function(e,n,r){o.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},o.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(n,"a",n),n},o.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},o.p="/",o.oe=function(e){throw console.error(e),e}}([]);
//# sourceMappingURL=manifest.6e32a8920e40b3fc2d09.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-7
View File
@@ -1,7 +0,0 @@
{
"env": {
"jest": true
},
"globals": {
}
}
-26
View File
@@ -1,26 +0,0 @@
const path = require('path')
module.exports = {
rootDir: path.resolve(__dirname, '../../'),
moduleFileExtensions: [
'js',
'json',
'vue'
],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
transform: {
'^.+\\.js$': '<rootDir>/node_modules/babel-jest',
'.*\\.(vue)$': '<rootDir>/node_modules/vue-jest'
},
snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'],
setupFiles: ['<rootDir>/test/unit/setup'],
mapCoverage: true,
coverageDirectory: '<rootDir>/test/unit/coverage',
collectCoverageFrom: [
'src/**/*.{js,vue}',
'!src/main.js',
'!**/node_modules/**'
]
}
-3
View File
@@ -1,3 +0,0 @@
import Vue from 'vue'
Vue.config.productionTip = false
-11
View File
@@ -1,11 +0,0 @@
import Vue from 'vue'
import HelloWorld from '@/components/HelloWorld'
describe('HelloWorld.vue', () => {
it('should render correct contents', () => {
const Constructor = Vue.extend(HelloWorld)
const vm = new Constructor().$mount()
expect(vm.$el.querySelector('.hello h1').textContent)
.toEqual('Welcome to Your Vue.js App')
})
})