The fill of a sliced frame defaulted to `Stretch`. It is `Repeat` now, and the Fill dropdown lists `Repeat` first. The saved document keeps writing only the fill that differs from the default, so the key it carries is now `stretch`, and a frame without it repeats. A frame saved before with the key absent and meaning stretch would therefore tile; none of the stored projects has one - both sliced frames on the server carry `fill: "repeat"` explicitly, so they draw as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
172 lines
6.3 KiB
TypeScript
172 lines
6.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import {
|
||
CreateAssetCommand,
|
||
SetAssetPropertyCommand,
|
||
SetMachineScopeCommand,
|
||
} from '../../src/core/commands/assetCommands';
|
||
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||
import { AddVariableCommand } from '../../src/core/commands/variableCommands';
|
||
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
||
import { makeBehavior, makeSpriteScene, place, roundTrip } from '../helpers';
|
||
|
||
describe('serialize', () => {
|
||
it('round-trips a project', () => {
|
||
const f = makeSpriteScene();
|
||
const box = place(f, f.spriteId, { x: 7, y: 8 });
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, box, 'width', 33));
|
||
expect(roundTrip(f.project)).toEqual(f.project);
|
||
});
|
||
|
||
/** A behavior's own rows are not the common ones: coercing against those alone dropped the value. */
|
||
it('keeps a behavior running in the editor across a save', () => {
|
||
const f = makeSpriteScene();
|
||
const behavior = makeBehavior(f, 'B');
|
||
f.stack.execute(new SetAssetPropertyCommand(behavior.id, 'runInEditor', true));
|
||
const loaded = roundTrip(f.project).assets.find((a) => a.id === behavior.id)!;
|
||
expect(loaded.props.runInEditor).toBe(true);
|
||
});
|
||
|
||
/** The declarations of a Variables asset and their starting values are the whole asset. */
|
||
it('round-trips a Variables asset and the scope of a state machine', () => {
|
||
const f = makeSpriteScene();
|
||
const shared = new CreateAssetCommand('variables', 'Score');
|
||
f.stack.execute(shared);
|
||
f.stack.execute(new AddVariableCommand(shared.asset.id, 'score', 'number'));
|
||
f.stack.execute(new SetAssetPropertyCommand(shared.asset.id, 'score', 7));
|
||
const machine = new CreateAssetCommand('statemachine', 'M');
|
||
f.stack.execute(machine);
|
||
f.stack.execute(new SetMachineScopeCommand(machine.asset.id, 'global'));
|
||
expect(roundTrip(f.project)).toEqual(f.project);
|
||
});
|
||
|
||
/** `local` is the absence of the field, so a project made before scopes existed saves back byte-alike. */
|
||
it('writes no scope for a local machine and reads an unknown one as local', () => {
|
||
const f = makeSpriteScene();
|
||
makeBehavior(f, 'B', ['score']);
|
||
f.stack.execute(new CreateAssetCommand('statemachine', 'M'));
|
||
expect(JSON.stringify(serializeProject(f.project))).not.toContain('scope');
|
||
|
||
const loaded = deserializeProject({
|
||
assets: [{ id: 'm1', type: 'statemachine', name: 'M', scope: 'planet' }],
|
||
});
|
||
const machine = loaded.assets[0]!;
|
||
if (machine.type !== 'statemachine') throw new Error('wrong types');
|
||
expect(machine.scope).toBeUndefined();
|
||
});
|
||
|
||
/** #41: the borders are part of the frame, and a frame nobody sliced still saves without them. */
|
||
it('round-trips the nine-slice of a frame and repairs an impossible one', () => {
|
||
const file = `${'ab'.repeat(32)}.png`;
|
||
const loaded = deserializeProject({
|
||
assets: [
|
||
{
|
||
id: 't1',
|
||
type: 'texture',
|
||
name: 'Sheet',
|
||
file,
|
||
width: 64,
|
||
height: 64,
|
||
frames: [
|
||
{
|
||
id: 'f1',
|
||
name: 'Button',
|
||
x: 0,
|
||
y: 0,
|
||
w: 24,
|
||
h: 24,
|
||
border: { left: 8, right: 8 },
|
||
fill: 'stretch',
|
||
},
|
||
{ id: 'f2', name: 'Plain', x: 24, y: 0, w: 24, h: 24 },
|
||
{
|
||
id: 'f3',
|
||
name: 'Greedy',
|
||
x: 0,
|
||
y: 24,
|
||
w: 8,
|
||
h: 8,
|
||
border: { top: 99, bottom: 99 },
|
||
fill: 'wat',
|
||
},
|
||
],
|
||
},
|
||
],
|
||
});
|
||
const texture = loaded.assets[0]!;
|
||
if (texture.type !== 'texture') throw new Error('wrong types');
|
||
const [sliced, plain, greedy] = texture.frames;
|
||
expect(sliced?.border).toEqual({ left: 8, top: 0, right: 8, bottom: 0 });
|
||
// Only the fill that is not the default is written; an unknown one reads as the default.
|
||
expect(sliced?.fill).toBe('stretch');
|
||
expect(plain?.border).toBeUndefined();
|
||
expect(plain?.fill).toBeUndefined();
|
||
// Measured against the frame, not the image, and never leaving it without a middle.
|
||
expect(greedy?.border).toEqual({ left: 0, top: 4, right: 0, bottom: 3 });
|
||
expect(greedy?.fill).toBeUndefined();
|
||
expect(roundTrip(loaded)).toEqual(loaded);
|
||
});
|
||
|
||
it('repairs broken documents', () => {
|
||
const project = deserializeProject(
|
||
{
|
||
name: 'Broken',
|
||
activeSceneId: 'missing',
|
||
assets: [
|
||
{
|
||
id: 's1',
|
||
type: 'scene',
|
||
name: 'Scene',
|
||
props: { backgroundColor: '#000000', junk: 1 },
|
||
objects: [
|
||
{
|
||
id: 'o1',
|
||
assetId: 'sp1',
|
||
position: { x: '5', y: null },
|
||
overrides: { color: '#00ff00', width: -3, foo: 1 },
|
||
},
|
||
{ id: 'o2', assetId: 'nope', position: {} },
|
||
{ id: 'o3', assetId: 's1', position: {} },
|
||
],
|
||
},
|
||
{ id: 'sp1', type: 'sprite', props: { width: 10 } },
|
||
// An asset type this build does not know (a newer document, a renamed type). It used to
|
||
// be `sound`, which this build does know since #45 – hence a type nothing has claimed.
|
||
{ id: 'x', type: 'video' },
|
||
{ type: 'camera' },
|
||
],
|
||
},
|
||
'fallback-id',
|
||
);
|
||
expect(project.id).toBe('fallback-id');
|
||
expect(project.activeSceneId).toBe('s1');
|
||
expect(project.assets.map((a) => a.id)).toEqual(['s1', 'sp1']);
|
||
const scene = project.assets[0]!;
|
||
expect(scene.type === 'scene' && scene.props).toEqual({ backgroundColor: 0 });
|
||
const objects = scene.type === 'scene' ? scene.objects : [];
|
||
expect(objects).toHaveLength(1);
|
||
expect(objects[0]).toEqual({
|
||
id: 'o1',
|
||
assetId: 'sp1',
|
||
name: '',
|
||
position: { x: 5, y: 0, z: 0 },
|
||
editorOffset: { x: 0, y: 0 },
|
||
overrides: { color: 0x00ff00, width: 1 },
|
||
parentId: '',
|
||
instanceEdits: {},
|
||
});
|
||
expect(project.assets[1]!.name).toBe('sprite');
|
||
expect(project.assets[1]!.props).toEqual({
|
||
enabled: true,
|
||
scale: 1,
|
||
rotation: 0,
|
||
mirrorX: false,
|
||
mirrorY: false,
|
||
width: 10,
|
||
height: 100,
|
||
color: 0xffffff,
|
||
texture: '',
|
||
shader: '',
|
||
});
|
||
});
|
||
});
|