From 3a93d6ea5d62cd96cab7d244efaa35747622f2fa Mon Sep 17 00:00:00 2001
From: th
Date: Sun, 7 Jun 2026 23:58:05 +0200
Subject: [PATCH] Add camera frontend tests
---
src/devtools/tests.ts | 147 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 147 insertions(+)
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.
|