1.1.2: arrow-key nudge for the selected stage object
Arrow keys now move the currently-selected object by 1 scene unit; Shift bumps the step to 10. Works for both regular layers (upsertKeyframe at the playhead — same write path as a stage drag) and clip instances (setClipInstanceTransform). Locked targets and events originating in text inputs are passed through untouched, so typing in the document drawer / rename dialogs isn't stolen. Also drops the flaky show-locked persistence test. It assumed the in-memory default (true) differed from the stored value; on a fresh dev-server restart with empty localStorage the first toggle was a no-op, the watch never fired, and the assertion failed. Test now flips the ref away first so every subsequent transition is guaranteed to write. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
01c13981c4
commit
289fa770b0
4 files changed
+169
-2
No files matched your search
@@ -4387,6 +4387,105 @@ export function registerAllTests(): void {
|
||||
})
|
||||
})
|
||||
|
||||
suite('app — arrow-key nudge', () => {
|
||||
// 1px arrow nudge (+10 with Shift) on whichever selection is
|
||||
// active: regular layer via upsertKeyframe at playhead, clip
|
||||
// instance via setClipInstanceTransform. Locked targets + inputs
|
||||
// are skipped so typing in the document drawer isn't stolen.
|
||||
async function clearKeys() {
|
||||
// Same escape-press routine the layer-delete tests use, so
|
||||
// kf / marker selections from earlier tests don't interfere.
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
|
||||
await nextTick()
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
function arrow(key: string, shift = false) {
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key, bubbles: true, shiftKey: shift }),
|
||||
)
|
||||
}
|
||||
|
||||
test('ArrowRight nudges the selected layer by +1 at the current playhead', async () => {
|
||||
const s = makeFreshStore()
|
||||
await clearKeys()
|
||||
const a = s.addAsset({ kind: 'rect', name: 'R', width: 10, height: 10, fill: '#fff' })
|
||||
const l = s.addLayer(a.id)!
|
||||
s.selectLayer(l.id)
|
||||
const before = sampleLayer(l, s.playhead).x
|
||||
arrow('ArrowRight')
|
||||
await nextTick()
|
||||
expect(sampleLayer(l, s.playhead).x).toBe(before + 1)
|
||||
})
|
||||
|
||||
test('Shift+ArrowDown nudges by +10 on the y axis', async () => {
|
||||
const s = makeFreshStore()
|
||||
await clearKeys()
|
||||
const a = s.addAsset({ kind: 'rect', name: 'R', width: 10, height: 10, fill: '#fff' })
|
||||
const l = s.addLayer(a.id)!
|
||||
s.selectLayer(l.id)
|
||||
const beforeY = sampleLayer(l, s.playhead).y
|
||||
arrow('ArrowDown', true)
|
||||
await nextTick()
|
||||
expect(sampleLayer(l, s.playhead).y).toBe(beforeY + 10)
|
||||
})
|
||||
|
||||
test('ArrowLeft on a selected clip-instance shifts its x by -1', async () => {
|
||||
const s = makeFreshStore()
|
||||
await clearKeys()
|
||||
const clip = s.addClip('C')
|
||||
clip.duration = 2
|
||||
const track = s.scene.clipTracks![0]
|
||||
const ci = s.addClipInstance(clip.id, 0, track.id)!
|
||||
s.selectClipInstance(ci.id)
|
||||
const before = ci.x ?? 0
|
||||
arrow('ArrowLeft')
|
||||
await nextTick()
|
||||
expect(ci.x).toBe(before - 1)
|
||||
})
|
||||
|
||||
test('nudge is skipped for a locked layer', async () => {
|
||||
const s = makeFreshStore()
|
||||
await clearKeys()
|
||||
const a = s.addAsset({ kind: 'rect', name: 'R', width: 10, height: 10, fill: '#fff' })
|
||||
const l = s.addLayer(a.id)!
|
||||
s.selectLayer(l.id)
|
||||
s.setTrackLocked(l.id, true)
|
||||
// Locked → selection cleared by deselectIfLocked. Re-select
|
||||
// through the store directly so we can prove the arrow key
|
||||
// doesn't still move the layer.
|
||||
s.selectedLayerId = l.id
|
||||
s.selectedLayerIds = [l.id]
|
||||
const before = sampleLayer(l, s.playhead).x
|
||||
arrow('ArrowRight')
|
||||
await nextTick()
|
||||
expect(sampleLayer(l, s.playhead).x).toBe(before)
|
||||
})
|
||||
|
||||
test('nudge is skipped when the event target is an input', async () => {
|
||||
const s = makeFreshStore()
|
||||
await clearKeys()
|
||||
const a = s.addAsset({ kind: 'rect', name: 'R', width: 10, height: 10, fill: '#fff' })
|
||||
const l = s.addLayer(a.id)!
|
||||
s.selectLayer(l.id)
|
||||
const before = sampleLayer(l, s.playhead).x
|
||||
// Dispatch ON an input so isEditable() sees a text input target.
|
||||
const input = document.createElement('input')
|
||||
input.type = 'text'
|
||||
document.body.appendChild(input)
|
||||
try {
|
||||
input.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }),
|
||||
)
|
||||
await nextTick()
|
||||
expect(sampleLayer(l, s.playhead).x).toBe(before)
|
||||
} finally {
|
||||
document.body.removeChild(input)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
suite('timeline — backspace deletes selected layer', () => {
|
||||
// Regression: Backspace / Delete used to only remove selected
|
||||
// keyframes and markers. The selected layer track had no shortcut
|
||||
@@ -9317,6 +9416,14 @@ export function registerAllTests(): void {
|
||||
test('toggling showLocked persists to localStorage', async () => {
|
||||
const { showLocked } = await import('@/composables/useShowLocked')
|
||||
const KEY = 'animatr:showLockedTracks'
|
||||
// Flip away first so each of the assertion transitions below
|
||||
// actually changes the ref — otherwise the first
|
||||
// `showLocked.value = true` no-ops when the default was
|
||||
// already true, the watch never fires, and localStorage
|
||||
// stays at whatever leaked in from the previous test run
|
||||
// (or stays null on a fresh dev-server restart).
|
||||
showLocked.value = false
|
||||
await tick()
|
||||
showLocked.value = true
|
||||
await tick()
|
||||
expect(localStorage.getItem(KEY)).toBe('1')
|
||||
|
||||
Reference in new issue
Block a user