/** * A color and a texture are kinds of their own wherever a kind is picked – of a variable, of a struct * field, of a declared port – while on a data edge either is still the number or the text it wraps. */ import { describe, expect, it } from 'vitest'; import { AddStructFieldCommand, AddVariableCommand } from '../../src/core/commands/variableCommands'; import { declaredPortType, inlineInputDef, isPortOf, OBJECT_TYPE_PARAM, portDefOf, portKindOf, propertyPort, withInlineDefault, } from '../../src/core/graph'; import { BUILT_IN_NODES } from '../../src/core/graph/nodes'; import { tagged } from '../../src/core/model/Properties'; import { findAsset } from '../../src/core/model/Project'; import { type PropertyDef, structOf, variableDef } from '../../src/core/properties/PropertySchema'; import { makeBehavior, makeProject, roundTrip } from '../helpers'; const COLOR: PropertyDef = { key: 'color', label: 'Color', kind: 'color', default: 0 }; const SPRITE = { [OBJECT_TYPE_PARAM]: 'sprite' }; describe('a variable of kind color or texture', () => { it('declares its default as the number or the id, empty when nothing is given', () => { expect(variableDef('pic', 'texture', 'fr-0-1')).toEqual({ key: 'pic', label: 'pic', kind: 'texture', default: 'fr-0-1', }); expect(variableDef('tint', 'color', '#ff0000').default).toBe(0xff0000); expect(variableDef('pic', 'texture').default).toBe(''); expect(variableDef('tint', 'color').default).toBe(0); }); it('survives a save and load with its kind', () => { const f = makeProject(); const behavior = makeBehavior(f, 'B'); f.stack.execute(new AddVariableCommand(behavior.id, 'pic', 'texture')); f.stack.execute(new AddVariableCommand(behavior.id, 'tint', 'color')); findAsset(f.project, behavior.id)!.props.pic = 'fr-0-1'; const back = findAsset(roundTrip(f.project), behavior.id); expect(back?.type === 'behavior' && back.variables.map((v) => `${v.key}:${v.kind}`)).toEqual([ 'pic:texture', 'tint:color', ]); expect(back?.props).toMatchObject({ pic: 'fr-0-1', tint: 0 }); }); }); describe('a struct field of kind texture', () => { /** Declared, so the value needs no tag: the field says what it is, as a color field does. */ it('holds the plain id, and starts empty', () => { expect(structOf([{ key: 'pic', kind: 'texture' }], { pic: tagged('texture', 'fr-0-1') })).toEqual({ pic: 'fr-0-1', }); expect(structOf([{ key: 'pic', kind: 'texture' }], undefined)).toEqual({ pic: '' }); }); it('is added to a struct variable like any other field', () => { const f = makeProject(); const behavior = makeBehavior(f, 'B'); f.stack.execute(new AddVariableCommand(behavior.id, 'look', 'struct')); f.stack.execute(new AddStructFieldCommand(behavior.id, 'look', { key: 'pic', kind: 'texture' })); findAsset(f.project, behavior.id)!.props.look = { pic: 'fr-0-1' }; expect(findAsset(roundTrip(f.project), behavior.id)?.props.look).toEqual({ pic: 'fr-0-1' }); }); }); describe('a declared port of kind color or texture', () => { it('is the kind of the property it names, travelling as a number or a text', () => { expect(portKindOf(COLOR)).toBe('color'); expect(portKindOf({ key: 'pic', label: 'pic', kind: 'texture', default: '' })).toBe('texture'); expect(portKindOf({ key: 'align', label: 'align', kind: 'enum', default: 'left', options: [] })).toBe( 'text', ); expect(declaredPortType('color')).toBe('number'); expect(declaredPortType('texture')).toBe('string'); }); it('carries the field that edits it, so a typed-in value gets the picker', () => { const pic = withInlineDefault(portDefOf({ key: 'pic', kind: 'texture' })); expect(pic).toMatchObject({ type: 'string', default: '' }); expect(inlineInputDef(pic).kind).toBe('texture'); const tint = withInlineDefault(portDefOf({ key: 'tint', kind: 'color' })); expect(tint).toMatchObject({ type: 'number', default: 0 }); expect(inlineInputDef(tint).kind).toBe('color'); expect(portDefOf({ key: 'n', kind: 'number' }).field).toBeUndefined(); }); /** A `color` port declared as a number still names the property: colour travels as a number. */ it('names a property when it travels the way the property does', () => { expect(isPortOf({ key: 'color', kind: 'color' }, COLOR)).toBe(true); expect(isPortOf({ key: 'color', kind: 'number' }, COLOR)).toBe(true); expect(isPortOf({ key: 'color', kind: 'text' }, COLOR)).toBe(false); expect(propertyPort({ key: 'color', kind: 'color' }, SPRITE).field?.kind).toBe('color'); }); }); describe('the literal nodes', () => { /** The palette and the kind picker say the same word: a text is "Text", nowhere "String". */ it('are labelled by the kind they hand out', () => { const label = (type: string) => BUILT_IN_NODES.find((d) => d.type === type)?.label; expect(['Number', 'Boolean', 'String', 'Texture', 'StateMachine', 'List'].map(label)).toEqual([ 'Number', 'Boolean', 'Text', 'Texture', 'State machine', 'List', ]); }); }); describe('the whole-number inputs', () => { const input = (type: string, key: string) => BUILT_IN_NODES.find((d) => d.type === type)!.inputs.find((p) => p.key === key)!; it('show no decimals, since the node floors what arrives', () => { const whole = [ ['ItemAt', 'index'], ['SetItem', 'index'], ['SubList', 'start'], ['SubList', 'count'], ['Fill', 'count'], ['Repeat', 'times'], ['Substring', 'start'], ['Substring', 'count'], ['ToText', 'decimals'], ] as const; for (const [type, key] of whole) { const def = inlineInputDef(input(type, key)); expect(def.kind === 'number' && def.decimals, `${type}.${key}`).toBe(0); } }); it('keep their default on the port and in the field alike', () => { for (const [type, key, value] of [ ['SubList', 'count', 1], ['Fill', 'count', 1], ['Repeat', 'times', 1], ['Substring', 'count', -1], ['ItemAt', 'index', 0], ] as const) { const port = input(type, key); expect([port.default, inlineInputDef(port).default], `${type}.${key}`).toEqual([value, value]); } }); });