Files
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

96 lines
3.1 KiB
TypeScript

import { describe, expect, test } from 'vitest';
import { resolvePlaybackCursor, EMPTY_SCENE_DWELL } from '@/lib/choreography';
import type { Action } from '@/lib/types/action';
import type { Scene } from '@/lib/types/stage';
const a = (id: string): Action => ({ id, type: 'speech', text: id }) as Action;
const sc = (id: string, actions: Action[]): Scene =>
({
id,
stageId: 's',
type: 'slide',
title: id,
order: 1,
content: { type: 'slide', canvas: {} },
actions,
}) as unknown as Scene;
describe('resolvePlaybackCursor', () => {
test('returns the action at the cursor within a scene', () => {
const scenes = [sc('S0', [a('a0'), a('a1')])];
expect(resolvePlaybackCursor(scenes, 0, 0)).toMatchObject({
action: { id: 'a0' },
sceneId: 'S0',
sceneIndex: 0,
actionIndex: 0,
});
expect(resolvePlaybackCursor(scenes, 0, 1)).toMatchObject({
action: { id: 'a1' },
sceneId: 'S0',
actionIndex: 1,
});
});
test('advances past an exhausted scene to the next scene', () => {
const scenes = [sc('S0', [a('a0')]), sc('S1', [a('b0')])];
expect(resolvePlaybackCursor(scenes, 0, 1)).toMatchObject({
action: { id: 'b0' },
sceneId: 'S1',
sceneIndex: 1,
actionIndex: 0,
});
});
test('returns null when all scenes are consumed', () => {
expect(resolvePlaybackCursor([sc('S0', [a('a0')])], 0, 1)).toBeNull();
expect(resolvePlaybackCursor([], 0, 0)).toBeNull();
});
test('a zero-action scene yields ONE synthetic dwell beat (not skipped)', () => {
const scenes = [sc('S0', [])];
expect(resolvePlaybackCursor(scenes, 0, 0)).toMatchObject({
action: EMPTY_SCENE_DWELL,
sceneId: 'S0',
sceneIndex: 0,
actionIndex: 0,
});
// After the dwell beat is consumed (actionIndex advanced), the scene is done.
expect(resolvePlaybackCursor(scenes, 0, 1)).toBeNull();
});
test('the dwell beat is an empty-text speech (routes to the reading-timer dwell)', () => {
expect(EMPTY_SCENE_DWELL).toMatchObject({ type: 'speech', text: '' });
});
test('a zero-action scene shows, then playback continues to the next scene', () => {
const scenes = [sc('S0', []), sc('S1', [a('b0')])];
expect(resolvePlaybackCursor(scenes, 0, 0)).toMatchObject({
action: EMPTY_SCENE_DWELL,
sceneId: 'S0',
});
expect(resolvePlaybackCursor(scenes, 0, 1)).toMatchObject({
action: { id: 'b0' },
sceneId: 'S1',
sceneIndex: 1,
actionIndex: 0,
});
});
test('a zero-action scene in the middle still dwells when reached from the prior scene', () => {
const scenes = [sc('S0', [a('a0')]), sc('S1', []), sc('S2', [a('c0')])];
// Finished S0 (actionIndex past its single action) → advance into empty S1 → dwell.
expect(resolvePlaybackCursor(scenes, 0, 1)).toMatchObject({
action: EMPTY_SCENE_DWELL,
sceneId: 'S1',
sceneIndex: 1,
actionIndex: 0,
});
// After S1's dwell → C0 on S2.
expect(resolvePlaybackCursor(scenes, 1, 1)).toMatchObject({
action: { id: 'c0' },
sceneId: 'S2',
sceneIndex: 2,
});
});
});