diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2b04dbe..757d997c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,3 +42,26 @@ jobs: cache: npm - run: npm ci - run: npx vite build + + e2e: + name: E2E Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npx playwright install --with-deps chromium + # Install Electron system dependencies not covered by Playwright's chromium deps + - run: npx electron . --version || sudo apt-get install -y libgbm-dev + - run: npm run build-vite + # xvfb provides a virtual display; Electron needs one on Linux even with show:false + - run: xvfb-run --auto-servernum npm run test:e2e + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 diff --git a/.gitignore b/.gitignore index 680ad3bf..70cc387d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,8 @@ dist-ssr *.sw? release/** *.kiro/ -# npx electron-builder --mac --win \ No newline at end of file +# npx electron-builder --mac --win + +# Playwright +test-results +playwright-report/ \ No newline at end of file diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index b771e469..f66af16a 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -51,6 +51,13 @@ interface Window { openVideoFilePicker: () => Promise<{ success: boolean; path?: string; canceled?: boolean }>; setCurrentVideoPath: (path: string) => Promise<{ success: boolean }>; getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }>; + readBinaryFile: (filePath: string) => Promise<{ + success: boolean; + data?: ArrayBuffer; + path?: string; + message?: string; + error?: string; + }>; clearCurrentVideoPath: () => Promise<{ success: boolean }>; saveProjectFile: ( projectData: unknown, diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 70d3ae40..5665cd77 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -200,6 +200,29 @@ export function registerIpcHandlers( } }); + ipcMain.handle("read-binary-file", async (_, inputPath: string) => { + try { + const normalizedPath = normalizeVideoSourcePath(inputPath); + if (!normalizedPath) { + return { success: false, message: "Invalid file path" }; + } + + const data = await fs.readFile(normalizedPath); + return { + success: true, + data: data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength), + path: normalizedPath, + }; + } catch (error) { + console.error("Failed to read binary file:", error); + return { + success: false, + message: "Failed to read binary file", + error: String(error), + }; + } + }); + ipcMain.handle("set-recording-state", (_, recording: boolean) => { if (recording) { stopCursorCapture(); diff --git a/electron/main.ts b/electron/main.ts index 257e0b20..23bcc492 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -70,6 +70,19 @@ function createWindow() { mainWindow = createHudOverlayWindow(); } +function showMainWindow() { + if (mainWindow && !mainWindow.isDestroyed()) { + if (mainWindow.isMinimized()) { + mainWindow.restore(); + } + mainWindow.show(); + mainWindow.focus(); + return; + } + + createWindow(); +} + function isEditorWindow(window: BrowserWindow) { return window.webContents.getURL().includes("windowType=editor"); } @@ -177,6 +190,12 @@ function setupApplicationMenu() { function createTray() { tray = new Tray(defaultTrayIcon); + tray.on("click", () => { + showMainWindow(); + }); + tray.on("double-click", () => { + showMainWindow(); + }); } function getTrayIcon(filename: string) { @@ -208,11 +227,7 @@ function updateTrayMenu(recording: boolean = false) { { label: "Open", click: () => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.isMinimized() && mainWindow.restore(); - } else { - createWindow(); - } + showMainWindow(); }, }, { @@ -355,7 +370,7 @@ app.whenReady().then(async () => { if (!tray) createTray(); updateTrayMenu(recording); if (!recording) { - if (mainWindow) mainWindow.restore(); + showMainWindow(); } }, ); diff --git a/electron/preload.ts b/electron/preload.ts index 9eeb5b16..acdec4fc 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -60,6 +60,9 @@ contextBridge.exposeInMainWorld("electronAPI", { getCurrentVideoPath: () => { return ipcRenderer.invoke("get-current-video-path"); }, + readBinaryFile: (filePath: string) => { + return ipcRenderer.invoke("read-binary-file", filePath); + }, clearCurrentVideoPath: () => { return ipcRenderer.invoke("clear-current-video-path"); }, diff --git a/electron/windows.ts b/electron/windows.ts index 77cb3a50..8bd0ba62 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -7,6 +7,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const APP_ROOT = path.join(__dirname, ".."); const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"]; const RENDERER_DIST = path.join(APP_ROOT, "dist"); +const HEADLESS = process.env["HEADLESS"] === "true"; let hudOverlayWindow: BrowserWindow | null = null; @@ -41,6 +42,7 @@ export function createHudOverlayWindow(): BrowserWindow { alwaysOnTop: true, skipTaskbar: true, hasShadow: false, + show: !HEADLESS, webPreferences: { preload: path.join(__dirname, "preload.mjs"), nodeIntegration: false, @@ -90,6 +92,7 @@ export function createEditorWindow(): BrowserWindow { skipTaskbar: false, title: "OpenScreen", backgroundColor: "#000000", + show: !HEADLESS, webPreferences: { preload: path.join(__dirname, "preload.mjs"), nodeIntegration: false, diff --git a/package-lock.json b/package-lock.json index 86295865..38d3e0cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.3.13", + "@playwright/test": "^1.58.2", "@types/node": "^25.0.3", "@types/react": "^18.2.64", "@types/react-dom": "^18.2.21", @@ -2942,6 +2943,21 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -10892,6 +10908,50 @@ "url": "https://opencollective.com/pixijs" } }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", diff --git a/package.json b/package.json index fbb618a9..7275192a 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "build:linux": "tsc && vite build && electron-builder --linux", "test": "vitest --run", "test:watch": "vitest", + "build-vite": "tsc && vite build", + "test:e2e": "playwright test", "prepare": "husky" }, "dependencies": { @@ -61,6 +63,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.3.13", + "@playwright/test": "^1.58.2", "@types/node": "^25.0.3", "@types/react": "^18.2.64", "@types/react-dom": "^18.2.21", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..d268975b --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests/e2e", + timeout: 120_000, // GIF encoding is CPU-bound; give it room + retries: 0, + reporter: "list", +}); diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index ec589ab2..4e0c0676 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -33,6 +33,7 @@ import type { ExportFormat, ExportQuality, GifFrameRate, GifSizePreset } from "@ import { GIF_FRAME_RATES, GIF_SIZE_PRESETS } from "@/lib/exporter"; import { cn } from "@/lib/utils"; import { type AspectRatio } from "@/utils/aspectRatioUtils"; +import { getTestId } from "@/utils/getTestId"; import { AnnotationSettingsPanel } from "./AnnotationSettingsPanel"; import { CropControl } from "./CropControl"; import { KeyboardShortcutsHelp } from "./KeyboardShortcutsHelp"; @@ -968,6 +969,7 @@ export function SettingsPanel({ MP4