Refactor test execution and linter to support new 'follow' command for window switching, enhance status tracking with visual indicators, and update extraction syntax for improved clarity
This commit is contained in:
425
src/executor.js
425
src/executor.js
@@ -37,8 +37,24 @@ class TestExecutor {
|
||||
try {
|
||||
await this.setup(profileName);
|
||||
|
||||
// Set total commands for status tracking
|
||||
this.totalCommands = commands.length;
|
||||
if (this.statusPage && !this.headless) {
|
||||
await this.updateStatus({ type: 'initializing' }, false);
|
||||
}
|
||||
|
||||
for (const command of commands) {
|
||||
// Update status before executing command
|
||||
if (!this.headless) {
|
||||
await this.updateStatus(command, false);
|
||||
}
|
||||
|
||||
await this.executeCommand(command);
|
||||
|
||||
// Update status after completing command
|
||||
if (!this.headless) {
|
||||
await this.updateStatus(command, true);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -81,6 +97,11 @@ class TestExecutor {
|
||||
|
||||
this.page = await this.context.newPage();
|
||||
|
||||
// Create status window for headed mode AFTER main page so it appears on top
|
||||
if (!this.headless) {
|
||||
await this.createStatusWindow();
|
||||
}
|
||||
|
||||
// Ensure consistent viewport size
|
||||
await this.page.setViewportSize(profile.viewport);
|
||||
|
||||
@@ -206,8 +227,8 @@ class TestExecutor {
|
||||
await this.sleep(command);
|
||||
break;
|
||||
|
||||
case 'switchToNewWindow':
|
||||
await this.switchToNewWindow();
|
||||
case 'follow':
|
||||
await this.followToNewWindow();
|
||||
break;
|
||||
|
||||
case 'switchToTab':
|
||||
@@ -580,82 +601,93 @@ class TestExecutor {
|
||||
|
||||
// Add visual sleep animation with countdown in headed mode
|
||||
if (!this.headless) {
|
||||
await this.page.addStyleTag({
|
||||
content: `
|
||||
.sleep-indicator {
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 20px 40px;
|
||||
border-radius: 10px;
|
||||
font-size: 24px;
|
||||
font-family: Arial, sans-serif;
|
||||
z-index: 999999;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.sleep-icon {
|
||||
font-size: 40px;
|
||||
animation: sleepPulse 2s ease-in-out infinite;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@keyframes sleepPulse {
|
||||
0%, 100% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1);
|
||||
try {
|
||||
await this.page.addStyleTag({
|
||||
content: `
|
||||
.sleep-indicator {
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 20px 40px;
|
||||
border-radius: 10px;
|
||||
font-size: 24px;
|
||||
font-family: Arial, sans-serif;
|
||||
z-index: 999999;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
|
||||
.sleep-icon {
|
||||
font-size: 40px;
|
||||
animation: sleepPulse 2s ease-in-out infinite;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sleepPulse {
|
||||
0%, 100% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.countdown {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
// Create sleep indicator with countdown
|
||||
await this.page.evaluate(({ duration, message }) => {
|
||||
const indicator = document.createElement('div');
|
||||
indicator.className = 'sleep-indicator';
|
||||
indicator.id = 'sleep-indicator';
|
||||
indicator.innerHTML = `
|
||||
<div class="sleep-icon">💤</div>
|
||||
<div>SLEEP: ${message || 'waiting'}</div>
|
||||
<div class="countdown" id="countdown">${Math.ceil(duration / 1000)}s</div>
|
||||
`;
|
||||
document.body.appendChild(indicator);
|
||||
|
||||
.countdown {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
// Create sleep indicator with countdown
|
||||
await this.page.evaluate(({ duration, message }) => {
|
||||
const indicator = document.createElement('div');
|
||||
indicator.className = 'sleep-indicator';
|
||||
indicator.id = 'sleep-indicator';
|
||||
indicator.innerHTML = `
|
||||
<div class="sleep-icon">💤</div>
|
||||
<div>SLEEP: ${message || 'waiting'}</div>
|
||||
<div class="countdown" id="countdown">${Math.ceil(duration / 1000)}s</div>
|
||||
`;
|
||||
document.body.appendChild(indicator);
|
||||
|
||||
// Update countdown every second
|
||||
let remaining = Math.ceil(duration / 1000);
|
||||
const countdownEl = document.getElementById('countdown');
|
||||
|
||||
const interval = setInterval(() => {
|
||||
remaining--;
|
||||
if (countdownEl) {
|
||||
countdownEl.textContent = remaining + 's';
|
||||
}
|
||||
if (remaining <= 0) {
|
||||
clearInterval(interval);
|
||||
const element = document.getElementById('sleep-indicator');
|
||||
if (element) {
|
||||
element.remove();
|
||||
// Update countdown every second
|
||||
let remaining = Math.ceil(duration / 1000);
|
||||
const countdownEl = document.getElementById('countdown');
|
||||
|
||||
const interval = setInterval(() => {
|
||||
remaining--;
|
||||
if (countdownEl) {
|
||||
countdownEl.textContent = remaining + 's';
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
}, { duration: command.milliseconds, message: command.message });
|
||||
if (remaining <= 0) {
|
||||
clearInterval(interval);
|
||||
const element = document.getElementById('sleep-indicator');
|
||||
if (element) {
|
||||
element.remove();
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
}, { duration: command.milliseconds, message: command.message });
|
||||
} catch (error) {
|
||||
// Ignore errors related to destroyed execution context
|
||||
// This happens when the page navigates during sleep
|
||||
if (error.message.includes('Execution context was destroyed') ||
|
||||
error.message.includes('Target page, context or browser has been closed')) {
|
||||
console.log('⚠️ Page navigated during sleep - skipping visual animation');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
@@ -663,7 +695,229 @@ class TestExecutor {
|
||||
});
|
||||
}
|
||||
|
||||
async switchToNewWindow() {
|
||||
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 }
|
||||
});
|
||||
|
||||
// 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
|
||||
const statusHTML = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>PlayWrong Test Status</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid rgba(255,255,255,0.3);
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
.logo {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.status-section {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.status-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.current-command {
|
||||
font-family: 'Courier New', monospace;
|
||||
background: rgba(0,0,0,0.3);
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
word-break: break-all;
|
||||
min-height: 80px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.stat {
|
||||
text-align: center;
|
||||
}
|
||||
.stat-number {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.progress-bar {
|
||||
background: rgba(255,255,255,0.2);
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.progress-fill {
|
||||
background: #4CAF50;
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.status-running { background: #4CAF50; animation: pulse 2s infinite; }
|
||||
.status-waiting { background: #FF9800; }
|
||||
.status-error { background: #F44336; }
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="logo">🎭 PlayWrong</div>
|
||||
<div class="subtitle">Test Execution Status</div>
|
||||
</div>
|
||||
|
||||
<div class="status-section">
|
||||
<div class="status-title">
|
||||
<span id="status-indicator" class="status-indicator status-waiting"></span>
|
||||
Current Command
|
||||
</div>
|
||||
<div id="current-command" class="current-command">Initializing...</div>
|
||||
<div class="progress-bar">
|
||||
<div id="progress-fill" class="progress-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-section">
|
||||
<div class="status-title">Statistics</div>
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<span id="completed-count" class="stat-number">0</span>
|
||||
<span class="stat-label">Completed</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span id="total-count" class="stat-number">0</span>
|
||||
<span class="stat-label">Total</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span id="elapsed-time" class="stat-number">0s</span>
|
||||
<span class="stat-label">Elapsed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
// Load the status HTML
|
||||
await this.statusPage.setContent(statusHTML);
|
||||
|
||||
// Initialize status tracking
|
||||
this.startTime = Date.now();
|
||||
this.completedCommands = 0;
|
||||
this.totalCommands = 0;
|
||||
|
||||
console.log('🎭 PlayWrong status window created');
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Failed to create status window:', error.message);
|
||||
this.statusPage = null;
|
||||
}
|
||||
}
|
||||
|
||||
async updateStatus(command, isCompleted = false) {
|
||||
if (!this.statusPage || this.headless) return;
|
||||
|
||||
try {
|
||||
// Check if status page is still valid
|
||||
if (this.statusPage.isClosed()) {
|
||||
this.statusPage = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = Date.now();
|
||||
const elapsedSeconds = Math.floor((currentTime - this.startTime) / 1000);
|
||||
|
||||
if (isCompleted) {
|
||||
this.completedCommands++;
|
||||
}
|
||||
|
||||
const progress = this.totalCommands > 0 ? (this.completedCommands / this.totalCommands) * 100 : 0;
|
||||
|
||||
await this.statusPage.evaluate(({ command, completed, total, elapsed, progress }) => {
|
||||
// Update current command
|
||||
const commandEl = document.getElementById('current-command');
|
||||
if (commandEl) commandEl.textContent = command;
|
||||
|
||||
// Update statistics
|
||||
const completedEl = document.getElementById('completed-count');
|
||||
if (completedEl) completedEl.textContent = completed;
|
||||
|
||||
const totalEl = document.getElementById('total-count');
|
||||
if (totalEl) totalEl.textContent = total;
|
||||
|
||||
const elapsedEl = document.getElementById('elapsed-time');
|
||||
if (elapsedEl) elapsedEl.textContent = elapsed + 's';
|
||||
|
||||
// Update progress bar
|
||||
const progressEl = document.getElementById('progress-fill');
|
||||
if (progressEl) progressEl.style.width = progress + '%';
|
||||
|
||||
// Update status indicator
|
||||
const indicator = document.getElementById('status-indicator');
|
||||
if (indicator) indicator.className = 'status-indicator status-running';
|
||||
|
||||
}, {
|
||||
command: this.formatCommandForOutput(command),
|
||||
completed: this.completedCommands,
|
||||
total: this.totalCommands,
|
||||
elapsed: elapsedSeconds,
|
||||
progress: progress
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// Silently ignore status window errors to not interfere with main test
|
||||
// console.log('Status window update failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async followToNewWindow() {
|
||||
console.log('🔄 Switching to new tab...');
|
||||
|
||||
// Get all current pages/tabs
|
||||
@@ -755,8 +1009,8 @@ class TestExecutor {
|
||||
return `break "${command.message}"`;
|
||||
case 'sleep':
|
||||
return command.message ? `sleep ${command.milliseconds} "${command.message}"` : `sleep ${command.milliseconds}`;
|
||||
case 'switchToNewWindow':
|
||||
return 'switchToNewWindow';
|
||||
case 'follow':
|
||||
return 'follow';
|
||||
case 'switchToTab':
|
||||
return `switchToTab tabIndex=${command.tabIndex}`;
|
||||
case 'extract':
|
||||
@@ -973,6 +1227,7 @@ class TestExecutor {
|
||||
};
|
||||
}
|
||||
|
||||
// Take screenshot of the main test page only (not status window)
|
||||
await this.page.screenshot(screenshotOptions);
|
||||
|
||||
// Restore viewport after screenshot
|
||||
@@ -992,7 +1247,7 @@ class TestExecutor {
|
||||
const keepElements = ['button', 'input', 'textarea', 'span', 'a'];
|
||||
|
||||
// Attributes we want to keep
|
||||
const keepAttributes = ['id', 'class', 'name', 'value', 'type'];
|
||||
const keepAttributes = ['id', 'name', 'value', 'type'];
|
||||
|
||||
function processElement(element) {
|
||||
const tagName = element.tagName.toLowerCase();
|
||||
@@ -1221,6 +1476,20 @@ class TestExecutor {
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
if (this.statusPage && !this.headless) {
|
||||
try {
|
||||
await this.statusPage.close();
|
||||
} catch (error) {
|
||||
// Ignore errors when closing status window
|
||||
}
|
||||
}
|
||||
if (this.statusContext && !this.headless) {
|
||||
try {
|
||||
await this.statusContext.close();
|
||||
} catch (error) {
|
||||
// Ignore errors when closing status context
|
||||
}
|
||||
}
|
||||
if (this.page) await this.page.close();
|
||||
if (this.context) await this.context.close();
|
||||
if (this.browser) await this.browser.close();
|
||||
|
||||
Reference in New Issue
Block a user