refactor test helpers for Meteor modern test suite

This commit is contained in:
Nacho Codoñer
2025-08-06 20:45:13 +02:00
parent 5d0033ba8c
commit 5142182c1d
2 changed files with 151 additions and 124 deletions

View File

@@ -0,0 +1,143 @@
const execa = require('execa');
const waitOn = require('wait-on');
const path = require('path');
const fs = require('fs-extra');
const os = require('os');
const rimraf = require('rimraf');
// Get the absolute path to the meteor executable
const REPO_ROOT = path.resolve(__dirname, '../..');
const METEOR_EXECUTABLE = path.join(REPO_ROOT, 'meteor');
/**
* Helper function to set up a Meteor app in a temporary directory
* Copies the app and runs npm install
* @param {string} appName - Name of the app in the apps directory
* @returns {string} - Path to the temporary directory containing the app
*/
export async function setupMeteorApp(appName) {
// Create a unique temporary directory
const randomSuffix = Math.random().toString(36).substring(2, 10);
const tempDir = path.join(os.tmpdir(), `${appName}-${randomSuffix}`);
// Source app directory
const sourceAppDir = path.join(__dirname, 'apps', appName);
console.log(`Source app directory: ${sourceAppDir}`);
console.log(`Temporary directory: ${tempDir}`);
try {
// Create the destination directory if it doesn't exist
if (!fs.existsSync(tempDir)) {
await fs.mkdir(tempDir, { recursive: true });
}
// Use fs-extra's copy method with recursive option
await fs.copy(sourceAppDir, tempDir, {
dereference: true,
preserveTimestamps: true,
overwrite: true
});
console.log(`Copied app to temporary directory: ${tempDir}`);
} catch (err) {
console.error('Error during copy:', err);
}
// Run npm install in the temporary directory
console.log('Running npm install...');
await execa.command('npm install', {
cwd: tempDir,
stdio: 'inherit',
shell: true,
});
return { tempDir };
}
/**
* Helper function to run a Meteor app
* @param {string} tempDir - Path to the directory containing the app
* @param {number} port - Port to run the app on
* @returns {Object} - The meteor process
*/
export async function runMeteorApp(tempDir, port) {
// Start Meteor CLI in dev mode
console.log(`Starting Meteor app on port ${port}...`);
const meteorProcess = execa(METEOR_EXECUTABLE, ['run', '--port', port.toString()], {
cwd: tempDir,
stdio: 'inherit',
});
// Wait for server to be up
console.log(`Waiting for app to be available on port ${port}...`);
await waitOn({
resources: [`http-get://localhost:${port}`],
timeout: 60000
});
return { meteorProcess };
}
/**
* Helper function to kill a Meteor process
* @param {Object} meteorProcess - The Meteor process to kill
* @returns {Promise<void>}
*/
export async function killMeteorProcess(meteorProcess) {
if (meteorProcess) {
try {
await meteorProcess.kill('SIGKILL');
console.log('Successfully killed meteor process');
} catch (err) {
console.log(`Error killing meteor process: ${err.message}`);
}
}
}
/**
* Kills any process running on the specified port
* @param {number} port - The port to kill processes on
* @returns {Promise<void>}
*/
export async function killProcessByPort(port) {
try {
// Different commands based on OS
const command = process.platform === 'win32'
? `FOR /F "tokens=5" %a in ('netstat -ano ^| find "LISTENING" ^| find ":${port}"') do taskkill /F /PID %a`
: `lsof -i :${port} -t | xargs -r kill -9`;
console.log(`Killing process on port ${port}...`);
try {
// Use { reject: false } to prevent execa from throwing on non-zero exit codes
const result = await execa.command(command, { shell: true, reject: false });
if (result.failed) {
// It's okay if this fails because there might not be a process on that port
console.log(`No process found on port ${port} or command returned non-zero exit code`);
} else {
console.log(`Successfully killed process on port ${port}`);
}
} catch (err) {
// This catch block will only be reached for operational errors, not for command failures
console.log(`Error executing kill command: ${err.message}`);
}
console.log(`Successfully ensured no process is running on port ${port}`);
} catch (error) {
// This should never be reached with the inner try/catch, but keeping as a safety net
console.error(`Error killing process on port ${port}:`, error);
}
}
/**
* Helper function to clean up a temporary directory
* @param {string} tempDir - Path to the temporary directory to clean up
* @returns {Promise<void>}
*/
export async function cleanupTempDir(tempDir) {
if (tempDir) {
try {
rimraf.sync(tempDir, { disableGlob: true, maxRetries: 5, retryDelay: 500 });
console.log(`Removed temporary directory: ${tempDir}`);
} catch (err) {
console.log(`Sync removal failed, trying async removal: ${err}`);
}
}
}

View File

