wavesurfer.js
Version:
Audio waveform player
279 lines (278 loc) • 10.5 kB
JavaScript
import { signal } from '../store';
import { fromEvent, map, filter, debounce, throttle, cleanup } from '../event-streams';
describe('fromEvent', () => {
let button;
beforeEach(() => {
button = document.createElement('button');
});
it('should convert DOM events to signal', () => {
const clicks = fromEvent(button, 'click');
const callback = jest.fn();
clicks.subscribe(callback);
button.click();
expect(callback).toHaveBeenCalledWith(expect.objectContaining({ type: 'click' }));
});
it('should start with null value', () => {
const clicks = fromEvent(button, 'click');
expect(clicks.value).toBeNull();
});
it('should update signal on each event', () => {
const clicks = fromEvent(button, 'click');
const values = [];
clicks.subscribe((event) => values.push(event));
button.click();
button.click();
expect(values).toHaveLength(2);
expect(values[0]).toMatchObject({ type: 'click' });
expect(values[1]).toMatchObject({ type: 'click' });
});
it('should cleanup event listener on cleanup()', () => {
const clicks = fromEvent(button, 'click');
const callback = jest.fn();
clicks.subscribe(callback);
button.click();
expect(callback).toHaveBeenCalledTimes(1);
cleanup(clicks);
button.click();
expect(callback).toHaveBeenCalledTimes(1); // Should not increase
});
it('should work with different event types', () => {
const input = document.createElement('input');
const changes = fromEvent(input, 'change');
const callback = jest.fn();
changes.subscribe(callback);
input.dispatchEvent(new Event('change'));
expect(callback).toHaveBeenCalledWith(expect.objectContaining({ type: 'change' }));
});
});
describe('map', () => {
it('should transform stream values', () => {
const numbers = signal(5);
const doubled = map(numbers, (n) => n * 2);
expect(doubled.value).toBe(10);
numbers.set(10);
expect(doubled.value).toBe(20);
});
it('should notify subscribers when mapped value changes', () => {
const numbers = signal(5);
const doubled = map(numbers, (n) => n * 2);
const callback = jest.fn();
doubled.subscribe(callback);
numbers.set(10);
expect(callback).toHaveBeenCalledWith(20);
});
it('should work with complex transformations', () => {
const button = document.createElement('button');
const clicks = fromEvent(button, 'click');
const positions = map(clicks, (e) => (e ? e.clientX : 0));
expect(positions.value).toBe(0);
button.dispatchEvent(new MouseEvent('click', { clientX: 100 }));
expect(positions.value).toBe(100);
});
it('should cleanup subscriptions on cleanup()', () => {
const numbers = signal(5);
const doubled = map(numbers, (n) => n * 2);
const callback = jest.fn();
doubled.subscribe(callback);
cleanup(doubled);
numbers.set(10);
expect(callback).not.toHaveBeenCalled();
});
});
describe('filter', () => {
it('should filter stream values by predicate', () => {
const numbers = signal(5);
const evens = filter(numbers, (n) => n % 2 === 0);
expect(evens.value).toBeNull(); // 5 is odd
numbers.set(6);
expect(evens.value).toBe(6); // 6 is even
numbers.set(7);
expect(evens.value).toBeNull(); // 7 is odd
});
it('should emit null for filtered values', () => {
const numbers = signal(2);
const evens = filter(numbers, (n) => n % 2 === 0);
const values = [];
evens.subscribe((v) => values.push(v));
numbers.set(3); // Odd - should emit null
numbers.set(4); // Even - should emit 4
numbers.set(5); // Odd - should emit null
expect(values).toEqual([null, 4, null]);
});
it('should start with correct initial value', () => {
const evens1 = filter(signal(2), (n) => n % 2 === 0);
expect(evens1.value).toBe(2);
const evens2 = filter(signal(3), (n) => n % 2 === 0);
expect(evens2.value).toBeNull();
});
it('should cleanup subscriptions on cleanup()', () => {
const numbers = signal(2);
const evens = filter(numbers, (n) => n % 2 === 0);
const callback = jest.fn();
evens.subscribe(callback);
cleanup(evens);
numbers.set(4);
expect(callback).not.toHaveBeenCalled();
});
});
describe('debounce', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('should delay updates', () => {
const numbers = signal(0);
const debounced = debounce(numbers, 300);
const callback = jest.fn();
debounced.subscribe(callback);
numbers.set(1);
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersByTime(200);
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersByTime(100);
expect(callback).toHaveBeenCalledWith(1);
});
it('should reset timer on new update', () => {
const numbers = signal(0);
const debounced = debounce(numbers, 300);
const callback = jest.fn();
debounced.subscribe(callback);
numbers.set(1);
jest.advanceTimersByTime(200);
numbers.set(2); // Reset timer
jest.advanceTimersByTime(200);
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersByTime(100);
expect(callback).toHaveBeenCalledWith(2);
expect(callback).toHaveBeenCalledTimes(1);
});
it('should emit latest value after delay', () => {
const numbers = signal(0);
const debounced = debounce(numbers, 100);
const values = [];
debounced.subscribe((v) => values.push(v));
numbers.set(1);
numbers.set(2);
numbers.set(3);
jest.advanceTimersByTime(100);
expect(values).toEqual([3]); // Only last value
});
it('should cleanup timeout on cleanup()', () => {
const numbers = signal(0);
const debounced = debounce(numbers, 300);
const callback = jest.fn();
debounced.subscribe(callback);
numbers.set(1);
cleanup(debounced);
jest.advanceTimersByTime(300);
expect(callback).not.toHaveBeenCalled();
});
it('should have initial value', () => {
const numbers = signal(5);
const debounced = debounce(numbers, 300);
expect(debounced.value).toBe(5);
});
});
describe('throttle', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('should emit immediately on first update', () => {
const numbers = signal(0);
const throttled = throttle(numbers, 1000);
const callback = jest.fn();
throttled.subscribe(callback);
numbers.set(1);
expect(callback).toHaveBeenCalledWith(1);
});
it('should throttle subsequent updates', () => {
const numbers = signal(0);
const throttled = throttle(numbers, 1000);
const values = [];
throttled.subscribe((v) => values.push(v));
numbers.set(1); // Immediate
numbers.set(2); // Too soon, scheduled
numbers.set(3); // Too soon, scheduled (replaces 2)
jest.advanceTimersByTime(1000);
expect(values).toEqual([1, 3]); // 1 immediate, 3 after throttle
});
it('should allow emission after throttle period', () => {
const numbers = signal(0);
const throttled = throttle(numbers, 100);
const values = [];
throttled.subscribe((v) => values.push(v));
numbers.set(1); // Immediate
jest.advanceTimersByTime(100);
numbers.set(2); // Enough time passed, immediate
expect(values).toEqual([1, 2]);
});
it('should cleanup timeout on cleanup()', () => {
const numbers = signal(0);
const throttled = throttle(numbers, 1000);
const callback = jest.fn();
throttled.subscribe(callback);
numbers.set(1);
numbers.set(2);
cleanup(throttled);
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledTimes(1); // Only immediate call
});
it('should have initial value', () => {
const numbers = signal(5);
const throttled = throttle(numbers, 1000);
expect(throttled.value).toBe(5);
});
});
describe('stream composition', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('should compose map + filter', () => {
const numbers = signal(1);
const doubled = map(numbers, (n) => n * 2);
const evens = filter(doubled, (n) => n > 5);
expect(evens.value).toBeNull(); // 2 is not > 5
numbers.set(3);
expect(evens.value).toBe(6); // 6 is > 5
numbers.set(2);
expect(evens.value).toBeNull(); // 4 is not > 5
});
it('should compose map + debounce', () => {
const numbers = signal(0);
const doubled = map(numbers, (n) => n * 2);
const debounced = debounce(doubled, 100);
const values = [];
debounced.subscribe((v) => values.push(v));
numbers.set(1);
numbers.set(2);
numbers.set(3);
jest.advanceTimersByTime(100);
expect(values).toEqual([6]); // 3 * 2 = 6
});
it('should compose fromEvent + map + filter + debounce', () => {
const button = document.createElement('button');
const clicks = fromEvent(button, 'click');
const positions = map(clicks, (e) => (e ? e.clientX : 0));
const filtered = filter(positions, (x) => x > 50);
const debounced = debounce(filtered, 200);
const values = [];
debounced.subscribe((v) => values.push(v));
button.dispatchEvent(new MouseEvent('click', { clientX: 100 }));
button.dispatchEvent(new MouseEvent('click', { clientX: 30 })); // Filtered
button.dispatchEvent(new MouseEvent('click', { clientX: 75 }));
jest.advanceTimersByTime(200);
expect(values).toEqual([75]);
cleanup(clicks);
cleanup(positions);
cleanup(filtered);
cleanup(debounced);
});
});