Files
feuerwerk/tests/core/asset-ref.test.ts
T
rootandClaude Opus 5 8f92a06c84 Consolidate duplicated editor code, trim comments, refresh README
Split GraphCanvas (1076 -> 831 lines) into EdgeLayer and graphMenus;
the port and marker menus were the same list and are now one function.
Value specs of the inspector and the list tab share valueSpecs.ts, and
the remaining copies (compound commands, type glyphs, typed asset
lookup, name rule, play/editor world setup, marquee) are merged.

Four copies had already drifted apart; merging them fixes:
- "Disconnect N links" in the port menu is one undo step, not N
- "+ Add collider" is hidden on a placed container instance, as in
  the hierarchy menu, instead of always failing
- a bool property coerced from the text "false" becomes false
- a port command on an unknown node type reports instead of crashing

Comment-only edits leave the bundle byte-identical. README corrected in
35 places (step limit, jump-run clock, suite duration, 409 body, ...).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 11:00:30 +02:00

134 lines
4.8 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 params', () => {
// An asset param 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.params).filter((p) => p.key === 'asset');
expect(params.length).toBeGreaterThan(0);
for (const param of params) {
expect(param.kind).toBe('asset');
expect(param.kind === 'asset' && param.assetTypes).toBeUndefined();
}
});
});