import { beforeEach, describe, expect, it } from 'vitest'; import { AddAnimationTrackCommand, MoveAnimationKeysCommand, RemoveAnimationKeysCommand, RemoveAnimationTracksCommand, SetAnimationKeyCommand, SetAnimationRootCommand, } from '../../src/core/commands/animationCommands'; import { CreateAssetCommand } from '../../src/core/commands/assetCommands'; import type { AnimationAsset } from '../../src/core/model/Asset'; import { createEmptyProject } from '../../src/core/model/Project'; import { EditorState, trackSelection } from '../../src/editor/state'; /** A sprite, an animation over it, one row on `x` with keys on 0 and 4, and a second row on `y`. */ function setup(): { state: EditorState; animation: AnimationAsset; x: string; y: string } { const state = new EditorState(); state.loadProject(createEmptyProject('p', 'P'), 1); const sprite = new CreateAssetCommand('sprite', 'Dancer'); expect(state.execute(sprite)).toBe(true); const create = new CreateAssetCommand('animation', 'Walk'); expect(state.execute(create)).toBe(true); const animation = create.asset as AnimationAsset; expect(state.execute(new SetAnimationRootCommand(animation.id, sprite.asset.id))).toBe(true); const x = new AddAnimationTrackCommand(animation.id, '', 'x'); const y = new AddAnimationTrackCommand(animation.id, '', 'y'); expect(state.execute(x)).toBe(true); expect(state.execute(y)).toBe(true); expect(state.execute(new SetAnimationKeyCommand(animation.id, x.trackId, 0, 0))).toBe(true); expect(state.execute(new SetAnimationKeyCommand(animation.id, x.trackId, 4, 40))).toBe(true); return { state, animation, x: x.trackId, y: y.trackId }; } describe('selecting rows and keys of a timeline', () => { let f: ReturnType; beforeEach(() => { f = setup(); }); it('a row alone is a track selection without keys, and keeps the animation on the tab', () => { f.state.select({ kind: 'track', assetId: f.animation.id, trackId: f.x, keys: [] }); expect(f.state.selectedKeys).toEqual([]); expect(f.state.selectedAnimationId).toBe(f.animation.id); }); it('the row of the key picked last is the one the inspector describes', () => { const keys = [ { trackId: f.x, frame: 0 }, { trackId: f.x, frame: 4 }, ]; f.state.select(trackSelection(f.animation.id, keys)); expect(f.state.selection).toMatchObject({ kind: 'track', trackId: f.x, keys }); // The same key twice is one key. f.state.select(trackSelection(f.animation.id, [...keys, keys[0]!])); expect(f.state.selectedKeys).toHaveLength(2); }); it('accepts the short form a test hook passes, without keys', () => { f.state.select({ kind: 'track', assetId: f.animation.id, trackId: f.y } as never); expect(f.state.selection).toEqual({ kind: 'track', assetId: f.animation.id, trackId: f.y, keys: [] }); }); it('drops a key that was deleted and keeps the rest', () => { f.state.select( trackSelection(f.animation.id, [ { trackId: f.x, frame: 0 }, { trackId: f.x, frame: 4 }, ]), ); f.state.execute(new RemoveAnimationKeysCommand(f.animation.id, [{ trackId: f.x, frame: 4 }])); expect(f.state.selectedKeys).toEqual([{ trackId: f.x, frame: 0 }]); }); it('a moved key leaves the selection – the frame it was picked at holds no key any more', () => { f.state.select(trackSelection(f.animation.id, [{ trackId: f.x, frame: 4 }])); f.state.execute(new MoveAnimationKeysCommand(f.animation.id, [{ trackId: f.x, frame: 4 }], 2)); // The canvas re-selects at the new frame after a drag; the state alone falls back to the row. expect(f.state.selection).toEqual({ kind: 'track', assetId: f.animation.id, trackId: f.x, keys: [] }); }); it('falls back to the animation when the row itself is deleted, so the tab stays open', () => { f.state.select({ kind: 'track', assetId: f.animation.id, trackId: f.x, keys: [] }); f.state.execute(new RemoveAnimationTracksCommand(f.animation.id, [f.x])); expect(f.state.selection).toEqual({ kind: 'asset', assetId: f.animation.id }); }); it('comes back with undo: the row returns, the selection was already elsewhere', () => { f.state.select({ kind: 'track', assetId: f.animation.id, trackId: f.x, keys: [] }); f.state.execute(new RemoveAnimationTracksCommand(f.animation.id, [f.x])); f.state.undo(); const rows = (f.state.project.assets.find((a) => a.id === f.animation.id) as AnimationAsset).tracks; expect(rows.map((t) => t.id)).toEqual([f.x, f.y]); expect(f.state.selection).toEqual({ kind: 'asset', assetId: f.animation.id }); }); });