diff --git a/.gitignore b/.gitignore index 9e1e0cfa1..f5915983f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ lib examples/element-ui fe.element/element-ui .npmrc +coverage diff --git a/build/config.js b/build/config.js index 9e4361bee..ede163b3c 100644 --- a/build/config.js +++ b/build/config.js @@ -28,3 +28,27 @@ exports.alias = { }; exports.jsexclude = /node_modules|utils\/popper\.js|utils\/date.\js/; + +exports.postcss = function(webapck) { + return [ + require('postcss-salad')({ + browser: ['ie > 8', 'last 2 version'], + features: { + 'partialImport': { + addDependencyTo: webapck + }, + 'bem': { + 'shortcuts': { + 'component': 'b', + 'modifier': 'm', + 'descendent': 'e' + }, + 'separators': { + 'descendent': '__', + 'modifier': '--' + } + } + } + }) + ]; +}; diff --git a/build/cooking.demo.js b/build/cooking.demo.js index 8da2f11c5..1d8d7e515 100644 --- a/build/cooking.demo.js +++ b/build/cooking.demo.js @@ -28,29 +28,7 @@ cooking.set({ sourceMap: true, alias: config.alias, extends: ['vue2', 'lint'], - postcss: function(webapck) { - return [ - require('postcss-salad')({ - browser: ['ie > 8', 'last 2 version'], - features: { - 'partialImport': { - addDependencyTo: webapck - }, - 'bem': { - 'shortcuts': { - 'component': 'b', - 'modifier': 'm', - 'descendent': 'e' - }, - 'separators': { - 'descendent': '__', - 'modifier': '--' - } - } - } - }) - ]; - } + postcss: config.postcss }); cooking.add('loader.md', { diff --git a/build/cooking.test.js b/build/cooking.test.js new file mode 100644 index 000000000..cdac935d7 --- /dev/null +++ b/build/cooking.test.js @@ -0,0 +1,25 @@ +var path = require('path'); +var cooking = require('cooking'); +var config = require('./config'); +var projectRoot = path.resolve(__dirname, '../'); +var ProgressBarPlugin = require('progress-bar-webpack-plugin'); + +cooking.set({ + entry: './src/index.js', + extends: ['vue2'], + minimize: false, + alias: config.alias, + postcss: config.postcss, + sourceMap: '#inline-source-map' +}); + +cooking.add('vue.loaders.js', 'isparta'); +cooking.add('loader.js.exclude', config.jsexclude); +cooking.add('preLoader.js', { + test: /\.js$/, + loader: 'isparta-loader', + include: path.resolve(projectRoot, 'src') +}); + +cooking.add('plugins.process', new ProgressBarPlugin()); +module.exports = cooking.resolve(); diff --git a/examples/docs/zh-cn/development.md b/examples/docs/zh-cn/development.md index df73283e3..61e64080d 100644 --- a/examples/docs/zh-cn/development.md +++ b/examples/docs/zh-cn/development.md @@ -25,7 +25,11 @@ npm run bootstrap registry=https://registry.npm.taobao.org ``` -然后再运行 `npm run bootstrap` 安装依赖。 +然后再运行 + +```shell +PHANTOMJS_CDNURL=http://npm.taobao.org/mirrors/phantomjs npm run bootstrap +``` ### 启动开发环境 diff --git a/package.json b/package.json index 90e21f918..9d5db9602 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "pub:all": "npm run dist:all && lerna publish", "build:utils": "babel src/utils --out-dir lib/utils", "clean": "rimraf lib && rimraf packages/*/lib", - "lint": "eslint src/**/*.js packages/**/*.{js,vue} build/**/*.js --quiet" + "lint": "eslint src/**/*.js test/**/*.js packages/**/*.{js,vue} build/**/*.js --quiet", + "test:watch": "karma start test/unit/karma.conf.js", + "test": "karma start test/unit/karma.conf.js --single-run" }, "repository": { "type": "git", @@ -43,6 +45,7 @@ "babel-plugin-syntax-jsx": "^6.8.0", "babel-plugin-transform-vue-jsx": "^3.1.0", "babel-preset-es2015": "^6.14.0", + "chai": "^3.5.0", "cheerio": "^0.18.0", "cooking": "^1.1.0", "cooking-lint": "^0.1.3", @@ -57,16 +60,31 @@ "highlight.js": "^9.3.0", "html-loader": "^0.4.3", "html-webpack-plugin": "^2.22.0", + "inject-loader": "^3.0.0-beta2", + "isparta-loader": "^2.0.0", "json-loader": "^0.5.4", "json-templater": "^1.0.4", + "karma": "^1.3.0", + "karma-coverage": "^1.1.1", + "karma-mocha": "^1.2.0", + "karma-phantomjs-launcher": "^1.0.2", + "karma-sinon-chai": "^1.2.4", + "karma-sourcemap-loader": "^0.3.7", + "karma-spec-reporter": "0.0.26", + "karma-webpack": "^1.8.0", "lerna": "2.0.0-beta.18", + "lolex": "^1.5.1", "markdown-it": "^6.1.1", "markdown-it-container": "^2.0.0", + "mocha": "^3.1.1", "object-assign": "^4.1.0", + "phantomjs-prebuilt": "^2.1.13", "postcss": "^5.1.2", "postcss-loader": "^0.11.1", "postcss-salad": "^1.0.5", "rimraf": "^2.5.4", + "sinon": "^1.17.6", + "sinon-chai": "^2.8.0", "style-loader": "^0.13.1", "theaterjs": "^3.0.0", "uppercamelcase": "^1.1.0", diff --git a/test/unit/.eslintrc b/test/unit/.eslintrc new file mode 100644 index 000000000..959a4f4b5 --- /dev/null +++ b/test/unit/.eslintrc @@ -0,0 +1,9 @@ +{ + "env": { + "mocha": true + }, + "globals": { + "expect": true, + "sinon": true + } +} diff --git a/test/unit/index.js b/test/unit/index.js new file mode 100644 index 000000000..c1157e780 --- /dev/null +++ b/test/unit/index.js @@ -0,0 +1,14 @@ +// Polyfill fn.bind() for PhantomJS +/* eslint-disable no-extend-native */ +Function.prototype.bind = require('function-bind'); +require('packages/theme-default/src/index.css'); + +// require all test files (files that ends with .spec.js) +const testsContext = require.context('./specs', true, /\.spec$/); +testsContext.keys().forEach(testsContext); + +// require all src files except main.js for coverage. +// you can also change this to match only the subset of files that +// you want coverage for. +const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/); +srcContext.keys().forEach(srcContext); diff --git a/test/unit/karma.conf.js b/test/unit/karma.conf.js new file mode 100644 index 000000000..0f1d31642 --- /dev/null +++ b/test/unit/karma.conf.js @@ -0,0 +1,31 @@ +var webpackConfig = require('../../build/cooking.test'); + +// no need for app entry during tests +delete webpackConfig.entry; + +module.exports = function(config) { + config.set({ + // to run in additional browsers: + // 1. install corresponding karma launcher + // http://karma-runner.github.io/0.13/config/browsers.html + // 2. add it to the `browsers` array below. + browsers: ['PhantomJS'], + frameworks: ['mocha', 'sinon-chai'], + reporters: ['spec', 'coverage'], + files: ['./index.js'], + preprocessors: { + './index.js': ['webpack', 'sourcemap'] + }, + webpack: webpackConfig, + webpackMiddleware: { + noInfo: true + }, + coverageReporter: { + dir: './coverage', + reporters: [ + { type: 'lcov', subdir: '.' }, + { type: 'text-summary' } + ] + } + }); +}; diff --git a/test/unit/specs/timeselect.spec.js b/test/unit/specs/timeselect.spec.js new file mode 100644 index 000000000..b63392942 --- /dev/null +++ b/test/unit/specs/timeselect.spec.js @@ -0,0 +1,60 @@ +import { createTest, createVue } from '../util'; +import TimeSelect from 'packages/time-select'; +import Vue from 'vue'; + +describe('TimeSelect', () => { + it('should render correct contents', done => { + const vm = createTest(TimeSelect, { + pickerOptions: { + start: '08:30', + step: '00:15', + end: '18:30' + }, + placeholder: 'test' + }, true); + vm.$el.querySelector('input').blur(); + vm.$el.querySelector('input').focus(); + vm.$el.querySelector('input').blur(); + + Vue.nextTick(_ => { + expect(vm.picker.start).to.equal('08:30'); + expect(vm.picker.end).to.equal('18:30'); + expect(vm.picker.step).to.equal('00:15'); + expect(vm.$el.querySelector('input').getAttribute('placeholder')).to.equal('test'); + done(); + }); + }); + + it('click time', done => { + const vm = createVue({ + template: ` +
+ + +
+ `, + + data() { + return { + value: '' + }; + } + }, true); + + vm.$el.querySelector('input').blur(); + vm.$el.querySelector('input').focus(); + vm.$el.querySelector('input').blur(); + + Vue.nextTick(_ => { + const items = vm.$refs.compo.picker.$el.querySelectorAll('.time-select-item'); + const target = items[4]; + const time = target.textContent.trim(); + + target.click(); + Vue.nextTick(_ => { + expect(vm.value).to.equal(time); + done(); + }); + }); + }); +}); diff --git a/test/unit/util.js b/test/unit/util.js new file mode 100644 index 000000000..88d76acdc --- /dev/null +++ b/test/unit/util.js @@ -0,0 +1,40 @@ +import Vue from 'vue/dist/vue'; +import Element from 'main/index.js'; + +Vue.use(Element); + +let id = 0; + +const createElm = function() { + const elm = document.createElement('div'); + + elm.id = 'app' + ++id; + document.body.appendChild(elm); + + return elm; +}; + +/** + * 创建一个 Vue 的实例对象 + * @param {Object} Compo 组件配置 + * @param {Boolean=false} mounted 是否添加到 DOM 上 + * @return {Object} vm + */ +exports.createVue = function(Compo, mounted = false) { + const elm = createElm(); + return new Vue(Compo).$mount(mounted === false ? null : elm); +}; + +/** + * 创建一个测试组件实例 + * @link http://vuejs.org/guide/unit-testing.html#Writing-Testable-Components + * @param {Object} Compo - 组件对象 + * @param {Object} propsData - props 数据 + * @param {Boolean=false} mounted - 是否添加到 DOM 上 + * @return {Object} vm + */ +exports.createTest = function(Compo, propsData = {}, mounted = false) { + const elm = createElm(); + const Ctor = Vue.extend(Compo); + return new Ctor({ propsData }).$mount(mounted === false ? null : elm); +};