Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 25 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion electron/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ export function createEditorWindow(): BrowserWindow {
additionalArguments: [ASSET_BASE_URL_ARG],
nodeIntegration: false,
contextIsolation: true,
webSecurity: false,
backgroundThrottling: false,
},
});
Expand Down
14 changes: 0 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
3 changes: 1 addition & 2 deletions src/components/video-editor/timeline/TimelineEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
18 changes: 9 additions & 9 deletions src/hooks/useScreenRecorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -450,7 +458,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
webcamRecorder.current = null;
teardownMedia();
};
}, [teardownMedia]);
}, [safeHideCountdownOverlay, teardownMedia]);

const safeShowCountdownOverlay = async (value: number, runId: number) => {
try {
Expand All @@ -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;

Expand Down
4 changes: 4 additions & 0 deletions src/i18n/__tests__/tutorialHelpTranslations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -39,6 +41,8 @@ const dialogsByLocale = {
fr: frDialogs,
tr: trDialogs,
"ko-KR": koKRDialogs,
"zh-TW": zhTWDialogs,
"ja-JP": jaJPDialogs,
} satisfies Record<Locale, { tutorial: Record<string, unknown> }>;

describe("TutorialHelp translations", () => {
Expand Down
7 changes: 0 additions & 7 deletions src/i18n/locales/fr/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
13 changes: 1 addition & 12 deletions src/i18n/locales/tr/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 0 additions & 11 deletions src/i18n/locales/tr/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
7 changes: 0 additions & 7 deletions src/i18n/locales/zh-TW/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,6 @@
"manual": "手動",
"auto": "自動",
"autoDescription": "攝影機跟隨錄製時的游標位置"
},
"speed": {
"title": "縮放速度",
"instant": "即時",
"fast": "快速",
"smooth": "平滑",
"lazy": "緩慢"
}
},
"speed": {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/blurEffects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)");
});
});
56 changes: 43 additions & 13 deletions tests/e2e/gif-export.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ElectronApplication["process"]>,
timeoutMs: number,
) {
if (child.exitCode !== null || child.killed) return;

await Promise.race([
new Promise<void>((resolve) => child.once("exit", () => resolve())),
new Promise<void>((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 = "";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}"],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Vitest 4.x config: does setting test.excludeindefineConfigreplace the default exclude list, and isconfigDefaults.exclude the supported way to extend it?

💡 Result:

Yes, setting test.exclude in defineConfig replaces the default exclude list. The official Vitest documentation explicitly states: "manually setting exclude in your config will replace the default value." The default is ['/node_modules/', '/.git/']. Yes, configDefaults.exclude is the supported way to extend it. The docs provide this exact example: import { configDefaults, defineConfig } from 'vitest/config' export default defineConfig({ test: { exclude: [ ...configDefaults.exclude, 'packages/template/*', './temp/', ], }, }) This behavior applies to Vitest 4.x, as confirmed by the current official documentation at vitest.dev/config/exclude (published 2026-03-04) and v4.vitest.dev/config/exclude.

Citations:


Preserve Vitest's default excludes here.

Setting test.exclude manually replaces Vitest's default exclude list (which includes /node_modules/ and /.git/), so this drops the built-in patterns and can make the test runner pick up folders it shouldn't. lowkey risky for CI noise/perf. Fold your browser glob into configDefaults.exclude instead per the official docs.

nit: cleaner fix
-import { defineConfig } from "vitest/config";
+import { configDefaults, defineConfig } from "vitest/config";
 
 export default defineConfig({
 	test: {
 		globals: true,
 		environment: "jsdom",
 		include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
-		exclude: ["src/**/*.browser.test.{ts,tsx}"],
+		exclude: [...configDefaults.exclude, "src/**/*.browser.test.{ts,tsx}"],
 	},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vitest.config.ts` at line 9, The test.exclude assignment overrides Vitest's
built-in excludes (like /node_modules/ and /.git/), so keep those defaults and
add your browser-only pattern instead: use configDefaults.exclude and merge or
append the glob "src/**/*.browser.test.{ts,tsx}" rather than replacing
test.exclude in the vitest config (refer to test.exclude, configDefaults.exclude
and the browser glob to locate where to change).

},
resolve: {
alias: {
Expand Down
Loading