A kind like colour or texture used to be spelled out in five parallel lists, its port type, its empty value, its glyph, its label and its list tag - 22 literal places for texture alone. core/properties/kinds.ts now holds one row per kind and the rest is derived from it, so adding a kind is one row. The five literal nodes collapse into literals.ts, the six kind menus into one helper. Number, Text, Boolean is now the one order everywhere; the variable and port menus used to list Boolean before Text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
152 lines
6.1 KiB
TypeScript
152 lines
6.1 KiB
TypeScript
/**
|
||
* 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 file from before the kind existed declares a sprite's `color` port as a number; it still is one. */
|
||
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', 'List'].map(label)).toEqual([
|
||
'Number',
|
||
'Boolean',
|
||
'Text',
|
||
'Texture',
|
||
'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]);
|
||
}
|
||
});
|
||
});
|