Skip to content

Commit

Permalink
Merge pull request #4599 from rfru/ripple-test
Browse files Browse the repository at this point in the history
test(ripple): add ripple tests
  • Loading branch information
rfru committed Jul 27, 2016
2 parents 2ff26e8 + 5fffaee commit 69cce5b
Show file tree
Hide file tree
Showing 5 changed files with 341 additions and 3 deletions.
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
"build:min": "mkdir -p build && MDL_ENV=production webpack -p",
"dist": "npm run clean && npm run build && npm run build:min",
"dev": "npm run clean && MDL_ENV=development webpack-dev-server --content-base demos --inline --hot",
"fix:js": "eslint --fix packages webpack.config.js karma.conf.js",
"fix:js": "eslint --fix packages test webpack.config.js karma.conf.js",
"fix:css": "stylefmt -R packages",
"fix": "npm-run-all --parallel fix:*",
"lint:js": "eslint packages webpack.config.js karma.conf.js",
"lint:js": "eslint packages test webpack.config.js karma.conf.js",
"lint:css": "stylelint packages/**/*.scss",
"lint": "npm-run-all --parallel lint:*",
"postinstall": "lerna bootstrap",
Expand All @@ -29,13 +29,15 @@
"bel": "^4.4.3",
"css-loader": "^0.23.1",
"cz-conventional-changelog": "^1.1.6",
"dom-events": "^0.1.1",
"eslint": "^2.12.0",
"eslint-config-google": "^0.5.0",
"eslint-plugin-tape": "^1.1.0",
"extract-text-webpack-plugin": "^1.0.1",
"ghooks": "^1.3.2",
"isparta-loader": "^2.0.0",
"istanbul": "^0.4.4",
"json-loader": "^0.5.4",
"karma": "^1.1.1",
"karma-chrome-launcher": "^1.0.1",
"karma-coverage": "^1.1.0",
Expand All @@ -45,6 +47,7 @@
"karma-tap": "^2.0.1",
"karma-webpack": "^1.7.0",
"lerna": "2.0.0-beta.18",
"lolex": "^1.5.0",
"node-sass": "^3.7.0",
"npm-run-all": "^2.3.0",
"postcss-loader": "^0.9.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/mdl-ripple/mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export default function MDLRippleMixin(renderer) {

