├── .babelrc
├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── README.md
├── build
├── build.js
├── css-loaders.js
├── dev-client.js
├── dev-server.js
├── webpack.base.conf.js
├── webpack.dev.conf.js
└── webpack.prod.conf.js
├── config.js
├── docker-compose.yml
├── index.html
├── package.json
├── src
├── App.vue
├── Chat.vue
├── Login.vue
├── assets
│ └── logo.png
├── components
│ ├── InputBar.vue
│ ├── Message.vue
│ └── Messages.vue
├── main.js
├── routes.js
├── services
│ └── kuzzle.js
├── store
│ ├── messages.js
│ └── user.js
└── style
│ ├── global.css
│ └── login.css
├── static
└── .gitkeep
└── test
├── e2e
├── custom-assertions
│ └── elementCount.js
├── nightwatch.conf.js
├── runner.js
└── specs
│ └── test.js
└── unit
├── .eslintrc
├── index.js
├── karma.conf.js
└── specs
└── Hello.spec.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-2"],
3 | "plugins": ["transform-runtime"],
4 | "comments": false
5 | }
6 |
--------------------------------------------------------------------------------
/.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 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
4 | extends: 'standard',
5 | // required to lint *.vue files
6 | plugins: [
7 | 'html'
8 | ],
9 | // add your custom rules here
10 | 'rules': {
11 | // allow paren-less arrow functions
12 | 'arrow-parens': 0,
13 | // allow debugger during development
14 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log
5 | selenium-debug.log
6 | test/unit/coverage
7 | test/e2e/reports
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Building a Slack-clone with Vue.js and Kuzzle.io
2 |
3 | This repository contains the code you can build by following the tutorial at https://medium.com/@xbill82/d0c56ef9e6cb
4 |
--------------------------------------------------------------------------------
/build/build.js:
--------------------------------------------------------------------------------
1 | // https://github.com/shelljs/shelljs
2 | require('shelljs/global')
3 | env.NODE_ENV = 'production'
4 |
5 | var path = require('path')
6 | var config = require('../config')
7 | var ora = require('ora')
8 | var webpack = require('webpack')
9 | var webpackConfig = require('./webpack.prod.conf')
10 |
11 | console.log(
12 | ' Tip:\n' +
13 | ' Built files are meant to be served over an HTTP server.\n' +
14 | ' Opening index.html over file:// won\'t work.\n'
15 | )
16 |
17 | var spinner = ora('building for production...')
18 | spinner.start()
19 |
20 | var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
21 | rm('-rf', assetsPath)
22 | mkdir('-p', assetsPath)
23 | cp('-R', 'static/', assetsPath)
24 |
25 | webpack(webpackConfig, function (err, stats) {
26 | spinner.stop()
27 | if (err) throw err
28 | process.stdout.write(stats.toString({
29 | colors: true,
30 | modules: false,
31 | children: false,
32 | chunks: false,
33 | chunkModules: false
34 | }) + '\n')
35 | })
36 |
--------------------------------------------------------------------------------
/build/css-loaders.js:
--------------------------------------------------------------------------------
1 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
2 |
3 | module.exports = function (options) {
4 | options = options || {}
5 | // generate loader string to be used with extract text plugin
6 | function generateLoaders (loaders) {
7 | var sourceLoader = loaders.map(function (loader) {
8 | var extraParamChar
9 | if (/\?/.test(loader)) {
10 | loader = loader.replace(/\?/, '-loader?')
11 | extraParamChar = '&'
12 | } else {
13 | loader = loader + '-loader'
14 | extraParamChar = '?'
15 | }
16 | return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '')
17 | }).join('!')
18 |
19 | if (options.extract) {
20 | return ExtractTextPlugin.extract('vue-style-loader', sourceLoader)
21 | } else {
22 | return ['vue-style-loader', sourceLoader].join('!')
23 | }
24 | }
25 |
26 | // http://vuejs.github.io/vue-loader/configurations/extract-css.html
27 | return {
28 | css: generateLoaders(['css']),
29 | postcss: generateLoaders(['css']),
30 | less: generateLoaders(['css', 'less']),
31 | sass: generateLoaders(['css', 'sass?indentedSyntax']),
32 | scss: generateLoaders(['css', 'sass']),
33 | stylus: generateLoaders(['css', 'stylus']),
34 | styl: generateLoaders(['css', 'stylus'])
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/build/dev-client.js:
--------------------------------------------------------------------------------
1 | require('eventsource-polyfill')
2 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
3 |
4 | hotClient.subscribe(function (event) {
5 | if (event.action === 'reload') {
6 | window.location.reload()
7 | }
8 | })
9 |
--------------------------------------------------------------------------------
/build/dev-server.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var express = require('express')
3 | var webpack = require('webpack')
4 | var config = require('../config')
5 | var proxyMiddleware = require('http-proxy-middleware')
6 | var webpackConfig = process.env.NODE_ENV === 'testing'
7 | ? require('./webpack.prod.conf')
8 | : require('./webpack.dev.conf')
9 |
10 | // default port where dev server listens for incoming traffic
11 | var port = process.env.PORT || config.dev.port
12 | // Define HTTP proxies to your custom API backend
13 | // https://github.com/chimurai/http-proxy-middleware
14 | var proxyTable = config.dev.proxyTable
15 |
16 | var app = express()
17 | var compiler = webpack(webpackConfig)
18 |
19 | var devMiddleware = require('webpack-dev-middleware')(compiler, {
20 | publicPath: webpackConfig.output.publicPath,
21 | stats: {
22 | colors: true,
23 | chunks: false
24 | }
25 | })
26 |
27 | var hotMiddleware = require('webpack-hot-middleware')(compiler)
28 | // force page reload when html-webpack-plugin template changes
29 | compiler.plugin('compilation', function (compilation) {
30 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
31 | hotMiddleware.publish({ action: 'reload' })
32 | cb()
33 | })
34 | })
35 |
36 | // proxy api requests
37 | Object.keys(proxyTable).forEach(function (context) {
38 | var options = proxyTable[context]
39 | if (typeof options === 'string') {
40 | options = { target: options }
41 | }
42 | app.use(proxyMiddleware(context, options))
43 | })
44 |
45 | // handle fallback for HTML5 history API
46 | app.use(require('connect-history-api-fallback')())
47 |
48 | // serve webpack bundle output
49 | app.use(devMiddleware)
50 |
51 | // enable hot-reload and state-preserving
52 | // compilation error display
53 | app.use(hotMiddleware)
54 |
55 | // serve pure static assets
56 | var staticPath = path.join(config.build.assetsPublicPath, config.build.assetsSubDirectory)
57 | app.use(staticPath, express.static('./static'))
58 |
59 | module.exports = app.listen(port, function (err) {
60 | if (err) {
61 | console.log(err)
62 | return
63 | }
64 | console.log('Listening at http://localhost:' + port + '\n')
65 | })
66 |
--------------------------------------------------------------------------------
/build/webpack.base.conf.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var config = require('../config')
3 | var cssLoaders = require('./css-loaders')
4 | var projectRoot = path.resolve(__dirname, '../')
5 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
6 |
7 | module.exports = {
8 | entry: {
9 | app: './src/main.js'
10 | },
11 | output: {
12 | path: config.build.assetsRoot,
13 | publicPath: config.build.assetsPublicPath,
14 | filename: '[name].js'
15 | },
16 | resolve: {
17 | extensions: ['', '.js', '.vue'],
18 | fallback: [path.join(__dirname, '../node_modules')],
19 | alias: {
20 | 'src': path.resolve(__dirname, '../src')
21 | }
22 | },
23 | resolveLoader: {
24 | fallback: [path.join(__dirname, '../node_modules')]
25 | },
26 | module: {
27 | preLoaders: [
28 | {
29 | test: /\.vue$/,
30 | loader: 'eslint',
31 | include: projectRoot,
32 | exclude: /node_modules/
33 | },
34 | {
35 | test: /\.js$/,
36 | loader: 'eslint',
37 | include: projectRoot,
38 | exclude: /node_modules/
39 | }
40 | ],
41 | loaders: [
42 | {
43 | test: /\.vue$/,
44 | loader: 'vue'
45 | },
46 | {
47 | test: /\.js$/,
48 | loader: 'babel',
49 | include: projectRoot,
50 | exclude: /node_modules/
51 | },
52 | {
53 | test: /\.json$/,
54 | loader: 'json'
55 | },
56 | {
57 | test: /\.html$/,
58 | loader: 'vue-html'
59 | },
60 | {
61 | test: /\.(png|jpe?g|gif|svg|woff2?|eot|ttf|otf)(\?.*)?$/,
62 | loader: 'url',
63 | query: {
64 | limit: 10000,
65 | name: path.join(config.build.assetsSubDirectory, '[name].[ext]?[hash:7]')
66 | }
67 | },
68 | {
69 | test: /\.css$/,
70 | loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
71 | }
72 | ]
73 | },
74 | vue: {
75 | loaders: cssLoaders()
76 | },
77 | eslint: {
78 | formatter: require('eslint-friendly-formatter')
79 | },
80 | plugins: [
81 | new ExtractTextPlugin('[name].css')
82 | ]
83 | }
84 |
--------------------------------------------------------------------------------
/build/webpack.dev.conf.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack')
2 | var merge = require('webpack-merge')
3 | var baseWebpackConfig = require('./webpack.base.conf')
4 | var HtmlWebpackPlugin = require('html-webpack-plugin')
5 |
6 | // add hot-reload related code to entry chunks
7 | Object.keys(baseWebpackConfig.entry).forEach(function (name) {
8 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
9 | })
10 |
11 | module.exports = merge(baseWebpackConfig, {
12 | // eval-source-map is faster for development
13 | devtool: '#eval-source-map',
14 | plugins: [
15 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
16 | new webpack.optimize.OccurenceOrderPlugin(),
17 | new webpack.HotModuleReplacementPlugin(),
18 | new webpack.NoErrorsPlugin(),
19 | // https://github.com/ampedandwired/html-webpack-plugin
20 | new HtmlWebpackPlugin({
21 | filename: 'index.html',
22 | template: 'index.html',
23 | inject: true
24 | })
25 | ]
26 | })
27 |
--------------------------------------------------------------------------------
/build/webpack.prod.conf.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var config = require('../config')
3 | var webpack = require('webpack')
4 | var merge = require('webpack-merge')
5 | var baseWebpackConfig = require('./webpack.base.conf')
6 | var cssLoaders = require('./css-loaders')
7 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
8 | var HtmlWebpackPlugin = require('html-webpack-plugin')
9 |
10 | module.exports = merge(baseWebpackConfig, {
11 | devtool: config.build.productionSourceMap ? '#source-map' : false,
12 | output: {
13 | path: config.build.assetsRoot,
14 | filename: path.join(config.build.assetsSubDirectory, '[name].[chunkhash].js'),
15 | chunkFilename: path.join(config.build.assetsSubDirectory, '[id].[chunkhash].js')
16 | },
17 | vue: {
18 | loaders: cssLoaders({
19 | sourceMap: config.build.productionSourceMap,
20 | extract: true
21 | })
22 | },
23 | plugins: [
24 | // http://vuejs.github.io/vue-loader/workflow/production.html
25 | new webpack.DefinePlugin({
26 | 'process.env': {
27 | NODE_ENV: '"production"'
28 | }
29 | }),
30 | new webpack.optimize.UglifyJsPlugin({
31 | compress: {
32 | warnings: false
33 | }
34 | }),
35 | new webpack.optimize.OccurenceOrderPlugin(),
36 | // extract css into its own file
37 | new ExtractTextPlugin(path.join(config.build.assetsSubDirectory, '[name].[contenthash].css')),
38 | // generate dist index.html with correct asset hash for caching.
39 | // you can customize output by editing /index.html
40 | // see https://github.com/ampedandwired/html-webpack-plugin
41 | new HtmlWebpackPlugin({
42 | filename: process.env.NODE_ENV === 'testing'
43 | ? 'index.html'
44 | : config.build.index,
45 | template: 'index.html',
46 | inject: true,
47 | minify: {
48 | removeComments: true,
49 | collapseWhitespace: true,
50 | removeAttributeQuotes: true
51 | // more options:
52 | // https://github.com/kangax/html-minifier#options-quick-reference
53 | }
54 | })
55 | ]
56 | })
57 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | // see http://vuejs-templates.github.io/webpack for documentation.
2 | var path = require('path')
3 |
4 | module.exports = {
5 | build: {
6 | index: path.resolve(__dirname, 'dist/index.html'),
7 | assetsRoot: path.resolve(__dirname, 'dist'),
8 | assetsSubDirectory: 'static',
9 | assetsPublicPath: '/',
10 | productionSourceMap: true
11 | },
12 | dev: {
13 | port: 8080,
14 | proxyTable: {}
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | kuzzle:
2 | image: kuzzleio/kuzzle:alpine
3 | ports:
4 | - "7511:7511"
5 | - "7512:7512"
6 | links:
7 | - elasticsearch
8 | - redis
9 | environment:
10 | - LIKE_A_VIRGIN
11 | - FIXTURES
12 | - DEFAULT_MAPPING
13 | - FEATURE_COVERAGE
14 |
15 | redis:
16 | image: redis:3.0-alpine
17 |
18 | elasticsearch:
19 | image: kuzzleio/elasticsearch:2.2
20 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | medium-klack
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "medium-klack",
3 | "version": "0.1.0",
4 | "description": "A Vue.js project",
5 | "author": "Luca Marchesini ",
6 | "private": true,
7 | "scripts": {
8 | "dev": "node build/dev-server.js",
9 | "build": "node build/build.js",
10 | "unit": "karma start test/unit/karma.conf.js --single-run",
11 | "e2e": "node test/e2e/runner.js",
12 | "test": "npm run unit && npm run e2e"
13 | },
14 | "dependencies": {
15 | "babel-runtime": "^5.8.0",
16 | "kuzzle-sdk": "^1.7.4",
17 | "vue": "^1.0.18",
18 | "vue-router": "^0.7.13"
19 | },
20 | "devDependencies": {
21 | "babel-core": "^6.0.0",
22 | "babel-loader": "^6.0.0",
23 | "babel-plugin-transform-runtime": "^6.0.0",
24 | "babel-preset-es2015": "^6.0.0",
25 | "babel-preset-stage-2": "^6.0.0",
26 | "chai": "^3.5.0",
27 | "chromedriver": "^2.21.2",
28 | "connect-history-api-fallback": "^1.1.0",
29 | "cross-spawn": "^2.1.5",
30 | "css-loader": "^0.23.1",
31 | "eslint": "^2.0.0",
32 | "eslint-config-standard": "^5.1.0",
33 | "eslint-friendly-formatter": "^1.2.2",
34 | "eslint-loader": "^1.3.0",
35 | "eslint-plugin-html": "^1.3.0",
36 | "eslint-plugin-promise": "^1.0.8",
37 | "eslint-plugin-standard": "^1.3.2",
38 | "eventsource-polyfill": "^0.9.6",
39 | "express": "^4.13.3",
40 | "extract-text-webpack-plugin": "^1.0.1",
41 | "file-loader": "^0.8.4",
42 | "function-bind": "^1.0.2",
43 | "html-webpack-plugin": "^2.8.1",
44 | "http-proxy-middleware": "^0.12.0",
45 | "inject-loader": "^2.0.1",
46 | "isparta-loader": "^2.0.0",
47 | "json-loader": "^0.5.4",
48 | "karma": "^0.13.15",
49 | "karma-coverage": "^0.5.5",
50 | "karma-mocha": "^0.2.2",
51 | "karma-phantomjs-launcher": "^1.0.0",
52 | "karma-sinon-chai": "^1.2.0",
53 | "karma-sourcemap-loader": "^0.3.7",
54 | "karma-spec-reporter": "0.0.24",
55 | "karma-webpack": "^1.7.0",
56 | "lolex": "^1.4.0",
57 | "mocha": "^2.4.5",
58 | "nightwatch": "^0.8.18",
59 | "ora": "^0.2.0",
60 | "phantomjs-prebuilt": "^2.1.3",
61 | "selenium-server": "2.53.0",
62 | "shelljs": "^0.6.0",
63 | "sinon": "^1.17.3",
64 | "sinon-chai": "^2.8.0",
65 | "style-loader": "^0.13.1",
66 | "url-loader": "^0.5.7",
67 | "vue-hot-reload-api": "^1.2.0",
68 | "vue-html-loader": "^1.0.0",
69 | "vue-loader": "^8.2.1",
70 | "vue-style-loader": "^1.0.0",
71 | "webpack": "^1.12.2",
72 | "webpack-dev-middleware": "^1.4.0",
73 | "webpack-hot-middleware": "^2.6.0",
74 | "webpack-merge": "^0.8.3"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
13 |
--------------------------------------------------------------------------------
/src/Chat.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
11 |
12 |
13 |
22 |
23 |
24 |
46 |
47 |
50 |
--------------------------------------------------------------------------------
/src/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
33 |
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xbill82/slack-vuejs-kuzzleio/ed9d612e817baabe46c635b2d89da37f0eb71c11/src/assets/logo.png
--------------------------------------------------------------------------------
/src/components/InputBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
32 |
--------------------------------------------------------------------------------
/src/components/Message.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{message.user.username}}
4 | {{message.content}}
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/src/components/Messages.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
10 |
20 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import {} from './style/global.css'
2 | import Vue from 'vue'
3 | import App from './App'
4 | import VueRouter from 'vue-router'
5 | import configRouter from './routes'
6 | import userStore from './store/user'
7 |
8 | Vue.use(VueRouter)
9 | export var router = new VueRouter()
10 |
11 | configRouter(router)
12 | userStore.getCurrentUser(() => {
13 | console.log('Starting...')
14 | router.start(App, 'body')
15 | })
16 |
--------------------------------------------------------------------------------
/src/routes.js:
--------------------------------------------------------------------------------
1 | import Chat from './Chat'
2 | import Login from './Login'
3 | import UserStore from './store/user'
4 |
5 | export default function (router) {
6 | router.map({
7 | '/': {
8 | name: 'chat',
9 | component: Chat,
10 | auth: true
11 | },
12 | '/login': {
13 | name: 'login',
14 | component: Login
15 | }
16 | })
17 |
18 | router.beforeEach(function (transition) {
19 | if (transition.to.auth && !UserStore.isAuthenticated()) {
20 | transition.redirect('/login')
21 | } else {
22 | transition.next()
23 | }
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/src/services/kuzzle.js:
--------------------------------------------------------------------------------
1 | import Kuzzle from 'kuzzle-sdk'
2 |
3 | export default new Kuzzle('http://localhost:7512', {defaultIndex: 'klack'})
4 |
--------------------------------------------------------------------------------
/src/store/messages.js:
--------------------------------------------------------------------------------
1 | import kuzzle from '../services/kuzzle'
2 |
3 | export default {
4 | state: {
5 | messages: []
6 | },
7 |
8 | sendMessage (content, user) {
9 | let message = {content, user, date: Date.now()}
10 | kuzzle
11 | .dataCollectionFactory('messages')
12 | .createDocument(message)
13 | },
14 |
15 | subscribeMessages () {
16 | kuzzle
17 | .dataCollectionFactory('messages')
18 | .subscribe({}, null, (error, response) => {
19 | if (error) {
20 | console.log(error.message)
21 | return
22 | }
23 | this.state.messages.push({
24 | ...response.result._source,
25 | id: response.result._id
26 | })
27 | })
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/store/user.js:
--------------------------------------------------------------------------------
1 | import {router} from '../main'
2 | import kuzzle from '../services/kuzzle'
3 |
4 | export default {
5 | state: {
6 | errorMessage: null,
7 | username: null
8 | },
9 |
10 | login (username, password) {
11 | this.state.errorMessage = null
12 | kuzzle.login('local', {username, password}, '1h', (error, response) => {
13 | if (error) {
14 | this.state.errorMessage = error.message
15 | return
16 | }
17 | if (response.jwt) {
18 | window.sessionStorage.setItem('jwt', response.jwt)
19 | this.getCurrentUser((error, user) => {
20 | if (error) {
21 | console.error(error.message)
22 | return
23 | }
24 | router.go({name: 'chat'})
25 | })
26 | }
27 | })
28 | },
29 |
30 | isAuthenticated () {
31 | return Boolean(this.state.username)
32 | },
33 |
34 | getCurrentUser (cb) {
35 | var jwt = window.sessionStorage.getItem('jwt')
36 | if (!jwt) {
37 | cb('No current user.')
38 | kuzzle.setJwtToken(undefined)
39 | return false
40 | }
41 | kuzzle.setJwtToken(jwt)
42 |
43 | kuzzle
44 | .whoAmI((error, kuzzleUser) => {
45 | if (error) {
46 | window.sessionStorage.removeItem('jwt')
47 | kuzzle.setJwtToken(undefined)
48 | cb(error)
49 |
50 | return false
51 | }
52 |
53 | this.state.username = kuzzleUser.id
54 |
55 | cb(null, kuzzleUser)
56 | })
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/style/global.css:
--------------------------------------------------------------------------------
1 | html, body, div, span, applet, object, iframe,
2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
3 | a, abbr, acronym, address, big, cite, code,
4 | del, dfn, em, img, ins, kbd, q, s, samp,
5 | small, strike, strong, sub, sup, tt, var,
6 | b, u, i, center,
7 | dl, dt, dd, ol, ul, li,
8 | fieldset, form, label, legend,
9 | table, caption, tbody, tfoot, thead, tr, th, td,
10 | article, aside, canvas, details, embed,
11 | figure, figcaption, footer, header,
12 | menu, nav, output, ruby, section, summary,
13 | time, mark, audio, video {
14 | margin: 0;
15 | padding: 0;
16 | border: 0;
17 | font: inherit;
18 | font-size: 100%;
19 | vertical-align: baseline;
20 | }
21 |
22 | html {
23 | line-height: 1;
24 | }
25 |
26 | ol, ul {
27 | list-style: none;
28 | }
29 |
30 | table {
31 | border-collapse: collapse;
32 | border-spacing: 0;
33 | }
34 |
35 | caption, th, td {
36 | text-align: left;
37 | font-weight: normal;
38 | vertical-align: middle;
39 | }
40 |
41 | q, blockquote {
42 | quotes: none;
43 | }
44 |
45 | q:before, q:after, blockquote:before, blockquote:after {
46 | content: "";
47 | content: none;
48 | }
49 |
50 | a img {
51 | border: none;
52 | }
53 |
54 | article, aside, details, figcaption, figure, footer, header, main, menu, nav, section, summary {
55 | display: block;
56 | }
57 |
58 | * {
59 | box-sizing: border-box;
60 | }
61 |
62 | html {
63 | position: relative;
64 | height: 100%;
65 | font-size: 16px;
66 | font-family: 'Lato', sans-serif;
67 | }
68 |
69 | body {
70 | height: 100%;
71 | width: 100%;
72 | margin: 0;
73 | padding-top: 53px;
74 | padding-bottom: 64px;
75 | background-color: #F3F3F3;
76 | }
77 |
78 | .header {
79 | position: fixed;
80 | top: 0;
81 | left: 0;
82 | height: 53px;
83 | width: 100%;
84 | }
85 |
86 | .main {
87 | height: 100%;
88 | width: 80%;
89 | }
90 |
91 | .footer {
92 | position: absolute;
93 | left: 0;
94 | bottom: 0;
95 | height: 64px;
96 | }
97 |
98 | .team-menu {
99 | position: relative;
100 | width: 220px;
101 | height: 53px;
102 | line-height: 53px;
103 | font-weight: 900;
104 | padding: 0 1rem;
105 | color: #ffffff;
106 | background: #3e313c;
107 | border-bottom: 2px solid #372c36;
108 | float: left;
109 | }
110 |
111 | .team-menu .status {
112 | color: red;
113 | }
114 |
115 | .team-menu .status.connected {
116 | color: green;
117 | }
118 |
119 | .team-menu .logout {
120 | font-weight: normal;
121 | float: right;
122 | color: #B7AFB7;
123 | text-decoration: underline;
124 | }
125 |
126 | .channel-menu-name {
127 | display: inline-block;
128 | padding: 0 .5rem 0 1.5rem;
129 | color: #555459;
130 | font-size: 1.4rem;
131 | font-weight: 900;
132 | line-height: 53px;
133 | }
134 |
135 | .channel-menu-prefix {
136 | color: #9e9ea6;
137 | padding-right: .1rem;
138 | font-weight: 500;
139 | }
140 |
141 | .listings {
142 | height: 100%;
143 | width: 220px;
144 | float: left;
145 | color: #ab9ba9;
146 | background-color: #4d394b;
147 | overflow-y: auto;
148 | overflow-x: hidden;
149 | }
150 |
151 | .message-history {
152 | margin-left: 220px;
153 | overflow-y: auto;
154 | overflow-x: hidden;
155 | height: 100%;
156 | padding: 5px 18px 1rem 1.5rem;
157 | }
158 |
159 | .listings-channels {
160 | margin: 1rem 0 2rem;
161 | }
162 |
163 | .listings-header {
164 | text-align: left;
165 | font-size: .8rem;
166 | line-height: 1.25rem;
167 | margin: 0 1rem .1rem;
168 | text-transform: uppercase;
169 | font-weight: 700;
170 | color: #ab9ba9;
171 | width: 165px;
172 | position: relative;
173 | }
174 |
175 | .channel-list {
176 | list-style-type: none;
177 | text-align: left;
178 | color: #ab9ba9;
179 | }
180 |
181 | .channel {
182 | height: 24px;
183 | line-height: 24px;
184 | -moz-border-radius-topright: 0.25rem;
185 | -webkit-border-top-right-radius: 0.25rem;
186 | border-top-right-radius: 0.25rem;
187 | -moz-border-radius-bottomright: 0.25rem;
188 | -webkit-border-bottom-right-radius: 0.25rem;
189 | border-bottom-right-radius: 0.25rem;
190 | margin-right: 17px;
191 | padding-left: 1rem;
192 | cursor: pointer;
193 | }
194 |
195 | .channel a {
196 | color: #ffffff;
197 | text-decoration: none;
198 | }
199 |
200 | .unread {
201 | color: #ffffff;
202 | background: #eb4d5c;
203 | border-radius: 9px;
204 | padding: 2px 9px;
205 | font-size: .8rem;
206 | line-height: 14px;
207 | font-weight: 700;
208 | vertical-align: baseline;
209 | white-space: nowrap;
210 | text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
211 | float: right;
212 | margin-right: 3px;
213 | margin-top: 3px;
214 | }
215 |
216 | .channel.active {
217 | background: #4c9689;
218 | }
219 |
220 | .channel-prefix {
221 | color: #b2d5c9;
222 | }
223 |
224 | .disclaimer {
225 | font-size: 0.8rem;
226 | padding-left: 1rem;
227 | margin-right: 17px;
228 | }
229 |
230 | .message {
231 | position: relative;
232 | margin-top: .5rem;
233 | padding: 0.25rem 2rem 1.1rem 3rem;
234 | min-height: 36px;
235 | }
236 |
237 | .message:hover {
238 | background: #f9f9f9;
239 | }
240 |
241 | .action-hover-container {
242 | display: none;
243 | position: absolute;
244 | top: -13px;
245 | right: 18px;
246 | border: 1px solid rgba(0,0,0,.15);
247 | border-radius: 6px;
248 | overflow: hidden;
249 | -webkit-transition: border 50ms ease-in 0s;
250 | -moz-transition: border 50ms ease-in 0s;
251 | transition: border 50ms ease-in 0s;
252 | z-index: 97;
253 | background-color: #fff;
254 | }
255 |
256 | .action-hover-container .delete-message {
257 | border-radius: 0 6px 6px 0;
258 | border-right: none;
259 | cursor: pointer;
260 | box-shadow: 0 1px 1px rgba(0,0,0,.1);
261 | border-color: rgba(0,0,0,.3);
262 | padding: 5px 10px 5px 10px;
263 | display: inline-block;
264 | }
265 |
266 | .action-hover-container .delete-message:hover {
267 | color: #3aa3e3;
268 | }
269 |
270 | .message:hover .action-hover-container {
271 | display: block;
272 | }
273 |
274 | .message-profile-pic {
275 | position: absolute;
276 | left: 0;
277 | display: block;
278 | -moz-border-radius: 0.2rem;
279 | -webkit-border-radius: 0.2rem;
280 | border-radius: 0.2rem;
281 | width: 36px;
282 | height: 36px;
283 | background-size: cover;
284 | }
285 |
286 | .message-username {
287 | font-weight: 900;
288 | padding-right: .25rem;
289 | color: #3d3c40 !important;
290 | margin-left: 0;
291 | font-style: normal;
292 | text-decoration: none;
293 | }
294 |
295 | .message-timestamp {
296 | text-align: left;
297 | display: inline;
298 | position: relative;
299 | top: 0;
300 | left: 0;
301 | color: #babbbf;
302 | font-size: 12px;
303 | line-height: 1.2rem;
304 | width: 36px;
305 | margin-right: 0;
306 | margin-left: 0;
307 | }
308 |
309 | .message-content {
310 | color: #8b898f;
311 | display: block;
312 | min-height: 1rem;
313 | }
314 |
315 | .user-menu {
316 | float: left;
317 | width: 220px;
318 | cursor: pointer;
319 | background: #3e313c;
320 | border-top: 2px solid #372c36;
321 | padding: 7px 0 9px 8px;
322 | height: 4rem;
323 | position: fixed;
324 | bottom: 0;
325 | left: 0;
326 | }
327 |
328 | .user-menu-profile-pic {
329 | display: inline-block;
330 | float: left;
331 | -moz-border-radius: 0.2rem;
332 | -webkit-border-radius: 0.2rem;
333 | border-radius: 0.2rem;
334 | width: 48px;
335 | height: 48px;
336 | background-size: cover;
337 | margin-right: 8px;
338 | }
339 |
340 | .user-menu .user-menu-username {
341 | display: block;
342 | font-weight: 900;
343 | line-height: 1.5rem;
344 | margin-top: .2rem;
345 | width: 146px;
346 | height: 35px;
347 | }
348 |
349 | .user-menu span.user-menu-username {
350 | color: #fff;
351 | }
352 |
353 | .connection-icon {
354 | width: 12px;
355 | height: 12px;
356 | }
357 |
358 | .connection-status {
359 | color: #ab9ba9;
360 | }
361 |
362 | .input-box {
363 | height: 100%;
364 | margin-left: 220px;
365 | }
366 |
367 | #header-search-form {
368 | margin: 9pt 0 0;
369 | width: 309px;
370 | padding-right: 3rem;
371 | position: absolute;
372 | top: 0;
373 | right: 6px;
374 | }
375 |
376 | .search-form {
377 | padding-right: 118px !important;
378 | }
379 |
380 | .no-bottom-margin {
381 | margin-bottom: 0!important;
382 | }
383 |
384 | #header-search-form .search-input {
385 | margin-right: 0;
386 | outline: 0;
387 | }
388 |
389 | .search-form .search-input {
390 | padding: 2px 2rem;
391 | font-size: 15px;
392 | height: 1.9rem;
393 | width: 100%;
394 | border-radius: 40px;
395 | background: #FFF;
396 | border: 1px solid rgba(214,214,214,.61);
397 | }
398 |
399 | input.search-input {
400 | display: inline-block;
401 | margin-bottom: 0;
402 | margin-right: .5rem;
403 | border-radius: 1rem;
404 | width: 100%;
405 | padding: 0 .8rem;
406 | font-size: .9rem;
407 | }
408 |
409 | input.search-input {
410 | margin: 0;
411 | padding-left: 1.9rem!important;
412 | padding-right: 1.9rem!important;
413 | }
414 |
415 | .search-input-highlighted {
416 | position: relative;
417 | background-color: transparent!important;
418 | z-index: 2;
419 | text-rendering: optimizeSpeed;
420 | }
421 |
422 | .highlighter-underlay {
423 | outline: rgb(85, 84, 89) none 0;
424 | border-width: 1px;
425 | text-indent: 0px;
426 | font-size: 15px;
427 | line-height: normal;
428 | font-family: Slack-Lato, appleLogo, sans-serif;
429 | letter-spacing: 0;
430 | word-spacing: 0;
431 | font-weight: 400;
432 | margin: 2px 30px;
433 | width: 230px;
434 | top: 0;
435 | left: 0;
436 | }
437 |
438 | #header-search-form .highlighter-underlay {
439 | line-height: 24px!important;
440 | border-width: 1px!important;
441 | font-size: 15px!important;
442 | }
443 |
444 | .highlighter-underlay {
445 | border-color: transparent!important;
446 | border-style: solid;
447 | color: transparent!important;
448 | position: absolute;
449 | top: 0;
450 | left: 0;
451 | z-index: 1;
452 | -webkit-touch-callout: none;
453 | -webkit-user-select: none;
454 | -khtml-user-select: none;
455 | -moz-user-select: none;
456 | -ms-user-select: none;
457 | user-select: none;
458 | white-space: pre;
459 | overflow: hidden;
460 | text-rendering: optimizeSpeed;
461 | }
462 |
463 | #header-search-form.search-form .icon-search {
464 | opacity: .5;
465 | -moz-opacity: .5;
466 | -khtml-opacity: .5;
467 | top: 6px;
468 | left: 7px;
469 | color: #67686e;
470 | }
471 |
472 | #header-search-form.search-form .icon-search {
473 | top: 8px;
474 | left: 10px;
475 | text-shadow: none;
476 | }
477 |
478 | #client-ui .icon-search {
479 | position: absolute;
480 | font-size: .9rem;
481 | top: 3px;
482 | left: 7px;
483 | color: #CCC;
484 | }
485 |
486 | .search-form .icon-search {
487 | position: absolute;
488 | top: 6px;
489 | left: 11px;
490 | color: #babbbf;
491 | }
492 |
493 | .input-box-text {
494 | font-size: .95rem;
495 | width: 75%;
496 | margin-left: 2%;
497 | margin-bottom: auto;
498 | line-height: 1.2rem;
499 | border: 2px solid #e0e0e0;
500 | -moz-border-radius: 0.2rem;
501 | -webkit-border-radius: 0.2rem;
502 | border-radius: 0.2rem;
503 | -moz-background-clip: padding-box;
504 | -webkit-background-clip: padding-box;
505 | background-clip: padding-box;
506 | color: #3d3c40;
507 | -webkit-appearance: none;
508 | -webkit-box-shadow: none;
509 | -moz-box-shadow: none;
510 | box-shadow: none;
511 | outline: 0;
512 | bottom: 0;
513 | min-height: 41px;
514 | padding: 9px 5px 9px 8px;
515 | }
516 |
517 | .channels-list-new-btn {
518 | float: right;
519 | margin-right: 1pc;
520 | margin-top: -1px;
521 | font-size: 1rem;
522 | cursor: pointer;
523 | color: #ab9ba9;
524 | opacity: .6;
525 | -moz-opacity: .6;
526 | -khtml-opacity: .6;
527 | -webkit-transition: opacity 50ms;
528 | -moz-transition: opacity 50ms;
529 | -o-transition: opacity 50ms;
530 | transition: opacity 50ms;
531 | border-radius: 10px;
532 | padding: 2px 7px 3px 7px;
533 | background: #fff;
534 | }
535 | .channels-list-new-btn:hover {
536 | opacity: .8;
537 | -moz-opacity: .8;
538 | -khtml-opacity: .8;
539 | }
540 |
541 | .new-channel-name {
542 | margin-left: 9px;
543 | margin-top: 5px;
544 | padding: 5px 0 6px 5px;
545 | margin-bottom: 10px;
546 | }
547 |
548 | .channels-headline {
549 | z-index: 2;
550 | position: relative;
551 | background-color: #4D394B;
552 | }
553 |
554 | .new-channel {
555 | z-index: 1;
556 | }
557 |
558 | /* always present */
559 | .expand-transition {
560 | transition: all .3s ease;
561 | height: 30px;
562 | padding: 10px;
563 | background-color: #eee;
564 | overflow: hidden;
565 | }
566 |
567 | /* .expand-enter defines the starting state for entering */
568 | /* .expand-leave defines the ending state for leaving */
569 | .expand-enter, .expand-leave {
570 | height: 0;
571 | padding: 0 10px;
572 | opacity: 0;
573 | }
574 |
575 | .search-right {
576 | position: absolute;
577 | top: 53px;
578 | width: 20%;
579 | right: 0;
580 | overflow: hidden;
581 | height: 84%;
582 | }
583 |
584 | .search-right .message-history {
585 | margin-left: 0;
586 | padding: 0 0 15px 0;
587 | }
588 |
589 | #app {
590 | height: 100%;
591 | }
592 |
593 | .bottom-input {
594 | position: absolute;
595 | left: 0;
596 | bottom: 0;
597 | height: 64px;
598 | width: 100%;
599 | }
600 |
601 | .messages {
602 | height: 100%;
603 | }
--------------------------------------------------------------------------------
/src/style/login.css:
--------------------------------------------------------------------------------
1 | #login {
2 | width: 300px;
3 | height: 280px;
4 | margin: auto;
5 | border: 1px solid rgba(0,0,0,.15);
6 | border-radius: 6px;
7 | background-color: #fff;
8 | padding-top: 40px;
9 | }
10 |
11 | #login .title {
12 | margin-left: 10px;
13 | color: #3e313c;
14 | font-weight: bold;
15 | font-size: 1.5em;
16 | }
17 |
18 | #login input {
19 | display: block;
20 | width: 275px;
21 | margin: 20px auto 10px auto;
22 | }
23 |
24 | #login .login {
25 | border-radius: 6px;
26 | cursor: pointer;
27 | box-shadow: 0 1px 1px rgba(0,0,0,.1);
28 | padding: 5px 10px 5px 10px;
29 | border: 1px solid rgba(0,0,0,.15);
30 | display: inline-block;
31 | margin-left: 12px;
32 | background-color: #FBFBFB;
33 | }
34 |
35 | #login p.error {
36 | margin: 20px 0 0 10px;
37 | color: red;
38 | font-weight: bold;
39 | }
--------------------------------------------------------------------------------
/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xbill82/slack-vuejs-kuzzleio/ed9d612e817baabe46c635b2d89da37f0eb71c11/static/.gitkeep
--------------------------------------------------------------------------------
/test/e2e/custom-assertions/elementCount.js:
--------------------------------------------------------------------------------
1 | // A custom Nightwatch assertion.
2 | // the name of the method is the filename.
3 | // can be used in tests like this:
4 | //
5 | // browser.assert.elementCount(selector, count)
6 | //
7 | // for how to write custom assertions see
8 | // http://nightwatchjs.org/guide#writing-custom-assertions
9 | exports.assertion = function (selector, count) {
10 | this.message = 'Testing if element <' + selector + '> has count: ' + count
11 | this.expected = count
12 | this.pass = function (val) {
13 | return val === this.expected
14 | }
15 | this.value = function (res) {
16 | return res.value
17 | }
18 | this.command = function (cb) {
19 | var self = this
20 | return this.api.execute(function (selector) {
21 | return document.querySelectorAll(selector).length
22 | }, [selector], function (res) {
23 | cb.call(self, res)
24 | })
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/test/e2e/nightwatch.conf.js:
--------------------------------------------------------------------------------
1 | // http://nightwatchjs.org/guide#settings-file
2 | module.exports = {
3 | "src_folders": ["test/e2e/specs"],
4 | "output_folder": "test/e2e/reports",
5 | "custom_assertions_path": ["test/e2e/custom-assertions"],
6 |
7 | "selenium": {
8 | "start_process": true,
9 | "server_path": "node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.0.jar",
10 | "host": "127.0.0.1",
11 | "port": 4444,
12 | "cli_args": {
13 | "webdriver.chrome.driver": require('chromedriver').path
14 | }
15 | },
16 |
17 | "test_settings": {
18 | "default": {
19 | "selenium_port": 4444,
20 | "selenium_host": "localhost",
21 | "silent": true
22 | },
23 |
24 | "chrome": {
25 | "desiredCapabilities": {
26 | "browserName": "chrome",
27 | "javascriptEnabled": true,
28 | "acceptSslCerts": true
29 | }
30 | },
31 |
32 | "firefox": {
33 | "desiredCapabilities": {
34 | "browserName": "firefox",
35 | "javascriptEnabled": true,
36 | "acceptSslCerts": true
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/test/e2e/runner.js:
--------------------------------------------------------------------------------
1 | // 1. start the dev server using production config
2 | process.env.NODE_ENV = 'testing'
3 | var server = require('../../build/dev-server.js')
4 |
5 | // 2. run the nightwatch test suite against it
6 | // to run in additional browsers:
7 | // 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings"
8 | // 2. add it to the --env flag below
9 | // For more information on Nightwatch's config file, see
10 | // http://nightwatchjs.org/guide#settings-file
11 | var spawn = require('cross-spawn')
12 | var runner = spawn(
13 | './node_modules/.bin/nightwatch',
14 | [
15 | '--config', 'test/e2e/nightwatch.conf.js',
16 | '--env', 'chrome,firefox'
17 | ],
18 | {
19 | stdio: 'inherit'
20 | }
21 | )
22 |
23 | runner.on('exit', function (code) {
24 | server.close()
25 | process.exit(code)
26 | })
27 |
28 | runner.on('error', function (err) {
29 | server.close()
30 | throw err
31 | })
32 |
--------------------------------------------------------------------------------
/test/e2e/specs/test.js:
--------------------------------------------------------------------------------
1 | // For authoring Nightwatch tests, see
2 | // http://nightwatchjs.org/guide#usage
3 |
4 | module.exports = {
5 | 'default e2e tests': function (browser) {
6 | browser
7 | .url('http://localhost:8080')
8 | .waitForElementVisible('#app', 5000)
9 | .assert.elementPresent('.logo')
10 | .assert.containsText('h1', 'Hello World!')
11 | .assert.elementCount('p', 3)
12 | .end()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/test/unit/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | },
5 | "globals": {
6 | "expect": true,
7 | "sinon": true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/test/unit/index.js:
--------------------------------------------------------------------------------
1 | // Polyfill fn.bind() for PhantomJS
2 | /* eslint-disable no-extend-native */
3 | Function.prototype.bind = require('function-bind')
4 |
5 | // require all test files (files that ends with .spec.js)
6 | var 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 | var 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 path = require('path')
7 | var merge = require('webpack-merge')
8 | var baseConfig = require('../../build/webpack.base.conf')
9 | var projectRoot = path.resolve(__dirname, '../../')
10 |
11 | var webpackConfig = merge(baseConfig, {
12 | // use inline sourcemap for karma-sourcemap-loader
13 | devtool: '#inline-source-map',
14 | vue: {
15 | loaders: {
16 | js: 'isparta'
17 | }
18 | }
19 | })
20 |
21 | // no need for app entry during tests
22 | delete webpackConfig.entry
23 |
24 | // make sure isparta loader is applied before eslint
25 | webpackConfig.module.preLoaders = webpackConfig.module.preLoaders || []
26 | webpackConfig.module.preLoaders.unshift({
27 | test: /\.js$/,
28 | loader: 'isparta',
29 | include: projectRoot,
30 | exclude: /test\/unit|node_modules/
31 | })
32 |
33 | // only apply babel for test files when using isparta
34 | webpackConfig.module.loaders.some(function (loader, i) {
35 | if (loader.loader === 'babel') {
36 | loader.include = /test\/unit/
37 | return true
38 | }
39 | })
40 |
41 | module.exports = function (config) {
42 | config.set({
43 | // to run in additional browsers:
44 | // 1. install corresponding karma launcher
45 | // http://karma-runner.github.io/0.13/config/browsers.html
46 | // 2. add it to the `browsers` array below.
47 | browsers: ['PhantomJS'],
48 | frameworks: ['mocha', 'sinon-chai'],
49 | reporters: ['spec', 'coverage'],
50 | files: ['./index.js'],
51 | preprocessors: {
52 | './index.js': ['webpack', 'sourcemap']
53 | },
54 | webpack: webpackConfig,
55 | webpackMiddleware: {
56 | noInfo: true
57 | },
58 | coverageReporter: {
59 | dir: './coverage',
60 | reporters: [
61 | { type: 'lcov', subdir: '.' },
62 | { type: 'text-summary' }
63 | ]
64 | }
65 | })
66 | }
67 |
--------------------------------------------------------------------------------
/test/unit/specs/Hello.spec.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Hello from 'src/components/Hello'
3 |
4 | describe('Hello.vue', () => {
5 | it('should render correct contents', () => {
6 | const vm = new Vue({
7 | template: '
',
8 | components: { Hello }
9 | }).$mount()
10 | expect(vm.$el.querySelector('.hello h1').textContent).to.contain('Hello World!')
11 | })
12 | })
13 |
--------------------------------------------------------------------------------