diff --git a/src/devtools/tests.ts b/src/devtools/tests.ts index d62d9ee..2d8dc01 100644 --- a/src/devtools/tests.ts +++ b/src/devtools/tests.ts @@ -10199,6 +10199,153 @@ export function registerAllTests(): void { }) }) + suite('ui — camera framing', () => { + function resetPropertySectionPrefs() { + try { + localStorage.removeItem('animatr:propSections') + } catch { + /* noop */ + } + } + + function stubLaneRect(lane: HTMLElement, width = 1000): void { + const fakeRect = { + left: 0, + top: 0, + width, + height: 20, + right: width, + bottom: 20, + x: 0, + y: 0, + toJSON: () => ({}), + } as DOMRect + ;(lane as unknown as { getBoundingClientRect: () => DOMRect }).getBoundingClientRect = () => + fakeRect + } + + test('stage camera button enables camera without resizing the editor stage', async () => { + const s = makeFreshStore() + s.scene.canvas.width = 640 + s.scene.canvas.height = 360 + const handle = mountComponent(CanvasStage, {}) + try { + await nextTick() + const dims = handle.host.querySelector('.dims') as HTMLElement + const btn = handle.host.querySelector('.zoom-controls .camera-btn') as HTMLButtonElement + expect(dims).toBeTruthy() + expect(btn).toBeTruthy() + expect(dims.textContent?.trim()).toBe('640 × 360') + expect(s.scene.camera).toBe(undefined) + + btn.click() + await nextTick() + expect(s.scene.camera?.enabled).toBe(true) + expect(s.selectedCamera).toBe(true) + expect(btn.classList.contains('active')).toBe(true) + expect(dims.textContent?.trim()).toBe('640 × 360') + expect(s.scene.canvas.width).toBe(640) + expect(s.scene.canvas.height).toBe(360) + + btn.click() + await nextTick() + expect(s.scene.camera?.enabled).toBe(false) + expect(dims.textContent?.trim()).toBe('640 × 360') + } finally { + handle.unmount() + } + }) + + test('properties panel edits the selected camera frame, not scene canvas size', async () => { + resetPropertySectionPrefs() + const s = makeFreshStore() + s.scene.canvas.width = 800 + s.scene.canvas.height = 450 + s.enableCamera() + s.selectCamera(true) + const handle = mountComponent(PropertiesPanel, {}) + try { + await nextTick() + const banner = handle.host.querySelector('.multi-banner') as HTMLElement + expect(banner.textContent).toContain(i18nT('properties.camera.banner')) + const inputs = handle.host.querySelectorAll( + '.prop-group input[type="number"]', + ) + expect(inputs.length).toBe(4) + const [xInput, yInput, widthInput, heightInput] = Array.from(inputs) + expect(Number(widthInput.value)).toBe(800) + expect(Number(heightInput.value)).toBe(450) + + xInput.value = '24' + xInput.dispatchEvent(new Event('input', { bubbles: true })) + widthInput.value = '320' + widthInput.dispatchEvent(new Event('input', { bubbles: true })) + await nextTick() + + expect(s.scene.camera?.keyframes.length).toBe(1) + expect(s.scene.camera?.keyframes[0].props.x).toBe(24) + expect(s.scene.camera?.keyframes[0].props.width).toBe(320) + expect(s.scene.canvas.width).toBe(800) + expect(s.scene.canvas.height).toBe(450) + void yInput + } finally { + handle.unmount() + } + }) + + test('timeline camera row selects, hides, locks, and adds camera keyframes', async () => { + const s = makeFreshStore() + s.setDuration(10) + s.enableCamera() + s.selectCamera(false) + const handle = mountComponent(Timeline, {}) + try { + await nextTick() + const trackName = handle.host.querySelector('.camera-track .track-name') as HTMLElement + const eye = handle.host.querySelector('.camera-track .eye-btn') as HTMLButtonElement + const lock = handle.host.querySelector('.camera-track .lock-btn') as HTMLButtonElement + const lane = handle.host.querySelector('.camera-track .camera-lane') as HTMLElement + expect(trackName).toBeTruthy() + expect(eye).toBeTruthy() + expect(lock).toBeTruthy() + expect(lane).toBeTruthy() + + trackName.click() + await nextTick() + expect(s.selectedCamera).toBe(true) + + eye.click() + await nextTick() + expect(s.scene.camera?.overlayVisible).toBe(false) + expect(eye.classList.contains('hidden')).toBe(true) + + lock.click() + await nextTick() + expect(s.scene.camera?.locked).toBe(true) + expect(lock.classList.contains('locked')).toBe(true) + + stubLaneRect(lane) + lane.dispatchEvent( + new MouseEvent('dblclick', { bubbles: true, cancelable: true, clientX: 500 }), + ) + await nextTick() + expect(s.scene.camera?.keyframes.length).toBe(0) + + lock.click() + await nextTick() + lane.dispatchEvent( + new MouseEvent('dblclick', { bubbles: true, cancelable: true, clientX: 500 }), + ) + await nextTick() + expect(s.scene.camera?.keyframes.length).toBe(1) + expect(Math.abs((s.scene.camera?.keyframes[0].time ?? 0) - 5) < 0.1).toBe(true) + expect(Math.abs(s.playhead - 5) < 0.1).toBe(true) + } finally { + handle.unmount() + } + }) + }) + suite('ui — CanvasStage centre crosshair (Konva)', () => { // Verify the lines actually render at the correct coordinates in // Konva when the toggle is on. Requires a proper VueKonva mount.