Object.defineProperty(this, 'maxRadius', {
get: function() {
if (this.maxRadius_) {
if (this.maxRadius_ || !this.boundingRect_) {
return this.maxRadius_;
}

Expand Down
102 changes: 102 additions & 0 deletions test/unit/mdl-ripple/mdl-ripple.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import bel from 'bel';
import domEvents from 'dom-events';
import test from 'tape';
import MDLRipple from '../../../packages/mdl-ripple';
import {Class, Identifier} from '../../../packages/mdl-ripple/mixin';

test('buildDom', t => {
const expected = `
<div class="mdl-ripple">
<div class="mdl-ripple__background"></div>
<div class="mdl-ripple__foreground">
<div class="mdl-ripple__foreground-circle"></div>
</div>
<div class="mdl-ripple__foreground">
<div class="mdl-ripple__foreground-circle"></div>
</div>
<div class="mdl-ripple__foreground">
<div class="mdl-ripple__foreground-circle"></div>
</div>
<div class="mdl-ripple__foreground">
<div class="mdl-ripple__foreground-circle"></div>
</div>
</div>
`.trim().replace(/>\s+</g, '><');

const root = MDLRipple.buildDom();
t.equal(root.outerHTML, expected);
t.end();
});

test('attachTo assigns options', t => {
const root = MDLRipple.buildDom();
let surface = bel`
<div></div>
`;
let component = MDLRipple.attachTo(surface, root);
t.equal(component.isBounded, true);

surface = bel`
<div bounded="false" max-radius="30"></div>
`;
component = MDLRipple.attachTo(surface, root);
t.equal(component.isBounded, false);
t.equal(component.maxRadius, 30);

t.end();
});

test('constructor initializes with options', t => {
const root = MDLRipple.buildDom();
const surface = bel`
<div></div>
`;
const elements = {
[Identifier.ROOT]: root,
[Identifier.SURFACE]: surface
};

let component = new MDLRipple(elements, {});
t.equal(component.isBounded, true);

component = new MDLRipple(elements, {bounded: false, maxRadius: 30});
t.equal(component.isBounded, false);
t.equal(component.maxRadius, 30);
t.end();
});

test('ripple activates on click', t => {
const root = MDLRipple.buildDom();
const surface = bel`<div></div>`;
const background = root.querySelector(`.${Class.BACKGROUND}`);
const firstForeground = root.querySelector(`.${Class.FOREGROUND}`);

MDLRipple.attachTo(surface, root);

domEvents.emit(surface, 'mousedown');
t.true(background.classList.contains(Class.BACKGROUND_ACTIVE));

domEvents.emit(surface, 'mouseup');
t.false(background.classList.contains(Class.BACKGROUND_ACTIVE));
t.true(firstForeground.classList.contains(Class.FOREGROUND_BOUNDED_ACTIVE));

t.end();
});

test('ripple cancels on mouseout', t => {
const root = MDLRipple.buildDom();
const surface = bel`<div></div>`;
const background = root.querySelector(`.${Class.BACKGROUND}`);
const firstForeground = root.querySelector(`.${Class.FOREGROUND}`);

MDLRipple.attachTo(surface, root);

domEvents.emit(surface, 'mousedown');
t.true(background.classList.contains(Class.BACKGROUND_ACTIVE));

domEvents.emit(surface, 'mouseout');
t.false(background.classList.contains(Class.BACKGROUND_ACTIVE));
t.false(firstForeground.classList.contains(Class.FOREGROUND_BOUNDED_ACTIVE));

t.end();
});
230 changes: 230 additions & 0 deletions test/unit/mdl-ripple/mixin.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import lolex from 'lolex';
import td from 'testdouble';
import test from 'tape';
import MDLRippleMixin, {Class, Identifier} from '../../../packages/mdl-ripple/mixin';

function createComponent() {
const adapter = td.object([
'addClass',
'forceLayout',
'get',
'getBoundingClientRect',
'getComputedValue',
'removeClass',
'setStyles'
]);
const component = {};
MDLRippleMixin.call(component, adapter);
component.initMdlRipple_();
td.when(adapter.get(Identifier.ROOT)).thenReturn(Identifier.ROOT);
td.when(adapter.get(Identifier.SURFACE)).thenReturn(Identifier.SURFACE);
td.when(adapter.get(Identifier.BACKGROUND)).thenReturn(Identifier.BACKGROUND);
td.when(adapter.get(Identifier.FOREGROUND, 0)).thenReturn('F0');
td.when(adapter.get(Identifier.FOREGROUND_CIRCLE, 0)).thenReturn('FC0');
td.when(adapter.get(Identifier.FOREGROUND, 1)).thenReturn('F1');
td.when(adapter.get(Identifier.FOREGROUND_CIRCLE, 1)).thenReturn('FC1');
td.when(adapter.get(Identifier.FOREGROUND, 2)).thenReturn('F2');
td.when(adapter.get(Identifier.FOREGROUND_CIRCLE, 2)).thenReturn('FC2');
td.when(adapter.get(Identifier.FOREGROUND, 3)).thenReturn('F3');
td.when(adapter.get(Identifier.FOREGROUND_CIRCLE, 3)).thenReturn('FC3');
td.when(adapter.getBoundingClientRect(Identifier.SURFACE)).thenReturn({
width: 100,
height: 50
});
return {component, adapter};
}

test('layout sets initial ripple bounding box', t => {
const {anything} = td.matchers;
const {component, adapter} = createComponent();

component.layout();

// Verify ripple box.
t.doesNotThrow(() =>
td.verify(adapter.setStyles(Identifier.ROOT, {
height: '141.4213562373095px',
width: '141.4213562373095px',
top: '-45.710678118654755px',
left: '-20.710678118654755px',
visibility: 'visible'
})));

// Layout not called if not dirty.
component.layout();
t.doesNotThrow(() =>
td.verify(adapter.setStyles(Identifier.ROOT, anything()), {times: 1}));

t.end();
});

test('startTouchBeganAnimationAtPoint begins bounded animation', t => {
const {component, adapter} = createComponent();

component.startTouchBeganAnimationAtPoint(0, 0);
t.doesNotThrow(() =>
td.verify(adapter.addClass(Identifier.BACKGROUND, Class.BACKGROUND_ACTIVE)));
t.end();
});

test('startTouchBeganAnimationAtPoint begins unbounded animation', t => {
const {contains} = td.matchers;
const {component, adapter} = createComponent();
component.isBounded = false;
component.maxRadius = 10;

component.startTouchBeganAnimationAtPoint(0, 0);
t.doesNotThrow(() =>
td.verify(adapter.addClass(Identifier.BACKGROUND, Class.BACKGROUND_ACTIVE)));

// Circle begins expanding and fading in.
t.doesNotThrow(() =>
td.verify(adapter.setStyles('FC0', contains({
opacity: 1,
transform: 'scale(1)',
transition: 'opacity 110ms linear, transform 98.82117688026186ms linear 80ms'
}))));

// Circle has correct origin.
t.doesNotThrow(() =>
td.verify(adapter.setStyles('F0', contains({
transform: 'translate3d(-50px, -25px, 0)'
}))));

// Circle begins gravitating towards center.
t.doesNotThrow(() =>
td.verify(adapter.setStyles('F0', contains({
transform: 'translate3d(0.01px,0,0)',
transition: 'transform 98.82117688026186ms linear 80ms'
}))));

t.end();
});

test('startTouchEndedAnimationAtPoint begins bounded animation', t => {
const {component, adapter} = createComponent();

component.startTouchEndedAnimationAtPoint(0, 0);

t.doesNotThrow(() =>
td.verify(adapter.removeClass(Identifier.BACKGROUND, Class.BACKGROUND_ACTIVE)));
t.doesNotThrow(() =>
td.verify(adapter.addClass('F0', Class.FOREGROUND_BOUNDED_ACTIVE)));

// Circle has correct origin.
t.doesNotThrow(() =>
td.verify(adapter.setStyles('F0', {
transform: 'translate3d(-50px, -25px, 0)'
})));

// Circle gravitates part of the way.
t.doesNotThrow(() =>
td.verify(adapter.setStyles('F0', {
transform: 'translate3d(-33.333333333333336px, -16.666666666666668px, 0)'
})));

t.end();
});

test('startTouchEndedAnimationAtPoint begins unbounded animation', t => {
const clock = lolex.install();
const {component, adapter} = createComponent();
component.isBounded = false;
component.maxRadius = 10;

// Partially expanded
td.when(adapter.getComputedValue('FC0', 'opacity')).thenReturn(0.2);
td.when(adapter.getBoundingClientRect('FC0')).thenReturn({
width: 10,
height: 5
});

component.startTouchEndedAnimationAtPoint(0, 0);

t.doesNotThrow(() =>
td.verify(adapter.removeClass(Identifier.BACKGROUND, Class.BACKGROUND_ACTIVE)));

// Circle changes to faster animation timings and fades out.
t.doesNotThrow(() =>
td.verify(adapter.setStyles('FC0', {
opacity: '0',
transform: 'scale(1.00000001)',
transition: 'opacity 66.66666666666667ms linear, transform 41.1740011720207ms ' +
'cubic-bezier(0.157, 0.72, 0.386, 0.987)'
})));

// Circle translates to center.
t.doesNotThrow(() =>
td.verify(adapter.setStyles('F0', {
transform: 'translate3d(0,0,0)',
transition: 'transform 41.1740011720207ms cubic-bezier(0.157, 0.72, 0.386, 0.987)'
})));

// Check styles only reset after animation.
clock.tick(66);
t.doesNotThrow(() =>
td.verify(adapter.setStyles('FC0', {
opacity: '',
transform: '',
transition: ''
}), {times: 0}));

// Tick past opacity finished.
clock.tick(1);
t.doesNotThrow(() =>
td.verify(adapter.setStyles('FC0', {
opacity: '',
transform: '',
transition: ''
})));

clock.uninstall();
t.end();
});

test('maxRadius correctly computed', t => {
const {component} = createComponent();

// Default constructor value, layout/measurements not yet triggered.
t.equal(0, component.maxRadius);

component.layout();

// Non-zero user setting.
component.maxRadius = 30;
t.equal(component.maxRadius, 30);

// Computed.
component.maxRadius = 0;
t.equal(component.maxRadius, 70.71067811865476);
t.end();
});

test('multiple ripples are created and cycled', t => {
const {component, adapter} = createComponent();

for (let i = 0; i < 8; i++) {
component.startTouchEndedAnimationAtPoint(0, 0);
}

t.doesNotThrow(() =>
td.verify(adapter.addClass('F0', Class.FOREGROUND_BOUNDED_ACTIVE), {times: 2}));
t.doesNotThrow(() =>
td.verify(adapter.addClass('F1', Class.FOREGROUND_BOUNDED_ACTIVE), {times: 2}));
t.doesNotThrow(() =>
td.verify(adapter.addClass('F2', Class.FOREGROUND_BOUNDED_ACTIVE), {times: 2}));
t.doesNotThrow(() =>
td.verify(adapter.addClass('F3', Class.FOREGROUND_BOUNDED_ACTIVE), {times: 2}));
t.end();
});

test('press animation is canceled', t => {
const {component, adapter} = createComponent();

component.startTouchBeganAnimationAtPoint(0, 0);
component.cancelAnimations();

t.doesNotThrow(() =>
td.verify(adapter.removeClass(Identifier.BACKGROUND, Class.BACKGROUND_ACTIVE)));
t.end();
});
3 changes: 3 additions & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ module.exports = [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
}, {
test: /\.json$/,
loader: 'json-loader'
}]
}
}, {
Expand Down

0 comments on commit 69cce5b

Please sign in to comment.