Implement graceful shutdown handling in CLI; add cleanup logic to TestRunner and Executor for improved resource management during termination. Update sleep durations in step1.test for consistency.

This commit is contained in:
seb
2025-07-19 08:49:35 +02:00
parent 140852be07
commit 85537b26bf
6 changed files with 395 additions and 19 deletions

View File

@@ -79,8 +79,47 @@ class TestRunner {
process.exit(1);
}
}
async cleanup() {
if (this.executor) {
await this.executor.cleanup();
}
}
}
// Global reference to runner for cleanup
let currentRunner = null;
// Signal handler for clean shutdown
async function handleShutdown(signal) {
console.log(`\n🛑 Received ${signal}, shutting down gracefully...`);
if (currentRunner) {
// Signal the executor to stop
if (currentRunner.executor) {
currentRunner.executor.shouldStop = true;
}
try {
await currentRunner.cleanup();
console.log('✅ Browser cleanup completed');
} catch (error) {
console.log('⚠️ Browser cleanup had issues:', error.message);
}
}
console.log('👋 Goodbye!');
// Give a moment for any remaining cleanup, then exit
setTimeout(() => {
process.exit(0);
}, 100);
}
// Register signal handlers
process.on('SIGINT', handleShutdown); // Ctrl+C
process.on('SIGTERM', handleShutdown); // Termination signal
// CLI handling
async function main() {
const args = process.argv.slice(2);
@@ -118,13 +157,32 @@ async function main() {
}
const runner = new TestRunner({ headless, disableAnimations, fullPageScreenshots, lint, strict });
currentRunner = runner; // Store reference for cleanup
await runner.runTestFile(testFile, profile);
try {
await runner.runTestFile(testFile, profile);
} catch (error) {
console.error('❌ Test execution failed:', error.message);
// Clean up on error
try {
await runner.cleanup();
} catch (cleanupError) {
// Ignore cleanup errors
}
process.exit(1);
} finally {
currentRunner = null;
}
}
// Run if called directly
if (require.main === module) {
main().catch(console.error);
main().catch((error) => {
console.error('❌ Unexpected error:', error.message);
process.exit(1);
});
}
module.exports = TestRunner;

View File

