chrome-extension-test-framework
Version:
Universal testing framework for Chrome extensions - Fast static analysis without browser dependencies
726 lines (622 loc) • 26.2 kB
JavaScript
/**
* Chrome Extension Test Framework CLI
*/
const path = require('path');
const fs = require('fs');
const ChromeExtensionTestFramework = require('../index');
const LocalInstallChecker = require('../lib/LocalInstallChecker');
// CLIのバージョン
const packageJson = require('../package.json');
const VERSION = packageJson.version;
// ローカルインストールチェック
const installChecker = new LocalInstallChecker();
installChecker.showInstallationHint();
// コマンドライン引数の解析
const args = process.argv.slice(2);
const options = {
help: false,
version: false,
config: null,
output: {
format: ['console', 'json', 'html'],
directory: './test-results'
},
extensionPath: process.cwd(),
suites: ['all'],
parallel: false,
watch: false,
fix: false,
fixDryRun: false,
exclude: [],
include: [],
profile: null,
failOnWarning: false,
failOnError: false,
changed: false,
sinceLastRun: false,
clearCache: false,
noVersionCheck: false,
showConfig: false,
init: false,
quiet: false,
quick: false
};
// 引数を解析
for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case '-h':
case '--help':
options.help = true;
break;
case '-v':
case '--version':
options.version = true;
break;
case '-c':
case '--config':
options.config = args[++i];
break;
case '-o':
case '--output':
options.output.format = args[++i].split(',');
break;
case '-d':
case '--output-dir':
options.output.directory = args[++i];
break;
case '-p':
case '--path':
options.extensionPath = path.resolve(args[++i]);
break;
case '-s':
case '--suites':
options.suites = args[++i].split(',');
break;
case '--parallel':
options.parallel = true;
break;
case '-w':
case '--watch':
options.watch = true;
break;
case '--no-progress':
options.progress = false;
break;
case '--verbose':
options.verbose = true;
break;
case '--fix':
options.fix = true;
break;
case '--fix-dry-run':
options.fixDryRun = true;
options.fix = true;
break;
case '--exclude':
options.exclude = args[++i].split(',');
break;
case '--include':
options.include = args[++i].split(',');
break;
case '--profile':
options.profile = args[++i];
break;
case '--fail-on-warning':
options.failOnWarning = true;
break;
case '--fail-on-error':
options.failOnError = true;
break;
case '--changed':
options.changed = true;
break;
case '--since-last-run':
options.sinceLastRun = true;
break;
case '--clear-cache':
options.clearCache = true;
break;
case '--no-version-check':
options.noVersionCheck = true;
break;
case '--show-config':
options.showConfig = true;
break;
case '--debug-config':
options.debugConfig = true;
break;
case '--init':
options.init = true;
break;
case '--quiet':
case '-q':
options.quiet = true;
options.progress = false;
break;
case '--quick':
options.quick = true;
break;
default:
if (!arg.startsWith('-')) {
options.extensionPath = path.resolve(arg);
}
}
}
// ヘルプを表示
if (options.help) {
console.log(`
Chrome Extension Test Framework v${VERSION}
Usage: cext-test [options] [extension-path]
Options:
-h, --help Show this help message
-v, --version Show version number
-c, --config <file> Path to config file
-o, --output <formats> Output formats (comma-separated: console,json,html,markdown)
-d, --output-dir <dir> Output directory for reports (default: ./test-results)
-p, --path <path> Path to Chrome extension (default: current directory)
-s, --suites <suites> Test suites to run (comma-separated or 'all')
--parallel Run tests in parallel
-w, --watch Watch mode - re-run tests on file changes
--no-progress Disable progress display
--verbose Show detailed progress information
--fix Automatically fix common issues
--fix-dry-run Show what would be fixed without making changes
--exclude <patterns> Exclude files/directories (comma-separated glob patterns)
--include <patterns> Include only specific files/directories
--profile <name> Use a predefined profile (development, production, ci, quick)
--fail-on-warning Exit with error code if warnings are found
--fail-on-error Exit with error code if errors are found
--changed Test only changed files (requires git)
--since-last-run Test only files changed since last run
--clear-cache Clear the test cache
--no-version-check Disable update notifications
--show-config Show the current configuration and exit
--debug-config Show detailed configuration loading process
--init Initialize a new configuration file
-q, --quiet Quiet mode for CI (errors/warnings only)
--quick Quick mode - run only essential tests
Test Suites:
manifest - Validate manifest.json
security - Security checks
performance - Performance optimization
structure - File structure validation
localization - i18n support validation
all - Run all test suites (default)
Examples:
cext-test # Test current directory
cext-test /path/to/extension # Test specific extension
cext-test -o json,html # Generate JSON and HTML reports
cext-test -s manifest,security # Run specific test suites
cext-test -c my-config.json # Use custom config file
cext-test --fix # Automatically fix issues
cext-test --fix-dry-run # Preview fixes without applying them
cext-test --exclude "test/**,docs/**" # Exclude test and docs directories
cext-test --include "js/**,css/**" # Test only js and css files
cext-test --profile production # Use production profile
cext-test --changed # Test only changed files (git)
cext-test --since-last-run # Test files changed since last run
cext-test --quick # Quick test with essential checks only
Configuration:
Create a cext-test.config.js or .cextrc.json file for custom settings:
module.exports = {
extensionPath: './src',
output: {
format: ['console', 'json', 'html'],
directory: './reports'
},
validation: {
manifest: true,
permissions: true,
csp: true
},
rules: [
// Custom validation rules
]
};
`);
process.exit(0);
}
// バージョンを表示
if (options.version) {
console.log(`v${VERSION}`);
process.exit(0);
}
// 設定ファイルの初期化
if (options.init) {
const configPath = path.join(process.cwd(), '.cextrc.json');
if (fs.existsSync(configPath)) {
console.error('❌ Configuration file already exists: .cextrc.json');
console.log('💡 Use --config to specify a different config file');
process.exit(1);
}
const defaultConfig = {
"$schema": "./node_modules/chrome-extension-test-framework/.cextrc.schema.json",
"extensionPath": ".",
"output": {
"format": ["console", "json"],
"directory": "./test-results"
},
"exclude": [
"node_modules/**",
"test/**",
"tests/**",
"*.test.js",
"*.spec.js"
],
"consoleThresholds": {
"production": 10,
"development": 50,
"test": null
},
"profile": "development",
"failOnError": true,
"failOnWarning": false
};
try {
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
console.log('✅ Created .cextrc.json configuration file');
console.log('\n📝 Configuration includes:');
console.log(' • VS Code autocomplete support ($schema)');
console.log(' • Common exclude patterns');
console.log(' • Console usage thresholds');
console.log(' • Development profile by default');
console.log('\n💡 Next steps:');
console.log(' 1. Edit .cextrc.json to customize settings');
console.log(' 2. Run: cext-test');
console.log('\n📚 Documentation: https://github.com/ibushimaru/chrome-extension-test-framework');
process.exit(0);
} catch (error) {
console.error(`❌ Failed to create config file: ${error.message}`);
process.exit(1);
}
}
// 拡張機能パスの検証
if (!fs.existsSync(options.extensionPath)) {
console.error(`❌ Extension path not found: ${options.extensionPath}`);
process.exit(1);
}
// Auto-fix モード
if (options.fix) {
const AutoFixer = require('../lib/AutoFixer');
const fixer = new AutoFixer({
dryRun: options.fixDryRun,
verbose: options.verbose
});
(async () => {
try {
const result = await fixer.fixAll(options.extensionPath);
console.log('\n📊 Auto-fix Summary:');
console.log(` Total fixes: ${result.summary.total}`);
if (result.summary.total > 0) {
console.log('\n By type:');
for (const [type, count] of Object.entries(result.summary.byType)) {
console.log(` - ${type}: ${count}`);
}
}
if (options.fixDryRun) {
console.log('\n💡 Run with --fix (without --dry-run) to apply these fixes');
} else if (result.summary.total > 0) {
console.log('\n✅ Fixes applied successfully!');
console.log('💡 Run tests again to verify the fixes');
} else {
console.log('\n✨ No issues found that can be auto-fixed');
}
process.exit(0);
} catch (error) {
console.error(`\n❌ Auto-fix failed: ${error.message}`);
process.exit(1);
}
})();
return;
}
const manifestPath = path.join(options.extensionPath, 'manifest.json');
if (!fs.existsSync(manifestPath)) {
console.error(`❌ manifest.json not found in: ${options.extensionPath}`);
console.log('\n💡 Tip: Run with --fix to create a default manifest.json');
process.exit(1);
}
// メイン実行関数
async function runTests() {
try {
// フレームワークのインスタンスを作成
const framework = new ChromeExtensionTestFramework({
extensionPath: options.extensionPath,
output: options.output,
parallel: options.parallel,
exclude: options.exclude,
include: options.include,
failOnWarning: options.failOnWarning,
failOnError: options.failOnError,
profile: options.profile,
progress: options.progress,
verbose: options.verbose,
quiet: options.quiet
});
// 設定ファイルを読み込み
let defaultConfigPath = null;
if (options.config) {
await framework.loadConfig(options.config);
defaultConfigPath = options.config;
} else {
// 設定ファイルの自動検出
const ConfigLoader = require('../lib/ConfigLoader');
const configLoader = new ConfigLoader();
defaultConfigPath = configLoader.findDefaultConfig(options.extensionPath);
if (defaultConfigPath) {
console.log(`📄 Found config file: ${path.basename(defaultConfigPath)}`);
await framework.loadConfig(defaultConfigPath);
}
}
// プロファイルが指定されている場合(設定ファイル後に適用)
if (options.profile && !framework.config.profile) {
framework.applyProfile(options.profile);
}
// --debug-configオプションの処理
if (options.debugConfig) {
console.log('🔍 Configuration Debug Mode\n');
console.log('1. Loading configuration...');
console.log(` ✅ Working directory: ${process.cwd()}`);
console.log(` ✅ Extension path: ${options.extensionPath}`);
if (defaultConfigPath) {
console.log(` ✅ Config file found: ${defaultConfigPath}`);
console.log(` ✅ Config loaded from: ${path.basename(defaultConfigPath)}`);
} else {
console.log(' ℹ️ No config file found, using defaults');
}
if (options.profile) {
console.log(` ✅ Profile applied: ${options.profile}`);
}
console.log('\n2. File discovery...');
// ファイル探索のシミュレーション
const allFiles = await framework.suites[0]?.getAllFiles('', [], { skipExclude: true }) || [];
console.log(` ✅ Total files found: ${allFiles.length}`);
if (framework.config.exclude && framework.config.exclude.length > 0) {
console.log(`\n3. Applying exclude patterns: ${JSON.stringify(framework.config.exclude)}`);
const excludedCount = allFiles.length - (await framework.suites[0]?.getAllFiles() || []).length;
console.log(` ✅ Files excluded: ${excludedCount}`);
}
console.log('\n4. Final configuration:');
console.log(` - Console thresholds:`);
const thresholds = framework.config.consoleThresholds || {};
console.log(` • production: ${thresholds.production !== undefined ? thresholds.production : 10}`);
console.log(` • development: ${thresholds.development !== undefined ? thresholds.development : 100}`);
console.log(` • test: ${thresholds.test !== undefined ? thresholds.test : 'Infinity'}`);
if (thresholds.warn !== undefined) {
console.log(` • warn: ${thresholds.warn}`);
}
if (thresholds.error !== undefined) {
console.log(` • error: ${thresholds.error}`);
}
if (framework.config.allowedDevFiles) {
console.log(` - Allowed dev files: ${JSON.stringify(framework.config.allowedDevFiles)}`);
}
console.log('\n5. Test execution plan:');
const testSuites = options.suites.includes('all') ?
['manifest', 'security', 'performance', 'structure', 'localization'] :
options.suites;
console.log(` - Test suites: ${testSuites.join(', ')}`);
console.log(` - Parallel execution: ${framework.config.parallel ? 'enabled' : 'disabled'}`);
console.log(` - Timeout: ${framework.config.timeout}ms`);
process.exit(0);
}
// --show-configオプションの処理
if (options.showConfig) {
console.log('📄 Current Configuration:');
// 基本設定
const effectiveConfig = {
extensionPath: framework.config.extensionPath,
output: framework.config.output,
validation: framework.config.validation,
parallel: framework.config.parallel,
timeout: framework.config.timeout,
exclude: framework.config.exclude || [],
include: framework.config.include || [],
failOnWarning: framework.config.failOnWarning,
failOnError: framework.config.failOnError
};
// プロファイル情報
if (framework.config.profile) {
effectiveConfig.profile = {
name: framework.config.profile.name,
description: framework.config.profile.description,
skipTests: framework.config.profile.skipTests || [],
warningLevels: framework.config.profile.warningLevels || {},
maxFileSize: framework.config.profile.maxFileSize
};
}
// カスタム設定
if (framework.config.consoleThresholds) {
effectiveConfig.consoleThresholds = framework.config.consoleThresholds;
}
if (framework.config.allowedDevFiles) {
effectiveConfig.allowedDevFiles = framework.config.allowedDevFiles;
}
console.log(JSON.stringify(effectiveConfig, null, 2));
if (framework.config.profile) {
console.log('\n📝 Active Profile:', framework.config.profile.name);
console.log(' Description:', framework.config.profile.description);
if (framework.config.profile.skipTests && framework.config.profile.skipTests.length > 0) {
console.log(' Skipped tests:', framework.config.profile.skipTests.join(', '));
}
}
console.log('\n🔍 Exclude Patterns:');
console.log(JSON.stringify(framework.excludeManager.getPatterns(), null, 2));
console.log('\n📊 Settings Source:');
console.log(' - Default values: Built-in framework defaults');
if (defaultConfigPath) {
console.log(` - Config file: ${path.basename(defaultConfigPath)}`);
}
if (options.profile) {
console.log(` - Profile: ${options.profile}`);
}
console.log(' - CLI arguments: Override all above');
process.exit(0);
}
// キャッシュクリアオプション
if (options.clearCache) {
console.log('🗑️ Clearing test cache...');
framework.incrementalTester.clearCache();
console.log('✅ Cache cleared');
if (!options.changed && !options.sinceLastRun) {
return;
}
}
// インクリメンタルテストの判定
if (options.changed || options.sinceLastRun) {
const testTargets = await framework.incrementalTester.determineTestTargets({
all: false,
useGit: options.changed,
sinceLastRun: options.sinceLastRun
});
console.log(`\n🔍 Incremental test mode: ${testTargets.reason}`);
if (testTargets.mode === 'none') {
console.log('✨ No changes detected - all tests are up to date!');
process.exit(0);
} else if (testTargets.mode === 'incremental') {
console.log(` Testing ${testTargets.files.length} affected files`);
console.log(` Suites: ${testTargets.suites.join(', ') || 'all'}`);
// 特定のスイートのみ実行
if (testTargets.suites.length > 0) {
options.suites = testTargets.suites;
}
}
}
// クイックモードの場合は必須テストのみ
if (options.quick) {
console.log('⚡ Quick mode: Running essential tests only\n');
// 必須テストのみ実行
const quickSuites = {
'manifest': require('../suites/ManifestTestSuite'),
'security': require('../suites/SecurityTestSuite')
};
Object.entries(quickSuites).forEach(([name, Suite]) => {
const suiteConfig = {
...framework.config,
excludeManager: framework.excludeManager,
// クイックモードでは一部のテストをスキップ
quickMode: true
};
framework.addSuite(new Suite(suiteConfig));
});
} else if (options.suites.includes('all')) {
framework.useBuiltinTests();
} else {
// 個別のスイートを追加
const suiteMap = {
'manifest': require('../suites/ManifestTestSuite'),
'security': require('../suites/SecurityTestSuite'),
'performance': require('../suites/PerformanceTestSuite'),
'structure': require('../suites/StructureTestSuite'),
'localization': require('../suites/LocalizationTestSuite')
};
options.suites.forEach(suiteName => {
const Suite = suiteMap[suiteName.toLowerCase()];
if (Suite) {
const suiteConfig = {
...framework.config,
excludeManager: framework.excludeManager
};
framework.addSuite(new Suite(suiteConfig));
} else {
console.warn(`⚠️ Unknown test suite: ${suiteName}`);
}
});
}
// テストを実行
const results = await framework.run();
// インクリメンタルテストの結果を記録
if (options.changed || options.sinceLastRun) {
framework.incrementalTester.recordTestRun(results);
}
// 終了コードを設定
let exitCode = 0;
// テストが失敗した場合は常に1を返す(CI/CDで重要)
if (results.summary.failed > 0) {
exitCode = 1;
}
// --fail-on-warningが指定されている場合、警告でも1を返す
if (options.failOnWarning && results.warnings && results.warnings.length > 0) {
exitCode = 1;
}
process.exit(exitCode);
} catch (error) {
console.error(`\n❌ Test execution failed: ${error.message}`);
if (error.stack) {
console.error(error.stack);
}
process.exit(1);
}
}
// ウォッチモード
if (options.watch) {
const FileWatcher = require('../lib/FileWatcher');
// 初回実行
console.log('🚀 Running initial tests...\n');
runTests().then(() => {
// ファイル監視を開始
const watcher = new FileWatcher({
extensionPath: options.extensionPath,
debounceTime: 500,
ignorePatterns: [
'node_modules',
'.git',
'test-results',
'test-output',
'.DS_Store',
'Thumbs.db',
'*.log',
'*.tmp'
]
});
// テスト実行中フラグ
let isRunning = false;
// 変更時の処理
watcher.on('change', async (changeInfo) => {
if (isRunning) {
console.log(' ⏳ Test already running, skipping...');
return;
}
isRunning = true;
console.log('\n🔄 Re-running tests...\n');
try {
await runTests();
} catch (error) {
// エラーが発生してもウォッチモードは継続
console.error('Test execution error:', error.message);
} finally {
isRunning = false;
}
});
// 特定ファイルの変更に対する特別な処理
watcher.on('manifest-change', () => {
console.log(' ⚠️ manifest.json changed - full test suite will run');
});
// Ctrl+Cで終了
process.on('SIGINT', () => {
console.log('\n\n👋 Stopping watch mode...');
watcher.stop();
// 統計を表示
const stats = watcher.getStats();
console.log('\n📊 Watch Mode Statistics:');
console.log(` Total changes detected: ${stats.totalChanges}`);
console.log(` Unique files changed: ${stats.changedFiles.size}`);
if (Object.keys(stats.fileTypes).length > 0) {
console.log('\n Changes by file type:');
Object.entries(stats.fileTypes).forEach(([type, count]) => {
console.log(` - ${type}: ${count}`);
});
}
process.exit(0);
});
// ウォッチャーを開始
watcher.start();
}).catch(error => {
console.error('Initial test run failed:', error.message);
process.exit(1);
});
} else {
// 通常実行
runTests();
}