/** * The eyedropper of an object's place (#276): a click in the view only moves a candidate, Enter or * Take writes it as one undo step, and every other way out – cancelling, or the editor moving on – * writes nothing. */ import { describe, expect, it } from 'vitest'; import { RenameSceneObjectCommand, SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands'; import { AddVariableCommand } from '../../src/core/commands/variableCommands'; import { objectChoices, objectReference, referencedId } from '../../src/core/model/objectRefs'; import { findObjectHolder } from '../../src/core/model/Project'; import { objectPicker, pickAt, pickView } from '../../src/editor/objectPick'; import { EditorState, objectSelection } from '../../src/editor/state'; import { makeBehavior, makeSpriteScene, place } from '../helpers'; /** A Watcher aimed at a Crate, a Barrel beside them, and the Watcher selected in the editor. */ function setup() { const f = makeSpriteScene(); const behavior = makeBehavior(f, 'Watcher'); f.stack.execute(new AddVariableCommand(behavior.id, 'target', 'object')); const watcher = place(f, behavior.id, { x: 0, y: 0 }); const crate = place(f, f.spriteId, { x: 10, y: 0 }); const barrel = place(f, f.spriteId, { x: 20, y: 0 }); f.stack.execute(new RenameSceneObjectCommand(f.sceneId, watcher, 'Watcher')); f.stack.execute(new RenameSceneObjectCommand(f.sceneId, crate, 'Crate')); f.stack.execute(new SetOverrideCommand(f.sceneId, watcher, 'target', objectReference(crate))); f.project.activeSceneId = f.sceneId; const state = new EditorState(); state.loadProject(f.project, 1); state.select(objectSelection(f.sceneId, [watcher])); const target = () => referencedId( findObjectHolder(state.project, f.sceneId)!.objects.find((o) => o.id === watcher)!.overrides.target, ); const choices = () => objectChoices(state.project, findObjectHolder(state.project, f.sceneId)!, watcher); const picker = objectPicker(state, f.sceneId, watcher, choices); const owner = {}; let ended = 0; const start = () => picker.toggle(owner, { field: 'target', current: target(), take: (ref) => state.execute(new SetOverrideCommand(f.sceneId, watcher, 'target', objectReference(ref))), ended: () => ended++, }); return { f, state, watcher, crate, barrel, target, picker, owner, start, endedCount: () => ended }; } describe('the eyedropper of an object place (#276)', () => { it('starts on the current target and names the field it fills', () => { const s = setup(); expect(pickView(s.state, s.f.sceneId)).toBe('scene'); s.start(); expect(s.state.objectPick?.candidate).toBe(s.crate); expect(s.state.objectPick?.name).toBe('Watcher · target'); expect(s.picker.picking(s.owner)).toBe(true); }); it('moves only a candidate on a click, and Enter writes it as one undo step', () => { const s = setup(); s.start(); pickAt(s.state, [s.crate, s.barrel]); expect(s.state.objectPick?.candidate).toBe(s.barrel); expect(s.target()).toBe(s.crate); expect(s.state.commands.canUndo).toBe(false); s.state.endObjectPick('take'); expect(s.target()).toBe(s.barrel); expect(s.endedCount()).toBe(1); s.state.undo(); expect(s.target()).toBe(s.crate); expect(s.state.commands.canUndo).toBe(false); }); it('leaves no undo step when the candidate is the target already', () => { const s = setup(); s.start(); s.state.endObjectPick('take'); expect(s.state.pickEnd).toBe('take'); expect(s.state.commands.canUndo).toBe(false); }); it('writes nothing when cancelled – a second press of its button is that too', () => { const s = setup(); s.start(); pickAt(s.state, [s.barrel]); s.start(); expect(s.state.objectPick).toBeNull(); expect(s.state.pickEnd).toBe('cancel'); expect(s.target()).toBe(s.crate); expect(s.endedCount()).toBe(1); }); it('keeps the candidate on empty space and on what it may not name, and says why for the latter', () => { const s = setup(); s.start(); pickAt(s.state, []); expect(s.state.objectPick?.candidate).toBe(s.crate); expect(s.state.notice).toBe(''); pickAt(s.state, ['not-in-this-scene']); expect(s.state.objectPick?.candidate).toBe(s.crate); expect(s.state.notice).toContain('Watcher · target'); }); it('is dropped, unwritten, by another selection, an undo and play', () => { for (const moveOn of [ (state: EditorState) => state.select(null), (state: EditorState) => state.undo(), (state: EditorState) => void state.enterPlay(), ]) { const s = setup(); s.start(); pickAt(s.state, [s.barrel]); moveOn(s.state); expect(s.state.objectPick).toBeNull(); expect(s.state.pickEnd).toBe('drop'); expect(s.endedCount()).toBe(1); s.state.exitPlay(); expect(s.target()).toBe(s.crate); expect(s.state.commands.canUndo).toBe(false); } }); it('does not start for a scene other than the one open', () => { const s = setup(); s.state.project.activeSceneId = ''; s.start(); expect(s.state.objectPick).toBeNull(); expect(s.state.notice).toContain('Open the scene'); }); });