import type { Texture } from 'pixi.js'; import { afterEach, describe, expect, it } from 'vitest'; import { CreateAssetCommand } from '../../src/core/commands/assetCommands'; import { registerPictureSource } from '../../src/core/model/generated'; import { GeneratedPictures, projectPictureFactory, registerPictureGenerator, } from '../../src/viewport/generatedPictures'; import { makeSpriteScene } from '../helpers'; /** The pictures are plain strings here: what is measured is who is kept and who is let go. */ function library() { const freed: string[] = []; const drawn: string[] = []; const pictures = new GeneratedPictures((picture) => freed.push(picture)); let answer: (assetId: string) => string | null = (assetId) => assetId; pictures.setFactory((assetId) => { drawn.push(assetId); return answer(assetId); }); return { pictures, freed, drawn, answerWith: (fn: (assetId: string) => string | null) => { answer = fn; }, }; } describe('generated pictures kept in memory', () => { it('draws a key once and hands the same picture out again', () => { const f = library(); f.answerWith(() => 'first'); expect(f.pictures.get('gen:a@1')).toBe('first'); expect(f.pictures.get('gen:a@1')).toBe('first'); expect(f.drawn).toEqual(['a']); expect(f.freed).toEqual([]); }); it('lets the old revision go the moment the new one is drawn', () => { const f = library(); f.answerWith((id) => `${id}-one`); f.pictures.get('gen:a@1'); f.answerWith((id) => `${id}-two`); expect(f.pictures.get('gen:a@2')).toBe('a-two'); expect(f.freed).toEqual(['a-one']); expect(f.pictures.keys()).toEqual(['gen:a@2']); // Only that asset's own older picture goes; a neighbour keeps hers. f.answerWith((id) => `${id}-b`); f.pictures.get('gen:b@1'); f.pictures.get('gen:a@3'); expect(f.freed).toEqual(['a-one', 'a-two']); expect(f.pictures.keys()).toEqual(['gen:b@1', 'gen:a@3']); }); it('asks again for a picture that could not be drawn yet', () => { const f = library(); f.answerWith(() => null); expect(f.pictures.get('gen:a@1')).toBeNull(); expect(f.pictures.peek('gen:a@1')).toBeNull(); f.answerWith(() => 'ready'); expect(f.pictures.get('gen:a@1')).toBe('ready'); expect(f.drawn).toEqual(['a', 'a']); }); it('draws nothing for a name that is not a key', () => { const f = library(); expect(f.pictures.get(`${'ab'.repeat(32)}.png`)).toBeNull(); expect(f.pictures.get('gen:a')).toBeNull(); expect(f.drawn).toEqual([]); }); it('gives everything back when another session takes over', () => { const f = library(); f.pictures.get('gen:a@1'); f.pictures.get('gen:b@1'); f.pictures.setFactory(null); expect(f.freed).toEqual(['a', 'b']); expect(f.pictures.keys()).toEqual([]); expect(f.pictures.get('gen:a@1')).toBeNull(); }); it('answers peek only with what has been drawn', () => { const f = library(); expect(f.pictures.peek('gen:a@1')).toBeNull(); f.pictures.get('gen:a@1'); expect(f.pictures.peek('gen:a@1')).toBe('a'); expect(f.drawn).toEqual(['a']); }); }); describe('the factory the editor runs with', () => { const registered: (() => void)[] = []; afterEach(() => { for (const undo of registered.splice(0)) undo(); }); it('hands an asset to the generator of its type and nothing else to anyone', () => { const f = makeSpriteScene(); const sound = new CreateAssetCommand('sound', 'Beep'); f.stack.execute(sound); registered.push(registerPictureSource('sound', () => ({ width: 8, height: 8, revision: 1 }))); const seen: string[] = []; const picture = {} as Texture; registered.push( registerPictureGenerator('sound', (asset) => { seen.push(asset.name); return picture; }), ); const factory = projectPictureFactory(() => f.project); expect(factory(sound.asset.id)).toBe(picture); expect(seen).toEqual(['Beep']); // A sprite has no generator, and an id that is gone has no asset. expect(factory(f.spriteId)).toBeNull(); expect(factory('nobody')).toBeNull(); expect(seen).toEqual(['Beep']); }); it('draws nothing while no project is open', () => { const factory = projectPictureFactory(() => null); expect(factory('a')).toBeNull(); }); });