@adobe/reactor-sdk
Version:
JavaScript SDK for the Reactor API
329 lines (297 loc) • 10.2 kB
JavaScript
/*
Copyright 2025 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
import { spawn, exec } from 'child_process';
import { promisify } from 'util';
import path from 'path';
const execAsync = promisify(exec);
let testProcess = null;
let cleanupRan = false;
let stoppingTestProcess = false;
let isShuttingDown = false;
// Cleanup function that runs the delete-test-properties.js script
async function runDeleteTestProperties() {
// prevent multiple runs
if (cleanupRan) {
return;
}
cleanupRan = true;
try {
console.log('Cleaning up test properties (delete-test-properties.js)...');
const scriptPath = path.resolve(
process.cwd(),
'scripts/delete-test-properties.js'
);
const { stdout } = await execAsync(`node ${scriptPath}`);
if (stdout) {
console.log(stdout.trim());
}
console.log('✅ Cleanup completed');
} catch (error) {
console.error('❌ Cleanup failed:', error.message);
}
}
/**
* Handle the spawned test processes
* @returns {Promise<void>}
*/
const killTestProcess = async () => {
// Already cleaning up
if (stoppingTestProcess) {
return;
}
stoppingTestProcess = true;
if (!testProcess || testProcess.exitCode !== null) {
return Promise.resolve();
}
return new Promise((resolve) => {
console.log(`🛑 Killing test process...`);
const forceKillTimeout = setTimeout(() => {
if (testProcess.exitCode === null) {
console.log(`⚡ Forcing kill of the test process...`);
testProcess.kill('SIGKILL');
}
}, 5000);
// calling SIGTERM or SIGKILL invokes this handler
testProcess.once('exit', () => {
console.log(`✅ The test process exited.`);
clearTimeout(forceKillTimeout);
resolve();
});
testProcess.kill('SIGTERM');
});
};
/**
* Kill the server and the test processes. Run cleanup script.
* @returns {Promise<void>}
*/
async function shutdown() {
if (isShuttingDown) {
return Promise.resolve();
}
isShuttingDown = true;
await killTestProcess();
await runDeleteTestProperties();
}
// Register main process handlers
process.on('SIGINT', async () => {
if (isShuttingDown) {
console.log('⚠️ Shutdown already in progress, please wait...');
return;
}
console.log('\nReceived SIGINT - cleaning up...');
await shutdown();
process.exit(0);
});
process.on('SIGTERM', async () => {
if (isShuttingDown) {
console.log('⚠️ Shutdown already in progress, please wait...');
return;
}
console.log('\nReceived SIGTERM - cleaning up...');
await shutdown();
process.exit(0);
});
process.on('uncaughtException', async (err) => {
console.error('Uncaught exception:', err);
await shutdown();
process.exit(1);
});
process.on('unhandledRejection', async (reason) => {
console.error('Unhandled rejection:', reason);
await shutdown();
process.exit(1);
});
// Register main process handlers
/**
* Spin up the test server. Spin up multiple processes to run the tests sequentially.
* @returns {Promise<void>}
*/
async function runIntegrationTests() {
try {
// Step 1: Run version check
console.log('🔍 Running version check...');
try {
const { stdout } = await execAsync('node scripts/check-version.js');
if (stdout) {
console.log(stdout.trim());
}
} catch (error) {
console.error('Version check failed:', error.message);
throw error;
}
// Step 2: Run Node.js integration tests sequentially with synchronized reporting
console.log('🚀 Running Node.js integration tests sequentially...');
const TEST_TYPES = { COMMON_JS: 'commonJS', ES_MODULE: 'esModule' };
// Buffer output for synchronized reporting
const testResults = { commonJSResult: null, esModuleResult: null };
const bundles = {
[TEST_TYPES.COMMON_JS]: 'tmp.tests/commonjs/index.cjs',
[TEST_TYPES.ES_MODULE]: 'tmp.tests/esmodule/index.js'
};
const runTestsSynchronously = async function () {
for (const testTypeKey of Object.keys(TEST_TYPES)) {
const testType = TEST_TYPES[testTypeKey];
await new Promise((resolve, reject) => {
const resultsVariable = `${testType}Result`;
// tmp.tests/.../index.c?js
const bundlePath = bundles[testType];
testProcess = spawn('jasmine', [bundlePath], {
stdio: ['inherit', 'pipe', 'pipe']
});
console.log(`Starting ${testType} tests...`);
let synchronizedReportingOutput = '';
testProcess.stdout.on('data', (data) => {
if (!stoppingTestProcess) {
const received = data.toString();
// Only show real-time output, not the summary
const lines = received.split('\n');
lines.forEach((line) => {
line = line.trim();
if (
line &&
!line.match(/^\d+ specs?, \d+ failures?/) &&
!line.match(/^Finished in \d+/) &&
!line.match(/^Pending:$/)
) {
// something like "[commonJS] build waiting after 3000ms"
console.log(`[${testType}] ${line}`);
} else {
// for synchronized reporting later
synchronizedReportingOutput += `${line}\n`;
}
});
}
});
testProcess.stderr.on('data', (data) => {
if (!stoppingTestProcess) {
console.error(`[${testType}] ${data.toString().trim()}`);
}
});
testProcess.on('error', (error) => {
if (stoppingTestProcess) {
return;
}
console.error(
`❌ Failed to start ${testType} jasmine:`,
error.message
);
killTestProcess();
reject(error);
});
testProcess.on('close', (code) => {
if (stoppingTestProcess) {
return;
}
testResults[resultsVariable] = {
code,
output: synchronizedReportingOutput
};
if (code === 0) {
console.log(
`✅ ${testType} integration tests completed successfully`
);
resolve();
} else {
console.error(
`❌ ${testType} integration tests failed with exit code ${code}`
);
killTestProcess();
reject(
new Error(`${testType} tests failed with exit code ${code}`)
);
}
});
});
}
};
const displaySynchronizedResults = () => {
// Extract and display summary information for both
function extractSummary(output, label) {
if (!output?.length) {
return;
}
const lines = output.split('\n');
const summaryLine = lines.find((line) =>
line.match(/^\d+ specs?, \d+ failures?/)
);
const timeLine = lines.find((line) => line.match(/^Finished in \d+/));
console.log(`\n${label}:`);
if (summaryLine) console.log(` ${summaryLine.trim()}`);
if (timeLine) console.log(` ${timeLine.trim()}`);
}
// Show pending tests if any
function showPending(output, label) {
if (!output?.length) {
return;
}
const lines = output.split('\n');
const pendingIndex = lines.findIndex(
(line) => line.trim() === 'Pending:'
);
if (pendingIndex !== -1) {
console.log(`\n${label} Pending Tests:`);
for (let i = pendingIndex + 1; i < lines.length; i++) {
const line = lines[i].trim();
if (
line &&
!line.match(/^\d+ specs?, \d+ failures?/) &&
!line.match(/^Finished in \d+/)
) {
console.log(` ${line}`);
}
}
}
}
console.log('\n' + '='.repeat(80));
console.log('📊 SYNCHRONIZED TEST RESULTS\n');
extractSummary(testResults.commonJSResult?.output, '📦 CommonJS Results');
extractSummary(
testResults.esModuleResult?.output,
'📦 ES Module Results'
);
console.log('\n' + '='.repeat(80));
console.log('\n' + '='.repeat(80));
console.log('📊 PENDING TEST(S) RESULTS\n');
showPending(testResults.commonJSResult?.output, '📦 CommonJS');
showPending(testResults.esModuleResult?.output, '📦 ES Module Results');
console.log('\n' + '='.repeat(80));
};
try {
await runTestsSynchronously();
// Display synchronized results
displaySynchronizedResults();
console.log('🎉 All integration tests completed successfully!');
} catch (error) {
console.error('❌ One or more test suites failed:', error.message);
// Still show results if we have them
if (testResults.commonJSResult || testResults.esModuleResult) {
displaySynchronizedResults();
}
// Ensure all processes are terminated
await killTestProcess();
throw error;
}
// Step 6: Graceful shutdown and cleanup
console.log('✅ Tests completed, shutting down server and cleaning up...');
await shutdown();
process.exit(0);
} catch (error) {
console.error('❌ Integration tests failed:', error.message);
await shutdown();
process.exit(1);
}
}
// Only run if this script is executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
runIntegrationTests();
}
export { runIntegrationTests, shutdown };