import {createEvent, fireEvent} from 'sentry-test/reactTestingLibrary'; import {VideoReplayer} from './videoReplayer'; // XXX: Not quite sure the best way to mock RAF - here we use fake timers // VideoReplayer uses `app/util/replays/timer` which uses RAF to count up. This // is used to render the progress of the seeker bar and sync with video // replays. // // advancing by 2000ms ~== 20000s in Timer, but this may depend on hardware, TBD jest.useFakeTimers(); jest.spyOn(window.HTMLMediaElement.prototype, 'pause').mockImplementation(() => {}); describe('VideoReplayer - no starting gap', () => { beforeEach(() => { jest.clearAllTimers(); }); const attachments = [ { id: 0, timestamp: 0, duration: 5000, }, // no gap { id: 1, timestamp: 5000, duration: 5000, }, { id: 2, timestamp: 10_001, duration: 5000, }, // 5 second gap { id: 3, timestamp: 20_000, duration: 5000, }, // 5 second gap { id: 4, timestamp: 30_000, duration: 5000, }, { id: 5, timestamp: 35_002, duration: 5000, }, ]; const extra = [ { id: 6, timestamp: 40_002, duration: 5000, }, { id: 7, timestamp: 45_002, duration: 5000, }, ]; const skip = [ { id: 7, timestamp: 45_002, duration: 5000, }, { id: 8, timestamp: 50_002, duration: 5000, }, ]; it('plays and seeks inside of a segment', async () => { const root = document.createElement('div'); const inst = new VideoReplayer(attachments, { videoApiPrefix: '/foo/', root, start: 0, onFinished: jest.fn(), onLoaded: jest.fn(), onBuffer: jest.fn(), durationMs: 40000, config: {skipInactive: false, speed: 1.0}, }); // @ts-expect-error private expect(inst._currentIndex).toEqual(0); const playPromise = inst.play(6500); jest.advanceTimersByTime(10000); await playPromise; // @ts-expect-error private expect(inst._currentIndex).toEqual(1); // `currentTime` is in seconds // @ts-expect-error private expect(inst.getVideo(inst._currentIndex)?.currentTime).toEqual(1.5); }); it('seeks to a gap in a video', async () => { const root = document.createElement('div'); const inst = new VideoReplayer(attachments, { videoApiPrefix: '/foo/', root, start: 0, onFinished: jest.fn(), onLoaded: jest.fn(), onBuffer: jest.fn(), durationMs: 40000, config: {skipInactive: false, speed: 1.0}, }); const playPromise = inst.play(18100); // @ts-expect-error private const video = inst.getVideo(2)!; // the replay is actually stopped right now to wait for loading fireEvent(video, createEvent.loadedData(video)); // 15000 -> 20000 is a gap, so player should start playing @ index 3, from // the beginning. jest.advanceTimersByTime(2500); await playPromise; // @ts-expect-error private expect(inst._currentIndex).toEqual(3); // `currentTime` is in seconds // @ts-expect-error private expect(inst.getVideo(inst._currentIndex)?.currentTime).toEqual(0); }); it('seeks past end of the replay', async () => { const root = document.createElement('div'); const inst = new VideoReplayer(attachments, { videoApiPrefix: '/foo/', root, start: 0, // Unfortunately, `video.play()` is not implemented in jsdom, so no events, // so can't check that onFinished is called onFinished: jest.fn(), onLoaded: jest.fn(), onBuffer: jest.fn(), durationMs: 40000, config: {skipInactive: false, speed: 1.0}, }); const playPromise = inst.play(50000); // 15000 -> 20000 is a gap, so player should start playing @ index 3, from // the beginning. jest.advanceTimersByTime(5000); await playPromise; // @ts-expect-error private expect(inst._currentIndex).toEqual(5); // `currentTime` is in seconds // @ts-expect-error private expect(inst.getVideo(inst._currentIndex)?.currentTime).toEqual(5); }); it('initially only loads videos from 0 to BUFFER', async () => { const root = document.createElement('div'); const inst = new VideoReplayer(attachments, { videoApiPrefix: '/foo/', root, start: 0, onFinished: jest.fn(), onLoaded: jest.fn(), onBuffer: jest.fn(), durationMs: 40000, config: {skipInactive: false, speed: 1.0}, }); const playPromise = inst.play(0); jest.advanceTimersByTime(2500); await playPromise; // @ts-expect-error private expect(inst._currentIndex).toEqual(0); // @ts-expect-error private expect(inst._videos.size).toEqual(3); }); it('should load the correct videos after playing at a timestamp', async () => { const root = document.createElement('div'); const inst = new VideoReplayer(attachments.concat(extra), { videoApiPrefix: '/foo/', root, start: 0, onFinished: jest.fn(), onLoaded: jest.fn(), onBuffer: jest.fn(), durationMs: 50000, config: {skipInactive: false, speed: 1.0}, }); // play at segment 7 const playPromise = inst.play(45_003); jest.advanceTimersByTime(2500); await playPromise; // @ts-expect-error private expect(inst._currentIndex).toEqual(7); // videos loaded should be [0, 1, 2, 4, 5, 6, 7] // since we have [0, 1, 2] preloaded initially // and only [4, 5, 6, 7] loaded when segment 7 is requested // @ts-expect-error private const videos = inst._videos; // @ts-expect-error private const getVideo = index => inst.getVideo(index); expect(videos.size).toEqual(7); expect(videos.get(0)).toEqual(getVideo(0)); expect(videos.get(2)).toEqual(getVideo(2)); expect(videos.get(3)).toEqual(undefined); expect(videos.get(4)).toEqual(getVideo(4)); expect(videos.get(7)).toEqual(getVideo(7)); }); it('should work correctly if we have missing segments', async () => { const root = document.createElement('div'); const inst = new VideoReplayer(attachments.concat(skip), { videoApiPrefix: '/foo/', root, start: 0, onFinished: jest.fn(), onLoaded: jest.fn(), onBuffer: jest.fn(), durationMs: 55000, config: {skipInactive: false, speed: 1.0}, }); // play at segment 7 const playPromise = inst.play(45_003); jest.advanceTimersByTime(2500); await playPromise; // @ts-expect-error private expect(inst._currentIndex).toEqual(6); // @ts-expect-error private const videos = inst._videos; // @ts-expect-error private const getVideo = index => inst.getVideo(index); // videos loaded should be [0, 1, 2, 3, 4, 5, 7, 8] expect(videos.size).toEqual(8); expect(videos.get(0)).toEqual(getVideo(0)); expect(videos.get(2)).toEqual(getVideo(2)); expect(videos.get(5)).toEqual(getVideo(5)); expect(videos.get(6)).toEqual(getVideo(6)); expect(videos.get(7)).toEqual(getVideo(7)); }); }); describe('VideoReplayer - with starting gap', () => { beforeEach(() => { jest.clearAllTimers(); }); const attachments = [ { id: 0, timestamp: 2500, duration: 5000, }, // no gap { id: 1, timestamp: 5000, duration: 5000, }, { id: 2, timestamp: 10_001, duration: 5000, }, // 5 second gap { id: 3, timestamp: 20_000, duration: 5000, }, // 5 second gap { id: 4, timestamp: 30_000, duration: 5000, }, { id: 5, timestamp: 35_002, duration: 5000, }, ]; it('plays and seeks before replay starts', async () => { const root = document.createElement('div'); const inst = new VideoReplayer(attachments, { videoApiPrefix: '/foo/', root, start: 0, onFinished: jest.fn(), onLoaded: jest.fn(), onBuffer: jest.fn(), durationMs: 40000, config: {skipInactive: false, speed: 1.0}, }); // @ts-expect-error private expect(inst._currentIndex).toEqual(0); const playPromise = inst.play(1500); jest.advanceTimersByTime(2000); await playPromise; // @ts-expect-error private expect(inst._currentIndex).toEqual(0); // `currentTime` is in seconds // @ts-expect-error private expect(inst.getVideo(inst._currentIndex)?.currentTime).toEqual(0); }); it('seeks to a gap in a video', async () => { const root = document.createElement('div'); const inst = new VideoReplayer(attachments, { videoApiPrefix: '/foo/', root, start: 0, onFinished: jest.fn(), onLoaded: jest.fn(), onBuffer: jest.fn(), durationMs: 40000, config: {skipInactive: false, speed: 1.0}, }); const playPromise = inst.play(18100); // @ts-expect-error private const video = inst.getVideo(2)!; // the replay is actually stopped right now to wait for loading fireEvent(video, createEvent.loadedData(video)); // 15000 -> 20000 is a gap, so player should start playing @ index 3, from // the beginning. jest.advanceTimersByTime(2500); await playPromise; // @ts-expect-error private expect(inst._currentIndex).toEqual(3); // `currentTime` is in seconds // @ts-expect-error private expect(inst.getVideo(inst._currentIndex)?.currentTime).toEqual(0); }); it('seeks past end of the replay', async () => { const root = document.createElement('div'); const inst = new VideoReplayer(attachments, { videoApiPrefix: '/foo/', root, start: 0, // Unfortunately, `video.play()` is not implemented in jsdom, so no events, // so can't check that onFinished is called onFinished: jest.fn(), onLoaded: jest.fn(), onBuffer: jest.fn(), durationMs: 40000, config: {skipInactive: false, speed: 1.0}, }); const playPromise = inst.play(50000); // 15000 -> 20000 is a gap, so player should start playing @ index 3, from // the beginning. jest.advanceTimersByTime(5000); await playPromise; // @ts-expect-error private expect(inst._currentIndex).toEqual(5); // `currentTime` is in seconds // @ts-expect-error private expect(inst.getVideo(inst._currentIndex)?.currentTime).toEqual(5); }); }); describe('VideoReplayer - with ending gap', () => { beforeEach(() => { jest.clearAllTimers(); }); const attachments = [ { id: 0, timestamp: 2500, duration: 5000, }, // no gap { id: 1, timestamp: 5000, duration: 5000, }, { id: 2, timestamp: 10_001, duration: 5000, }, // 5 second gap { id: 3, timestamp: 20_000, duration: 5000, }, // 5 second gap { id: 4, timestamp: 30_000, duration: 5000, }, { id: 5, timestamp: 35_002, duration: 5000, }, ]; it('keeps playing until the end if there is an ending gap', async () => { const root = document.createElement('div'); const inst = new VideoReplayer(attachments, { videoApiPrefix: '/foo/', root, start: 0, onFinished: jest.fn(), onLoaded: jest.fn(), onBuffer: jest.fn(), durationMs: 50000, config: {skipInactive: false, speed: 1.0}, }); // actual length of the segments is 40s // 10s gap at the end // play at the last segment const playPromise = inst.play(36000); await playPromise; jest.advanceTimersByTime(4000); // we're still within the last segment (5) // @ts-expect-error private expect(inst._currentIndex).toEqual(5); expect(inst.getCurrentTime()).toEqual(40000); // now we are in the gap // timer should still be going since the duration is 50s jest.advanceTimersByTime(5000); // @ts-expect-error private expect(inst._isPlaying).toEqual(true); // a long time passes // ensure the timer stops at the end duration (50s) jest.advanceTimersByTime(60000); expect(inst.getCurrentTime()).toEqual(50000); // @ts-expect-error private expect(inst._isPlaying).toEqual(false); }); it('ends at the proper time if seeking into a gap at the end', async () => { const root = document.createElement('div'); const inst = new VideoReplayer(attachments, { videoApiPrefix: '/foo/', root, start: 0, onFinished: jest.fn(), onLoaded: jest.fn(), onBuffer: jest.fn(), durationMs: 50000, config: {skipInactive: false, speed: 1.0}, }); // actual length of the segments is 40s // 10s gap at the end // play at the gap const playPromise = inst.play(40002); await playPromise; jest.advanceTimersByTime(4000); // we should be still playing in the gap expect(inst.getCurrentTime()).toEqual(44002); // @ts-expect-error private expect(inst._isPlaying).toEqual(true); // a long time passes // ensure the timer stops at the end duration (50s) jest.advanceTimersByTime(60000); expect(inst.getCurrentTime()).toBeLessThan(50100); // @ts-expect-error private expect(inst._isPlaying).toEqual(false); }); });