From ea68300634566ae6f01d670ec6f4f2c4f45334b1 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Mon, 16 Mar 2026 13:01:32 +0100 Subject: [PATCH 1/7] fix: read local export sources via electron ipc --- electron/electron-env.d.ts | 7 +++++++ electron/ipc/handlers.ts | 23 +++++++++++++++++++++ electron/preload.ts | 3 +++ src/lib/exporter/streamingDecoder.ts | 30 ++++++++++++++++++++++++++-- 4 files changed, 61 insertions(+), 2 deletions(-) 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/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/src/lib/exporter/streamingDecoder.ts b/src/lib/exporter/streamingDecoder.ts index 2d0a5a3e..7ebd78dc 100644 --- a/src/lib/exporter/streamingDecoder.ts +++ b/src/lib/exporter/streamingDecoder.ts @@ -32,11 +32,37 @@ export class StreamingVideoDecoder { private cancelled = false; private metadata: DecodedVideoInfo | null = null; - async loadMetadata(videoUrl: string): Promise { + private async loadSourceFile(videoUrl: string): Promise<{ file: File; blob: Blob }> { + const isRemoteUrl = /^(https?:|blob:|data:)/i.test(videoUrl); + + if (!isRemoteUrl && window.electronAPI?.readBinaryFile) { + const result = await window.electronAPI.readBinaryFile(videoUrl); + if (!result.success || !result.data) { + throw new Error(result.message || result.error || "Failed to read source video"); + } + + const filename = (result.path || videoUrl).split(/[\\/]/).pop() || "video"; + const blob = new Blob([result.data]); + return { + blob, + file: new File([blob], filename, { type: blob.type || "application/octet-stream" }), + }; + } + const response = await fetch(videoUrl); + if (!response.ok) { + throw new Error(`Failed to fetch source video: ${response.status} ${response.statusText}`); + } const blob = await response.blob(); const filename = videoUrl.split("/").pop() || "video"; - const file = new File([blob], filename, { type: blob.type }); + return { + blob, + file: new File([blob], filename, { type: blob.type }), + }; + } + + async loadMetadata(videoUrl: string): Promise { + const { file } = await this.loadSourceFile(videoUrl); // Relative URL so it resolves correctly in both dev (http) and packaged (file://) builds const wasmUrl = new URL("./wasm/web-demuxer.wasm", window.location.href).href; From 9f6ef0f582bd55e330e9b81c306237726beb5428 Mon Sep 17 00:00:00 2001 From: Marc Diaz Date: Mon, 16 Mar 2026 11:17:26 -0400 Subject: [PATCH 2/7] feat(test): add an e2e test --- .github/workflows/ci.yml | 20 ++ .gitignore | 6 +- electron/windows.ts | 3 + package-lock.json | 176 ++++++++++++++---- package.json | 6 + playwright.config.ts | 8 + src/components/video-editor/SettingsPanel.tsx | 4 + src/utils/getTestId.ts | 5 + tests/e2e/gif-export.spec.ts | 116 ++++++++++++ tests/fixtures/sample.webm | Bin 0 -> 23869 bytes 10 files changed, 307 insertions(+), 37 deletions(-) create mode 100644 playwright.config.ts create mode 100644 src/utils/getTestId.ts create mode 100644 tests/e2e/gif-export.spec.ts create mode 100644 tests/fixtures/sample.webm diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2b04dbe..03865e11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,3 +42,23 @@ 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 + - run: npm run build-vite + - run: 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/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 d9edd82e..9be61f85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "openscreen", "version": "1.2.0", "dependencies": { + "@ffmpeg/core": "^0.12.10", + "@ffmpeg/ffmpeg": "^0.12.15", + "@ffmpeg/util": "^0.12.2", "@fix-webm-duration/fix": "^1.0.1", "@pixi/filter-drop-shadow": "^5.2.0", "@radix-ui/react-accordion": "^1.2.12", @@ -50,6 +53,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", @@ -116,7 +120,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -345,7 +348,6 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -1320,6 +1322,7 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, + "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1341,6 +1344,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1357,6 +1361,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -1371,6 +1376,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 10.0.0" } @@ -1817,6 +1823,41 @@ "node": ">=12" } }, + "node_modules/@ffmpeg/core": { + "version": "0.12.10", + "resolved": "https://registry.npmjs.org/@ffmpeg/core/-/core-0.12.10.tgz", + "integrity": "sha512-dzNplnn2Nxle2c2i2rrDhqcB19q9cglCkWnoMTDN9Q9l3PvdjZWd1HfSPjCNWc/p8Q3CT+Es9fWOR0UhAeYQZA==", + "engines": { + "node": ">=16.x" + } + }, + "node_modules/@ffmpeg/ffmpeg": { + "version": "0.12.15", + "resolved": "https://registry.npmjs.org/@ffmpeg/ffmpeg/-/ffmpeg-0.12.15.tgz", + "integrity": "sha512-1C8Obr4GsN3xw+/1Ww6PFM84wSQAGsdoTuTWPOj2OizsRDLT4CXTaVjPhkw6ARyDus1B9X/L2LiXHqYYsGnRFw==", + "dependencies": { + "@ffmpeg/types": "^0.12.4" + }, + "engines": { + "node": ">=18.x" + } + }, + "node_modules/@ffmpeg/types": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/@ffmpeg/types/-/types-0.12.4.tgz", + "integrity": "sha512-k9vJQNBGTxE5AhYDtOYR5rO5fKsspbg51gbcwtbkw2lCdoIILzklulcjJfIDwrtn7XhDeF2M+THwJ2FGrLeV6A==", + "engines": { + "node": ">=16.x" + } + }, + "node_modules/@ffmpeg/util": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@ffmpeg/util/-/util-0.12.2.tgz", + "integrity": "sha512-ouyoW+4JB7WxjeZ2y6KpRvB+dLp7Cp4ro8z0HIVpZVCM7AwFlHa0c4R8Y/a4M3wMqATpYKhC7lSFHQ0T11MEDw==", + "engines": { + "node": ">=18.x" + } + }, "node_modules/@fix-webm-duration/fix": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@fix-webm-duration/fix/-/fix-1.0.1.tgz", @@ -2054,7 +2095,6 @@ "integrity": "sha512-LTATglVUPGkPf15zX1wTMlZ0+AU7cGEGF6ekVF1crA8eHUWsGjrYTB+Ht4E3HTrCok8weQG+K01rJndCp/l4XA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.7.2", "@jimp/core": "^0.16.13" @@ -2097,7 +2137,6 @@ "integrity": "sha512-8Z1k96ZFxlhK2bgrY1JNWNwvaBeI/bciLM0yDOni2+aZwfIIiC7Y6PeWHTAvjHNjphz+XCt01WQmOYWCn0ML6g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.7.2", "@jimp/utils": "^0.16.13" @@ -2112,7 +2151,6 @@ "integrity": "sha512-PvLrfa8vkej3qinlebyhLpksJgCF5aiysDMSVhOZqwH5nQLLtDE9WYbnsofGw4r0VVpyw3H/ANCIzYTyCtP9Cg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.7.2", "@jimp/utils": "^0.16.13" @@ -2141,7 +2179,6 @@ "integrity": "sha512-xW+9BtEvoIkkH/Wde9ql4nAFbYLkVINhpgAE7VcBUsuuB34WUbcBl/taOuUYQrPEFQJ4jfXiAJZ2H/rvKjCVnQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.7.2", "@jimp/utils": "^0.16.13", @@ -2191,7 +2228,6 @@ "integrity": "sha512-WEl2tPVYwzYL8OKme6Go2xqiWgKsgxlMwyHabdAU4tXaRwOCnOI7v4021gCcBb9zn/oWwguHuKHmK30Fw2Z/PA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.7.2", "@jimp/utils": "^0.16.13" @@ -2335,7 +2371,6 @@ "integrity": "sha512-qoqtN8LDknm3fJm9nuPygJv30O3vGhSBD2TxrsCnhtOsxKAqVPJtFVdGd/qVuZ8nqQANQmTlfqTiK9mVWQ7MiQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.7.2", "@jimp/utils": "^0.16.13" @@ -2350,7 +2385,6 @@ "integrity": "sha512-Ev+Jjmj1nHYw897z9C3R9dYsPv7S2/nxdgfFb/h8hOwK0Ovd1k/+yYS46A0uj/JCKK0pQk8wOslYBkPwdnLorw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.7.2", "@jimp/utils": "^0.16.13" @@ -2368,7 +2402,6 @@ "integrity": "sha512-05POQaEJVucjTiSGMoH68ZiELc7QqpIpuQlZ2JBbhCV+WCbPFUBcGSmE7w4Jd0E2GvCho/NoMODLwgcVGQA97A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.7.2", "@jimp/utils": "^0.16.13" @@ -2795,6 +2828,7 @@ "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.4.3.tgz", "integrity": "sha512-a6R+bXKeXMDcRmjYQoBIK+v2EYqxSX49wcjAY579EYM/WrFKS98nSees6lqVUcLKrcQh2DT9srJHX7XMny3voQ==", "license": "MIT", + "peer": true, "dependencies": { "@pixi/colord": "^2.9.6" } @@ -2809,7 +2843,8 @@ "version": "7.4.3", "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.4.3.tgz", "integrity": "sha512-QGmwJUNQy/vVEHzL6VGQvnwawLZ1wceZMI8HwJAT4/I2uAzbBeFDdmCS8WsTpSWLZjF/DszDc1D8BFp4pVJ5UQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@pixi/core": { "version": "7.4.3", @@ -2836,7 +2871,8 @@ "version": "7.4.3", "resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.4.3.tgz", "integrity": "sha512-FhoiYkHQEDYHUE7wXhqfsTRz6KxLXjuMbSiAwnLb9uG1vAgp6q6qd6HEsf4X30YaZbLFY8a4KY6hFZWjF+4Fdw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@pixi/filter-drop-shadow": { "version": "5.2.0", @@ -2863,19 +2899,22 @@ "version": "7.4.3", "resolved": "https://registry.npmjs.org/@pixi/math/-/math-7.4.3.tgz", "integrity": "sha512-/uJOVhR2DOZ+zgdI6Bs/CwcXT4bNRKsS+TqX3ekRIxPCwaLra+Qdm7aDxT5cTToDzdxbKL5+rwiLu3Y1egILDw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@pixi/runner": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-7.4.3.tgz", "integrity": "sha512-TJyfp7y23u5vvRAyYhVSa7ytq0PdKSvPLXu4G3meoFh1oxTLHH6g/RIzLuxUAThPG2z7ftthuW3qWq6dRV+dhw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@pixi/settings": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-7.4.3.tgz", "integrity": "sha512-SmGK8smc0PxRB9nr0UJioEtE9hl4gvj9OedCvZx3bxBwA3omA5BmP3CyhQfN8XJ29+o2OUL01r3zAPVol4l4lA==", "license": "MIT", + "peer": true, "dependencies": { "@pixi/constants": "7.4.3", "@types/css-font-loading-module": "^0.0.12", @@ -2887,6 +2926,7 @@ "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.4.3.tgz", "integrity": "sha512-tHsAD0iOUb6QSGGw+c8cyRBvxsq/NlfzIFBZLEHhWZ+Bx4a0MmXup6I/yJDGmyPCYE+ctCcAfY13wKAzdiVFgQ==", "license": "MIT", + "peer": true, "dependencies": { "@pixi/extensions": "7.4.3", "@pixi/settings": "7.4.3", @@ -2898,6 +2938,7 @@ "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-7.4.3.tgz", "integrity": "sha512-NO3Y9HAn2UKS1YdxffqsPp+kDpVm8XWvkZcS/E+rBzY9VTLnNOI7cawSRm+dacdET3a8Jad3aDKEDZ0HmAqAFA==", "license": "MIT", + "peer": true, "dependencies": { "@pixi/color": "7.4.3", "@pixi/constants": "7.4.3", @@ -2912,19 +2953,22 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.4.tgz", "integrity": "sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@pixi/utils/node_modules/earcut": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/@pixi/utils/node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", @@ -2936,6 +2980,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", @@ -4439,7 +4498,6 @@ "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4451,7 +4509,6 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -4760,7 +4817,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5581,7 +5637,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -5894,6 +5949,7 @@ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", + "peer": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" @@ -6343,7 +6399,8 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -6639,7 +6696,6 @@ "integrity": "sha512-uOOBA3f+kW3o4KpSoMQ6SNpdXU7WtxlJRb9vCZgOvqhTz4b3GjcoWKstdisizNZLsylhTMv8TLHFPFW0Uxsj/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "app-builder-lib": "26.7.0", "builder-util": "26.4.1", @@ -7066,6 +7122,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -7086,6 +7143,7 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -8768,7 +8826,6 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -10331,6 +10388,7 @@ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4" }, @@ -10869,6 +10927,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", @@ -10920,7 +11022,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11065,6 +11166,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -11082,6 +11184,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -11230,6 +11333,7 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "side-channel": "^1.1.0" }, @@ -11288,7 +11392,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -11301,7 +11404,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -12092,6 +12194,7 @@ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", + "peer": true, "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", @@ -12111,6 +12214,7 @@ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", + "peer": true, "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" @@ -12127,6 +12231,7 @@ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "license": "MIT", + "peer": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -12145,6 +12250,7 @@ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", + "peer": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -12792,7 +12898,6 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -12865,6 +12970,7 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -12928,6 +13034,7 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -12942,6 +13049,7 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -12955,7 +13063,6 @@ "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -13108,7 +13215,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13342,6 +13448,7 @@ "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", "license": "MIT", + "peer": true, "dependencies": { "punycode": "^1.4.1", "qs": "^6.12.3" @@ -13354,7 +13461,8 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/use-callback-ref": { "version": "1.3.3", @@ -13468,7 +13576,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -13543,8 +13650,7 @@ "resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz", "integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/vitest": { "version": "4.0.16", @@ -14108,7 +14214,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14122,7 +14227,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/package.json b/package.json index ef2c1975..561116b7 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,14 @@ "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": { + "@ffmpeg/core": "^0.12.10", + "@ffmpeg/ffmpeg": "^0.12.15", + "@ffmpeg/util": "^0.12.2", "@fix-webm-duration/fix": "^1.0.1", "@pixi/filter-drop-shadow": "^5.2.0", "@radix-ui/react-accordion": "^1.2.12", @@ -60,6 +65,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 dfe3f501..00dbeca6 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -31,6 +31,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"; @@ -775,6 +776,7 @@ export function SettingsPanel({ MP4