✨ Support test packaging process
This commit is contained in:
4
bin/README.md
vendored
4
bin/README.md
vendored
@@ -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
4
bin/README_CN.md
vendored
@@ -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
|
||||
|
||||
45
bin/builders/BaseBuilder.ts
vendored
45
bin/builders/BaseBuilder.ts
vendored
@@ -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}`,
|
||||
);
|
||||
}
|
||||
|
||||
15
bin/builders/MacBuilder.ts
vendored
15
bin/builders/MacBuilder.ts
vendored
@@ -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
29
bin/helpers/merge.ts
vendored
@@ -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
9
bin/utils/dir.ts
vendored
@@ -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
95
dist/cli.js
vendored
@@ -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())
|
||||
|
||||
@@ -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
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