✨ Support test packaging process
This commit is contained in:
448
tests/e2e.test.js
Normal file
448
tests/e2e.test.js
Normal file
@@ -0,0 +1,448 @@
|
||||
#!/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);
|
||||
}
|
||||
@@ -4,12 +4,16 @@
|
||||
* Main Test Runner for Pake CLI
|
||||
*
|
||||
* This is the entry point for running all tests.
|
||||
* Usage: node tests/index.js [--unit] [--integration] [--manual]
|
||||
* Usage: node tests/index.js [--unit] [--integration] [--builder] [--e2e] [--full]
|
||||
*
|
||||
* By default, runs all tests including E2E packaging tests.
|
||||
* Use specific flags to run only certain test suites.
|
||||
*/
|
||||
|
||||
import cliTestRunner from "./cli.test.js";
|
||||
import integrationTestRunner from "./integration.test.js";
|
||||
import builderTestRunner from "./builder.test.js";
|
||||
import e2eTestRunner from "./e2e.test.js";
|
||||
import { execSync } from "child_process";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
@@ -23,6 +27,8 @@ const args = process.argv.slice(2);
|
||||
const runUnit = args.length === 0 || args.includes("--unit");
|
||||
const runIntegration = args.length === 0 || args.includes("--integration");
|
||||
const runBuilder = args.length === 0 || args.includes("--builder");
|
||||
const runE2E =
|
||||
args.length === 0 || args.includes("--e2e") || args.includes("--full");
|
||||
|
||||
async function runAllTests() {
|
||||
console.log("🚀 Pake CLI Test Suite");
|
||||
@@ -55,6 +61,14 @@ async function runAllTests() {
|
||||
console.log("");
|
||||
}
|
||||
|
||||
if (runE2E) {
|
||||
console.log("🚀 Running End-to-End Tests...\n");
|
||||
await e2eTestRunner.runAll();
|
||||
totalPassed += e2eTestRunner.results.filter((r) => r.passed).length;
|
||||
totalTests += e2eTestRunner.results.length;
|
||||
console.log("");
|
||||
}
|
||||
|
||||
// Final summary
|
||||
console.log("🎯 Overall Test Summary");
|
||||
console.log("=======================");
|
||||
|
||||
Reference in New Issue
Block a user