'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); });