/** * Which scene Play starts and the game view scale (#226): both live in the project now, not in the * browser. The start-scene switch is an undo step like every other project setting; the scale is * saved but never lands on the undo stack. */ import { describe, expect, it } from 'vitest'; import { SetGameLinkZoomCommand, SetPlayFromCommand } from '../../src/core/commands/projectCommands'; import { gameLinkZoomOf, gameScaleOf, playFromOf } from '../../src/core/model/playSettings'; import { createEmptyProject } from '../../src/core/model/Project'; import { deserializeProject, serializeProject } from '../../src/core/store/serialize'; import { EditorState } from '../../src/editor/state'; import { cameraView } from '../../src/viewport/view'; function loaded() { const state = new EditorState(); state.loadProject(createEmptyProject('p', 'P'), 1); return state; } describe('play settings in the project', () => { it('start from the start scene at 100% in a new project and in one written before there were any', () => { const fresh = createEmptyProject('p', 'P'); const old = deserializeProject({ id: 'p', name: 'P', assets: [] }); expect([fresh.playFrom, fresh.gameScale]).toEqual(['start', 1]); expect([old.playFrom, old.gameScale]).toEqual(['start', 1]); }); it('survive a save and a load', () => { const project = { ...createEmptyProject('p', 'P'), playFrom: 'active' as const, gameScale: 'auto' as const, }; const back = deserializeProject(serializeProject(project)); expect([back.playFrom, back.gameScale]).toEqual(['active', 'auto']); }); it('read untrusted JSON as the defaults unless it is one of the choices', () => { expect(playFromOf({ playFrom: 'active' })).toBe('active'); expect(playFromOf({ playFrom: 'elsewhere' })).toBe('start'); expect(playFromOf(null)).toBe('start'); expect(gameScaleOf({ gameScale: 'fit' })).toBe('fit'); expect(gameScaleOf({ gameScale: 0.25 })).toBe(0.25); expect(gameScaleOf({ gameScale: 3 })).toBe(3); expect(gameScaleOf({ gameScale: 20 })).toBe(20); expect(gameScaleOf({ gameScale: 2.5 })).toBe(1); expect(gameScaleOf({ gameScale: 21 })).toBe(1); expect(gameScaleOf({ gameScale: '2' })).toBe(1); }); it('make the start-scene switch one undo step', () => { const state = loaded(); state.execute(new SetPlayFromCommand('active')); expect(state.playFrom).toBe('active'); expect(state.saveStatus).toBe('dirty'); state.undo(); expect(state.playFrom).toBe('start'); }); it('save the game view scale without an undo step', () => { const state = loaded(); state.setGameScale(4); expect(state.project.gameScale).toBe(4); expect(state.saveStatus).toBe('dirty'); expect(state.commands.canUndo).toBe(false); }); it('keep a scale picked while playing after Stop', () => { const state = loaded(); state.enterPlay(); state.setGameScale('fit'); expect(state.gameScale).toBe('fit'); state.exitPlay(); expect(state.gameScale).toBe('fit'); expect(state.saveStatus).toBe('dirty'); }); }); describe('the zoom of the game link (#279)', () => { it('is Auto in a new project and in one written before there was a choice', () => { expect(createEmptyProject('p', 'P').gameLinkZoom).toBe('auto'); expect(deserializeProject({ id: 'p', name: 'P', assets: [] }).gameLinkZoom).toBe('auto'); }); it('survives a save and a load, apart from the scale of the game view', () => { const project = { ...createEmptyProject('p', 'P'), gameLinkZoom: 'fit' as const, gameScale: 2 }; const back = deserializeProject(serializeProject(project)); expect([back.gameLinkZoom, back.gameScale]).toEqual(['fit', 2]); }); it('reads untrusted JSON as Auto unless it is one of the two choices', () => { expect(gameLinkZoomOf({ gameLinkZoom: 'fit' })).toBe('fit'); expect(gameLinkZoomOf({ gameLinkZoom: 'auto' })).toBe('auto'); expect(gameLinkZoomOf({ gameLinkZoom: 2 })).toBe('auto'); expect(gameLinkZoomOf({ gameLinkZoom: 'Fit' })).toBe('auto'); expect(gameLinkZoomOf({ gameScale: 'fit' })).toBe('auto'); expect(gameLinkZoomOf(null)).toBe('auto'); }); it('is one undo step', () => { const state = loaded(); state.execute(new SetGameLinkZoomCommand('fit')); expect(state.project.gameLinkZoom).toBe('fit'); expect(state.saveStatus).toBe('dirty'); state.undo(); expect(state.project.gameLinkZoom).toBe('auto'); state.redo(); expect(state.project.gameLinkZoom).toBe('fit'); }); it('leaves the scale of the game view alone', () => { const state = loaded(); state.setGameScale(3); state.execute(new SetGameLinkZoomCommand('fit')); expect(state.gameScale).toBe(3); }); it('fills the window at Fit where Auto stops at the step below', () => { const camera = { x: 0, y: 0, props: { width: 300, height: 180, zoom: 1 } }; const auto = cameraView(camera, 1000, 700, 'auto'); const fit = cameraView(camera, 1000, 700, 'fit'); expect(auto.scale).toBe(3); expect(auto.rect).toEqual({ x: 50, y: 80, w: 900, h: 540 }); expect(fit.scale).toBeCloseTo(10 / 3, 9); expect(fit.rect.x).toBeCloseTo(0, 9); expect(fit.rect.w).toBeCloseTo(1000, 9); expect(fit.rect.h).toBeCloseTo(600, 9); expect(fit.rect.y).toBeCloseTo(50, 9); }); it('shrinks the picture at Fit in a window smaller than the camera', () => { const camera = { x: 0, y: 0, props: { width: 300, height: 180, zoom: 1 } }; const fit = cameraView(camera, 200, 400, 'fit'); expect(fit.scale).toBeCloseTo(2 / 3, 9); expect(fit.rect.w).toBeCloseTo(200, 9); expect(fit.rect.h).toBeCloseTo(120, 9); }); });