mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
refactor(store): merge getSnapshot/subscribe into attach (#364)
This commit is contained in:
@@ -11,14 +11,17 @@ export const bufferFeature = defineFeature<HTMLMediaElement>()({
|
||||
seekable: [] as [number, number][],
|
||||
}),
|
||||
|
||||
getSnapshot: ({ target }) => ({
|
||||
buffered: serializeTimeRanges(target.buffered),
|
||||
seekable: serializeTimeRanges(target.seekable),
|
||||
}),
|
||||
attach({ target, signal, set }) {
|
||||
const sync = () =>
|
||||
set({
|
||||
buffered: serializeTimeRanges(target.buffered),
|
||||
seekable: serializeTimeRanges(target.seekable),
|
||||
});
|
||||
|
||||
subscribe: ({ target, update, signal }) => {
|
||||
listen(target, 'progress', update, { signal });
|
||||
listen(target, 'emptied', update, { signal });
|
||||
sync();
|
||||
|
||||
listen(target, 'progress', sync, { signal });
|
||||
listen(target, 'emptied', sync, { signal });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -36,19 +36,22 @@ export const playbackFeature = defineFeature<HTMLMediaElement>()({
|
||||
},
|
||||
}),
|
||||
|
||||
getSnapshot: ({ target }) => ({
|
||||
paused: target.paused,
|
||||
ended: target.ended,
|
||||
started: !target.paused || target.currentTime > 0,
|
||||
waiting: target.readyState < HTMLMediaElement.HAVE_FUTURE_DATA && !target.paused,
|
||||
}),
|
||||
attach({ target, signal, set }) {
|
||||
const sync = () =>
|
||||
set({
|
||||
paused: target.paused,
|
||||
ended: target.ended,
|
||||
started: !target.paused || target.currentTime > 0,
|
||||
waiting: target.readyState < HTMLMediaElement.HAVE_FUTURE_DATA && !target.paused,
|
||||
});
|
||||
|
||||
subscribe: ({ target, update, signal }) => {
|
||||
listen(target, 'play', update, { signal });
|
||||
listen(target, 'pause', update, { signal });
|
||||
listen(target, 'ended', update, { signal });
|
||||
listen(target, 'playing', update, { signal });
|
||||
listen(target, 'waiting', update, { signal });
|
||||
sync();
|
||||
|
||||
listen(target, 'play', sync, { signal });
|
||||
listen(target, 'pause', sync, { signal });
|
||||
listen(target, 'ended', sync, { signal });
|
||||
listen(target, 'playing', sync, { signal });
|
||||
listen(target, 'waiting', sync, { signal });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export const sourceFeature = defineFeature<HTMLMediaElement>()({
|
||||
source: null as string | null,
|
||||
/** Whether enough data is loaded to begin playback. */
|
||||
canPlay: false,
|
||||
|
||||
/** Load a new media source. Cancels all pending operations. Returns the new source URL. */
|
||||
loadSource(src: string) {
|
||||
return task({
|
||||
@@ -23,16 +24,19 @@ export const sourceFeature = defineFeature<HTMLMediaElement>()({
|
||||
},
|
||||
}),
|
||||
|
||||
getSnapshot: ({ target }) => ({
|
||||
source: target.currentSrc || target.src || null,
|
||||
canPlay: target.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA,
|
||||
}),
|
||||
attach({ target, signal, set }) {
|
||||
const sync = () =>
|
||||
set({
|
||||
source: target.currentSrc || target.src || null,
|
||||
canPlay: target.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA,
|
||||
});
|
||||
|
||||
subscribe: ({ target, update, signal }) => {
|
||||
listen(target, 'canplay', update, { signal });
|
||||
listen(target, 'canplaythrough', update, { signal });
|
||||
listen(target, 'loadstart', update, { signal });
|
||||
listen(target, 'emptied', update, { signal });
|
||||
sync();
|
||||
|
||||
listen(target, 'canplay', sync, { signal });
|
||||
listen(target, 'canplaythrough', sync, { signal });
|
||||
listen(target, 'loadstart', sync, { signal });
|
||||
listen(target, 'emptied', sync, { signal });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createStore } from '@videojs/store';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { bufferFeature } from '../buffer';
|
||||
|
||||
describe('bufferFeature', () => {
|
||||
describe('getSnapshot', () => {
|
||||
it('captures buffered and seekable ranges from video element', () => {
|
||||
describe('attach', () => {
|
||||
it('syncs buffered and seekable ranges on attach', () => {
|
||||
const video = createMockVideo({
|
||||
buffered: createTimeRanges([[0, 60]]),
|
||||
seekable: createTimeRanges([[0, 120]]),
|
||||
});
|
||||
|
||||
const snapshot = bufferFeature.getSnapshot({
|
||||
target: video,
|
||||
get: () => ({ buffered: [], seekable: [] }),
|
||||
initialState: { buffered: [], seekable: [] },
|
||||
});
|
||||
const store = createStore({ features: [bufferFeature] });
|
||||
store.attach(video);
|
||||
|
||||
expect(snapshot).toEqual({
|
||||
buffered: [[0, 60]],
|
||||
seekable: [[0, 120]],
|
||||
});
|
||||
expect(store.state.buffered).toEqual([[0, 60]]);
|
||||
expect(store.state.seekable).toEqual([[0, 120]]);
|
||||
});
|
||||
|
||||
it('handles multiple ranges', () => {
|
||||
@@ -31,56 +27,61 @@ describe('bufferFeature', () => {
|
||||
seekable: createTimeRanges([[0, 120]]),
|
||||
});
|
||||
|
||||
const snapshot = bufferFeature.getSnapshot({
|
||||
target: video,
|
||||
get: () => ({ buffered: [], seekable: [] }),
|
||||
initialState: { buffered: [], seekable: [] },
|
||||
});
|
||||
const store = createStore({ features: [bufferFeature] });
|
||||
store.attach(video);
|
||||
|
||||
expect(snapshot.buffered).toEqual([
|
||||
expect(store.state.buffered).toEqual([
|
||||
[0, 30],
|
||||
[60, 90],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe', () => {
|
||||
it('calls update on progress event', () => {
|
||||
it('updates on progress event', () => {
|
||||
const video = createMockVideo({
|
||||
buffered: createTimeRanges([[0, 50]]),
|
||||
seekable: createTimeRanges([[0, 100]]),
|
||||
});
|
||||
const update = vi.fn();
|
||||
const controller = new AbortController();
|
||||
|
||||
bufferFeature.subscribe({
|
||||
target: video,
|
||||
update,
|
||||
signal: controller.signal,
|
||||
get: () => ({ buffered: [], seekable: [] }),
|
||||
const store = createStore({ features: [bufferFeature] });
|
||||
store.attach(video);
|
||||
|
||||
// Update the mock video's buffered range
|
||||
Object.defineProperty(video, 'buffered', {
|
||||
value: createTimeRanges([[0, 75]]),
|
||||
writable: false,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
video.dispatchEvent(new Event('progress'));
|
||||
|
||||
expect(update).toHaveBeenCalled();
|
||||
expect(store.state.buffered).toEqual([[0, 75]]);
|
||||
});
|
||||
|
||||
it('calls update on emptied event', () => {
|
||||
it('updates on emptied event', () => {
|
||||
const video = createMockVideo({
|
||||
buffered: createTimeRanges([]),
|
||||
seekable: createTimeRanges([]),
|
||||
buffered: createTimeRanges([[0, 50]]),
|
||||
seekable: createTimeRanges([[0, 100]]),
|
||||
});
|
||||
const update = vi.fn();
|
||||
const controller = new AbortController();
|
||||
|
||||
bufferFeature.subscribe({
|
||||
target: video,
|
||||
update,
|
||||
signal: controller.signal,
|
||||
get: () => ({ buffered: [], seekable: [] }),
|
||||
const store = createStore({ features: [bufferFeature] });
|
||||
store.attach(video);
|
||||
|
||||
// Update the mock video to have no buffered content
|
||||
Object.defineProperty(video, 'buffered', {
|
||||
value: createTimeRanges([]),
|
||||
writable: false,
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(video, 'seekable', {
|
||||
value: createTimeRanges([]),
|
||||
writable: false,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
video.dispatchEvent(new Event('emptied'));
|
||||
|
||||
expect(update).toHaveBeenCalled();
|
||||
expect(store.state.buffered).toEqual([]);
|
||||
expect(store.state.seekable).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -94,10 +95,10 @@ function createMockVideo(
|
||||
const video = document.createElement('video');
|
||||
|
||||
if (overrides.buffered !== undefined) {
|
||||
Object.defineProperty(video, 'buffered', { value: overrides.buffered, writable: false });
|
||||
Object.defineProperty(video, 'buffered', { value: overrides.buffered, writable: false, configurable: true });
|
||||
}
|
||||
if (overrides.seekable !== undefined) {
|
||||
Object.defineProperty(video, 'seekable', { value: overrides.seekable, writable: false });
|
||||
Object.defineProperty(video, 'seekable', { value: overrides.seekable, writable: false, configurable: true });
|
||||
}
|
||||
|
||||
return video;
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
import { createStore } from '@videojs/store';
|
||||
import { noop } from '@videojs/utils/function';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { PlaybackState } from '../playback';
|
||||
import { playbackFeature } from '../playback';
|
||||
|
||||
const mockState = () =>
|
||||
({
|
||||
paused: true,
|
||||
ended: false,
|
||||
started: false,
|
||||
waiting: false,
|
||||
play: noop,
|
||||
pause: noop,
|
||||
}) as unknown as PlaybackState;
|
||||
|
||||
describe('playbackFeature', () => {
|
||||
describe('getSnapshot', () => {
|
||||
it('captures current playback state from video element', () => {
|
||||
describe('attach', () => {
|
||||
it('syncs playback state on attach', () => {
|
||||
const video = createMockVideo({
|
||||
paused: false,
|
||||
ended: false,
|
||||
@@ -25,18 +13,13 @@ describe('playbackFeature', () => {
|
||||
readyState: HTMLMediaElement.HAVE_ENOUGH_DATA,
|
||||
});
|
||||
|
||||
const snapshot = playbackFeature.getSnapshot({
|
||||
target: video,
|
||||
get: mockState,
|
||||
initialState: mockState(),
|
||||
});
|
||||
const store = createStore({ features: [playbackFeature] });
|
||||
store.attach(video);
|
||||
|
||||
expect(snapshot).toEqual({
|
||||
paused: false,
|
||||
ended: false,
|
||||
started: true,
|
||||
waiting: false,
|
||||
});
|
||||
expect(store.state.paused).toBe(false);
|
||||
expect(store.state.ended).toBe(false);
|
||||
expect(store.state.started).toBe(true);
|
||||
expect(store.state.waiting).toBe(false);
|
||||
});
|
||||
|
||||
it('detects waiting state when buffering', () => {
|
||||
@@ -45,13 +28,10 @@ describe('playbackFeature', () => {
|
||||
readyState: HTMLMediaElement.HAVE_CURRENT_DATA,
|
||||
});
|
||||
|
||||
const snapshot = playbackFeature.getSnapshot({
|
||||
target: video,
|
||||
get: mockState,
|
||||
initialState: mockState(),
|
||||
});
|
||||
const store = createStore({ features: [playbackFeature] });
|
||||
store.attach(video);
|
||||
|
||||
expect(snapshot.waiting).toBe(true);
|
||||
expect(store.state.waiting).toBe(true);
|
||||
});
|
||||
|
||||
it('detects started from currentTime', () => {
|
||||
@@ -60,13 +40,10 @@ describe('playbackFeature', () => {
|
||||
currentTime: 5,
|
||||
});
|
||||
|
||||
const snapshot = playbackFeature.getSnapshot({
|
||||
target: video,
|
||||
get: mockState,
|
||||
initialState: mockState(),
|
||||
});
|
||||
const store = createStore({ features: [playbackFeature] });
|
||||
store.attach(video);
|
||||
|
||||
expect(snapshot.started).toBe(true);
|
||||
expect(store.state.started).toBe(true);
|
||||
});
|
||||
|
||||
it('detects started from playing state', () => {
|
||||
@@ -75,80 +52,71 @@ describe('playbackFeature', () => {
|
||||
currentTime: 0,
|
||||
});
|
||||
|
||||
const snapshot = playbackFeature.getSnapshot({
|
||||
target: video,
|
||||
get: mockState,
|
||||
initialState: mockState(),
|
||||
});
|
||||
const store = createStore({ features: [playbackFeature] });
|
||||
store.attach(video);
|
||||
|
||||
expect(snapshot.started).toBe(true);
|
||||
expect(store.state.started).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe', () => {
|
||||
it('calls update on play event', () => {
|
||||
const video = createMockVideo({});
|
||||
const update = vi.fn();
|
||||
const controller = new AbortController();
|
||||
it('updates on play event', () => {
|
||||
const video = createMockVideo({ paused: true });
|
||||
|
||||
playbackFeature.subscribe({
|
||||
target: video,
|
||||
update,
|
||||
signal: controller.signal,
|
||||
get: mockState,
|
||||
});
|
||||
const store = createStore({ features: [playbackFeature] });
|
||||
store.attach(video);
|
||||
|
||||
expect(store.state.paused).toBe(true);
|
||||
|
||||
// Update mock to playing state
|
||||
Object.defineProperty(video, 'paused', { value: false, writable: false, configurable: true });
|
||||
video.dispatchEvent(new Event('play'));
|
||||
|
||||
expect(update).toHaveBeenCalled();
|
||||
expect(store.state.paused).toBe(false);
|
||||
});
|
||||
|
||||
it('calls update on pause event', () => {
|
||||
const video = createMockVideo({});
|
||||
const update = vi.fn();
|
||||
const controller = new AbortController();
|
||||
it('updates on pause event', () => {
|
||||
const video = createMockVideo({ paused: false });
|
||||
|
||||
playbackFeature.subscribe({
|
||||
target: video,
|
||||
update,
|
||||
signal: controller.signal,
|
||||
get: mockState,
|
||||
});
|
||||
const store = createStore({ features: [playbackFeature] });
|
||||
store.attach(video);
|
||||
|
||||
expect(store.state.paused).toBe(false);
|
||||
|
||||
// Update mock to paused state
|
||||
Object.defineProperty(video, 'paused', { value: true, writable: false, configurable: true });
|
||||
video.dispatchEvent(new Event('pause'));
|
||||
|
||||
expect(update).toHaveBeenCalled();
|
||||
expect(store.state.paused).toBe(true);
|
||||
});
|
||||
|
||||
it('calls update on ended event', () => {
|
||||
const video = createMockVideo({});
|
||||
const update = vi.fn();
|
||||
const controller = new AbortController();
|
||||
it('updates on ended event', () => {
|
||||
const video = createMockVideo({ ended: false });
|
||||
|
||||
playbackFeature.subscribe({
|
||||
target: video,
|
||||
update,
|
||||
signal: controller.signal,
|
||||
get: mockState,
|
||||
});
|
||||
const store = createStore({ features: [playbackFeature] });
|
||||
store.attach(video);
|
||||
|
||||
expect(store.state.ended).toBe(false);
|
||||
|
||||
// Update mock to ended state
|
||||
Object.defineProperty(video, 'ended', { value: true, writable: false, configurable: true });
|
||||
video.dispatchEvent(new Event('ended'));
|
||||
|
||||
expect(update).toHaveBeenCalled();
|
||||
expect(store.state.ended).toBe(true);
|
||||
});
|
||||
|
||||
it('unsubscribes when signal aborted', () => {
|
||||
it('stops listening when store is destroyed', () => {
|
||||
const video = createMockVideo({});
|
||||
const update = vi.fn();
|
||||
const controller = new AbortController();
|
||||
|
||||
playbackFeature.subscribe({
|
||||
target: video,
|
||||
update,
|
||||
signal: controller.signal,
|
||||
get: mockState,
|
||||
});
|
||||
controller.abort();
|
||||
const store = createStore({ features: [playbackFeature] });
|
||||
store.attach(video);
|
||||
|
||||
store.destroy();
|
||||
|
||||
// Update mock to playing state
|
||||
Object.defineProperty(video, 'paused', { value: false, writable: false, configurable: true });
|
||||
video.dispatchEvent(new Event('play'));
|
||||
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
// State should not update after destroy
|
||||
expect(store.state.paused).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -190,16 +158,16 @@ function createMockVideo(
|
||||
const video = document.createElement('video');
|
||||
|
||||
if (overrides.paused !== undefined) {
|
||||
Object.defineProperty(video, 'paused', { value: overrides.paused, writable: false });
|
||||
Object.defineProperty(video, 'paused', { value: overrides.paused, writable: false, configurable: true });
|
||||
}
|
||||
if (overrides.ended !== undefined) {
|
||||
Object.defineProperty(video, 'ended', { value: overrides.ended, writable: false });
|
||||
Object.defineProperty(video, 'ended', { value: overrides.ended, writable: false, configurable: true });
|
||||
}
|
||||
if (overrides.currentTime !== undefined) {
|
||||
video.currentTime = overrides.currentTime;
|
||||
}
|
||||
if (overrides.readyState !== undefined) {
|
||||
Object.defineProperty(video, 'readyState', { value: overrides.readyState, writable: false });
|
||||
Object.defineProperty(video, 'readyState', { value: overrides.readyState, writable: false, configurable: true });
|
||||
}
|
||||
|
||||
return video;
|
||||
|
||||
@@ -1,36 +1,22 @@
|
||||
import { createStore } from '@videojs/store';
|
||||
import { noop } from '@videojs/utils/function';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { SourceState } from '../source';
|
||||
import { sourceFeature } from '../source';
|
||||
|
||||
const mockState = () =>
|
||||
({
|
||||
source: null,
|
||||
canPlay: false,
|
||||
loadSource: noop,
|
||||
}) as unknown as SourceState;
|
||||
|
||||
describe('sourceFeature', () => {
|
||||
describe('getSnapshot', () => {
|
||||
it('captures source state from video element', () => {
|
||||
describe('attach', () => {
|
||||
it('syncs source state on attach', () => {
|
||||
const video = createMockVideo({
|
||||
currentSrc: 'https://example.com/video.mp4',
|
||||
src: 'https://example.com/video.mp4',
|
||||
readyState: HTMLMediaElement.HAVE_ENOUGH_DATA,
|
||||
});
|
||||
|
||||
const snapshot = sourceFeature.getSnapshot({
|
||||
target: video,
|
||||
get: mockState,
|
||||
initialState: mockState(),
|
||||
});
|
||||
const store = createStore({ features: [sourceFeature] });
|
||||
store.attach(video);
|
||||
|
||||
expect(snapshot).toEqual({
|
||||
source: 'https://example.com/video.mp4',
|
||||
canPlay: true,
|
||||
});
|
||||
expect(store.state.source).toBe('https://example.com/video.mp4');
|
||||
expect(store.state.canPlay).toBe(true);
|
||||
});
|
||||
|
||||
it('returns null source when no source set', () => {
|
||||
@@ -39,67 +25,78 @@ describe('sourceFeature', () => {
|
||||
Object.defineProperty(video, 'currentSrc', { value: '', writable: false });
|
||||
Object.defineProperty(video, 'readyState', { value: HTMLMediaElement.HAVE_NOTHING, writable: false });
|
||||
|
||||
const snapshot = sourceFeature.getSnapshot({
|
||||
target: video,
|
||||
get: mockState,
|
||||
initialState: mockState(),
|
||||
const store = createStore({ features: [sourceFeature] });
|
||||
store.attach(video);
|
||||
|
||||
expect(store.state.source).toBe(null);
|
||||
expect(store.state.canPlay).toBe(false);
|
||||
});
|
||||
|
||||
it('updates on canplay event', () => {
|
||||
const video = createMockVideo({
|
||||
currentSrc: '',
|
||||
readyState: HTMLMediaElement.HAVE_NOTHING,
|
||||
});
|
||||
|
||||
expect(snapshot.source).toBe(null);
|
||||
expect(snapshot.canPlay).toBe(false);
|
||||
});
|
||||
});
|
||||
const store = createStore({ features: [sourceFeature] });
|
||||
store.attach(video);
|
||||
|
||||
describe('subscribe', () => {
|
||||
it('calls update on canplay event', () => {
|
||||
const video = createMockVideo({});
|
||||
const update = vi.fn();
|
||||
const controller = new AbortController();
|
||||
expect(store.state.canPlay).toBe(false);
|
||||
|
||||
sourceFeature.subscribe({
|
||||
target: video,
|
||||
update,
|
||||
signal: controller.signal,
|
||||
get: mockState,
|
||||
// Update mock to ready state
|
||||
Object.defineProperty(video, 'readyState', {
|
||||
value: HTMLMediaElement.HAVE_ENOUGH_DATA,
|
||||
writable: false,
|
||||
configurable: true,
|
||||
});
|
||||
video.dispatchEvent(new Event('canplay'));
|
||||
|
||||
expect(update).toHaveBeenCalled();
|
||||
expect(store.state.canPlay).toBe(true);
|
||||
});
|
||||
|
||||
it('calls update on loadstart event', () => {
|
||||
it('updates on loadstart event', () => {
|
||||
const video = createMockVideo({
|
||||
currentSrc: 'https://example.com/new.mp4',
|
||||
src: 'https://example.com/new.mp4',
|
||||
currentSrc: 'https://example.com/video.mp4',
|
||||
});
|
||||
const update = vi.fn();
|
||||
const controller = new AbortController();
|
||||
|
||||
sourceFeature.subscribe({
|
||||
target: video,
|
||||
update,
|
||||
signal: controller.signal,
|
||||
get: mockState,
|
||||
const store = createStore({ features: [sourceFeature] });
|
||||
store.attach(video);
|
||||
|
||||
expect(store.state.source).toBe('https://example.com/video.mp4');
|
||||
|
||||
// Update mock with new source
|
||||
Object.defineProperty(video, 'currentSrc', {
|
||||
value: 'https://example.com/new.mp4',
|
||||
writable: false,
|
||||
configurable: true,
|
||||
});
|
||||
video.dispatchEvent(new Event('loadstart'));
|
||||
|
||||
expect(update).toHaveBeenCalled();
|
||||
expect(store.state.source).toBe('https://example.com/new.mp4');
|
||||
});
|
||||
|
||||
it('calls update on emptied event', () => {
|
||||
const video = createMockVideo({});
|
||||
const update = vi.fn();
|
||||
const controller = new AbortController();
|
||||
it('updates on emptied event', () => {
|
||||
const video = createMockVideo({
|
||||
currentSrc: 'https://example.com/video.mp4',
|
||||
readyState: HTMLMediaElement.HAVE_ENOUGH_DATA,
|
||||
});
|
||||
|
||||
sourceFeature.subscribe({
|
||||
target: video,
|
||||
update,
|
||||
signal: controller.signal,
|
||||
get: mockState,
|
||||
const store = createStore({ features: [sourceFeature] });
|
||||
store.attach(video);
|
||||
|
||||
expect(store.state.canPlay).toBe(true);
|
||||
|
||||
// Update mock to empty state
|
||||
Object.defineProperty(video, 'currentSrc', { value: '', writable: false, configurable: true });
|
||||
Object.defineProperty(video, 'readyState', {
|
||||
value: HTMLMediaElement.HAVE_NOTHING,
|
||||
writable: false,
|
||||
configurable: true,
|
||||
});
|
||||
video.dispatchEvent(new Event('emptied'));
|
||||
|
||||
expect(update).toHaveBeenCalled();
|
||||
expect(store.state.source).toBe(null);
|
||||
expect(store.state.canPlay).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -132,13 +129,13 @@ function createMockVideo(
|
||||
const video = document.createElement('video');
|
||||
|
||||
if (overrides.currentSrc !== undefined) {
|
||||
Object.defineProperty(video, 'currentSrc', { value: overrides.currentSrc, writable: false });
|
||||
Object.defineProperty(video, 'currentSrc', { value: overrides.currentSrc, writable: false, configurable: true });
|
||||
}
|
||||
if (overrides.src !== undefined) {
|
||||
video.src = overrides.src;
|
||||
}
|
||||
if (overrides.readyState !== undefined) {
|
||||
Object.defineProperty(video, 'readyState', { value: overrides.readyState, writable: false });
|
||||
Object.defineProperty(video, 'readyState', { value: overrides.readyState, writable: false, configurable: true });
|
||||
}
|
||||
|
||||
return video;
|
||||
|
||||
@@ -1,35 +1,21 @@
|
||||
import { createStore } from '@videojs/store';
|
||||
import { noop } from '@videojs/utils/function';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { TimeState } from '../time';
|
||||
import { timeFeature } from '../time';
|
||||
|
||||
const mockState = () =>
|
||||
({
|
||||
currentTime: 0,
|
||||
duration: 0,
|
||||
seek: noop,
|
||||
}) as unknown as TimeState;
|
||||
|
||||
describe('timeFeature', () => {
|
||||
describe('getSnapshot', () => {
|
||||
it('captures current time state from video element', () => {
|
||||
describe('attach', () => {
|
||||
it('syncs time state on attach', () => {
|
||||
const video = createMockVideo({
|
||||
currentTime: 30,
|
||||
duration: 120,
|
||||
});
|
||||
|
||||
const snapshot = timeFeature.getSnapshot({
|
||||
target: video,
|
||||
get: mockState,
|
||||
initialState: mockState(),
|
||||
});
|
||||
const store = createStore({ features: [timeFeature] });
|
||||
store.attach(video);
|
||||
|
||||
expect(snapshot).toEqual({
|
||||
currentTime: 30,
|
||||
duration: 120,
|
||||
});
|
||||
expect(store.state.currentTime).toBe(30);
|
||||
expect(store.state.duration).toBe(120);
|
||||
});
|
||||
|
||||
it('handles NaN duration', () => {
|
||||
@@ -38,79 +24,71 @@ describe('timeFeature', () => {
|
||||
duration: Number.NaN,
|
||||
});
|
||||
|
||||
const snapshot = timeFeature.getSnapshot({
|
||||
target: video,
|
||||
get: mockState,
|
||||
initialState: mockState(),
|
||||
});
|
||||
const store = createStore({ features: [timeFeature] });
|
||||
store.attach(video);
|
||||
|
||||
expect(snapshot.duration).toBe(0);
|
||||
expect(store.state.duration).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe', () => {
|
||||
it('calls update on timeupdate event', () => {
|
||||
const video = createMockVideo({ currentTime: 42 });
|
||||
const update = vi.fn();
|
||||
const controller = new AbortController();
|
||||
it('updates on timeupdate event', () => {
|
||||
const video = createMockVideo({ currentTime: 0 });
|
||||
|
||||
timeFeature.subscribe({
|
||||
target: video,
|
||||
update,
|
||||
signal: controller.signal,
|
||||
get: mockState,
|
||||
});
|
||||
const store = createStore({ features: [timeFeature] });
|
||||
store.attach(video);
|
||||
|
||||
expect(store.state.currentTime).toBe(0);
|
||||
|
||||
// Update mock currentTime
|
||||
video.currentTime = 42;
|
||||
video.dispatchEvent(new Event('timeupdate'));
|
||||
|
||||
expect(update).toHaveBeenCalled();
|
||||
expect(store.state.currentTime).toBe(42);
|
||||
});
|
||||
|
||||
it('calls update on durationchange event', () => {
|
||||
const video = createMockVideo({ duration: 100 });
|
||||
const update = vi.fn();
|
||||
const controller = new AbortController();
|
||||
it('updates on durationchange event', () => {
|
||||
const video = createMockVideo({ duration: 0 });
|
||||
|
||||
timeFeature.subscribe({
|
||||
target: video,
|
||||
update,
|
||||
signal: controller.signal,
|
||||
get: mockState,
|
||||
});
|
||||
const store = createStore({ features: [timeFeature] });
|
||||
store.attach(video);
|
||||
|
||||
expect(store.state.duration).toBe(0);
|
||||
|
||||
// Update mock duration
|
||||
Object.defineProperty(video, 'duration', { value: 100, writable: false, configurable: true });
|
||||
video.dispatchEvent(new Event('durationchange'));
|
||||
|
||||
expect(update).toHaveBeenCalled();
|
||||
expect(store.state.duration).toBe(100);
|
||||
});
|
||||
|
||||
it('calls update on seeked event', () => {
|
||||
const video = createMockVideo({ currentTime: 50 });
|
||||
const update = vi.fn();
|
||||
const controller = new AbortController();
|
||||
it('updates on seeked event', () => {
|
||||
const video = createMockVideo({ currentTime: 0 });
|
||||
|
||||
timeFeature.subscribe({
|
||||
target: video,
|
||||
update,
|
||||
signal: controller.signal,
|
||||
get: mockState,
|
||||
});
|
||||
const store = createStore({ features: [timeFeature] });
|
||||
store.attach(video);
|
||||
|
||||
// Update mock currentTime
|
||||
video.currentTime = 50;
|
||||
video.dispatchEvent(new Event('seeked'));
|
||||
|
||||
expect(update).toHaveBeenCalled();
|
||||
expect(store.state.currentTime).toBe(50);
|
||||
});
|
||||
|
||||
it('calls update on emptied event', () => {
|
||||
const video = createMockVideo({});
|
||||
const update = vi.fn();
|
||||
const controller = new AbortController();
|
||||
|
||||
timeFeature.subscribe({
|
||||
target: video,
|
||||
update,
|
||||
signal: controller.signal,
|
||||
get: mockState,
|
||||
it('updates on emptied event', () => {
|
||||
const video = createMockVideo({
|
||||
currentTime: 30,
|
||||
duration: 120,
|
||||
});
|
||||
|
||||
const store = createStore({ features: [timeFeature] });
|
||||
store.attach(video);
|
||||
|
||||
// Update mock to empty state
|
||||
video.currentTime = 0;
|
||||
Object.defineProperty(video, 'duration', { value: Number.NaN, writable: false, configurable: true });
|
||||
video.dispatchEvent(new Event('emptied'));
|
||||
|
||||
expect(update).toHaveBeenCalled();
|
||||
expect(store.state.currentTime).toBe(0);
|
||||
expect(store.state.duration).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -147,7 +125,7 @@ function createMockVideo(
|
||||
video.currentTime = overrides.currentTime;
|
||||
}
|
||||
if (overrides.duration !== undefined) {
|
||||
Object.defineProperty(video, 'duration', { value: overrides.duration, writable: false });
|
||||
Object.defineProperty(video, 'duration', { value: overrides.duration, writable: false, configurable: true });
|
||||
}
|
||||
|
||||
return video;
|
||||
|
||||
@@ -1,54 +1,38 @@
|
||||
import { createStore } from '@videojs/store';
|
||||
import { noop } from '@videojs/utils/function';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { VolumeState } from '../volume';
|
||||
import { volumeFeature } from '../volume';
|
||||
|
||||
const mockState = () =>
|
||||
({
|
||||
volume: 1,
|
||||
muted: false,
|
||||
changeVolume: noop,
|
||||
toggleMute: noop,
|
||||
}) as unknown as VolumeState;
|
||||
|
||||
describe('volumeFeature', () => {
|
||||
describe('getSnapshot', () => {
|
||||
it('captures volume state from video element', () => {
|
||||
describe('attach', () => {
|
||||
it('syncs volume state on attach', () => {
|
||||
const video = createMockVideo({
|
||||
volume: 0.8,
|
||||
muted: false,
|
||||
});
|
||||
|
||||
const snapshot = volumeFeature.getSnapshot({
|
||||
target: video,
|
||||
get: mockState,
|
||||
initialState: mockState(),
|
||||
});
|
||||
const store = createStore({ features: [volumeFeature] });
|
||||
store.attach(video);
|
||||
|
||||
expect(snapshot).toEqual({
|
||||
volume: 0.8,
|
||||
muted: false,
|
||||
});
|
||||
expect(store.state.volume).toBe(0.8);
|
||||
expect(store.state.muted).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe', () => {
|
||||
it('calls update on volumechange event', () => {
|
||||
const video = createMockVideo({ volume: 0.5, muted: true });
|
||||
const update = vi.fn();
|
||||
const controller = new AbortController();
|
||||
it('updates on volumechange event', () => {
|
||||
const video = createMockVideo({ volume: 1, muted: false });
|
||||
|
||||
volumeFeature.subscribe({
|
||||
target: video,
|
||||
update,
|
||||
signal: controller.signal,
|
||||
get: mockState,
|
||||
});
|
||||
const store = createStore({ features: [volumeFeature] });
|
||||
store.attach(video);
|
||||
|
||||
expect(store.state.volume).toBe(1);
|
||||
|
||||
// Update mock volume
|
||||
video.volume = 0.5;
|
||||
video.muted = true;
|
||||
video.dispatchEvent(new Event('volumechange'));
|
||||
|
||||
expect(update).toHaveBeenCalled();
|
||||
expect(store.state.volume).toBe(0.5);
|
||||
expect(store.state.muted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -23,17 +23,20 @@ export const timeFeature = defineFeature<HTMLMediaElement>()({
|
||||
},
|
||||
}),
|
||||
|
||||
getSnapshot: ({ target }) => ({
|
||||
currentTime: target.currentTime,
|
||||
duration: target.duration || 0,
|
||||
}),
|
||||
attach({ target, signal, set }) {
|
||||
const sync = () =>
|
||||
set({
|
||||
currentTime: target.currentTime,
|
||||
duration: target.duration || 0,
|
||||
});
|
||||
|
||||
subscribe: ({ target, update, signal }) => {
|
||||
listen(target, 'timeupdate', update, { signal });
|
||||
listen(target, 'durationchange', update, { signal });
|
||||
listen(target, 'seeked', update, { signal });
|
||||
listen(target, 'loadedmetadata', update, { signal });
|
||||
listen(target, 'emptied', update, { signal });
|
||||
sync();
|
||||
|
||||
listen(target, 'timeupdate', sync, { signal });
|
||||
listen(target, 'durationchange', sync, { signal });
|
||||
listen(target, 'seeked', sync, { signal });
|
||||
listen(target, 'loadedmetadata', sync, { signal });
|
||||
listen(target, 'emptied', sync, { signal });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -33,13 +33,12 @@ export const volumeFeature = defineFeature<HTMLMediaElement>()({
|
||||
},
|
||||
}),
|
||||
|
||||
getSnapshot: ({ target }) => ({
|
||||
volume: target.volume,
|
||||
muted: target.muted,
|
||||
}),
|
||||
attach({ target, signal, set }) {
|
||||
const sync = () => set({ volume: target.volume, muted: target.muted });
|
||||
|
||||
subscribe: ({ target, update, signal }) => {
|
||||
listen(target, 'volumechange', update, { signal });
|
||||
sync();
|
||||
|
||||
listen(target, 'volumechange', sync, { signal });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user