wavesurfer.js 7.11.1 → 7.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +1 -1
  2. package/dist/__tests__/memory-leaks.test.d.ts +7 -0
  3. package/dist/__tests__/memory-leaks.test.js +281 -0
  4. package/dist/__tests__/player.test.js +107 -0
  5. package/dist/__tests__/renderer-utils.test.js +55 -2
  6. package/dist/draggable.d.ts +4 -0
  7. package/dist/draggable.js +4 -0
  8. package/dist/player.d.ts +21 -0
  9. package/dist/player.js +86 -0
  10. package/dist/plugins/envelope.cjs +1 -1
  11. package/dist/plugins/envelope.esm.js +1 -1
  12. package/dist/plugins/envelope.js +1 -1
  13. package/dist/plugins/envelope.min.js +1 -1
  14. package/dist/plugins/hover.cjs +1 -1
  15. package/dist/plugins/hover.d.ts +0 -3
  16. package/dist/plugins/hover.esm.js +1 -1
  17. package/dist/plugins/hover.js +1 -1
  18. package/dist/plugins/hover.min.js +1 -1
  19. package/dist/plugins/minimap.cjs +1 -1
  20. package/dist/plugins/minimap.esm.js +1 -1
  21. package/dist/plugins/minimap.js +1 -1
  22. package/dist/plugins/minimap.min.js +1 -1
  23. package/dist/plugins/record.cjs +1 -1
  24. package/dist/plugins/record.esm.js +1 -1
  25. package/dist/plugins/record.js +1 -1
  26. package/dist/plugins/record.min.js +1 -1
  27. package/dist/plugins/regions.cjs +1 -1
  28. package/dist/plugins/regions.esm.js +1 -1
  29. package/dist/plugins/regions.js +1 -1
  30. package/dist/plugins/regions.min.js +1 -1
  31. package/dist/plugins/timeline.cjs +1 -1
  32. package/dist/plugins/timeline.d.ts +1 -1
  33. package/dist/plugins/timeline.esm.js +1 -1
  34. package/dist/plugins/timeline.js +1 -1
  35. package/dist/plugins/timeline.min.js +1 -1
  36. package/dist/plugins/zoom.cjs +1 -1
  37. package/dist/plugins/zoom.esm.js +1 -1
  38. package/dist/plugins/zoom.js +1 -1
  39. package/dist/plugins/zoom.min.js +1 -1
  40. package/dist/reactive/__tests__/drag-stream.test.d.ts +1 -0
  41. package/dist/reactive/__tests__/drag-stream.test.js +199 -0
  42. package/dist/reactive/__tests__/event-stream-emitter.test.d.ts +1 -0
  43. package/dist/reactive/__tests__/event-stream-emitter.test.js +230 -0
  44. package/dist/reactive/__tests__/event-streams.test.d.ts +1 -0
  45. package/dist/reactive/__tests__/event-streams.test.js +278 -0
  46. package/dist/reactive/__tests__/media-event-bridge.test.d.ts +1 -0
  47. package/dist/reactive/__tests__/media-event-bridge.test.js +202 -0
  48. package/dist/reactive/__tests__/render-scheduler.test.d.ts +1 -0
  49. package/dist/reactive/__tests__/render-scheduler.test.js +216 -0
  50. package/dist/reactive/__tests__/scroll-stream.test.d.ts +1 -0
  51. package/dist/reactive/__tests__/scroll-stream.test.js +191 -0
  52. package/dist/reactive/__tests__/state-event-emitter.test.d.ts +1 -0
  53. package/dist/reactive/__tests__/state-event-emitter.test.js +234 -0
  54. package/dist/reactive/__tests__/store.test.d.ts +1 -0
  55. package/dist/reactive/__tests__/store.test.js +271 -0
  56. package/dist/reactive/drag-stream.d.ts +48 -0
  57. package/dist/reactive/drag-stream.js +146 -0
  58. package/dist/reactive/event-stream-emitter.d.ts +104 -0
  59. package/dist/reactive/event-stream-emitter.js +151 -0
  60. package/dist/reactive/event-streams.d.ts +65 -0
  61. package/dist/reactive/event-streams.js +146 -0
  62. package/dist/reactive/media-event-bridge.d.ts +51 -0
  63. package/dist/reactive/media-event-bridge.js +152 -0
  64. package/dist/reactive/render-scheduler.d.ts +43 -0
  65. package/dist/reactive/render-scheduler.js +75 -0
  66. package/dist/reactive/scroll-stream.d.ts +93 -0
  67. package/dist/reactive/scroll-stream.js +131 -0
  68. package/dist/reactive/state-event-emitter.d.ts +105 -0
  69. package/dist/reactive/state-event-emitter.js +213 -0
  70. package/dist/reactive/store.d.ts +66 -0
  71. package/dist/reactive/store.js +115 -0
  72. package/dist/renderer-utils.d.ts +13 -4
  73. package/dist/renderer-utils.js +35 -15
  74. package/dist/renderer.d.ts +2 -1
  75. package/dist/renderer.js +51 -48
  76. package/dist/state/__tests__/wavesurfer-state.test.d.ts +1 -0
  77. package/dist/state/__tests__/wavesurfer-state.test.js +268 -0
  78. package/dist/state/wavesurfer-state.d.ts +90 -0
  79. package/dist/state/wavesurfer-state.js +124 -0
  80. package/dist/types.d.ts +165 -1
  81. package/dist/wavesurfer.cjs +1 -1
  82. package/dist/wavesurfer.d.ts +16 -0
  83. package/dist/wavesurfer.esm.js +1 -1
  84. package/dist/wavesurfer.js +32 -0
  85. package/dist/wavesurfer.min.js +1 -1
  86. package/package.json +2 -3
