/** * The rules behind the Variables tab (#252): which document a target is – the same list asked for * twice is one tab, a list inside it another – what the tab says, and how much of a list the * inspector draws. */ import { beforeEach, describe, expect, it } from 'vitest'; import { CreateAssetCommand, DeleteAssetCommand } from '../../src/core/commands/assetCommands'; import { AddNodeCommand } from '../../src/core/commands/graphCommands'; import { PlaceAssetCommand, RenameSceneObjectCommand } from '../../src/core/commands/sceneObjectCommands'; import { AddVariableCommand } from '../../src/core/commands/variableCommands'; import { createEmptyProject } from '../../src/core/model/Project'; import { resolveList } from '../../src/editor/list/slot'; import { EditorState, type ListSlot, type ValuesTarget } from '../../src/editor/state'; import { deeperSlot, INLINE_LIMIT, shownItems, valuesKey } from '../../src/editor/variables/target'; import { valuesTitle } from '../../src/editor/variables/VariablesEditor'; function setup() { const state = new EditorState(); state.loadProject(createEmptyProject('p', 'P'), 1); const create = new CreateAssetCommand('behavior', 'Pickup'); expect(state.execute(create)).toBe(true); const id = create.asset.id; expect(state.execute(new AddVariableCommand(id, 'loots', 'list'))).toBe(true); return { state, id }; } describe('the document of a Variables tab', () => { const slot: ListSlot = { kind: 'asset', assetId: 'a', key: 'loots' }; it('is the same for the same list, however the address was built', () => { const built = { key: 'loots', assetId: 'a', kind: 'asset', path: [] } as const; expect(valuesKey({ kind: 'list', slot })).toBe(valuesKey({ kind: 'list', slot: built })); expect(valuesKey({ kind: 'list', slot: deeperSlot(slot, [2]) })).toBe( valuesKey({ kind: 'list', slot: { ...slot, path: [2] } }), ); }); it('is another one for a list inside it, so opening that one adds a tab', () => { const outer = valuesKey({ kind: 'list', slot }); const inner = valuesKey({ kind: 'list', slot: deeperSlot(slot, [2, 'drops']) }); expect(inner).not.toBe(outer); // A key and an index that read alike are still two steps. expect(valuesKey({ kind: 'list', slot: deeperSlot(slot, ['2']) })).not.toBe( valuesKey({ kind: 'list', slot: deeperSlot(slot, [2]) }), ); }); it('keeps the variables of an owner apart from a list of it and from another owner', () => { const asset: ValuesTarget = { kind: 'variables', owner: { kind: 'asset', assetId: 'a' } }; const object: ValuesTarget = { kind: 'variables', owner: { kind: 'object', holderId: 's', objectId: 'a' }, }; const keys = new Set([asset, object, { kind: 'list', slot } as const].map(valuesKey)); expect(keys.size).toBe(3); }); }); describe('how much of a list the inspector draws', () => { it('draws everything up to the limit and counts the rest', () => { expect(shownItems(5, INLINE_LIMIT)).toEqual({ shown: 5, more: 0 }); expect(shownItems(INLINE_LIMIT, INLINE_LIMIT)).toEqual({ shown: INLINE_LIMIT, more: 0 }); expect(shownItems(23, INLINE_LIMIT)).toEqual({ shown: INLINE_LIMIT, more: 3 }); }); it('draws all of it without a limit – the tab', () => { expect(shownItems(500)).toEqual({ shown: 500, more: 0 }); }); }); describe('what a Variables tab says', () => { let f: ReturnType; beforeEach(() => (f = setup())); it('names the asset, the placed object and the list', () => { expect(valuesTitle(f.state, { kind: 'variables', owner: { kind: 'asset', assetId: f.id } })).toBe( 'Pickup · Variables', ); const createScene = new CreateAssetCommand('scene', 'Level'); expect(f.state.execute(createScene)).toBe(true); const scene = createScene.asset.id; const place = new PlaceAssetCommand(scene, f.id, { x: 0, y: 0 }); expect(f.state.execute(place)).toBe(true); const object: ValuesTarget = { kind: 'variables', owner: { kind: 'object', holderId: scene, objectId: place.objectId }, }; expect(valuesTitle(f.state, object)).toBe('Pickup · Variables'); expect(f.state.execute(new RenameSceneObjectCommand(scene, place.objectId, 'Coin 3'))).toBe(true); expect(valuesTitle(f.state, object)).toBe('Coin 3 · Variables'); const slot: ListSlot = { kind: 'asset', assetId: f.id, key: 'loots' }; expect(valuesTitle(f.state, { kind: 'list', slot })).toBe('Pickup · loots'); }); it('says nothing once the owner is gone – the tab closes then', () => { const target: ValuesTarget = { kind: 'variables', owner: { kind: 'asset', assetId: f.id } }; const list: ValuesTarget = { kind: 'list', slot: { kind: 'asset', assetId: f.id, key: 'loots' } }; expect(f.state.execute(new DeleteAssetCommand(f.id))).toBe(true); expect(valuesTitle(f.state, target)).toBe(null); expect(valuesTitle(f.state, list)).toBe(null); }); it('has no Variables tab for an asset that declares none', () => { const texture = new CreateAssetCommand('texture', 'T'); expect(f.state.execute(texture)).toBe(true); const target: ValuesTarget = { kind: 'variables', owner: { kind: 'asset', assetId: texture.asset.id } }; expect(valuesTitle(f.state, target)).toBe(null); }); }); describe('opening a list in a tab', () => { let f: ReturnType; beforeEach(() => (f = setup())); it('asks for a tab on the list itself, or on one inside it', () => { const list = resolveList(f.state, { kind: 'asset', assetId: f.id, key: 'loots' })!; list.replace([[1, 2]]); let asked = 0; f.state.on('values', () => (asked += 1)); list.openList!([]); expect(f.state.openedValues).toEqual({ kind: 'list', slot: { kind: 'asset', assetId: f.id, key: 'loots', path: [] }, }); list.openList!([0]); expect(f.state.openedValues).toEqual({ kind: 'list', slot: { kind: 'asset', assetId: f.id, key: 'loots', path: [0] }, }); expect(asked).toBe(2); }); it('offers no tab for a list a node holds inline', () => { const add = new AddNodeCommand(f.id, 'List', 0, 0); expect(f.state.execute(add)).toBe(true); const node = resolveList(f.state, { kind: 'node', assetId: f.id, nodeId: add.node.id, key: 'value' })!; expect(node.openList).toBeUndefined(); node.replace([[1]]); expect(node.sub([0])!.openList).toBeUndefined(); }); });