Support test packaging process

This commit is contained in:
Tw93
2025-08-14 20:51:42 +08:00
parent b2072b5e80
commit f76d567895
10 changed files with 640 additions and 29 deletions

4
bin/README.md vendored
View File

@@ -47,6 +47,8 @@ pake [url] [options]
The packaged application will be located in the current working directory by default. The first packaging might take some time due to environment configuration. Please be patient.
> **macOS Output**: On macOS, Pake creates DMG installers by default. To create `.app` bundles for testing (to avoid user interaction), set the environment variable `PAKE_CREATE_APP=1`.
>
> **Note**: Packaging requires the Rust environment. If Rust is not installed, you will be prompted for installation confirmation. In case of installation failure or timeout, you can [install it manually](https://www.rust-lang.org/tools/install).
### [url]
@@ -279,7 +281,7 @@ Supports both comma-separated and multiple option formats:
#### [proxy-url]
Set proxy server for all network requests. Supports HTTP, HTTPS, and SOCKS5.
Set proxy server for all network requests. Supports HTTP, HTTPS, and SOCKS5. Available on Windows and Linux. On macOS, requires macOS 14+.
```shell
--proxy-url http://127.0.0.1:7890

4
bin/README_CN.md vendored
View File

@@ -47,6 +47,8 @@ pake [url] [options]
应用程序的打包结果将默认保存在当前工作目录。由于首次打包需要配置环境,这可能需要一些时间,请耐心等待。
> **macOS 输出**:在 macOS 上Pake 默认创建 DMG 安装程序。如需创建 `.app` 包进行测试(避免用户交互),请设置环境变量 `PAKE_CREATE_APP=1`。
>
> **注意**:打包过程需要使用 `Rust` 环境。如果您没有安装 `Rust`,系统会提示您是否要安装。如果遇到安装失败或超时的问题,您可以 [手动安装](https://www.rust-lang.org/tools/install)。
### [url]
@@ -281,7 +283,7 @@ pake [url] [options]
#### [proxy-url]
为所有网络请求设置代理服务器。支持 HTTP、HTTPS 和 SOCKS5。
为所有网络请求设置代理服务器。支持 HTTP、HTTPS 和 SOCKS5。在 Windows 和 Linux 上可用。在 macOS 上需要 macOS 14+。
```shell
--proxy-url http://127.0.0.1:7890

View File

@@ -118,8 +118,44 @@ export default abstract class BaseBuilder {
abstract getFileName(): string;
protected getBuildCommand(): string {
// the debug option should support `--debug` and `--release`
return this.options.debug ? 'npm run build:debug' : 'npm run build';
const baseCommand = this.options.debug
? 'npm run build:debug'
: 'npm run build';
// Use temporary config directory to avoid modifying source files
const configPath = path.join(
npmDirectory,
'src-tauri',
'.pake',
'tauri.conf.json',
);
let fullCommand = `${baseCommand} -- -c "${configPath}"`;
// For macOS, use app bundles by default unless DMG is explicitly requested
if (IS_MAC && this.options.targets === 'app') {
fullCommand += ' --bundles app';
}
// Add macos-proxy feature for modern macOS (Darwin 23+ = macOS 14+)
if (IS_MAC) {
const macOSVersion = this.getMacOSMajorVersion();
if (macOSVersion >= 23) {
fullCommand += ' --features macos-proxy';
}
}
return fullCommand;
}
private getMacOSMajorVersion(): number {
try {
const os = require('os');
const release = os.release();
const majorVersion = parseInt(release.split('.')[0], 10);
return majorVersion;
} catch (error) {
return 0; // Disable proxy feature if version detection fails
}
}
protected getBasePath(): string {
@@ -132,10 +168,13 @@ export default abstract class BaseBuilder {
fileName: string,
fileType: string,
): string {
// For app bundles on macOS, the directory is 'macos', not 'app'
const bundleDir =
fileType.toLowerCase() === 'app' ? 'macos' : fileType.toLowerCase();
return path.join(
npmDirectory,
this.getBasePath(),
fileType.toLowerCase(),
bundleDir,
`${fileName}.${fileType}`,
);
}

View File

@@ -5,11 +5,24 @@ import BaseBuilder from './BaseBuilder';
export default class MacBuilder extends BaseBuilder {
constructor(options: PakeAppOptions) {
super(options);
this.options.targets = 'dmg';
// Use DMG by default for distribution
// Only create app bundles for testing to avoid user interaction
if (process.env.PAKE_CREATE_APP === '1') {
this.options.targets = 'app';
} else {
this.options.targets = 'dmg';
}
}
getFileName(): string {
const { name } = this.options;
// For app bundles, use simple name without version/arch
if (this.options.targets === 'app') {
return name;
}
// For DMG files, use versioned filename
let arch: string;
if (this.options.multiArch) {
arch = 'universal';

29
bin/helpers/merge.ts vendored
View File

@@ -1,17 +1,42 @@
import path from 'path';
import fsExtra from 'fs-extra';
import { npmDirectory } from '@/utils/dir';
import combineFiles from '@/utils/combine';
import logger from '@/options/logger';
import { PakeAppOptions, PlatformMap } from '@/types';
import { tauriConfigDirectory } from '@/utils/dir';
import { tauriConfigDirectory, npmDirectory } from '@/utils/dir';
export async function mergeConfig(
url: string,
options: PakeAppOptions,
tauriConf: any,
) {
// Ensure .pake directory exists and copy source templates if needed
const srcTauriDir = path.join(npmDirectory, 'src-tauri');
await fsExtra.ensureDir(tauriConfigDirectory);
// Copy source config files to .pake directory (as templates)
const sourceFiles = [
'tauri.conf.json',
'tauri.macos.conf.json',
'tauri.windows.conf.json',
'tauri.linux.conf.json',
'pake.json',
];
await Promise.all(
sourceFiles.map(async (file) => {
const sourcePath = path.join(srcTauriDir, file);
const destPath = path.join(tauriConfigDirectory, file);
if (
(await fsExtra.pathExists(sourcePath)) &&
!(await fsExtra.pathExists(destPath))
) {
await fsExtra.copy(sourcePath, destPath);
}
}),
);
const {
width,
height,

9
bin/utils/dir.ts vendored
View File

@@ -7,7 +7,8 @@ const currentModulePath = fileURLToPath(import.meta.url);
// Resolve the parent directory of the current module
export const npmDirectory = path.join(path.dirname(currentModulePath), '..');
export const tauriConfigDirectory =
process.env.NODE_ENV === 'development'
? path.join(npmDirectory, 'src-tauri', '.pake')
: path.join(npmDirectory, 'src-tauri');
export const tauriConfigDirectory = path.join(
npmDirectory,
'src-tauri',
'.pake',
);

95
dist/cli.js vendored
View File

@@ -50,7 +50,7 @@ var files = [
var scripts = {
start: "npm run dev",
dev: "npm run tauri dev",
build: "npm run tauri build --release",
build: "npm run tauri build --",
"build:debug": "npm run tauri build -- --debug",
"build:mac": "npm run tauri build -- --target universal-apple-darwin",
"build:config": "chmod +x script/app_config.mjs && node script/app_config.mjs",
@@ -59,8 +59,10 @@ var scripts = {
cli: "rollup -c rollup.config.js --watch",
"cli:dev": "cross-env NODE_ENV=development rollup -c rollup.config.js -w",
"cli:build": "cross-env NODE_ENV=production rollup -c rollup.config.js",
test: "npm run cli:build && node tests/index.js",
test: "npm run cli:build && PAKE_CREATE_APP=1 node tests/index.js",
format: "npx prettier --write . --ignore-unknown && cd src-tauri && cargo fmt --verbose",
"hooks:setup": "bash .githooks/setup.sh",
postinstall: "npm run hooks:setup",
prepublishOnly: "npm run cli:build"
};
var type = "module";
@@ -133,7 +135,8 @@ var windows = [
dark_mode: false,
activation_shortcut: "",
disabled_web_shortcuts: false,
hide_on_close: true
hide_on_close: true,
incognito: false
}
];
var user_agent = {
@@ -222,10 +225,10 @@ var MacConf = {
bundle: bundle$1
};
var productName = "we-read";
var productName = "weekly";
var bundle = {
icon: [
"png/weekly.png"
"png/weekly_512.png"
],
active: true,
linux: {
@@ -312,20 +315,24 @@ const IS_LINUX = platform$1 === 'linux';
const currentModulePath = fileURLToPath(import.meta.url);
// Resolve the parent directory of the current module
const npmDirectory = path.join(path.dirname(currentModulePath), '..');
const tauriConfigDirectory = path.join(npmDirectory, 'src-tauri');
const tauriConfigDirectory = path.join(npmDirectory, 'src-tauri', '.pake');
async function shellExec(command) {
async function shellExec(command, timeout = 300000) {
try {
const { exitCode } = await execa(command, {
cwd: npmDirectory,
stdio: 'inherit',
shell: true,
timeout,
});
return exitCode;
}
catch (error) {
const exitCode = error.exitCode ?? 'unknown';
const errorMessage = error.message || 'Unknown error occurred';
if (error.timedOut) {
throw new Error(`Command timed out after ${timeout}ms: "${command}". Try increasing timeout or check network connectivity.`);
}
throw new Error(`Error occurred while executing command "${command}". Exit code: ${exitCode}. Details: ${errorMessage}`);
}
}
@@ -438,7 +445,19 @@ async function combineFiles(files, output) {
}
async function mergeConfig(url, options, tauriConf) {
const { width, height, fullscreen, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name, resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, } = options;
// Ensure .pake directory exists and copy source templates if needed
const srcTauriDir = path.join(npmDirectory, 'src-tauri');
await fsExtra.ensureDir(tauriConfigDirectory);
// Copy source config files to .pake directory (as templates)
const sourceFiles = ['tauri.conf.json', 'tauri.macos.conf.json', 'tauri.windows.conf.json', 'tauri.linux.conf.json', 'pake.json'];
await Promise.all(sourceFiles.map(async (file) => {
const sourcePath = path.join(srcTauriDir, file);
const destPath = path.join(tauriConfigDirectory, file);
if (await fsExtra.pathExists(sourcePath) && !(await fsExtra.pathExists(destPath))) {
await fsExtra.copy(sourcePath, destPath);
}
}));
const { width, height, fullscreen, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name, resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, incognito, title, } = options;
const { platform } = process;
// Set Windows parameters.
const tauriConfWindowOptions = {
@@ -452,6 +471,8 @@ async function mergeConfig(url, options, tauriConf) {
dark_mode: darkMode,
disabled_web_shortcuts: disabledWebShortcuts,
hide_on_close: hideOnClose,
incognito: incognito,
title: title || null,
};
Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
tauriConf.productName = name;
@@ -650,14 +671,16 @@ class BaseBuilder {
const registryOption = isChina
? ' --registry=https://registry.npmmirror.com'
: '';
// Windows环境下需要更长的超时时间
const timeout = process.platform === 'win32' ? 600000 : 300000;
if (isChina) {
logger.info('✺ Located in China, using npm/rsProxy CN mirror.');
const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
await fsExtra.copy(projectCnConf, projectConf);
await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}`);
await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}`, timeout);
}
else {
await shellExec(`cd "${npmDirectory}" && ${packageManager} install`);
await shellExec(`cd "${npmDirectory}" && ${packageManager} install`, timeout);
}
spinner.succeed(chalk.green('Package installed!'));
if (!tauriTargetPathExists) {
@@ -691,25 +714,66 @@ class BaseBuilder {
return target;
}
getBuildCommand() {
// the debug option should support `--debug` and `--release`
return this.options.debug ? 'npm run build:debug' : 'npm run build';
const baseCommand = this.options.debug
? 'npm run build:debug'
: 'npm run build';
// Use temporary config directory to avoid modifying source files
const configPath = path.join(npmDirectory, 'src-tauri', '.pake', 'tauri.conf.json');
let fullCommand = `${baseCommand} -- -c "${configPath}"`;
// For macOS, use app bundles by default unless DMG is explicitly requested
if (IS_MAC && this.options.targets === 'app') {
fullCommand += ' --bundles app';
}
// Add macos-proxy feature for modern macOS (Darwin 23+ = macOS 14+)
if (IS_MAC) {
const macOSVersion = this.getMacOSMajorVersion();
if (macOSVersion >= 23) {
fullCommand += ' --features macos-proxy';
}
}
return fullCommand;
}
getMacOSMajorVersion() {
try {
const os = require('os');
const release = os.release();
const majorVersion = parseInt(release.split('.')[0], 10);
return majorVersion;
}
catch (error) {
return 0; // Disable proxy feature if version detection fails
}
}
getBasePath() {
const basePath = this.options.debug ? 'debug' : 'release';
return `src-tauri/target/${basePath}/bundle/`;
}
getBuildAppPath(npmDirectory, fileName, fileType) {
return path.join(npmDirectory, this.getBasePath(), fileType.toLowerCase(), `${fileName}.${fileType}`);
// For app bundles on macOS, the directory is 'macos', not 'app'
const bundleDir = fileType.toLowerCase() === 'app' ? 'macos' : fileType.toLowerCase();
return path.join(npmDirectory, this.getBasePath(), bundleDir, `${fileName}.${fileType}`);
}
}
class MacBuilder extends BaseBuilder {
constructor(options) {
super(options);
this.options.targets = 'dmg';
// Use DMG by default for distribution
// Only create app bundles for testing to avoid user interaction
if (process.env.PAKE_CREATE_APP === '1') {
this.options.targets = 'app';
}
else {
this.options.targets = 'dmg';
}
}
getFileName() {
const { name } = this.options;
// For app bundles, use simple name without version/arch
if (this.options.targets === 'app') {
return name;
}
// For DMG files, use versioned filename
let arch;
if (this.options.multiArch) {
arch = 'universal';
@@ -816,6 +880,7 @@ const DEFAULT_PAKE_OPTIONS = {
inject: [],
installerLanguage: 'en-US',
hideOnClose: true,
incognito: false,
};
async function checkUpdateTips() {
@@ -1059,6 +1124,8 @@ program
.addOption(new Option('--hide-on-close', 'Hide window on close instead of exiting')
.default(DEFAULT_PAKE_OPTIONS.hideOnClose)
.hideHelp())
.addOption(new Option('--title <string>', 'Window title').hideHelp())
.addOption(new Option('--incognito', 'Launch app in incognito/private mode').default(DEFAULT_PAKE_OPTIONS.incognito))
.addOption(new Option('--installer-language <string>', 'Installer language')
.default(DEFAULT_PAKE_OPTIONS.installerLanguage)
.hideHelp())

View File

@@ -31,7 +31,7 @@
"scripts": {
"start": "npm run dev",
"dev": "npm run tauri dev",
"build": "npm run tauri build --release",
"build": "npm run tauri build --",
"build:debug": "npm run tauri build -- --debug",
"build:mac": "npm run tauri build -- --target universal-apple-darwin",
"build:config": "chmod +x script/app_config.mjs && node script/app_config.mjs",
@@ -40,7 +40,7 @@
"cli": "rollup -c rollup.config.js --watch",
"cli:dev": "cross-env NODE_ENV=development rollup -c rollup.config.js -w",
"cli:build": "cross-env NODE_ENV=production rollup -c rollup.config.js",
"test": "npm run cli:build && node tests/index.js",
"test": "npm run cli:build && PAKE_CREATE_APP=1 node tests/index.js",
"format": "npx prettier --write . --ignore-unknown && cd src-tauri && cargo fmt --verbose",
"hooks:setup": "bash .githooks/setup.sh",
"postinstall": "npm run hooks:setup",

448
tests/e2e.test.js Normal file
View 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);
}

View File

@@ -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("=======================");