0.13.1: new track-group lands above the active layer

addTrackGroup takes an optional anchorTrackId and inserts directly
above it in trackOrder; anchors nested inside another group resolve
to their parent's slot. The timeline's group-add button forwards
selectedLayerId so the new group lands where the user is working
instead of always jumping to the top. Covered by store + UI tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-24 08:08:23 +02:00
1 parent 86ec7aa835
commit 8aa91fc27b
5 files changed
+113 -5

No files matched your search

+80
View File
@@ -2666,6 +2666,41 @@ export function registerAllTests(): void {
expect(s.scene.trackOrder![0]).toBe(g.id)
})
test('addTrackGroup with an anchor inserts directly above that track', () => {
const s = makeFreshStore()
const a = s.addAsset({ kind: 'rect', name: 'X', width: 10, height: 10, fill: '#111' })
const l1 = s.addLayer(a.id)! // trackOrder ends up [l2, l1] via unshift
const l2 = s.addLayer(a.id)!
// Anchor on the second layer → group lands at its index, shoving it down.
const g = s.addTrackGroup('G', l1.id)
const order = s.scene.trackOrder ?? []
const gIdx = order.indexOf(g.id)
const l1Idx = order.indexOf(l1.id)
expect(gIdx).toBe(l1Idx - 1)
void l2
})
test('addTrackGroup with an anchor inside an existing group lands above the parent group', () => {
const s = makeFreshStore()
const a = s.addAsset({ kind: 'rect', name: 'X', width: 10, height: 10, fill: '#111' })
const l = s.addLayer(a.id)!
const parent = s.addTrackGroup('Parent')
s.moveTrackIntoGroup(l.id, parent.id)
// Anchor is nested inside `parent` — new group must sit above `parent`,
// not attempt to nest itself inside another group.
const fresh = s.addTrackGroup('Fresh', l.id)
const order = s.scene.trackOrder ?? []
expect(order.indexOf(fresh.id)).toBe(order.indexOf(parent.id) - 1)
})
test('addTrackGroup with an unknown anchor falls back to the top', () => {
const s = makeFreshStore()
const a = s.addAsset({ kind: 'rect', name: 'X', width: 10, height: 10, fill: '#111' })
s.addLayer(a.id)!
const g = s.addTrackGroup('G', 'ghost-id')
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' })
@@ -5027,6 +5062,51 @@ export function registerAllTests(): void {
})
})
suite('ui — Timeline new-group button', () => {
test('clicking the new-group button with a selected layer inserts the group above it', async () => {
const s = makeFreshStore()
const a = s.addAsset({ kind: 'rect', name: 'Anchor', width: 10, height: 10, fill: '#111' })
const top = s.addLayer(a.id)!
const anchor = s.addLayer(a.id)!
s.selectLayer(anchor.id)
const handle = mountComponent(Timeline, {})
await nextTick()
const btn = handle.host.querySelector(
`button[aria-label="${i18nT('timeline.addGroupTooltip')}"]`,
) as HTMLButtonElement
expect(btn).toBeTruthy()
btn.click()
await nextTick()
const order = s.scene.trackOrder ?? []
const groupId = (s.scene.groups ?? [])[0]?.id
expect(groupId).toBeTruthy()
const gIdx = order.indexOf(groupId!)
const anchorIdx = order.indexOf(anchor.id)
// Group sits right above the anchor layer, pushing it down by one slot.
expect(gIdx).toBe(anchorIdx - 1)
// Unrelated `top` layer keeps its own slot — not dragged along.
expect(order.indexOf(top.id) >= 0).toBe(true)
handle.unmount()
})
test('clicking the new-group button with no selection unshifts to the top (regression)', async () => {
const s = makeFreshStore()
const a = s.addAsset({ kind: 'rect', name: 'X', width: 10, height: 10, fill: '#111' })
s.addLayer(a.id)
s.selectLayer(null)
const handle = mountComponent(Timeline, {})
await nextTick()
const btn = handle.host.querySelector(
`button[aria-label="${i18nT('timeline.addGroupTooltip')}"]`,
) as HTMLButtonElement
btn.click()
await nextTick()
const groupId = (s.scene.groups ?? [])[0]?.id
expect(s.scene.trackOrder?.[0]).toBe(groupId)
handle.unmount()
})
})
suite('ui — Timeline audio track', () => {
test('audio track renders as an .audio-track lane with one block per clip', async () => {
const s = makeFreshStore()