This commit is contained in:
th committed 2026-04-20 00:29:12 +02:00
1 parent 3ef5fb5a2d
commit 43933f93d1
11 files changed
+813 -76

No files matched your search

+200
View File
@@ -12,6 +12,7 @@ import { createHistory } from '@/composables/useHistory'
import { useKfSelection, kfSelectionKey } from '@/composables/useKfSelection'
import { APP_NAME, APP_VERSION } from '@/version'
import { drawSceneFrame } from '@/lib/render'
import { expandedTrackOrder } from '@/lib/sceneOrder'
import {
deleteStoredProject,
getStoredProject,
@@ -1221,6 +1222,200 @@ export function registerAllTests(): void {
})
})
suite('scene store — track groups', () => {
test('addTrackGroup creates an empty group at the top of trackOrder', () => {
const s = makeFreshStore()
const a = s.addAsset({ kind: 'rect', name: 'G1', width: 10, height: 10, fill: '#111' })
s.addLayer(a.id)
const g = s.addTrackGroup('My group')
expect(s.scene.groups).toHaveLength(1)
expect(g.childIds).toHaveLength(0)
expect(s.scene.trackOrder![0]).toBe(g.id)
})
test('moveTrackIntoGroup moves track from top-level to group childIds', () => {
const s = makeFreshStore()
const a = s.addAsset({ kind: 'rect', name: 'G2', width: 10, height: 10, fill: '#222' })
const l = s.addLayer(a.id)
const g = s.addTrackGroup('G')
s.moveTrackIntoGroup(l.id, g.id)
expect(g.childIds).toContain(l.id)
expect((s.scene.trackOrder ?? []).includes(l.id)).toBe(false)
})
test('moveTrackOutOfGroup promotes track back to top-level', () => {
const s = makeFreshStore()
const a = s.addAsset({ kind: 'rect', name: 'G3', width: 10, height: 10, fill: '#333' })
const l = s.addLayer(a.id)
const g = s.addTrackGroup('G')
s.moveTrackIntoGroup(l.id, g.id)
s.moveTrackOutOfGroup(l.id)
expect(g.childIds.includes(l.id)).toBe(false)
expect(s.scene.trackOrder).toContain(l.id)
})
test('removeTrackGroup promotes children back to top-level', () => {
const s = makeFreshStore()
const a = s.addAsset({ kind: 'rect', name: 'G4', width: 10, height: 10, fill: '#444' })
const l = s.addLayer(a.id)
const g = s.addTrackGroup('G')
s.moveTrackIntoGroup(l.id, g.id)
s.removeTrackGroup(g.id)
expect(s.scene.groups).toHaveLength(0)
expect(s.scene.trackOrder).toContain(l.id)
})
test('toggleGroupCollapsed flips the collapsed flag', () => {
const s = makeFreshStore()
const g = s.addTrackGroup('G')
expect(!!g.collapsed).toBe(false)
s.toggleGroupCollapsed(g.id)
expect(g.collapsed).toBe(true)
s.toggleGroupCollapsed(g.id)
expect(g.collapsed).toBe(false)
})
test('renameGroup updates the name', () => {
const s = makeFreshStore()
const g = s.addTrackGroup('Old')
s.renameGroup(g.id, 'New')
expect(g.name).toBe('New')
})
test('a track can only be in one group at a time', () => {
const s = makeFreshStore()
const a = s.addAsset({ kind: 'rect', name: 'X', width: 10, height: 10, fill: '#555' })
const l = s.addLayer(a.id)
const g1 = s.addTrackGroup('A')
const g2 = s.addTrackGroup('B')
s.moveTrackIntoGroup(l.id, g1.id)
s.moveTrackIntoGroup(l.id, g2.id)
expect(g1.childIds.includes(l.id)).toBe(false)
expect(g2.childIds).toContain(l.id)
})
test('expandedTrackOrder expands group IDs to their childIds inline', () => {
const s = makeFreshStore()
const a1 = s.addAsset({ kind: 'rect', name: 'A', width: 10, height: 10, fill: '#abc' })
const a2 = s.addAsset({ kind: 'rect', name: 'B', width: 10, height: 10, fill: '#def' })
const l1 = s.addLayer(a1.id)
const l2 = s.addLayer(a2.id)
const g = s.addTrackGroup('G')
s.moveTrackIntoGroup(l1.id, g.id)
// scene.trackOrder: [group, l2] (l1 is inside group)
// expanded: [l1, l2]
const expanded = expandedTrackOrder(s.scene)
expect(expanded).toEqual([l1.id, l2.id])
})
test('moveTrackIntoGroup respects atIndex for insertion', () => {
const s = makeFreshStore()
const a1 = s.addAsset({ kind: 'rect', name: 'A', width: 10, height: 10, fill: '#111' })
const a2 = s.addAsset({ kind: 'rect', name: 'B', width: 10, height: 10, fill: '#222' })
const a3 = s.addAsset({ kind: 'rect', name: 'C', width: 10, height: 10, fill: '#333' })
const l1 = s.addLayer(a1.id)
const l2 = s.addLayer(a2.id)
const l3 = s.addLayer(a3.id)
const g = s.addTrackGroup('G')
s.moveTrackIntoGroup(l1.id, g.id) // [l1]
s.moveTrackIntoGroup(l2.id, g.id) // [l1, l2]
s.moveTrackIntoGroup(l3.id, g.id, 1) // [l1, l3, l2]
expect(g.childIds).toEqual([l1.id, l3.id, l2.id])
})
test('removeTrackGroup inserts children at the former group position', () => {
const s = makeFreshStore()
const a = s.addAsset({ kind: 'rect', name: 'X', width: 10, height: 10, fill: '#aaa' })
const top1 = s.addLayer(a.id)
const top2 = s.addLayer(a.id)
const childA = s.addLayer(a.id)
const childB = s.addLayer(a.id)
const g = s.addTrackGroup('G')
s.moveTrackIntoGroup(childA.id, g.id)
s.moveTrackIntoGroup(childB.id, g.id)
// trackOrder currently: [childB? no, children are removed from top-level]
// trackOrder contains: [g, top2, top1] (newest addLayer unshifts)
// Wait: addLayer unshifts; the creation order is top2(later), then top1(earlier)
// Then addLayer(childA), childB — all unshifted. Then moved into group.
// Let's just position group explicitly to be sure:
const order = s.scene.trackOrder ?? []
const gIdx = order.indexOf(g.id)
expect(gIdx).toBeGreaterThan(-1)
s.removeTrackGroup(g.id)
const newOrder = s.scene.trackOrder ?? []
// children should now sit at the former group position
expect(newOrder[gIdx]).toBe(childA.id)
expect(newOrder[gIdx + 1]).toBe(childB.id)
void top1
void top2
})
test('renameGroup ignores empty / whitespace names', () => {
const s = makeFreshStore()
const g = s.addTrackGroup('Original')
s.renameGroup(g.id, '')
expect(g.name).toBe('Original')
s.renameGroup(g.id, ' ')
expect(g.name).toBe('Original')
s.renameGroup(g.id, ' Renamed ')
expect(g.name).toBe('Renamed')
})
test('removeTrackGroup on unknown id is a no-op', () => {
const s = makeFreshStore()
const g = s.addTrackGroup('G')
const beforeCount = (s.scene.groups ?? []).length
s.removeTrackGroup('does-not-exist')
expect((s.scene.groups ?? []).length).toBe(beforeCount)
// The real group still exists
expect(s.scene.groups?.find((x) => x.id === g.id)).toBeTruthy()
})
test('setTrackColor applies to groups too', () => {
const s = makeFreshStore()
const g = s.addTrackGroup('Col')
s.setTrackColor(g.id, '#ff0000')
expect(g.color).toBe('#ff0000')
s.setTrackColor(g.id, null)
expect(g.color).toBe(undefined)
})
test('moveTrackIntoGroup with same group twice moves within group', () => {
const s = makeFreshStore()
const a = s.addAsset({ kind: 'rect', name: 'M', width: 10, height: 10, fill: '#555' })
const l1 = s.addLayer(a.id)
const l2 = s.addLayer(a.id)
const g = s.addTrackGroup('G')
s.moveTrackIntoGroup(l1.id, g.id)
s.moveTrackIntoGroup(l2.id, g.id)
// l1 currently at index 0. Move l1 to index 1.
s.moveTrackIntoGroup(l1.id, g.id, 1)
expect(g.childIds).toEqual([l2.id, l1.id])
})
test('normalizeScene (via loadScene) removes orphan childIds and deduplicates', () => {
const s = makeFreshStore()
const a = s.addAsset({ kind: 'rect', name: 'N', width: 10, height: 10, fill: '#666' })
const l = s.addLayer(a.id)
// Snapshot with a broken state
const snap = JSON.parse(JSON.stringify(s.scene))
snap.groups = [
{ id: 'g1', name: 'G1', childIds: [l.id, 'ghost-layer', l.id] },
{ id: 'g2', name: 'G2', childIds: [l.id] },
]
snap.trackOrder = [l.id, 'g1', 'g2']
s.loadScene(snap)
// Orphan 'ghost-layer' filtered out
const g1 = s.scene.groups?.find((g) => g.id === 'g1')!
const g2 = s.scene.groups?.find((g) => g.id === 'g2')!
expect(g1.childIds.includes('ghost-layer')).toBe(false)
// Dedupe: l.id appears only once across all groups
const inG1 = g1.childIds.filter((id) => id === l.id).length
const inG2 = g2.childIds.filter((id) => id === l.id).length
expect(inG1 + inG2).toBe(1)
})
})
suite('app smoke — live DOM', () => {
test('app root, topbar, timeline, stage are mounted', () => {
expect(document.querySelector('.app')).toBeTruthy()
@@ -1254,6 +1449,11 @@ export function registerAllTests(): void {
const dz = document.querySelector('.clip-track-dropzone')
expect(dz).toBeTruthy()
})
test('timeline has add-group button', () => {
const btn = document.querySelector('button[title="Create a new track group"]')
expect(btn).toBeTruthy()
})
})
suite('version — app metadata', () => {