![]()
diff --git a/src/devtools/tests.ts b/src/devtools/tests.ts
index 395d34c..1ecff7b 100644
--- a/src/devtools/tests.ts
+++ b/src/devtools/tests.ts
@@ -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
) =>
+ 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
+ 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
+ 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()
diff --git a/src/version.ts b/src/version.ts
index d2ffe8c..2d51acd 100644
--- a/src/version.ts
+++ b/src/version.ts
@@ -1,2 +1,2 @@
-export const APP_VERSION = '0.16.0'
+export const APP_VERSION = '0.16.1'
export const APP_NAME = 'RUWIO'