diff --git a/lib/DatePicker.js b/lib/DatePicker.js index 1246cc0..1690ede 100644 --- a/lib/DatePicker.js +++ b/lib/DatePicker.js @@ -1,7 +1,7 @@ /** * @module DatePicker组件 */ -import './index.css'; + import React, { Component, PropTypes } from 'react'; import { nextTime, getTimeName } from './time.js'; import { @@ -26,9 +26,9 @@ class DatePicker extends Component { }; }); - this.animating = false; - this.touchY = 0; - this.angle = 0; + this.animating = false; // 判断是否在transition过渡动画之中 + this.touchY = 0; // 保存touchstart的pageY + this.angle = 0; // 容器转过的角度 this.state = { angle: 0, dates, @@ -41,16 +41,31 @@ class DatePicker extends Component { this._moveToNext = this._moveToNext.bind(this); } + /** + * 根据角度返回透明度(0-1之间) + * @param {number} angle 角度 + * @return + */ _setOpacity(angle) { return angle > 0 ? ((40 - angle) / 40 * 100 | 0) / 100 : ((40 + angle) / 40 * 100 | 0) / 100; } + /** + * 清除对象的transition样式 + * @param {Dom} obj 指定的对象 + * @return {undefined} + */ _clearTransition(obj) { obj.style[TRANSITION] = ''; // eslint-disable-line } + /** + * 滑动到下一日期 + * @param {number} direction 滑动方向 + * @return {undefined} + */ _moveToNext(direction) { const scroll = this.refs.scroll; const angle = this.angle; @@ -70,6 +85,12 @@ class DatePicker extends Component { } } + /** + * 添加滑动动画 + * @param {DOM} obj DOM对象 + * @param {number} angle 角度 + * @return {undefined} + */ _moveTo(obj, angle) { this.animating = true; obj.style[TRANSITION] = `${TRANSFORM_CSS} .2s ease-out`; // eslint-disable-line @@ -78,11 +99,20 @@ class DatePicker extends Component { }); } + /** + * 点击完成按钮事件 + * @return {undefined} + */ handleFinishBtnClick() { const date = this.state.dates.find(value => value.angle + this.state.angle === 0); this.props.onSelect(date.value); } + /** + * 滑动日期选择器事件 + * @param {Object} event 事件对象 + * @return {undefined} + */ handleContentTouch(event) { event.preventDefault(); if (this.animating) return; @@ -107,6 +137,10 @@ class DatePicker extends Component { } } + /** + * transition过渡完成事件 + * @return {undefined} + */ handleContentTransitionEnd() { const { dates, angle } = this.state; const date = dates.find(value => value.angle + angle === 0); @@ -126,6 +160,11 @@ class DatePicker extends Component { }); } + /** + * 渲染一个日期DOM对象 + * @param {Object} date date数据 + * @return {Object} JSX对象 + */ renderDatepickerItem(date) { const itemStyle = { [TRANSFORM]: `rotateX(${date.angle}deg) translate3d(0,0,100px)`, @@ -141,6 +180,10 @@ class DatePicker extends Component { ); } + /** + * render函数 + * @return {Object} JSX对象 + */ render() { const { layerBackground, btnColor } = this.props; const scrollStyle = { diff --git a/lib/index.js b/lib/index.js index 627f870..4271eb3 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,2 +1,3 @@ +import './index.css'; import DatePicker from './DatePicker.js'; export default DatePicker; diff --git a/lib/time.js b/lib/time.js index 6fb2349..9758686 100644 --- a/lib/time.js +++ b/lib/time.js @@ -1,3 +1,7 @@ +/** + * @module time工具 + */ + function convertDate(timestamp, formate) { const date = new Date(timestamp); const year = date.getFullYear(); @@ -16,7 +20,11 @@ function convertDate(timestamp, formate) { .replace(/s+/, second); } - +/** + * 获取相对日期的偏移日期 + * @param {Date} 日期 + * @return {number} 相对的天数 + */ export function nextTime(now = new Date(), index = 1) { if (Object.prototype.toString.call(now, null) !== '[object Date]') { throw new Error('参数类型不对'); @@ -27,6 +35,11 @@ export function nextTime(now = new Date(), index = 1) { return date; } +/** + * 获取指定日期的格式化日期名称 + * @param {Date} 日期 + * @return {String} 格式化日期名称 + */ export function getTimeName(now) { if (Object.prototype.toString.call(now, null) !== '[object Date]') { throw new Error('参数类型不对'); diff --git a/lib/transition.js b/lib/transition.js index b99da5f..8fb876f 100644 --- a/lib/transition.js +++ b/lib/transition.js @@ -6,7 +6,7 @@ if (typeof document.body.style.transition === 'string') { TRANSITIONEND = 'transitionend'; TRANSITION_CSS = 'transition'; } else if (typeof document.body.style.webkitTransition === 'string') { - TRANSITION = 'webkitTransition'; + TRANSITION = 'WebkitTransition'; TRANSITION_CSS = '-webkit-transition'; TRANSITIONEND = 'webkitTransitionEnd'; } @@ -17,7 +17,7 @@ if (typeof document.body.style.transform === 'string') { TRANSFORM = 'transform'; TRANSFORM_CSS = 'transform'; } else if (typeof document.body.style.webkitTransform === 'string') { - TRANSFORM = 'webkitTransform'; + TRANSFORM = 'WebkitTransform'; TRANSFORM_CSS = '-webkit-transform'; } diff --git a/package.json b/package.json index fc60eff..2d037b0 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "scripts": { "build": "webpack", "start": "webpack-dev-server", - "release": "scripts/release", + "release": "npm install build;scripts/release", "preview-release": "scripts/preview-release", "mocha": "./node_modules/.bin/mocha --compilers js:babel-core/register --require ./test/helper.js --recursive", "test": "./node_modules/.bin/nyc npm run mocha", @@ -20,6 +20,7 @@ "babel-core": "^6.9.0", "babel-eslint": "^6.0.4", "babel-loader": "^6.2.4", + "babel-polyfill": "^6.9.1", "babel-preset-es2015": "^6.9.0", "babel-preset-react": "^6.5.0", "babel-preset-stage-0": "^6.5.0", @@ -27,6 +28,7 @@ "coveralls": "^2.11.9", "css-loader": "^0.23.1", "cssnext": "^1.8.4", + "enzyme": "^2.3.0", "eslint": "^2.10.2", "eslint-config-airbnb": "9.0.1", "eslint-plugin-import": "^1.8.0", @@ -41,7 +43,9 @@ "postcss-cssnext": "^2.5.2", "postcss-loader": "^0.9.1", "postcss-nested": "^1.0.0", + "react-addons-test-utils": "^15.1.0", "rf-release": "^0.4.0", + "sinon": "^1.17.4", "style-loader": "^0.13.1", "webpack": "^1.13.1", "webpack-dev-server": "^1.14.1", @@ -52,4 +56,4 @@ "react-dom": "^15.1.0" }, "license": "ISC" -} \ No newline at end of file +} diff --git a/test/functional/DatePicker_spec.js b/test/functional/DatePicker_spec.js new file mode 100644 index 0000000..9a5e885 --- /dev/null +++ b/test/functional/DatePicker_spec.js @@ -0,0 +1,177 @@ + +import React from 'react'; +import { assert, expect } from 'chai'; +import sinon from 'sinon'; +import { mount, shallow } from 'enzyme'; +import DatePicker from '../../lib/DatePicker'; +import {nextTime} from '../../lib/time'; + +describe('时间选择器组件DatePicker', () => { + describe('测试初始化的过程:', () => { + it('测试传入_setOpacity的参数', () => { + const spyFunction = sinon.spy(DatePicker.prototype, '_setOpacity'); + const datePicker = mount( + + ); + + const list = [ + spyFunction.getCall(0).args[0], + spyFunction.getCall(1).args[0], + spyFunction.getCall(2).args[0], + spyFunction.getCall(3).args[0], + spyFunction.getCall(4).args[0], + ]; + + const expectList = [45, 22.5, 0, -22.5, -45]; + expect(list).to.deep.equals(expectList); + spyFunction.restore(); + }) + }) + + + describe('测试方法:', () => { + it('_setOpacity应该返回相应的值,当传入不同的临界值', () => { + const datePicker = mount( + + ); + const inst = datePicker.instance(); + + const test1 = [0, 1]; + const test2 = [40, 0]; + const test3 = [-40, 0]; + + expect(inst._setOpacity(test1[0])).to.equals(test1[1]); + expect(inst._setOpacity(test2[0])).to.equals(test2[1]); + expect(inst._setOpacity(test3[0])).to.equals(test3[1]); + }) + + it('应该滑动到上一个日期,当调用_moveToNext方法', () => { + const spyFunction = sinon.spy(); + const datePicker = mount( + + ); + datePicker.instance()._moveToNext(-1); + datePicker.find('.datepicker-finish-btn').simulate('click'); + sinon.assert.calledWith(spyFunction, nextTime(new Date(), -1)); + }) + + + it('应该向_moveToNext传入-1, 当滑动到上一个日期', () => { + const spyFunction = sinon.spy(DatePicker.prototype, '_moveToNext'); + const datePicker = mount( + + ); + const touchstartEvent = { + type: 'touchstart', + targetTouches: [{ pageY: 0 }], + }; + const touchendEvent = { + type: 'touchend', + changedTouches: [{ pageY: 50 }], + }; + + + datePicker.find('.datepicker-content').simulate('touchStart', touchstartEvent); + datePicker.find('.datepicker-content').simulate('touchEnd', touchendEvent); + sinon.assert.calledWith(spyFunction, -1); + spyFunction.restore(); + }) + + }) + + describe('测试交互事件:', () => { + it('应该触发handleContent3次, 在触摸屏幕之后', () => { + const spyFunction = sinon.spy(DatePicker.prototype, 'handleContentTouch'); + const datePicker = mount( + + ); + + datePicker.find('.datepicker-content').simulate('touchStart'); + datePicker.find('.datepicker-content').simulate('touchMove'); + datePicker.find('.datepicker-content').simulate('touchEnd'); + sinon.assert.callCount(spyFunction, 3); + spyFunction.restore(); + }) + + it('应该触发onSelect, 在点击完成按钮之后', () => { + const spyFunction = sinon.spy(); + const datePicker = mount( + + ); + datePicker.find('.datepicker-finish-btn').simulate('click'); + sinon.assert.calledOnce(spyFunction); + }) + + + it('应该向onSelect传入上一个日期, 当向下滑动超过touchLen', () => { + const spyFunction = sinon.spy(); + const datePicker = mount( + + ); + const touchstartEvent = { + type: 'touchstart', + targetTouches: [{ pageY: 0 }], + }; + const touchendEvent = { + type: 'touchend', + changedTouches: [{ pageY: 50 }], + }; + + datePicker.find('.datepicker-content').simulate('touchStart', touchstartEvent); + datePicker.find('.datepicker-content').simulate('touchEnd', touchendEvent); + datePicker.find('.datepicker-content').simulate('transitionEnd'); + datePicker.find('.datepicker-finish-btn').simulate('click'); + sinon.assert.calledWith(spyFunction, nextTime(new Date(), -1)); + }) + + it('应该向onSelect传入下一个日期, 当向上滑动超过touchLen', () => { + const spyFunction = sinon.spy(); + const datePicker = mount( + + ); + const touchstartEvent = { + type: 'touchstart', + targetTouches: [{ pageY: 0 }], + }; + const touchendEvent = { + type: 'touchend', + changedTouches: [{ pageY: -50 }], + }; + + datePicker.find('.datepicker-content').simulate('touchStart', touchstartEvent); + datePicker.find('.datepicker-content').simulate('touchEnd', touchendEvent); + datePicker.find('.datepicker-content').simulate('transitionEnd'); + datePicker.find('.datepicker-finish-btn').simulate('click'); + sinon.assert.calledWith(spyFunction, nextTime(new Date(), 1)); + }) + + it('应该恢复当前日期, 但滑动超过touchLen但上一个日期小于minDate', () => { + const spyFunction = sinon.spy(); + const datePicker = mount( + + ); + const touchstartEvent = { + type: 'touchstart', + targetTouches: [{ pageY: 0 }], + }; + const touchendEvent = { + type: 'touchend', + changedTouches: [{ pageY: 50 }], + }; + + datePicker.find('.datepicker-content').simulate('touchStart', touchstartEvent); + datePicker.find('.datepicker-content').simulate('touchEnd', touchendEvent); + datePicker.find('.datepicker-content').simulate('transitionEnd'); + datePicker.find('.datepicker-finish-btn').simulate('click'); + sinon.assert.calledWith(spyFunction, nextTime(new Date(), 0)); + }) + }) + +}); diff --git a/test/functional/time_spec.js b/test/functional/time_spec.js index 29f98af..ab333f7 100644 --- a/test/functional/time_spec.js +++ b/test/functional/time_spec.js @@ -1,47 +1,54 @@ -var chai = require('chai'); -var expect = chai.expect; -import { nextTime, getTimeName } from '../../lib/time.js'; - - -describe('nextTime函数', () => { - it('参数类型不对', function () { - const now = 1466654887871; - expect(function(){ nextTime(1466654887871) }).to.throw('参数类型不对'); - }); - it("参数2015.5.31应该返回2016.6.1", function() { - const now = new Date(2016, 4, 31); - expect(nextTime(now).getTime()).to.equal(new Date(2016, 5, 1).getTime()); - }); - it("参数2015.5.31应该返回2016.6.1", function() { - const now = new Date(2016, 4, 31); - expect(nextTime(now, 1).getTime()).to.equal(new Date(2016, 5, 1).getTime()); - }); +import { expect } from 'chai'; +import { nextTime, getTimeName } from '../../lib/time.js'; - it("参数2015.6.1应该返回2016.5.31", function() { - const now = new Date(2016, 5, 1); - expect(nextTime(now, -1).getTime()).to.equal(new Date(2016, 4, 31).getTime()); +describe('time工具', () => { + + describe('nextTime函数', () => { + it('参数类型不对', function () { + const now = 1466654887871; + expect(function(){ nextTime(1466654887871) }).to.throw('参数类型不对'); + }); + + it('默认参数的情况下', () => { + expect(nextTime().getTime()).to.equal(nextTime(new Date(), 1).getTime()); + }); + + it("参数2015.5.31应该返回2016.6.1", function() { + const now = new Date(2016, 4, 31); + expect(nextTime(now).getTime()).to.equal(new Date(2016, 5, 1).getTime()); + }); + it("参数2015.5.31应该返回2016.6.1", function() { + const now = new Date(2016, 4, 31); + expect(nextTime(now, 1).getTime()).to.equal(new Date(2016, 5, 1).getTime()); + }); + + it("参数2015.6.1应该返回2016.5.31", function() { + const now = new Date(2016, 5, 1); + expect(nextTime(now, -1).getTime()).to.equal(new Date(2016, 4, 31).getTime()); + }); }); -}); -describe('getTimeName函数', () => { - it('参数类型不对', function () { - const now = 1466654887871; - expect(function(){ getTimeName(1466654887871) }).to.throw('参数类型不对'); + describe('getTimeName函数', () => { + it('参数类型不对', function () { + const now = 1466654887871; + expect(function(){ getTimeName(1466654887871) }).to.throw('参数类型不对'); + }); + + it('应该返回昨天', () => { + const data = nextTime(new Date(), -1); + expect(getTimeName(data)).to.equal('昨天'); + }) + + it('应该返回今天', () => { + const data = new Date(); + expect(getTimeName(data)).to.equal('今天'); + }) + + it('应该返回2016-5-10', () => { + const data = new Date(2016, 4, 10); + expect(getTimeName(data)).to.equal('2016-5-10'); + }) }); - it('应该返回昨天', () => { - const data = nextTime(new Date(), -1); - expect(getTimeName(data)).to.equal('昨天'); - }) - - it('应该返回今天', () => { - const data = new Date(); - expect(getTimeName(data)).to.equal('今天'); - }) - - it('应该返回2016-5-10', () => { - const data = new Date(2016, 4, 10); - expect(getTimeName(data)).to.equal('2016-5-10'); - }) -}); +}) diff --git a/test/helper.js b/test/helper.js index 8b13789..93eecba 100644 --- a/test/helper.js +++ b/test/helper.js @@ -1 +1,16 @@ +require('babel-polyfill'); +var jsdom = require('jsdom').jsdom; +var exposedProperties = ['window', 'navigator', 'document']; +global.document = jsdom(''); +global.window = document.defaultView; +Object.keys(document.defaultView).forEach((property) => { + if (typeof global[property] === 'undefined') { + exposedProperties.push(property); + global[property] = document.defaultView[property]; + } +}); + +global.navigator = { + userAgent: 'node.js' +};