#!/usr/bin/env node /** * End-to-End (E2E) Tests for Pake CLI * * These tests perform complete packaging operations and verify * the entire build pipeline works correctly. They include cleanup * to avoid leaving artifacts on the system. */ import { spawn, execSync } from "child_process"; import fs from "fs"; import path from "path"; import config, { TIMEOUTS, TEST_URLS, TEST_NAMES } from "./test.config.js"; import ora from "ora"; class E2ETestRunner { constructor() { this.tests = []; this.results = []; this.generatedFiles = []; this.tempDirectories = []; } addTest(name, testFn, timeout = TIMEOUTS.LONG) { this.tests.push({ name, testFn, timeout }); } async runAll() { console.log("šŸš€ End-to-End Tests"); console.log("===================\n"); try { for (const [index, test] of this.tests.entries()) { const spinner = ora(`Running ${test.name}...`).start(); try { const result = await Promise.race([ test.testFn(), new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), test.timeout), ), ]); if (result) { spinner.succeed(`${index + 1}. ${test.name}: PASS`); this.results.push({ name: test.name, passed: true }); } else { spinner.fail(`${index + 1}. ${test.name}: FAIL`); this.results.push({ name: test.name, passed: false }); } } catch (error) { spinner.fail(`${index + 1}. ${test.name}: ERROR - ${error.message}`); this.results.push({ name: test.name, passed: false, error: error.message, }); } } } finally { // Always cleanup generated files await this.cleanup(); } this.showSummary(); } async cleanup() { const cleanupSpinner = ora("Cleaning up generated files...").start(); try { // Remove generated app files for (const file of this.generatedFiles) { if (fs.existsSync(file)) { if (fs.statSync(file).isDirectory()) { fs.rmSync(file, { recursive: true, force: true }); } else { fs.unlinkSync(file); } } } // Remove temporary directories for (const dir of this.tempDirectories) { if (fs.existsSync(dir)) { fs.rmSync(dir, { recursive: true, force: true }); } } // Clean up .pake directory if it's a test run const pakeDir = path.join(config.PROJECT_ROOT, "src-tauri", ".pake"); if (fs.existsSync(pakeDir)) { fs.rmSync(pakeDir, { recursive: true, force: true }); } // Remove any test app files in current directory const projectFiles = fs.readdirSync(config.PROJECT_ROOT); const testFilePatterns = ["Weekly", "TestApp", "E2ETest"]; const allTestFiles = projectFiles.filter( (file) => testFilePatterns.some((pattern) => file.startsWith(pattern)) && (file.endsWith(".app") || file.endsWith(".exe") || file.endsWith(".deb") || file.endsWith(".dmg")), ); for (const file of allTestFiles) { const fullPath = path.join(config.PROJECT_ROOT, file); if (fs.existsSync(fullPath)) { if (fs.statSync(fullPath).isDirectory()) { fs.rmSync(fullPath, { recursive: true, force: true }); } else { fs.unlinkSync(fullPath); } } } // Remove test-generated icon files (only specific test-generated files) const iconsDir = path.join(config.PROJECT_ROOT, "src-tauri", "icons"); if (fs.existsSync(iconsDir)) { const iconFiles = fs.readdirSync(iconsDir); const testIconFiles = iconFiles.filter( (file) => (file.toLowerCase().startsWith("e2etest") || file.toLowerCase().startsWith("testapp") || file.toLowerCase() === "test.icns") && file.endsWith(".icns"), ); for (const file of testIconFiles) { const fullPath = path.join(iconsDir, file); if (fs.existsSync(fullPath)) { fs.unlinkSync(fullPath); } } } cleanupSpinner.succeed("Cleanup completed"); } catch (error) { cleanupSpinner.fail(`Cleanup failed: ${error.message}`); } } showSummary() { const passed = this.results.filter((r) => r.passed).length; const total = this.results.length; console.log(`\nšŸ“Š E2E Test Results: ${passed}/${total} passed`); if (passed === total) { console.log("šŸŽ‰ All E2E tests passed!"); } else { console.log("āŒ Some E2E tests failed"); this.results .filter((r) => !r.passed) .forEach((r) => console.log(` - ${r.name}: ${r.error || "FAIL"}`)); } } registerFile(filePath) { this.generatedFiles.push(filePath); } registerDirectory(dirPath) { this.tempDirectories.push(dirPath); } } // Create test runner instance const runner = new E2ETestRunner(); // Add tests runner.addTest( "Weekly App Full Build (Debug Mode)", async () => { return new Promise((resolve, reject) => { const testName = "E2ETestWeekly"; const command = `node "${config.CLI_PATH}" "${TEST_URLS.WEEKLY}" --name "${testName}" --debug`; const child = spawn(command, { shell: true, cwd: config.PROJECT_ROOT, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, PAKE_E2E_TEST: "1", PAKE_CREATE_APP: "1", }, }); let stdout = ""; let stderr = ""; child.stdout.on("data", (data) => { stdout += data.toString(); }); child.stderr.on("data", (data) => { stderr += data.toString(); }); child.on("close", (code) => { // Register potential output files for cleanup (app bundles only for E2E tests) const appFile = path.join(config.PROJECT_ROOT, `${testName}.app`); runner.registerFile(appFile); // Check if build got to the bundling stage (indicates successful compile) const buildingApp = stdout.includes("Bundling") || stdout.includes("Built application at:"); const compilationSuccess = stdout.includes("Finished") && stdout.includes("target(s)"); // For E2E tests, we care more about compilation success than packaging if (buildingApp || compilationSuccess) { resolve(true); return; } // Check actual file existence as fallback const outputExists = fs.existsSync(appFile); if (outputExists) { resolve(true); } else if (code === 0) { reject( new Error("Build completed but no clear success indicators found"), ); } else { // Only fail if it's a genuine compilation error if (stderr.includes("error[E") || stderr.includes("cannot find")) { reject(new Error(`Compilation failed: ${stderr.slice(0, 200)}...`)); } else { // Packaging error - check if compilation was successful resolve(compilationSuccess); } } }); child.on("error", (error) => { reject(new Error(`Process error: ${error.message}`)); }); // Send empty input to handle any prompts child.stdin.end(); }); }, TIMEOUTS.LONG, ); runner.addTest( "Weekly App Quick Build Verification", async () => { return new Promise((resolve, reject) => { const testName = "E2ETestQuick"; const command = `node "${config.CLI_PATH}" "${TEST_URLS.WEEKLY}" --name "${testName}" --debug --width 800 --height 600`; const child = spawn(command, { shell: true, cwd: config.PROJECT_ROOT, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, PAKE_E2E_TEST: "1", PAKE_CREATE_APP: "1", }, }); let buildStarted = false; child.stdout.on("data", (data) => { const output = data.toString(); if ( output.includes("Building app") || output.includes("Compiling") || output.includes("Installing package") || output.includes("Bundling") ) { buildStarted = true; } }); child.stderr.on("data", (data) => { const output = data.toString(); // Check for build progress in stderr (Tauri outputs to stderr) if ( output.includes("Building app") || output.includes("Compiling") || output.includes("Installing package") || output.includes("Bundling") || output.includes("Finished") || output.includes("Built application at:") ) { buildStarted = true; } // Only log actual errors, ignore build progress and warnings if ( !output.includes("warning:") && !output.includes("verbose") && !output.includes("npm info") && !output.includes("Installing package") && !output.includes("Package installed") && !output.includes("Building app") && !output.includes("Compiling") && !output.includes("Finished") && !output.includes("Built application at:") && !output.includes("Bundling") && !output.includes("npm http") && output.trim().length > 0 ) { console.log(`Build error: ${output}`); } }); // Kill process after 30 seconds if build started successfully const timeout = setTimeout(() => { child.kill("SIGTERM"); // Register cleanup files (app bundle only for E2E tests) const appFile = path.join(config.PROJECT_ROOT, `${testName}.app`); runner.registerFile(appFile); if (buildStarted) { resolve(true); } else { reject(new Error("Build did not start within timeout")); } }, 30000); child.on("close", (code) => { clearTimeout(timeout); const appFile = path.join(config.PROJECT_ROOT, `${testName}.app`); runner.registerFile(appFile); if (buildStarted) { resolve(true); } else { reject(new Error("Build process ended before starting")); } }); child.on("error", (error) => { reject(new Error(`Process error: ${error.message}`)); }); child.stdin.end(); }); }, 35000, // 35 seconds timeout ); runner.addTest( "Config File System Verification", async () => { const testName = "E2ETestConfig"; const pakeDir = path.join(config.PROJECT_ROOT, "src-tauri", ".pake"); return new Promise((resolve, reject) => { const command = `node "${config.CLI_PATH}" "${TEST_URLS.VALID}" --name "${testName}" --debug`; const child = spawn(command, { shell: true, cwd: config.PROJECT_ROOT, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, PAKE_E2E_TEST: "1", PAKE_CREATE_APP: "1", }, }); const checkConfigFiles = () => { if (fs.existsSync(pakeDir)) { // Verify config files exist in .pake directory const configFile = path.join(pakeDir, "tauri.conf.json"); const pakeConfigFile = path.join(pakeDir, "pake.json"); if (fs.existsSync(configFile) && fs.existsSync(pakeConfigFile)) { // Verify config contains our test app name try { const config = JSON.parse(fs.readFileSync(configFile, "utf8")); if (config.productName === testName) { child.kill("SIGTERM"); // Register cleanup runner.registerDirectory(pakeDir); resolve(true); return true; } } catch (error) { // Continue if config parsing fails } } } return false; }; child.stdout.on("data", (data) => { const output = data.toString(); // Check if .pake directory is created if ( output.includes("Installing package") || output.includes("Building app") ) { checkConfigFiles(); } }); child.stderr.on("data", (data) => { const output = data.toString(); // Check stderr for build progress and config creation if ( output.includes("Installing package") || output.includes("Building app") || output.includes("Package installed") ) { checkConfigFiles(); } }); // Timeout after 15 seconds setTimeout(() => { child.kill("SIGTERM"); runner.registerDirectory(pakeDir); reject(new Error("Config verification timeout")); }, 15000); child.on("error", (error) => { reject(new Error(`Process error: ${error.message}`)); }); child.stdin.end(); }); }, 20000, ); export default runner; // Run if called directly if (import.meta.url === `file://${process.argv[1]}`) { runner.runAll().catch(console.error); }