Files
feuerwerk/tests/core/asset-ref.test.ts
T
adminandClaude Opus 5.5 923baf2557 Let Spawn take an asset over an edge and find a child by name (#237)
An asset could only be picked as a setting on the Spawn node. It could
not be wired in, held in a variable or put into a list. Finding an
object below the own one needed a scene-wide search by name.

- "Asset" is now a kind like Animation or Sound: port, variable, list
  item and a new "Asset" literal node, with its own cube icon.
- Spawn and Is asset have an `asset` input. It uses the key of the old
  setting, so a saved asset reads as the value of the input and older
  projects keep spawning without a migration.
- New node "Child by name" searches below an object level by level.
  A child wins over a grandchild of the same name. Nothing found or no
  object gives null.

Tests:
- ./check.sh: 168 files, 2453 tests, CHECK EXIT 0.
- 9 new unit tests in tests/core/asset-kind.test.ts, catalog cases for
  Spawn, Is asset, Child by name and the literal.
- e2e 6z-ii (2 checks): node rows and a real drag of the literal onto
  `asset`. e2e 6: 119 checks passed. Spawn checks in 3 and 9 passed.
- Counter-checks: depth-first search fails 1, Spawn reading only the
  old setting fails 3, runtime ignoring the parent fails 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:36:49 +02:00

136 lines
4.9 KiB
TypeScript

/**
* The two pure halves of the `asset` property kind: how a stored id reads, and which assets a
* picker may offer for it. Used by the inspector row and the graph node view alike.
*/
import { describe, expect, it } from 'vitest';
import { CreateAssetCommand } from '../../src/core/commands/assetCommands';
import { BUILT_IN_NODES } from '../../src/core/graph/nodes';
import type { AssetType } from '../../src/core/model/AssetTypes';
import type { Project } from '../../src/core/model/Project';
import { assetChoiceLabel, assetRefChoices, assetRefLabel } from '../../src/core/properties/assetRef';
import { makeProject, type ProjectFixture } from '../helpers';
function add(fixture: ProjectFixture, type: AssetType, name: string): string {
const command = new CreateAssetCommand(type, name);
fixture.stack.execute(command);
return command.asset.id;
}
/** A project holding one asset of every type, each named after its type. */
function oneOfEach(): { project: Project; ids: Record<AssetType, string> } {
const fixture = makeProject();
const types: AssetType[] = [
'scene',
'camera',
'sprite',
'text',
'behavior',
'collider',
'container',
'texture',
];
const ids = Object.fromEntries(types.map((type) => [type, add(fixture, type, type)])) as Record<
AssetType,
string
>;
return { project: fixture.project, ids };
}
describe('assetRefLabel', () => {
it('reads as the referenced asset name', () => {
const fixture = makeProject();
const id = add(fixture, 'sprite', 'Coin');
expect(assetRefLabel(fixture.project, id)).toBe('Coin');
});
it('reads as "None" when nothing is referenced', () => {
expect(assetRefLabel(makeProject().project, '')).toBe('None');
});
it('keeps showing the id of a reference that no longer resolves', () => {
expect(assetRefLabel(makeProject().project, 'gone')).toBe('gone?');
});
it('marks a deleted asset as dangling rather than falling back to "None"', () => {
const fixture = makeProject();
const id = add(fixture, 'sprite', 'Coin');
fixture.stack.undo();
expect(assetRefLabel(fixture.project, id)).toBe(`${id}?`);
});
it('dangles without a project, but the empty reference is still "None"', () => {
expect(assetRefLabel(null, 'x')).toBe('x?');
expect(assetRefLabel(null, '')).toBe('None');
});
});
describe('assetRefChoices', () => {
it('offers every placeable asset when no types are given', () => {
const { project } = oneOfEach();
expect(assetRefChoices(project).map((a) => a.type)).toEqual([
'behavior',
'camera',
'collider',
'container',
'sprite',
'text',
]);
});
it('never offers a scene or a texture as a placeable asset', () => {
const { project } = oneOfEach();
const types = assetRefChoices(project).map((a) => a.type);
expect(types).not.toContain('scene');
expect(types).not.toContain('texture');
});
it('offers exactly the given types, placeable or not', () => {
const { project, ids } = oneOfEach();
expect(assetRefChoices(project, ['texture']).map((a) => a.id)).toEqual([ids.texture]);
expect(assetRefChoices(project, ['scene', 'sprite']).map((a) => a.id)).toEqual([ids.scene, ids.sprite]);
});
it('offers nothing when no asset has one of the given types', () => {
const fixture = makeProject();
add(fixture, 'sprite', 'Coin');
expect(assetRefChoices(fixture.project, ['texture'])).toEqual([]);
});
it('sorts by name, not by creation order', () => {
const fixture = makeProject();
add(fixture, 'sprite', 'Zebra');
add(fixture, 'sprite', 'Apple');
add(fixture, 'text', 'Mango');
expect(assetRefChoices(fixture.project).map((a) => a.name)).toEqual(['Apple', 'Mango', 'Zebra']);
});
it('sorts namesakes by type so the order does not depend on creation order', () => {
const fixture = makeProject();
add(fixture, 'text', 'Coin');
add(fixture, 'sprite', 'Coin');
expect(assetRefChoices(fixture.project).map(assetChoiceLabel)).toEqual(['Coin (sprite)', 'Coin (text)']);
});
it('leaves the asset order of the project alone', () => {
const fixture = makeProject();
add(fixture, 'sprite', 'Zebra');
add(fixture, 'sprite', 'Apple');
assetRefChoices(fixture.project);
expect(fixture.project.assets.map((a) => a.name)).toEqual(['Zebra', 'Apple']);
});
});
describe('node asset inputs', () => {
// An asset input's field uses the asset kind, never an enum with empty options filled in by the inspector.
it('use the asset kind and offer every placeable asset', () => {
const params = BUILT_IN_NODES.flatMap((def) => def.inputs)
.filter((p) => p.key === 'asset')
.map((p) => p.field!);
expect(params.map((p) => p.key)).toEqual(['asset', 'asset']);
for (const param of params) {
expect(param.kind).toBe('asset');
expect(param.kind === 'asset' && param.assetTypes).toBeUndefined();
}
});
});