Merge pull request #91 from chenjiahan/dev

CellSwipe: improve test coverage && code review
This commit is contained in:
neverland 2017-08-25 09:39:10 +08:00 committed by GitHub
commit 28c3354419
10 changed files with 346 additions and 329 deletions

View File

@ -26,6 +26,7 @@
"test": "karma start test/unit/karma.conf.js --single-run", "test": "karma start test/unit/karma.conf.js --single-run",
"test:coverage": "open test/unit/coverage/lcov-report/index.html", "test:coverage": "open test/unit/coverage/lcov-report/index.html",
"test:watch": "karma start test/unit/karma.conf.js", "test:watch": "karma start test/unit/karma.conf.js",
"test:single": "node ./test/unit/selector.js",
"release": "npm run bootstrap && sh build/release.sh" "release": "npm run bootstrap && sh build/release.sh"
}, },
"repository": { "repository": {

View File

@ -1,15 +1,19 @@
<template> <template>
<div v-clickoutside:touchstart="swipeMove" @click="swipeMove()" @touchstart="startDrag" @touchmove="onDrag" @touchend="endDrag" class="van-cell-swipe" ref="cell"> <div
<div class="van-cell-wrapper"> v-clickoutside:touchstart="swipeMove"
<slot>单元格内容</slot> class="van-cell-swipe"
</div> @click="swipeMove()"
<div class="van-cell-left"> @touchstart="startDrag"
<div ref="left"> @touchmove="onDrag"
@touchend="endDrag"
@touchcancel="endDrag"
>
<div class="van-cell-swipe__wrapper" :style="wrapperStyle" @transitionend="swipe = false">
<div class="van-cell-swipe__left" v-if="leftWidth">
<slot name="left"></slot> <slot name="left"></slot>
</div> </div>
</div> <slot></slot>
<div class="van-cell-right"> <div class="van-cell-swipe__right" v-if="rightWidth">
<div ref="right">
<slot name="right"></slot> <slot name="right"></slot>
</div> </div>
</div> </div>
@ -17,112 +21,101 @@
</template> </template>
<script> <script>
import { once } from '../utils/dom';
import Clickoutside from '../utils/clickoutside'; import Clickoutside from '../utils/clickoutside';
export default { export default {
name: 'van-cell-swipe', name: 'van-cell-swipe',
props: { props: {
'leftWidth': { type: Number, default: 0 }, leftWidth: {
'rightWidth': { type: Number, default: 0 } type: Number,
}, default: 0
directives: { Clickoutside },
data() {
return {
start: { x: 0, y: 0 }
};
},
computed: {
leftDefaultTransform() {
return this.translate3d(-this.leftWidth - 1);
}, },
rightDefaultTransform() { rightWidth: {
return this.translate3d(this.rightWidth); type: Number,
default: 0
} }
}, },
mounted() {
this.wrap = this.$refs.cell.querySelector('.van-cell-wrapper');
this.leftElm = this.$refs.left;
this.leftWrapElm = this.leftElm.parentNode;
this.leftWrapElm.style.webkitTransform = this.leftDefaultTransform;
this.rightElm = this.$refs.right; directives: {
this.rightWrapElm = this.rightElm.parentNode; Clickoutside
this.rightWrapElm.style.webkitTransform = this.rightDefaultTransform;
}, },
data() {
return {
offset: 0
};
},
computed: {
wrapperStyle() {
return {
transform: `translate3d(${this.offset}px, 0, 0)`
}
}
},
methods: { methods: {
resetSwipeStatus() { resetSwipeStatus() {
this.swiping = false; // this.swiping = false; //
this.opened = true; // this.opened = true; //
this.offsetLeft = 0; //
},
translate3d(offset) {
return `translate3d(${offset}px, 0, 0)`;
}, },
swipeMove(offset = 0) { swipeMove(offset = 0) {
this.wrap.style.webkitTransform = this.translate3d(offset); this.offset = offset;
this.rightWrapElm.style.webkitTransform = this.translate3d(this.rightWidth + offset);
this.leftWrapElm.style.webkitTransform = this.translate3d(-this.leftWidth + offset);
offset && (this.swiping = true); offset && (this.swiping = true);
}, },
swipeLeaveTransition(direction) { swipeLeaveTransition(direction) {
setTimeout(() => { const { offset, leftWidth, rightWidth } = this;
this.swipeLeave = true; // right
// left if (direction > 0 && -offset > rightWidth * 0.4 && rightWidth > 0) {
if (direction > 0 && -this.offsetLeft > this.rightWidth * 0.4 && this.rightWidth > 0) { this.swipeMove(-rightWidth);
this.swipeMove(-this.rightWidth); this.resetSwipeStatus();
this.resetSwipeStatus(); }
return; // left
// right else if (direction < 0 && offset >leftWidth * 0.4 && leftWidth > 0) {
} else if (direction < 0 && this.offsetLeft > this.leftWidth * 0.4 && this.leftWidth > 0) { this.swipeMove(leftWidth);
this.swipeMove(this.leftWidth); this.resetSwipeStatus();
this.resetSwipeStatus(); } else {
return; this.swipeMove();
} else { }
this.swipeMove(0);
once(this.wrap, 'webkitTransitionEnd', () => {
this.wrap.style.webkitTransform = '';
this.rightWrapElm.style.webkitTransform = this.rightDefaultTransform;
this.leftWrapElm.style.webkitTransform = this.leftDefaultTransform;
this.swipeLeave = false;
this.swiping = false;
});
}
}, 0);
}, },
startDrag(evt) {
evt = evt.changedTouches ? evt.changedTouches[0] : evt; startDrag(event) {
this.dragging = true; this.startX = event.changedTouches[0].pageX;
this.start.x = evt.pageX; this.startY = event.changedTouches[0].pageY;
this.start.y = evt.pageY;
}, },
onDrag(evt) {
onDrag(event) {
if (this.opened) { if (this.opened) {
!this.swiping && this.swipeMove(0); !this.swiping && this.swipeMove();
this.opened = false; this.opened = false;
return; return;
} }
if (!this.dragging) return;
let swiping; const offsetTop = event.changedTouches[0].pageY - this.startY;
const e = evt.changedTouches ? evt.changedTouches[0] : evt; const offsetLeft = event.changedTouches[0].pageX - this.startX;
const offsetTop = e.pageY - this.start.y;
const offsetLeft = this.offsetLeft = e.pageX - this.start.x;
if ((offsetLeft < 0 && -offsetLeft > this.rightWidth) || if ((offsetLeft < 0 && -offsetLeft > this.rightWidth) ||
(offsetLeft > 0 && offsetLeft > this.leftWidth) || (offsetLeft > 0 && offsetLeft > this.leftWidth) ||
(offsetLeft > 0 && !this.leftWidth) || (offsetLeft > 0 && !this.leftWidth) ||
(offsetLeft < 0 && !this.rightWidth)) { (offsetLeft < 0 && !this.rightWidth)) {
return; return;
} }
const y = Math.abs(offsetTop); const y = Math.abs(offsetTop);
const x = Math.abs(offsetLeft); const x = Math.abs(offsetLeft);
swiping = !(x < 5 || (x >= 5 && y >= x * 1.73)); const swiping = !(x < 5 || (x >= 5 && y >= x * 1.73));
if (!swiping) return; if (swiping) {
evt.preventDefault(); event.preventDefault();
this.swipeMove(offsetLeft); this.swipeMove(offsetLeft);
};
}, },
endDrag() { endDrag() {
if (!this.swiping) return; if (this.swiping) {
this.swipeLeaveTransition(this.offsetLeft > 0 ? -1 : 1); this.swipeLeaveTransition(this.offset > 0 ? -1 : 1);
};
} }
} }
}; };

