#!/usr/bin/env node /** * Pake CLI Test Suite * * This is the main test file for the Pake CLI tool. * It includes unit tests, integration tests, and manual test scenarios. */ import { execSync, spawn } from "child_process"; import { fileURLToPath } from "url"; import path from "path"; import fs from "fs"; import ora from "ora"; import config, { TIMEOUTS, TEST_URLS, TEST_NAMES, TEST_ASSETS, } from "./test.config.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = config.PROJECT_ROOT; const cliPath = config.CLI_PATH; // Test utilities class TestRunner { constructor() { this.tests = []; this.results = []; } addTest(name, testFn, description = "") { this.tests.push({ name, testFn, description }); } async runAll() { console.log("๐Ÿงช Pake CLI Test Suite"); console.log("======================\n"); // Quick validation this.validateEnvironment(); console.log("๐Ÿ” Running Unit Tests:"); console.log("----------------------\n"); for (const [index, test] of this.tests.entries()) { const spinner = ora(`Running ${test.name}...`).start(); try { const result = await test.testFn(); 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.slice(0, 50)}...`, ); this.results.push({ name: test.name, passed: false, error: error.message, }); } } this.displayResults(); this.displayManualTestScenarios(); } validateEnvironment() { console.log("๐Ÿ”ง Environment Validation:"); console.log("---------------------------"); // Check if CLI file exists if (!fs.existsSync(cliPath)) { console.log("โŒ CLI file not found. Run: npm run cli:build"); process.exit(1); } console.log("โœ… CLI file exists"); // Check if CLI is executable try { execSync(`node "${cliPath}" --version`, { encoding: "utf8", timeout: 3000, }); console.log("โœ… CLI is executable"); } catch (error) { console.log("โŒ CLI is not executable"); process.exit(1); } console.log("โœ… Environment is ready\n"); } displayResults() { const passed = this.results.filter((r) => r.passed).length; const total = this.results.length; console.log("\n๐Ÿ“Š Test Results:"); console.log("================"); this.results.forEach((result) => { const status = result.passed ? "โœ…" : "โŒ"; const error = result.error ? ` (${result.error})` : ""; console.log(`${status} ${result.name}${error}`); }); console.log(`\n๐ŸŽฏ Summary: ${passed}/${total} tests passed`); if (passed === total) { console.log("๐ŸŽ‰ All tests passed!\n"); } else { console.log(`โŒ ${total - passed} test(s) failed\n`); } } displayManualTestScenarios() { console.log("๐Ÿ”ง Manual Test Scenarios:"); console.log("=========================\n"); const scenarios = [ { name: "Weekly App Creation (Primary Test Case)", command: `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "${TEST_NAMES.WEEKLY}"`, description: "Creates Weekly desktop app from tw93 weekly site", expectedTime: "2-5 minutes (first time), 30-60s (subsequent)", }, { name: "Weekly App with Custom Size", command: `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "${TEST_NAMES.WEEKLY}" --width 1200 --height 800`, description: "Creates Weekly app with optimal window dimensions", expectedTime: "2-5 minutes", }, { name: "Debug Build with Weekly", command: `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "${TEST_NAMES.DEBUG}" --debug`, description: "Creates debug build with verbose output for troubleshooting", expectedTime: "2-5 minutes", }, { name: "Google Translate with Spaces in Name", command: `node "${cliPath}" https://translate.google.com --name "${TEST_NAMES.GOOGLE_TRANSLATE}"`, description: "Tests app name with spaces (auto-handled per platform)", expectedTime: "2-5 minutes", }, { name: "Always On Top App", command: `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "TopWeekly" --always-on-top`, description: "Creates app that stays on top of other windows", expectedTime: "2-5 minutes", }, { name: "Full Screen App", command: `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "FullWeekly" --fullscreen`, description: "Creates full-screen Weekly app", expectedTime: "2-5 minutes", }, { name: "System Tray App", command: `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "TrayWeekly" --show-system-tray`, description: "Creates app with system tray integration", expectedTime: "2-5 minutes", }, { name: "Custom User Agent", command: `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "UAWeekly" --user-agent "Pake/1.0 Weekly App"`, description: "Creates app with custom browser user agent", expectedTime: "2-5 minutes", }, { name: "Version Controlled App", command: `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "VersionWeekly" --app-version "2.1.0"`, description: "Creates app with specific version number", expectedTime: "2-5 minutes", }, { name: "Weekly App with Remote Icon", command: `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "IconWeekly" --icon "${TEST_ASSETS.WEEKLY_ICNS}"`, description: "Creates Weekly app with remote icns icon from CDN", expectedTime: "2-5 minutes", }, { name: "Weekly App with Proxy", command: `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "ProxyWeekly" --proxy-url "http://127.0.0.1:7890"`, description: "Creates Weekly app with HTTP proxy configuration", expectedTime: "2-5 minutes", }, { name: "Weekly App with Global Shortcut", command: `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "ShortcutWeekly" --activation-shortcut "CmdOrControl+Shift+W"`, description: "Creates Weekly app with global activation shortcut", expectedTime: "2-5 minutes", }, { name: "Weekly App with Hide on Close", command: `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "HideWeekly" --hide-on-close`, description: "Creates Weekly app that hides instead of closing", expectedTime: "2-5 minutes", }, { name: "Weekly App with Disabled Web Shortcuts", command: `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "NoShortcutWeekly" --disabled-web-shortcuts`, description: "Creates Weekly app with web shortcuts disabled", expectedTime: "2-5 minutes", }, ]; if (process.platform === "darwin") { scenarios.push( { name: "Mac Universal Binary (Weekly)", command: `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "${TEST_NAMES.WEEKLY}" --multi-arch`, description: "Creates universal binary for Intel and Apple Silicon", expectedTime: "5-10 minutes", }, { name: "Mac Dark Mode App", command: `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "DarkWeekly" --dark-mode`, description: "Forces dark mode on macOS", expectedTime: "2-5 minutes", }, { name: "Mac Immersive Title Bar", command: `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "ImmersiveWeekly" --hide-title-bar`, description: "Creates app with hidden title bar (macOS only)", expectedTime: "2-5 minutes", }, ); } if (process.platform === "linux") { scenarios.push( { name: "Linux AppImage Build", command: `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "${TEST_NAMES.WEEKLY}" --targets appimage`, description: "Creates AppImage package for Linux", expectedTime: "3-7 minutes", }, { name: "Linux RPM Build", command: `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "${TEST_NAMES.WEEKLY}" --targets rpm`, description: "Creates RPM package for Linux", expectedTime: "3-7 minutes", }, ); } if (process.platform === "win32") { scenarios.push({ name: "Windows with Chinese Installer", command: `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "${TEST_NAMES.WEEKLY}" --installer-language zh-CN`, description: "Creates Windows installer with Chinese language", expectedTime: "3-7 minutes", }); } scenarios.forEach((scenario, index) => { console.log(`${index + 1}. ${scenario.name}`); console.log(` Command: ${scenario.command}`); console.log(` Description: ${scenario.description}`); console.log(` Expected Time: ${scenario.expectedTime}\n`); }); console.log("๐Ÿ’ก Usage Instructions:"); console.log(" 1. Copy any command above"); console.log(" 2. Run it in your terminal"); console.log(" 3. Wait for the build to complete"); console.log(" 4. Check for the generated app file in current directory"); console.log(" 5. Launch the app to verify it works\n"); } } // Test suite implementation const runner = new TestRunner(); // Unit Tests runner.addTest( "Version Command", () => { const output = execSync(`node "${cliPath}" --version`, { encoding: "utf8", timeout: 3000, }); return /^\d+\.\d+\.\d+/.test(output.trim()); }, "Should output version number", ); runner.addTest( "Help Command", () => { const output = execSync(`node "${cliPath}" --help`, { encoding: "utf8", timeout: 3000, }); return output.includes("Usage: cli [url] [options]"); }, "Should display help information", ); runner.addTest( "No Arguments Behavior", () => { const output = execSync(`node "${cliPath}"`, { encoding: "utf8", timeout: 3000, }); return output.includes("Usage: cli [url] [options]"); }, "Should display help when no arguments provided", ); runner.addTest( "Invalid Number Validation", () => { try { execSync(`node "${cliPath}" https://example.com --width abc`, { encoding: "utf8", timeout: 3000, }); return false; // Should throw error } catch (error) { return error.message.includes("Not a number"); } }, "Should reject invalid number inputs", ); runner.addTest( "URL Validation", () => { try { // Test with a clearly invalid URL that should fail execSync(`node "${cliPath}" "${TEST_URLS.INVALID}" --name TestApp`, { encoding: "utf8", timeout: 3000, }); return false; // Should have failed } catch (error) { // Should fail with non-zero exit code for invalid URL return error.status !== 0; } }, "Should reject malformed URLs", ); runner.addTest( "Required Dependencies Check", () => { // Check if essential Node.js modules are available try { const packageJson = JSON.parse( fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"), ); const hasEssentialDeps = [ "commander", "chalk", "fs-extra", "execa", ].every((dep) => packageJson.dependencies[dep]); return hasEssentialDeps; } catch { return false; } }, "Should have all required dependencies", ); // Performance and Integration Tests runner.addTest( "CLI Response Time", () => { const start = Date.now(); execSync(`node "${cliPath}" --version`, { encoding: "utf8", timeout: 3000, }); const elapsed = Date.now() - start; // CLI should respond within 2 seconds return elapsed < 2000; }, "Should respond quickly to simple commands", ); runner.addTest( "Build Command Generation", () => { // Test that getBuildCommand logic works const output = execSync(`node "${cliPath}" --help`, { encoding: "utf8", timeout: 3000, }); return output.includes("--debug") && output.includes("Debug build"); }, "Should support debug build options", ); // New comprehensive option validation tests runner.addTest( "CLI Options Validation - Core Options Present", () => { const output = execSync(`node "${cliPath}" --help`, { encoding: "utf8", timeout: 3000, }); const coreOptions = [ "--name", "--icon", "--height", "--width", "--hide-title-bar", "--fullscreen", "--multi-arch", "--use-local-file", "--inject", "--debug", ]; return coreOptions.every((option) => output.includes(option)); }, "Should include core CLI options", ); runner.addTest( "Weekly URL Accessibility", () => { try { // Test that weekly.tw93.fun is accessible for our test cases const testCommand = `node "${cliPath}" ${TEST_URLS.WEEKLY} --name "URLTest" --debug`; // We're not actually building, just testing URL parsing doesn't fail immediately execSync(`echo "n" | timeout 5s ${testCommand} || true`, { encoding: "utf8", timeout: 8000, }); return true; // If we get here, URL was parsed successfully } catch (error) { // Check if it's a timeout (expected) vs URL error (unexpected) return ( !error.message.includes("Invalid URL") && !error.message.includes("invalid") ); } }, "Should accept weekly.tw93.fun as valid URL", ); runner.addTest( "App Version Format Validation", () => { try { // Test with valid version format first execSync(`node "${cliPath}" --help`, { encoding: "utf8", timeout: 3000 }); // If CLI accepts --app-version in help, it should validate the format const helpOutput = execSync(`node "${cliPath}" --help`, { encoding: "utf8", timeout: 3000, }); return helpOutput.includes("--") && helpOutput.includes("version"); } catch (error) { return false; } }, "Should have app version option available", ); runner.addTest( "Activation Shortcut Format", () => { try { // Test valid shortcut format const output = execSync( `echo "n" | timeout 3s node "${cliPath}" ${TEST_URLS.WEEKLY} --activation-shortcut "CmdOrControl+Shift+P" --name "ShortcutTest" || true`, { encoding: "utf8", timeout: 5000, }, ); // Should not immediately fail on valid shortcut format return !output.includes("Invalid shortcut"); } catch (error) { return !error.message.includes("Invalid shortcut"); } }, "Should accept valid activation shortcut format", ); // Critical implementation tests based on bin/ analysis runner.addTest( "File Naming Pattern Validation", () => { try { // Test that app names are properly sanitized for filenames const testCases = [ { name: "Simple", expected: true }, { name: "With Spaces", expected: true }, { name: "Special-Chars_123", expected: true }, { name: "", expected: false }, ]; // Test name validation logic exists return testCases.every((testCase) => { if (testCase.name === "") { // Empty names should be handled return true; } // Non-empty names should be accepted return testCase.name.length > 0; }); } catch (error) { return false; } }, "Should handle various app name formats for file naming", ); runner.addTest( "Platform-specific Build Output Validation", () => { try { // Verify that platform detection works properly const platform = process.platform; const expectedExtensions = { darwin: ".dmg", win32: ".msi", linux: ".deb", }; const expectedExt = expectedExtensions[platform]; return expectedExt !== undefined; } catch (error) { return false; } }, "Should detect platform and use correct file extensions", ); runner.addTest( "URL Validation and Processing", () => { try { // Test URL validation logic with various formats const validUrls = [ "https://weekly.tw93.fun", "http://example.com", "https://subdomain.example.com/path", ]; const invalidUrls = ["not-a-url", "ftp://invalid-protocol.com", ""]; // All valid URLs should be accepted by our validation // This tests the URL processing logic without actually building return validUrls.every((url) => { try { new URL(url); return true; } catch { return false; } }); } catch (error) { return false; } }, "Should properly validate and process URLs", ); runner.addTest( "Icon Format Validation", () => { try { // Test icon extension validation logic based on platform const platform = process.platform; const validIconExtensions = { darwin: [".icns"], win32: [".ico"], linux: [".png"], }; const platformIcons = validIconExtensions[platform]; return platformIcons && platformIcons.length > 0; } catch (error) { return false; } }, "Should validate icon formats per platform", ); runner.addTest( "Injection File Validation", () => { try { // Test injection file validation (CSS/JS only) const validFiles = ["style.css", "script.js", "custom.CSS", "app.JS"]; const invalidFiles = ["image.png", "doc.txt", "app.html"]; const isValidInjectionFile = (filename) => { return ( filename.toLowerCase().endsWith(".css") || filename.toLowerCase().endsWith(".js") ); }; const validResults = validFiles.every(isValidInjectionFile); const invalidResults = invalidFiles.every( (file) => !isValidInjectionFile(file), ); return validResults && invalidResults; } catch (error) { return false; } }, "Should validate injection file formats (CSS/JS only)", ); runner.addTest( "Configuration Merging Logic", () => { try { // Test that configuration options are properly structured const mockConfig = { width: 1200, height: 800, fullscreen: false, debug: false, name: "TestApp", }; // Verify all critical config properties exist and have correct types return ( typeof mockConfig.width === "number" && typeof mockConfig.height === "number" && typeof mockConfig.fullscreen === "boolean" && typeof mockConfig.debug === "boolean" && typeof mockConfig.name === "string" && mockConfig.name.length > 0 ); } catch (error) { return false; } }, "Should handle configuration merging properly", ); runner.addTest( "Build Command Generation", () => { try { // Test build command logic based on debug flag const debugCommand = "npm run build:debug"; const releaseCommand = "npm run build"; // Verify command generation logic const generateBuildCommand = (debug) => { return debug ? debugCommand : releaseCommand; }; return ( generateBuildCommand(true) === debugCommand && generateBuildCommand(false) === releaseCommand ); } catch (error) { return false; } }, "Should generate correct build commands for debug/release", ); runner.addTest( "Remote Icon URL Validation", () => { try { // Test that remote icon URLs are properly validated const iconUrl = TEST_ASSETS.WEEKLY_ICNS; // Basic URL validation const url = new URL(iconUrl); const isValidHttps = url.protocol === "https:"; const hasIconExtension = iconUrl.toLowerCase().endsWith(".icns"); return isValidHttps && hasIconExtension; } catch (error) { return false; } }, "Should validate remote icon URLs correctly", ); runner.addTest( "Icon Download Accessibility", () => { try { // Test if the weekly.icns URL is accessible (without actually downloading) const iconUrl = TEST_ASSETS.WEEKLY_ICNS; // Quick URL format check const expectedDomain = "gw.alipayobjects.com"; const expectedPath = "/os/k/fw/weekly.icns"; return iconUrl.includes(expectedDomain) && iconUrl.includes(expectedPath); } catch (error) { return false; } }, "Should have accessible CDN icon URL for testing", ); // New Tauri runtime functionality tests based on src-tauri/src/ analysis runner.addTest( "Proxy URL Configuration", () => { try { // Test proxy URL validation logic const validProxies = [ "http://127.0.0.1:7890", "https://proxy.example.com:8080", "socks5://127.0.0.1:7891", ]; const invalidProxies = ["not-a-url", "ftp://invalid-protocol.com", ""]; // Test valid proxy URLs const validResults = validProxies.every((proxy) => { try { new URL(proxy); return proxy.startsWith("http") || proxy.startsWith("socks5"); } catch { return false; } }); return validResults; } catch (error) { return false; } }, "Should validate proxy URL configurations", ); runner.addTest( "User Agent String Validation", () => { try { // Test user agent string handling const testUserAgents = [ "Pake/1.0 Weekly App", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", "Custom-App/2.0", "", // Empty should be allowed ]; // All should be valid strings return testUserAgents.every((ua) => typeof ua === "string"); } catch (error) { return false; } }, "Should handle user agent string configurations", ); runner.addTest( "Global Shortcut Key Format", () => { try { // Test global shortcut format validation const validShortcuts = [ "CmdOrControl+Shift+P", "Alt+F4", "Ctrl+Shift+X", "Cmd+Option+A", "", // Empty should be allowed ]; return validShortcuts.every((shortcut) => { if (shortcut === "") return true; // Check basic shortcut format: has modifiers and key separated by + const parts = shortcut.split("+"); if (parts.length < 2) return false; // Last part should be the key (letter or special key) const key = parts[parts.length - 1]; if (!key || key.length === 0) return false; // Check modifiers are valid const modifiers = parts.slice(0, -1); const validModifiers = [ "Cmd", "Ctrl", "CmdOrControl", "Alt", "Option", "Shift", "Meta", ]; return modifiers.every((mod) => validModifiers.includes(mod)); }); } catch (error) { return false; } }, "Should validate global shortcut key formats", ); runner.addTest( "System Tray Configuration", () => { try { // Test system tray option validation const trayConfigs = [ { show_system_tray: true, valid: true }, { show_system_tray: false, valid: true }, ]; return trayConfigs.every((config) => { const isValidBoolean = typeof config.show_system_tray === "boolean"; return isValidBoolean === config.valid; }); } catch (error) { return false; } }, "Should validate system tray configuration options", ); runner.addTest( "Window Behavior Configuration", () => { try { // Test window behavior options const windowOptions = [ "hide_on_close", "fullscreen", "always_on_top", "resizable", "hide_title_bar", "dark_mode", ]; // Test that all window options are recognized return windowOptions.every((option) => { return typeof option === "string" && option.length > 0; }); } catch (error) { return false; } }, "Should support all window behavior configurations", ); runner.addTest( "Download File Extension Detection", () => { try { // Test file download detection logic (from inject/event.js) const downloadableExtensions = [ "pdf", "zip", "dmg", "msi", "deb", "AppImage", "jpg", "png", "gif", "mp4", "mp3", "json", ]; const testUrls = [ "https://example.com/file.pdf", "https://example.com/app.dmg", "https://example.com/archive.zip", "https://example.com/page.html", // Not downloadable ]; // Test download detection logic const isDownloadUrl = (url) => { const fileExtPattern = new RegExp( `\\.(${downloadableExtensions.join("|")})$`, "i", ); return fileExtPattern.test(url); }; return ( testUrls.slice(0, 3).every(isDownloadUrl) && !isDownloadUrl(testUrls[3]) ); } catch (error) { return false; } }, "Should detect downloadable file extensions correctly", ); runner.addTest( "Keyboard Shortcut Mapping", () => { try { // Test keyboard shortcuts from inject/event.js const shortcuts = { "[": "history.back", "]": "history.forward", "-": "zoom.out", "=": "zoom.in", "+": "zoom.in", 0: "zoom.reset", r: "reload", ArrowUp: "scroll.top", ArrowDown: "scroll.bottom", }; // Verify shortcut mapping structure return ( Object.keys(shortcuts).length > 0 && Object.values(shortcuts).every((action) => typeof action === "string") ); } catch (error) { return false; } }, "Should provide comprehensive keyboard shortcut mappings", ); runner.addTest( "Locale-based Message Support", () => { try { // Test internationalization support from util.rs const messageTypes = ["start", "success", "failure"]; const locales = ["en", "zh"]; // Test message structure return ( messageTypes.every((type) => typeof type === "string") && locales.every((locale) => typeof locale === "string") ); } catch (error) { return false; } }, "Should support locale-based download messages", ); // Run the test suite if (import.meta.url === `file://${process.argv[1]}`) { runner.runAll().catch(console.error); } export default runner;