wavesurfer.js
Version:
Audio waveform player
217 lines (216 loc) • 9.27 kB
JavaScript
import { RenderScheduler } from '../render-scheduler';
describe('RenderScheduler', () => {
let scheduler;
let renderFn;
beforeEach(() => {
scheduler = new RenderScheduler();
renderFn = jest.fn();
jest.useFakeTimers();
});
afterEach(() => {
scheduler.cancelRender();
jest.restoreAllMocks();
jest.useRealTimers();
});
describe('scheduleRender', () => {
it('should schedule a render on next frame', () => {
scheduler.scheduleRender(renderFn);
expect(renderFn).not.toHaveBeenCalled();
// Advance to next frame
jest.advanceTimersByTime(16);
expect(renderFn).toHaveBeenCalledTimes(1);
});
it('should batch multiple render requests into one', () => {
scheduler.scheduleRender(renderFn);
scheduler.scheduleRender(renderFn);
scheduler.scheduleRender(renderFn);
expect(renderFn).not.toHaveBeenCalled();
jest.advanceTimersByTime(16);
expect(renderFn).toHaveBeenCalledTimes(1);
});
it('should allow new render after previous completes', () => {
scheduler.scheduleRender(renderFn);
jest.advanceTimersByTime(16);
expect(renderFn).toHaveBeenCalledTimes(1);
scheduler.scheduleRender(renderFn);
jest.advanceTimersByTime(16);
expect(renderFn).toHaveBeenCalledTimes(2);
});
it('should execute high priority renders immediately', () => {
scheduler.scheduleRender(renderFn, 'high');
expect(renderFn).toHaveBeenCalledTimes(1);
// No additional call on next frame
jest.advanceTimersByTime(16);
expect(renderFn).toHaveBeenCalledTimes(1);
});
it('should cancel batched render when high priority render is called', () => {
const batchedRender = jest.fn();
const highPriorityRender = jest.fn();
scheduler.scheduleRender(batchedRender, 'normal');
expect(batchedRender).not.toHaveBeenCalled();
scheduler.scheduleRender(highPriorityRender, 'high');
expect(highPriorityRender).toHaveBeenCalledTimes(1);
// Batched render should still execute
jest.advanceTimersByTime(16);
expect(batchedRender).not.toHaveBeenCalled();
});
it('should handle low priority same as normal', () => {
scheduler.scheduleRender(renderFn, 'low');
expect(renderFn).not.toHaveBeenCalled();
jest.advanceTimersByTime(16);
expect(renderFn).toHaveBeenCalledTimes(1);
});
});
describe('cancelRender', () => {
it('should cancel pending render', () => {
scheduler.scheduleRender(renderFn);
scheduler.cancelRender();
jest.advanceTimersByTime(16);
expect(renderFn).not.toHaveBeenCalled();
});
it('should allow scheduling after cancel', () => {
scheduler.scheduleRender(renderFn);
scheduler.cancelRender();
scheduler.scheduleRender(renderFn);
jest.advanceTimersByTime(16);
expect(renderFn).toHaveBeenCalledTimes(1);
});
it('should be safe to call multiple times', () => {
scheduler.scheduleRender(renderFn);
scheduler.cancelRender();
scheduler.cancelRender();
scheduler.cancelRender();
jest.advanceTimersByTime(16);
expect(renderFn).not.toHaveBeenCalled();
});
it('should be safe to call when no render is pending', () => {
expect(() => scheduler.cancelRender()).not.toThrow();
});
});
describe('flushRender', () => {
it('should execute render immediately', () => {
scheduler.flushRender(renderFn);
expect(renderFn).toHaveBeenCalledTimes(1);
});
it('should cancel pending batched render', () => {
const batchedRender = jest.fn();
const flushRender = jest.fn();
scheduler.scheduleRender(batchedRender);
scheduler.flushRender(flushRender);
expect(flushRender).toHaveBeenCalledTimes(1);
expect(batchedRender).not.toHaveBeenCalled();
jest.advanceTimersByTime(16);
expect(batchedRender).not.toHaveBeenCalled();
});
it('should work for testing scenarios', () => {
let renderCount = 0;
const testRender = () => renderCount++;
// Simulate multiple state changes
scheduler.scheduleRender(testRender);
scheduler.scheduleRender(testRender);
scheduler.scheduleRender(testRender);
// Force immediate render for assertion
scheduler.flushRender(testRender);
expect(renderCount).toBe(1);
// No additional render on next frame
jest.advanceTimersByTime(16);
expect(renderCount).toBe(1);
});
});
describe('isPending', () => {
it('should return false initially', () => {
expect(scheduler.isPending()).toBe(false);
});
it('should return true when render is scheduled', () => {
scheduler.scheduleRender(renderFn);
expect(scheduler.isPending()).toBe(true);
});
it('should return false after render executes', () => {
scheduler.scheduleRender(renderFn);
jest.advanceTimersByTime(16);
expect(scheduler.isPending()).toBe(false);
});
it('should return false after cancel', () => {
scheduler.scheduleRender(renderFn);
scheduler.cancelRender();
expect(scheduler.isPending()).toBe(false);
});
it('should return false after flush', () => {
scheduler.scheduleRender(renderFn);
scheduler.flushRender(renderFn);
expect(scheduler.isPending()).toBe(false);
});
it('should return false for high priority renders', () => {
scheduler.scheduleRender(renderFn, 'high');
expect(scheduler.isPending()).toBe(false);
});
});
describe('real-world scenarios', () => {
it('should batch rapid state changes', () => {
let renderCount = 0;
const render = () => renderCount++;
// Simulate rapid state changes (e.g., during animation)
for (let i = 0; i < 100; i++) {
scheduler.scheduleRender(render);
}
expect(renderCount).toBe(0);
jest.advanceTimersByTime(16);
expect(renderCount).toBe(1);
});
it('should handle mixed priority renders', () => {
const normalRenders = [];
const highRenders = [];
scheduler.scheduleRender(() => normalRenders.push(1), 'normal');
scheduler.scheduleRender(() => highRenders.push(1), 'high');
scheduler.scheduleRender(() => normalRenders.push(2), 'normal');
scheduler.scheduleRender(() => highRenders.push(2), 'high');
expect(highRenders).toEqual([1, 2]);
expect(normalRenders).toEqual([]);
jest.advanceTimersByTime(16);
expect(normalRenders).toEqual([]);
});
it('should handle errors in render function', () => {
const errorRender = () => {
throw new Error('Render error');
};
const successRender = jest.fn();
scheduler.scheduleRender(errorRender);
expect(() => {
jest.advanceTimersByTime(16);
}).toThrow('Render error');
// After error, scheduler is no longer pending
expect(scheduler.isPending()).toBe(false);
// Should be able to schedule after error
scheduler.scheduleRender(successRender);
expect(scheduler.isPending()).toBe(true);
jest.advanceTimersByTime(16);
expect(successRender).toHaveBeenCalledTimes(1);
});
it('should support cleanup on unmount', () => {
scheduler.scheduleRender(renderFn);
expect(scheduler.isPending()).toBe(true);
// Simulate component unmount
scheduler.cancelRender();
expect(scheduler.isPending()).toBe(false);
jest.advanceTimersByTime(100);
expect(renderFn).not.toHaveBeenCalled();
});
});
describe('performance', () => {
it('should handle thousands of schedule calls efficiently', () => {
const start = performance.now();
for (let i = 0; i < 10000; i++) {
scheduler.scheduleRender(renderFn);
}
const duration = performance.now() - start;
expect(duration).toBeLessThan(10); // Should be near-instant
});
it('should not leak memory with repeated scheduling', () => {
for (let i = 0; i < 1000; i++) {
scheduler.scheduleRender(renderFn);
scheduler.cancelRender();
}
expect(scheduler.isPending()).toBe(false);
});
});
});