@@ -16,6 +16,7 @@ class TestExecutor {
this.fullPageScreenshots = options.fullPageScreenshots || false; // Default to viewport screenshots
this.enableScreenshots = options.enableScreenshots !== false; // Default to enable screenshots
this.variables = {}; // Store extracted variables
this.shouldStop = false; // Flag to stop execution on shutdown
this.profiles = {
Chrome: {
@@ -44,6 +45,12 @@ class TestExecutor {
}
for (let i = 0; i < commands.length; i++) {
// Check if we should stop execution (e.g., due to Ctrl+C)
if (this.shouldStop) {
console.log('🛑 Stopping test execution due to shutdown request');
break;
}
const command = commands[i];
// Check for pause before executing command (only for interactive commands)
@@ -58,7 +65,16 @@ class TestExecutor {
// Update status before executing command
if (!this.headless) {
await this.updateStatus(command, false);
// Find next interactive command for display when paused
let nextInteractiveCommand = null;
for (let j = i; j < commands.length; j++) {
if (this.isInteractiveCommand(commands[j])) {
nextInteractiveCommand = commands[j];
break;
}
}
await this.updateStatus(command, false, nextInteractiveCommand);
}
// Check for manual dump requests
@@ -66,6 +82,12 @@ class TestExecutor {
await this.checkDumpRequests();
}
// Check again if we should stop before executing the command
if (this.shouldStop) {
console.log('🛑 Stopping test execution due to shutdown request');
break;
}
const jumpCount = await this.executeCommand(command);
// Handle jump commands
@@ -76,7 +98,16 @@ class TestExecutor {
// Update status after completing command
if (!this.headless) {
await this.updateStatus(command, true);
// Find next interactive command for display when paused
let nextInteractiveCommand = null;
for (let j = i + 1; j < commands.length; j++) {
if (this.isInteractiveCommand(commands[j])) {
nextInteractiveCommand = commands[j];
break;
}
}
await this.updateStatus(command, true, nextInteractiveCommand);
// If this was a step request, pause again after this interactive command
if (this.shouldPauseAfterStep && this.isInteractiveCommand(command)) {
@@ -204,6 +235,12 @@ class TestExecutor {
}
async executeCommand(command) {
// Check if we should stop before executing any command
if (this.shouldStop) {
console.log('🛑 Skipping command execution due to shutdown request');
return 0;
}
// Create a clean one-line representation of the command
const commandStr = this.formatCommandForOutput(command);
console.log(`Executing: ${commandStr}`);
@@ -1003,6 +1040,7 @@ class TestExecutor {
const button = this;
const stepBtn = document.getElementById('step-btn');
const indicator = document.getElementById('status-indicator');
const titleEl = document.querySelector('.status-section .status-title');
if (isPaused) {
button.textContent = '▶️ Resume';
@@ -1010,6 +1048,11 @@ class TestExecutor {
indicator.className = 'status-indicator status-paused';
stepBtn.disabled = false;
// Update title to show next command
if (titleEl) {
titleEl.innerHTML = '<span id="status-indicator" class="status-indicator status-paused"></span>Next Command (+1 Step)';
}
// Store pause state in window to communicate with executor
window.playwrongPaused = true;
} else {
@@ -1018,6 +1061,11 @@ class TestExecutor {
indicator.className = 'status-indicator status-running';
stepBtn.disabled = true;
// Update title to show current command
if (titleEl) {
titleEl.innerHTML = '<span id="status-indicator" class="status-indicator status-running"></span>Current Command';
}
// Resume execution
window.playwrongPaused = false;
}
@@ -1106,6 +1154,12 @@ class TestExecutor {
// Wait until user resumes, but also check for dump requests and step requests while paused
while (true) {
// Check if we should stop execution (e.g., due to Ctrl+C)
if (this.shouldStop) {
console.log('🛑 Breaking out of pause due to shutdown request');
return false;
}
await this.page.waitForTimeout(500); // Check every 500ms
// Check for dump requests even while paused
@@ -1201,7 +1255,7 @@ class TestExecutor {
}
}
async updateStatus(command, isCompleted = false) {
async updateStatus(command, isCompleted = false, nextInteractiveCommand = null) {
if (!this.statusPage || this.headless) return;
try {
@@ -1220,10 +1274,27 @@ class TestExecutor {
const progress = this.totalCommands > 0 ? (this.completedCommands / this.totalCommands) * 100 : 0;
await this.statusPage.evaluate(({ command, completed, total, elapsed, progress }) => {
// Update current command
await this.statusPage.evaluate(({ command, nextInteractiveCommand, completed, total, elapsed, progress }) => {
// Update command display based on pause state
const commandEl = document.getElementById('current-command');
if (commandEl) commandEl.textContent = command;
const indicator = document.getElementById('status-indicator');
const titleEl = document.querySelector('.status-section .status-title');
if (window.playwrongPaused && nextInteractiveCommand) {
// When paused, show next interactive command
if (commandEl) commandEl.textContent = nextInteractiveCommand;
if (indicator) indicator.className = 'status-indicator status-paused';
if (titleEl) {
titleEl.innerHTML = '<span id="status-indicator" class="status-indicator status-paused"></span>Next Command (+1 Step)';
}
} else {
// When running, show current command
if (commandEl) commandEl.textContent = command;
if (indicator) indicator.className = 'status-indicator status-running';
if (titleEl) {
titleEl.innerHTML = '<span id="status-indicator" class="status-indicator status-running"></span>Current Command';
}
}
// Update statistics
const completedEl = document.getElementById('completed-count');
@@ -1239,14 +1310,9 @@ class TestExecutor {
const progressEl = document.getElementById('progress-fill');
if (progressEl) progressEl.style.width = progress + '%';
// Update status indicator (only if not paused)
const indicator = document.getElementById('status-indicator');
if (indicator && !window.playwrongPaused) {
indicator.className = 'status-indicator status-running';
}
}, {
command: this.formatCommandForOutput(command),
nextInteractiveCommand: nextInteractiveCommand ? this.formatCommandForOutput(nextInteractiveCommand) : null,
completed: this.completedCommands,
total: this.totalCommands,
elapsed: elapsedSeconds,
@@ -1925,9 +1991,27 @@ class TestExecutor {
// Ignore errors when closing status browser
}
}
if (this.page) await this.page.close();
if (this.context) await this.context.close();
if (this.browser) await this.browser.close();
if (this.page) {
try {
await this.page.close();
} catch (error) {
// Ignore errors when closing page
}
}
if (this.context) {
try {
await this.context.close();
} catch (error) {
// Ignore errors when closing context
}
}
if (this.browser) {
try {
await this.browser.close();
} catch (error) {
// Ignore errors when closing browser
}
}
}
resolveEnvironmentVariables(value) {

View File

@@ -62,12 +62,12 @@ sleep 100 "stadt fill"
wait element=textarea name="note"
scroll element=textarea name="note"
fill element=textarea name="note" value="Musteranmerkung"
sleep 1000000 "note fill"
sleep 100 "note fill"
scroll element=button childText="Bestellung abschließen"
wait element=label childText="Bestimmungen"
click element=label childText="Bestimmungen"
sleep 100 "checkbox checked"
wait element=button childText="Bestellung abschließen"
click element=button childText="Bestellung abschließen"
sleep 300000 "order completion"
sleep 3000 "order completion"

77
step1_dhl.test Normal file
View File

@@ -0,0 +1,77 @@
/*
PlayWrong Test - Complete E-commerce Flow
This test demonstrates the full purchase flow from product selection to order completion
with beautiful animations and environment variable support
Part 1: Load Growheads, put one item in the cart and go to checkout
Part 2: Fill in the checkout form
Part 3: Login to the email account
*/
use "Chrome"
# Part 1 - Load Growheads, put one item in the cart and go to checkout
open "https://dev.seedheads.de"
sleep 200 "page load"
wait element=a href=/Kategorie/Seeds
click element=a href=/Kategorie/Seeds
sleep 200 "seed click"
wait element=button childText="In den Korb"
click element=button childText="In den Korb"
sleep 200 "in korb click"
wait element=span class="MuiBadge-badge" childText="1"
click element=button child=span(class="MuiBadge-badge" childText="1")
sleep 200 "korb click"
wait element=button childText="Weiter zur Kasse"
click element=button childText="Weiter zur Kasse"
sleep 200 "weiter click"
wait element=input type="email"
fill element=input type="email" value="autotest@growheads.de"
sleep 200 "email fill"
wait element=input type="password"
fill element=input type="password" value="$PASSWORD"
sleep 200 "password fill"
wait element=button childText="ANMELDEN" class="MuiButton-fullWidth"
click element=button childText="ANMELDEN" class="MuiButton-fullWidth"
sleep 2000 "anmelden click"
#dump "isServer"
jumpIfNot element=span childText="Server-Warenkorb löschen" jump=2
click element=button childText="Weiter"
sleep 2000 "anmelden click"
# Part 2 - Fill in the checkout form
wait element=input name="firstName"
scroll element=span childText="Vorname"
fill element=input name="firstName" value="Max"
sleep 100 "vorname fill"
wait element=input name="lastName"
fill element=input name="lastName" value="Muster"
sleep 100 "nachname fill"
wait element=input name="street"
fill element=input name="street" value="Muster"
sleep 100 "strasse fill"
wait element=input name="houseNumber"
fill element=input name="houseNumber" value="420"
sleep 100 "hausnummer fill"
wait element=input name="postalCode"
fill element=input name="postalCode" value="42023"
sleep 100 "plz fill"
wait element=input name="city"
fill element=input name="city" value="Muster"
sleep 100 "stadt fill"
wait element=textarea name="note"
scroll element=textarea name="note"
fill element=textarea name="note" value="Musteranmerkung"
sleep 100 "note fill"
wait element=input id="DHL" name="deliveryMethod" value="DHL" type="radio"
scroll element=input id="DHL" name="deliveryMethod" value="DHL" type="radio"
click element=input id="DHL" name="deliveryMethod" value="DHL" type="radio"
sleep 100 "dhl click"
wait element=label childText="Bestimmungen"
scroll element=button childText="Bestellung abschließen"
click element=label childText="Bestimmungen"
sleep 100 "checkbox checked"
wait element=button childText="Bestellung abschließen"
click element=button childText="Bestellung abschließen"
sleep 3000 "order completion"

80
step1_dhl_nach.test Normal file
View File

@@ -0,0 +1,80 @@
/*
PlayWrong Test - Complete E-commerce Flow
This test demonstrates the full purchase flow from product selection to order completion
with beautiful animations and environment variable support
Part 1: Load Growheads, put one item in the cart and go to checkout
Part 2: Fill in the checkout form
Part 3: Login to the email account
*/
use "Chrome"
# Part 1 - Load Growheads, put one item in the cart and go to checkout
open "https://dev.seedheads.de"
sleep 200 "page load"
wait element=a href=/Kategorie/Seeds
click element=a href=/Kategorie/Seeds
sleep 200 "seed click"
wait element=button childText="In den Korb"
click element=button childText="In den Korb"
sleep 200 "in korb click"
wait element=span class="MuiBadge-badge" childText="1"
click element=button child=span(class="MuiBadge-badge" childText="1")
sleep 200 "korb click"
wait element=button childText="Weiter zur Kasse"
click element=button childText="Weiter zur Kasse"
sleep 200 "weiter click"
wait element=input type="email"
fill element=input type="email" value="autotest@growheads.de"
sleep 200 "email fill"
wait element=input type="password"
fill element=input type="password" value="$PASSWORD"
sleep 200 "password fill"
wait element=button childText="ANMELDEN" class="MuiButton-fullWidth"
click element=button childText="ANMELDEN" class="MuiButton-fullWidth"
sleep 2000 "anmelden click"
#dump "isServer"
jumpIfNot element=span childText="Server-Warenkorb löschen" jump=2
click element=button childText="Weiter"
sleep 2000 "anmelden click"
# Part 2 - Fill in the checkout form
wait element=input name="firstName"
scroll element=span childText="Vorname"
fill element=input name="firstName" value="Max"
sleep 100 "vorname fill"
wait element=input name="lastName"
fill element=input name="lastName" value="Muster"
sleep 100 "nachname fill"
wait element=input name="street"
fill element=input name="street" value="Muster"
sleep 100 "strasse fill"
wait element=input name="houseNumber"
fill element=input name="houseNumber" value="420"
sleep 100 "hausnummer fill"
wait element=input name="postalCode"
fill element=input name="postalCode" value="42023"
sleep 100 "plz fill"
wait element=input name="city"
fill element=input name="city" value="Muster"
sleep 100 "stadt fill"
wait element=textarea name="note"
scroll element=textarea name="note"
fill element=textarea name="note" value="Musteranmerkung"
sleep 100 "note fill"
wait element=input id="DHL" name="deliveryMethod" value="DHL" type="radio"
scroll element=input id="DHL" name="deliveryMethod" value="DHL" type="radio"
click element=input id="DHL" name="deliveryMethod" value="DHL" type="radio"
sleep 100 "dhl click"
wait element=input id="onDelivery" name="paymentMethod" value="onDelivery" type="radio"
scroll element=input id="onDelivery" name="paymentMethod" value="onDelivery" type="radio"
click element=input id="onDelivery" name="paymentMethod" value="onDelivery" type="radio"
sleep 100 "on delivery click"
scroll element=button childText="Bestellung abschließen"
click element=label childText="Bestimmungen"
sleep 100 "checkbox checked"
wait element=button childText="Bestellung abschließen"
click element=button childText="Bestellung abschließen"
sleep 3000 "order completion"

77
step1_dpd.test Normal file
View File

@@ -0,0 +1,77 @@
/*
PlayWrong Test - Complete E-commerce Flow
This test demonstrates the full purchase flow from product selection to order completion
with beautiful animations and environment variable support
Part 1: Load Growheads, put one item in the cart and go to checkout
Part 2: Fill in the checkout form
Part 3: Login to the email account
*/
use "Chrome"
# Part 1 - Load Growheads, put one item in the cart and go to checkout
open "https://dev.seedheads.de"
sleep 200 "page load"
wait element=a href=/Kategorie/Seeds
click element=a href=/Kategorie/Seeds
sleep 200 "seed click"
wait element=button childText="In den Korb"
click element=button childText="In den Korb"
sleep 200 "in korb click"
wait element=span class="MuiBadge-badge" childText="1"
click element=button child=span(class="MuiBadge-badge" childText="1")
sleep 200 "korb click"
wait element=button childText="Weiter zur Kasse"
click element=button childText="Weiter zur Kasse"
sleep 200 "weiter click"
wait element=input type="email"
fill element=input type="email" value="autotest@growheads.de"
sleep 200 "email fill"
wait element=input type="password"
fill element=input type="password" value="$PASSWORD"
sleep 200 "password fill"
wait element=button childText="ANMELDEN" class="MuiButton-fullWidth"
click element=button childText="ANMELDEN" class="MuiButton-fullWidth"
sleep 2000 "anmelden click"
#dump "isServer"
jumpIfNot element=span childText="Server-Warenkorb löschen" jump=2
click element=button childText="Weiter"
sleep 2000 "anmelden click"
# Part 2 - Fill in the checkout form
wait element=input name="firstName"
scroll element=span childText="Vorname"
fill element=input name="firstName" value="Max"
sleep 100 "vorname fill"
wait element=input name="lastName"
fill element=input name="lastName" value="Muster"
sleep 100 "nachname fill"
wait element=input name="street"
fill element=input name="street" value="Muster"
sleep 100 "strasse fill"
wait element=input name="houseNumber"
fill element=input name="houseNumber" value="420"
sleep 100 "hausnummer fill"
wait element=input name="postalCode"
fill element=input name="postalCode" value="42023"
sleep 100 "plz fill"
wait element=input name="city"
fill element=input name="city" value="Muster"
sleep 100 "stadt fill"
wait element=textarea name="note"
scroll element=textarea name="note"
fill element=textarea name="note" value="Musteranmerkung"
sleep 100 "note fill"
wait element=input id="DPD" name="deliveryMethod" value="DPD" type="radio"
scroll element=input id="DPD" name="deliveryMethod" value="DPD" type="radio"
click element=input id="DPD" name="deliveryMethod" value="DPD" type="radio"
sleep 100 "dpd click"
wait element=label childText="Bestimmungen"
scroll element=button childText="Bestellung abschließen"
click element=label childText="Bestimmungen"
sleep 100 "checkbox checked"
wait element=button childText="Bestellung abschließen"
click element=button childText="Bestellung abschließen"
sleep 3000 "order completion"