96 lines
4.5 KiB
TypeScript
96 lines
4.5 KiB
TypeScript
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<typeof setup>;
|
||
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 });
|
||
});
|
||
});
|