Files
feuerwerk/tests/core/resolve.test.ts
T
rootandClaude Opus 5 2511483a67 Reset/apply buttons only for real deviations, collapsible hierarchy tree
- Inspector rows compute their reset/apply entries through providers, so an
  entry that would not change anything is dropped; a single remaining entry
  renders as a direct button instead of a menu, none removes it entirely.
- Bold and buttons now follow the same rule (deviatesFromAsset /
  deviatesFromTemplate in core/properties/resolve.ts).
- Hierarchy panel is a collapsible tree: chevrons, arrow keys, double click,
  expand/collapse all. Everything starts collapsed; selecting an object
  expands its ancestors and scrolls it into view.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 06:56:10 +02:00

112 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import { buildAsset } from '../../src/core/commands/assetCommands';
import { createSceneObject } from '../../src/core/model/SceneObject';
import { coerceValue, defaultsFor, schemaFor } from '../../src/core/properties/PropertySchema';
import {
deviatesFromAsset,
deviatesFromTemplate,
inheritedValue,
isModified,
isOverridden,
resolveAssetProps,
resolveSceneObjectProps,
} from '../../src/core/properties/resolve';
describe('PropertySchema', () => {
it('has no position on any asset type', () => {
for (const type of ['scene', 'camera', 'sprite'] as const) {
const keys = schemaFor(type).map((d) => d.key);
expect(keys).not.toContain('x');
expect(keys).not.toContain('y');
expect(keys).not.toContain('position');
}
});
it('provides defaults for every key', () => {
expect(defaultsFor('sprite')).toEqual({ enabled: true, width: 100, height: 100, color: 0xe0553f });
});
it('coerces values against the definition', () => {
const width = schemaFor('sprite').find((d) => d.key === 'width')!;
expect(coerceValue(width, -5)).toBe(1);
expect(coerceValue(width, '42')).toBe(42);
expect(coerceValue(width, 'abc')).toBe(100);
const color = schemaFor('sprite').find((d) => d.key === 'color')!;
expect(coerceValue(color, '#ff0000')).toBe(0xff0000);
expect(coerceValue(color, 0x1ffffff)).toBe(0xffffff);
});
});
describe('resolve', () => {
it('fills asset props with defaults and ignores unknown keys', () => {
const sprite = buildAsset('sprite', 'Box');
sprite.props = { width: 50, bogus: 1 };
expect(resolveAssetProps(sprite)).toEqual({ enabled: true, width: 50, height: 100, color: 0xe0553f });
});
it('applies sparse overrides on top of asset values', () => {
const sprite = buildAsset('sprite', 'Box');
sprite.props.color = 0x00ff00;
const object = createSceneObject('o1', sprite.id, { x: 10, y: 20 });
expect(resolveSceneObjectProps(sprite, object)).toEqual({ enabled: true, width: 100, height: 100, color: 0x00ff00 });
object.overrides.color = 0x0000ff;
expect(resolveSceneObjectProps(sprite, object).color).toBe(0x0000ff);
expect(isOverridden(object, 'color')).toBe(true);
expect(isOverridden(object, 'width')).toBe(false);
expect(inheritedValue(sprite, 'color')).toBe(0x00ff00);
});
it('detects asset values that differ from the schema default', () => {
const sprite = buildAsset('sprite', 'Box');
expect(isModified(sprite, 'width')).toBe(false); // explicit default counts as unmodified
sprite.props.width = 250;
expect(isModified(sprite, 'width')).toBe(true);
delete sprite.props.height; // missing → resolved from default → unmodified
expect(isModified(sprite, 'height')).toBe(false);
expect(isModified(sprite, 'bogus')).toBe(false);
});
it('ignores an override that carries the asset value – nothing to reset or apply', () => {
const sprite = buildAsset('sprite', 'Box');
sprite.props.width = 250;
const object = createSceneObject('o1', sprite.id, { x: 0, y: 0 });
expect(deviatesFromAsset(object, sprite, 'width')).toBe(false); // no override at all
object.overrides.width = 250; // same value as the asset → not a deviation
expect(isOverridden(object, 'width')).toBe(true);
expect(deviatesFromAsset(object, sprite, 'width')).toBe(false);
object.overrides.width = 300;
expect(deviatesFromAsset(object, sprite, 'width')).toBe(true);
// The asset moving to the object's value settles the difference again.
sprite.props.width = 300;
expect(deviatesFromAsset(object, sprite, 'width')).toBe(false);
});
it('compares an instance against its template as overrides, not as values', () => {
const sprite = buildAsset('sprite', 'Box');
const template = createSceneObject('t1', sprite.id, { x: 0, y: 0 });
const object = createSceneObject('o1', sprite.id, { x: 0, y: 0 });
expect(deviatesFromTemplate(object, template, 'width')).toBe(false); // neither overrides
template.overrides.width = 140;
expect(deviatesFromTemplate(object, template, 'width')).toBe(true);
object.overrides.width = 140;
expect(deviatesFromTemplate(object, template, 'width')).toBe(false);
object.overrides.width = 77;
expect(deviatesFromTemplate(object, template, 'width')).toBe(true);
});
it('keeps position on the scene object only', () => {
const sprite = buildAsset('sprite', 'Box');
const object = createSceneObject('o1', sprite.id, { x: 10, y: 20 });
expect(object.position).toEqual({ x: 10, y: 20, z: 0 });
expect('position' in sprite).toBe(false);
expect(resolveSceneObjectProps(sprite, object)).not.toHaveProperty('x');
});
});