@@ -1,107 +1,4 @@
const execa = require('execa');
const waitOn = require('wait-on');
const path = require('path');
const fs = require('fs-extra');
const os = require('os');
const rimraf = require('rimraf');
/**
* Kills any process running on the specified port
* @param {number} port - The port to kill processes on
* @returns {Promise<void>}
*/
async function killProcessByPort(port) {
try {
// Different commands based on OS
const command = process.platform === 'win32'
? `FOR /F "tokens=5" %a in ('netstat -ano ^| find "LISTENING" ^| find ":${port}"') do taskkill /F /PID %a`
: `lsof -i :${port} -t | xargs -r kill -9`;
console.log(`Killing process on port ${port}...`);
try {
// Use { reject: false } to prevent execa from throwing on non-zero exit codes
const result = await execa.command(command, { shell: true, reject: false });
if (result.failed) {
// It's okay if this fails because there might not be a process on that port
console.log(`No process found on port ${port} or command returned non-zero exit code`);
} else {
console.log(`Successfully killed process on port ${port}`);
}
} catch (err) {
// This catch block will only be reached for operational errors, not for command failures
console.log(`Error executing kill command: ${err.message}`);
}
console.log(`Successfully ensured no process is running on port ${port}`);
} catch (error) {
// This should never be reached with the inner try/catch, but keeping as a safety net
console.error(`Error killing process on port ${port}:`, error);
}
}
// Get the absolute path to the meteor executable
const REPO_ROOT = path.resolve(__dirname, '../..');
console.log('REPO_ROOT:', REPO_ROOT);
const METEOR_EXECUTABLE = path.join(REPO_ROOT, 'meteor');
console.log('METEOR_EXECUTABLE:', METEOR_EXECUTABLE);
/**
* Helper function to run a Meteor app in a temporary directory
* Uses fs-extra for file operations
* @param {string} appName - Name of the app in the apps directory
* @param {number} port - Port to run the app on
* @returns {Object} - Object containing the meteor process and temp directory path
*/
async function runMeteorApp(appName, port) {
// Create a unique temporary directory
const randomSuffix = Math.random().toString(36).substring(2, 10);
const tempDir = path.join(os.tmpdir(), `${appName}-${randomSuffix}`);
// Source app directory
const sourceAppDir = path.join(__dirname, 'apps', appName);
console.log(`Source app directory: ${sourceAppDir}`);
console.log(`Temporary directory: ${tempDir}`);
try {
// Create the destination directory if it doesn't exist
if (!fs.existsSync(tempDir)) {
await fs.mkdir(tempDir, { recursive: true });
}
// Use fs-extra's copy method with recursive option
await fs.copy(sourceAppDir, tempDir, {
dereference: true,
preserveTimestamps: true,
overwrite: true
});
console.log(`Copied app to temporary directory: ${tempDir}`);
} catch (err) {
console.error('Error during copy:', err);
}
// Run npm install in the temporary directory
console.log('Running npm install...');
await execa('npm', ['install'], {
cwd: tempDir,
stdio: 'inherit',
shell: true,
});
// Start Meteor CLI in dev mode
console.log(`Starting Meteor app on port ${port}...`);
const meteorProcess = execa(METEOR_EXECUTABLE, ['run', '--port', port.toString()], {
cwd: tempDir,
stdio: 'inherit',
});
// Wait for server to be up
console.log(`Waiting for app to be available on port ${port}...`);
await waitOn({
resources: [`http-get://localhost:${port}`],
timeout: 60000
});
return { meteorProcess, tempDir };
}
import { killProcessByPort, setupMeteorApp, runMeteorApp, cleanupTempDir, killMeteorProcess } from './helpers';
describe('rspack build and serve', () => {
let meteorProcess;
@@ -109,35 +6,22 @@ describe('rspack build and serve', () => {
const PORT = 3100;
beforeAll(async () => {
// Run the react app using our helper
const result = await runMeteorApp('react', PORT);
meteorProcess = result.meteorProcess;
tempDir = result.tempDir;
// Setup the Meteor app
tempDir = (await setupMeteorApp('react'))?.tempDir;
// Run the Meteor app
meteorProcess = (await runMeteorApp(tempDir, PORT))?.meteorProcess;
});
afterAll(async () => {
// Kill the meteor process
if (meteorProcess) {
try {
await meteorProcess.kill('SIGKILL');
} catch (err) {
console.log(`Error killing meteor process: ${err.message}`);
}
}
await killMeteorProcess(meteorProcess);
// Ensure any process on the port is killed
await killProcessByPort(PORT);
// Clean up the temporary directory
if (tempDir) {
try {
// First try synchronous rimraf
rimraf.sync(tempDir, { disableGlob: true, maxRetries: 5, retryDelay: 500 });
console.log(`Removed temporary directory: ${tempDir}`);
} catch (err) {
console.log(`Sync removal failed, trying async removal: ${err}`);
}
}
await cleanupTempDir(tempDir);
});
test('loads and has correct content', async () => {