diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 634fec26d..2cc58c67d 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -231,6 +231,7 @@ function LaunchWindowContent() { size="lg" className={` ${styles.electronNoDrag} group gap-2 px-3 min-w-0 max-w-[180px] shrink-0 ${openId === "sources" ? "border-[var(--launch-border-strong)] bg-[var(--launch-hover)]" : ""} `} title={selectedSource} + aria-label={`${t("recording.source")}: ${selectedSource}`} > {microphoneEnabled ? ( {webcamEnabled ? ( 0 ? "text-accent" : ""} + aria-label={`${t("recording.countdownDelay")}: ${countdownDelay === 0 ? t("recording.noDelay") : `${countdownDelay}s`}`} >
@@ -420,6 +425,15 @@ function LaunchWindowContent() { +
+ {finalizing + ? t("recording.preparing", "Preparing recording") + : recording + ? paused + ? t("recording.paused") + : t("recording.recordingInProgress") + : ""} +
-
+
- + {formatTime(elapsed)} {paused && ( diff --git a/src/components/launch/SourceSelector.tsx b/src/components/launch/SourceSelector.tsx index c5906697c..4b0654f9d 100644 --- a/src/components/launch/SourceSelector.tsx +++ b/src/components/launch/SourceSelector.tsx @@ -58,6 +58,7 @@ export const SourceSelectorContent = ({ +
+ {t("sourceSelector.loadingSources")}
); } @@ -148,7 +150,7 @@ export const SourceSelectorContent = ({ ) : null} ) : ( -
+
{t("recording.noSourcesFound")}
)} diff --git a/src/i18n/locales/de/launch.json b/src/i18n/locales/de/launch.json index 68e009b11..d99417393 100644 --- a/src/i18n/locales/de/launch.json +++ b/src/i18n/locales/de/launch.json @@ -39,6 +39,11 @@ "language": "Sprache", "paused": "PAUSIERT", "rec": "AUFNAHME", + "recordingInProgress": "Aufnahme läuft", + "setup": "Aufnahme einrichten", + "source": "Quelle", + "on": "ein", + "off": "aus", "resume": "Fortsetzen", "pause": "Pause", "stop": "Stopp", diff --git a/src/i18n/locales/en/launch.json b/src/i18n/locales/en/launch.json index 5b9b9cc26..ac13c9085 100644 --- a/src/i18n/locales/en/launch.json +++ b/src/i18n/locales/en/launch.json @@ -39,6 +39,11 @@ "language": "Language", "paused": "PAUSED", "rec": "REC", + "recordingInProgress": "Recording in progress", + "setup": "Recording setup", + "source": "Source", + "on": "on", + "off": "off", "resume": "Resume", "pause": "Pause", "stop": "Stop", diff --git a/src/i18n/locales/es/launch.json b/src/i18n/locales/es/launch.json index b9e2bfa61..3dd6f4cbf 100644 --- a/src/i18n/locales/es/launch.json +++ b/src/i18n/locales/es/launch.json @@ -39,6 +39,11 @@ "language": "Idioma", "paused": "PAUSADO", "rec": "REC", + "recordingInProgress": "Grabación en curso", + "setup": "Configuración de grabación", + "source": "Fuente", + "on": "activado", + "off": "desactivado", "resume": "Reanudar", "pause": "Pausa", "stop": "Detener", diff --git a/src/i18n/locales/fr/launch.json b/src/i18n/locales/fr/launch.json index 793ba2855..6a7036958 100644 --- a/src/i18n/locales/fr/launch.json +++ b/src/i18n/locales/fr/launch.json @@ -39,6 +39,11 @@ "language": "Langue", "paused": "EN PAUSE", "rec": "REC", + "recordingInProgress": "Enregistrement en cours", + "setup": "Configuration de l'enregistrement", + "source": "Source", + "on": "activé", + "off": "désactivé", "resume": "Reprendre", "pause": "Pause", "stop": "Arrêter", diff --git a/src/i18n/locales/it/launch.json b/src/i18n/locales/it/launch.json index f607b7680..1b4f09556 100644 --- a/src/i18n/locales/it/launch.json +++ b/src/i18n/locales/it/launch.json @@ -39,6 +39,11 @@ "language": "Lingua", "paused": "IN PAUSA", "rec": "REC", + "recordingInProgress": "Registrazione in corso", + "setup": "Impostazioni di registrazione", + "source": "Sorgente", + "on": "attivo", + "off": "disattivo", "resume": "Riprendi", "pause": "Pausa", "stop": "Stop", diff --git a/src/i18n/locales/ko/launch.json b/src/i18n/locales/ko/launch.json index 3ce4af823..c12d5ff6e 100644 --- a/src/i18n/locales/ko/launch.json +++ b/src/i18n/locales/ko/launch.json @@ -39,6 +39,11 @@ "language": "언어", "paused": "일시 정지", "rec": "녹화 중", + "recordingInProgress": "녹화 진행 중", + "setup": "녹화 설정", + "source": "소스", + "on": "켜짐", + "off": "꺼짐", "resume": "계속", "pause": "일시 정지", "stop": "중지", diff --git a/src/i18n/locales/nl/launch.json b/src/i18n/locales/nl/launch.json index 2843ca28c..303eba339 100644 --- a/src/i18n/locales/nl/launch.json +++ b/src/i18n/locales/nl/launch.json @@ -39,6 +39,11 @@ "language": "Taal", "paused": "GEPAUZEERD", "rec": "OPN", + "recordingInProgress": "Opname bezig", + "setup": "Opname instellen", + "source": "Bron", + "on": "aan", + "off": "uit", "resume": "Hervatten", "pause": "Pauzeren", "stop": "Stoppen", diff --git a/src/i18n/locales/pt-BR/launch.json b/src/i18n/locales/pt-BR/launch.json index 51f26e23d..8f12b7d80 100644 --- a/src/i18n/locales/pt-BR/launch.json +++ b/src/i18n/locales/pt-BR/launch.json @@ -39,6 +39,11 @@ "language": "Idioma", "paused": "PAUSADO", "rec": "GRAV", + "recordingInProgress": "Gravação em andamento", + "setup": "Configuração da gravação", + "source": "Fonte", + "on": "ligado", + "off": "desligado", "resume": "Retomar", "pause": "Pausar", "stop": "Parar", diff --git a/src/i18n/locales/ru/launch.json b/src/i18n/locales/ru/launch.json index f6bca7cda..4d0867098 100644 --- a/src/i18n/locales/ru/launch.json +++ b/src/i18n/locales/ru/launch.json @@ -39,6 +39,11 @@ "language": "Язык", "paused": "ПАУЗА", "rec": "ЗАПИСЬ", + "recordingInProgress": "Идёт запись", + "setup": "Настройка записи", + "source": "Источник", + "on": "включено", + "off": "выключено", "resume": "Возобновить", "pause": "Пауза", "stop": "Стоп", diff --git a/src/i18n/locales/zh-CN/launch.json b/src/i18n/locales/zh-CN/launch.json index e0c5ed7bf..70113d799 100644 --- a/src/i18n/locales/zh-CN/launch.json +++ b/src/i18n/locales/zh-CN/launch.json @@ -39,6 +39,11 @@ "language": "语言", "paused": "已暂停", "rec": "录制中", + "recordingInProgress": "正在录制", + "setup": "录制设置", + "source": "来源", + "on": "开", + "off": "关", "resume": "恢复", "pause": "暂停", "stop": "停止", diff --git a/src/i18n/locales/zh-TW/launch.json b/src/i18n/locales/zh-TW/launch.json index fb4894863..bbe1b1a21 100644 --- a/src/i18n/locales/zh-TW/launch.json +++ b/src/i18n/locales/zh-TW/launch.json @@ -39,6 +39,11 @@ "language": "語言", "paused": "已暫停", "rec": "錄製中", + "recordingInProgress": "正在錄製", + "setup": "錄製設定", + "source": "來源", + "on": "開", + "off": "關", "resume": "繼續", "pause": "暫停", "stop": "停止", diff --git a/tests/ui/hud-accessibility.spec.ts b/tests/ui/hud-accessibility.spec.ts new file mode 100644 index 000000000..fc9047a94 --- /dev/null +++ b/tests/ui/hud-accessibility.spec.ts @@ -0,0 +1,64 @@ +import { expect, test } from "@playwright/test"; +import { installDesktopBridge } from "./bridge"; + +test("recording setup exposes controls and supports keyboard source selection", async ({ + page, +}) => { + await installDesktopBridge(page); + await page.goto("/?windowType=hud-overlay"); + + const setup = page.getByRole("group", { name: "Recording setup" }); + await expect(setup.getByRole("button", { name: "Source: Built-in Display" })).toBeVisible(); + await expect(setup.getByRole("button", { name: "Microphone: off" })).toBeVisible(); + await expect(setup.getByRole("button", { name: "Webcam: off" })).toBeVisible(); + await expect(setup.getByRole("button", { name: "Countdown delay: 3s" })).toBeVisible(); + await expect(setup.getByRole("button", { name: "Record", exact: true })).toBeVisible(); + + const sourceTrigger = setup.getByRole("button", { name: "Source: Built-in Display" }); + await sourceTrigger.focus(); + await page.keyboard.press("Enter"); + const source = page.getByRole("button", { name: "Screen: Built-in Display" }); + await expect(source).toBeVisible(); + await source.focus(); + await page.keyboard.press("Space"); + await expect(source).toHaveCount(0); +}); + +test("source picker announces loading and empty results", async ({ page }) => { + await installDesktopBridge(page); + await page.goto("/?windowType=hud-overlay"); + await page.evaluate(() => { + window.electronAPI.getSources = () => + new Promise((resolve) => { + window.addEventListener("test-resolve-sources", () => resolve([]), { once: true }); + }); + }); + + const sourceTrigger = page.getByRole("button", { name: "Source: Built-in Display" }); + await sourceTrigger.focus(); + await page.keyboard.press("Enter"); + await expect(page.getByRole("status").filter({ hasText: "Loading sources..." })).toBeVisible(); + await page.evaluate(() => window.dispatchEvent(new Event("test-resolve-sources"))); + await expect(page.getByRole("status").filter({ hasText: "No sources found" })).toBeVisible(); +}); + +test("recording state is announced separately from the running timer", async ({ page }) => { + await installDesktopBridge(page); + await page.addInitScript(() => { + window.electronAPI.onRecordingStateChanged = (callback) => { + const listener = () => callback({ recording: true, sourceName: "Built-in Display" }); + window.addEventListener("test-recording-started", listener); + return () => window.removeEventListener("test-recording-started", listener); + }; + }); + await page.goto("/?windowType=hud-overlay"); + await expect(page.getByRole("group", { name: "Recording setup" })).toBeVisible(); + await page.evaluate(() => window.dispatchEvent(new Event("test-recording-started"))); + + const controls = page.getByRole("group", { name: "Recording controls" }); + await expect(controls).toBeVisible(); + await expect(page.getByRole("status")).toHaveText("Recording in progress"); + await expect(controls.getByRole("timer")).toHaveAttribute("aria-live", "off"); + await expect(controls.getByRole("button", { name: "Pause" })).toBeVisible(); + await expect(controls.getByRole("button", { name: "Stop" })).toBeVisible(); +}); diff --git a/tests/ui/hud-layout.spec.ts b/tests/ui/hud-layout.spec.ts index 8116374de..d28bc58b8 100644 --- a/tests/ui/hud-layout.spec.ts +++ b/tests/ui/hud-layout.spec.ts @@ -43,7 +43,7 @@ test("recording HUD uses uniform controls and a readable timer", async ({ page } await page.evaluate(() => window.dispatchEvent(new Event("test-recording-started"))); const controls = page.getByRole("group", { name: "Recording controls" }); await expect(controls).toBeVisible(); - await expect(controls.getByRole("status")).toContainText("00:00"); + await expect(controls.getByRole("timer")).toContainText("00:00"); await expect(controls.getByRole("button", { name: "Home", exact: true })).toBeVisible(); const sizes = await controls.getByRole("button").evaluateAll((buttons) => buttons.map((button) => {