diff --git a/.vscode/extensions/playwrong-syntax/syntaxes/playwrong.tmLanguage.json b/.vscode/extensions/playwrong-syntax/syntaxes/playwrong.tmLanguage.json
index 6e935ea..db707ce 100644
--- a/.vscode/extensions/playwrong-syntax/syntaxes/playwrong.tmLanguage.json
+++ b/.vscode/extensions/playwrong-syntax/syntaxes/playwrong.tmLanguage.json
@@ -59,7 +59,7 @@
"patterns": [
{
"name": "keyword.control.playwrong",
- "match": "\\b(use|open|wait|click|fill|scroll|sleep|dump|break|switch to new window|switch to tab|extract)\\b"
+ "match": "\\b(use|open|wait|click|fill|scroll|sleep|dump|break|follow|switch to tab|extract)\\b"
},
{
"name": "entity.name.function.playwrong",
diff --git a/src/executor.js b/src/executor.js
index d309272..981f71f 100644
--- a/src/executor.js
+++ b/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 = `
+
💤
+ SLEEP: ${message || 'waiting'}
+ ${Math.ceil(duration / 1000)}s
+ `;
+ 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 = `
- 💤
- SLEEP: ${message || 'waiting'}
- ${Math.ceil(duration / 1000)}s
- `;
- 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 = `
+
+
+
+ PlayWrong Test Status
+
+
+
+
+
+
+
+
+ Current Command
+
+
Initializing...
+
+
+
+
+
Statistics
+
+
+ 0
+ Completed
+
+
+ 0
+ Total
+
+
+ 0s
+ Elapsed
+
+
+
+
+
+ `;
+
+ // 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();
diff --git a/src/linter.js b/src/linter.js
index 80fbbf9..b509877 100644
--- a/src/linter.js
+++ b/src/linter.js
@@ -6,7 +6,7 @@ class TestLinter {
this.info = [];
// Valid commands
- this.validCommands = ['use', 'open', 'wait', 'click', 'scroll', 'fill', 'break', 'sleep', 'dump', 'switchToNewWindow', 'switchToTab', 'extract'];
+ this.validCommands = ['use', 'open', 'wait', 'click', 'scroll', 'fill', 'break', 'sleep', 'dump', 'follow', 'switchToTab', 'extract'];
// Valid HTML elements
this.validElements = [
@@ -30,7 +30,7 @@ class TestLinter {
'break': [],
'sleep': ['milliseconds'],
'dump': ['name'],
- 'switchToNewWindow': [],
+ 'follow': [],
'switchToTab': ['tabIndex'],
'extract': ['element', 'attribute']
};
@@ -256,11 +256,7 @@ class TestLinter {
const command = line.cleaned.split(' ')[0];
// Handle multi-word commands
- if (line.cleaned.startsWith('switch to new window')) {
- continue; // This is valid
- }
-
- if (line.cleaned.startsWith('extract ')) {
+ if (line.cleaned.startsWith('follow')) {
continue; // This is valid
}
@@ -268,6 +264,10 @@ class TestLinter {
continue; // This is valid
}
+ if (line.cleaned.startsWith('extract ')) {
+ continue; // This is valid
+ }
+
if (!this.validCommands.includes(command)) {
this.addError(`Invalid command '${command}'. Valid commands: ${this.validCommands.join(', ')}`, line.lineNumber);
}
@@ -332,6 +332,12 @@ class TestLinter {
if (!line.cleaned.match(/\d+/)) {
this.addError(`Command '${command}' requires numeric tabIndex`, line.lineNumber);
}
+ } else if (reqParam === 'attribute' && command === 'extract') {
+ // Special handling for extract command - attribute is part of command syntax
+ const extractMatch = line.cleaned.match(/extract\s+(\w+)\s+from\s+(.+?)\s+to\s+"([^"]+)"/);
+ if (!extractMatch) {
+ this.addError(`Command '${command}' has invalid syntax. Expected: extract from to ""`, line.lineNumber);
+ }
} else if (!params[reqParam]) {
this.addError(`Command '${command}' missing required parameter '${reqParam}'`, line.lineNumber);
}
diff --git a/src/parser.js b/src/parser.js
index d15e13f..cd7d318 100644
--- a/src/parser.js
+++ b/src/parser.js
@@ -186,10 +186,10 @@ class TestParser {
}
}
- // Parse switch to new window command
- if (line.startsWith('switch to new window')) {
+ // Parse follow command (previously switch to new window)
+ if (line.startsWith('follow')) {
return {
- type: 'switchToNewWindow'
+ type: 'follow'
};
}
@@ -204,9 +204,9 @@ class TestParser {
}
}
- // Parse extract command: extract href from element=a target="_blank" to variable="ORDER_URL"
+ // Parse extract command: extract href from element=a childText="text" to "ORDER_URL"
if (line.startsWith('extract ')) {
- const match = line.match(/extract\s+(\w+)\s+from\s+(.+?)\s+to\s+variable="([^"]+)"/);
+ const match = line.match(/extract\s+(\w+)\s+from\s+(.+?)\s+to\s+"([^"]+)"/);
if (match) {
const attribute = match[1];
const selectorPart = match[2];
diff --git a/step1.test b/step1.test
index e4caca2..af7d7fb 100644
--- a/step1.test
+++ b/step1.test
@@ -9,7 +9,7 @@ Part 3: Login to the email account
use "Chrome"
# Part 1 - Load Growheads, put one item in the cart and go to checkout
-/*open "https://growheads.de"
+open "https://dev.seedheads.de"
sleep 2000 "page load"
wait element=a href=/Kategorie/Seeds
click element=a href=/Kategorie/Seeds
@@ -63,7 +63,7 @@ sleep 1000 "checkbox checked"
wait element=button childText="Bestellung abschließen"
click element=button childText="Bestellung abschließen"
sleep 3000 "order completion"
-*/
+
# Part 3 - Login to the email account
open "https://mail.growbnb.de/"
sleep 100 "page load"
@@ -89,33 +89,38 @@ sleep 300 "mehr button click"
# Click on "In neuem Fenster öffnen" link
wait element=a id="rcmbtn134"
click element=a id="rcmbtn134"
-sleep 3000 "new window open"
# Switch to the new window that was opened
-switch to new window
-
-# Wait a bit more for the new window to fully load
-sleep 2000 "new window load"
+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://growheads.de/profile#W-" to variable="ORDER_URL"
+extract href from element=a childText="https://dev.seedheads.de/profile#W-" to "ORDER_URL"
-sleep 300 "bestellbestätigung click"
-dump "after_bestellbestaetigung_click"
-sleep 3000 "bestellbestätigung click"
-# Delete the email by clicking "Löschen"
wait element=a id="rcmbtn105"
click element=a id="rcmbtn105"
-sleep 1000 "email deleted"
+sleep 300 "email deleted"
# Now open the extracted URL
open "$ORDER_URL"
-sleep 2000 "order page load"
-dump "order_page_from_extracted_url"
-
-dump "after_new_window_click"
-
-sleep 30000 "login submit"
\ No newline at end of file
+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