Compare commits
3 Commits
49270f8d2f
...
41e4424650
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41e4424650 | ||
|
|
a69911e874 | ||
|
|
fe4ce936c6 |
@@ -59,7 +59,7 @@
|
|||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"name": "keyword.control.playwrong",
|
"name": "keyword.control.playwrong",
|
||||||
"match": "\\b(use|open|wait|click|fill|scroll|sleep|dump|break)\\b"
|
"match": "\\b(use|open|wait|click|fill|scroll|sleep|dump|break|follow|switch to tab|extract)\\b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "entity.name.function.playwrong",
|
"name": "entity.name.function.playwrong",
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
use "Chrome"
|
|
||||||
|
|
||||||
# Part 1 - Load Google and accept cookies
|
|
||||||
open "https://google.de"
|
|
||||||
wait element=button childText="Alle akzeptieren"
|
|
||||||
click element=button childText="Alle akzeptieren"
|
|
||||||
sleep 1000 "after cookie accept"
|
|
||||||
dump "after_cookie_accept"
|
|
||||||
|
|
||||||
@@ -4,12 +4,8 @@
|
|||||||
"description": "Custom test language using Playwright",
|
"description": "Custom test language using Playwright",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node src/cli.js step1.test Chrome --headed --screenshot-none",
|
"test": "node src/cli.js step1.test Chrome --headed --screenshot --full-page",
|
||||||
"install-browsers": "playwright install chromium",
|
"install-browsers": "playwright install chromium"
|
||||||
"lint": "node src/linter-cli.js step1.test",
|
|
||||||
"lint:strict": "node src/linter-cli.js --strict step1.test",
|
|
||||||
"lint:verbose": "node src/linter-cli.js --verbose step1.test",
|
|
||||||
"lint:all": "node src/linter-cli.js step1.test"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^17.2.0",
|
"dotenv": "^17.2.0",
|
||||||
|
|||||||
@@ -92,12 +92,14 @@ async function main() {
|
|||||||
console.log(' --headed Run in headed mode (show browser)');
|
console.log(' --headed Run in headed mode (show browser)');
|
||||||
console.log(' --headless Run in headless mode (default)');
|
console.log(' --headless Run in headless mode (default)');
|
||||||
console.log(' --enable-animations Enable CSS animations (default: disabled)');
|
console.log(' --enable-animations Enable CSS animations (default: disabled)');
|
||||||
|
console.log(' --full-page Take full page screenshots instead of viewport only');
|
||||||
console.log(' --lint Run linter before execution');
|
console.log(' --lint Run linter before execution');
|
||||||
console.log(' --strict Treat linter warnings as errors');
|
console.log(' --strict Treat linter warnings as errors');
|
||||||
console.log('Examples:');
|
console.log('Examples:');
|
||||||
console.log(' node src/cli.js tests/example.test Chrome');
|
console.log(' node src/cli.js tests/example.test Chrome');
|
||||||
console.log(' node src/cli.js tests/example.test Chrome --headed');
|
console.log(' node src/cli.js tests/example.test Chrome --headed');
|
||||||
console.log(' node src/cli.js tests/example.test Chrome --headed --enable-animations');
|
console.log(' node src/cli.js tests/example.test Chrome --headed --enable-animations');
|
||||||
|
console.log(' node src/cli.js tests/example.test Chrome --headed --full-page');
|
||||||
console.log(' node src/cli.js tests/example.test Chrome --lint --strict');
|
console.log(' node src/cli.js tests/example.test Chrome --lint --strict');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -106,6 +108,7 @@ async function main() {
|
|||||||
const profile = args[1] || 'Chrome';
|
const profile = args[1] || 'Chrome';
|
||||||
const headless = !args.includes('--headed');
|
const headless = !args.includes('--headed');
|
||||||
const disableAnimations = !args.includes('--enable-animations');
|
const disableAnimations = !args.includes('--enable-animations');
|
||||||
|
const fullPageScreenshots = args.includes('--full-page');
|
||||||
const lint = args.includes('--lint');
|
const lint = args.includes('--lint');
|
||||||
const strict = args.includes('--strict');
|
const strict = args.includes('--strict');
|
||||||
|
|
||||||
@@ -114,7 +117,8 @@ async function main() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const runner = new TestRunner({ headless, disableAnimations, lint, strict });
|
const runner = new TestRunner({ headless, disableAnimations, fullPageScreenshots, lint, strict });
|
||||||
|
|
||||||
await runner.runTestFile(testFile, profile);
|
await runner.runTestFile(testFile, profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
525
src/executor.js
525
src/executor.js
@@ -15,6 +15,8 @@ class TestExecutor {
|
|||||||
this.disableAnimations = options.disableAnimations !== false; // Default to disable animations
|
this.disableAnimations = options.disableAnimations !== false; // Default to disable animations
|
||||||
this.fullPageScreenshots = options.fullPageScreenshots || false; // Default to viewport screenshots
|
this.fullPageScreenshots = options.fullPageScreenshots || false; // Default to viewport screenshots
|
||||||
this.enableScreenshots = options.enableScreenshots !== false; // Default to enable screenshots
|
this.enableScreenshots = options.enableScreenshots !== false; // Default to enable screenshots
|
||||||
|
this.variables = {}; // Store extracted variables
|
||||||
|
|
||||||
this.profiles = {
|
this.profiles = {
|
||||||
Chrome: {
|
Chrome: {
|
||||||
viewport: { width: 1280, height: 720 },
|
viewport: { width: 1280, height: 720 },
|
||||||
@@ -35,8 +37,24 @@ class TestExecutor {
|
|||||||
try {
|
try {
|
||||||
await this.setup(profileName);
|
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) {
|
for (const command of commands) {
|
||||||
|
// Update status before executing command
|
||||||
|
if (!this.headless) {
|
||||||
|
await this.updateStatus(command, false);
|
||||||
|
}
|
||||||
|
|
||||||
await this.executeCommand(command);
|
await this.executeCommand(command);
|
||||||
|
|
||||||
|
// Update status after completing command
|
||||||
|
if (!this.headless) {
|
||||||
|
await this.updateStatus(command, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -79,6 +97,11 @@ class TestExecutor {
|
|||||||
|
|
||||||
this.page = await this.context.newPage();
|
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
|
// Ensure consistent viewport size
|
||||||
await this.page.setViewportSize(profile.viewport);
|
await this.page.setViewportSize(profile.viewport);
|
||||||
|
|
||||||
@@ -148,7 +171,8 @@ class TestExecutor {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'open':
|
case 'open':
|
||||||
await this.page.goto(command.url, { waitUntil: 'networkidle' });
|
const resolvedUrl = this.resolveEnvironmentVariables(command.url);
|
||||||
|
await this.page.goto(resolvedUrl, { waitUntil: 'networkidle' });
|
||||||
// Small delay to ensure page is stable
|
// Small delay to ensure page is stable
|
||||||
if (!this.headless) {
|
if (!this.headless) {
|
||||||
await this.page.waitForTimeout(200);
|
await this.page.waitForTimeout(200);
|
||||||
@@ -203,6 +227,18 @@ class TestExecutor {
|
|||||||
await this.sleep(command);
|
await this.sleep(command);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'follow':
|
||||||
|
await this.followToNewWindow();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'switchToTab':
|
||||||
|
await this.switchToTab(command);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'extract':
|
||||||
|
await this.extractToVariable(command);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn(`Unknown command type: ${command.type}`);
|
console.warn(`Unknown command type: ${command.type}`);
|
||||||
}
|
}
|
||||||
@@ -470,8 +506,8 @@ class TestExecutor {
|
|||||||
for (let i = 0; i < value.length; i++) {
|
for (let i = 0; i < value.length; i++) {
|
||||||
const char = value[i];
|
const char = value[i];
|
||||||
|
|
||||||
// Human-like typing speed variation (80-200ms between keystrokes)
|
// Human-like typing speed variation (40-100ms between keystrokes) - 50% faster
|
||||||
const typingDelay = Math.random() * 120 + 80;
|
const typingDelay = Math.random() * 60 + 40;
|
||||||
|
|
||||||
// Create flying letter animation
|
// Create flying letter animation
|
||||||
await this.page.evaluate(({ sel, character, isPassword, currentIndex, totalLength }) => {
|
await this.page.evaluate(({ sel, character, isPassword, currentIndex, totalLength }) => {
|
||||||
@@ -564,83 +600,96 @@ class TestExecutor {
|
|||||||
console.log(sleepMsg);
|
console.log(sleepMsg);
|
||||||
|
|
||||||
// Add visual sleep animation with countdown in headed mode
|
// Add visual sleep animation with countdown in headed mode
|
||||||
if (!this.headless) {
|
// Only show visualization for sleeps >= 1000ms
|
||||||
await this.page.addStyleTag({
|
if (!this.headless && command.milliseconds >= 1000) {
|
||||||
content: `
|
try {
|
||||||
.sleep-indicator {
|
await this.page.addStyleTag({
|
||||||
position: fixed;
|
content: `
|
||||||
top: 50px;
|
.sleep-indicator {
|
||||||
left: 50%;
|
position: fixed;
|
||||||
transform: translateX(-50%);
|
top: 50px;
|
||||||
background: rgba(0, 0, 0, 0.8);
|
left: 50%;
|
||||||
color: white;
|
transform: translateX(-50%);
|
||||||
padding: 20px 40px;
|
background: rgba(0, 0, 0, 0.8);
|
||||||
border-radius: 10px;
|
color: white;
|
||||||
font-size: 24px;
|
padding: 20px 40px;
|
||||||
font-family: Arial, sans-serif;
|
border-radius: 10px;
|
||||||
z-index: 999999;
|
font-size: 24px;
|
||||||
pointer-events: none;
|
font-family: Arial, sans-serif;
|
||||||
text-align: center;
|
z-index: 999999;
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
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);
|
|
||||||
}
|
}
|
||||||
50% {
|
|
||||||
opacity: 1;
|
.sleep-icon {
|
||||||
transform: scale(1.1);
|
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 {
|
// Update countdown every second
|
||||||
font-size: 20px;
|
let remaining = Math.ceil(duration / 1000);
|
||||||
font-weight: bold;
|
const countdownEl = document.getElementById('countdown');
|
||||||
color: #4CAF50;
|
|
||||||
}
|
const interval = setInterval(() => {
|
||||||
`
|
remaining--;
|
||||||
});
|
if (countdownEl) {
|
||||||
|
countdownEl.textContent = remaining + 's';
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
}
|
// Hide visualization when less than 0.5 seconds remaining
|
||||||
}, 1000);
|
if (remaining <= 0.5) {
|
||||||
|
clearInterval(interval);
|
||||||
}, { duration: command.milliseconds, message: command.message });
|
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) => {
|
return new Promise((resolve) => {
|
||||||
@@ -648,6 +697,297 @@ 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 }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const pages = this.context.pages();
|
||||||
|
|
||||||
|
if (pages.length < 2) {
|
||||||
|
throw new Error('No additional tabs found to switch to');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to the last (most recently opened) tab
|
||||||
|
const newTab = pages[pages.length - 1];
|
||||||
|
|
||||||
|
// Wait for the tab to be ready
|
||||||
|
await newTab.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Switch to the new tab
|
||||||
|
this.page = newTab;
|
||||||
|
|
||||||
|
console.log('🔄 Switched to new tab');
|
||||||
|
}
|
||||||
|
|
||||||
|
async switchToTab(command) {
|
||||||
|
const tabIndex = command.tabIndex;
|
||||||
|
if (tabIndex === undefined || tabIndex < 0 || tabIndex >= this.context.pages().length) {
|
||||||
|
throw new Error(`Invalid tab index: ${tabIndex}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPage = this.context.pages()[tabIndex];
|
||||||
|
this.page = targetPage;
|
||||||
|
console.log(`🔄 Switched to tab index ${tabIndex}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async extractToVariable(command) {
|
||||||
|
const selector = this.buildSelector(command);
|
||||||
|
const attribute = command.attribute;
|
||||||
|
const variableName = command.variableName;
|
||||||
|
|
||||||
|
if (!variableName) {
|
||||||
|
throw new Error('extract command requires a variableName');
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = '';
|
||||||
|
try {
|
||||||
|
const element = await this.page.locator(selector).first();
|
||||||
|
|
||||||
|
if (attribute === 'text') {
|
||||||
|
value = await element.textContent();
|
||||||
|
} else if (attribute === 'innerHTML') {
|
||||||
|
value = await element.innerHTML();
|
||||||
|
} else {
|
||||||
|
// Extract attribute value
|
||||||
|
value = await element.getAttribute(attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
throw new Error(`Attribute "${attribute}" not found on element`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📋 Extracted ${attribute} from "${selector}" to variable "${variableName}": "${value}"`);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Could not extract ${attribute} from "${selector}" to variable "${variableName}":`, e.message);
|
||||||
|
throw new Error(`Could not extract ${attribute} from "${selector}" to variable "${variableName}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.variables[variableName] = value;
|
||||||
|
console.log(`✅ Variable "${variableName}" set to: "${value}"`);
|
||||||
|
}
|
||||||
|
|
||||||
formatCommandForOutput(command) {
|
formatCommandForOutput(command) {
|
||||||
switch (command.type) {
|
switch (command.type) {
|
||||||
case 'use':
|
case 'use':
|
||||||
@@ -671,6 +1011,12 @@ class TestExecutor {
|
|||||||
return `break "${command.message}"`;
|
return `break "${command.message}"`;
|
||||||
case 'sleep':
|
case 'sleep':
|
||||||
return command.message ? `sleep ${command.milliseconds} "${command.message}"` : `sleep ${command.milliseconds}`;
|
return command.message ? `sleep ${command.milliseconds} "${command.message}"` : `sleep ${command.milliseconds}`;
|
||||||
|
case 'follow':
|
||||||
|
return 'follow';
|
||||||
|
case 'switchToTab':
|
||||||
|
return `switchToTab tabIndex=${command.tabIndex}`;
|
||||||
|
case 'extract':
|
||||||
|
return `extract ${this.formatSelector(command)} variableName="${command.variableName}"`;
|
||||||
default:
|
default:
|
||||||
return `${command.type} ${JSON.stringify(command)}`;
|
return `${command.type} ${JSON.stringify(command)}`;
|
||||||
}
|
}
|
||||||
@@ -883,6 +1229,7 @@ class TestExecutor {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Take screenshot of the main test page only (not status window)
|
||||||
await this.page.screenshot(screenshotOptions);
|
await this.page.screenshot(screenshotOptions);
|
||||||
|
|
||||||
// Restore viewport after screenshot
|
// Restore viewport after screenshot
|
||||||
@@ -902,7 +1249,7 @@ class TestExecutor {
|
|||||||
const keepElements = ['button', 'input', 'textarea', 'span', 'a'];
|
const keepElements = ['button', 'input', 'textarea', 'span', 'a'];
|
||||||
|
|
||||||
// Attributes we want to keep
|
// Attributes we want to keep
|
||||||
const keepAttributes = ['id', 'class', 'name', 'value', 'type'];
|
const keepAttributes = ['id', 'name', 'value', 'type'];
|
||||||
|
|
||||||
function processElement(element) {
|
function processElement(element) {
|
||||||
const tagName = element.tagName.toLowerCase();
|
const tagName = element.tagName.toLowerCase();
|
||||||
@@ -1131,6 +1478,20 @@ class TestExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async cleanup() {
|
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.page) await this.page.close();
|
||||||
if (this.context) await this.context.close();
|
if (this.context) await this.context.close();
|
||||||
if (this.browser) await this.browser.close();
|
if (this.browser) await this.browser.close();
|
||||||
@@ -1141,11 +1502,17 @@ class TestExecutor {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace $VARIABLE or ${VARIABLE} with environment variable values
|
// Replace $VARIABLE or ${VARIABLE} with environment variable values or stored variables
|
||||||
return value.replace(/\$\{?([A-Z_][A-Z0-9_]*)\}?/g, (match, varName) => {
|
return value.replace(/\$\{?([A-Z_][A-Z0-9_]*)\}?/g, (match, varName) => {
|
||||||
|
// First check stored variables
|
||||||
|
if (this.variables[varName] !== undefined) {
|
||||||
|
return this.variables[varName];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check environment variables
|
||||||
const envValue = process.env[varName];
|
const envValue = process.env[varName];
|
||||||
if (envValue === undefined) {
|
if (envValue === undefined) {
|
||||||
console.warn(`Warning: Environment variable ${varName} is not defined`);
|
console.warn(`Warning: Variable ${varName} is not defined in stored variables or environment`);
|
||||||
return match; // Return original if not found
|
return match; // Return original if not found
|
||||||
}
|
}
|
||||||
return envValue;
|
return envValue;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class TestLinter {
|
|||||||
this.info = [];
|
this.info = [];
|
||||||
|
|
||||||
// Valid commands
|
// Valid commands
|
||||||
this.validCommands = ['use', 'open', 'wait', 'click', 'scroll', 'fill', 'break', 'sleep', 'dump'];
|
this.validCommands = ['use', 'open', 'wait', 'click', 'scroll', 'fill', 'break', 'sleep', 'dump', 'follow', 'switchToTab', 'extract'];
|
||||||
|
|
||||||
// Valid HTML elements
|
// Valid HTML elements
|
||||||
this.validElements = [
|
this.validElements = [
|
||||||
@@ -29,7 +29,10 @@ class TestLinter {
|
|||||||
'fill': ['element', 'value'],
|
'fill': ['element', 'value'],
|
||||||
'break': [],
|
'break': [],
|
||||||
'sleep': ['milliseconds'],
|
'sleep': ['milliseconds'],
|
||||||
'dump': ['name']
|
'dump': ['name'],
|
||||||
|
'follow': [],
|
||||||
|
'switchToTab': ['tabIndex'],
|
||||||
|
'extract': ['element', 'attribute']
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize rules
|
// Initialize rules
|
||||||
@@ -252,6 +255,19 @@ class TestLinter {
|
|||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const command = line.cleaned.split(' ')[0];
|
const command = line.cleaned.split(' ')[0];
|
||||||
|
|
||||||
|
// Handle multi-word commands
|
||||||
|
if (line.cleaned.startsWith('follow')) {
|
||||||
|
continue; // This is valid
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.cleaned.startsWith('switch to tab')) {
|
||||||
|
continue; // This is valid
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.cleaned.startsWith('extract ')) {
|
||||||
|
continue; // This is valid
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.validCommands.includes(command)) {
|
if (!this.validCommands.includes(command)) {
|
||||||
this.addError(`Invalid command '${command}'. Valid commands: ${this.validCommands.join(', ')}`, line.lineNumber);
|
this.addError(`Invalid command '${command}'. Valid commands: ${this.validCommands.join(', ')}`, line.lineNumber);
|
||||||
}
|
}
|
||||||
@@ -312,6 +328,16 @@ class TestLinter {
|
|||||||
if (!line.cleaned.match(/\d+/)) {
|
if (!line.cleaned.match(/\d+/)) {
|
||||||
this.addError(`Command '${command}' requires numeric milliseconds`, line.lineNumber);
|
this.addError(`Command '${command}' requires numeric milliseconds`, line.lineNumber);
|
||||||
}
|
}
|
||||||
|
} else if (reqParam === 'tabIndex' && command === 'switchToTab') {
|
||||||
|
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 <attribute> from <selector> to "<variable>"`, line.lineNumber);
|
||||||
|
}
|
||||||
} else if (!params[reqParam]) {
|
} else if (!params[reqParam]) {
|
||||||
this.addError(`Command '${command}' missing required parameter '${reqParam}'`, line.lineNumber);
|
this.addError(`Command '${command}' missing required parameter '${reqParam}'`, line.lineNumber);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,6 +186,50 @@ class TestParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse follow command (previously switch to new window)
|
||||||
|
if (line.startsWith('follow')) {
|
||||||
|
return {
|
||||||
|
type: 'follow'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse switch to tab command: switch to tab 0
|
||||||
|
if (line.startsWith('switch to tab ')) {
|
||||||
|
const match = line.match(/switch to tab (\d+)/);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
type: 'switchToTab',
|
||||||
|
tabIndex: parseInt(match[1])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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+"([^"]+)"/);
|
||||||
|
if (match) {
|
||||||
|
const attribute = match[1];
|
||||||
|
const selectorPart = match[2];
|
||||||
|
const variableName = match[3];
|
||||||
|
const params = this.parseParameters(selectorPart);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'extract',
|
||||||
|
attribute: attribute,
|
||||||
|
variableName: variableName,
|
||||||
|
element: params.element,
|
||||||
|
name: params.name,
|
||||||
|
id: params.id,
|
||||||
|
class: params.class,
|
||||||
|
href: params.href,
|
||||||
|
htmlType: params.type,
|
||||||
|
child: params.child,
|
||||||
|
childText: params.childText,
|
||||||
|
target: params.target
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
95
step1.test
95
step1.test
@@ -9,71 +9,118 @@ Part 3: Login to the email account
|
|||||||
use "Chrome"
|
use "Chrome"
|
||||||
|
|
||||||
# Part 1 - Load Growheads, put one item in the cart and go to checkout
|
# 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"
|
sleep 200 "page load"
|
||||||
wait element=a href=/Kategorie/Seeds
|
wait element=a href=/Kategorie/Seeds
|
||||||
click element=a href=/Kategorie/Seeds
|
click element=a href=/Kategorie/Seeds
|
||||||
sleep 2000 "seed click"
|
sleep 200 "seed click"
|
||||||
wait element=button childText="In den Korb"
|
wait element=button childText="In den Korb"
|
||||||
click element=button childText="In den Korb"
|
click element=button childText="In den Korb"
|
||||||
sleep 2000 "in korb click"
|
sleep 200 "in korb click"
|
||||||
wait element=span class="MuiBadge-badge" childText="1"
|
wait element=span class="MuiBadge-badge" childText="1"
|
||||||
click element=button child=span(class="MuiBadge-badge" childText="1")
|
click element=button child=span(class="MuiBadge-badge" childText="1")
|
||||||
sleep 2000 "korb click"
|
sleep 200 "korb click"
|
||||||
wait element=button childText="Weiter zur Kasse"
|
wait element=button childText="Weiter zur Kasse"
|
||||||
click element=button childText="Weiter zur Kasse"
|
click element=button childText="Weiter zur Kasse"
|
||||||
sleep 2000 "weiter click"
|
sleep 200 "weiter click"
|
||||||
wait element=input type="email"
|
wait element=input type="email"
|
||||||
fill element=input type="email" value="autotest@growheads.de"
|
fill element=input type="email" value="autotest@growheads.de"
|
||||||
sleep 2000 "email fill"
|
sleep 200 "email fill"
|
||||||
wait element=input type="password"
|
wait element=input type="password"
|
||||||
fill element=input type="password" value="$PASSWORD"
|
fill element=input type="password" value="$PASSWORD"
|
||||||
sleep 2000 "password fill"
|
sleep 200 "password fill"
|
||||||
wait element=button childText="ANMELDEN" class="MuiButton-fullWidth"
|
wait element=button childText="ANMELDEN" class="MuiButton-fullWidth"
|
||||||
click element=button childText="ANMELDEN" class="MuiButton-fullWidth"
|
click element=button childText="ANMELDEN" class="MuiButton-fullWidth"
|
||||||
sleep 3000 "anmelden click"
|
sleep 300 "anmelden click"
|
||||||
|
|
||||||
# Part 2 - Fill in the checkout form
|
# Part 2 - Fill in the checkout form
|
||||||
scroll element=span childText="Vorname"
|
scroll element=span childText="Vorname"
|
||||||
wait element=input name="firstName"
|
wait element=input name="firstName"
|
||||||
fill element=input name="firstName" value="Max"
|
fill element=input name="firstName" value="Max"
|
||||||
sleep 1000 "vorname fill"
|
sleep 100 "vorname fill"
|
||||||
wait element=input name="lastName"
|
wait element=input name="lastName"
|
||||||
fill element=input name="lastName" value="Muster"
|
fill element=input name="lastName" value="Muster"
|
||||||
sleep 1000 "nachname fill"
|
sleep 100 "nachname fill"
|
||||||
wait element=input name="street"
|
wait element=input name="street"
|
||||||
fill element=input name="street" value="Muster"
|
fill element=input name="street" value="Muster"
|
||||||
sleep 1000 "strasse fill"
|
sleep 100 "strasse fill"
|
||||||
wait element=input name="houseNumber"
|
wait element=input name="houseNumber"
|
||||||
fill element=input name="houseNumber" value="420"
|
fill element=input name="houseNumber" value="420"
|
||||||
sleep 1000 "hausnummer fill"
|
sleep 100 "hausnummer fill"
|
||||||
wait element=input name="postalCode"
|
wait element=input name="postalCode"
|
||||||
fill element=input name="postalCode" value="42023"
|
fill element=input name="postalCode" value="42023"
|
||||||
sleep 1000 "plz fill"
|
sleep 100 "plz fill"
|
||||||
wait element=input name="city"
|
wait element=input name="city"
|
||||||
fill element=input name="city" value="Muster"
|
fill element=input name="city" value="Muster"
|
||||||
sleep 1000 "stadt fill"
|
sleep 100 "stadt fill"
|
||||||
wait element=textarea name="note"
|
wait element=textarea name="note"
|
||||||
fill element=textarea name="note" value="Musteranmerkung"
|
fill element=textarea name="note" value="Musteranmerkung"
|
||||||
sleep 1000 "note fill"
|
sleep 100 "note fill"
|
||||||
scroll element=button childText="Bestellung abschließen"
|
scroll element=button childText="Bestellung abschließen"
|
||||||
wait element=label childText="Bestimmungen"
|
wait element=label childText="Bestimmungen"
|
||||||
click element=label childText="Bestimmungen"
|
click element=label childText="Bestimmungen"
|
||||||
sleep 1000 "checkbox checked"
|
sleep 100 "checkbox checked"
|
||||||
wait element=button childText="Bestellung abschließen"
|
wait element=button childText="Bestellung abschließen"
|
||||||
#click element=button childText="Bestellung abschließen"
|
click element=button childText="Bestellung abschließen"
|
||||||
sleep 3000 "order completion"
|
sleep 300 "order completion"
|
||||||
|
|
||||||
# Part 3 - Login to the email account
|
# Part 3 - Login to the email account
|
||||||
open "https://mail.growbnb.de/"
|
open "https://mail.growbnb.de/"
|
||||||
sleep 2000 "page load"
|
sleep 100 "page load"
|
||||||
wait element=input name="_user" id="rcmloginuser"
|
wait element=input name="_user" id="rcmloginuser"
|
||||||
fill element=input name="_user" id="rcmloginuser" value="autotest@growheads.de"
|
fill element=input name="_user" id="rcmloginuser" value="autotest@growheads.de"
|
||||||
sleep 1000 "username fill"
|
sleep 100 "username fill"
|
||||||
wait element=input name="_pass" id="rcmloginpwd"
|
wait element=input name="_pass" id="rcmloginpwd"
|
||||||
fill element=input name="_pass" id="rcmloginpwd" value="$PASSWORDMAIL"
|
fill element=input name="_pass" id="rcmloginpwd" value="$PASSWORDMAIL"
|
||||||
sleep 1000 "password fill"
|
sleep 100 "password fill"
|
||||||
wait element=button type="submit" id="rcmloginsubmit"
|
wait element=button type="submit" id="rcmloginsubmit"
|
||||||
click element=button type="submit" id="rcmloginsubmit"
|
click element=button type="submit" id="rcmloginsubmit"
|
||||||
sleep 3000 "login submit"
|
sleep 100 "login submit"
|
||||||
dump "email_logged_in"
|
# 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"
|
||||||
65
step2.test
65
step2.test
@@ -1,65 +0,0 @@
|
|||||||
/*
|
|
||||||
PlayWrong Test - Complete E-commerce Flow
|
|
||||||
This test demonstrates the full purchase flow from product selection to order completion
|
|
||||||
with beautiful animations and environment variable support
|
|
||||||
Part 1: Load Growheads, put one item in the cart and go to checkout
|
|
||||||
Part 2: Fill in the checkout form
|
|
||||||
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"
|
|
||||||
sleep 2000 "page load"
|
|
||||||
wait element=a href=/Kategorie/Seeds
|
|
||||||
click element=a href=/Kategorie/Seeds
|
|
||||||
sleep 2000 "seed click"
|
|
||||||
wait element=button childText="In den Korb"
|
|
||||||
click element=button childText="In den Korb"
|
|
||||||
sleep 2000 "in korb click"
|
|
||||||
wait element=span class="MuiBadge-badge" childText="1"
|
|
||||||
click element=button child=span(class="MuiBadge-badge" childText="1")
|
|
||||||
sleep 2000 "korb click"
|
|
||||||
wait element=button childText="Weiter zur Kasse"
|
|
||||||
click element=button childText="Weiter zur Kasse"
|
|
||||||
sleep 2000 "weiter click"
|
|
||||||
wait element=input type="email"
|
|
||||||
fill element=input type="email" value="autotest@growheads.de"
|
|
||||||
sleep 2000 "email fill"
|
|
||||||
wait element=input type="password"
|
|
||||||
fill element=input type="password" value="$PASSWORD"
|
|
||||||
sleep 2000 "password fill"
|
|
||||||
wait element=button childText="ANMELDEN" class="MuiButton-fullWidth"
|
|
||||||
click element=button childText="ANMELDEN" class="MuiButton-fullWidth"
|
|
||||||
sleep 3000 "anmelden click"
|
|
||||||
|
|
||||||
# Part 2 - Fill in the checkout form
|
|
||||||
scroll element=span childText="Vorname"
|
|
||||||
wait element=input name="firstName"
|
|
||||||
fill element=input name="firstName" value="Max"
|
|
||||||
sleep 1000 "vorname fill"
|
|
||||||
wait element=input name="lastName"
|
|
||||||
fill element=input name="lastName" value="Muster"
|
|
||||||
sleep 1000 "nachname fill"
|
|
||||||
wait element=input name="street"
|
|
||||||
fill element=input name="street" value="Muster"
|
|
||||||
sleep 1000 "strasse fill"
|
|
||||||
wait element=input name="houseNumber"
|
|
||||||
fill element=input name="houseNumber" value="420"
|
|
||||||
sleep 1000 "hausnummer fill"
|
|
||||||
wait element=input name="postalCode"
|
|
||||||
fill element=input name="postalCode" value="42023"
|
|
||||||
sleep 1000 "plz fill"
|
|
||||||
wait element=input name="city"
|
|
||||||
fill element=input name="city" value="Muster"
|
|
||||||
sleep 1000 "stadt fill"
|
|
||||||
wait element=textarea name="note"
|
|
||||||
fill element=textarea name="note" value="Musteranmerkung"
|
|
||||||
sleep 1000 "note fill"
|
|
||||||
scroll element=button childText="Bestellung abschließen"
|
|
||||||
wait element=label childText="Bestimmungen"
|
|
||||||
click element=label childText="Bestimmungen"
|
|
||||||
sleep 1000 "checkbox checked"
|
|
||||||
wait element=button childText="Bestellung abschließen"
|
|
||||||
sleep 1000 "order completion"
|
|
||||||
dump "order_completed"
|
|
||||||
Reference in New Issue
Block a user