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"
+