Skip to content

Commit 2d41832

Browse files
committed
integrated enhanced clip lifecycle
1 parent f4faf22 commit 2d41832

27 files changed

+1140
-1175
lines changed

src/clips/audio/audio.spec.ts

Lines changed: 133 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@
88
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
99
import { Composition } from '../../composition';
1010
import { AudioClip } from './audio';
11-
12-
import type { MockInstance } from 'vitest';
13-
import type { frame } from '../../types';
1411
import { Timestamp } from '../../models';
1512
import { AudioSource } from '../../sources';
1613

14+
import type { MockInstance } from 'vitest';
15+
1716
const file = new File([], 'audio.mp3', { type: 'audio/mp3' });
1817

1918
describe('The Audio Clip', () => {
@@ -26,23 +25,40 @@ describe('The Audio Clip', () => {
2625
let clip: AudioClip;
2726

2827
beforeEach(async () => {
29-
clip = await new AudioClip().load(file);
28+
clip = new AudioClip(file);
3029
clip.on('update', updateFn);
3130
clip.on('error', errorFn);
3231
clip.on('load', loadFn);
3332

34-
playFn = vi.spyOn(clip.element, 'play').mockImplementation(async () => {
35-
clip.element.dispatchEvent(new Event('play'));
36-
});
37-
pauseFn = vi.spyOn(clip.element, 'pause').mockImplementation(async () => {
38-
clip.element.dispatchEvent(new Event('pause'));
39-
});
33+
playFn = mockPlayValid(clip);
34+
pauseFn = mockPauseValid(clip);
35+
36+
mockAudioValid(clip);
37+
mockDurationValid(clip);
38+
39+
await clip.init();
4040
});
4141

42-
it('should have an initial state', async () => {
43-
clip.element.dispatchEvent(new Event('canplay'));
44-
expect(clip.element).toBeDefined();
45-
expect(loadFn).toBeCalledTimes(1);
42+
it('should initialize', async () => {
43+
const clip = new AudioClip(file);
44+
45+
expect(clip.state).toBe('IDLE');
46+
expect(clip.duration.seconds).not.toBe(20);
47+
expect(clip.source.file).toBeInstanceOf(File);
48+
expect(clip.element.src).toBeFalsy();
49+
50+
const evtSpy = mockAudioValid(clip);
51+
const timeSpy = mockDurationValid(clip);
52+
53+
await clip.init();
54+
55+
expect(clip.duration.seconds).toBe(20);
56+
expect(clip.range[0].seconds).toBe(0);
57+
expect(clip.range[1].seconds).toBe(20);
58+
59+
expect(evtSpy).toHaveBeenCalledOnce();
60+
expect(timeSpy).toHaveBeenCalledOnce();
61+
4662
expect(clip.type).toBe('audio');
4763
expect(clip.state).toBe('READY');
4864
expect(clip.source.name).toBe('audio.mp3');
@@ -51,83 +67,112 @@ describe('The Audio Clip', () => {
5167
);
5268
});
5369

54-
it('should update its state when canplay is called', () => {
55-
vi.spyOn(clip.element, 'duration', 'get').mockReturnValue(30);
56-
expect(clip.element.duration).toBe(30);
57-
expect(clip.duration.seconds).toBe(0);
58-
clip.element.dispatchEvent(new Event('canplay'));
59-
expect(clip.duration.seconds).toBe(30);
60-
expect(clip.range[0].seconds).toBe(0);
61-
expect(clip.range[1].seconds).toBe(30);
62-
expect(clip.state).toBe('READY');
63-
expect(loadFn).toBeCalledTimes(1);
64-
});
70+
it("should throw an error if the media can't be loaded", async () => {
71+
const clip = new AudioClip(file);
6572

66-
it('should go to idle state if the media gets emptied', () => {
67-
vi.spyOn(clip.element, 'duration', 'get').mockReturnValue(30);
68-
clip.element.dispatchEvent(new Event('canplay'));
73+
mockDurationValid(clip);
74+
const evtSpy = mockAudioInvalid(clip);
75+
mockDurationValid(clip);
6976

70-
clip.playing = true;
71-
clip.element.dispatchEvent(new Event('emptied'));
72-
expect(clip.playing).toBe(false);
73-
expect(clip.state).toBe('IDLE');
74-
expect(clip.track).toBeUndefined();
75-
});
77+
await expect(() => clip.init()).rejects.toThrowError();
78+
79+
expect(clip.duration.seconds).not.toBe(20);
80+
expect(evtSpy).toHaveBeenCalledOnce();
7681

77-
it("should throw an error if the media can't be loaded", () => {
78-
vi.spyOn(clip.element, 'duration', 'get').mockReturnValue(30);
79-
clip.element.dispatchEvent(new Event('canplay'));
80-
clip.element.dispatchEvent(new Event('error'));
8182
expect(clip.state).toBe('ERROR');
82-
expect(clip.track).toBeUndefined();
83-
expect(errorFn).toBeCalledTimes(1);
8483
});
8584

86-
it('should play and pause the audio with the render method', () => {
87-
vi.spyOn(clip.element, 'duration', 'get').mockReturnValue(5);
88-
85+
it('should play and pause the audio with the render method', async () => {
8986
const composition = new Composition();
90-
Object.assign(clip, { track: { composition } });
87+
await composition.add(clip.offsetBy(30));
9188

92-
clip.element.dispatchEvent(new Event('canplay'));
93-
expect(clip.duration.seconds).toBe(5);
89+
const enterSpy = vi.spyOn(clip, 'enter');
90+
const updateSpy = vi.spyOn(clip, 'update');
91+
const exitSpy = vi.spyOn(clip, 'exit');
9492

9593
expect(clip.playing).toBe(false);
94+
9695
composition.state = 'PLAY';
97-
clip.render();
96+
composition.computeFrame();
97+
98+
expect(playFn).toBeCalledTimes(0);
99+
expect(clip.playing).toBe(false);
100+
expect(enterSpy).toHaveBeenCalledTimes(0);
101+
expect(updateSpy).toHaveBeenCalledTimes(0);
102+
expect(exitSpy).toHaveBeenCalledTimes(0);
103+
104+
composition.frame = 30;
105+
composition.computeFrame();
98106

99-
expect(clip.playing).toBe(true);
100107
expect(playFn).toBeCalledTimes(1);
108+
expect(clip.playing).toBe(true);
109+
expect(enterSpy).toHaveBeenCalledTimes(1);
110+
expect(updateSpy).toHaveBeenCalledTimes(1);
111+
expect(exitSpy).toHaveBeenCalledTimes(0);
101112

102113
composition.state = 'IDLE';
103-
clip.render();
114+
composition.frame = 60;
115+
composition.computeFrame();
116+
104117
expect(clip.playing).toBe(false);
105118
expect(pauseFn).toBeCalledTimes(1);
119+
expect(enterSpy).toHaveBeenCalledTimes(1);
120+
expect(updateSpy).toHaveBeenCalledTimes(2);
121+
expect(exitSpy).toHaveBeenCalledTimes(0);
106122

107-
pauseFn.mockClear();
123+
composition.state = 'PLAY';
124+
composition.frame = 90;
125+
composition.computeFrame();
108126

109-
clip.playing = true;
110-
clip.unrender();
127+
expect(playFn).toBeCalledTimes(2);
128+
expect(clip.playing).toBe(true);
129+
expect(enterSpy).toHaveBeenCalledTimes(1);
130+
expect(updateSpy).toHaveBeenCalledTimes(3);
131+
expect(exitSpy).toHaveBeenCalledTimes(0);
132+
133+
composition.frame = clip.stop.frames + 1;
134+
composition.computeFrame();
111135

136+
expect(pauseFn).toBeCalledTimes(2);
112137
expect(clip.playing).toBe(false);
113-
expect(pauseFn).toBeCalledTimes(1);
138+
expect(enterSpy).toHaveBeenCalledTimes(1);
139+
expect(updateSpy).toHaveBeenCalledTimes(3);
140+
expect(exitSpy).toHaveBeenCalledTimes(1);
114141
});
115142

116143
it('slice should be persistant after adding clip', async () => {
117-
vi.spyOn(clip.element, 'duration', 'get').mockReturnValue(30);
118-
vi.spyOn(clip, 'seek').mockImplementation(async () => { });
119-
120-
clip.subclip(<frame>20, <frame>60);
144+
clip.offsetBy(30).subclip(20, 60).set({ muted: true, volume: 0.2 });
121145

122146
const composition = new Composition();
123-
const promise = composition.add(clip);
124-
clip.element.dispatchEvent(new Event('canplay'));
125-
await promise;
147+
await composition.add(clip)
126148

127149
expect(clip.state).toBe('ATTACHED');
128-
expect(clip.duration.seconds).toBe(30);
150+
expect(clip.duration.seconds).toBe(20);
129151
expect(clip.range[0].frames).toBe(20);
130152
expect(clip.range[1].frames).toBe(60);
153+
expect(clip.offset.frames).toBe(30);
154+
expect(clip.muted).toBe(true);
155+
expect(clip.volume).toBe(0.2);
156+
});
157+
158+
it("should not render the clip if it's disabled", async () => {
159+
const composition = new Composition();
160+
await composition.add(clip);
161+
162+
const exitSpy = vi.spyOn(clip, 'exit');
163+
const updateSpy = vi.spyOn(clip, 'update');
164+
165+
composition.state = 'PLAY';
166+
composition.computeFrame();
167+
168+
expect(updateSpy).toHaveBeenCalledTimes(1);
169+
expect(exitSpy).not.toHaveBeenCalled();
170+
expect(clip.playing).toBe(true);
171+
172+
clip.set({ disabled: true });
173+
174+
expect(exitSpy).toHaveBeenCalledTimes(1);
175+
expect(clip.playing).toBe(false);
131176
});
132177

133178
afterEach(() => {
@@ -143,8 +188,8 @@ describe('The Audio Clip', () => {
143188
describe('Copying the AudioClip', () => {
144189
let clip: AudioClip;
145190

146-
beforeEach(async () => {
147-
clip = await new AudioClip().load(file);
191+
beforeEach(() => {
192+
clip = new AudioClip();
148193
});
149194

150195
it('should transfer audio properties', async () => {
@@ -180,22 +225,34 @@ describe('Copying the AudioClip', () => {
180225
expect(copy.id).not.toBe(clip.id);
181226
expect(copy.track).not.toBeDefined();
182227
});
183-
})
184-
185-
// copied from src/clips/clip/clip.decorator.spec.ts
186-
describe('The render decorator', () => {
187-
it('should not render the compostition if the clip is disabled', () => {
188-
const clip = new AudioClip();
228+
});
189229

190-
const unrenderSpy = vi.spyOn(clip, 'unrender');
230+
function mockAudioValid(clip: AudioClip) {
231+
return vi.spyOn(clip.element, 'oncanplay', 'set')
232+
.mockImplementation(function (this: HTMLMediaElement, fn) {
233+
fn?.call(this, new Event('canplay'));
234+
});
235+
}
191236

192-
clip.render();
237+
function mockDurationValid(clip: AudioClip) {
238+
return vi.spyOn(clip.element, 'duration', 'get').mockReturnValue(20);
239+
}
193240

194-
expect(unrenderSpy).not.toHaveBeenCalled();
241+
function mockAudioInvalid(clip: AudioClip) {
242+
return vi.spyOn(clip.element, 'onerror', 'set')
243+
.mockImplementation(function (this: HTMLMediaElement, fn) {
244+
fn?.call(this, new Event('error'));
245+
});
246+
}
195247

196-
clip.set({ disabled: true });
197-
clip.render();
248+
function mockPlayValid(clip: AudioClip) {
249+
return vi.spyOn(clip.element, 'play').mockImplementation(async () => {
250+
clip.element.dispatchEvent(new Event('play'));
251+
});
252+
}
198253

199-
expect(unrenderSpy).toHaveBeenCalledOnce()
254+
function mockPauseValid(clip: AudioClip) {
255+
return vi.spyOn(clip.element, 'pause').mockImplementation(async () => {
256+
clip.element.dispatchEvent(new Event('pause'));
200257
});
201-
});
258+
}

0 commit comments

Comments
 (0)