diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41947974a..9f3bc6c7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,16 +36,31 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - - run: npm ci - - run: npm run test:browser:install - - run: npm run test:browser - - build: - name: Build + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run i18n:check + - run: npm run test + - run: npm run test:browser:install + - run: npm run test:browser + + e2e: + name: Electron E2E + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run build-vite + - run: xvfb-run -a npm run test:e2e + + build: + name: Build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/electron/windows.ts b/electron/windows.ts index f94009ab0..a0e3c7fbd 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -121,7 +121,6 @@ export function createEditorWindow(): BrowserWindow { additionalArguments: [ASSET_BASE_URL_ARG], nodeIntegration: false, contextIsolation: true, - webSecurity: false, backgroundThrottling: false, }, }); diff --git a/package-lock.json b/package-lock.json index e823ad1c0..8fbcd5916 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,6 @@ "tailwind-merge": "^3.5.0", "tailwindcss-animate": "^1.0.7", "uiohook-napi": "^1.5.5", - "uuid": "^13.0.0", "web-demuxer": "^4.0.0" }, "devDependencies": { @@ -10450,19 +10449,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, "node_modules/verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", diff --git a/package.json b/package.json index 2e8421efc..0bffb0a3f 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,6 @@ "tailwind-merge": "^3.5.0", "tailwindcss-animate": "^1.0.7", "uiohook-napi": "^1.5.5", - "uuid": "^13.0.0", "web-demuxer": "^4.0.0" }, "devDependencies": { diff --git a/src/components/video-editor/timeline/TimelineEditor.tsx b/src/components/video-editor/timeline/TimelineEditor.tsx index 6fe3474d8..bb31bcbb5 100644 --- a/src/components/video-editor/timeline/TimelineEditor.tsx +++ b/src/components/video-editor/timeline/TimelineEditor.tsx @@ -12,7 +12,6 @@ import { } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; -import { v4 as uuidv4 } from "uuid"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -835,7 +834,7 @@ export default function TimelineEditor({ if (totalMs === 0) return; const time = Math.max(0, Math.min(currentTimeMs, totalMs)); if (keyframes.some((kf) => Math.abs(kf.time - time) < 1)) return; - setKeyframes((prev) => [...prev, { id: uuidv4(), time }]); + setKeyframes((prev) => [...prev, { id: crypto.randomUUID(), time }]); }, [currentTimeMs, totalMs, keyframes]); // Delete selected keyframe diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index dc9758fbd..6077d1617 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -408,6 +408,14 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } }); + const safeHideCountdownOverlay = useCallback(async (runId: number) => { + try { + await window.electronAPI.hideCountdownOverlay(runId); + } catch (error) { + console.warn("Failed to hide countdown overlay:", error); + } + }, []); + useEffect(() => { let cleanup: (() => void) | undefined; @@ -450,7 +458,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { webcamRecorder.current = null; teardownMedia(); }; - }, [teardownMedia]); + }, [safeHideCountdownOverlay, teardownMedia]); const safeShowCountdownOverlay = async (value: number, runId: number) => { try { @@ -477,14 +485,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } }; - const safeHideCountdownOverlay = async (runId: number) => { - try { - await window.electronAPI.hideCountdownOverlay(runId); - } catch (error) { - console.warn("Failed to hide countdown overlay:", error); - } - }; - const isCountdownRunActive = (runId?: number) => runId === undefined || countdownRunId.current === runId; diff --git a/src/i18n/__tests__/tutorialHelpTranslations.test.ts b/src/i18n/__tests__/tutorialHelpTranslations.test.ts index fcfa9d309..a1a9e8a18 100644 --- a/src/i18n/__tests__/tutorialHelpTranslations.test.ts +++ b/src/i18n/__tests__/tutorialHelpTranslations.test.ts @@ -3,9 +3,11 @@ import { type Locale, SUPPORTED_LOCALES } from "@/i18n/config"; import enDialogs from "@/i18n/locales/en/dialogs.json"; import esDialogs from "@/i18n/locales/es/dialogs.json"; import frDialogs from "@/i18n/locales/fr/dialogs.json"; +import jaJPDialogs from "@/i18n/locales/ja-JP/dialogs.json"; import koKRDialogs from "@/i18n/locales/ko-KR/dialogs.json"; import trDialogs from "@/i18n/locales/tr/dialogs.json"; import zhCNDialogs from "@/i18n/locales/zh-CN/dialogs.json"; +import zhTWDialogs from "@/i18n/locales/zh-TW/dialogs.json"; const tutorialHelpKeys = [ "triggerLabel", @@ -39,6 +41,8 @@ const dialogsByLocale = { fr: frDialogs, tr: trDialogs, "ko-KR": koKRDialogs, + "zh-TW": zhTWDialogs, + "ja-JP": jaJPDialogs, } satisfies Record }>; describe("TutorialHelp translations", () => { diff --git a/src/i18n/locales/fr/settings.json b/src/i18n/locales/fr/settings.json index 66df1ba98..2caa8deb9 100644 --- a/src/i18n/locales/fr/settings.json +++ b/src/i18n/locales/fr/settings.json @@ -8,13 +8,6 @@ "manual": "Manuel", "auto": "Auto", "autoDescription": "La caméra suit la position du curseur enregistré" - }, - "speed": { - "title": "Vitesse du zoom", - "instant": "Instantané", - "fast": "Rapide", - "smooth": "Fluide", - "lazy": "Lent" } }, "speed": { diff --git a/src/i18n/locales/tr/launch.json b/src/i18n/locales/tr/launch.json index 177ba3f52..c4f0b0bd4 100644 --- a/src/i18n/locales/tr/launch.json +++ b/src/i18n/locales/tr/launch.json @@ -14,18 +14,7 @@ "disableSystemAudio": "Sistem sesini devre dışı bırak", "enableMicrophone": "Mikrofonu etkinleştir", "disableMicrophone": "Mikrofonu devre dışı bırak", - "defaultMicrophone": "Varsayılan Mikrofon", - "enableNoiseReduction": "Gürültü azaltmayı etkinleştir (yapay zeka destekli)", - "disableNoiseReduction": "Gürültü azaltmayı devre dışı bırak", - "noiseReduction": "Gürültü azaltma", - "clickToCycle": "Seviye değiştirmek için tıklayın", - "nrLevel": { - "light": "Hafif", - "moderate": "Orta", - "aggressive": "Güçlü" - }, - "noiseReductionPrompt": "Daha net ses için yapay zeka destekli gürültü azaltmayı etkinleştirmek ister misiniz?", - "enableNoiseReductionShort": "Etkinleştir" + "defaultMicrophone": "Varsayılan Mikrofon" }, "webcam": { "enableWebcam": "Kamerayı etkinleştir", diff --git a/src/i18n/locales/tr/settings.json b/src/i18n/locales/tr/settings.json index 143c467cd..261dadebd 100644 --- a/src/i18n/locales/tr/settings.json +++ b/src/i18n/locales/tr/settings.json @@ -176,16 +176,5 @@ }, "language": { "title": "Dil" - }, - "audio": { - "title": "Ses", - "noiseReduction": "Gürültü Azaltma", - "level": "Seviye", - "nrLevel": { - "light": "Hafif", - "moderate": "Orta", - "aggressive": "Güçlü" - }, - "nrDescription": "Yapay zeka destekli gürültü azaltma arka plan gürültüsünü temizler. Daha yüksek seviyeler daha agresiftir ancak ses kalitesini etkileyebilir." } } diff --git a/src/i18n/locales/zh-TW/settings.json b/src/i18n/locales/zh-TW/settings.json index c93e8d5d7..9fc29c86e 100644 --- a/src/i18n/locales/zh-TW/settings.json +++ b/src/i18n/locales/zh-TW/settings.json @@ -8,13 +8,6 @@ "manual": "手動", "auto": "自動", "autoDescription": "攝影機跟隨錄製時的游標位置" - }, - "speed": { - "title": "縮放速度", - "instant": "即時", - "fast": "快速", - "smooth": "平滑", - "lazy": "緩慢" } }, "speed": { diff --git a/src/lib/blurEffects.test.ts b/src/lib/blurEffects.test.ts index 4797e6909..1a6a9c9a9 100644 --- a/src/lib/blurEffects.test.ts +++ b/src/lib/blurEffects.test.ts @@ -75,6 +75,6 @@ describe("blur color helpers", () => { intensity: 12, blockSize: 12, }), - ).toBe("rgba(0, 0, 0, 0.18)"); + ).toBe("rgba(0, 0, 0, 0.56)"); }); }); diff --git a/tests/e2e/gif-export.spec.ts b/tests/e2e/gif-export.spec.ts index d1fa3f7aa..98ca4f506 100644 --- a/tests/e2e/gif-export.spec.ts +++ b/tests/e2e/gif-export.spec.ts @@ -2,13 +2,44 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { _electron as electron, expect, test } from "@playwright/test"; +import { type ElectronApplication, _electron as electron, expect, test } from "@playwright/test"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.join(__dirname, "../.."); const MAIN_JS = path.join(ROOT, "dist-electron/main.js"); const TEST_VIDEO = path.join(__dirname, "../fixtures/sample.webm"); +async function waitForProcessExit( + child: ReturnType, + timeoutMs: number, +) { + if (child.exitCode !== null || child.killed) return; + + await Promise.race([ + new Promise((resolve) => child.once("exit", () => resolve())), + new Promise((resolve) => setTimeout(resolve, timeoutMs)), + ]); +} + +async function closeElectronApp(app: ElectronApplication) { + const child = app.process(); + + await app + .evaluate(({ app: electronApp }) => { + electronApp.exit(0); + }) + .catch(() => { + // The app may already be closing. + }); + + await waitForProcessExit(child, 2_000); + + if (child.exitCode === null && !child.killed) { + child.kill("SIGKILL"); + await waitForProcessExit(child, 2_000); + } +} + test("exports a GIF from a loaded video", async () => { const outputPath = path.join(os.tmpdir(), `test-gif-export-${Date.now()}.gif`); let testVideoInRecordings = ""; @@ -71,22 +102,21 @@ test("exports a GIF from a loaded video", async () => { fs.mkdirSync(recordingsDir, { recursive: true }); fs.copyFileSync(TEST_VIDEO, testVideoInRecordings); - try { - await hudWindow.evaluate((videoPath: string) => { - window.electronAPI.setCurrentVideoPath(videoPath); - window.electronAPI.switchToEditor(); - }, testVideoInRecordings); - } catch { - // Expected: switchToEditor() closes the HUD window, terminating - // the Playwright page context before evaluate() can resolve. - } - // ── 3. Switch to the editor window. This closes the HUD and opens // a new BrowserWindow with ?windowType=editor. - const editorWindow = await app.waitForEvent("window", { + const editorWindowPromise = app.waitForEvent("window", { predicate: (w) => w.url().includes("windowType=editor"), timeout: 15_000, }); + try { + await hudWindow.evaluate(async (videoPath: string) => { + await window.electronAPI.setCurrentVideoPath(videoPath); + void window.electronAPI.switchToEditor(); + }, testVideoInRecordings); + } catch { + // Expected if switching windows tears down the HUD page context. + } + const editorWindow = await editorWindowPromise; // WebCodecs (VideoEncoder) may not be registered in the renderer on first // load of a second BrowserWindow. A single reload ensures the feature is @@ -126,7 +156,7 @@ test("exports a GIF from a loaded video", async () => { const stats = fs.statSync(outputPath); expect(stats.size).toBeGreaterThan(1024); // at least 1 KB } finally { - await app.close(); + await closeElectronApp(app); if (fs.existsSync(outputPath)) { fs.unlinkSync(outputPath); } diff --git a/vitest.config.ts b/vitest.config.ts index ea60216f4..9108f6991 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -6,6 +6,7 @@ export default defineConfig({ globals: true, environment: "jsdom", include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], + exclude: ["src/**/*.browser.test.{ts,tsx}"], }, resolve: { alias: {