0.16.1: welcome screen arrow-key navigation

Up/Down arrow keys now move focus through the recents list in the
welcome dialog; Home/End jump to the first/last entry. Tab / Enter
/ Space behaviour is untouched — arrows are additive. Covered by
two UI tests (arrow roundtrip + Home/End) that poll for the async
IDB load instead of a fixed delay.

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 10:29:07 +02:00
1 parent fcfd21428d
commit baf775c8de
4 files changed
+137 -2

No files matched your search

+108
View File
@@ -51,6 +51,7 @@ import Timeline from '@/components/Timeline.vue'
import AssetSidebar from '@/components/AssetSidebar.vue'
import CanvasStage from '@/components/CanvasStage.vue'
import ExportDialog from '@/components/ExportDialog.vue'
import WelcomeDialog from '@/components/WelcomeDialog.vue'
import FullscreenPreview from '@/components/FullscreenPreview.vue'
import HelpDialog from '@/components/HelpDialog.vue'
import { createAudioRuntime } from '@/composables/useAudioRuntime'
@@ -2338,6 +2339,113 @@ export function registerAllTests(): void {
})
})
suite('ui — WelcomeDialog', () => {
// Modal teleports to body — snapshot/diff dialogs to isolate.
const snapshotDialogs = () =>
new Set(Array.from(document.querySelectorAll('[role="dialog"]')))
const dialogsAddedSince = (baseline: Set<Element>) =>
Array.from(document.querySelectorAll('[role="dialog"]')).filter(
(el) => !baseline.has(el),
) as HTMLElement[]
/** Poll until the predicate matches or the budget elapses. The
* dialog's IDB load + Vue render cycle is fully async, so fixed
* sleeps were flaky on slower machines. */
async function waitUntil(predicate: () => boolean, budgetMs = 1000, stepMs = 10) {
const start = performance.now()
while (!predicate()) {
if (performance.now() - start > budgetMs) return
await tick(stepMs)
}
}
test('ArrowDown / ArrowUp move focus through the recents list', async () => {
// Seed two recents via the real IDB path so the dialog's own
// `listRecentProjects` call finds them.
const s = makeFreshStore()
const idA = `test-arrow-a-${Date.now()}`
const idB = `test-arrow-b-${Date.now()}`
await saveProjectToStore(s.project, { id: idA, name: 'A' })
await tick(5)
await saveProjectToStore(s.project, { id: idB, name: 'B' })
const baseline = snapshotDialogs()
const handle = mountComponent(WelcomeDialog, { modelValue: true })
// Wait for the dialog's async reload() to populate the list.
await waitUntil(() => {
const d = dialogsAddedSince(baseline)[0]
return !!d && d.querySelectorAll('.recent').length >= 2
})
const dialog = dialogsAddedSince(baseline)[0]
expect(dialog).toBeTruthy()
const items = dialog.querySelectorAll('.recent') as NodeListOf<HTMLElement>
expect(items.length >= 2).toBe(true)
// The initial focus should already be on the first recent.
items[0].focus()
expect(document.activeElement).toBe(items[0])
// ArrowDown moves to the next item.
items[0].dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }),
)
await nextTick()
expect(document.activeElement).toBe(items[1])
// ArrowUp moves back.
items[1].dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }),
)
await nextTick()
expect(document.activeElement).toBe(items[0])
handle.unmount()
await deleteStoredProject(idA)
await deleteStoredProject(idB)
})
test('Home and End jump to the first / last recent', async () => {
const s = makeFreshStore()
const ids = [
`test-home-end-a-${Date.now()}`,
`test-home-end-b-${Date.now()}`,
`test-home-end-c-${Date.now()}`,
]
for (const id of ids) {
await saveProjectToStore(s.project, { id, name: id })
await tick(5)
}
const baseline = snapshotDialogs()
const handle = mountComponent(WelcomeDialog, { modelValue: true })
await waitUntil(() => {
const d = dialogsAddedSince(baseline)[0]
return !!d && d.querySelectorAll('.recent').length >= 3
})
const dialog = dialogsAddedSince(baseline)[0]
const items = dialog.querySelectorAll('.recent') as NodeListOf<HTMLElement>
expect(items.length >= 3).toBe(true)
// Start somewhere in the middle, then End jumps to the last.
items[1].focus()
items[1].dispatchEvent(
new KeyboardEvent('keydown', { key: 'End', bubbles: true }),
)
await nextTick()
expect(document.activeElement).toBe(items[items.length - 1])
// Home returns to the first.
items[items.length - 1].dispatchEvent(
new KeyboardEvent('keydown', { key: 'Home', bubbles: true }),
)
await nextTick()
expect(document.activeElement).toBe(items[0])
handle.unmount()
for (const id of ids) await deleteStoredProject(id)
})
})
suite('clips — library & scope', () => {
test('addClip creates empty clip in library', () => {
const s = makeFreshStore()