diff --git a/src/executor.js b/src/executor.js index d50b74a..15ec27d 100644 --- a/src/executor.js +++ b/src/executor.js @@ -46,11 +46,26 @@ class TestExecutor { for (let i = 0; i < commands.length; i++) { const command = commands[i]; + // Check for pause before executing command (only for interactive commands) + if (!this.headless && this.isInteractiveCommand(command)) { + const wasStepRequested = await this.checkPauseState(); + + // If this was a step request, set pause state after this command + if (wasStepRequested) { + this.shouldPauseAfterStep = true; + } + } + // Update status before executing command if (!this.headless) { await this.updateStatus(command, false); } + // Check for manual dump requests + if (!this.headless) { + await this.checkDumpRequests(); + } + const jumpCount = await this.executeCommand(command); // Handle jump commands @@ -62,6 +77,15 @@ class TestExecutor { // Update status after completing command if (!this.headless) { await this.updateStatus(command, true); + + // If this was a step request, pause again after this interactive command + if (this.shouldPauseAfterStep && this.isInteractiveCommand(command)) { + this.shouldPauseAfterStep = false; + await this.statusPage.evaluate(() => { + window.playwrongPaused = true; + }); + console.log('⏸️ Paused after executing one interactive step'); + } } } @@ -86,6 +110,7 @@ class TestExecutor { headless: this.headless, args: this.headless ? [] : [ `--window-size=${profile.viewport.width},${profile.viewport.height + 100}`, // Add space for browser chrome + '--window-position=0,0', // Position main window at top-left '--disable-web-security', '--disable-features=VizDisplayCompositor', '--disable-infobars', @@ -725,12 +750,28 @@ class TestExecutor { async createStatusWindow() { try { - // Create a separate browser context for the status window (opens in new window) - this.statusContext = await this.browser.newContext({ - viewport: { width: 700, height: 500 } + // Calculate position for status window (to the right of main window) + const mainWindowWidth = this.currentProfile.viewport.width; + const statusWindowX = mainWindowWidth + 50; // 50px gap between windows + + // Launch a separate browser instance for the status window + this.statusBrowser = await chromium.launch({ + headless: false, + args: [ + '--window-size=700,680', + `--window-position=${statusWindowX},0`, // Position to the right + '--disable-web-security', + '--disable-features=VizDisplayCompositor', + '--disable-infobars', + '--disable-extensions' + ] + }); + + // Create a context and page for the status window + this.statusContext = await this.statusBrowser.newContext({ + viewport: { width: 700, height: 680 } }); - // Create a page in the new context (this will be in a separate window) this.statusPage = await this.statusContext.newPage(); // Create the status window HTML @@ -827,12 +868,80 @@ class TestExecutor { .status-running { background: #4CAF50; animation: pulse 2s infinite; } .status-waiting { background: #FF9800; } .status-error { background: #F44336; } + .status-paused { background: #FF5722; } @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } } + + .controls-section { + display: flex; + gap: 10px; + margin-top: 15px; + } + + .control-button { + flex: 1; + padding: 12px; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + background: rgba(255,255,255,0.2); + color: white; + backdrop-filter: blur(5px); + } + + .control-button:hover { + background: rgba(255,255,255,0.3); + transform: translateY(-2px); + } + + .control-button:active { + transform: translateY(0); + } + + .control-button.pause-btn { + background: rgba(255, 87, 34, 0.8); + } + + .control-button.pause-btn:hover { + background: rgba(255, 87, 34, 1); + } + + .control-button.resume-btn { + background: rgba(76, 175, 80, 0.8); + } + + .control-button.resume-btn:hover { + background: rgba(76, 175, 80, 1); + } + + .control-button.dump-btn { + background: rgba(33, 150, 243, 0.8); + } + + .control-button.dump-btn:hover { + background: rgba(33, 150, 243, 1); + } + + .control-button.step-btn { + background: rgba(156, 39, 176, 0.8); + } + + .control-button.step-btn:hover { + background: rgba(156, 39, 176, 1); + } + + .control-button:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; + } @@ -869,7 +978,91 @@ class TestExecutor { + +
+
Controls
+
+ + + +
+
+ + +
+
+ `; @@ -889,6 +1082,125 @@ class TestExecutor { } } + isInteractiveCommand(command) { + const interactiveCommands = ['fill', 'click', 'open', 'scroll', 'extract']; + return interactiveCommands.includes(command.type); + } + + async checkPauseState() { + if (!this.statusPage || this.headless) return false; + + try { + // Check if status page is still valid + if (this.statusPage.isClosed()) { + this.statusPage = null; + return false; + } + + const isPaused = await this.statusPage.evaluate(() => { + return window.playwrongPaused === true; + }); + + if (isPaused) { + console.log('⏸️ Test execution paused by user'); + + // Wait until user resumes, but also check for dump requests and step requests while paused + while (true) { + await this.page.waitForTimeout(500); // Check every 500ms + + // Check for dump requests even while paused + await this.checkDumpRequests(); + + // Check for step requests + const stepRequested = await this.statusPage.evaluate(() => { + if (window.playwrongStepRequest) { + window.playwrongStepRequest = false; + return true; + } + return false; + }); + + if (stepRequested) { + console.log('⏭️ Executing one step by user request'); + return true; // Return true to indicate this was a step request + } + + const stillPaused = await this.statusPage.evaluate(() => { + return window.playwrongPaused === true; + }); + + if (!stillPaused) { + console.log('▶️ Test execution resumed by user'); + break; + } + } + } + } catch (error) { + // Silently ignore pause check errors + } + + return false; + } + + async checkDumpRequests() { + if (!this.statusPage || this.headless) return; + + try { + // Check if status page is still valid + if (this.statusPage.isClosed()) { + this.statusPage = null; + return; + } + + const dumpRequest = await this.statusPage.evaluate(() => { + const request = window.playwrongDumpRequest; + if (request) { + window.playwrongDumpRequest = null; + return request; + } + return null; + }); + + // Debug: Log when we check for dump requests + if (process.env.DEBUG_DUMP) { + console.log(`🔍 Checking for dump requests: ${dumpRequest ? 'Found' : 'None'}`); + } + + if (dumpRequest) { + console.log(`📸 Manual dump requested: ${dumpRequest.name} (${dumpRequest.type})`); + + try { + // Temporarily override screenshot settings for this dump + const originalScreenshots = this.enableScreenshots; + const originalFullPage = this.fullPageScreenshots; + + this.enableScreenshots = true; + + if (dumpRequest.type === 'screen') { + this.fullPageScreenshots = false; + } else if (dumpRequest.type === 'fullscreen') { + this.fullPageScreenshots = true; + } else { + // For 'standard' dump, use original screenshot settings + this.enableScreenshots = originalScreenshots; + } + + await this.createDump(dumpRequest.name); + + // Restore original settings + this.enableScreenshots = originalScreenshots; + this.fullPageScreenshots = originalFullPage; + + console.log(`✅ Manual dump completed: ${dumpRequest.name}`); + } catch (error) { + console.error(`❌ Manual dump failed: ${error.message}`); + } + } + } catch (error) { + // Silently ignore dump request errors + } + } + async updateStatus(command, isCompleted = false) { if (!this.statusPage || this.headless) return; @@ -927,9 +1239,11 @@ class TestExecutor { const progressEl = document.getElementById('progress-fill'); if (progressEl) progressEl.style.width = progress + '%'; - // Update status indicator + // Update status indicator (only if not paused) const indicator = document.getElementById('status-indicator'); - if (indicator) indicator.className = 'status-indicator status-running'; + if (indicator && !window.playwrongPaused) { + indicator.className = 'status-indicator status-running'; + } }, { command: this.formatCommandForOutput(command), @@ -1604,6 +1918,13 @@ class TestExecutor { // Ignore errors when closing status context } } + if (this.statusBrowser && !this.headless) { + try { + await this.statusBrowser.close(); + } catch (error) { + // 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(); diff --git a/step1.test b/step1.test index abc1ffc..663570f 100644 --- a/step1.test +++ b/step1.test @@ -38,8 +38,9 @@ click element=button childText="Weiter" sleep 2000 "anmelden click" # Part 2 - Fill in the checkout form -scroll element=span childText="Vorname" + 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" @@ -57,74 +58,16 @@ 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" +sleep 1000000 "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 300 "order completion" +sleep 300000 "order completion" -# Part 3 - Login to the email account -open "https://mail.growbnb.de/" -sleep 100 "page load" -wait element=input name="_user" id="rcmloginuser" -fill element=input name="_user" id="rcmloginuser" value="autotest@growheads.de" -sleep 100 "username fill" -wait element=input name="_pass" id="rcmloginpwd" -fill element=input name="_pass" id="rcmloginpwd" value="$PASSWORDMAIL" -sleep 100 "password fill" -wait element=button type="submit" id="rcmloginsubmit" -click element=button type="submit" id="rcmloginsubmit" -sleep 100 "login submit" -# Wait for and click on the Bestellbestätigung link -wait element=a childText="Bestellbestätigung" -click element=a childText="Bestellbestätigung" - - -# Click on "Mehr" button to open dropdown menu -wait element=a id="messagemenulink" -click element=a id="messagemenulink" -sleep 300 "mehr button click" - -# Click on "In neuem Fenster öffnen" link -wait element=a id="rcmbtn134" -click element=a id="rcmbtn134" - -# Switch to the new window that was opened -follow - -# Verify that "Musteranmerkung" exists in the content -wait element=p childText="Musteranmerkung" - -# Extract the order URL from the link -extract href from element=a childText="https://dev.seedheads.de/profile#W-" to "ORDER_URL" - -wait element=a id="rcmbtn105" -click element=a id="rcmbtn105" -sleep 300 "email deleted" - -# Now open the extracted URL -open "$ORDER_URL" -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 300 "anmelden click" -wait element=button childText="Schließen" -click element=button childText="Schließen" -sleep 100 "schließen click" -wait element=button class="MuiIconButton-colorError" -click element=button class="MuiIconButton-colorError" -sleep 100 "stornieren click" -wait element=button childText="Stornieren" -click element=button childText="Stornieren" -sleep 100 "stornieren click" -sleep 10000 "completed" \ No newline at end of file diff --git a/step2.test b/step2.test new file mode 100644 index 0000000..aae91c7 --- /dev/null +++ b/step2.test @@ -0,0 +1,62 @@ +use "Chrome" + +# Part 3 - Login to the email account +open "https://mail.growbnb.de/" +sleep 100 "page load" +wait element=input name="_user" id="rcmloginuser" +fill element=input name="_user" id="rcmloginuser" value="autotest@growheads.de" +sleep 100 "username fill" +wait element=input name="_pass" id="rcmloginpwd" +fill element=input name="_pass" id="rcmloginpwd" value="$PASSWORDMAIL" +sleep 100 "password fill" +wait element=button type="submit" id="rcmloginsubmit" +click element=button type="submit" id="rcmloginsubmit" +sleep 100 "login submit" +# Wait for and click on the Bestellbestätigung link +wait element=a childText="Bestellbestätigung" +click element=a childText="Bestellbestätigung" + + +# Click on "Mehr" button to open dropdown menu +wait element=a id="messagemenulink" +click element=a id="messagemenulink" +sleep 300 "mehr button click" + +# Click on "In neuem Fenster öffnen" link +wait element=a id="rcmbtn134" +click element=a id="rcmbtn134" + +# Switch to the new window that was opened +follow + +# Verify that "Musteranmerkung" exists in the content +wait element=p childText="Musteranmerkung" + +# Extract the order URL from the link +extract href from element=a childText="https://dev.seedheads.de/profile#W-" to "ORDER_URL" + +wait element=a id="rcmbtn105" +click element=a id="rcmbtn105" +sleep 300 "email deleted" + +# Now open the extracted URL +open "$ORDER_URL" +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 300 "anmelden click" +wait element=button childText="Schließen" +click element=button childText="Schließen" +sleep 100 "schließen click" +wait element=button class="MuiIconButton-colorError" +click element=button class="MuiIconButton-colorError" +sleep 100 "stornieren click" +wait element=button childText="Stornieren" +#click element=button childText="Stornieren" +sleep 100 "stornieren click" +sleep 10000 "completed" \ No newline at end of file