View File

@ -1,11 +0,0 @@
import Vue from 'vue';
export const once = function(el, event, fn) {
const listener = function() {
if (fn) {
fn.apply(this, arguments);
}
el.removeEventListener(event, listener);
};
el.addEventListener(event, listener);
};

View File

@ -1,29 +1,26 @@
.van-cell { .van-cell-swipe {
&-swipe { overflow: hidden;
position: relative; position: relative;
min-height: 48px;
overflow: hidden;
.van-cell-wrapper, &__wrapper,
.van-cell-left, &__left,
.van-cell-right { &__right {
transition: transform 150ms ease-in-out; transition: transform .15s ease-in-out;
}
} }
&-left, &__left,
&-right { &__right {
position: absolute;
height: 100%;
top: 0; top: 0;
height: 100%;
position: absolute;
} }
&-left { &__left {
left: 0; left: 0;
transform: translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0);
} }
&-right { &__right {
right: 0; right: 0;
transform: translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0);
} }

View File

@ -2,96 +2,96 @@ const path = require('path');
const webpack = require('webpack'); const webpack = require('webpack');
const ProgressBarPlugin = require('progress-bar-webpack-plugin'); const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const webpackConfig = { function getWebpackConfig(testFileName) {
output: { return {
path: path.resolve(process.cwd(), 'dist'), output: {
publicPath: '/dist/', path: path.resolve(process.cwd(), 'dist'),
filename: '[name].js', publicPath: '/dist/',
chunkFilename: '[id].js', filename: '[name].js',
libraryTarget: 'umd' chunkFilename: '[id].js',
}, libraryTarget: 'umd'
plugins: [ },
new ProgressBarPlugin(), plugins: [
new webpack.LoaderOptionsPlugin({ new ProgressBarPlugin(),
minimize: true, new webpack.LoaderOptionsPlugin({
options: { minimize: true,
babel: { options: {
presets: ['env'], babel: {
plugins: ['transform-runtime', 'transform-vue-jsx'] presets: ['env'],
}, plugins: ['transform-runtime', 'transform-vue-jsx']
vue: { },
autoprefixer: false, vue: {
preserveWhitespace: false autoprefixer: false,
preserveWhitespace: false
}
} }
} }),
}) new webpack.DefinePlugin({
], 'process.env': {
stats: 'errors-only', TEST_FILE: `"${testFileName}"`
resolve: { }
modules: [ })
path.resolve(process.cwd(), 'node_modules'),
'node_modules'
], ],
extensions: ['.js', '.json', '.vue'], stats: 'errors-only',
alias: { resolve: {
src: path.resolve(process.cwd(), 'src'), modules: [path.resolve(process.cwd(), 'node_modules'), 'node_modules'],
packages: path.resolve(process.cwd(), 'packages'), extensions: ['.js', '.json', '.vue'],
examples: path.resolve(process.cwd(), 'examples'), alias: {
vue$: 'vue/dist/vue.common.js' src: path.resolve(process.cwd(), 'src'),
} packages: path.resolve(process.cwd(), 'packages'),
}, examples: path.resolve(process.cwd(), 'examples'),
module: { vue$: 'vue/dist/vue.common.js'
rules: [
{
enforce: 'pre',
test: /\.js$/,
exclude: /node_modules|vue-router\/|vue-loader\/|docs|test|src\/index|src\/utils|src\/mixins|packages\/swipe/,
use: ['isparta-loader']
},
{
test: /\.js$/,
exclude: /node_modules|vue-router\/|vue-loader\//,
use: ['babel-loader']
},
{
test: /\.(css|pcss)$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
},
{
test: /\.(gif|png|jpe?g)(\?\S*)?$/,
use: [{
loader: 'url-loader',
options: {
query: {
limit: 10000,
name: 'static/[name].[hash:7].[ext]'
}
}
}]
},
{
test: /\.vue$/,
use: [{
loader: 'vue-loader',
options: {
loaders: {
css: [
'style-loader',
'css-loader',
'postcss-loader'
],
js: ['isparta-loader']
}
}
}]
} }
] },
}, module: {
devtool: '#inline-source-map' rules: [
}; {
enforce: 'pre',
test: /\.js$/,
exclude: /node_modules|vue-router\/|vue-loader\/|docs|test|src\/index|src\/utils|src\/mixins|packages\/swipe/,
use: ['isparta-loader']
},
{
test: /\.js$/,
exclude: /node_modules|vue-router\/|vue-loader\//,
use: ['babel-loader']
},
{
test: /\.(css|pcss)$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
},
{
test: /\.(gif|png|jpe?g)(\?\S*)?$/,
use: [
{
loader: 'url-loader',
options: {
query: {
limit: 10000,
name: 'static/[name].[hash:7].[ext]'
}
}
}
]
},
{
test: /\.vue$/,
use: [
{
loader: 'vue-loader',
options: {
loaders: {
css: ['style-loader', 'css-loader', 'postcss-loader'],
js: ['isparta-loader']
}
}
}
]
}
]
},
devtool: '#inline-source-map'
};
}
module.exports = webpackConfig; module.exports = getWebpackConfig;

