117 lines
4.5 KiB
TypeScript
117 lines
4.5 KiB
TypeScript
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";
|
|
|
|
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");
|
|
|
|
test("exports a GIF from a loaded video", async () => {
|
|
const outputPath = path.join(os.tmpdir(), `test-gif-export-${Date.now()}.gif`);
|
|
|
|
const app = await electron.launch({
|
|
args: [MAIN_JS],
|
|
env: {
|
|
...process.env,
|
|
// Set HEADLESS=false to show windows while debugging.
|
|
HEADLESS: process.env["HEADLESS"] ?? "true",
|
|
},
|
|
});
|
|
|
|
// Print all main-process stdout/stderr so failures are diagnosable.
|
|
app.process().stdout?.on("data", (d) => process.stdout.write(`[electron] ${d}`));
|
|
app.process().stderr?.on("data", (d) => process.stderr.write(`[electron] ${d}`));
|
|
|
|
try {
|
|
// ── 1. Wait for the HUD overlay window. The window is created after
|
|
// registerIpcHandlers() completes, so all IPC handlers are live
|
|
// by the time firstWindow() resolves.
|
|
const hudWindow = await app.firstWindow({ timeout: 60_000 });
|
|
await hudWindow.waitForLoadState("domcontentloaded");
|
|
|
|
// ── 2. Intercept the native save dialog in the main process.
|
|
// Must happen after firstWindow() so registerIpcHandlers() has
|
|
// already registered its version — otherwise our early handle()
|
|
// call causes registerIpcHandlers() to throw and abort, leaving
|
|
// other handlers (like set-current-video-path) never registered.
|
|
// Store the exported buffer as a base64 global in the main process.
|
|
// We can't use require() or import() inside app.evaluate() because the
|
|
// main process is ESM and Playwright runs the callback via eval(), which
|
|
// has no dynamic-import hook. We retrieve and write the file below after
|
|
// the export finishes.
|
|
await app.evaluate(({ ipcMain }) => {
|
|
ipcMain.removeHandler("save-exported-video");
|
|
ipcMain.handle(
|
|
"save-exported-video",
|
|
(_event: Electron.IpcMainInvokeEvent, buffer: ArrayBuffer) => {
|
|
(globalThis as Record<string, unknown>)["__testExportData"] =
|
|
Buffer.from(buffer).toString("base64");
|
|
return { success: true, path: "pending" };
|
|
},
|
|
);
|
|
});
|
|
|
|
await hudWindow.evaluate((videoPath: string) => {
|
|
window.electronAPI.setCurrentVideoPath(videoPath);
|
|
try {
|
|
window.electronAPI.switchToEditor();
|
|
} catch {
|
|
// Expected: HUD window closes during this call, killing the context.
|
|
}
|
|
}, TEST_VIDEO);
|
|
|
|
// ── 3. Switch to the editor window. This closes the HUD and opens
|
|
// a new BrowserWindow with ?windowType=editor.
|
|
const editorWindow = await app.waitForEvent("window", {
|
|
predicate: (w) => w.url().includes("windowType=editor"),
|
|
timeout: 15_000,
|
|
});
|
|
|
|
// WebCodecs (VideoEncoder) may not be registered in the renderer on first
|
|
// load of a second BrowserWindow. A single reload ensures the feature is
|
|
// fully initialized before we start encoding.
|
|
await editorWindow.reload();
|
|
await editorWindow.waitForLoadState("domcontentloaded");
|
|
await expect(editorWindow.getByText("Loading video...")).not.toBeVisible({
|
|
timeout: 15_000,
|
|
});
|
|
|
|
// ── 5. Select GIF as the export format.
|
|
await editorWindow.getByTestId("testId-gif-format-button").click();
|
|
await editorWindow.getByTestId("testId-export-button").click();
|
|
|
|
// ── 6. Wait for the toast to say exported successfully
|
|
await expect(editorWindow.getByText(`GIF exported successfully to pending`)).toBeVisible({
|
|
timeout: 90_000,
|
|
});
|
|
|
|
// ── 7. Write the captured buffer from the main-process global to disk.
|
|
const base64 = await app.evaluate(
|
|
() => (globalThis as Record<string, unknown>)["__testExportData"] as string,
|
|
);
|
|
fs.writeFileSync(outputPath, Buffer.from(base64, "base64"));
|
|
|
|
// ── 8. Verify the file on disk is a valid GIF.
|
|
expect(fs.existsSync(outputPath), `GIF not found at ${outputPath}`).toBe(true);
|
|
|
|
const header = Buffer.alloc(6);
|
|
const fd = fs.openSync(outputPath, "r");
|
|
fs.readSync(fd, header, 0, 6, 0);
|
|
fs.closeSync(fd);
|
|
|
|
// GIF magic bytes are either "GIF87a" or "GIF89a"
|
|
expect(header.toString("ascii")).toMatch(/^GIF8[79]a/);
|
|
|
|
const stats = fs.statSync(outputPath);
|
|
expect(stats.size).toBeGreaterThan(1024); // at least 1 KB
|
|
} finally {
|
|
await app.close();
|
|
if (fs.existsSync(outputPath)) {
|
|
fs.unlinkSync(outputPath);
|
|
}
|
|
}
|
|
});
|