Files
feuerwerk/tests/core/references.test.ts
T
adminandClaude Opus 5.5 2e4c0a09ea
feuerwerk / gate (push) Successful in 4m58s
feuerwerk / suite (push) Failing after 19m37s
Open clips and frames from the asset panel by double click (#430)
A double click on a clip row did not open its tab. The first click of
the double click had already selected the clip for the inspector, so
the second click selected the same thing again and the editor saw no
change. Frames of textures and pixel art had no rows at all.

- A double click now asks for the tab with openSelection, even when
  the selection is already right.
- Textures with frames and pixel art with more than one frame get a
  chevron and one row per frame, like sounds and videos. Which rows an
  asset has is decided in one place, src/core/model/assetParts.ts,
  used by the panel and the inspector link.
- A texture frame shows in the inspector on click and opens the
  texture with the frame selected on double click.
- A pixel art frame selects the pixel art on click, so an open tab
  paints on that frame. A double click opens the tab on the frame.
- A frame can be dragged onto a composition track like a video clip.
  The item carries the sprite of the whole image, the frame becomes
  the texture of its object, and it is 3 s long. The agent can do the
  same with add_composition_item.
- A texture label that names a frame now points at the frame row.

Found on the way: the e2e check from #425 only double clicked the
texture label, where the first click is a ping, so the bug never
showed there.

Tests:
- Unit: asset-parts (new, 6), composition-sprites and references
  extended, related tests 303/303, agent tests 21/21
- 5 mutations, all red in the matching test
- e2e 69 (atx): ALL PASSED, 26 checks, 69t new
- Gate (atx): CHECK EXIT 0, 325 files, 5017 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:30:29 +02:00

149 lines
5.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.
/** Which asset a reference field points at – what "Open texture" and the inspector link open (#221). */
import { describe, expect, it } from 'vitest';
import {
CreateAssetCommand,
CreateSoundCommand,
CreateTextureCommand,
CreateVideoCommand,
} from '../../src/core/commands/assetCommands';
import { AddClipCommand } from '../../src/core/commands/clipCommands';
import { AddFrameCommand } from '../../src/core/commands/frameCommands';
import type { PixelArtAsset } from '../../src/core/model/Asset';
import { findAsset } from '../../src/core/model/Project';
import { type PropertyDef, schemaForAsset } from '../../src/core/properties/PropertySchema';
import { referencedAssetId, referencedTarget, referencesOf } from '../../src/core/properties/references';
import { resolveAssetProps } from '../../src/core/properties/resolve';
import { SHADER_KEY } from '../../src/core/properties/shaders';
import { makeSpriteScene } from '../helpers';
const FILE = `${'ab'.repeat(32)}.png`;
const TEXTURE: PropertyDef = { key: 'texture', label: 'Texture', kind: 'texture', default: '' };
const SHADER: PropertyDef = {
key: SHADER_KEY,
label: 'Shader',
kind: 'asset',
default: '',
assetTypes: ['shader'],
};
const SOUND: PropertyDef = { key: 'sound', label: 'Sound', kind: 'sound', default: '' };
function withClips() {
const f = setup();
const video = new CreateVideoCommand('Film', `${'cd'.repeat(32)}.mp4`, {
width: 320,
height: 180,
duration: 4,
fps: 30,
audio: false,
});
const sound = new CreateSoundCommand('Kick', `${'ef'.repeat(32)}.ogg`);
f.stack.execute(video);
f.stack.execute(sound);
const videoClip = new AddClipCommand(video.asset.id, { start: 1, end: 2 });
const soundClip = new AddClipCommand(sound.asset.id, { start: 0.1, end: 0.2 });
f.stack.execute(videoClip);
f.stack.execute(soundClip);
return {
...f,
videoId: video.asset.id,
soundId: sound.asset.id,
videoClip: videoClip.clipId,
soundClip: soundClip.clipId,
};
}
function setup() {
const fixture = makeSpriteScene();
const texture = new CreateTextureCommand('Sheet', { file: FILE, width: 64, height: 32 });
fixture.stack.execute(texture);
const frame = new AddFrameCommand(texture.asset.id, { x: 0, y: 0, w: 16, h: 16 }, 'Idle');
fixture.stack.execute(frame);
const shader = new CreateAssetCommand('shader', 'Glow');
fixture.stack.execute(shader);
const art = new CreateAssetCommand('pixelart', 'Art');
fixture.stack.execute(art);
return {
...fixture,
textureId: texture.asset.id,
frameId: frame.frameId,
shaderId: shader.asset.id,
art: art.asset as PixelArtAsset,
};
}
describe('referencedAssetId', () => {
it('is the asset a reference names', () => {
const f = setup();
expect(referencedAssetId(f.project, TEXTURE, f.textureId)).toBe(f.textureId);
expect(referencedAssetId(f.project, SHADER, f.shaderId)).toBe(f.shaderId);
});
it('is the sheet for a frame of a texture or of pixel art', () => {
const f = setup();
expect(referencedAssetId(f.project, TEXTURE, f.frameId)).toBe(f.textureId);
f.art.frames.push({ id: 'second', cels: {} });
expect(referencedAssetId(f.project, TEXTURE, 'second')).toBe(f.art.id);
});
it('is nothing for an empty or dangling reference', () => {
const f = setup();
expect(referencedAssetId(f.project, TEXTURE, '')).toBeNull();
expect(referencedAssetId(f.project, SHADER, 'gone')).toBeNull();
});
it('only resolves frames in a texture field', () => {
const f = setup();
expect(referencedAssetId(f.project, SHADER, f.frameId)).toBeNull();
});
it('is nothing for a field that names no asset, whatever it holds', () => {
const f = setup();
const text: PropertyDef = { key: 'name', label: 'Name', kind: 'text', default: '' };
expect(referencedAssetId(f.project, text, f.textureId)).toBeNull();
});
it('is the video for a clip of one in a texture field (#425)', () => {
const f = withClips();
expect(referencedAssetId(f.project, TEXTURE, f.videoClip)).toBe(f.videoId);
expect(referencedAssetId(f.project, SOUND, f.soundClip)).toBe(f.soundId);
});
});
describe('referencedTarget (#425, #430)', () => {
it('is the clip itself for a clip of a video or a sound, the asset for anything else', () => {
const f = withClips();
expect(referencedTarget(f.project, TEXTURE, f.videoClip)).toBe(f.videoClip);
expect(referencedTarget(f.project, SOUND, f.soundClip)).toBe(f.soundClip);
expect(referencedTarget(f.project, TEXTURE, f.videoId)).toBe(f.videoId);
expect(referencedTarget(f.project, SHADER, f.videoClip)).toBeNull();
});
it('is the frame itself for a frame of a texture, and for one of pixel art with more than one', () => {
const f = setup();
expect(referencedTarget(f.project, TEXTURE, f.frameId)).toBe(f.frameId);
const only = f.art.frames[0]!.id;
expect(referencedTarget(f.project, TEXTURE, only)).toBe(f.art.id);
f.art.frames.push({ id: 'second', cels: {} });
expect(referencedTarget(f.project, TEXTURE, 'second')).toBe('second');
expect(referencedTarget(f.project, TEXTURE, only)).toBe(only);
});
});
describe('referencesOf', () => {
it("lists a sprite asset's texture and shader in schema order, and nothing unset", () => {
const f = setup();
const sprite = findAsset(f.project, f.spriteId)!;
const refs = () =>
referencesOf(f.project, schemaForAsset(f.project, sprite), resolveAssetProps(f.project, sprite));
sprite.props.texture = '';
expect(refs()).toEqual([]);
sprite.props[SHADER_KEY] = f.shaderId;
sprite.props.texture = f.frameId;
expect(refs()).toEqual([
{ label: 'Texture', assetId: f.textureId },
{ label: 'Shader', assetId: f.shaderId },
]);
});
});