From 85537b26bfe4fbfa74b9313dc2690823bd1beab7 Mon Sep 17 00:00:00 2001 From: seb Date: Sat, 19 Jul 2025 08:49:35 +0200 Subject: [PATCH] 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. --- src/cli.js | 62 +++++++++++++++++++++++- src/executor.js | 114 ++++++++++++++++++++++++++++++++++++++------ step1.test | 4 +- step1_dhl.test | 77 ++++++++++++++++++++++++++++++ step1_dhl_nach.test | 80 +++++++++++++++++++++++++++++++ step1_dpd.test | 77 ++++++++++++++++++++++++++++++ 6 files changed, 395 insertions(+), 19 deletions(-) create mode 100644 step1_dhl.test create mode 100644 step1_dhl_nach.test create mode 100644 step1_dpd.test diff --git a/src/cli.js b/src/cli.js index ae0517e..5a9de94 100644 --- a/src/cli.js +++ b/src/cli.js @@ -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; \ No newline at end of file diff --git a/src/executor.js b/src/executor.js index 15ec27d..1c6ae96 100644 --- a/src/executor.js +++ b/src/executor.js @@ -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 = '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 = '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 = '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 = '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) { diff --git a/step1.test b/step1.test index 663570f..4ec1ef7 100644 --- a/step1.test +++ b/step1.test @@ -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" diff --git a/step1_dhl.test b/step1_dhl.test new file mode 100644 index 0000000..1f8fc22 --- /dev/null +++ b/step1_dhl.test @@ -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" + diff --git a/step1_dhl_nach.test b/step1_dhl_nach.test new file mode 100644 index 0000000..ba5441e --- /dev/null +++ b/step1_dhl_nach.test @@ -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" + diff --git a/step1_dpd.test b/step1_dpd.test new file mode 100644 index 0000000..7b6df91 --- /dev/null +++ b/step1_dpd.test @@ -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" +