View File

@ -1,5 +1,13 @@
require('packages/vant-css/src/index.css'); require('packages/vant-css/src/index.css');
// require all test files (files that ends with .spec.js) // 读取配置文件,判断运行单个测试文件还是所有测试文件
const testsReq = require.context('./specs', true, /\.spec$/); const testsReq = require.context('./specs', true, /\.spec$/);
testsReq.keys().forEach(testsReq); if (process.env.TEST_FILE) {
testsReq.keys().forEach((file) => {
if (file.indexOf(process.env.TEST_FILE) !== -1) {
testsReq(file);
}
});
} else {
testsReq.keys().forEach(testsReq);
}

View File

@ -4,7 +4,7 @@ require('babel-core/register')({
presets: [require('babel-preset-env')] presets: [require('babel-preset-env')]
}); });
var webpackConfig = require('./get-webpack-conf'); var getWebpackConfig = require('./get-webpack-conf');
var travis = process.env.TRAVIS; var travis = process.env.TRAVIS;
module.exports = function(config) { module.exports = function(config) {
@ -16,7 +16,7 @@ module.exports = function(config) {
preprocessors: { preprocessors: {
'./index.js': ['webpack', 'sourcemap'] './index.js': ['webpack', 'sourcemap']
}, },
webpack: webpackConfig, webpack: getWebpackConfig(getTestFileName()),
webpackMiddleware: { webpackMiddleware: {
noInfo: true noInfo: true
}, },
@ -30,3 +30,8 @@ module.exports = function(config) {
singleRun: false singleRun: false
}); });
}; };
function getTestFileName() {
const flagIndex = process.argv.indexOf('--file');
return flagIndex !== -1 ? process.argv[flagIndex + 1] : '';
}

