├── .babelrc
├── .deployment
├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── build
├── deploy.cmd
├── dev-server.js
├── web.config
├── webpack.base.config.js
├── webpack.client.config.js
├── webpack.server.config.js
└── webpack.test.config.js
├── index.html
├── package-lock.json
├── package.json
├── server.js
├── src
├── app.js
├── app.service.js
├── client-entry.js
├── event-bus.js
├── router.js
├── server-entry.js
├── theme
│ ├── AppFooter.vue
│ ├── AppHeader.vue
│ ├── Category.vue
│ ├── Layout.vue
│ ├── Login.vue
│ ├── NotFound.vue
│ └── Post.vue
└── vuex
│ ├── index.js
│ └── posts.js
└── test
└── unit
├── .eslintrc
├── index.js
├── karma.config.js
└── specs
├── Category.spec.js
└── Post.spec.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "es2015",
5 | {
6 | "modules": false
7 | }
8 | ],
9 | "stage-2"
10 | ],
11 | "ignore": [
12 | "node_modules/*"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/.deployment:
--------------------------------------------------------------------------------
1 | [config]
2 | command = build/deploy.cmd
3 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: 'babel-eslint',
4 | parserOptions: {
5 | sourceType: 'module'
6 | },
7 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
8 | extends: 'standard',
9 | // required to lint *.vue files
10 | plugins: [
11 | 'html'
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "7.5"
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Bill Stavroulakis
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/bstavroulakis/vue-spa)
2 |
--------------------------------------------------------------------------------
/build/deploy.cmd:
--------------------------------------------------------------------------------
1 | call npm install
2 | call npm run build
3 |
4 | call cd %DEPLOYMENT_TARGET%
5 | for /F "delims=" %%i in ('dir /b') do (rmdir "%%i" /s/q || del "%%i" /s/q)
6 |
7 | xcopy /d %DEPLOYMENT_SOURCE%\dist\* %DEPLOYMENT_TARGET%\dist /s /i
8 | xcopy /d %DEPLOYMENT_SOURCE%\index.html %DEPLOYMENT_TARGET%\index.html*
9 | xcopy /d %DEPLOYMENT_SOURCE%\server.js %DEPLOYMENT_TARGET%\server.js*
10 | xcopy /d %DEPLOYMENT_SOURCE%\package.json %DEPLOYMENT_TARGET%\package.json*
11 | xcopy /d %DEPLOYMENT_SOURCE%\build\web.config %DEPLOYMENT_TARGET%\web.config*
12 |
13 | call npm install --only=production
14 | echo Deployed.
15 |
--------------------------------------------------------------------------------
/build/dev-server.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 | const clientConfig = require('./webpack.client.config')
3 | const serverConfig = require('./webpack.server.config')
4 | const MFS = require('memory-fs')
5 | const path = require("path")
6 |
7 | module.exports = function setupDevServer (app, onUpdate) {
8 | clientConfig.entry.app = [
9 | 'webpack-hot-middleware/client',
10 | clientConfig.entry.app
11 | ]
12 | clientConfig.plugins.push(
13 | new webpack.HotModuleReplacementPlugin(),
14 | new webpack.NoEmitOnErrorsPlugin()
15 | )
16 | const clientCompiler = webpack(clientConfig)
17 | app.use(
18 | require('webpack-dev-middleware')(clientCompiler, {
19 | stats: {
20 | colors: true
21 | }
22 | })
23 | )
24 | app.use(require('webpack-hot-middleware')(clientCompiler))
25 |
26 | const serverCompiler = webpack(serverConfig)
27 | const mfs = new MFS()
28 | const outputPath = path.join(serverConfig.output.path, 'server/main.js')
29 | serverCompiler.outputFileSystem = mfs
30 | serverCompiler.watch({}, () => {
31 | onUpdate(mfs.readFileSync(outputPath, 'utf-8'))
32 | })
33 | }
34 |
--------------------------------------------------------------------------------
/build/web.config:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/build/webpack.base.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const config = {
4 | entry: {
5 | app: path.resolve(__dirname, '../src/client-entry.js'),
6 | vendor: ['vue', 'vue-router', 'vuex', 'axios']
7 | },
8 | module: {
9 | rules: [
10 | {
11 | enforce: 'pre',
12 | test: /(\.js$)|(\.vue$)/,
13 | loader: 'eslint-loader',
14 | exclude: /node_modules/
15 | },
16 | {
17 | test: /\.vue$/,
18 | loader: 'vue-loader',
19 | options: {
20 | css: 'css-loader',
21 | 'scss': 'css-loader|sass-loader'
22 | }
23 | },
24 | {
25 | test: /\.js$/,
26 | loader: 'babel-loader',
27 | exclude: /node_modules/
28 | }
29 | ]
30 | },
31 | output: {
32 | path: path.resolve(__dirname, '../dist'),
33 | publicPath: '/',
34 | filename: 'assets/js/[name].js'
35 | }
36 | }
37 |
38 | module.exports = config
39 |
--------------------------------------------------------------------------------
/build/webpack.client.config.js:
--------------------------------------------------------------------------------
1 | const base = require('./webpack.base.config')
2 | const webpack = require('webpack')
3 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
4 |
5 | const config = Object.assign({}, base, {
6 | plugins: (base.plugins || []).concat([
7 | new webpack.optimize.CommonsChunkPlugin({
8 | name: 'vendor',
9 | filename: 'assets/js/[name].js'
10 | })
11 | ])
12 | })
13 |
14 | config.module.rules
15 | .filter(x => { return x.loader == 'vue-loader' })
16 | .forEach(x => x.options.extractCSS = true)
17 |
18 | config.plugins.push(
19 | new ExtractTextPlugin('assets/styles.css')
20 | )
21 |
22 | if (process.env.NODE_ENV === 'production') {
23 | config.plugins.push(
24 | new webpack.DefinePlugin({
25 | 'process.env': {
26 | NODE_ENV: '"production"'
27 | }
28 | }),
29 | new webpack.optimize.UglifyJsPlugin({
30 | compress: {
31 | warnings: false
32 | }
33 | })
34 | )
35 | }
36 |
37 | module.exports = config
38 |
--------------------------------------------------------------------------------
/build/webpack.server.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 | const base = require('./webpack.base.config')
4 | const nodeExternals = require('webpack-node-externals')
5 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
6 |
7 | const config = Object.assign({}, base, {
8 | entry: path.resolve(__dirname, '../src/server-entry.js'),
9 | target: 'node',
10 | devtool: 'source-map',
11 | output: {
12 | path: path.resolve(__dirname, '../dist'),
13 | filename: 'server/[name].js',
14 | libraryTarget: 'commonjs2'
15 | },
16 | externals: nodeExternals({
17 | whitelist: /\.css$/
18 | }),
19 | plugins: [
20 | new ExtractTextPlugin('server/styles.css')
21 | ]
22 | })
23 |
24 | module.exports = config
25 |
--------------------------------------------------------------------------------
/build/webpack.test.config.js:
--------------------------------------------------------------------------------
1 | const base = require('./webpack.base.config.js')
2 |
3 | let config = Object.assign({}, base, {})
4 |
5 | // no need for app entry during tests
6 | delete config.entry
7 |
8 | module.exports = config
9 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vue SPA
7 |
8 |
9 |
10 |
11 |
12 | {{ APP }}
13 | {{ STATE }}
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-spa",
3 | "version": "1.0.0",
4 | "description": "",
5 | "scripts": {
6 | "start": "node server",
7 | "dev": "rimraf ./dist && cross-env NODE_ENV=development node server",
8 | "test": "karma start test/unit/karma.config.js --single-run",
9 | "build:client": "cross-env NODE_ENV=production webpack --config ./build/webpack.client.config.js --progress --hide-modules",
10 | "build:server": "cross-env NODE_ENV=production webpack --config ./build/webpack.server.config.js --progress --hide-modules",
11 | "build": "rimraf ./dist && npm run build:client && npm run build:server",
12 | "start:prod": "cross-env NODE_ENV=production npm run start"
13 | },
14 | "keywords": [],
15 | "author": "",
16 | "license": "MIT",
17 | "dependencies": {
18 | "axios": "^0.16.2",
19 | "bulma": "^0.5.1",
20 | "express": "^4.15.4",
21 | "serialize-javascript": "^1.3.0",
22 | "vue": "^2.4.2",
23 | "vue-router": "^2.7.0",
24 | "vue-server-renderer": "^2.4.2",
25 | "vuex": "^2.4.0"
26 | },
27 | "devDependencies": {
28 | "babel-core": "^6.26.0",
29 | "babel-eslint": "^7.2.3",
30 | "babel-loader": "^7.1.2",
31 | "babel-preset-es2015": "^6.24.1",
32 | "babel-preset-stage-2": "^6.24.1",
33 | "chai": "^4.1.2",
34 | "cross-env": "^5.0.5",
35 | "css-loader": "^0.28.5",
36 | "eslint": "^4.5.0",
37 | "eslint-config-standard": "^10.2.1",
38 | "eslint-loader": "^1.9.0",
39 | "eslint-plugin-html": "^3.2.0",
40 | "eslint-plugin-import": "^2.7.0",
41 | "eslint-plugin-node": "^5.1.1",
42 | "eslint-plugin-promise": "^3.5.0",
43 | "eslint-plugin-standard": "^3.0.1",
44 | "extract-text-webpack-plugin": "^3.0.0",
45 | "karma": "^1.7.1",
46 | "karma-mocha": "^1.3.0",
47 | "karma-phantomjs-launcher": "^1.0.4",
48 | "karma-sinon-chai": "^1.3.2",
49 | "karma-webpack": "^2.0.4",
50 | "mocha": "^3.5.3",
51 | "node-sass": "^4.5.3",
52 | "sass-loader": "^6.0.6",
53 | "sinon": "^4.0.0",
54 | "sinon-chai": "^2.14.0",
55 | "vue-loader": "^13.0.2",
56 | "vue-template-compiler": "^2.4.2",
57 | "webpack": "^3.5.5",
58 | "webpack-dev-middleware": "^1.12.0",
59 | "webpack-hot-middleware": "^2.18.2",
60 | "webpack-node-externals": "^1.6.0"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const app = express();
3 | const fs = require("fs");
4 | const path = require("path");
5 | const serialize = require('serialize-javascript');
6 | const { createBundleRenderer } = require('vue-server-renderer');
7 | const isProd = typeof process.env.NODE_ENV !== 'undefined' && (process.env.NODE_ENV === 'production')
8 | let renderer;
9 |
10 | const indexHTML = (() => {
11 | return fs.readFileSync(path.resolve(__dirname, "./index.html"), "utf-8");
12 | })();
13 |
14 | if (isProd) {
15 | app.use("/", express.static(path.resolve(__dirname, "./dist")));
16 | } else {
17 | app.use("/dist", express.static(path.resolve(__dirname, "./dist")));
18 | }
19 |
20 | if (isProd) {
21 | const bundlePath = path.resolve(__dirname, './dist/server/main.js')
22 | renderer = createBundleRenderer(fs.readFileSync(bundlePath, 'utf-8'))
23 | } else {
24 | require("./build/dev-server")(app, bundle => {
25 | renderer = createBundleRenderer(bundle)
26 | });
27 | }
28 |
29 | app.get("*", (req, res) => {
30 | const context = { url: req.url };
31 | renderer.renderToString(context, (err, html) => {
32 | if (err) {
33 | return res.status(500).send('Server Error')
34 | }
35 | html = indexHTML.replace('{{ APP }}', html)
36 | html = html.replace('{{ STATE }}',
37 | ``)
38 | res.write(html);
39 | res.end();
40 | })
41 | });
42 |
43 | const port = process.env.PORT || 3000;
44 | app.listen(port, () => {
45 | console.log(`server started at http://localhost:${port}`);
46 | });
47 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import store from './vuex/index.js'
3 | import AppLayout from './theme/Layout.vue'
4 | import router from './router'
5 |
6 | const app = new Vue({
7 | router,
8 | ...AppLayout,
9 | store
10 | })
11 |
12 | export { app, router, store }
13 |
--------------------------------------------------------------------------------
/src/app.service.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | axios.defaults.baseURL = 'https://api.fullstackweekly.com'
4 |
5 | axios.interceptors.request.use(function (config) {
6 | if (typeof window === 'undefined') {
7 | return config
8 | }
9 | const token = window.localStorage.getItem('token')
10 | if (token) {
11 | config.headers.Authorization = `Bearer ${token}`
12 | }
13 |
14 | return config
15 | })
16 |
17 | const appService = {
18 | getPosts (categoryId) {
19 | return new Promise((resolve) => {
20 | axios.get(`/wp-json/wp/v2/posts?categories=${categoryId}&per_page=6`)
21 | .then(response => {
22 | resolve(response.data)
23 | })
24 | })
25 | },
26 | getProfile () {
27 | return new Promise((resolve) => {
28 | axios.get('/services/profile.php')
29 | .then(response => {
30 | resolve(response.data)
31 | })
32 | })
33 | },
34 | login (credentials) {
35 | return new Promise((resolve, reject) => {
36 | axios.post('/services/auth.php', credentials)
37 | .then(response => {
38 | resolve(response.data)
39 | }).catch(response => {
40 | reject(response.status)
41 | })
42 | })
43 | }
44 | }
45 |
46 | export default appService
47 |
--------------------------------------------------------------------------------
/src/client-entry.js:
--------------------------------------------------------------------------------
1 | import { app, router } from './app'
2 |
3 | router.onReady(() => {
4 | app.$mount('#app')
5 | })
6 |
--------------------------------------------------------------------------------
/src/event-bus.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | const eventBus = new Vue()
4 |
5 | export default eventBus
6 |
--------------------------------------------------------------------------------
/src/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueRouter from 'vue-router'
3 | import Category from './theme/Category.vue'
4 | import Login from './theme/Login.vue'
5 | import NotFound from './theme/NotFound.vue'
6 |
7 | // const Category = () => System.import('./theme/Category.vue')
8 | // const Login = () => System.import('./theme/Login.vue')
9 | // const NotFound = () => System.import('./theme/NotFound.vue')
10 |
11 | Vue.use(VueRouter)
12 |
13 | const router = new VueRouter({
14 | mode: 'history',
15 | linkActiveClass: 'is-active',
16 | scrollBehavior: (to, from, savedPosition) => ({ y: 0 }),
17 | routes: [
18 | { path: '/login', component: Login },
19 | { path: '/category/:id', name: 'category', component: Category },
20 | { path: '/', redirect: '/category/front-end' },
21 | { path: '*', component: NotFound }
22 | ]
23 | })
24 |
25 | export default router
26 |
--------------------------------------------------------------------------------
/src/server-entry.js:
--------------------------------------------------------------------------------
1 | import { app, router, store } from './app'
2 |
3 | export default context => {
4 | router.push(context.url)
5 | return Promise.all(router.getMatchedComponents().map(component => {
6 | if (component.asyncData) {
7 | return component.asyncData(store, router.currentRoute)
8 | }
9 | })).then(() => {
10 | context.initialState = store.state
11 | return app
12 | })
13 | }
14 |
--------------------------------------------------------------------------------
/src/theme/AppFooter.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
--------------------------------------------------------------------------------
/src/theme/AppHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
26 |
--------------------------------------------------------------------------------
/src/theme/Category.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
48 |
--------------------------------------------------------------------------------
/src/theme/Layout.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
22 |
30 |
--------------------------------------------------------------------------------
/src/theme/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hello authenticated user!
5 |
8 |
9 |
10 |
Login
11 |
12 |
13 |
14 |
15 |
23 |
24 |
25 |
26 |
27 |
28 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
80 |
--------------------------------------------------------------------------------
/src/theme/NotFound.vue:
--------------------------------------------------------------------------------
1 | Oops, page not found!
2 |
--------------------------------------------------------------------------------
/src/theme/Post.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
18 |
30 |
--------------------------------------------------------------------------------
/src/vuex/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import appService from '../app.service.js'
4 | import postsModule from './posts'
5 |
6 | Vue.use(Vuex)
7 |
8 | const state = {
9 | isAuthenticated: false
10 | }
11 |
12 | const store = new Vuex.Store({
13 | modules: {
14 | postsModule
15 | },
16 | state,
17 | getters: {
18 | isAuthenticated: (state) => {
19 | return state.isAuthenticated
20 | }
21 | },
22 | actions: {
23 | logout (context) {
24 | context.commit('logout')
25 | },
26 | login (context, credentials) {
27 | return new Promise((resolve) => {
28 | appService.login(credentials)
29 | .then((data) => {
30 | context.commit('login', data)
31 |
32 | resolve()
33 | })
34 | .catch(() => {
35 | if (typeof window !== 'undefined') { window.alert('Could not login!') }
36 | })
37 | })
38 | }
39 | },
40 | mutations: {
41 | logout (state) {
42 | if (typeof window !== 'undefined') {
43 | window.localStorage.setItem('token', null)
44 | window.localStorage.setItem('tokenExpiration', null)
45 | }
46 | state.isAuthenticated = false
47 | },
48 | login (state, token) {
49 | if (typeof window !== 'undefined') {
50 | window.localStorage.setItem('token', token.token)
51 | window.localStorage.setItem('tokenExpiration', token.expiration)
52 | }
53 | state.isAuthenticated = true
54 | }
55 | }
56 | })
57 |
58 | if (typeof window !== 'undefined') {
59 | document.addEventListener('DOMContentLoaded', function (event) {
60 | let expiration = window.localStorage.getItem('tokenExpiration')
61 | var unixTimestamp = new Date().getTime() / 1000
62 | if (expiration !== null && parseInt(expiration) - unixTimestamp > 0) {
63 | store.state.isAuthenticated = true
64 | }
65 | })
66 | }
67 |
68 | export default store
69 |
--------------------------------------------------------------------------------
/src/vuex/posts.js:
--------------------------------------------------------------------------------
1 | import appService from '../app.service.js'
2 | const defaultState = {
3 | posts: [],
4 | categoryId: 0
5 | }
6 |
7 | const inBrowser = typeof window !== 'undefined'
8 | const state = (inBrowser && window.__INITIAL_STATE__) ? window.__INITIAL_STATE__.postsModule : defaultState
9 |
10 | const getters = {
11 | posts: state => state.posts
12 | }
13 |
14 | const actions = {
15 | updateCategory (context, categoryId) {
16 | return appService.getPosts(categoryId).then(data => {
17 | context.commit('updateCategory', { categoryId, posts: data })
18 | })
19 | }
20 | }
21 |
22 | const mutations = {
23 | updateCategory (state, category) {
24 | state.categoryId = category.categoryId
25 | state.posts = category.posts
26 | }
27 | }
28 |
29 | export default {
30 | namespaced: true,
31 | state,
32 | getters,
33 | actions,
34 | mutations
35 | }
36 |
--------------------------------------------------------------------------------
/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 | const testsContext = require.context('./specs', true, /\.spec$/)
2 | testsContext.keys().forEach(testsContext)
3 |
--------------------------------------------------------------------------------
/test/unit/karma.config.js:
--------------------------------------------------------------------------------
1 |
2 | var webpackConfig = require('../../build/webpack.test.config.js')
3 | module.exports = function (config) {
4 | config.set({
5 | browsers: ['PhantomJS'],
6 | frameworks: ['mocha', 'sinon-chai'],
7 | files: ['./index.js'],
8 | preprocessors: {
9 | './index.js': ['webpack']
10 | },
11 | plugins: [
12 | 'karma-mocha',
13 | 'karma-sinon-chai',
14 | 'karma-phantomjs-launcher',
15 | 'karma-webpack'
16 | ],
17 | webpack: webpackConfig,
18 | webpackMiddleware: {
19 | noInfo: true
20 | }
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/test/unit/specs/Category.spec.js:
--------------------------------------------------------------------------------
1 | import 'es6-promise/auto'
2 | import Vue from 'vue'
3 | import store from '../../../src/vuex/index.js'
4 | import VueRouter from 'vue-router'
5 | import Category from '../../../src/theme/Category.vue'
6 |
7 | describe('Category.vue', () => {
8 | it('should load front-end links', done => {
9 | Vue.use(VueRouter)
10 | const router = new VueRouter({
11 | routes: [
12 | { path: '/', component: Category }
13 | ]
14 | })
15 |
16 | const vm = new Vue({
17 | el: document.createElement('div'),
18 | router,
19 | store,
20 | render: h => h('router-view')
21 | })
22 |
23 | store.watch(
24 | (state) => {
25 | return state.postsModule.posts
26 | },
27 | function () {
28 | expect(vm.$el.querySelectorAll('.column').length).to.equal(6)
29 | done()
30 | }
31 | )
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/test/unit/specs/Post.spec.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Post from '../../../src/theme/Post.vue'
3 |
4 | describe('Post.vue', () => {
5 | const createComponent = () => {
6 | const PostConstructor = Vue.extend(Post)
7 | const comp = new PostConstructor({
8 | propsData: {
9 | link: 'http://www.pluralsight.com'
10 | }
11 | }).$mount()
12 | return comp
13 | }
14 |
15 | it('should render the link', () => {
16 | const comp = createComponent()
17 | expect(comp.$el.querySelector('.card-footer-item').getAttribute('href'))
18 | .to.equal('http://www.pluralsight.com')
19 | })
20 | it('should update element\'s href when property link changes', (done) => {
21 | const comp = createComponent()
22 | expect(comp.$el.querySelector('.card-footer-item').getAttribute('href'))
23 | .to.equal('http://www.pluralsight.com')
24 |
25 | comp.link = 'http://fullstackweekly.com'
26 | Vue.nextTick(() => {
27 | expect(comp.$el.querySelector('.card-footer-item').getAttribute('href'))
28 | .to.equal('http://fullstackweekly.com')
29 | done()
30 | })
31 | })
32 | })
33 |
--------------------------------------------------------------------------------