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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
96
step1_dhl_nach2.test
Normal 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"
|
||||
|
||||
Reference in New Issue
Block a user