#!/usr/bin/env node /** * File Association Test Script for 4DSTAR Bundle Manager * * This script tests that the app correctly handles file association events * for .fbundle and .opat files when they are opened via the OS. */ const { spawn } = require('child_process'); const fs = require('fs'); const path = require('path'); class FileAssociationTester { constructor() { this.appPath = path.join(__dirname, 'dist', 'mac-arm64', '4DSTAR Bundle Manager.app'); this.testFilesDir = path.join(__dirname, 'test-files'); } log(message, type = 'info') { const timestamp = new Date().toISOString(); const prefix = { 'info': 'šŸ“‹', 'success': 'āœ…', 'warning': 'āš ļø', 'error': 'āŒ' }[type] || 'šŸ“‹'; console.log(`${prefix} [${timestamp}] ${message}`); } async createTestFiles() { this.log('Creating test files for file association testing...', 'info'); // Create test files directory if (!fs.existsSync(this.testFilesDir)) { fs.mkdirSync(this.testFilesDir, { recursive: true }); } // Create a minimal test .fbundle file (ZIP format) const fbundlePath = path.join(this.testFilesDir, 'test-bundle.fbundle'); const fbundleContent = Buffer.from('PK\x03\x04'); // ZIP file header fs.writeFileSync(fbundlePath, fbundleContent); this.log(`āœ“ Created test .fbundle file: ${fbundlePath}`, 'success'); // Create a minimal test .opat file const opatPath = path.join(this.testFilesDir, 'test-data.opat'); const opatContent = Buffer.alloc(100); // Minimal binary file fs.writeFileSync(opatPath, opatContent); this.log(`āœ“ Created test .opat file: ${opatPath}`, 'success'); return { fbundlePath, opatPath }; } async testAppExists() { this.log('Checking if app bundle exists...', 'info'); if (!fs.existsSync(this.appPath)) { throw new Error(`App bundle not found at: ${this.appPath}`); } this.log(`āœ“ App bundle found: ${this.appPath}`, 'success'); return true; } async testFileAssociation(filePath, fileType) { this.log(`Testing ${fileType} file association: ${path.basename(filePath)}`, 'info'); return new Promise((resolve, reject) => { // Use 'open' command to simulate double-clicking the file const openProcess = spawn('open', [filePath], { stdio: ['pipe', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; openProcess.stdout.on('data', (data) => { stdout += data.toString(); }); openProcess.stderr.on('data', (data) => { stderr += data.toString(); }); openProcess.on('close', (code) => { if (code === 0) { this.log(`āœ“ ${fileType} file opened successfully`, 'success'); resolve({ success: true, stdout, stderr }); } else { this.log(`āœ— ${fileType} file failed to open (exit code: ${code})`, 'error'); if (stderr) this.log(`Error: ${stderr}`, 'error'); resolve({ success: false, stdout, stderr, code }); } }); openProcess.on('error', (error) => { this.log(`āœ— Error opening ${fileType} file: ${error.message}`, 'error'); reject(error); }); // Timeout after 10 seconds setTimeout(() => { openProcess.kill(); this.log(`āš ļø ${fileType} file association test timed out`, 'warning'); resolve({ success: false, timeout: true }); }, 10000); }); } async testAppLaunch() { this.log('Testing direct app launch...', 'info'); return new Promise((resolve, reject) => { const appProcess = spawn('open', [this.appPath], { stdio: ['pipe', 'pipe', 'pipe'] }); let stderr = ''; appProcess.stderr.on('data', (data) => { stderr += data.toString(); }); appProcess.on('close', (code) => { if (code === 0) { this.log('āœ“ App launched successfully', 'success'); resolve({ success: true }); } else { this.log(`āœ— App failed to launch (exit code: ${code})`, 'error'); if (stderr) this.log(`Error: ${stderr}`, 'error'); resolve({ success: false, code, stderr }); } }); appProcess.on('error', (error) => { this.log(`āœ— Error launching app: ${error.message}`, 'error'); reject(error); }); // Timeout after 15 seconds setTimeout(() => { appProcess.kill(); this.log('āš ļø App launch test timed out', 'warning'); resolve({ success: false, timeout: true }); }, 15000); }); } async runTests() { try { this.log('Starting file association tests for 4DSTAR Bundle Manager...', 'info'); // Test 1: Check if app exists await this.testAppExists(); // Test 2: Create test files const { fbundlePath, opatPath } = await this.createTestFiles(); // Test 3: Test direct app launch const launchResult = await this.testAppLaunch(); // Wait a moment for app to fully start await new Promise(resolve => setTimeout(resolve, 3000)); // Test 4: Test .fbundle file association const fbundleResult = await this.testFileAssociation(fbundlePath, '.fbundle'); // Wait between tests await new Promise(resolve => setTimeout(resolve, 2000)); // Test 5: Test .opat file association const opatResult = await this.testFileAssociation(opatPath, '.opat'); // Summary this.log('\n=== TEST RESULTS SUMMARY ===', 'info'); this.log(`App Launch: ${launchResult.success ? 'āœ… PASS' : 'āŒ FAIL'}`, launchResult.success ? 'success' : 'error'); this.log(`Bundle File Association: ${fbundleResult.success ? 'āœ… PASS' : 'āŒ FAIL'}`, fbundleResult.success ? 'success' : 'error'); this.log(`OPAT File Association: ${opatResult.success ? 'āœ… PASS' : 'āŒ FAIL'}`, opatResult.success ? 'success' : 'error'); const allPassed = launchResult.success && fbundleResult.success && opatResult.success; if (allPassed) { this.log('\nšŸŽ‰ All file association tests PASSED!', 'success'); this.log('The app correctly handles file associations for both .fbundle and .opat files.', 'success'); } else { this.log('\nāš ļø Some tests FAILED. Check the logs above for details.', 'warning'); } // Cleanup this.log('\nCleaning up test files...', 'info'); if (fs.existsSync(this.testFilesDir)) { fs.rmSync(this.testFilesDir, { recursive: true, force: true }); this.log('āœ“ Test files cleaned up', 'success'); } return allPassed; } catch (error) { this.log(`āŒ Test suite failed: ${error.message}`, 'error'); return false; } } async checkFileAssociations() { this.log('Checking macOS file associations...', 'info'); try { // Check what app is associated with .fbundle files const fbundleCheck = spawn('duti', ['-x', 'fbundle'], { stdio: ['pipe', 'pipe', 'pipe'] }); fbundleCheck.on('close', (code) => { if (code === 0) { this.log('āœ“ .fbundle file association registered', 'success'); } else { this.log('āš ļø .fbundle file association may not be registered', 'warning'); } }); // Check what app is associated with .opat files const opatCheck = spawn('duti', ['-x', 'opat'], { stdio: ['pipe', 'pipe', 'pipe'] }); opatCheck.on('close', (code) => { if (code === 0) { this.log('āœ“ .opat file association registered', 'success'); } else { this.log('āš ļø .opat file association may not be registered', 'warning'); } }); } catch (error) { this.log('āš ļø Could not check file associations (duti not available)', 'warning'); } } } // Run tests if called directly if (require.main === module) { const tester = new FileAssociationTester(); tester.runTests().then(success => { process.exit(success ? 0 : 1); }).catch(error => { console.error('Test suite crashed:', error); process.exit(1); }); } module.exports = FileAssociationTester;