@@ -0,0 +1,216 @@
1
+ import { RenderScheduler } from '../render-scheduler';
2
+ describe('RenderScheduler', () => {
3
+ let scheduler;
4
+ let renderFn;
5
+ beforeEach(() => {
6
+ scheduler = new RenderScheduler();
7
+ renderFn = jest.fn();
8
+ jest.useFakeTimers();
9
+ });
10
+ afterEach(() => {
11
+ scheduler.cancelRender();
12
+ jest.restoreAllMocks();
13
+ jest.useRealTimers();
14
+ });
15
+ describe('scheduleRender', () => {
16
+ it('should schedule a render on next frame', () => {
17
+ scheduler.scheduleRender(renderFn);
18
+ expect(renderFn).not.toHaveBeenCalled();
19
+ // Advance to next frame
20
+ jest.advanceTimersByTime(16);
21
+ expect(renderFn).toHaveBeenCalledTimes(1);
22
+ });
23
+ it('should batch multiple render requests into one', () => {
24
+ scheduler.scheduleRender(renderFn);
25
+ scheduler.scheduleRender(renderFn);
26
+ scheduler.scheduleRender(renderFn);
27
+ expect(renderFn).not.toHaveBeenCalled();
28
+ jest.advanceTimersByTime(16);
29
+ expect(renderFn).toHaveBeenCalledTimes(1);
30
+ });
31
+ it('should allow new render after previous completes', () => {
32
+ scheduler.scheduleRender(renderFn);
33
+ jest.advanceTimersByTime(16);
34
+ expect(renderFn).toHaveBeenCalledTimes(1);
35
+ scheduler.scheduleRender(renderFn);
36
+ jest.advanceTimersByTime(16);
37
+ expect(renderFn).toHaveBeenCalledTimes(2);
38
+ });
39
+ it('should execute high priority renders immediately', () => {
40
+ scheduler.scheduleRender(renderFn, 'high');
41
+ expect(renderFn).toHaveBeenCalledTimes(1);
42
+ // No additional call on next frame
43
+ jest.advanceTimersByTime(16);
44
+ expect(renderFn).toHaveBeenCalledTimes(1);
45
+ });
46
+ it('should cancel batched render when high priority render is called', () => {
47
+ const batchedRender = jest.fn();
48
+ const highPriorityRender = jest.fn();
49
+ scheduler.scheduleRender(batchedRender, 'normal');
50
+ expect(batchedRender).not.toHaveBeenCalled();
51
+ scheduler.scheduleRender(highPriorityRender, 'high');
52
+ expect(highPriorityRender).toHaveBeenCalledTimes(1);
53
+ // Batched render should still execute
54
+ jest.advanceTimersByTime(16);
55
+ expect(batchedRender).not.toHaveBeenCalled();
56
+ });
57
+ it('should handle low priority same as normal', () => {
58
+ scheduler.scheduleRender(renderFn, 'low');
59
+ expect(renderFn).not.toHaveBeenCalled();
60
+ jest.advanceTimersByTime(16);
61
+ expect(renderFn).toHaveBeenCalledTimes(1);
62
+ });
63
+ });
64
+ describe('cancelRender', () => {
65
+ it('should cancel pending render', () => {
66
+ scheduler.scheduleRender(renderFn);
67
+ scheduler.cancelRender();
68
+ jest.advanceTimersByTime(16);
69
+ expect(renderFn).not.toHaveBeenCalled();
70
+ });
71
+ it('should allow scheduling after cancel', () => {
72
+ scheduler.scheduleRender(renderFn);
73
+ scheduler.cancelRender();
74
+ scheduler.scheduleRender(renderFn);
75
+ jest.advanceTimersByTime(16);
76
+ expect(renderFn).toHaveBeenCalledTimes(1);
77
+ });
78
+ it('should be safe to call multiple times', () => {
79
+ scheduler.scheduleRender(renderFn);
80
+ scheduler.cancelRender();
81
+ scheduler.cancelRender();
82
+ scheduler.cancelRender();
83
+ jest.advanceTimersByTime(16);
84
+ expect(renderFn).not.toHaveBeenCalled();
85
+ });
86
+ it('should be safe to call when no render is pending', () => {
87
+ expect(() => scheduler.cancelRender()).not.toThrow();
88
+ });
89
+ });
90
+ describe('flushRender', () => {
91
+ it('should execute render immediately', () => {
92
+ scheduler.flushRender(renderFn);
93
+ expect(renderFn).toHaveBeenCalledTimes(1);
94
+ });
95
+ it('should cancel pending batched render', () => {
96
+ const batchedRender = jest.fn();
97
+ const flushRender = jest.fn();
98
+ scheduler.scheduleRender(batchedRender);
99
+ scheduler.flushRender(flushRender);
100
+ expect(flushRender).toHaveBeenCalledTimes(1);
101
+ expect(batchedRender).not.toHaveBeenCalled();
102
+ jest.advanceTimersByTime(16);
103
+ expect(batchedRender).not.toHaveBeenCalled();
104
+ });
105
+ it('should work for testing scenarios', () => {
106
+ let renderCount = 0;
107
+ const testRender = () => renderCount++;
108
+ // Simulate multiple state changes
109
+ scheduler.scheduleRender(testRender);
110
+ scheduler.scheduleRender(testRender);
111
+ scheduler.scheduleRender(testRender);
112
+ // Force immediate render for assertion
113
+ scheduler.flushRender(testRender);
114
+ expect(renderCount).toBe(1);
115
+ // No additional render on next frame
116
+ jest.advanceTimersByTime(16);
117
+ expect(renderCount).toBe(1);
118
+ });
119
+ });
120
+ describe('isPending', () => {
121
+ it('should return false initially', () => {
122
+ expect(scheduler.isPending()).toBe(false);
123
+ });
124
+ it('should return true when render is scheduled', () => {
125
+ scheduler.scheduleRender(renderFn);
126
+ expect(scheduler.isPending()).toBe(true);
127
+ });
128
+ it('should return false after render executes', () => {
129
+ scheduler.scheduleRender(renderFn);
130
+ jest.advanceTimersByTime(16);
131
+ expect(scheduler.isPending()).toBe(false);
132
+ });
133
+ it('should return false after cancel', () => {
134
+ scheduler.scheduleRender(renderFn);
135
+ scheduler.cancelRender();
136
+ expect(scheduler.isPending()).toBe(false);
137
+ });
138
+ it('should return false after flush', () => {
139
+ scheduler.scheduleRender(renderFn);
140
+ scheduler.flushRender(renderFn);
141
+ expect(scheduler.isPending()).toBe(false);
142
+ });
143
+ it('should return false for high priority renders', () => {
144
+ scheduler.scheduleRender(renderFn, 'high');
145
+ expect(scheduler.isPending()).toBe(false);
146
+ });
147
+ });
148
+ describe('real-world scenarios', () => {
149
+ it('should batch rapid state changes', () => {
150
+ let renderCount = 0;
151
+ const render = () => renderCount++;
152
+ // Simulate rapid state changes (e.g., during animation)
153
+ for (let i = 0; i < 100; i++) {
154
+ scheduler.scheduleRender(render);
155
+ }
156
+ expect(renderCount).toBe(0);
157
+ jest.advanceTimersByTime(16);
158
+ expect(renderCount).toBe(1);
159
+ });
160
+ it('should handle mixed priority renders', () => {
161
+ const normalRenders = [];
162
+ const highRenders = [];
163
+ scheduler.scheduleRender(() => normalRenders.push(1), 'normal');
164
+ scheduler.scheduleRender(() => highRenders.push(1), 'high');
165
+ scheduler.scheduleRender(() => normalRenders.push(2), 'normal');
166
+ scheduler.scheduleRender(() => highRenders.push(2), 'high');
167
+ expect(highRenders).toEqual([1, 2]);
168
+ expect(normalRenders).toEqual([]);
169
+ jest.advanceTimersByTime(16);
170
+ expect(normalRenders).toEqual([]);
171
+ });
172
+ it('should handle errors in render function', () => {
173
+ const errorRender = () => {
174
+ throw new Error('Render error');
175
+ };
176
+ const successRender = jest.fn();
177
+ scheduler.scheduleRender(errorRender);
178
+ expect(() => {
179
+ jest.advanceTimersByTime(16);
180
+ }).toThrow('Render error');
181
+ // After error, scheduler is no longer pending
182
+ expect(scheduler.isPending()).toBe(false);
183
+ // Should be able to schedule after error
184
+ scheduler.scheduleRender(successRender);
185
+ expect(scheduler.isPending()).toBe(true);
186
+ jest.advanceTimersByTime(16);
187
+ expect(successRender).toHaveBeenCalledTimes(1);
188
+ });
189
+ it('should support cleanup on unmount', () => {
190
+ scheduler.scheduleRender(renderFn);
191
+ expect(scheduler.isPending()).toBe(true);
192
+ // Simulate component unmount
193
+ scheduler.cancelRender();
194
+ expect(scheduler.isPending()).toBe(false);
195
+ jest.advanceTimersByTime(100);
196
+ expect(renderFn).not.toHaveBeenCalled();
197
+ });
198
+ });
199
+ describe('performance', () => {
200
+ it('should handle thousands of schedule calls efficiently', () => {
201
+ const start = performance.now();
202
+ for (let i = 0; i < 10000; i++) {
203
+ scheduler.scheduleRender(renderFn);
204
+ }
205
+ const duration = performance.now() - start;
206
+ expect(duration).toBeLessThan(10); // Should be near-instant
207
+ });
208
+ it('should not leak memory with repeated scheduling', () => {
209
+ for (let i = 0; i < 1000; i++) {
210
+ scheduler.scheduleRender(renderFn);
211
+ scheduler.cancelRender();
212
+ }
213
+ expect(scheduler.isPending()).toBe(false);
214
+ });
215
+ });
216
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,191 @@
1
+ import { createScrollStream, createScrollStreamWithAction, calculateScrollPercentages, calculateScrollBounds, } from '../scroll-stream';
2
+ describe('scroll-stream', () => {
3
+ describe('calculateScrollPercentages', () => {
4
+ it('should calculate percentages for basic scroll', () => {
5
+ const data = {
6
+ scrollLeft: 100,
7
+ scrollWidth: 1000,
8
+ clientWidth: 200,
9
+ };
10
+ const result = calculateScrollPercentages(data);
11
+ expect(result.startX).toBe(0.1);
12
+ expect(result.endX).toBe(0.3);
13
+ });
14
+ it('should handle zero scroll width', () => {
15
+ const data = {
16
+ scrollLeft: 0,
17
+ scrollWidth: 0,
18
+ clientWidth: 200,
19
+ };
20
+ const result = calculateScrollPercentages(data);
21
+ expect(result.startX).toBe(0);
22
+ expect(result.endX).toBe(1);
23
+ });
24
+ it('should handle scroll at start', () => {
25
+ const data = {
26
+ scrollLeft: 0,
27
+ scrollWidth: 1000,
28
+ clientWidth: 200,
29
+ };
30
+ const result = calculateScrollPercentages(data);
31
+ expect(result.startX).toBe(0);
32
+ expect(result.endX).toBe(0.2);
33
+ });
34
+ it('should handle scroll at end', () => {
35
+ const data = {
36
+ scrollLeft: 800,
37
+ scrollWidth: 1000,
38
+ clientWidth: 200,
39
+ };
40
+ const result = calculateScrollPercentages(data);
41
+ expect(result.startX).toBe(0.8);
42
+ expect(result.endX).toBe(1);
43
+ });
44
+ it('should clamp values to 0-1 range', () => {
45
+ const data = {
46
+ scrollLeft: -10,
47
+ scrollWidth: 1000,
48
+ clientWidth: 200,
49
+ };
50
+ const result = calculateScrollPercentages(data);
51
+ expect(result.startX).toBeGreaterThanOrEqual(0);
52
+ expect(result.endX).toBeLessThanOrEqual(1);
53
+ });
54
+ });
55
+ describe('calculateScrollBounds', () => {
56
+ it('should calculate scroll bounds', () => {
57
+ const data = {
58
+ scrollLeft: 100,
59
+ scrollWidth: 1000,
60
+ clientWidth: 200,
61
+ };
62
+ const result = calculateScrollBounds(data);
63
+ expect(result.left).toBe(100);
64
+ expect(result.right).toBe(300);
65
+ });
66
+ it('should handle zero scroll', () => {
67
+ const data = {
68
+ scrollLeft: 0,
69
+ scrollWidth: 1000,
70
+ clientWidth: 200,
71
+ };
72
+ const result = calculateScrollBounds(data);
73
+ expect(result.left).toBe(0);
74
+ expect(result.right).toBe(200);
75
+ });
76
+ });
77
+ describe('createScrollStream', () => {
78
+ let element;
79
+ beforeEach(() => {
80
+ element = document.createElement('div');
81
+ document.body.appendChild(element);
82
+ // Mock scroll properties
83
+ Object.defineProperties(element, {
84
+ scrollLeft: { value: 100, writable: true, configurable: true },
85
+ scrollWidth: { value: 1000, writable: true, configurable: true },
86
+ clientWidth: { value: 200, writable: true, configurable: true },
87
+ });
88
+ });
89
+ afterEach(() => {
90
+ document.body.removeChild(element);
91
+ });
92
+ it('should create a scroll stream', () => {
93
+ const stream = createScrollStream(element);
94
+ expect(stream.scrollData).toBeDefined();
95
+ expect(stream.percentages).toBeDefined();
96
+ expect(stream.bounds).toBeDefined();
97
+ expect(stream.cleanup).toBeDefined();
98
+ stream.cleanup();
99
+ });
100
+ it('should initialize with current scroll values', () => {
101
+ const stream = createScrollStream(element);
102
+ expect(stream.scrollData.value).toEqual({
103
+ scrollLeft: 100,
104
+ scrollWidth: 1000,
105
+ clientWidth: 200,
106
+ });
107
+ stream.cleanup();
108
+ });
109
+ it('should compute percentages', () => {
110
+ const stream = createScrollStream(element);
111
+ expect(stream.percentages.value.startX).toBe(0.1);
112
+ expect(stream.percentages.value.endX).toBe(0.3);
113
+ stream.cleanup();
114
+ });
115
+ it('should compute bounds', () => {
116
+ const stream = createScrollStream(element);
117
+ expect(stream.bounds.value.left).toBe(100);
118
+ expect(stream.bounds.value.right).toBe(300);
119
+ stream.cleanup();
120
+ });
121
+ it('should update on scroll event', () => {
122
+ const stream = createScrollStream(element);
123
+ // Update scroll position
124
+ Object.defineProperty(element, 'scrollLeft', {
125
+ value: 200,
126
+ writable: true,
127
+ configurable: true,
128
+ });
129
+ // Dispatch scroll event
130
+ element.dispatchEvent(new Event('scroll'));
131
+ expect(stream.scrollData.value.scrollLeft).toBe(200);
132
+ expect(stream.percentages.value.startX).toBe(0.2);
133
+ expect(stream.percentages.value.endX).toBe(0.4);
134
+ stream.cleanup();
135
+ });
136
+ it('should cleanup event listeners', () => {
137
+ const stream = createScrollStream(element);
138
+ const addSpy = jest.spyOn(element, 'addEventListener');
139
+ const removeSpy = jest.spyOn(element, 'removeEventListener');
140
+ stream.cleanup();
141
+ expect(removeSpy).toHaveBeenCalledWith('scroll', expect.any(Function));
142
+ addSpy.mockRestore();
143
+ removeSpy.mockRestore();
144
+ });
145
+ });
146
+ describe('createScrollStreamWithAction', () => {
147
+ let element;
148
+ beforeEach(() => {
149
+ element = document.createElement('div');
150
+ document.body.appendChild(element);
151
+ Object.defineProperties(element, {
152
+ scrollLeft: { value: 100, writable: true, configurable: true },
153
+ scrollWidth: { value: 1000, writable: true, configurable: true },
154
+ clientWidth: { value: 200, writable: true, configurable: true },
155
+ });
156
+ });
157
+ afterEach(() => {
158
+ document.body.removeChild(element);
159
+ });
160
+ it('should call action on scroll', () => {
161
+ const onScrollChange = jest.fn();
162
+ const stream = createScrollStreamWithAction(element, onScrollChange);
163
+ // Initial call from effect
164
+ expect(onScrollChange).toHaveBeenCalledWith(100);
165
+ // Update scroll
166
+ Object.defineProperty(element, 'scrollLeft', {
167
+ value: 200,
168
+ writable: true,
169
+ configurable: true,
170
+ });
171
+ element.dispatchEvent(new Event('scroll'));
172
+ expect(onScrollChange).toHaveBeenCalledWith(200);
173
+ stream.cleanup();
174
+ });
175
+ it('should cleanup action effect', () => {
176
+ const onScrollChange = jest.fn();
177
+ const stream = createScrollStreamWithAction(element, onScrollChange);
178
+ const callCount = onScrollChange.mock.calls.length;
179
+ stream.cleanup();
180
+ // Update scroll after cleanup
181
+ Object.defineProperty(element, 'scrollLeft', {
182
+ value: 300,
183
+ writable: true,
184
+ configurable: true,
185
+ });
186
+ element.dispatchEvent(new Event('scroll'));
187
+ // Should not be called again
188
+ expect(onScrollChange).toHaveBeenCalledTimes(callCount);
189
+ });
190
+ });
191
+ });
@@ -0,0 +1,234 @@
1
+ import { setupStateEventEmission, setupSignalEventEmission, setupDebouncedEventEmission, setupConditionalEventEmission, } from '../state-event-emitter';
2
+ import { createWaveSurferState } from '../../state/wavesurfer-state';
3
+ import { signal } from '../store';
4
+ describe('state-event-emitter', () => {
5
+ let emitter;
6
+ beforeEach(() => {
7
+ emitter = { emit: jest.fn() };
8
+ jest.useFakeTimers();
9
+ });
10
+ afterEach(() => {
11
+ jest.useRealTimers();
12
+ });
13
+ describe('setupStateEventEmission', () => {
14
+ it('should emit play event when isPlaying becomes true', () => {
15
+ const { state, actions } = createWaveSurferState();
16
+ const cleanup = setupStateEventEmission(state, emitter);
17
+ // Initial state triggers pause (isPlaying starts false)
18
+ expect(emitter.emit).toHaveBeenCalledWith('pause');
19
+ emitter.emit.mockClear();
20
+ actions.setPlaying(true);
21
+ expect(emitter.emit).toHaveBeenCalledWith('play');
22
+ cleanup();
23
+ });
24
+ it('should emit pause event when isPlaying becomes false', () => {
25
+ const { state, actions } = createWaveSurferState();
26
+ actions.setPlaying(true);
27
+ emitter.emit.mockClear();
28
+ const cleanup = setupStateEventEmission(state, emitter);
29
+ actions.setPlaying(false);
30
+ expect(emitter.emit).toHaveBeenCalledWith('pause');
31
+ cleanup();
32
+ });
33
+ it('should emit timeupdate when currentTime changes', () => {
34
+ const { state, actions } = createWaveSurferState();
35
+ const cleanup = setupStateEventEmission(state, emitter);
36
+ emitter.emit.mockClear();
37
+ actions.setCurrentTime(42);
38
+ expect(emitter.emit).toHaveBeenCalledWith('timeupdate', 42);
39
+ cleanup();
40
+ });
41
+ it('should emit audioprocess when playing and time changes', () => {
42
+ const { state, actions } = createWaveSurferState();
43
+ actions.setPlaying(true);
44
+ const cleanup = setupStateEventEmission(state, emitter);
45
+ emitter.emit.mockClear();
46
+ actions.setCurrentTime(10);
47
+ expect(emitter.emit).toHaveBeenCalledWith('timeupdate', 10);
48
+ expect(emitter.emit).toHaveBeenCalledWith('audioprocess', 10);
49
+ cleanup();
50
+ });
51
+ it('should not emit audioprocess when paused', () => {
52
+ const { state, actions } = createWaveSurferState();
53
+ actions.setPlaying(false);
54
+ const cleanup = setupStateEventEmission(state, emitter);
55
+ emitter.emit.mockClear();
56
+ actions.setCurrentTime(10);
57
+ expect(emitter.emit).toHaveBeenCalledWith('timeupdate', 10);
58
+ expect(emitter.emit).not.toHaveBeenCalledWith('audioprocess', expect.anything());
59
+ cleanup();
60
+ });
61
+ it('should emit seeking event when isSeeking becomes true', () => {
62
+ const { state, actions } = createWaveSurferState();
63
+ actions.setCurrentTime(50);
64
+ const cleanup = setupStateEventEmission(state, emitter);
65
+ emitter.emit.mockClear();
66
+ actions.setSeeking(true);
67
+ expect(emitter.emit).toHaveBeenCalledWith('seeking', 50);
68
+ cleanup();
69
+ });
70
+ it('should emit ready event when state becomes ready', () => {
71
+ const { state, actions } = createWaveSurferState();
72
+ const cleanup = setupStateEventEmission(state, emitter);
73
+ emitter.emit.mockClear();
74
+ // Set duration and audio buffer to make state ready
75
+ actions.setDuration(100);
76
+ // Create a mock AudioBuffer since jsdom doesn't support it
77
+ const mockAudioBuffer = {
78
+ duration: 100,
79
+ length: 44100,
80
+ sampleRate: 44100,
81
+ numberOfChannels: 2,
82
+ };
83
+ actions.setAudioBuffer(mockAudioBuffer);
84
+ expect(emitter.emit).toHaveBeenCalledWith('ready', 100);
85
+ cleanup();
86
+ });
87
+ it('should only emit ready event once', () => {
88
+ const { state, actions } = createWaveSurferState();
89
+ const cleanup = setupStateEventEmission(state, emitter);
90
+ // Make ready
91
+ actions.setDuration(100);
92
+ const mockAudioBuffer = {
93
+ duration: 100,
94
+ length: 44100,
95
+ sampleRate: 44100,
96
+ numberOfChannels: 2,
97
+ };
98
+ actions.setAudioBuffer(mockAudioBuffer);
99
+ const readyCallCount = emitter.emit.mock.calls.filter((call) => call[0] === 'ready').length;
100
+ emitter.emit.mockClear();
101
+ // Change duration again
102
+ actions.setDuration(150);
103
+ // Should not emit ready again
104
+ expect(emitter.emit).not.toHaveBeenCalledWith('ready', expect.anything());
105
+ expect(readyCallCount).toBe(1);
106
+ cleanup();
107
+ });
108
+ it('should emit zoom event when zoom changes', () => {
109
+ const { state, actions } = createWaveSurferState();
110
+ const cleanup = setupStateEventEmission(state, emitter);
111
+ emitter.emit.mockClear();
112
+ actions.setZoom(2);
113
+ expect(emitter.emit).toHaveBeenCalledWith('zoom', 2);
114
+ cleanup();
115
+ });
116
+ it('should not emit zoom event for zoom=0', () => {
117
+ const { state, actions } = createWaveSurferState();
118
+ const cleanup = setupStateEventEmission(state, emitter);
119
+ emitter.emit.mockClear();
120
+ actions.setZoom(0);
121
+ expect(emitter.emit).not.toHaveBeenCalledWith('zoom', 0);
122
+ cleanup();
123
+ });
124
+ it('should cleanup all subscriptions', () => {
125
+ const { state, actions } = createWaveSurferState();
126
+ const cleanup = setupStateEventEmission(state, emitter);
127
+ cleanup();
128
+ emitter.emit.mockClear();
129
+ // These should not trigger events after cleanup
130
+ actions.setPlaying(true);
131
+ actions.setCurrentTime(42);
132
+ actions.setZoom(2);
133
+ expect(emitter.emit).not.toHaveBeenCalled();
134
+ });
135
+ });
136
+ describe('setupSignalEventEmission', () => {
137
+ it('should emit custom events from signal changes', () => {
138
+ const volumeSignal = signal(1);
139
+ const cleanup = setupSignalEventEmission(volumeSignal, emitter, (vol) => ['volume', vol]);
140
+ // Initial emission
141
+ expect(emitter.emit).toHaveBeenCalledWith('volume', 1);
142
+ emitter.emit.mockClear();
143
+ volumeSignal.set(0.5);
144
+ expect(emitter.emit).toHaveBeenCalledWith('volume', 0.5);
145
+ cleanup();
146
+ });
147
+ it('should cleanup subscription', () => {
148
+ const testSignal = signal(0);
149
+ const cleanup = setupSignalEventEmission(testSignal, emitter, (val) => ['test', val]);
150
+ cleanup();
151
+ emitter.emit.mockClear();
152
+ testSignal.set(1);
153
+ expect(emitter.emit).not.toHaveBeenCalled();
154
+ });
155
+ });
156
+ describe('setupDebouncedEventEmission', () => {
157
+ it('should debounce event emission', () => {
158
+ const scrollSignal = signal(0);
159
+ const cleanup = setupDebouncedEventEmission(scrollSignal, emitter, (pos) => ['scroll', pos], 100);
160
+ // Initial emission should be debounced
161
+ emitter.emit.mockClear();
162
+ scrollSignal.set(10);
163
+ expect(emitter.emit).not.toHaveBeenCalled();
164
+ scrollSignal.set(20);
165
+ expect(emitter.emit).not.toHaveBeenCalled();
166
+ scrollSignal.set(30);
167
+ expect(emitter.emit).not.toHaveBeenCalled();
168
+ // Fast forward time
169
+ jest.advanceTimersByTime(100);
170
+ // Should emit only the last value
171
+ expect(emitter.emit).toHaveBeenCalledTimes(1);
172
+ expect(emitter.emit).toHaveBeenCalledWith('scroll', 30);
173
+ cleanup();
174
+ });
175
+ it('should restart debounce timer on each change', () => {
176
+ const testSignal = signal(0);
177
+ const cleanup = setupDebouncedEventEmission(testSignal, emitter, (val) => ['test', val], 100);
178
+ emitter.emit.mockClear();
179
+ testSignal.set(1);
180
+ jest.advanceTimersByTime(50);
181
+ testSignal.set(2);
182
+ jest.advanceTimersByTime(50);
183
+ // Should not have emitted yet (timer restarted)
184
+ expect(emitter.emit).not.toHaveBeenCalled();
185
+ jest.advanceTimersByTime(50);
186
+ // Now should emit
187
+ expect(emitter.emit).toHaveBeenCalledWith('test', 2);
188
+ cleanup();
189
+ });
190
+ it('should cleanup pending timeout', () => {
191
+ const testSignal = signal(0);
192
+ const cleanup = setupDebouncedEventEmission(testSignal, emitter, (val) => ['test', val], 100);
193
+ emitter.emit.mockClear();
194
+ testSignal.set(1);
195
+ cleanup();
196
+ jest.advanceTimersByTime(100);
197
+ // Should not emit after cleanup
198
+ expect(emitter.emit).not.toHaveBeenCalled();
199
+ });
200
+ });
201
+ describe('setupConditionalEventEmission', () => {
202
+ it('should only emit when condition is true', () => {
203
+ const stateSignal = signal(0);
204
+ const cleanup = setupConditionalEventEmission(stateSignal, emitter, (val) => val > 5, (val) => ['threshold', val]);
205
+ emitter.emit.mockClear();
206
+ stateSignal.set(3);
207
+ expect(emitter.emit).not.toHaveBeenCalled();
208
+ stateSignal.set(7);
209
+ expect(emitter.emit).toHaveBeenCalledWith('threshold', 7);
210
+ cleanup();
211
+ });
212
+ it('should re-evaluate condition on each change', () => {
213
+ const testSignal = signal(false);
214
+ const cleanup = setupConditionalEventEmission(testSignal, emitter, (val) => val === true, () => ['activated']);
215
+ emitter.emit.mockClear();
216
+ testSignal.set(false);
217
+ expect(emitter.emit).not.toHaveBeenCalled();
218
+ testSignal.set(true);
219
+ expect(emitter.emit).toHaveBeenCalledWith('activated');
220
+ emitter.emit.mockClear();
221
+ testSignal.set(false);
222
+ expect(emitter.emit).not.toHaveBeenCalled();
223
+ cleanup();
224
+ });
225
+ it('should cleanup subscription', () => {
226
+ const testSignal = signal(0);
227
+ const cleanup = setupConditionalEventEmission(testSignal, emitter, (val) => val > 0, (val) => ['test', val]);
228
+ cleanup();
229
+ emitter.emit.mockClear();
230
+ testSignal.set(10);
231
+ expect(emitter.emit).not.toHaveBeenCalled();
232
+ });
233
+ });
234
+ });
@@ -0,0 +1 @@
1
+ export {};