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();