Files
feuerwerk/tests/core/commands.test.ts
T
rootandClaude Opus 5 5a6af74e44 Add textures for sprites
Images are uploaded to a content-addressed blob endpoint and stored as
data/textures/<sha256>.<ext>; the project only keeps the file name. The
sprite points at a texture asset through an ordinary property of the new
kind `asset`, so instance overrides, "Apply to container", "Reset to
asset", undo and `Set property` work without any extra code. The new kind
also replaces the ad-hoc asset picker the graph nodes faked as an enum.

Import goes through + -> Texture, drag & drop onto the canvas or Ctrl+V.
Images wider than 2048px are downscaled client-side and re-encoded to
WebP if needed, because nginx caps uploads at 1MB; an image that already
fits is uploaded byte for byte so pixel art is never resampled.

The sprite colour default changes from red to white, since colour tints
the texture multiplicatively and a tint of white shows an image as it is.

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

150 lines
5.4 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
CreateAssetCommand,
DeleteAssetCommand,
RenameAssetCommand,
ResetAssetPropertyCommand,
SetActiveSceneCommand,
SetAssetPropertyCommand,
} from '../../src/core/commands/assetCommands';
import {
ClearOverrideCommand,
MoveSceneObjectCommand,
PlaceAssetCommand,
RemoveSceneObjectCommand,
SetOverrideCommand,
} from '../../src/core/commands/sceneObjectCommands';
import { findAsset } from '../../src/core/model/Project';
import { makeSpriteScene, place } from '../helpers';
describe('asset commands', () => {
it('creates assets with defaults and activates the first scene', () => {
const { project, sceneId, spriteId } = makeSpriteScene();
expect(project.assets).toHaveLength(2);
expect(project.activeSceneId).toBe(sceneId);
expect(findAsset(project, spriteId)?.props).toEqual({
enabled: true,
width: 100,
height: 100,
color: 0xffffff,
texture: '',
});
});
it('renames and sets properties with undo/redo', () => {
const { project, stack, spriteId } = makeSpriteScene();
const sprite = () => findAsset(project, spriteId)!;
stack.execute(new RenameAssetCommand(spriteId, 'Crate'));
stack.execute(new SetAssetPropertyCommand(spriteId, 'width', 250));
expect(sprite().name).toBe('Crate');
expect(sprite().props.width).toBe(250);
stack.undo();
expect(sprite().props.width).toBe(100);
stack.undo();
expect(sprite().name).toBe('Box');
stack.redo();
stack.redo();
expect(sprite().name).toBe('Crate');
expect(sprite().props.width).toBe(250);
});
it('rejects unknown properties', () => {
const { stack, spriteId } = makeSpriteScene();
expect(() => stack.execute(new SetAssetPropertyCommand(spriteId, 'x', 5))).toThrow(/not defined/);
expect(() => stack.execute(new ResetAssetPropertyCommand(spriteId, 'x'))).toThrow(/not defined/);
});
it('resets a property to its schema default with undo', () => {
const { project, stack, spriteId } = makeSpriteScene();
const sprite = () => findAsset(project, spriteId)!;
stack.execute(new SetAssetPropertyCommand(spriteId, 'height', 60));
stack.execute(new ResetAssetPropertyCommand(spriteId, 'height'));
expect(sprite().props.height).toBe(100);
stack.undo();
expect(sprite().props.height).toBe(60);
stack.redo();
expect(sprite().props.height).toBe(100);
});
it('deletes an asset together with its scene objects and restores both on undo', () => {
const f = makeSpriteScene();
const { project, stack, spriteId, sceneOf } = f;
place(f, spriteId, { x: 1, y: 2 });
place(f, spriteId, { x: 3, y: 4 });
expect(sceneOf().objects).toHaveLength(2);
stack.execute(new DeleteAssetCommand(spriteId));
expect(project.assets).toHaveLength(1);
expect(sceneOf().objects).toHaveLength(0);
stack.undo();
expect(project.assets).toHaveLength(2);
expect(sceneOf().objects.map((o) => o.position)).toEqual([
{ x: 1, y: 2, z: 0 },
{ x: 3, y: 4, z: 0 },
]);
});
it('switches the active scene when the active one is deleted', () => {
const { project, stack, sceneId } = makeSpriteScene();
const second = new CreateAssetCommand('scene', 'Level 2');
stack.execute(second);
stack.execute(new SetActiveSceneCommand(second.asset.id));
stack.execute(new DeleteAssetCommand(second.asset.id));
expect(project.activeSceneId).toBe(sceneId);
stack.undo();
expect(project.activeSceneId).toBe(second.asset.id);
});
});
describe('scene object commands', () => {
it('places assets and refuses scenes', () => {
const f = makeSpriteScene();
const { stack, sceneId, spriteId, sceneOf } = f;
const box = place(f, spriteId, { x: 5, y: 6 });
const object = sceneOf().objects[0]!;
expect(object.id).toBe(box);
expect(object.assetId).toBe(spriteId);
expect(object.position).toEqual({ x: 5, y: 6, z: 0 });
expect(object.overrides).toEqual({});
expect(() => stack.execute(new PlaceAssetCommand(sceneId, sceneId, { x: 0, y: 0 }))).toThrow(
/cannot be placed/,
);
expect(sceneOf().objects).toHaveLength(1);
});
it('moves, overrides, clears and removes with undo', () => {
const f = makeSpriteScene();
const { stack, sceneId, spriteId, sceneOf, object } = f;
const box = place(f, spriteId, { x: 0, y: 0 });
stack.execute(new MoveSceneObjectCommand(sceneId, box, { x: 40, y: 50, z: 3 }));
expect(object(box).position).toEqual({ x: 40, y: 50, z: 3 });
stack.undo();
expect(object(box).position).toEqual({ x: 0, y: 0, z: 0 });
stack.redo();
stack.execute(new SetOverrideCommand(sceneId, box, 'color', 0x123456));
expect(object(box).overrides).toEqual({ color: 0x123456 });
expect(() => stack.execute(new SetOverrideCommand(sceneId, box, 'nope', 1))).toThrow();
stack.execute(new ClearOverrideCommand(sceneId, box, 'color'));
expect(object(box).overrides).toEqual({});
stack.undo();
expect(object(box).overrides).toEqual({ color: 0x123456 });
stack.execute(new RemoveSceneObjectCommand(sceneId, box));
expect(sceneOf().objects).toHaveLength(0);
stack.undo();
expect(object(box).position).toEqual({ x: 40, y: 50, z: 3 });
});
it('clears the redo stack on a new command', () => {
const f = makeSpriteScene();
place(f, f.spriteId, { x: 0, y: 0 });
f.stack.undo();
expect(f.stack.canRedo).toBe(true);
place(f, f.spriteId, { x: 1, y: 1 });
expect(f.stack.canRedo).toBe(false);
});
});