19
test/unit/selector.js Normal file
View File

@ -0,0 +1,19 @@
/**
* 运行单个测试文件
*/
const fs = require('fs');
const inquirer = require('inquirer');
const path = require('path');
const shell = require('shelljs');
const files = fs.readdirSync(path.resolve(__dirname, './specs'));
inquirer.prompt([{
type: 'list',
name: 'select',
message: '请选择要运行的测试文件:',
choices: files
}], (result) => {
const file = result.select.replace('.spec.js', '');
shell.exec('karma start test/unit/karma.conf.js --color alway --file ' + file);
});

View File

@ -1,141 +1,117 @@
import CellSwipe from 'packages/cell-swipe'; import CellSwipe from 'packages/cell-swipe';
import { mount } from 'avoriaz'; import { mount } from 'avoriaz';
import { triggerTouch } from '../utils';
const defaultProps = {
propsData: {
leftWidth: 100,
rightWidth: 100
}
}
describe('CellSwipe', () => { describe('CellSwipe', () => {
let wrapper; let wrapper;
afterEach(() => { afterEach(() => {
wrapper && wrapper.destroy(); wrapper && wrapper.destroy();
}); });
it('render left or right part when has width', () => {
wrapper = mount(CellSwipe, defaultProps);
expect(wrapper.find('.van-cell-swipe__left').length).to.equal(1);
expect(wrapper.find('.van-cell-swipe__right').length).to.equal(1);
});
it('create a CellSwipe', () => { it('not render left or right part when width is 0', () => {
wrapper = mount(CellSwipe, { wrapper = mount(CellSwipe);
propsData: { expect(wrapper.find('.van-cell-swipe__left').length).to.equal(0);
leftWidth: 2, expect(wrapper.find('.van-cell-swipe__right').length).to.equal(0);
rightWidth: 2 });
}
it('drag and show left part', () => {
wrapper = mount(CellSwipe, defaultProps);
triggerTouch(wrapper, 'touchstart', 0, 0);
expect(wrapper.vm.startX).to.equal(0);
expect(wrapper.vm.startY).to.equal(0);
triggerTouch(wrapper, 'touchmove', 50, 0);
expect(wrapper.vm.offset).to.equal(50);
triggerTouch(wrapper, 'touchend', 50, 0);
expect(wrapper.vm.offset).to.equal(100);
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.opened).to.be.true;
}); });
wrapper.vm.startDrag({ });
pageX: 0,
pageY: 0 it('drag and show right part', () => {
wrapper = mount(CellSwipe, defaultProps);
triggerTouch(wrapper, 'touchstart', 0, 0);
triggerTouch(wrapper, 'touchmove', -50, 0);
triggerTouch(wrapper, 'touchend', -50, 0);
expect(wrapper.vm.offset).to.equal(-100);
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.opened).to.be.true;
}); });
wrapper.vm.onDrag({ });
preventDefault() {},
pageY: 0, it('drag and reset left part', () => {
pageX: 50 wrapper = mount(CellSwipe, defaultProps);
triggerTouch(wrapper, 'touchstart', 0, 0);
triggerTouch(wrapper, 'touchmove', 10, 0);
triggerTouch(wrapper, 'touchmove', 30, 0);
triggerTouch(wrapper, 'touchend', 30, 0);
expect(wrapper.vm.offset).to.equal(0);
});
it('drag and reset right part', () => {
wrapper = mount(CellSwipe, defaultProps);
triggerTouch(wrapper, 'touchstart', 0, 0);
triggerTouch(wrapper, 'touchmove', -10, 0);
triggerTouch(wrapper, 'touchmove', -30, 0);
triggerTouch(wrapper, 'touchend', -30, 0);
expect(wrapper.vm.offset).to.equal(0);
});
it('drag distance out of ranges', () => {
wrapper = mount(CellSwipe, defaultProps);
triggerTouch(wrapper, 'touchstart', 0, 0);
triggerTouch(wrapper, 'touchmove', 1000, 0);
expect(wrapper.vm.offset).to.equal(0);
});
it('drag and hide left part', (done) => {
wrapper = mount(CellSwipe, defaultProps);
triggerTouch(wrapper, 'touchstart', 0, 0);
triggerTouch(wrapper, 'touchmove', 20, 0);
triggerTouch(wrapper, 'touchmove', 50, 0);
triggerTouch(wrapper, 'touchend', 50, 0);
expect(wrapper.vm.offset).to.equal(100);
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.opened).to.be.true;
triggerTouch(wrapper, 'touchstart', 0, 0);
triggerTouch(wrapper, 'touchmove', 1, 0);
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.offset).to.equal(0);
expect(wrapper.vm.opened).to.be.false;
done();
});
}); });
wrapper.vm.offsetLeft = -20; });
wrapper.vm.rightWidth = 10;
wrapper.vm.swipeLeaveTransition(1); it('drag vertical', () => {
wrapper.vm.endDrag(); wrapper = mount(CellSwipe, defaultProps);
expect(wrapper.hasClass('van-cell-swipe')).to.be.true;
}); triggerTouch(wrapper, 'touchstart', 0, 0);
}); triggerTouch(wrapper, 'touchmove', 0, 100);
triggerTouch(wrapper, 'touchend', 0, 100);
expect(wrapper.vm.offset).to.equal(0);
describe('CellSwipe-left', () => {
let wrapper;
afterEach(() => {
wrapper && wrapper.destroy();
});
it('create a CellSwipe left', () => {
wrapper = mount(CellSwipe, {
propsData: {
leftWidth: 2,
rightWidth: 2
}
});
wrapper.vm.startDrag({
changedTouches: [{
pageX: 0,
pageY: 0
}
]
});
wrapper.vm.onDrag({
preventDefault() {},
changedTouches: [{
pageX: 0,
pageY: -50
}
]
});
wrapper.vm.offsetLeft = 20;
wrapper.vm.rightWidth = 10;
wrapper.vm.swipeLeaveTransition(-1);
wrapper.vm.endDrag();
expect(wrapper.hasClass('van-cell-swipe')).to.be.true;
});
});
describe('CellSwipe-0', () => {
let wrapper;
afterEach(() => {
wrapper && wrapper.destroy();
});
it('create a CellSwipe 0', () => {
wrapper = mount(CellSwipe, {
propsData: {
leftWidth: 0,
rightWidth: 2
}
});
wrapper.vm.startDrag({
pageX: 0,
pageY: 0
});
wrapper.vm.onDrag({
preventDefault() {},
pageY: 0,
pageX: -2
});
wrapper.vm.opened = true;
wrapper.vm.onDrag({
preventDefault() {},
pageY: 0,
pageX: -2
});
wrapper.vm.opened = false;
wrapper.vm.onDrag({
preventDefault() {},
pageY: 0,
pageX: 40
});
wrapper.vm.swipeLeaveTransition(0);
wrapper.vm.endDrag();
expect(wrapper.hasClass('van-cell-swipe')).to.be.true;
});
});
describe('CellSwipe-0', () => {
let wrapper;
afterEach(() => {
wrapper && wrapper.destroy();
});
it('create a CellSwipe 0', () => {
wrapper = mount(CellSwipe, {
propsData: {
leftWidth: 0,
rightWidth: 2
}
});
wrapper.vm.startDrag({
pageX: 0,
pageY: 0
});
wrapper.vm.onDrag({
preventDefault() {},
pageY: 1000,
pageX: 40
});
wrapper.vm.swipeMove();
wrapper.vm.swiping = false;
wrapper.vm.endDrag();
wrapper.vm.swiping = true;
wrapper.vm.endDrag();
expect(wrapper.hasClass('van-cell-swipe')).to.be.true;
}); });
}); });

