├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .npmrc ├── .nvmrc ├── .postcssrc.js ├── .travis.yml ├── README.md ├── build ├── build.js ├── check-versions.js ├── dev-client.js ├── dev-server.js ├── load-minified.js ├── service-worker-dev.js ├── service-worker-prod.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-lock.json ├── package.json ├── src ├── App.vue ├── assets │ ├── images │ │ └── logo.png │ └── styles │ │ ├── _animations.less │ │ ├── _variables.less │ │ └── app.less ├── components │ ├── Counter.vue │ ├── Hello.vue │ ├── Router.vue │ ├── RouterChild.vue │ └── Store.vue ├── main.js ├── router │ └── index.js └── store │ ├── createState.js │ ├── index.js │ ├── mutations.js │ └── persistedState.js ├── static ├── img │ └── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── msapplication-icon-144x144.png │ │ ├── mstile-150x150.png │ │ └── safari-pinned-tab.svg └── manifest.json └── test ├── e2e ├── custom-assertions │ └── elementCount.js ├── nightwatch.conf.js ├── runner.js └── specs │ └── test.js └── unit ├── .eslintrc ├── index.js ├── karma.conf.js └── specs ├── components ├── App.spec.js ├── Counter.spec.js ├── Hello.spec.js └── RouterChild.spec.js └── store ├── createState.spec.js ├── mutations.spec.js └── persistedState.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions"] 7 | } 8 | }] 9 | ], 10 | "plugins": ["transform-runtime", "transform-object-rest-spread", "syntax-dynamic-import"], 11 | "env": { 12 | "test": { 13 | "presets": ["env"], 14 | "plugins": ["istanbul"] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.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 | dist/ 2 | -------------------------------------------------------------------------------- /.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 | extends: 'airbnb-base', 13 | // required to lint *.vue files 14 | plugins: [ 15 | 'html' 16 | ], 17 | // check if imports actually resolve 18 | settings: { 19 | 'import/resolver': { 20 | webpack: { 21 | config: 'build/webpack.base.conf.js' 22 | } 23 | } 24 | }, 25 | // add your custom rules here 26 | rules: { 27 | // don't require .vue extension when importing 28 | 'import/extensions': ['error', 'always', { 29 | 'js': 'never', 30 | 'vue': 'never' 31 | }], 32 | // allow optionalDependencies 33 | 'import/no-extraneous-dependencies': ['error', { 34 | 'optionalDependencies': ['test/unit/index.js'] 35 | }], 36 | // allow debugger during development 37 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 38 | 'max-len': ['error', 120], 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Force LF on javascript, vue, and json files. 5 | *.js text eol=lf 6 | *.vue text eol=lf 7 | *.json text eol=lf 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | test/unit/coverage 8 | test/e2e/reports 9 | selenium-debug.log 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/carbon 2 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: false 3 | addons: 4 | chrome: stable 5 | language: node_js 6 | node_js: 7 | - "6" 8 | - "8" 9 | - "10" 10 | script: "npm run unit" 11 | after_script: "npm install codecov.io && cat ./test/unit/coverage/lcov.info | codecov" 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Dependency Status][deps-image]][deps-url] 2 | [![Development Dependency Status][deps-dev-image]][deps-dev-url] 3 | [![Build Status][ci-image]][ci-url] 4 | [![Code Coverage status][codecov-image]][codecov-url] 5 | 6 | # vuejs-starterkit 7 | 8 | An opionated Vue.js PWA starter-kit project integrating `vue-router`, `vuex`, `vue-loader` and `webpack3` for non-trivial projects. 9 | 10 | ## Build Setup 11 | 12 | ``` bash 13 | # install dependencies 14 | npm install 15 | 16 | # serve with hot reload at localhost:8080 17 | npm run dev 18 | 19 | # build for production with minification 20 | npm run build 21 | 22 | # build for production and view the bundle analyzer report 23 | npm run build --report 24 | 25 | # run unit tests 26 | npm run unit 27 | 28 | # run e2e tests 29 | npm run e2e 30 | 31 | # run all tests 32 | npm test 33 | ``` 34 | 35 | You can provide custom port-numbers via the environment variable `PORT`: 36 | 37 | ```sh 38 | # run dev-server on port 1337 39 | PORT=1337 node run dev 40 | 41 | # run e2e tests on port 8888 42 | PORT=8888 node run e2e 43 | ``` 44 | 45 | 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). 46 | 47 | ## Demo 48 | 49 | Go to the [vuejs-starterkit-demo](https://macedigital.github.io/vuejs-starterkit/) demo page. 50 | 51 | ## Roadmap 52 | 53 | - [x] Fix aliases path-resolution issues with webpack. 54 | - [x] Fix issues with [vue-loader](https://vue-loader.vuejs.org/) webpack resolvers. 55 | - [ ] Add useful example components from [vue guide](https://vuejs.org/v2/guide/). 56 | - [x] Add more sophisticated [vue-router](https://router.vuejs.org/) examples. 57 | - [x] Add [vuex](https://vuex.vuejs.org/) integration for state-management. 58 | - [ ] Add first-class [Typescript support](https://www.typescriptlang.org/). 59 | - [ ] Revisit unit- and e2e-testing strategy, plenty of frameworks and styles to choose from. 60 | 61 | [deps-image]:https://img.shields.io/david/macedigital/vuejs-starterkit.svg?style=flat 62 | [deps-url]:https://david-dm.org/macedigital/vuejs-starterkit 63 | [deps-dev-image]:https://img.shields.io/david/dev/macedigital/vuejs-starterkit.svg?style=flat 64 | [deps-dev-url]:https://david-dm.org/macedigital/vuejs-starterkit?type=dev 65 | [ci-image]: https://img.shields.io/travis/macedigital/vuejs-starterkit.svg?style=flat 66 | [ci-url]: https://travis-ci.org/macedigital/vuejs-starterkit 67 | [codecov-image]:https://img.shields.io/codecov/c/github/macedigital/vuejs-starterkit.svg?style=flat 68 | [codecov-url]:https://codecov.io/github/macedigital/vuejs-starterkit 69 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')(); 2 | 3 | process.env.NODE_ENV = 'production'; 4 | 5 | const ora = require('ora'); 6 | const rm = require('rimraf'); 7 | const path = require('path'); 8 | const chalk = require('chalk'); 9 | const webpack = require('webpack'); 10 | const config = require('../config'); 11 | const webpackConfig = require('./webpack.prod.conf'); 12 | 13 | const spinner = ora('building for production...'); 14 | spinner.start(); 15 | 16 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), (err) => { 17 | if (err) throw err; 18 | webpack(webpackConfig, (error, stats) => { 19 | spinner.stop(); 20 | if (error) throw error; 21 | process.stdout.write(`${stats.toString({ 22 | colors: true, 23 | modules: false, 24 | children: false, 25 | chunks: false, 26 | chunkModules: false, 27 | })}\n\n`); 28 | // eslint-disable-next-line no-console 29 | console.log(chalk.cyan(' Build complete.\n')); 30 | // eslint-disable-next-line no-console 31 | console.log(chalk.yellow(` 32 | Tip: built files are meant to be served over an HTTP server. 33 | Opening index.html over file:// won't work. 34 | `)); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const semver = require('semver'); 3 | const packageConfig = require('../package.json'); 4 | const shell = require('shelljs'); 5 | const { execSync } = require('child_process'); 6 | 7 | const exec = cmd => execSync(cmd).toString().trim(); 8 | 9 | const versionRequirements = [ 10 | { 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node, 14 | }, 15 | ]; 16 | 17 | if (shell.which('npm')) { 18 | versionRequirements.push({ 19 | name: 'npm', 20 | currentVersion: exec('npm --version'), 21 | versionRequirement: packageConfig.engines.npm, 22 | }); 23 | } 24 | 25 | module.exports = () => { 26 | const warnings = []; 27 | versionRequirements.forEach((mod) => { 28 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 29 | warnings.push(`${mod.name}: ${ 30 | chalk.red(mod.currentVersion)} should be ${ 31 | chalk.green(mod.versionRequirement)}`); 32 | } 33 | }); 34 | 35 | if (warnings.length) { 36 | /* eslint-disable no-console */ 37 | console.log(`\n${chalk.yellow('To use this template, you must update following to modules:')}\n`); 38 | warnings.forEach(warning => console.log(` ${warning}`)); 39 | console.log(); 40 | process.exit(1); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /build/dev-client.js: -------------------------------------------------------------------------------- 1 | require('eventsource-polyfill'); 2 | const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true'); 3 | 4 | hotClient.subscribe((event) => { 5 | if (event.action === 'reload') { 6 | window.location.reload(); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')(); 2 | 3 | const config = require('../config'); 4 | 5 | if (!process.env.NODE_ENV) { 6 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV); 7 | } 8 | 9 | const opn = require('opn'); 10 | const path = require('path'); 11 | const express = require('express'); 12 | const webpack = require('webpack'); 13 | const proxyMiddleware = require('http-proxy-middleware'); 14 | const webpackConfig = process.env.NODE_ENV === 'testing' 15 | ? require('./webpack.prod.conf') 16 | : require('./webpack.dev.conf'); 17 | 18 | // default port where dev server listens for incoming traffic 19 | const port = process.env.PORT || config.dev.port; 20 | // automatically open browser, if not set will be false 21 | const autoOpenBrowser = !!config.dev.autoOpenBrowser; 22 | // Define HTTP proxies to your custom API backend 23 | // https://github.com/chimurai/http-proxy-middleware 24 | const { proxyTable } = config.dev; 25 | 26 | const app = express(); 27 | const compiler = webpack(webpackConfig); 28 | 29 | const devMiddleware = require('webpack-dev-middleware')(compiler, { 30 | publicPath: webpackConfig.output.publicPath, 31 | stats: 'errors-only', 32 | }); 33 | 34 | const hotMiddleware = require('webpack-hot-middleware')(compiler, { 35 | log: false, 36 | }); 37 | // force page reload when html-webpack-plugin template changes 38 | compiler.hooks.compilation.tap('html-webpack-plugin-after-emit', () => { 39 | hotMiddleware.publish({ action: 'reload' }); 40 | }); 41 | 42 | // proxy api requests 43 | Object.keys(proxyTable).forEach((context) => { 44 | let options = proxyTable[context]; 45 | if (typeof options === 'string') { 46 | options = { target: options }; 47 | } 48 | app.use(proxyMiddleware(options.filter || context, options)); 49 | }); 50 | 51 | // handle fallback for HTML5 history API 52 | app.use(require('connect-history-api-fallback')()); 53 | 54 | // serve webpack bundle output 55 | app.use(devMiddleware); 56 | 57 | // enable hot-reload and state-preserving 58 | // compilation error display 59 | app.use(hotMiddleware); 60 | 61 | // serve pure static assets 62 | const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory); 63 | app.use(staticPath, express.static('./static')); 64 | 65 | const uri = `http://localhost:${port}`; 66 | 67 | // eslint-disable-next-line no-underscore-dangle 68 | let _resolve; 69 | const readyPromise = new Promise((resolve) => { 70 | _resolve = resolve; 71 | }); 72 | 73 | // eslint-disable-next-line no-console 74 | console.log('> Starting dev server...'); 75 | devMiddleware.waitUntilValid(() => { 76 | // eslint-disable-next-line no-console 77 | console.log(`> Listening at ${uri}\n`); 78 | // when env is testing, don't need open it 79 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 80 | opn(uri); 81 | } 82 | _resolve(); 83 | }); 84 | 85 | const server = app.listen(port); 86 | 87 | module.exports = { 88 | ready: readyPromise, 89 | close: () => { 90 | server.close(); 91 | }, 92 | }; 93 | -------------------------------------------------------------------------------- /build/load-minified.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const UglifyJS = require('uglify-es'); 3 | 4 | module.exports = (filePath) => { 5 | const code = fs.readFileSync(filePath, 'utf-8'); 6 | const result = UglifyJS.minify(code); 7 | if (result.error) { 8 | return ''; 9 | } 10 | return result.code; 11 | }; 12 | -------------------------------------------------------------------------------- /build/service-worker-dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | // This service worker file is effectively a 'no-op' that will reset any 4 | // previous service worker registered for the same host:port combination. 5 | // In the production build, this file is replaced with an actual service worker 6 | // file that will precache your site's local assets. 7 | // See https://github.com/facebookincubator/create-react-app/issues/2272#issuecomment-302832432 8 | 9 | self.addEventListener('install', () => self.skipWaiting()); 10 | 11 | self.addEventListener('activate', () => { 12 | self.clients.matchAll({ type: 'window' }).then(windowClients => { 13 | for (let windowClient of windowClients) { 14 | // Force open pages to refresh, so that they have a chance to load the 15 | // fresh navigation response from the local dev server. 16 | windowClient.navigate(windowClient.url); 17 | } 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /build/service-worker-prod.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | (function() { 3 | 'use strict'; 4 | 5 | // Check to make sure service workers are supported in the current browser, 6 | // and that the current page is accessed from a secure origin. Using a 7 | // service worker from an insecure origin will trigger JS console errors. 8 | const isLocalhost = Boolean(window.location.hostname === 'localhost' || 9 | // [::1] is the IPv6 localhost address. 10 | window.location.hostname === '[::1]' || 11 | // 127.0.0.1/8 is considered localhost for IPv4. 12 | window.location.hostname.match( 13 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 14 | ) 15 | ); 16 | 17 | window.addEventListener('load', function() { 18 | if ('serviceWorker' in navigator && 19 | (window.location.protocol === 'https:' || isLocalhost)) { 20 | navigator.serviceWorker.register('service-worker.js') 21 | .then(function(registration) { 22 | // updatefound is fired if service-worker.js changes. 23 | registration.onupdatefound = function() { 24 | // updatefound is also fired the very first time the SW is installed, 25 | // and there's no need to prompt for a reload at that point. 26 | // So check here to see if the page is already controlled, 27 | // i.e. whether there's an existing service worker. 28 | if (navigator.serviceWorker.controller) { 29 | // The updatefound event implies that registration.installing is set 30 | const installingWorker = registration.installing; 31 | 32 | installingWorker.onstatechange = function() { 33 | switch (installingWorker.state) { 34 | case 'installed': 35 | // At this point, the old content will have been purged and the 36 | // fresh content will have been added to the cache. 37 | // It's the perfect time to display a "New content is 38 | // available; please refresh." message in the page's interface. 39 | break; 40 | 41 | case 'redundant': 42 | throw new Error('The installing ' + 43 | 'service worker became redundant.'); 44 | 45 | default: 46 | // Ignore 47 | } 48 | }; 49 | } 50 | }; 51 | }).catch(function(e) { 52 | console.error('Error during service worker registration:', e); 53 | }); 54 | } 55 | }); 56 | })(); 57 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const config = require('../config'); 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | 5 | exports.assetsPath = (_path) => { 6 | const 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 = (options) => { 13 | const opts = Object.assign({}, options); 14 | 15 | const cssLoader = { 16 | loader: 'css-loader', 17 | options: { 18 | minimize: process.env.NODE_ENV === 'production', 19 | sourceMap: opts.sourceMap, 20 | }, 21 | }; 22 | 23 | // generate loader string to be used with extract text plugin 24 | const generateLoaders = (loader, loaderOptions) => { 25 | const loaders = [cssLoader]; 26 | if (loader) { 27 | loaders.push({ 28 | loader: `${loader}-loader`, 29 | options: Object.assign({}, loaderOptions, { 30 | sourceMap: opts.sourceMap, 31 | }), 32 | }); 33 | } 34 | 35 | // Extract CSS when that option is specified 36 | // (which is the case during production build) 37 | if (opts.extract) { 38 | return ExtractTextPlugin.extract({ 39 | use: loaders, 40 | fallback: 'vue-style-loader', 41 | }); 42 | } 43 | return ['vue-style-loader'].concat(loaders); 44 | }; 45 | 46 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 47 | return { 48 | css: generateLoaders(), 49 | postcss: generateLoaders(), 50 | less: generateLoaders('less', { strictMath: true, strictUnits: true }), 51 | sass: generateLoaders('sass', { indentedSyntax: true }), 52 | scss: generateLoaders('sass'), 53 | stylus: generateLoaders('stylus'), 54 | styl: generateLoaders('stylus'), 55 | }; 56 | }; 57 | 58 | // Generate loaders for standalone style files (outside of .vue) 59 | exports.styleLoaders = (options) => { 60 | const output = []; 61 | const loaders = exports.cssLoaders(options); 62 | Object.keys(loaders).forEach((extension) => { 63 | const loader = loaders[extension]; 64 | output.push({ 65 | test: new RegExp(`\\.${extension}$`), 66 | use: loader, 67 | }); 68 | }); 69 | 70 | return output; 71 | }; 72 | 73 | /** 74 | * Rewrite paths in `manifest.json` files 75 | * @link https://developer.mozilla.org/en-US/docs/Web/Manifest 76 | * @param {Buffer} content 77 | * @param {String} filepath 78 | * @returns {Buffer} 79 | */ 80 | exports.updateManifest = (content, filepath) => { 81 | if (!filepath.match(/manifest\.json$/)) { 82 | return content; 83 | } 84 | 85 | const routePrefix = JSON.parse(config.build.env.ROUTER_PREFIX); 86 | const assetPrefix = config.build.assetsPublicPath; 87 | const addRoutePrefix = (route, prefix) => path.normalize(`${route.replace(/^\//, `${prefix}/`)}`); 88 | const iconPrefix = icon => addRoutePrefix(icon.src, assetPrefix); 89 | const json = JSON.parse(content); 90 | 91 | json.start_url = addRoutePrefix(json.start_url, routePrefix); 92 | json.scope = path.normalize(`${routePrefix}/`); 93 | json.icons = json.icons.map(icon => Object.assign(icon, { src: iconPrefix(icon) })); 94 | 95 | return Buffer.from(JSON.stringify(json), 'utf-8'); 96 | }; 97 | -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | const utils = require('./utils'); 2 | const config = require('../config'); 3 | 4 | const isProduction = process.env.NODE_ENV === 'production'; 5 | 6 | module.exports = { 7 | loaders: utils.cssLoaders({ 8 | sourceMap: isProduction 9 | ? config.build.productionSourceMap 10 | : config.dev.cssSourceMap, 11 | extract: isProduction, 12 | }), 13 | transformToRequire: { 14 | video: 'src', 15 | source: 'src', 16 | img: 'src', 17 | image: 'xlink:href', 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const friendlyFormatter = require('eslint-friendly-formatter'); 3 | const { VueLoaderPlugin } = require('vue-loader'); 4 | 5 | const utils = require('./utils'); 6 | const config = require('../config'); 7 | const vueLoaderConfig = require('./vue-loader.conf'); 8 | 9 | const resolve = dir => path.join(__dirname, '..', dir); 10 | 11 | module.exports = { 12 | entry: { 13 | app: './src/main.js', 14 | }, 15 | output: { 16 | path: config.build.assetsRoot, 17 | filename: '[name].js', 18 | publicPath: process.env.NODE_ENV === 'production' 19 | ? config.build.assetsPublicPath 20 | : config.dev.assetsPublicPath, 21 | }, 22 | resolve: { 23 | extensions: ['.js', '.vue', '.json'], 24 | alias: { 25 | '@': resolve('src'), 26 | }, 27 | }, 28 | stats: 'minimal', 29 | plugins: [ 30 | new VueLoaderPlugin(), 31 | ], 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.(js|vue)$/, 36 | loader: 'eslint-loader', 37 | enforce: 'pre', 38 | include: [resolve('src'), resolve('test')], 39 | options: { 40 | formatter: friendlyFormatter, 41 | }, 42 | }, 43 | { 44 | test: /\.vue$/, 45 | loader: 'vue-loader', 46 | options: vueLoaderConfig, 47 | }, 48 | { 49 | test: /\.js$/, 50 | loader: 'babel-loader', 51 | include: [resolve('src'), resolve('test')], 52 | }, 53 | { 54 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 55 | loader: 'url-loader', 56 | options: { 57 | limit: 4096, 58 | name: utils.assetsPath('img/[name].[hash:7].[ext]'), 59 | }, 60 | }, 61 | { 62 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 63 | loader: 'url-loader', 64 | options: { 65 | name: utils.assetsPath('media/[name].[hash:7].[ext]'), 66 | }, 67 | }, 68 | { 69 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 70 | loader: 'url-loader', 71 | options: { 72 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]'), 73 | }, 74 | }, 75 | ], 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const utils = require('./utils'); 4 | const webpack = require('webpack'); 5 | const config = require('../config'); 6 | const merge = require('webpack-merge'); 7 | const baseWebpackConfig = require('./webpack.base.conf'); 8 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 9 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin'); 10 | 11 | // add hot-reload related code to entry chunks 12 | Object.keys(baseWebpackConfig.entry).forEach((name) => { 13 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]); 14 | }); 15 | 16 | module.exports = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }), 19 | }, 20 | mode: 'development', 21 | optimization: { 22 | noEmitOnErrors: true, 23 | }, 24 | // fast incremental rebuilds 25 | // @see https://webpack.js.org/configuration/devtool/ for a comparison 26 | devtool: '#cheap-module-eval-source-map', 27 | plugins: [ 28 | new webpack.DefinePlugin({ 29 | 'process.env': config.dev.env, 30 | }), 31 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 32 | new webpack.HotModuleReplacementPlugin(), 33 | // https://github.com/ampedandwired/html-webpack-plugin 34 | new HtmlWebpackPlugin({ 35 | title: 'A Vuejs Starterkit', 36 | filename: 'index.html', 37 | template: 'index.html', 38 | inject: true, 39 | // eslint-disable-next-line max-len 40 | serviceWorkerLoader: ``, 41 | }), 42 | new FriendlyErrorsPlugin(), 43 | ], 44 | }); 45 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const utils = require('./utils'); 3 | const webpack = require('webpack'); 4 | const config = require('../config'); 5 | const merge = require('webpack-merge'); 6 | const baseWebpackConfig = require('./webpack.base.conf'); 7 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 8 | const CompressionWebpackPlugin = require('compression-webpack-plugin'); 9 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 10 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 11 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 12 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin'); 13 | const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin'); 14 | const loadMinified = require('./load-minified'); 15 | 16 | const env = process.env.NODE_ENV === 'testing' 17 | ? require('../config/test.env') 18 | : config.build.env; 19 | 20 | const webpackConfig = merge(baseWebpackConfig, { 21 | module: { 22 | rules: utils.styleLoaders({ 23 | sourceMap: config.build.productionSourceMap, 24 | extract: true, 25 | }), 26 | }, 27 | mode: 'production', 28 | devtool: config.build.productionSourceMap ? '#source-map' : false, 29 | output: { 30 | path: config.build.assetsRoot, 31 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 32 | publicPath: config.build.assetsPublicPath, 33 | crossOriginLoading: 'anonymous', 34 | }, 35 | optimization: { 36 | removeAvailableModules: true, 37 | removeEmptyChunks: true, 38 | mergeDuplicateChunks: true, 39 | flagIncludedChunks: true, 40 | occurrenceOrder: true, 41 | sideEffects: true, 42 | providedExports: true, 43 | usedExports: true, 44 | concatenateModules: true, 45 | splitChunks: { 46 | cacheGroups: { 47 | vendors: { 48 | test: /[\\/]node_modules[\\/]/, 49 | name: 'vendors', 50 | priority: -20, 51 | chunks: 'all', 52 | }, 53 | }, 54 | }, 55 | // chunk for the webpack runtime code and chunk manifest 56 | runtimeChunk: { 57 | name: 'manifest', 58 | }, 59 | noEmitOnErrors: true, 60 | minimize: true, 61 | namedModules: true, 62 | namedChunks: true, 63 | }, 64 | plugins: [ 65 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 66 | new webpack.DefinePlugin({ 67 | 'process.env': env, 68 | }), 69 | new ExtractTextPlugin({ 70 | filename: utils.assetsPath('css/[name].[hash].css'), 71 | }), 72 | // Compress extracted CSS. We are using this plugin so that possible 73 | // duplicated CSS from different components can be deduped. 74 | new OptimizeCSSPlugin({ 75 | cssProcessorOptions: { 76 | safe: true, 77 | }, 78 | }), 79 | // generate dist index.html with correct asset hash for caching. 80 | // you can customize output by editing /index.html 81 | // see https://github.com/ampedandwired/html-webpack-plugin 82 | new HtmlWebpackPlugin({ 83 | title: 'A Vuejs Starterkit', 84 | filename: process.env.NODE_ENV === 'testing' 85 | ? 'index.html' 86 | : config.build.index, 87 | template: 'index.html', 88 | inject: true, 89 | minify: { 90 | removeComments: true, 91 | collapseWhitespace: true, 92 | removeAttributeQuotes: true, 93 | // more options: 94 | // https://github.com/kangax/html-minifier#options-quick-reference 95 | }, 96 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 97 | chunksSortMode: 'dependency', 98 | serviceWorkerLoader: ``, 99 | }), 100 | // copy custom static assets 101 | new CopyWebpackPlugin([ 102 | { 103 | from: path.resolve(__dirname, '../static'), 104 | to: config.build.assetsSubDirectory, 105 | ignore: ['.*'], 106 | transform: (content, filepath) => utils.updateManifest(content, filepath), 107 | }, 108 | ]), 109 | // service worker caching 110 | new SWPrecacheWebpackPlugin({ 111 | cacheId: 'my-vue-app', 112 | filename: 'service-worker.js', 113 | staticFileGlobs: ['dist/**/*.{js,html,css}'], 114 | minify: true, 115 | stripPrefix: 'dist/', 116 | }), 117 | ], 118 | }); 119 | 120 | if (config.build.productionGzip) { 121 | webpackConfig.plugins.push(new CompressionWebpackPlugin({ 122 | asset: '[path].gz[query]', 123 | algorithm: 'gzip', 124 | test: new RegExp(`\\.(${config.build.productionGzipExtensions.join('|')})$`), 125 | threshold: 10240, 126 | minRatio: 0.8, 127 | })); 128 | } 129 | 130 | if (config.build.bundleAnalyzerReport) { 131 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()); 132 | } 133 | 134 | module.exports = webpackConfig; 135 | -------------------------------------------------------------------------------- /build/webpack.test.conf.js: -------------------------------------------------------------------------------- 1 | // This is the webpack config used for unit tests. 2 | 3 | const utils = require('./utils'); 4 | const webpack = require('webpack'); 5 | const merge = require('webpack-merge'); 6 | const baseConfig = require('./webpack.base.conf'); 7 | const testEnv = require('../config/test.env'); 8 | 9 | const webpackConfig = merge(baseConfig, { 10 | // use inline sourcemap for karma-sourcemap-loader 11 | module: { 12 | rules: utils.styleLoaders(), 13 | }, 14 | mode: 'production', 15 | devtool: '#inline-source-map', 16 | resolveLoader: { 17 | alias: { 18 | // necessary to to make lang="scss" work in test when using vue-loader's ?inject option 19 | // see discussion at https://github.com/vuejs/vue-loader/issues/724 20 | 'scss-loader': 'sass-loader', 21 | }, 22 | }, 23 | plugins: [ 24 | new webpack.DefinePlugin({ 25 | 'process.env': testEnv, 26 | }), 27 | ], 28 | }); 29 | 30 | // no need for app entry during tests 31 | delete webpackConfig.entry; 32 | 33 | module.exports = webpackConfig; 34 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const prodEnv = require('./prod.env'); 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"', 6 | ROUTER_PREFIX: '"/"', 7 | }); 8 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | const path = require('path'); 3 | 4 | const prodEnv = require('./prod.env'); 5 | const devEnv = require('./dev.env'); 6 | 7 | module.exports = { 8 | build: { 9 | env: prodEnv, 10 | index: path.resolve(__dirname, '../dist/index.html'), 11 | assetsRoot: path.resolve(__dirname, '../dist'), 12 | assetsSubDirectory: 'static', 13 | assetsPublicPath: '/', 14 | productionSourceMap: true, 15 | // Gzip off by default as many popular static hosts such as 16 | // Surge or Netlify already gzip all static assets for you. 17 | // Before setting to `true`, make sure to: 18 | // npm install --save-dev compression-webpack-plugin 19 | productionGzip: false, 20 | productionGzipExtensions: ['js', 'css', 'html'], 21 | // Run the build command with an extra argument to 22 | // View the bundle analyzer report after build finishes: 23 | // `npm run build --report` 24 | // Set to `true` or `false` to always turn it on or off 25 | bundleAnalyzerReport: process.env.npm_config_report, 26 | }, 27 | dev: { 28 | env: devEnv, 29 | port: process.env.PORT || 8080, 30 | autoOpenBrowser: true, 31 | assetsSubDirectory: 'static', 32 | assetsPublicPath: '/', 33 | proxyTable: {}, 34 | // CSS Sourcemaps off by default because relative paths are "buggy" 35 | // with this option, according to the CSS-Loader README 36 | // (https://github.com/webpack/css-loader#sourcemaps) 37 | // In our experience, they generally work as expected, 38 | // just be aware of this issue when enabling this option. 39 | cssSourceMap: false, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"', 3 | ROUTER_PREFIX: '"/vuejs-starterkit"', 4 | }; 5 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const devEnv = require('./dev.env'); 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"testing"', 6 | }); 7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= htmlWebpackPlugin.options.title %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | <% for (var chunk of webpack.chunks) { 25 | for (var file of chunk.files) { 26 | if (file.match(/\.(js|css)$/)) { %> 27 | <% }}} %> 28 | 29 | 30 |
31 | 34 | 35 | <%= htmlWebpackPlugin.options.serviceWorkerLoader %> 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuejs-starterkit", 3 | "version": "1.0.0", 4 | "description": "A Vue.js starter-kit project", 5 | "keywords": [ 6 | "vue", 7 | "vuex", 8 | "vue-loader", 9 | "vue-router", 10 | "webpack" 11 | ], 12 | "author": "Matthias Adler", 13 | "private": true, 14 | "license": "MIT", 15 | "scripts": { 16 | "prepush": "npm run eslint", 17 | "dev": "node build/dev-server.js", 18 | "start": "npm run dev", 19 | "build": "node build/build.js", 20 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", 21 | "e2e": "cross-env PORT=8081 node test/e2e/runner.js", 22 | "test": "npm run unit && npm run e2e", 23 | "eslint": "eslint --ext .js,.vue build config src test/unit/specs test/e2e/specs" 24 | }, 25 | "dependencies": { 26 | "js-cookie": "^2.2.0", 27 | "vue": "^2.5.16", 28 | "vue-router": "^3.0.1", 29 | "vuex": "^3.0.0", 30 | "vuex-persistedstate": "^2.5.3", 31 | "vuex-router-sync": "^5.0.0" 32 | }, 33 | "devDependencies": { 34 | "autoprefixer": "^8.5.0", 35 | "avoriaz": "^6.3.0", 36 | "babel-core": "^6.26.3", 37 | "babel-eslint": "^8.2.3", 38 | "babel-loader": "^7.1.4", 39 | "babel-plugin-istanbul": "^4.1.6", 40 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 41 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 42 | "babel-plugin-transform-runtime": "^6.22.0", 43 | "babel-preset-env": "^1.7.0", 44 | "babel-register": "^6.26.0", 45 | "babel-runtime": "^6.26.0", 46 | "chai": "^4.1.2", 47 | "chalk": "^2.4.1", 48 | "chromedriver": "^2.38.3", 49 | "compression-webpack-plugin": "^1.1.11", 50 | "connect-history-api-fallback": "^1.5.0", 51 | "copy-webpack-plugin": "^4.5.1", 52 | "cross-env": "^5.1.5", 53 | "cross-spawn": "^6.0.5", 54 | "css-loader": "^0.28.11", 55 | "cssnano": "^3.10.0", 56 | "eslint": "^4.19.1", 57 | "eslint-config-airbnb-base": "^12.1.0", 58 | "eslint-friendly-formatter": "^4.0.1", 59 | "eslint-import-resolver-webpack": "^0.9.0", 60 | "eslint-loader": "^2.0.0", 61 | "eslint-plugin-html": "^4.0.3", 62 | "eslint-plugin-import": "^2.11.0", 63 | "eventsource-polyfill": "^0.9.6", 64 | "express": "^4.16.3", 65 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 66 | "file-loader": "^1.1.11", 67 | "friendly-errors-webpack-plugin": "^1.7.0", 68 | "html-webpack-plugin": "^3.2.0", 69 | "http-proxy-middleware": "^0.18.0", 70 | "husky": "^0.14.3", 71 | "inject-loader": "^4.0.1", 72 | "karma": "^2.0.2", 73 | "karma-chrome-launcher": "^2.2.0", 74 | "karma-coverage": "^1.1.2", 75 | "karma-mocha": "^1.3.0", 76 | "karma-sinon-chai": "^1.3.4", 77 | "karma-sourcemap-loader": "^0.3.7", 78 | "karma-spec-reporter": "0.0.32", 79 | "karma-webpack": "^3.0.0", 80 | "less": "^3.0.4", 81 | "less-loader": "^4.1.0", 82 | "mocha": "^5.1.1", 83 | "nightwatch": "^0.9.21", 84 | "opn": "^5.3.0", 85 | "optimize-css-assets-webpack-plugin": "^4.0.1", 86 | "ora": "^2.1.0", 87 | "rimraf": "^2.6.2", 88 | "selenium-server": "^3.12.0", 89 | "semver": "^5.5.0", 90 | "shelljs": "^0.8.2", 91 | "sinon": "^4.5.0", 92 | "sinon-chai": "^3.0.0", 93 | "sw-precache": "^5.2.1", 94 | "sw-precache-webpack-plugin": "0.11.5", 95 | "uglify-es": "^3.3.10", 96 | "url-loader": "^1.0.1", 97 | "vue-loader": "^15.0.11", 98 | "vue-style-loader": "^4.1.0", 99 | "vue-template-compiler": "^2.5.16", 100 | "webpack": "^4.8.3", 101 | "webpack-bundle-analyzer": "^2.12.0", 102 | "webpack-dev-middleware": "^3.1.3", 103 | "webpack-hot-middleware": "^2.22.2", 104 | "webpack-merge": "^4.1.2" 105 | }, 106 | "engines": { 107 | "node": ">= 6.9.0", 108 | "npm": ">= 3.10.0" 109 | }, 110 | "browserslist": [ 111 | "> 1%", 112 | "last 2 versions" 113 | ], 114 | "optionalDependencies": { 115 | "karma-firefox-launcher": "^1.1.0" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macedigital/vuejs-starterkit/681dd3c71c01623965d810c12f92350bc7dd7009/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/styles/_animations.less: -------------------------------------------------------------------------------- 1 | // @link https://vuejs.org/v2/guide/transitions.html 2 | .fade-enter-active, .fade-leave-active { 3 | transition: opacity .5s; 4 | } 5 | .fade-enter, .fade-leave-to { 6 | opacity: 0; 7 | } 8 | 9 | // main-transition animation 10 | .slide-fade-enter-active { 11 | transition: all .1s ease-out; 12 | } 13 | .slide-fade-leave-active { 14 | transition: all .1s ease-out 15 | } 16 | .slide-fade-enter, .slide-fade-leave-to { 17 | transform: translateX(1rem); 18 | opacity: 0; 19 | } 20 | -------------------------------------------------------------------------------- /src/assets/styles/_variables.less: -------------------------------------------------------------------------------- 1 | 2 | // TYPOGRAPHY 3 | @font-family-base: 'Avenir', Helvetica, Arial, sans-serif; 4 | @font-color-base: #2c3e50; 5 | 6 | @header-background-color: #35495E; 7 | @header-text-color: #fff; 8 | -------------------------------------------------------------------------------- /src/assets/styles/app.less: -------------------------------------------------------------------------------- 1 | @import "_variables.less"; 2 | 3 | body { 4 | margin: 0; 5 | } 6 | 7 | a { 8 | text-decoration: none; 9 | } 10 | 11 | #app { 12 | font-family: @font-family-base; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | color: @font-color-base; 16 | } 17 | 18 | main { 19 | text-align: center; 20 | margin-top: 40px; 21 | } 22 | 23 | header { 24 | margin: 0; 25 | height: 56px; 26 | padding: 0 16px 0 24px; 27 | background-color: @header-background-color; 28 | color: @header-text-color; 29 | } 30 | 31 | header a { 32 | color: @header-text-color; 33 | display: inline-block; 34 | position: relative; 35 | font-size: 20px; 36 | line-height: 1; 37 | letter-spacing: .02em; 38 | font-weight: 400; 39 | box-sizing: border-box; 40 | padding-top: 16px; 41 | } 42 | 43 | @import "_animations.less"; 44 | -------------------------------------------------------------------------------- /src/components/Counter.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 39 | 40 | 45 | 46 | -------------------------------------------------------------------------------- /src/components/Hello.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 32 | 33 | 34 | 54 | -------------------------------------------------------------------------------- /src/components/Router.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/RouterChild.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 25 | -------------------------------------------------------------------------------- /src/components/Store.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { sync } from 'vuex-router-sync'; 3 | import App from './App'; 4 | import router from './router'; 5 | import store from './store'; 6 | 7 | Vue.config.productionTip = false; 8 | 9 | sync(store, router); 10 | 11 | /* eslint-disable no-new */ 12 | new Vue({ 13 | el: '#app', 14 | functional: true, 15 | router, 16 | store, 17 | render(h) { 18 | return h(App); 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import HelloComponent from '@/components/Hello'; 4 | import RouterComponent from '@/components/Router'; 5 | import RouterChildComponent from '@/components/RouterChild'; 6 | import StoreComponent from '@/components/Store'; 7 | 8 | Vue.use(Router); 9 | 10 | const router = new Router({ 11 | mode: 'history', 12 | base: process.env.ROUTER_PREFIX, 13 | routes: [ 14 | { 15 | path: '/', 16 | name: 'home', 17 | component: HelloComponent, 18 | }, 19 | { 20 | path: '/store', 21 | name: 'store', 22 | alias: '/vuex', 23 | component: StoreComponent, 24 | }, 25 | { 26 | path: '/router', 27 | component: RouterComponent, 28 | children: [ 29 | { 30 | path: '', 31 | name: 'router', 32 | component: RouterChildComponent, 33 | }, 34 | { 35 | path: ':child', 36 | component: RouterChildComponent, 37 | }, 38 | ], 39 | }, 40 | { 41 | path: '*', 42 | component: { 43 | functional: true, 44 | render(h) { 45 | return h('h1', 'Page not found!'); 46 | }, 47 | }, 48 | }, 49 | ], 50 | }); 51 | 52 | export default router; 53 | -------------------------------------------------------------------------------- /src/store/createState.js: -------------------------------------------------------------------------------- 1 | import { MIN_COUNT } from '@/store/mutations'; 2 | 3 | const defaultState = { 4 | count: MIN_COUNT, 5 | }; 6 | 7 | const createState = customState => Object.assign({}, defaultState, customState); 8 | 9 | export default createState; 10 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import mutations from './mutations'; 4 | import persistedState from './persistedState'; 5 | import createState from './createState'; 6 | 7 | Vue.use(Vuex); 8 | 9 | export const createStore = initialState => new Vuex.Store({ 10 | state: createState(initialState), 11 | plugins: [persistedState], 12 | mutations: { 13 | ...mutations, 14 | }, 15 | }); 16 | 17 | export default createStore(); 18 | -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | export const MIN_COUNT = 0; 2 | export const MAX_COUNT = 10; 3 | 4 | export default { 5 | increment(state) { 6 | if (state.count < MAX_COUNT) { 7 | // eslint-disable-next-line no-plusplus,no-param-reassign 8 | state.count++; 9 | } 10 | }, 11 | decrement(state) { 12 | if (state.count > MIN_COUNT) { 13 | // eslint-disable-next-line no-plusplus,no-param-reassign 14 | state.count--; 15 | } 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/store/persistedState.js: -------------------------------------------------------------------------------- 1 | import createPersistedState from 'vuex-persistedstate'; 2 | import * as Cookies from 'js-cookie'; 3 | 4 | export const cookieStorage = { 5 | getItem: key => Cookies.get(key), 6 | // Please see https://github.com/js-cookie/js-cookie#json, on how to handle JSON. 7 | setItem: (key, value) => Cookies.set(key, value, { expires: 30, secure: process.env.NODE_ENV === 'production' }), 8 | removeItem: key => Cookies.remove(key), 9 | }; 10 | 11 | export const persistedStateConfig = { 12 | key: '_vuex', 13 | filter({ type }) { 14 | // Don't store route state in cookie 15 | return !type.startsWith('route/'); 16 | }, 17 | storage: cookieStorage, 18 | }; 19 | 20 | export default createPersistedState(persistedStateConfig); 21 | -------------------------------------------------------------------------------- /static/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macedigital/vuejs-starterkit/681dd3c71c01623965d810c12f92350bc7dd7009/static/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macedigital/vuejs-starterkit/681dd3c71c01623965d810c12f92350bc7dd7009/static/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /static/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macedigital/vuejs-starterkit/681dd3c71c01623965d810c12f92350bc7dd7009/static/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /static/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macedigital/vuejs-starterkit/681dd3c71c01623965d810c12f92350bc7dd7009/static/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /static/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macedigital/vuejs-starterkit/681dd3c71c01623965d810c12f92350bc7dd7009/static/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /static/img/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macedigital/vuejs-starterkit/681dd3c71c01623965d810c12f92350bc7dd7009/static/img/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /static/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macedigital/vuejs-starterkit/681dd3c71c01623965d810c12f92350bc7dd7009/static/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /static/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macedigital/vuejs-starterkit/681dd3c71c01623965d810c12f92350bc7dd7009/static/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /static/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macedigital/vuejs-starterkit/681dd3c71c01623965d810c12f92350bc7dd7009/static/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /static/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macedigital/vuejs-starterkit/681dd3c71c01623965d810c12f92350bc7dd7009/static/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /static/img/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macedigital/vuejs-starterkit/681dd3c71c01623965d810c12f92350bc7dd7009/static/img/icons/favicon.ico -------------------------------------------------------------------------------- /static/img/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macedigital/vuejs-starterkit/681dd3c71c01623965d810c12f92350bc7dd7009/static/img/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /static/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macedigital/vuejs-starterkit/681dd3c71c01623965d810c12f92350bc7dd7009/static/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /static/img/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "VueJS Starterkit", 3 | "short_name": "Vue.js PWA", 4 | "description": "A webpack-based Vue.js starter-kit project for building PWAs.", 5 | "icons": [ 6 | { 7 | "src": "/static/img/icons/android-chrome-192x192.png", 8 | "sizes": "192x192", 9 | "type": "image/png" 10 | }, 11 | { 12 | "src": "/static/img/icons/android-chrome-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png" 15 | } 16 | ], 17 | "start_url": "/?utm_source=web_app_manifest", 18 | "display": "standalone", 19 | "background_color": "#fff", 20 | "theme_color": "#4DBA87", 21 | "orientation": "portrait-primary", 22 | "prefer_related_applications": false 23 | } 24 | -------------------------------------------------------------------------------- /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 elementCount(selector, count) { 10 | this.message = `Testing if element <${selector}> has count: ${count}`; 11 | this.expected = count; 12 | this.pass = function pass(val) { 13 | return val === this.expected; 14 | }; 15 | this.value = function value(res) { 16 | return res.value; 17 | }; 18 | this.command = function command(cb) { 19 | const self = this; 20 | return this.api.execute(sel => document.querySelectorAll(sel).length, [selector], (res) => { 21 | cb.call(self, res); 22 | }); 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | const config = require('../../config'); 3 | const chromedriver = require('chromedriver'); 4 | const selenium = require('selenium-server'); 5 | 6 | // http://nightwatchjs.org/gettingstarted#settings-file 7 | module.exports = { 8 | src_folders: ['test/e2e/specs'], 9 | output_folder: 'test/e2e/reports', 10 | custom_assertions_path: ['test/e2e/custom-assertions'], 11 | 12 | selenium: { 13 | start_process: true, 14 | server_path: selenium.path, 15 | host: '127.0.0.1', 16 | port: 4444, 17 | cli_args: { 18 | 'webdriver.chrome.driver': chromedriver.path, 19 | }, 20 | }, 21 | 22 | test_settings: { 23 | default: { 24 | selenium_port: 4444, 25 | selenium_host: '127.0.0.1', 26 | silent: true, 27 | globals: { 28 | devServerURL: `http://127.0.0.1:${process.env.PORT || config.dev.port}`, 29 | }, 30 | }, 31 | 32 | chrome: { 33 | desiredCapabilities: { 34 | browserName: 'chrome', 35 | javascriptEnabled: true, 36 | acceptSslCerts: true, 37 | }, 38 | }, 39 | 40 | firefox: { 41 | desiredCapabilities: { 42 | browserName: 'firefox', 43 | javascriptEnabled: true, 44 | acceptSslCerts: true, 45 | }, 46 | }, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server using production config 2 | process.env.NODE_ENV = 'testing'; 3 | const server = require('../../build/dev-server.js'); 4 | const spawn = require('cross-spawn'); 5 | 6 | server.ready.then(() => { 7 | // 2. run the nightwatch test suite against it 8 | // to run in additional browsers: 9 | // 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings" 10 | // 2. add it to the --env flag below 11 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 12 | // For more information on Nightwatch's config file, see 13 | // http://nightwatchjs.org/guide#settings-file 14 | let opts = process.argv.slice(2); 15 | if (opts.indexOf('--config') === -1) { 16 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']); 17 | } 18 | if (opts.indexOf('--env') === -1) { 19 | opts = opts.concat(['--env', 'chrome']); 20 | } 21 | 22 | const runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }); 23 | 24 | runner.on('exit', (code) => { 25 | server.close(); 26 | process.exit(code); 27 | }); 28 | 29 | runner.on('error', (err) => { 30 | server.close(); 31 | throw err; 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /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 test(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.title('A Vuejs Starterkit') 15 | .assert.elementPresent('.hello') 16 | .assert.containsText('h1', 'Welcome to Your Vue.js PWA') 17 | .assert.elementCount('img', 1) 18 | .end(); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /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 | import Vue from 'vue'; 2 | 3 | Vue.config.productionTip = false; 4 | Vue.config.devtools = false; 5 | 6 | // require all test files (files that ends with .spec.js) 7 | const testsContext = require.context('./specs', true, /\.spec$/); 8 | testsContext.keys().forEach(testsContext); 9 | 10 | // require all src files except main.js for coverage. 11 | // you can also change this to match only the subset of files that 12 | // you want coverage for. 13 | const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/); 14 | srcContext.keys().forEach(srcContext); 15 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This is a karma config file. For more details see 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | // we are also using it with karma-webpack 4 | // https://github.com/webpack/karma-webpack 5 | 6 | const webpackConfig = require('../../build/webpack.test.conf'); 7 | 8 | module.exports = function karmaConfig(config) { 9 | config.set({ 10 | frameworks: ['mocha', 'sinon-chai'], 11 | files: ['./index.js'], 12 | preprocessors: { 13 | './index.js': ['webpack', 'sourcemap'], 14 | }, 15 | client: { 16 | chai: { 17 | includeStack: true, 18 | showDiff: true, 19 | }, 20 | }, 21 | browsers: ['ChromeHeadless'], 22 | port: 9876, 23 | colors: true, 24 | logLevel: config.LOG_INFO, 25 | autoWatch: false, 26 | singleRun: true, 27 | concurrency: Infinity, 28 | reporters: ['spec', 'coverage'], 29 | webpack: webpackConfig, 30 | webpackMiddleware: { 31 | noInfo: true, 32 | }, 33 | coverageReporter: { 34 | dir: './coverage', 35 | reporters: [ 36 | { type: 'lcov', subdir: '.' }, 37 | { type: 'text-summary' }, 38 | ], 39 | }, 40 | customLaunchers: { 41 | ChromeHeadless: { 42 | base: 'Chrome', 43 | flags: [ 44 | '--no-sandbox', 45 | '--headless', 46 | '--disable-gpu', 47 | '--disable-translate', 48 | '--disable-extensions', 49 | '--remote-debugging-port=9222', // Without a remote debugging port, Google Chrome exits immediately. 50 | ], 51 | }, 52 | FirefoxHeadless: { 53 | base: 'Firefox', 54 | flags: [ 55 | '--safe-mode', 56 | '--headless', 57 | '--marionette', 58 | ], 59 | }, 60 | }, 61 | }); 62 | }; 63 | -------------------------------------------------------------------------------- /test/unit/specs/components/App.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import App from '@/App'; 4 | import router from '@/router'; 5 | import { shallow } from 'avoriaz'; 6 | 7 | Vue.use(Vuex); 8 | 9 | describe('App.vue', () => { 10 | let wrapper; 11 | let path; 12 | 13 | beforeEach(() => { 14 | wrapper = shallow(App, { 15 | router, 16 | store: new Vuex.Store({}), 17 | }); 18 | }); 19 | 20 | [ 21 | { 22 | routePath: '/', 23 | routeName: 'home', 24 | componentClass: '.hello', 25 | }, 26 | { 27 | routePath: '/router', 28 | routeName: 'router', 29 | componentClass: '.router', 30 | }, 31 | { 32 | routePath: '/store', 33 | routeName: 'store', 34 | componentClass: '.store', 35 | }, 36 | ].forEach(({ routePath, routeName, componentClass }) => { 37 | describe(`when visiting path ${routePath}`, () => { 38 | beforeEach(() => { 39 | router.push({ 40 | path: routePath, 41 | }); 42 | }); 43 | 44 | it(`should navigate to "${routeName}" route`, () => { 45 | expect(wrapper.vm.$route.name).to.equal(routeName); 46 | }); 47 | 48 | it(`should render "${componentClass}" component`, () => { 49 | expect(wrapper.find(`main > ${componentClass}`).length).to.equal(1); 50 | }); 51 | }); 52 | }); 53 | 54 | describe('When visiting an unregistered route', () => { 55 | beforeEach(() => { 56 | path = '/some/route/going/nowhere/'; 57 | router.push({ 58 | path, 59 | }); 60 | }); 61 | 62 | it('should be on visited path', () => { 63 | expect(wrapper.vm.$route.path).to.equal(path); 64 | }); 65 | 66 | it('should render "Not found"', () => { 67 | expect(wrapper.find('main > h1')[0].text()).to.equal('Page not found!'); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/unit/specs/components/Counter.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import { shallow } from 'avoriaz'; 4 | import Counter from '@/components/Counter'; 5 | import createState from '@/store/createState'; 6 | import mutations, { MIN_COUNT, MAX_COUNT } from '@/store/mutations'; 7 | 8 | Vue.use(Vuex); 9 | 10 | describe('Counter.vue', () => { 11 | let wrapper; 12 | let store; 13 | let initialCount; 14 | 15 | beforeEach(() => { 16 | initialCount = 3; 17 | 18 | store = new Vuex.Store({ 19 | state: createState({ 20 | count: initialCount, 21 | }), 22 | mutations: { 23 | ...mutations, 24 | }, 25 | }); 26 | 27 | wrapper = shallow(Counter, { store }); 28 | }); 29 | 30 | it('should show computed count value', () => { 31 | const preTag = wrapper.find('pre')[0].text(); 32 | expect(preTag).to.equal(`Current count: ${initialCount}`); 33 | }); 34 | 35 | it('should show remaing items value', () => { 36 | const remainingTag = wrapper.find('p')[0]; 37 | const remainingItems = MAX_COUNT - initialCount; 38 | expect(remainingTag.text()).to.equal(`You can add ${remainingItems} more items.`); 39 | expect(remainingTag.hasClass('alert')).to.equal(false); 40 | }); 41 | 42 | it('should decrement count when - button is clicked', () => { 43 | const expectedCount = initialCount - 1; 44 | wrapper.find('button')[0].trigger('click'); 45 | expect(store.state.count).to.equal(expectedCount); 46 | }); 47 | 48 | it('should increment count when + button is clicked', () => { 49 | const expectedCount = initialCount + 1; 50 | wrapper.find('button')[1].trigger('click'); 51 | expect(store.state.count).to.equal(expectedCount); 52 | }); 53 | 54 | describe('When counter is at minimum', () => { 55 | beforeEach(() => { 56 | store.state.count = MIN_COUNT; 57 | wrapper.update(); 58 | }); 59 | 60 | it('Should disable decrement button', () => { 61 | const btn = wrapper.find('button')[0]; 62 | expect(btn.hasAttribute('disabled')).to.equal(true); 63 | }); 64 | }); 65 | 66 | describe('When count value is one less below limit', () => { 67 | beforeEach(() => { 68 | store.state.count = MAX_COUNT - 1; 69 | wrapper.update(); 70 | }); 71 | 72 | it('Should print alert message to user', () => { 73 | const remainingTag = wrapper.find('p')[0]; 74 | expect(remainingTag.text()).to.equal('Only 1 items remain!'); 75 | expect(remainingTag.hasClass('alert')).to.equal(true); 76 | }); 77 | }); 78 | 79 | describe('When counter is at maximum', () => { 80 | beforeEach(() => { 81 | store.state.count = MAX_COUNT; 82 | wrapper.update(); 83 | }); 84 | 85 | it('Should print alert message to user', () => { 86 | const remainingTag = wrapper.find('p')[0]; 87 | expect(remainingTag.text()).to.equal('No more items!'); 88 | expect(remainingTag.hasClass('alert')).to.equal(true); 89 | }); 90 | 91 | it('Should disable increment button', () => { 92 | const btn = wrapper.find('button')[1]; 93 | expect(btn.hasAttribute('disabled')).to.equal(true); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /test/unit/specs/components/Hello.spec.js: -------------------------------------------------------------------------------- 1 | import { shallow } from 'avoriaz'; 2 | import Hello from '@/components/Hello'; 3 | 4 | describe('Hello.vue', () => { 5 | let wrapper; 6 | 7 | beforeEach(() => { 8 | wrapper = shallow(Hello); 9 | }); 10 | 11 | it('should render headline based on `msg` data-property', () => { 12 | const { msg } = wrapper.data(); 13 | expect(wrapper.find('.hello h1')[0].text()).to.equal(msg); 14 | }); 15 | 16 | it('should have reactive `msg` data-property', () => { 17 | const msg = 'Something else coming in'; 18 | wrapper.setData({ 19 | msg, 20 | }); 21 | expect(wrapper.find('.hello h1')[0].text()).to.equal(msg); 22 | }); 23 | 24 | it('should render 9 links', () => { 25 | expect(wrapper.find('.hello a').length).to.equal(9); 26 | }); 27 | 28 | it('should render correct sub-headline with essential links', () => { 29 | expect(wrapper.find('.hello h2')[0].text()).to.equal('Essential Links'); 30 | }); 31 | 32 | it('should render correct sub-headline with ecosystem links', () => { 33 | expect(wrapper.find('.hello h2')[1].text()).to.equal('Ecosystem'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/unit/specs/components/RouterChild.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import RouterChild from '@/components/RouterChild'; 4 | import { shallow } from 'avoriaz'; 5 | 6 | Vue.use(Router); 7 | 8 | const setupComponentWithRoute = ($route) => { 9 | const routes = [ 10 | { 11 | path: '/:child', 12 | component: RouterChild, 13 | }, 14 | ]; 15 | const router = new Router({ 16 | routes, 17 | }); 18 | 19 | const wrapper = shallow(RouterChild, { 20 | router, 21 | }); 22 | 23 | router.push($route); 24 | wrapper.update(); 25 | 26 | return wrapper; 27 | }; 28 | 29 | describe('RouterChild.vue', () => { 30 | it('should set Default title', () => { 31 | const $route = { 32 | path: '/', 33 | }; 34 | const wrapper = setupComponentWithRoute($route); 35 | expect(wrapper.find('h1')[0].text()).to.equal('Default'); 36 | }); 37 | 38 | it('should set capitalized title passed from router parameter', () => { 39 | const $route = { 40 | path: '/something', 41 | }; 42 | const wrapper = setupComponentWithRoute($route); 43 | 44 | expect(wrapper.vm.$route.params).to.deep.equal({ 45 | child: 'something', 46 | }); 47 | 48 | expect(wrapper.find('h1')[0].text()).to.equal('Something'); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/unit/specs/store/createState.spec.js: -------------------------------------------------------------------------------- 1 | import createState from '@/store/createState'; 2 | 3 | describe('store:createState', () => { 4 | const defaultState = { 5 | count: 0, 6 | }; 7 | 8 | it('should provide defaults', () => { 9 | expect(createState()).to.deep.equal(defaultState); 10 | }); 11 | 12 | it('should merge custom state with default state', () => { 13 | const customState = { 14 | count: 5, 15 | other: 'thing', 16 | }; 17 | expect(createState(customState)).to.deep.equal({ 18 | count: 5, 19 | other: 'thing', 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/unit/specs/store/mutations.spec.js: -------------------------------------------------------------------------------- 1 | import mutations, { MIN_COUNT, MAX_COUNT } from '@/store/mutations'; 2 | 3 | describe('store:mutations', () => { 4 | describe('increment()', () => { 5 | it('should increment count by one', () => { 6 | const state = { 7 | count: MIN_COUNT, 8 | }; 9 | mutations.increment(state); 10 | expect(state.count).to.equal(MIN_COUNT + 1); 11 | }); 12 | 13 | it('should stop incrementing if maximum is reached', () => { 14 | const state = { 15 | count: MAX_COUNT, 16 | }; 17 | mutations.increment(state); 18 | expect(state.count).to.equal(MAX_COUNT); 19 | }); 20 | }); 21 | 22 | describe('decrement()', () => { 23 | it('should decrement count by one', () => { 24 | const state = { 25 | count: MAX_COUNT, 26 | }; 27 | mutations.decrement(state); 28 | expect(state.count).to.equal(MAX_COUNT - 1); 29 | }); 30 | 31 | it('should stop decrementing if minimum is reached', () => { 32 | const state = { 33 | count: MIN_COUNT, 34 | }; 35 | mutations.decrement(state); 36 | expect(state.count).to.equal(MIN_COUNT); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/unit/specs/store/persistedState.spec.js: -------------------------------------------------------------------------------- 1 | import { cookieStorage, persistedStateConfig } from '@/store/persistedState'; 2 | 3 | describe('store:persistedState plugin', () => { 4 | describe('cookieStorage', () => { 5 | it('should set cookie values', () => { 6 | expect(cookieStorage.setItem('foo', 'bar')).to.match(/^foo=bar;/); 7 | }); 8 | 9 | it('should read cookie values', () => { 10 | expect(cookieStorage.getItem('foo')).to.equal('bar'); 11 | }); 12 | 13 | it('should delete cookie values', () => { 14 | expect(cookieStorage.removeItem('foo')).to.equal(undefined); 15 | expect(cookieStorage.getItem('foo')).to.equal(undefined); 16 | }); 17 | }); 18 | 19 | describe('persistedStateConfig', () => { 20 | it('should have correct name', () => { 21 | expect(persistedStateConfig.key).to.equal('_vuex'); 22 | }); 23 | 24 | it('should filter out route changes', () => { 25 | expect(persistedStateConfig.filter({ type: 'route/' })).to.equal(false); 26 | }); 27 | 28 | it('should permit non-route changes', () => { 29 | expect(persistedStateConfig.filter({ type: 'something/' })).to.equal(true); 30 | }); 31 | 32 | it('should use cookieStorage', () => { 33 | expect(persistedStateConfig.storage).to.deep.equal(cookieStorage); 34 | }); 35 | }); 36 | }); 37 | --------------------------------------------------------------------------------