diff --git a/src/cli.js b/src/cli.js index 95b95ab..ee465b0 100644 --- a/src/cli.js +++ b/src/cli.js @@ -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); diff --git a/src/executor.js b/src/executor.js index dc782ff..b47814a 100644 --- a/src/executor.js +++ b/src/executor.js @@ -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 = ` @@ -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 = '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(); diff --git a/src/linter.js b/src/linter.js index 76d0e5e..a929c90 100644 --- a/src/linter.js +++ b/src/linter.js @@ -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); } } diff --git a/src/parser.js b/src/parser.js index 13a103e..87f9d25 100644 --- a/src/parser.js +++ b/src/parser.js @@ -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) { diff --git a/step1_dhl_nach2.test b/step1_dhl_nach2.test new file mode 100644 index 0000000..befa343 --- /dev/null +++ b/step1_dhl_nach2.test @@ -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" +