View File

@ -31,7 +31,9 @@ export function DOMChecker(wrapper, rules) {
if (style) { if (style) {
Object.keys(style).forEach(key => { Object.keys(style).forEach(key => {
Object.keys(style[key]).forEach(prop => { Object.keys(style[key]).forEach(prop => {
expect(wrapper.find(key)[0].hasStyle(prop, style[key][prop])).to.equal(true); expect(wrapper.find(key)[0].hasStyle(prop, style[key][prop])).to.equal(
true
);
}); });
}); });
} }
@ -39,8 +41,35 @@ export function DOMChecker(wrapper, rules) {
if (noStyle) { if (noStyle) {
Object.keys(noStyle).forEach(key => { Object.keys(noStyle).forEach(key => {
Object.keys(noStyle[key]).forEach(prop => { Object.keys(noStyle[key]).forEach(prop => {
expect(wrapper.find(key)[0].hasStyle(prop, noStyle[key][prop])).to.equal(false); expect(
wrapper.find(key)[0].hasStyle(prop, noStyle[key][prop])
).to.equal(false);
}); });
}); });
} }
} }
// 触发一个 touch 事件
export function triggerTouch(wrapper, eventName, x, y) {
const el = wrapper.element ? wrapper.element : wrapper;
const touch = {
identifier: Date.now(),
target: el,
pageX: x,
pageY: y,
clientX: x,
clientY: y,
radiusX: 2.5,
radiusY: 2.5,
rotationAngle: 10,
force: 0.5,
};
const event = document.createEvent('CustomEvent');
event.initCustomEvent(eventName, true, true, {});
event.touches = [touch];
event.targetTouches = [touch];
event.changedTouches = [touch];
el.dispatchEvent(event);
}