Enhance test execution with interactive controls; add pause, step, and dump functionalities in executor.js, and update step1.test for improved form handling and scrolling behavior.

This commit is contained in:
seb
2025-07-18 07:22:22 +02:00
parent 85f7f81236
commit 140852be07
3 changed files with 395 additions and 69 deletions

View File

@@ -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;
}
</style>
</head>
<body>
@@ -869,7 +978,91 @@ class TestExecutor {
</div>
</div>
</div>
<div class="status-section">
<div class="status-title">Controls</div>
<div class="controls-section">
<button id="pause-btn" class="control-button pause-btn">⏸️ Pause</button>
<button id="step-btn" class="control-button step-btn" disabled>⏭️ +1 Step</button>
<button id="dump-btn" class="control-button dump-btn">📸 Dump</button>
</div>
<div class="controls-section">
<button id="dump-screen-btn" class="control-button dump-btn">📷 Dump + Screen</button>
<button id="dump-fullscreen-btn" class="control-button dump-btn">🖼️ Dump + Fullscreen</button>
</div>
</div>
</body>
<script>
// Global state for pause/resume
let isPaused = false;
let dumpCounter = 1;
// Button event handlers
document.getElementById('pause-btn').addEventListener('click', function() {
isPaused = !isPaused;
const button = this;
const stepBtn = document.getElementById('step-btn');
const indicator = document.getElementById('status-indicator');
if (isPaused) {
button.textContent = '▶️ Resume';
button.className = 'control-button resume-btn';
indicator.className = 'status-indicator status-paused';
stepBtn.disabled = false;
// Store pause state in window to communicate with executor
window.playwrongPaused = true;
} else {
button.textContent = '⏸️ Pause';
button.className = 'control-button pause-btn';
indicator.className = 'status-indicator status-running';
stepBtn.disabled = true;
// Resume execution
window.playwrongPaused = false;
}
});
// Step button handler
document.getElementById('step-btn').addEventListener('click', function() {
// Request one step execution
window.playwrongStepRequest = true;
console.log('Step request set');
});
// Dump button handlers
document.getElementById('dump-btn').addEventListener('click', function() {
const dumpName = 'manual_dump_' + dumpCounter++;
window.playwrongDumpRequest = {
name: dumpName,
type: 'standard'
};
console.log('Dump request set:', window.playwrongDumpRequest);
});
document.getElementById('dump-screen-btn').addEventListener('click', function() {
const dumpName = 'manual_dump_screen_' + dumpCounter++;
window.playwrongDumpRequest = {
name: dumpName,
type: 'screen'
};
console.log('Dump request set:', window.playwrongDumpRequest);
});
document.getElementById('dump-fullscreen-btn').addEventListener('click', function() {
const dumpName = 'manual_dump_fullscreen_' + dumpCounter++;
window.playwrongDumpRequest = {
name: dumpName,
type: 'fullscreen'
};
console.log('Dump request set:', window.playwrongDumpRequest);
});
// Initialize pause state
window.playwrongPaused = false;
window.playwrongDumpRequest = null;
window.playwrongStepRequest = false;
</script>
</html>
`;
@@ -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();

View File

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

62
step2.test Normal file
View File

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