├── .babelrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── README.md ├── build ├── build.js ├── check-versions.js ├── dev-client.js ├── dev-server.js ├── utils.js ├── vue-loader.conf.js ├── webpack.base.conf.js ├── webpack.dev.conf.js ├── webpack.prod.conf.js └── webpack.test.conf.js ├── config ├── dev.env.js ├── index.js ├── prod.env.js └── test.env.js ├── index.html ├── package-lock.json ├── package.json ├── src ├── App.vue ├── common │ ├── api.service.js │ ├── config.js │ ├── date.filter.js │ ├── error.filter.js │ └── jwt.service.js ├── components │ ├── ArticleActions.vue │ ├── ArticleList.vue │ ├── ArticleMeta.vue │ ├── Comment.vue │ ├── CommentEditor.vue │ ├── ListErrors.vue │ ├── TheFooter.vue │ ├── TheHeader.vue │ ├── VArticlePreview.vue │ ├── VPagination.vue │ └── VTag.vue ├── main.js ├── router │ └── index.js ├── store │ ├── actions.type.js │ ├── article.module.js │ ├── auth.module.js │ ├── getters.type.js │ ├── home.module.js │ ├── index.js │ ├── mutations.type.js │ ├── profile.module.js │ └── settings.module.js └── views │ ├── Article.vue │ ├── ArticleEdit.vue │ ├── Home.vue │ ├── HomeGlobal.vue │ ├── HomeMyFeed.vue │ ├── HomeTag.vue │ ├── Login.vue │ ├── Profile.vue │ ├── ProfileArticles.vue │ ├── ProfileFavorited.vue │ ├── Register.vue │ └── Settings.vue ├── static ├── .gitkeep ├── logo.png └── rwv-logo.png ├── test ├── e2e │ ├── custom-assertions │ │ └── elementCount.js │ ├── nightwatch.conf.js │ ├── runner.js │ └── specs │ │ └── test.js └── unit │ ├── .eslintrc │ ├── index.js │ ├── karma.conf.js │ └── specs │ └── Home.spec.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "browsers": ["last 2 versions", "> 5% in FR"], 6 | "uglify": true 7 | }, 8 | "modules": false 9 | }] 10 | ], 11 | "plugins": [ 12 | "syntax-dynamic-import", 13 | "transform-object-rest-spread" 14 | ], 15 | "env": { 16 | "test": { 17 | "presets": [ 18 | ["env", { 19 | "targets": { 20 | "node": "current" 21 | } 22 | }] 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | # editorconfig-tools is unable to ignore longs strings or urls 11 | max_line_length = null 12 | -------------------------------------------------------------------------------- /.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: 'standard', 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 | 'no-unused-expressions': process.env.NODE_ENV === 'production' ? 2 : 0 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /bower_components 6 | 7 | # Tooling Logs 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | selenium-debug.log 12 | 13 | # IDEs and editors 14 | /.idea 15 | .project 16 | .classpath 17 | *.launch 18 | .settings/ 19 | 20 | # System Files 21 | .DS_Store 22 | Thumbs.db 23 | 24 | # Generated Files 25 | dist/ 26 | test/unit/coverage 27 | test/e2e/reports 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | !!! Warning: This repo is now hosted by gothinkster. You can find the latest implementation of vue-realworld [here](https://github.com/gothinkster/vue-realworld-example-app) 5 | 6 | 7 | --- 8 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 9 | 10 | 11 | # ![RealWorld Example App](./static/rwv-logo.png) 12 | 13 | ### Vue.js codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld) spec and API. 14 | 15 | #### [Demo](https://demo.realworld.io/#/) 16 | 17 | 18 | This codebase was created to demonstrate a fully fledged fullstack application built with **Vue.js** including CRUD operations, authentication, routing, pagination, and more. 19 | 20 | We've gone to great lengths to adhere to the **Vue.js** community styleguides & best practices. 21 | 22 | For more information on how to this works with other frontends/backends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repo. 23 | 24 | 25 | # Getting started 26 | 27 | Before contributing please read the following: 28 | 1. [RealWorld guidelines](https://github.com/gothinkster/realworld/tree/master/spec) for implementing a new framework, 29 | 2. [RealWorld frontend instructions](https://github.com/gothinkster/realworld-starter-kit/blob/master/FRONTEND_INSTRUCTIONS.md) 30 | 3. [Realworld API endpoints](https://github.com/gothinkster/realworld/tree/master/api) 31 | 4. [Vue.js styleguide](https://vuejs.org/v2/style-guide/index.html). Priority A and B categories must be respected. 32 | 33 | 34 | The stack is built using [vue-cli webpack](https://github.com/vuejs-templates/webpack) so to get started all you have to do is: 35 | ``` bash 36 | # install dependencies 37 | > npm install 38 | # serve with hot reload at localhost:8080 39 | > npm run dev 40 | ``` 41 | 42 | Other commands available are: 43 | ``` bash 44 | # build for production with minification 45 | npm run build 46 | 47 | # build for production and view the bundle analyzer report 48 | npm run build --report 49 | 50 | # run single unit tests 51 | npm run unit 52 | 53 | # run continous unit tests 54 | npm run units 55 | 56 | # run e2e tests 57 | npm run e2e 58 | 59 | # run all tests 60 | npm test 61 | ``` 62 | 63 | # To know 64 | 65 | Current arbitrary choices are: 66 | - Vuex modules for store 67 | - Vue-axios for ajax requests 68 | - Standard for linting 69 | - 'rwv' as prefix for components 70 | 71 | These can be changed when the contributors reach a consensus. 72 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | var ora = require('ora') 6 | var rm = require('rimraf') 7 | var path = require('path') 8 | var chalk = require('chalk') 9 | var webpack = require('webpack') 10 | var config = require('../config') 11 | var webpackConfig = require('./webpack.prod.conf') 12 | 13 | var 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, function (err, stats) { 19 | spinner.stop() 20 | if (err) throw err 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 | 29 | if (stats.hasErrors()) { 30 | console.log(chalk.red(' Build failed with errors.\n')) 31 | process.exit(1) 32 | } 33 | 34 | console.log(chalk.cyan(' Build complete.\n')) 35 | console.log(chalk.yellow( 36 | ' Tip: built files are meant to be served over an HTTP server.\n' + 37 | ' Opening index.html over file:// won\'t work.\n' 38 | )) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var semver = require('semver') 3 | var packageConfig = require('../package.json') 4 | var shell = require('shelljs') 5 | function exec (cmd) { 6 | return require('child_process').execSync(cmd).toString().trim() 7 | } 8 | 9 | var versionRequirements = [ 10 | { 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node 14 | } 15 | ] 16 | 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 = function () { 26 | var warnings = [] 27 | for (var i = 0; i < versionRequirements.length; i++) { 28 | var mod = versionRequirements[i] 29 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 30 | warnings.push(mod.name + ': ' + 31 | chalk.red(mod.currentVersion) + ' should be ' + 32 | chalk.green(mod.versionRequirement) 33 | ) 34 | } 35 | } 36 | 37 | if (warnings.length) { 38 | console.log('') 39 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 40 | console.log() 41 | for (var i = 0; i < warnings.length; i++) { 42 | var warning = warnings[i] 43 | console.log(' ' + warning) 44 | } 45 | console.log() 46 | process.exit(1) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | var config = require('../config') 4 | if (!process.env.NODE_ENV) { 5 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 6 | } 7 | 8 | var opn = require('opn') 9 | var path = require('path') 10 | var express = require('express') 11 | var webpack = require('webpack') 12 | var proxyMiddleware = require('http-proxy-middleware') 13 | var webpackConfig = (process.env.NODE_ENV === 'testing' || process.env.NODE_ENV === 'production') 14 | ? require('./webpack.prod.conf') 15 | : require('./webpack.dev.conf') 16 | 17 | // default port where dev server listens for incoming traffic 18 | var port = process.env.PORT || config.dev.port 19 | // automatically open browser, if not set will be false 20 | var autoOpenBrowser = !!config.dev.autoOpenBrowser 21 | // Define HTTP proxies to your custom API backend 22 | // https://github.com/chimurai/http-proxy-middleware 23 | var proxyTable = config.dev.proxyTable 24 | 25 | var app = express() 26 | var compiler = webpack(webpackConfig) 27 | 28 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 29 | publicPath: webpackConfig.output.publicPath, 30 | quiet: true 31 | }) 32 | 33 | var hotMiddleware = require('webpack-hot-middleware')(compiler, { 34 | log: false, 35 | heartbeat: 2000 36 | }) 37 | // force page reload when html-webpack-plugin template changes 38 | compiler.plugin('compilation', function (compilation) { 39 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 40 | hotMiddleware.publish({ action: 'reload' }) 41 | cb() 42 | }) 43 | }) 44 | 45 | // proxy api requests 46 | Object.keys(proxyTable).forEach(function (context) { 47 | var options = proxyTable[context] 48 | if (typeof options === 'string') { 49 | options = { target: options } 50 | } 51 | app.use(proxyMiddleware(options.filter || context, options)) 52 | }) 53 | 54 | // handle fallback for HTML5 history API 55 | app.use(require('connect-history-api-fallback')()) 56 | 57 | // serve webpack bundle output 58 | app.use(devMiddleware) 59 | 60 | // enable hot-reload and state-preserving 61 | // compilation error display 62 | app.use(hotMiddleware) 63 | 64 | // serve pure static assets 65 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 66 | app.use(staticPath, express.static('./static')) 67 | 68 | var uri = 'http://localhost:' + port 69 | 70 | var _resolve 71 | var readyPromise = new Promise(resolve => { 72 | _resolve = resolve 73 | }) 74 | 75 | console.log('> Starting dev server...') 76 | devMiddleware.waitUntilValid(() => { 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 | var server = app.listen(port) 86 | 87 | module.exports = { 88 | ready: readyPromise, 89 | close: () => { 90 | server.close() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | 15 | var cssLoader = { 16 | loader: 'css-loader', 17 | options: { 18 | minimize: process.env.NODE_ENV === 'production', 19 | sourceMap: options.sourceMap 20 | } 21 | } 22 | 23 | // generate loader string to be used with extract text plugin 24 | function generateLoaders (loader, loaderOptions) { 25 | var loaders = [cssLoader] 26 | if (loader) { 27 | loaders.push({ 28 | loader: loader + '-loader', 29 | options: Object.assign({}, loaderOptions, { 30 | sourceMap: options.sourceMap 31 | }) 32 | }) 33 | } 34 | 35 | // Extract CSS when that option is specified 36 | // (which is the case during production build) 37 | if (options.extract) { 38 | return ExtractTextPlugin.extract({ 39 | use: loaders, 40 | fallback: 'vue-style-loader' 41 | }) 42 | } else { 43 | return ['vue-style-loader'].concat(loaders) 44 | } 45 | } 46 | 47 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 48 | return { 49 | css: generateLoaders(), 50 | postcss: generateLoaders(), 51 | less: generateLoaders('less'), 52 | sass: generateLoaders('sass', { indentedSyntax: true }), 53 | scss: generateLoaders('sass'), 54 | stylus: generateLoaders('stylus'), 55 | styl: generateLoaders('stylus') 56 | } 57 | } 58 | 59 | // Generate loaders for standalone style files (outside of .vue) 60 | exports.styleLoaders = function (options) { 61 | var output = [] 62 | var loaders = exports.cssLoaders(options) 63 | for (var extension in loaders) { 64 | var loader = loaders[extension] 65 | output.push({ 66 | test: new RegExp('\\.' + extension + '$'), 67 | use: loader 68 | }) 69 | } 70 | return output 71 | } 72 | -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var config = require('../config') 3 | var isProduction = process.env.NODE_ENV === 'production' 4 | 5 | module.exports = { 6 | loaders: utils.cssLoaders({ 7 | sourceMap: isProduction 8 | ? config.build.productionSourceMap 9 | : config.dev.cssSourceMap, 10 | extract: isProduction 11 | }), 12 | transformToRequire: { 13 | video: 'src', 14 | source: 'src', 15 | img: 'src', 16 | image: 'xlink:href' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var config = require('../config') 4 | var vueLoaderConfig = require('./vue-loader.conf') 5 | 6 | function resolve (dir) { 7 | return path.join(__dirname, '..', dir) 8 | } 9 | 10 | module.exports = { 11 | entry: { 12 | app: ['babel-polyfill', './src/main.js'] 13 | }, 14 | output: { 15 | path: config.build.assetsRoot, 16 | filename: '[name].js', 17 | publicPath: process.env.NODE_ENV === 'production' 18 | ? config.build.assetsPublicPath 19 | : config.dev.assetsPublicPath 20 | }, 21 | resolve: { 22 | extensions: ['.js', '.vue', '.json'], 23 | alias: { 24 | 'vue$': 'vue/dist/vue.esm.js', 25 | '@': resolve('src'), 26 | } 27 | }, 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.(js|vue)$/, 32 | loader: 'eslint-loader', 33 | enforce: 'pre', 34 | include: [resolve('src'), resolve('test')], 35 | options: { 36 | formatter: require('eslint-friendly-formatter') 37 | } 38 | }, 39 | { 40 | test: /\.vue$/, 41 | loader: 'vue-loader', 42 | options: vueLoaderConfig 43 | }, 44 | { 45 | test: /\.js$/, 46 | loader: 'babel-loader', 47 | include: [resolve('src'), resolve('test')] 48 | }, 49 | { 50 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 51 | loader: 'url-loader', 52 | options: { 53 | limit: 10000, 54 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 55 | } 56 | }, 57 | { 58 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 59 | loader: 'url-loader', 60 | options: { 61 | limit: 10000, 62 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 63 | } 64 | }, 65 | { 66 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 67 | loader: 'url-loader', 68 | options: { 69 | limit: 10000, 70 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 71 | } 72 | } 73 | ] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var webpack = require('webpack') 4 | var config = require('../config') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var HtmlWebpackPlugin = require('html-webpack-plugin') 8 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 9 | var FaviconsWebpackPlugin = require('favicons-webpack-plugin') 10 | 11 | // add hot-reload related code to entry chunks 12 | Object.keys(baseWebpackConfig.entry).forEach(function (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 | // cheap-module-eval-source-map is faster for development 21 | devtool: '#cheap-module-eval-source-map', 22 | plugins: [ 23 | new webpack.DefinePlugin({ 24 | 'process.env': config.dev.env 25 | }), 26 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 27 | new webpack.HotModuleReplacementPlugin(), 28 | new webpack.NoEmitOnErrorsPlugin(), 29 | // https://github.com/ampedandwired/html-webpack-plugin 30 | new HtmlWebpackPlugin({ 31 | filename: 'index.html', 32 | template: 'index.html', 33 | inject: true 34 | }), 35 | new FriendlyErrorsPlugin(), 36 | // generate favicons 37 | new FaviconsWebpackPlugin(path.resolve(__dirname, '../static/logo.png')) 38 | ] 39 | }) 40 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var webpack = require('webpack') 4 | var config = require('../config') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var CopyWebpackPlugin = require('copy-webpack-plugin') 8 | var HtmlWebpackPlugin = require('html-webpack-plugin') 9 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 10 | var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 11 | var FaviconsWebpackPlugin = require('favicons-webpack-plugin') 12 | 13 | var env = process.env.NODE_ENV === 'testing' 14 | ? require('../config/test.env') 15 | : config.build.env 16 | 17 | var webpackConfig = merge(baseWebpackConfig, { 18 | module: { 19 | rules: utils.styleLoaders({ 20 | sourceMap: config.build.productionSourceMap, 21 | extract: true 22 | }) 23 | }, 24 | devtool: config.build.productionSourceMap ? '#source-map' : false, 25 | output: { 26 | path: config.build.assetsRoot, 27 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 28 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 29 | }, 30 | plugins: [ 31 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 32 | new webpack.DefinePlugin({ 33 | 'process.env': env 34 | }), 35 | new webpack.optimize.UglifyJsPlugin({ 36 | compress: { 37 | warnings: false 38 | }, 39 | sourceMap: true 40 | }), 41 | // extract css into its own file 42 | new ExtractTextPlugin({ 43 | filename: utils.assetsPath('css/[name].[contenthash].css') 44 | }), 45 | // Compress extracted CSS. We are using this plugin so that possible 46 | // duplicated CSS from different components can be deduped. 47 | new OptimizeCSSPlugin({ 48 | cssProcessorOptions: { 49 | safe: true 50 | } 51 | }), 52 | // generate dist index.html with correct asset hash for caching. 53 | // you can customize output by editing /index.html 54 | // see https://github.com/ampedandwired/html-webpack-plugin 55 | new HtmlWebpackPlugin({ 56 | filename: process.env.NODE_ENV === 'testing' 57 | ? 'index.html' 58 | : config.build.index, 59 | template: 'index.html', 60 | inject: true, 61 | minify: { 62 | removeComments: true, 63 | collapseWhitespace: true, 64 | removeAttributeQuotes: true 65 | // more options: 66 | // https://github.com/kangax/html-minifier#options-quick-reference 67 | }, 68 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 69 | chunksSortMode: 'dependency' 70 | }), 71 | // keep module.id stable when vender modules does not change 72 | new webpack.HashedModuleIdsPlugin(), 73 | // split vendor js into its own file 74 | new webpack.optimize.CommonsChunkPlugin({ 75 | name: 'vendor', 76 | minChunks: function (module, count) { 77 | // any required modules inside node_modules are extracted to vendor 78 | return ( 79 | module.resource && 80 | /\.js$/.test(module.resource) && 81 | module.resource.indexOf( 82 | path.join(__dirname, '../node_modules') 83 | ) === 0 84 | ) 85 | } 86 | }), 87 | // extract webpack runtime and module manifest to its own file in order to 88 | // prevent vendor hash from being updated whenever app bundle is updated 89 | new webpack.optimize.CommonsChunkPlugin({ 90 | name: 'manifest', 91 | chunks: ['vendor'] 92 | }), 93 | // copy custom static assets 94 | new CopyWebpackPlugin([ 95 | { 96 | from: path.resolve(__dirname, '../static'), 97 | to: config.build.assetsSubDirectory, 98 | ignore: ['.*'] 99 | } 100 | ]), 101 | // generate favicons 102 | new FaviconsWebpackPlugin({ 103 | logo: path.resolve(__dirname, '../static/logo.png'), 104 | prefix: 'favicons-[hash]/' 105 | }) 106 | ] 107 | }) 108 | 109 | if (config.build.productionGzip) { 110 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 111 | 112 | webpackConfig.plugins.push( 113 | new CompressionWebpackPlugin({ 114 | asset: '[path].gz[query]', 115 | algorithm: 'gzip', 116 | test: new RegExp( 117 | '\\.(' + 118 | config.build.productionGzipExtensions.join('|') + 119 | ')$' 120 | ), 121 | threshold: 10240, 122 | minRatio: 0.8 123 | }) 124 | ) 125 | } 126 | 127 | if (config.build.bundleAnalyzerReport) { 128 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 129 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 130 | } 131 | 132 | module.exports = webpackConfig 133 | -------------------------------------------------------------------------------- /build/webpack.test.conf.js: -------------------------------------------------------------------------------- 1 | // This is the webpack config used for unit tests. 2 | 3 | var utils = require('./utils') 4 | var webpack = require('webpack') 5 | var merge = require('webpack-merge') 6 | var baseConfig = require('./webpack.base.conf') 7 | 8 | var webpackConfig = merge(baseConfig, { 9 | // use inline sourcemap for karma-sourcemap-loader 10 | module: { 11 | rules: utils.styleLoaders() 12 | }, 13 | devtool: '#inline-source-map', 14 | resolveLoader: { 15 | alias: { 16 | // necessary to to make lang="scss" work in test when using vue-loader's ?inject option 17 | // see discussion at https://github.com/vuejs/vue-loader/issues/724 18 | 'scss-loader': 'sass-loader' 19 | } 20 | }, 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | 'process.env': require('../config/test.env') 24 | }) 25 | ] 26 | }) 27 | 28 | // no need for app entry during tests 29 | delete webpackConfig.entry 30 | 31 | module.exports = webpackConfig 32 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../dist/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../dist'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: '/', 11 | productionSourceMap: true, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'], 18 | // Run the build command with an extra argument to 19 | // View the bundle analyzer report after build finishes: 20 | // `npm run build --report` 21 | // Set to `true` or `false` to always turn it on or off 22 | bundleAnalyzerReport: process.env.npm_config_report 23 | }, 24 | dev: { 25 | env: require('./dev.env'), 26 | port: 8080, 27 | autoOpenBrowser: true, 28 | assetsSubDirectory: 'static', 29 | assetsPublicPath: '/', 30 | proxyTable: {}, 31 | // CSS Sourcemaps off by default because relative paths are "buggy" 32 | // with this option, according to the CSS-Loader README 33 | // (https://github.com/webpack/css-loader#sourcemaps) 34 | // In our experience, they generally work as expected, 35 | // just be aware of this issue when enabling this option. 36 | cssSourceMap: false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var devEnv = require('./dev.env') 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"testing"' 6 | }) 7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Conduit 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "realworld-vue", 3 | "version": "1.0.0", 4 | "description": "TodoMVC for the RealWorld™", 5 | "author": "Emmanuel Vilsbol ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "start": "node build/dev-server.js", 10 | "build": "node build/build.js", 11 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", 12 | "units": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js", 13 | "e2e": "node test/e2e/runner.js", 14 | "test": "npm run unit && npm run e2e", 15 | "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs", 16 | "postinstall": "node node_modules/phantomjs-prebuilt/install.js" 17 | }, 18 | "dependencies": { 19 | "axios": "^0.16.2", 20 | "moment": "^2.19.2", 21 | "vue": "^2.5.8", 22 | "vue-axios": "^2.0.2", 23 | "vue-markdown": "^2.2.4", 24 | "vue-router": "^2.8.1", 25 | "vuex": "^2.5.0" 26 | }, 27 | "devDependencies": { 28 | "autoprefixer": "^7.1.6", 29 | "babel-core": "^6.22.1", 30 | "babel-eslint": "^7.1.1", 31 | "babel-loader": "^7.1.1", 32 | "babel-plugin-istanbul": "^4.1.5", 33 | "babel-plugin-transform-runtime": "^6.22.0", 34 | "babel-polyfill": "^6.26.0", 35 | "babel-preset-env": "^1.6.1", 36 | "babel-preset-stage-2": "^6.22.0", 37 | "babel-register": "^6.22.0", 38 | "chai": "^3.5.0", 39 | "chalk": "^2.3.0", 40 | "chromedriver": "^2.33.2", 41 | "connect-history-api-fallback": "^1.5.0", 42 | "copy-webpack-plugin": "^4.2.3", 43 | "cross-env": "^5.1.1", 44 | "cross-spawn": "^5.0.1", 45 | "css-loader": "^0.28.0", 46 | "cssnano": "^3.10.0", 47 | "eslint": "^3.19.0", 48 | "eslint-config-standard": "^6.2.1", 49 | "eslint-friendly-formatter": "^3.0.0", 50 | "eslint-loader": "^1.7.1", 51 | "eslint-plugin-html": "^3.2.2", 52 | "eslint-plugin-import": "^2.8.0", 53 | "eslint-plugin-promise": "^3.6.0", 54 | "eslint-plugin-standard": "^2.0.1", 55 | "eventsource-polyfill": "^0.9.6", 56 | "express": "^4.16.2", 57 | "extract-text-webpack-plugin": "^2.0.0", 58 | "favicons-webpack-plugin": "0.0.7", 59 | "file-loader": "^0.11.1", 60 | "friendly-errors-webpack-plugin": "^1.1.3", 61 | "html-webpack-plugin": "^2.28.0", 62 | "http-proxy-middleware": "^0.17.3", 63 | "inject-loader": "^3.0.0", 64 | "karma": "^1.4.1", 65 | "karma-coverage": "^1.1.1", 66 | "karma-mocha": "^1.3.0", 67 | "karma-phantomjs-launcher": "^1.0.2", 68 | "karma-phantomjs-shim": "^1.5.0", 69 | "karma-sinon-chai": "^1.3.3", 70 | "karma-sourcemap-loader": "^0.3.7", 71 | "karma-spec-reporter": "0.0.31", 72 | "karma-webpack": "^2.0.6", 73 | "mocha": "^3.2.0", 74 | "nightwatch": "^0.9.12", 75 | "opn": "^5.1.0", 76 | "optimize-css-assets-webpack-plugin": "^2.0.0", 77 | "ora": "^1.2.0", 78 | "phantomjs-prebuilt": "^2.1.16", 79 | "rimraf": "^2.6.0", 80 | "selenium-server": "^3.7.1", 81 | "semver": "^5.3.0", 82 | "shelljs": "^0.7.6", 83 | "sinon": "^2.1.0", 84 | "sinon-chai": "^2.14.0", 85 | "url-loader": "^0.5.8", 86 | "vue-loader": "^13.5.0", 87 | "vue-style-loader": "^3.0.3", 88 | "vue-template-compiler": "^2.5.8", 89 | "vue-test-utils": "^1.0.0-beta.6", 90 | "webpack": "^2.6.1", 91 | "webpack-bundle-analyzer": "^2.9.1", 92 | "webpack-dev-middleware": "^1.12.1", 93 | "webpack-hot-middleware": "^2.20.0", 94 | "webpack-merge": "^4.1.1" 95 | }, 96 | "engines": { 97 | "node": ">= 4.0.0", 98 | "npm": ">= 3.0.0" 99 | }, 100 | "browserslist": [ 101 | "> 1%", 102 | "last 2 versions", 103 | "not ie <= 8" 104 | ] 105 | } 106 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | 22 | 24 | -------------------------------------------------------------------------------- /src/common/api.service.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import axios from 'axios' 3 | import VueAxios from 'vue-axios' 4 | import JwtService from '@/common/jwt.service' 5 | import { API_URL } from '@/common/config' 6 | 7 | const ApiService = { 8 | init () { 9 | Vue.use(VueAxios, axios) 10 | Vue.axios.defaults.baseURL = API_URL 11 | }, 12 | 13 | setHeader () { 14 | Vue.axios.defaults.headers.common['Authorization'] = `Token ${JwtService.getToken()}` 15 | }, 16 | 17 | query (resource, params) { 18 | return Vue.axios 19 | .get(resource, params) 20 | .catch((error) => { 21 | throw new Error(`[RWV] ApiService ${error}`) 22 | }) 23 | }, 24 | 25 | get (resource, slug = '') { 26 | return Vue.axios 27 | .get(`${resource}/${slug}`) 28 | .catch((error) => { 29 | throw new Error(`[RWV] ApiService ${error}`) 30 | }) 31 | }, 32 | 33 | post (resource, params) { 34 | return Vue.axios.post(`${resource}`, params) 35 | }, 36 | 37 | update (resource, slug, params) { 38 | return Vue.axios.put(`${resource}/${slug}`, params) 39 | }, 40 | 41 | put (resource, params) { 42 | return Vue.axios 43 | .put(`${resource}`, params) 44 | }, 45 | 46 | delete (resource) { 47 | return Vue.axios 48 | .delete(resource) 49 | .catch((error) => { 50 | throw new Error(`[RWV] ApiService ${error}`) 51 | }) 52 | } 53 | } 54 | 55 | export default ApiService 56 | 57 | export const TagsService = { 58 | get () { 59 | return ApiService.get('tags') 60 | } 61 | } 62 | 63 | export const ArticlesService = { 64 | query (type, params) { 65 | return ApiService 66 | .query( 67 | 'articles' + (type === 'feed' ? '/feed' : ''), 68 | { params: params } 69 | ) 70 | }, 71 | get (slug) { 72 | return ApiService.get('articles', slug) 73 | }, 74 | create (params) { 75 | return ApiService.post('articles', {article: params}) 76 | }, 77 | update (slug, params) { 78 | return ApiService.update('articles', slug, {article: params}) 79 | }, 80 | destroy (slug) { 81 | return ApiService.delete(`articles/${slug}`) 82 | } 83 | } 84 | 85 | export const CommentsService = { 86 | get (slug) { 87 | if (typeof slug !== 'string') { 88 | throw new Error('[RWV] CommentsService.get() article slug required to fetch comments') 89 | } 90 | return ApiService.get('articles', `${slug}/comments`) 91 | }, 92 | 93 | post (slug, payload) { 94 | return ApiService.post( 95 | `articles/${slug}/comments`, { comment: { body: payload } }) 96 | }, 97 | 98 | destroy (slug, commentId) { 99 | return ApiService 100 | .delete(`articles/${slug}/comments/${commentId}`) 101 | } 102 | } 103 | 104 | export const FavoriteService = { 105 | add (slug) { 106 | return ApiService.post(`articles/${slug}/favorite`) 107 | }, 108 | remove (slug) { 109 | return ApiService.delete(`articles/${slug}/favorite`) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/common/config.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | export const API_URL = 'https://conduit.productionready.io/api' 3 | -------------------------------------------------------------------------------- /src/common/date.filter.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | export default (date) => { 4 | return moment(date).format('MMM DD, YYYY') 5 | } 6 | -------------------------------------------------------------------------------- /src/common/error.filter.js: -------------------------------------------------------------------------------- 1 | 2 | export default (errorValue) => { 3 | return `${errorValue[0]}` 4 | } 5 | -------------------------------------------------------------------------------- /src/common/jwt.service.js: -------------------------------------------------------------------------------- 1 | const ID_TOKEN_KEY = 'id_token' 2 | 3 | export default { 4 | getToken () { 5 | return window.localStorage.getItem(ID_TOKEN_KEY) 6 | }, 7 | 8 | saveToken (token) { 9 | window.localStorage.setItem(ID_TOKEN_KEY, token) 10 | }, 11 | 12 | destroyToken () { 13 | window.localStorage.removeItem(ID_TOKEN_KEY) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/ArticleActions.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 77 | -------------------------------------------------------------------------------- /src/components/ArticleList.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 142 | -------------------------------------------------------------------------------- /src/components/ArticleMeta.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 78 | -------------------------------------------------------------------------------- /src/components/Comment.vue: -------------------------------------------------------------------------------- 1 | 22 | 48 | -------------------------------------------------------------------------------- /src/components/CommentEditor.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 56 | -------------------------------------------------------------------------------- /src/components/ListErrors.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /src/components/TheFooter.vue: -------------------------------------------------------------------------------- 1 | 15 | 20 | -------------------------------------------------------------------------------- /src/components/TheHeader.vue: -------------------------------------------------------------------------------- 1 | 72 | 87 | -------------------------------------------------------------------------------- /src/components/VArticlePreview.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 37 | -------------------------------------------------------------------------------- /src/components/VPagination.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 43 | -------------------------------------------------------------------------------- /src/components/VTag.vue: -------------------------------------------------------------------------------- 1 | 6 | 21 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | 5 | import App from './App' 6 | import router from '@/router' 7 | import store from '@/store' 8 | import { CHECK_AUTH } from '@/store/actions.type' 9 | 10 | import ApiService from '@/common/api.service' 11 | import DateFilter from '@/common/date.filter' 12 | import ErrorFilter from '@/common/error.filter' 13 | 14 | Vue.config.productionTip = false 15 | Vue.filter('date', DateFilter) 16 | Vue.filter('error', ErrorFilter) 17 | 18 | ApiService.init() 19 | 20 | // Ensure we checked auth before each page load. 21 | router.beforeEach( 22 | (to, from, next) => { 23 | return Promise 24 | .all([store.dispatch(CHECK_AUTH)]) 25 | .then(next) 26 | }, 27 | ) 28 | 29 | /* eslint-disable no-new */ 30 | new Vue({ 31 | el: '#app', 32 | router, 33 | store, 34 | template: '', 35 | components: { App } 36 | }) 37 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | import RwvHome from '@/views/Home' 5 | import RwvLogin from '@/views/Login' 6 | import RwvRegister from '@/views/Register' 7 | import RwvProfile from '@/views/Profile' 8 | import RwvProfileArticles from '@/views/ProfileArticles' 9 | import RwvProfileFavorited from '@/views/ProfileFavorited' 10 | import RwvSettings from '@/views/Settings' 11 | import RwvArticle from '@/views/Article' 12 | import RwvArticleEdit from '@/views/ArticleEdit' 13 | import RwvHomeGlobal from '@/views/HomeGlobal' 14 | import RwvHomeTag from '@/views/HomeTag' 15 | import RwvHomeMyFeed from '@/views/HomeMyFeed' 16 | 17 | Vue.use(Router) 18 | 19 | export default new Router({ 20 | routes: [ 21 | { 22 | path: '/', 23 | component: RwvHome, 24 | children: [ 25 | { 26 | path: '', 27 | name: 'home', 28 | component: RwvHomeGlobal 29 | }, 30 | { 31 | path: 'my-feed', 32 | name: 'home-my-feed', 33 | component: RwvHomeMyFeed 34 | }, 35 | { 36 | path: 'tag/:tag', 37 | name: 'home-tag', 38 | component: RwvHomeTag 39 | } 40 | ] 41 | }, 42 | { 43 | name: 'login', 44 | path: '/login', 45 | component: RwvLogin 46 | }, 47 | { 48 | name: 'register', 49 | path: '/register', 50 | component: RwvRegister 51 | }, 52 | { 53 | name: 'settings', 54 | path: '/settings', 55 | component: RwvSettings 56 | }, 57 | // Handle child routes with a default, by giving the name to the 58 | // child. 59 | // SO: https://github.com/vuejs/vue-router/issues/777 60 | { 61 | path: '/@:username', 62 | component: RwvProfile, 63 | children: [ 64 | { 65 | path: '', 66 | name: 'profile', 67 | component: RwvProfileArticles 68 | }, 69 | { 70 | name: 'profile-favorites', 71 | path: 'favorites', 72 | component: RwvProfileFavorited 73 | } 74 | ] 75 | }, 76 | { 77 | name: 'article', 78 | path: '/articles/:slug', 79 | component: RwvArticle, 80 | props: true 81 | }, 82 | { 83 | name: 'article-edit', 84 | path: '/editor/:slug?', 85 | props: true, 86 | component: RwvArticleEdit 87 | } 88 | ] 89 | }) 90 | -------------------------------------------------------------------------------- /src/store/actions.type.js: -------------------------------------------------------------------------------- 1 | export const ARTICLE_PUBLISH = 'publishArticle' 2 | export const ARTICLE_DELETE = 'deleteArticle' 3 | export const ARTICLE_EDIT = 'editArticle' 4 | export const ARTICLE_EDIT_ADD_TAG = 'addTagToArticle' 5 | export const ARTICLE_EDIT_REMOVE_TAG = 'removeTagFromArticle' 6 | export const ARTICLE_RESET_STATE = 'resetArticleState' 7 | export const CHECK_AUTH = 'checkAuth' 8 | export const COMMENT_CREATE = 'createComment' 9 | export const COMMENT_DESTROY = 'destroyComment' 10 | export const FAVORITE_ADD = 'addFavorite' 11 | export const FAVORITE_REMOVE = 'removeFavorite' 12 | export const FETCH_ARTICLE = 'fetchArticle' 13 | export const FETCH_ARTICLES = 'fetchArticles' 14 | export const FETCH_COMMENTS = 'fetchComments' 15 | export const FETCH_PROFILE = 'fetchProfile' 16 | export const FETCH_PROFILE_FOLLOW = 'fetchProfileFollow' 17 | export const FETCH_PROFILE_UNFOLLOW = 'fetchProfileUnfollow' 18 | export const FETCH_TAGS = 'fetchTags' 19 | export const LOGIN = 'login' 20 | export const LOGOUT = 'logout' 21 | export const REGISTER = 'register' 22 | export const UPDATE_USER = 'updateUser' 23 | -------------------------------------------------------------------------------- /src/store/article.module.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { ArticlesService, CommentsService, FavoriteService } from '@/common/api.service' 3 | import { 4 | FETCH_ARTICLE, 5 | FETCH_COMMENTS, 6 | COMMENT_CREATE, 7 | COMMENT_DESTROY, 8 | FAVORITE_ADD, 9 | FAVORITE_REMOVE, 10 | ARTICLE_PUBLISH, 11 | ARTICLE_EDIT, 12 | ARTICLE_EDIT_ADD_TAG, 13 | ARTICLE_EDIT_REMOVE_TAG, 14 | ARTICLE_DELETE, 15 | ARTICLE_RESET_STATE 16 | } from './actions.type' 17 | import { 18 | RESET_STATE, 19 | SET_ARTICLE, 20 | SET_COMMENTS, 21 | TAG_ADD, 22 | TAG_REMOVE, 23 | UPDATE_ARTICLE_IN_LIST 24 | } from './mutations.type' 25 | import { 26 | GET_ARTICLE, 27 | GET_COMMENTS 28 | } from './getters.type' 29 | 30 | const initialState = { 31 | article: { 32 | author: {}, 33 | title: '', 34 | description: '', 35 | body: '', 36 | tagList: [] 37 | }, 38 | comments: [] 39 | } 40 | 41 | export const state = Object.assign({}, initialState) 42 | 43 | export const actions = { 44 | [FETCH_ARTICLE] (context, articleSlug, prevArticle) { 45 | // avoid extronuous network call if article exists 46 | if (prevArticle !== undefined) { 47 | return context.commit(SET_ARTICLE, prevArticle) 48 | } 49 | return ArticlesService.get(articleSlug) 50 | .then(({ data }) => { 51 | context.commit(SET_ARTICLE, data.article) 52 | return data 53 | }) 54 | }, 55 | [FETCH_COMMENTS] (context, articleSlug) { 56 | return CommentsService.get(articleSlug) 57 | .then(({ data }) => { 58 | context.commit(SET_COMMENTS, data.comments) 59 | }) 60 | }, 61 | [COMMENT_CREATE] (context, payload) { 62 | return CommentsService 63 | .post(payload.slug, payload.comment) 64 | .then(() => { context.dispatch(FETCH_COMMENTS, payload.slug) }) 65 | }, 66 | [COMMENT_DESTROY] (context, payload) { 67 | return CommentsService 68 | .destroy(payload.slug, payload.commentId) 69 | .then(() => { 70 | context.dispatch(FETCH_COMMENTS, payload.slug) 71 | }) 72 | }, 73 | [FAVORITE_ADD] (context, payload) { 74 | return FavoriteService 75 | .add(payload) 76 | .then(({ data }) => { 77 | // Update list as well. This allows us to favorite an article in the Home view. 78 | context.commit( 79 | UPDATE_ARTICLE_IN_LIST, 80 | data.article, 81 | { root: true } 82 | ) 83 | context.commit(SET_ARTICLE, data.article) 84 | }) 85 | }, 86 | [FAVORITE_REMOVE] (context, payload) { 87 | return FavoriteService 88 | .remove(payload) 89 | .then(({ data }) => { 90 | // Update list as well. This allows us to favorite an article in the Home view. 91 | context.commit( 92 | UPDATE_ARTICLE_IN_LIST, 93 | data.article, 94 | { root: true } 95 | ) 96 | context.commit(SET_ARTICLE, data.article) 97 | }) 98 | }, 99 | [ARTICLE_PUBLISH] ({ state }) { 100 | return ArticlesService.create(state.article) 101 | }, 102 | [ARTICLE_DELETE] (context, slug) { 103 | return ArticlesService.destroy(slug) 104 | }, 105 | [ARTICLE_EDIT] ({ state }) { 106 | return ArticlesService.update(state.article.slug, state.article) 107 | }, 108 | [ARTICLE_EDIT_ADD_TAG] (context, tag) { 109 | context.commit(TAG_ADD, tag) 110 | }, 111 | [ARTICLE_EDIT_REMOVE_TAG] (context, tag) { 112 | context.commit(TAG_REMOVE, tag) 113 | }, 114 | [ARTICLE_RESET_STATE] ({ commit }) { 115 | commit(RESET_STATE) 116 | } 117 | } 118 | 119 | /* eslint no-param-reassign: ["error", { "props": false }] */ 120 | export const mutations = { 121 | [SET_ARTICLE] (state, article) { 122 | state.article = article 123 | }, 124 | [SET_COMMENTS] (state, comments) { 125 | state.comments = comments 126 | }, 127 | [TAG_ADD] (state, tag) { 128 | state.article.tagList = state.article.tagList.concat([tag]) 129 | }, 130 | [TAG_REMOVE] (state, tag) { 131 | state.article.tagList = state.article.tagList.filter(t => t !== tag) 132 | }, 133 | [RESET_STATE] () { 134 | for (let f in state) { 135 | Vue.set(state, f, initialState[f]) 136 | } 137 | } 138 | } 139 | 140 | const getters = { 141 | [GET_ARTICLE] (state) { 142 | return state.article 143 | }, 144 | [GET_COMMENTS] (state) { 145 | return state.comments 146 | } 147 | } 148 | 149 | export default { 150 | state, 151 | actions, 152 | mutations, 153 | getters 154 | } 155 | -------------------------------------------------------------------------------- /src/store/auth.module.js: -------------------------------------------------------------------------------- 1 | import ApiService from '@/common/api.service' 2 | import JwtService from '@/common/jwt.service' 3 | import { LOGIN, LOGOUT, REGISTER, CHECK_AUTH, UPDATE_USER } from './actions.type' 4 | import { SET_AUTH, PURGE_AUTH, SET_ERROR } from './mutations.type' 5 | import { GET_CURRENT_USER, IS_AUTHENTICATED } from './getters.type' 6 | 7 | const state = { 8 | errors: null, 9 | user: {}, 10 | isAuthenticated: !!JwtService.getToken() 11 | } 12 | 13 | const getters = { 14 | [GET_CURRENT_USER] (state) { 15 | return state.user 16 | }, 17 | [IS_AUTHENTICATED] (state) { 18 | return state.isAuthenticated 19 | } 20 | } 21 | 22 | const actions = { 23 | [LOGIN] (context, credentials) { 24 | return new Promise((resolve) => { 25 | ApiService 26 | .post('users/login', {user: credentials}) 27 | .then(({data}) => { 28 | context.commit(SET_AUTH, data.user) 29 | resolve(data) 30 | }) 31 | .catch(({response}) => { 32 | context.commit(SET_ERROR, response.data.errors) 33 | }) 34 | }) 35 | }, 36 | [LOGOUT] (context) { 37 | context.commit(PURGE_AUTH) 38 | }, 39 | [REGISTER] (context, credentials) { 40 | return new Promise((resolve, reject) => { 41 | ApiService 42 | .post('users', {user: credentials}) 43 | .then(({data}) => { 44 | context.commit(SET_AUTH, data.user) 45 | resolve(data) 46 | }) 47 | .catch(({response}) => { 48 | context.commit(SET_ERROR, response.data.errors) 49 | }) 50 | }) 51 | }, 52 | [CHECK_AUTH] (context) { 53 | if (JwtService.getToken()) { 54 | ApiService.setHeader() 55 | ApiService 56 | .get('user') 57 | .then(({data}) => { 58 | context.commit(SET_AUTH, data.user) 59 | }) 60 | .catch(({response}) => { 61 | context.commit(SET_ERROR, response.data.errors) 62 | }) 63 | } else { 64 | context.commit(PURGE_AUTH) 65 | } 66 | }, 67 | [UPDATE_USER] (context, payload) { 68 | const {email, username, password, image, bio} = payload 69 | const user = { 70 | email, 71 | username, 72 | bio, 73 | image 74 | } 75 | if (password) { 76 | user.password = password 77 | } 78 | 79 | return ApiService 80 | .put('user', user) 81 | .then(({data}) => { 82 | context.commit(SET_AUTH, data.user) 83 | return data 84 | }) 85 | } 86 | } 87 | 88 | const mutations = { 89 | [SET_ERROR] (state, error) { 90 | state.errors = error 91 | }, 92 | [SET_AUTH] (state, user) { 93 | state.isAuthenticated = true 94 | state.user = user 95 | state.errors = {} 96 | JwtService.saveToken(state.user.token) 97 | }, 98 | [PURGE_AUTH] (state) { 99 | state.isAuthenticated = false 100 | state.user = {} 101 | state.errors = {} 102 | JwtService.destroyToken() 103 | } 104 | } 105 | 106 | export default { 107 | state, 108 | actions, 109 | mutations, 110 | getters 111 | } 112 | -------------------------------------------------------------------------------- /src/store/getters.type.js: -------------------------------------------------------------------------------- 1 | export const GET_ARTICLE = 'getArticle' 2 | export const GET_ARTICLE_COUNT = 'getArticleCount' 3 | export const GET_ARTICLES = 'getArticles' 4 | export const GET_ARTICLES_IS_LOADING = 'getArticlesIsLoading' 5 | export const GET_CURRENT_USER = 'getCurrentUser' 6 | export const GET_PROFILE = 'getProfile' 7 | export const GET_TAGS = 'getTags' 8 | export const GET_COMMENTS = 'getComments' 9 | export const IS_AUTHENTICATED = 'isAuthenticated' 10 | -------------------------------------------------------------------------------- /src/store/home.module.js: -------------------------------------------------------------------------------- 1 | import { 2 | TagsService, 3 | ArticlesService 4 | } from '@/common/api.service' 5 | import { 6 | GET_ARTICLE_COUNT, 7 | GET_ARTICLES, 8 | GET_ARTICLES_IS_LOADING, 9 | GET_TAGS 10 | } from './getters.type' 11 | import { 12 | FETCH_ARTICLES, 13 | FETCH_TAGS 14 | } from './actions.type' 15 | import { 16 | FETCH_START, 17 | FETCH_END, 18 | SET_TAGS, 19 | UPDATE_ARTICLE_IN_LIST 20 | } from './mutations.type' 21 | 22 | const state = { 23 | tags: [], 24 | articles: [], 25 | isLoading: true, 26 | articlesCount: 0 27 | } 28 | 29 | const getters = { 30 | [GET_ARTICLE_COUNT] (state) { 31 | return state.articlesCount 32 | }, 33 | [GET_ARTICLES] (state) { 34 | return state.articles 35 | }, 36 | [GET_ARTICLES_IS_LOADING] (state) { 37 | return state.isLoading 38 | }, 39 | [GET_TAGS] (state) { 40 | return state.tags 41 | } 42 | } 43 | 44 | const actions = { 45 | [FETCH_ARTICLES] ({ commit }, params) { 46 | commit(FETCH_START) 47 | return ArticlesService.query(params.type, params.filters) 48 | .then(({ data }) => { 49 | commit(FETCH_END, data) 50 | }) 51 | .catch((error) => { 52 | throw new Error(error) 53 | }) 54 | }, 55 | [FETCH_TAGS] ({ commit }) { 56 | return TagsService.get() 57 | .then(({ data }) => { 58 | commit(SET_TAGS, data.tags) 59 | }) 60 | .catch((error) => { 61 | throw new Error(error) 62 | }) 63 | } 64 | } 65 | 66 | /* eslint no-param-reassign: ["error", { "props": false }] */ 67 | const mutations = { 68 | [FETCH_START] (state) { 69 | state.isLoading = true 70 | }, 71 | [FETCH_END] (state, { articles, articlesCount }) { 72 | state.articles = articles 73 | state.articlesCount = articlesCount 74 | state.isLoading = false 75 | }, 76 | [SET_TAGS] (state, tags) { 77 | state.tags = tags 78 | }, 79 | [UPDATE_ARTICLE_IN_LIST] (state, data) { 80 | state.articles = state.articles.map((article) => { 81 | if (article.slug !== data.slug) { return article } 82 | // We could just return data, but it seems dangerous to 83 | // mix the results of different api calls, so we 84 | // protect ourselves by copying the information. 85 | article.favorited = data.favorited 86 | article.favoritesCount = data.favoritesCount 87 | return article 88 | }) 89 | } 90 | } 91 | 92 | export default { 93 | state, 94 | getters, 95 | actions, 96 | mutations 97 | } 98 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import home from './home.module' 5 | import auth from './auth.module' 6 | import article from './article.module' 7 | import profile from './profile.module' 8 | 9 | Vue.use(Vuex) 10 | 11 | export default new Vuex.Store({ 12 | modules: { 13 | home, 14 | auth, 15 | article, 16 | profile 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /src/store/mutations.type.js: -------------------------------------------------------------------------------- 1 | export const FETCH_END = 'setArticles' 2 | export const FETCH_START = 'setLoading' 3 | export const PURGE_AUTH = 'logOut' 4 | export const SET_ARTICLE = 'setArticle' 5 | export const SET_AUTH = 'setUser' 6 | export const SET_COMMENTS = 'setComments' 7 | export const SET_ERROR = 'setError' 8 | export const SET_PROFILE = 'setProfile' 9 | export const SET_TAGS = 'setTags' 10 | export const TAG_ADD = 'addTag' 11 | export const TAG_REMOVE = 'removeTag' 12 | export const UPDATE_ARTICLE_IN_LIST = 'updateAricleInList' 13 | export const RESET_STATE = 'resetModuleState' 14 | -------------------------------------------------------------------------------- /src/store/profile.module.js: -------------------------------------------------------------------------------- 1 | import ApiService from '@/common/api.service' 2 | import { GET_PROFILE } from './getters.type' 3 | import { FETCH_PROFILE, FETCH_PROFILE_FOLLOW, FETCH_PROFILE_UNFOLLOW } from './actions.type' 4 | import { SET_PROFILE } from './mutations.type' 5 | 6 | const state = { 7 | errors: {}, 8 | profile: {} 9 | } 10 | 11 | const getters = { 12 | [GET_PROFILE] (state) { 13 | return state.profile 14 | } 15 | } 16 | 17 | const actions = { 18 | [FETCH_PROFILE] (context, payload) { 19 | const {username} = payload 20 | return ApiService 21 | .get('profiles', username) 22 | .then(({data}) => { 23 | context.commit(SET_PROFILE, data.profile) 24 | return data 25 | }) 26 | .catch(({response}) => { 27 | // #todo SET_ERROR cannot work in multiple states 28 | // context.commit(SET_ERROR, response.data.errors) 29 | }) 30 | }, 31 | [FETCH_PROFILE_FOLLOW] (context, payload) { 32 | const {username} = payload 33 | return ApiService 34 | .post(`profiles/${username}/follow`) 35 | .then(({data}) => { 36 | context.commit(SET_PROFILE, data.profile) 37 | return data 38 | }) 39 | .catch(({response}) => { 40 | // #todo SET_ERROR cannot work in multiple states 41 | // context.commit(SET_ERROR, response.data.errors) 42 | }) 43 | }, 44 | [FETCH_PROFILE_UNFOLLOW] (context, payload) { 45 | const {username} = payload 46 | return ApiService 47 | .delete(`profiles/${username}/follow`) 48 | .then(({data}) => { 49 | context.commit(SET_PROFILE, data.profile) 50 | return data 51 | }) 52 | .catch(({response}) => { 53 | // #todo SET_ERROR cannot work in multiple states 54 | // context.commit(SET_ERROR, response.data.errors) 55 | }) 56 | } 57 | } 58 | 59 | const mutations = { 60 | // [SET_ERROR] (state, error) { 61 | // state.errors = error 62 | // }, 63 | [SET_PROFILE] (state, profile) { 64 | state.profile = profile 65 | state.errors = {} 66 | } 67 | } 68 | 69 | export default { 70 | state, 71 | actions, 72 | mutations, 73 | getters 74 | } 75 | -------------------------------------------------------------------------------- /src/store/settings.module.js: -------------------------------------------------------------------------------- 1 | import { ArticlesService, CommentsService } from '@/common/api.service' 2 | import { FETCH_ARTICLE, FETCH_COMMENTS } from './actions.type' 3 | import { SET_ARTICLE, SET_COMMENTS } from './mutations.type' 4 | 5 | export const state = { 6 | article: {}, 7 | comments: [] 8 | } 9 | 10 | export const actions = { 11 | [FETCH_ARTICLE] (context, articleSlug) { 12 | return ArticlesService.get(articleSlug) 13 | .then(({ data }) => { 14 | context.commit(SET_ARTICLE, data.article) 15 | }) 16 | .catch((error) => { 17 | throw new Error(error) 18 | }) 19 | }, 20 | [FETCH_COMMENTS] (context, articleSlug) { 21 | return CommentsService.get(articleSlug) 22 | .then(({ data }) => { 23 | context.commit(SET_COMMENTS, data.comments) 24 | }) 25 | .catch((error) => { 26 | throw new Error(error) 27 | }) 28 | } 29 | } 30 | 31 | /* eslint no-param-reassign: ["error", { "props": false }] */ 32 | export const mutations = { 33 | [SET_ARTICLE] (state, article) { 34 | state.article = article 35 | }, 36 | [SET_COMMENTS] (state, comments) { 37 | state.comments = comments 38 | } 39 | } 40 | 41 | export default { 42 | state, 43 | actions, 44 | mutations 45 | } 46 | -------------------------------------------------------------------------------- /src/views/Article.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 113 | -------------------------------------------------------------------------------- /src/views/ArticleEdit.vue: -------------------------------------------------------------------------------- 1 | 64 | 148 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 87 | -------------------------------------------------------------------------------- /src/views/HomeGlobal.vue: -------------------------------------------------------------------------------- 1 | 6 | 16 | -------------------------------------------------------------------------------- /src/views/HomeMyFeed.vue: -------------------------------------------------------------------------------- 1 | 6 | 16 | -------------------------------------------------------------------------------- /src/views/HomeTag.vue: -------------------------------------------------------------------------------- 1 | 6 | 21 | -------------------------------------------------------------------------------- /src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 43 | 68 | -------------------------------------------------------------------------------- /src/views/Profile.vue: -------------------------------------------------------------------------------- 1 | 68 | 106 | -------------------------------------------------------------------------------- /src/views/ProfileArticles.vue: -------------------------------------------------------------------------------- 1 | 6 | 21 | -------------------------------------------------------------------------------- /src/views/ProfileFavorited.vue: -------------------------------------------------------------------------------- 1 | 9 | 24 | -------------------------------------------------------------------------------- /src/views/Register.vue: -------------------------------------------------------------------------------- 1 | 34 | 63 | -------------------------------------------------------------------------------- /src/views/Settings.vue: -------------------------------------------------------------------------------- 1 | 49 | 78 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vilsbole/realworld-vue/4ef462384aab157c36557c7e80a4b1ddb3456913/static/.gitkeep -------------------------------------------------------------------------------- /static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vilsbole/realworld-vue/4ef462384aab157c36557c7e80a4b1ddb3456913/static/logo.png -------------------------------------------------------------------------------- /static/rwv-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vilsbole/realworld-vue/4ef462384aab157c36557c7e80a4b1ddb3456913/static/rwv-logo.png -------------------------------------------------------------------------------- /test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // the name of the method is the filename. 3 | // can be used in tests like this: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // for how to write custom assertions see 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | exports.assertion = function (selector, count) { 10 | this.message = 'Testing if element <' + selector + '> has count: ' + count 11 | this.expected = count 12 | this.pass = function (val) { 13 | return val === this.expected 14 | } 15 | this.value = function (res) { 16 | return res.value 17 | } 18 | this.command = function (cb) { 19 | var self = this 20 | return this.api.execute(function (selector) { 21 | return document.querySelectorAll(selector).length 22 | }, [selector], function (res) { 23 | cb.call(self, res) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | var config = require('../../config') 3 | 4 | // http://nightwatchjs.org/gettingstarted#settings-file 5 | module.exports = { 6 | src_folders: ['test/e2e/specs'], 7 | output_folder: 'test/e2e/reports', 8 | custom_assertions_path: ['test/e2e/custom-assertions'], 9 | 10 | selenium: { 11 | start_process: true, 12 | server_path: require('selenium-server').path, 13 | host: '127.0.0.1', 14 | port: 4444, 15 | cli_args: { 16 | 'webdriver.chrome.driver': require('chromedriver').path 17 | } 18 | }, 19 | 20 | test_settings: { 21 | default: { 22 | selenium_port: 4444, 23 | selenium_host: 'localhost', 24 | silent: true, 25 | globals: { 26 | devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port) 27 | } 28 | }, 29 | 30 | chrome: { 31 | desiredCapabilities: { 32 | browserName: 'chrome', 33 | javascriptEnabled: true, 34 | acceptSslCerts: true 35 | } 36 | }, 37 | 38 | firefox: { 39 | desiredCapabilities: { 40 | browserName: 'firefox', 41 | javascriptEnabled: true, 42 | acceptSslCerts: true 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server using production config 2 | process.env.NODE_ENV = 'testing' 3 | var server = require('../../build/dev-server.js') 4 | 5 | server.ready.then(() => { 6 | // 2. run the nightwatch test suite against it 7 | // to run in additional browsers: 8 | // 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings" 9 | // 2. add it to the --env flag below 10 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 11 | // For more information on Nightwatch's config file, see 12 | // http://nightwatchjs.org/guide#settings-file 13 | var opts = process.argv.slice(2) 14 | if (opts.indexOf('--config') === -1) { 15 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']) 16 | } 17 | if (opts.indexOf('--env') === -1) { 18 | opts = opts.concat(['--env', 'chrome']) 19 | } 20 | 21 | var spawn = require('cross-spawn') 22 | var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }) 23 | 24 | runner.on('exit', function (code) { 25 | server.close() 26 | process.exit(code) 27 | }) 28 | 29 | runner.on('error', function (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 (browser) { 6 | // automatically uses dev Server port from /config.index.js 7 | // default: http://localhost:8080 8 | // see nightwatch.conf.js 9 | const devServer = browser.globals.devServerURL 10 | 11 | browser 12 | .url(devServer) 13 | .waitForElementVisible('#app', 5000) 14 | .assert.elementPresent('.hello') 15 | .assert.containsText('h1', 'Welcome to Your Vue.js App') 16 | .assert.elementCount('img', 1) 17 | .end() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | Vue.config.productionTip = false 4 | 5 | // require all test files (files that ends with .spec.js) 6 | const testsContext = require.context('./specs', true, /\.spec$/) 7 | testsContext.keys().forEach(testsContext) 8 | 9 | // require all src files except main.js for coverage. 10 | // you can also change this to match only the subset of files that 11 | // you want coverage for. 12 | const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/) 13 | srcContext.keys().forEach(srcContext) 14 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This is a karma config file. For more details see 2 | // http://karma-runner.github.io/0.13/config/configuration-file.html 3 | // we are also using it with karma-webpack 4 | // https://github.com/webpack/karma-webpack 5 | 6 | var webpackConfig = require('../../build/webpack.test.conf') 7 | 8 | module.exports = function (config) { 9 | config.set({ 10 | // to run in additional browsers: 11 | // 1. install corresponding karma launcher 12 | // http://karma-runner.github.io/0.13/config/browsers.html 13 | // 2. add it to the `browsers` array below. 14 | browsers: ['PhantomJS'], 15 | frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'], 16 | reporters: ['spec', 'coverage'], 17 | // Babel only transpiles, for unit tests to work with PhantomJS 18 | // we also need to include a polyfill. 19 | // GH: https://github.com/vuejs-templates/webpack/issues/260 20 | files: [ 21 | '../../node_modules/babel-polyfill/dist/polyfill.js', 22 | './index.js' 23 | ], 24 | preprocessors: { 25 | './index.js': ['webpack', 'sourcemap'] 26 | }, 27 | webpack: webpackConfig, 28 | webpackMiddleware: { 29 | noInfo: true 30 | }, 31 | coverageReporter: { 32 | dir: './coverage', 33 | reporters: [ 34 | { type: 'lcov', subdir: '.' }, 35 | { type: 'text-summary' } 36 | ] 37 | } 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /test/unit/specs/Home.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'vue-test-utils' 2 | import Errors from '@/components/ListErrors.vue' 3 | 4 | describe('ListErrors.vue', () => { 5 | const example = { 6 | errors: { 7 | 'title': ['can\'t be blank'], 8 | 'body': ['can\'t be blank'] 9 | } 10 | } 11 | const wrap = (props) => { 12 | return mount(Errors, { propsData: props }) 13 | } 14 | 15 | it('compiles', () => { 16 | const wrapper = wrap(example) 17 | wrapper.isVueInstance().should.be.true 18 | }) 19 | 20 | it('renders the correct message', () => { 21 | const wrapper = wrap(example) 22 | wrapper.html().should.contain('title') 23 | }) 24 | }) 25 | --------------------------------------------------------------------------------