Enhance error handling and graceful shutdown in TestExecutor; implement browser disconnection checks and improve command execution flow. Update TestLinter to provide better guidance on 'use' command placement. Add support for 'for' and 'aria-label' attributes in TestParser.

This commit is contained in:
seb
2025-07-26 12:50:56 +02:00
parent 9e73900197
commit af4b774fea
5 changed files with 216 additions and 11 deletions

View File

@@ -163,7 +163,7 @@ async function main() {
currentRunner = runner; // Store reference for cleanup
try {
await runner.runTestFile(testFile, profile);
await runner.runTestFile(testFile, profile);
} catch (error) {
console.error('❌ Test execution failed:', error.message);

View File

@@ -89,7 +89,21 @@ class TestExecutor {
break;
}
const jumpCount = await this.executeCommand(command);
let jumpCount = 0;
try {
jumpCount = await this.executeCommand(command);
} catch (error) {
// Check if this is a browser disconnection error
if (error.message.includes('Target page, context or browser has been closed') ||
error.message.includes('Browser has been closed') ||
error.message.includes('Context was closed')) {
console.log('🔔 Browser was closed during command execution - stopping gracefully');
this.shouldStop = true;
break;
} else {
throw error; // Re-throw other errors
}
}
// Handle jump commands
if (jumpCount > 0) {
@@ -122,8 +136,16 @@ class TestExecutor {
}
} catch (error) {
console.error('Test execution failed:', error);
throw error;
// Check if this is a browser disconnection error
if (error.message.includes('Target page, context or browser has been closed') ||
error.message.includes('Browser has been closed') ||
error.message.includes('Context was closed')) {
console.log('🔔 Browser was closed by user - stopping test execution gracefully');
this.shouldStop = true;
} else {
console.error('Test execution failed:', error);
throw error;
}
} finally {
await this.cleanup();
}
@@ -150,6 +172,12 @@ class TestExecutor {
]
});
// Handle main browser disconnection
this.browser.on('disconnected', () => {
console.log('🔔 Main browser disconnected - stopping test execution');
this.shouldStop = true;
});
this.context = await this.browser.newContext({
viewport: profile.viewport,
userAgent: profile.userAgent,
@@ -242,6 +270,13 @@ class TestExecutor {
return 0;
}
// Check if browser is still connected
if (!this.browser || !this.browser.isConnected()) {
console.log('🛑 Skipping command execution - browser disconnected');
this.shouldStop = true;
return 0;
}
// Create a clean one-line representation of the command
const commandStr = this.formatCommandForOutput(command);
console.log(`Executing: ${commandStr}`);
@@ -805,6 +840,15 @@ class TestExecutor {
]
});
// Handle status browser disconnection
this.statusBrowser.on('disconnected', () => {
console.log('🔔 Status browser disconnected - stopping test execution');
this.shouldStop = true;
this.statusPage = null;
this.statusContext = null;
this.statusBrowser = null;
});
// Create a context and page for the status window
this.statusContext = await this.statusBrowser.newContext({
viewport: { width: 700, height: 680 }
@@ -812,6 +856,13 @@ class TestExecutor {
this.statusPage = await this.statusContext.newPage();
// Handle status window close event
this.statusPage.on('close', () => {
console.log('🔔 Status window closed by user - stopping test execution');
this.shouldStop = true;
this.statusPage = null;
});
// Create the status window HTML
const statusHTML = `
<!DOCTYPE html>
@@ -1290,7 +1341,7 @@ class TestExecutor {
}
} else {
// When running, show current command
if (commandEl) commandEl.textContent = 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';
@@ -1538,6 +1589,18 @@ class TestExecutor {
selector += ` href="${command.href}"`;
}
if (command.htmlType) {
selector += ` type="${command.htmlType}"`;
}
if (command.for) {
selector += ` for="${command.for}"`;
}
if (command.ariaLabel) {
selector += ` aria-label="${command.ariaLabel}"`;
}
if (command.child) {
// Handle new parentheses syntax
if (command.child.startsWith('child=')) {
@@ -1606,6 +1669,14 @@ class TestExecutor {
selector += `[type="${params.htmlType}"]`;
}
if (params.for) {
selector += `[for="${params.for}"]`;
}
if (params.ariaLabel) {
selector += `[aria-label="${params.ariaLabel}"]`;
}
// Handle child selectors (nested elements)
if (params.child) {
// Parse child selector like "span class="MuiBadge-badge""
@@ -1661,6 +1732,20 @@ class TestExecutor {
selector += `[href="${href}"]`;
}
// Extract for attribute
const forMatch = childString.match(/for=(?:"([^"]*)"|([^\s]+))/);
if (forMatch) {
const forValue = forMatch[1] || forMatch[2];
selector += `[for="${forValue}"]`;
}
// Extract aria-label attribute
const ariaLabelMatch = childString.match(/aria-label=(?:"([^"]*)"|([^\s]+))/);
if (ariaLabelMatch) {
const ariaLabelValue = ariaLabelMatch[1] || ariaLabelMatch[2];
selector += `[aria-label="${ariaLabelValue}"]`;
}
// Extract childText for the child element
const childTextMatch = childString.match(/childText=(?:"([^"]*)"|([^\s]+))/);
if (childTextMatch) {
@@ -1739,10 +1824,10 @@ class TestExecutor {
// Extract simplified DOM structure with only essential elements and attributes
const simplifiedStructure = await this.page.evaluate(() => {
// Elements we want to keep
const keepElements = ['button', 'input', 'textarea', 'span', 'a'];
const keepElements = ['button', 'input', 'textarea', 'span', 'a', 'label'];
// Attributes we want to keep
const keepAttributes = ['id', 'name', 'value', 'type'];
const keepAttributes = ['id', 'name', 'value', 'type', 'for', 'aria-label'];
function processElement(element) {
const tagName = element.tagName.toLowerCase();

View File

@@ -400,23 +400,33 @@ class TestLinter {
validateFlowLogic(lines) {
let hasUseCommand = false;
let useLineNumber = 0;
let firstCommandLineIndex = -1;
for (const line of lines) {
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const command = line.cleaned.split(' ')[0];
// Track the first command line (index in the cleaned lines array)
if (firstCommandLineIndex === -1) {
firstCommandLineIndex = i;
}
if (command === 'use') {
if (hasUseCommand) {
this.addWarning(`Multiple 'use' commands found. Consider using multi-profile syntax instead.`, line.lineNumber);
}
hasUseCommand = true;
useLineNumber = line.lineNumber;
// Check if this is NOT the first command in the cleaned lines
if (i > 0) {
this.addInfo(`'use' command found at line ${useLineNumber}. Consider placing it at the beginning of the test.`, useLineNumber);
}
}
}
if (!hasUseCommand) {
this.addWarning(`No 'use' command found. Tests should specify browser profile.`, 1);
} else if (useLineNumber > 1) {
this.addInfo(`'use' command found at line ${useLineNumber}. Consider placing it at the beginning of the test.`, useLineNumber);
}
}

View File

@@ -107,6 +107,8 @@ class TestParser {
class: params.class,
href: params.href,
htmlType: params.type, // Store HTML type as htmlType to avoid conflict
for: params.for,
ariaLabel: params['aria-label'],
child: params.child,
childText: params.childText
};
@@ -124,6 +126,8 @@ class TestParser {
class: params.class,
href: params.href,
htmlType: params.type, // Store HTML type as htmlType to avoid conflict
for: params.for,
ariaLabel: params['aria-label'],
child: params.child,
childText: params.childText
};
@@ -141,6 +145,8 @@ class TestParser {
class: params.class,
href: params.href,
htmlType: params.type, // Store HTML type as htmlType to avoid conflict
for: params.for,
ariaLabel: params['aria-label'],
child: params.child,
childText: params.childText
};
@@ -159,6 +165,8 @@ class TestParser {
class: params.class,
href: params.href,
htmlType: params.type, // Store HTML type as htmlType to avoid conflict
for: params.for,
ariaLabel: params['aria-label'],
child: params.child,
childText: params.childText
};
@@ -210,6 +218,8 @@ class TestParser {
class: params.class,
href: params.href,
htmlType: params.type,
for: params.for,
ariaLabel: params['aria-label'],
child: params.child,
childText: params.childText
};
@@ -233,6 +243,8 @@ class TestParser {
class: params.class,
href: params.href,
htmlType: params.type,
for: params.for,
ariaLabel: params['aria-label'],
child: params.child,
childText: params.childText
};
@@ -269,6 +281,8 @@ class TestParser {
class: params.class,
href: params.href,
htmlType: params.type,
for: params.for,
ariaLabel: params['aria-label'],
child: params.child,
childText: params.childText,
target: params.target
@@ -323,7 +337,7 @@ class TestParser {
}
parseSimpleParameters(paramString, params) {
const regex = /(\w+)=(?:"([^"]*)"|([^\s]+))/g;
const regex = /([\w-]+)=(?:"([^"]*)"|([^\s]+))/g;
let match;
while ((match = regex.exec(paramString)) !== null) {

96
step1_dhl_nach2.test Normal file
View File

@@ -0,0 +1,96 @@
/*
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
node src/cli.js step1_dhl_nach2.test Chrome --headed
*/
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"
click element=a href=/Kategorie/Zelte
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=button type="button" aria-label="Menge erhöhen"
click element=button type="button" aria-label="Menge erhöhen"
sleep 200 "menge erhoehen click"
wait element=span class="MuiBadge-badge" childText="3"
click element=button child=span(class="MuiBadge-badge" childText="3")
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"