Skip to content

Commit a0e6127

Browse files
committed
added composition unit tests
1 parent a4d26e8 commit a0e6127

File tree

4 files changed

+116
-6
lines changed

4 files changed

+116
-6
lines changed

src/composition/composition.spec.ts

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@
55
* Public License, v. 2.0 that can be found in the LICENSE file.
66
*/
77

8+
import * as PIXI from 'pixi.js';
89
import { describe, expect, it, beforeEach, vi, afterEach, afterAll, MockInstance } from 'vitest';
910
import { Composition } from './composition';
10-
import { Clip, TextClip } from '../clips';
11+
import { AudioClip, Clip, TextClip } from '../clips';
1112
import { AudioTrack, CaptionTrack, HtmlTrack, ImageTrack, TextTrack, Track, VideoTrack } from '../tracks';
1213
import { Timestamp } from '../models';
14+
import { sleep } from '../utils';
15+
import { AudioBufferMock, OfflineAudioContextMock } from '../../vitest.mocks';
16+
import { AudioSource } from '../sources';
1317

1418
describe('The composition', () => {
1519
let composition: Composition;
@@ -49,6 +53,17 @@ describe('The composition', () => {
4953
expect(composition.playing).toBe(false);
5054
});
5155

56+
it("should trigger an error when the composition can't be initialized", async () => {
57+
const errorFn = vi.fn();
58+
vi.spyOn(PIXI, 'autoDetectRenderer')
59+
.mockImplementationOnce(() => Promise.reject(new Error('Mocked rejection')));
60+
const composition = new Composition();
61+
composition.on('error', errorFn);
62+
expect(errorFn).toBeCalledTimes(0);
63+
await sleep(1);
64+
expect(errorFn).toBeCalledTimes(1);
65+
})
66+
5267
it('should return width and height', () => {
5368
expect(composition.settings.height).toBe(composition.height);
5469
expect(composition.settings.width).toBe(composition.width);
@@ -83,11 +98,14 @@ describe('The composition', () => {
8398
expect(composition.duration.frames).toBe(8 * 30);
8499
});
85100

86-
it('should append canvas to div', () => {
101+
it('should attach a canvas to the dom and be able to remove it', () => {
87102
const div = document.createElement('div');
88103
composition.attachPlayer(div);
89104
expect(div.children.length).toBe(1);
90105
expect(div.children[0] instanceof HTMLCanvasElement).toBe(true);
106+
107+
composition.detachPlayer(div);
108+
expect(div.children.length).toBe(0);
91109
});
92110

93111
it('should append new tracks', () => {
@@ -167,6 +185,22 @@ describe('The composition', () => {
167185
expect(search4.length).toBe(0);
168186
});
169187

188+
it('should seek a time by timestamp', async () => {
189+
const clip = new Clip({ stop: 15 });
190+
191+
const track = composition.createTrack('base');
192+
await track.add(clip);
193+
194+
const seekMock = vi.spyOn(track, 'seek');
195+
computeMock.mockClear();
196+
197+
const ts = new Timestamp(400) // 12 frames
198+
composition.seek(ts);
199+
expect(composition.frame).toBe(12);
200+
expect(seekMock).toBeCalledTimes(1);
201+
expect(seekMock.mock.calls[0][0].millis).toBe(400);
202+
});
203+
170204
it('should render clips when user called play', async () => {
171205
const clip = new Clip({ stop: 15 });
172206

@@ -239,16 +273,22 @@ describe('The composition', () => {
239273
});
240274

241275
it('should be able to screenshot a frame', async () => {
242-
const clip = new Clip({ stop: 6 * 30 });
243-
const track = composition.createTrack('base');
244-
await track.add(clip);
276+
await composition.add(new Clip({ stop: 6 * 30 }));
245277

246278
composition.frame = 10;
247279
expect(composition.screenshot()).toBe('data:image/png;base64,00');
248280
expect(composition.screenshot('webp')).toBe('data:image/webp;base64,00');
249281
expect(composition.screenshot('jpeg')).toBe('data:image/jpeg;base64,00');
250282
});
251283

284+
it('should not screenshot a frame when the renderer is undefined', async () => {
285+
await composition.add(new Clip({ stop: 6 * 30 }));
286+
287+
delete composition.renderer;
288+
289+
expect(() => composition.screenshot()).toThrowError();
290+
});
291+
252292
it('should be able to calculate the correct time', async () => {
253293
composition.duration = Timestamp.fromFrames(20 * 30);
254294
composition.frame = 10 * 30;
@@ -425,3 +465,55 @@ describe('The composition', () => {
425465
requestAnimationFrameMock.mockClear();
426466
});
427467
});
468+
469+
describe('Composition audio', () => {
470+
vi.stubGlobal('OfflineAudioContext', OfflineAudioContextMock);
471+
472+
const source = new AudioSource();
473+
474+
vi.spyOn(source, 'decode').mockImplementation(async (
475+
numberOfChannels: number = 2,
476+
sampleRate: number = 48000,
477+
) => {
478+
const buffer = new AudioBufferMock({ numberOfChannels, sampleRate, length: 8000 });
479+
480+
for (let i = 0; i < buffer.channelData.length; i++) {
481+
for (let j = 0; j < buffer.channelData[i].length; j++) {
482+
buffer.channelData[i][j] = 1;
483+
}
484+
}
485+
486+
return buffer as any;
487+
});
488+
489+
vi.spyOn(source, 'createObjectURL').mockImplementationOnce(async () => '');
490+
491+
const clip = new AudioClip(source);
492+
493+
vi.spyOn(clip.element, 'oncanplay', 'set')
494+
.mockImplementation(function (this: HTMLMediaElement, fn) {
495+
fn?.call(this, new Event('canplay'));
496+
});
497+
498+
vi.spyOn(clip.element, 'duration', 'get').mockReturnValue(1);
499+
500+
it('should merge audio clips', async () => {
501+
const composition = new Composition();
502+
await composition.add(clip.subclip(2, 26));
503+
504+
expect(composition.duration.frames).toBe(26);
505+
composition.duration = 30;
506+
expect(composition.duration.frames).toBe(30);
507+
508+
const buffer = await composition.audio(1, 8000);
509+
expect(buffer.length).toBe(8000);
510+
expect(buffer.numberOfChannels).toBe(1);
511+
expect(buffer.sampleRate).toBe(8000);
512+
513+
const data = buffer.getChannelData(0);
514+
// audio clip has been trimmed and contains all ones
515+
expect(data.at(0)).toBe(0);
516+
expect(data.at(-1)).toBe(0);
517+
expect(data.at(4000)).toBe(1);
518+
});
519+
});

src/composition/composition.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,8 @@ export class Composition extends EventEmitterMixin<CompositionEvents, typeof Ser
370370
* Play the composition
371371
*/
372372
public async play(): Promise<void> {
373+
if (this.rendering) return;
374+
373375
this.state = 'PLAY';
374376

375377
if (this.frame >= this.duration.frames) {

src/sources/audio.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class AudioSource extends Source {
1616

1717
public async decode(
1818
numberOfChannels: number = 2,
19-
sampleRate: number = 44_100,
19+
sampleRate: number = 48000,
2020
): Promise<AudioBuffer> {
2121
const buffer = await this.arrayBuffer();
2222

vitest.mocks.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,22 @@ export class AudioBufferMock {
340340
}
341341
}
342342

343+
export class OfflineAudioContextMock {
344+
sampleRate: number;
345+
length: number;
346+
numberOfChannels: number;
347+
348+
constructor({ sampleRate, length, numberOfChannels }: OfflineAudioContextOptions) {
349+
this.sampleRate = sampleRate;
350+
this.length = length;
351+
this.numberOfChannels = numberOfChannels ?? 2;
352+
}
353+
354+
createBuffer(numberOfChannels: number, length: number, sampleRate: number): AudioBuffer {
355+
return new AudioBufferMock({ numberOfChannels, length, sampleRate }) as any as AudioBuffer;
356+
}
357+
}
358+
343359
export class MockFileSystemFileHandleMock {
344360
kind: 'file' = 'file';
345361
name: string;

0 commit comments

Comments
 (0)