Add rich dashboard animations with reduced-motion support
This commit is contained in:
1 parent
25abce19e8
commit
f646339781
6 files changed
+255
-21
No files matched your search
@@ -0,0 +1,109 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const {readFileSync} = require('node:fs');
|
||||
const {test} = require('node:test');
|
||||
const vm = require('node:vm');
|
||||
|
||||
// Load the actual helpers without bootstrapping API polling or page controls.
|
||||
const source = readFileSync(`${__dirname}/../static/dashboard.js`, 'utf8');
|
||||
const helpers = source.slice(0, source.indexOf('\nfunction closeSidebar()'));
|
||||
|
||||
function node(overrides = {}) {
|
||||
return {
|
||||
textContent: '', style: {}, calls: [],
|
||||
getClientRects: () => [1], matches: () => false,
|
||||
querySelectorAll: () => [],
|
||||
animate(frames, options) {
|
||||
const animation = {cancelled: false, cancel() { this.cancelled = true; }};
|
||||
this.calls.push({frames, options, animation});
|
||||
return animation;
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function harness() {
|
||||
const nodes = new Map([['#dashboardConfig', {textContent: '{}'}]]);
|
||||
const preference = {matches: false, addEventListener(type, listener) { this.listener = listener; }};
|
||||
const document = {
|
||||
hidden: false,
|
||||
querySelector: selector => nodes.get(selector), querySelectorAll: () => [],
|
||||
getAnimations: () => [],
|
||||
};
|
||||
const context = vm.createContext({document, window: {matchMedia: () => preference}, localStorage: {getItem: () => null}});
|
||||
vm.runInContext(helpers, context);
|
||||
return {nodes, preference, document, context};
|
||||
}
|
||||
|
||||
test('animations respect reduced motion, background tabs, hidden nodes and older browsers', () => {
|
||||
const h = harness();
|
||||
const target = node();
|
||||
h.context.animate(target, []);
|
||||
assert.equal(target.calls.length, 1);
|
||||
h.preference.matches = true;
|
||||
h.context.animate(target, []);
|
||||
h.preference.matches = false;
|
||||
h.document.hidden = true;
|
||||
h.context.animate(target, []);
|
||||
h.document.hidden = false;
|
||||
target.getClientRects = () => [];
|
||||
h.context.animate(target, []);
|
||||
assert.equal(target.calls.length, 1);
|
||||
assert.doesNotThrow(() => h.context.animate(node({animate: undefined}), []));
|
||||
});
|
||||
|
||||
test('new animations cancel older ones and delayed entrances stay hidden until they start', () => {
|
||||
const h = harness();
|
||||
const target = node();
|
||||
h.context.animate(target, []);
|
||||
h.context.animate(target, [], {delay: 35});
|
||||
assert.equal(target.calls[0].animation.cancelled, true);
|
||||
assert.equal(target.calls[1].options.fill, 'backwards');
|
||||
assert.equal(target.calls[1].options.delay, 35);
|
||||
});
|
||||
|
||||
test('entrance work and stagger delays are bounded for large libraries', () => {
|
||||
const h = harness();
|
||||
const targets = Array.from({length: 1000}, () => node());
|
||||
h.context.animateEntrance(node({querySelectorAll: () => targets}));
|
||||
assert.equal(targets.filter(target => target.calls.length).length, 24);
|
||||
assert.equal(targets[23].calls[0].options.delay, 210);
|
||||
});
|
||||
|
||||
test('unchanged metrics do not animate again', () => {
|
||||
const h = harness();
|
||||
const target = node({matches: () => true});
|
||||
h.nodes.set('#cpuValue', target);
|
||||
vm.runInContext("set('cpuValue', '12.0%'); set('cpuValue', '12.0%');", h.context);
|
||||
assert.equal(target.textContent, '12.0%');
|
||||
assert.equal(target.calls.length, 1);
|
||||
});
|
||||
|
||||
test('polling preserves progress and does not replay container entrances', () => {
|
||||
const h = harness();
|
||||
let markup = '', bars = [], writes = 0;
|
||||
const card = node();
|
||||
const root = node({querySelectorAll: selector => selector === '.track > span' ? bars : [card]});
|
||||
Object.defineProperty(root, 'innerHTML', {
|
||||
get: () => markup,
|
||||
set(value) { markup = value; writes++; bars = [node({style: {width: value}})]; },
|
||||
});
|
||||
h.nodes.set('#progress', root);
|
||||
h.context.renderContent('progress', '25%');
|
||||
assert.equal(bars[0].calls[0].frames[0].width, '0%');
|
||||
h.context.renderContent('progress', '40%');
|
||||
assert.equal(bars[0].calls[0].frames[0].width, '25%');
|
||||
assert.equal(bars[0].calls[0].frames[1].width, '40%');
|
||||
assert.equal(card.calls.length, 1);
|
||||
h.context.renderContent('progress', '40%');
|
||||
assert.equal(writes, 2);
|
||||
});
|
||||
|
||||
test('enabling reduced motion cancels animations already in flight', () => {
|
||||
const h = harness();
|
||||
const animation = {cancelled: false, cancel() { this.cancelled = true; }};
|
||||
h.document.getAnimations = () => [animation];
|
||||
h.preference.listener({matches: true});
|
||||
assert.equal(animation.cancelled, true);
|
||||
});
|
||||
Reference in new issue
Block a user