mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-30 12:29:27 +00:00
147 lines
7.2 KiB
JavaScript
147 lines
7.2 KiB
JavaScript
import { describe, expect, it } from 'vitest';
|
|
import { EVENTS, MAX_MEDIA_TIME } from '../shared/constants.js';
|
|
import {
|
|
commitForceSyncMediaState,
|
|
effectiveMediaPosition,
|
|
snapshotMediaState,
|
|
updateMediaStateFromControl
|
|
} from './media-state.js';
|
|
|
|
function room(mediaState = null) {
|
|
return { mediaState };
|
|
}
|
|
|
|
describe('canonical media state', () => {
|
|
it('starts null and snapshots null', () => {
|
|
expect(snapshotMediaState(null, 1000)).toBeNull();
|
|
});
|
|
|
|
it('projects playing state and clamps elapsed time', () => {
|
|
const state = { revision: 1, playbackState: 'playing', currentTime: 10, updatedAt: 1000, updatedBy: 'a' };
|
|
expect(effectiveMediaPosition(state, 3500)).toBe(12.5);
|
|
expect(effectiveMediaPosition(state, 500)).toBe(10);
|
|
expect(effectiveMediaPosition({ ...state, currentTime: MAX_MEDIA_TIME }, 3500)).toBe(MAX_MEDIA_TIME);
|
|
});
|
|
|
|
it('keeps paused state fixed', () => {
|
|
const state = { revision: 1, playbackState: 'paused', currentTime: 10, updatedAt: 1000, updatedBy: 'a' };
|
|
expect(effectiveMediaPosition(state, 601000)).toBe(10);
|
|
expect(snapshotMediaState(state, 601000)).toEqual({
|
|
revision: 1,
|
|
playbackState: 'paused',
|
|
currentTime: 10,
|
|
updatedBy: 'a'
|
|
});
|
|
});
|
|
|
|
it('initializes and updates PLAY with server-owned revisions', () => {
|
|
const target = room();
|
|
expect(updateMediaStateFromControl(target, EVENTS.PLAY, { currentTime: 12, revision: 999 }, 'a', { now: 1000 })).toBe(true);
|
|
expect(target.mediaState).toEqual({ revision: 1, playbackState: 'playing', currentTime: 12, updatedAt: 1000, updatedBy: 'a' });
|
|
expect(updateMediaStateFromControl(target, EVENTS.PLAY, { currentTime: 20 }, 'b', { now: 2000 })).toBe(true);
|
|
expect(target.mediaState.revision).toBe(2);
|
|
expect(target.mediaState.updatedBy).toBe('b');
|
|
});
|
|
|
|
it('does not invent an initial PLAY position and preserves a known effective position', () => {
|
|
const target = room();
|
|
expect(updateMediaStateFromControl(target, EVENTS.PLAY, {}, 'a', { now: 1000 })).toBe(false);
|
|
expect(target.mediaState).toBeNull();
|
|
target.mediaState = { revision: 1, playbackState: 'playing', currentTime: 5, updatedAt: 1000, updatedBy: 'a' };
|
|
expect(updateMediaStateFromControl(target, EVENTS.PLAY, {}, 'a', { now: 3000 })).toBe(true);
|
|
expect(target.mediaState.currentTime).toBe(7);
|
|
});
|
|
|
|
it('freezes PAUSE at its event or effective canonical position', () => {
|
|
const target = room({ revision: 1, playbackState: 'playing', currentTime: 10, updatedAt: 1000, updatedBy: 'a' });
|
|
expect(updateMediaStateFromControl(target, EVENTS.PAUSE, {}, 'a', { now: 4000 })).toBe(true);
|
|
expect(target.mediaState).toEqual({ revision: 2, playbackState: 'paused', currentTime: 13, updatedAt: 4000, updatedBy: 'a' });
|
|
expect(effectiveMediaPosition(target.mediaState, 9000)).toBe(13);
|
|
});
|
|
|
|
it('uses SEEK targetTime, preserves playback state, and lets the second controller win', () => {
|
|
const target = room({ revision: 3, playbackState: 'playing', currentTime: 10, updatedAt: 1000, updatedBy: 'a' });
|
|
expect(updateMediaStateFromControl(target, EVENTS.SEEK, { currentTime: 50, targetTime: 100 }, 'a', { now: 2000 })).toBe(true);
|
|
expect(target.mediaState).toMatchObject({ revision: 4, playbackState: 'playing', currentTime: 100, updatedBy: 'a' });
|
|
expect(updateMediaStateFromControl(target, EVENTS.SEEK, { targetTime: 200 }, 'b', { now: 2001 })).toBe(true);
|
|
expect(target.mediaState).toMatchObject({ revision: 5, currentTime: 200, updatedBy: 'b' });
|
|
});
|
|
|
|
it('tracks only the current sender shared media title and honors an explicit privacy null', () => {
|
|
const target = room();
|
|
expect(updateMediaStateFromControl(
|
|
target,
|
|
EVENTS.PLAY,
|
|
{ currentTime: 10, mediaTitle: 'Series S01E01' },
|
|
'a',
|
|
{ now: 1000 }
|
|
)).toBe(true);
|
|
expect(snapshotMediaState(target.mediaState, 1000).mediaTitle).toBe('Series S01E01');
|
|
|
|
expect(updateMediaStateFromControl(
|
|
target,
|
|
EVENTS.SEEK,
|
|
{ targetTime: 20 },
|
|
'b',
|
|
{ now: 2000, senderMediaTitle: 'Series S01E02' }
|
|
)).toBe(true);
|
|
expect(target.mediaState.mediaTitle).toBe('Series S01E02');
|
|
|
|
expect(updateMediaStateFromControl(
|
|
target,
|
|
EVENTS.PAUSE,
|
|
{ currentTime: 20, mediaTitle: null },
|
|
'b',
|
|
{ now: 3000, senderMediaTitle: 'stale S01E01' }
|
|
)).toBe(true);
|
|
expect(target.mediaState).not.toHaveProperty('mediaTitle');
|
|
});
|
|
|
|
it('ignores client-supplied playback state while seeking', () => {
|
|
const target = room({ revision: 3, playbackState: 'paused', currentTime: 10, updatedAt: 1000, updatedBy: 'a' });
|
|
expect(updateMediaStateFromControl(
|
|
target,
|
|
EVENTS.SEEK,
|
|
{ targetTime: 100, playbackState: 'playing' },
|
|
'b',
|
|
{ now: 2000, senderPlaybackState: 'playing' }
|
|
)).toBe(true);
|
|
expect(target.mediaState).toMatchObject({ revision: 4, playbackState: 'paused', currentTime: 100 });
|
|
});
|
|
|
|
it('uses an observed sender state only to establish an otherwise ambiguous first SEEK', () => {
|
|
const target = room();
|
|
expect(updateMediaStateFromControl(target, EVENTS.SEEK, { targetTime: 50 }, 'a', { now: 1000 })).toBe(false);
|
|
expect(updateMediaStateFromControl(target, EVENTS.SEEK, { targetTime: 50 }, 'a', { now: 1000, senderPlaybackState: 'paused' })).toBe(true);
|
|
expect(target.mediaState).toMatchObject({ revision: 1, playbackState: 'paused', currentTime: 50 });
|
|
});
|
|
|
|
it('rejects non-finite/missing controls without corruption and clamps existing protocol bounds', () => {
|
|
const original = { revision: 2, playbackState: 'paused', currentTime: 30, updatedAt: 1000, updatedBy: 'a' };
|
|
for (const payload of [{ targetTime: NaN }, { targetTime: Infinity }, { targetTime: '50' }, {}]) {
|
|
const target = room({ ...original });
|
|
expect(updateMediaStateFromControl(target, EVENTS.SEEK, payload, 'b', { now: 2000 })).toBe(false);
|
|
expect(target.mediaState).toEqual(original);
|
|
}
|
|
const low = room({ ...original });
|
|
updateMediaStateFromControl(low, EVENTS.SEEK, { targetTime: -5 }, 'b', { now: 2000 });
|
|
expect(low.mediaState.currentTime).toBe(0);
|
|
const high = room({ ...original });
|
|
updateMediaStateFromControl(high, EVENTS.SEEK, { targetTime: MAX_MEDIA_TIME + 5 }, 'b', { now: 2000 });
|
|
expect(high.mediaState.currentTime).toBe(MAX_MEDIA_TIME);
|
|
});
|
|
|
|
it('commits Force Sync only at execute time', () => {
|
|
const target = room({ revision: 4, playbackState: 'paused', currentTime: 90, updatedAt: 1000, updatedBy: 'a' });
|
|
expect(commitForceSyncMediaState(target, 500, 'b', 2000, 'Series S02E03')).toBe(true);
|
|
expect(target.mediaState).toEqual({
|
|
revision: 5,
|
|
playbackState: 'playing',
|
|
currentTime: 500,
|
|
updatedAt: 2000,
|
|
updatedBy: 'b',
|
|
mediaTitle: 'Series S02E03'
|
|
});
|
|
});
|
|
});
|