Compare commits
5 Commits
41e4424650
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af4b774fea | ||
|
|
9e73900197 | ||
|
|
85537b26bf | ||
|
|
140852be07 | ||
|
|
85f7f81236 |
@@ -37,6 +37,8 @@ The `dump` command creates HTML snapshots in `test-results/Chrome/<dumpname>/` t
|
||||
- `break` - Pause execution (press any key to continue)
|
||||
- `break "waiting for user input"` - Pause with message
|
||||
- `dump "step_name"` - Take screenshot
|
||||
- `jumpIf element=span childText="Server-Warenkorb" jump=4` - Jump over 4 commands if element exists
|
||||
- `jumpIfNot element=span childText="Server-Warenkorb" jump=4` - Jump over 4 commands if element doesn't exist
|
||||
|
||||
### Element Selectors
|
||||
- `element=tagname` - HTML tag (div, span, button, input, a, form, etc.)
|
||||
|
||||
16
.vscode/extensions/playwrong-syntax/README.md
vendored
16
.vscode/extensions/playwrong-syntax/README.md
vendored
@@ -57,8 +57,24 @@ dump "login_page"
|
||||
- Unknown HTML elements
|
||||
- Generic element selectors
|
||||
- Semantic misuse (e.g., `click` on input fields)
|
||||
- **Undefined variables**: Variables that are not commonly defined environment variables
|
||||
|
||||
### Info
|
||||
- Environment variable usage
|
||||
- Command placement suggestions
|
||||
- Best practice tips
|
||||
|
||||
## Variable Validation
|
||||
|
||||
The extension now provides enhanced validation for environment variables:
|
||||
|
||||
- **Common variables** like `$PASSWORD`, `$PASSWORDMAIL`, `$EMAIL`, `$USERNAME`, `$API_KEY`, `$TOKEN`, `$BASE_URL` show as info messages
|
||||
- **Undefined variables** that are not commonly defined show as warnings
|
||||
- Use the CLI with `--strict-variables` flag to treat undefined variables as errors during execution
|
||||
|
||||
## CLI Integration
|
||||
|
||||
The extension works with the PlayWrong CLI which supports:
|
||||
- `--lint` - Run linter before execution
|
||||
- `--strict` - Treat linter warnings as errors
|
||||
- `--strict-variables` - Treat undefined variables as errors during execution
|
||||
39
README.md
39
README.md
@@ -33,6 +33,31 @@ Run in headless mode (default):
|
||||
node src/cli.js tests/example.test Chrome --headless
|
||||
```
|
||||
|
||||
## Variable Validation
|
||||
|
||||
PlayWrong now provides comprehensive validation for environment variables:
|
||||
|
||||
### Linting (Static Analysis)
|
||||
```bash
|
||||
node src/cli.js tests/example.test Chrome --lint
|
||||
```
|
||||
- **Common variables** like `$PASSWORD`, `$PASSWORDMAIL`, `$EMAIL`, `$USERNAME` show as info messages
|
||||
- **Undefined variables** that are not commonly defined show as warnings
|
||||
|
||||
### Runtime Validation
|
||||
During execution, undefined variables:
|
||||
- Show prominent warnings with helpful instructions
|
||||
- Are used as literal text (e.g., `$UNDEFINED_VAR` stays as `$UNDEFINED_VAR`)
|
||||
- Can be treated as errors with `--strict-variables` flag
|
||||
|
||||
### Strict Mode
|
||||
```bash
|
||||
node src/cli.js tests/example.test Chrome --strict-variables
|
||||
```
|
||||
- Treats undefined variables as errors
|
||||
- Stops execution immediately when an undefined variable is encountered
|
||||
- Useful for CI/CD pipelines to ensure all variables are properly set
|
||||
|
||||
## Test Language Syntax
|
||||
|
||||
### Profiles
|
||||
@@ -103,6 +128,20 @@ sleep 2500 "waiting for animation"
|
||||
sleep 500 "let page settle"
|
||||
```
|
||||
|
||||
#### jumpIf
|
||||
Jump over a specified number of commands if an element exists:
|
||||
```
|
||||
jumpIf element=span childText="Server-Warenkorb" jump=4
|
||||
jumpIf element=div id="error-message" jump=2
|
||||
```
|
||||
|
||||
#### jumpIfNot
|
||||
Jump over a specified number of commands if an element doesn't exist:
|
||||
```
|
||||
jumpIfNot element=span childText="Server-Warenkorb" jump=4
|
||||
jumpIfNot element=div id="success-message" jump=2
|
||||
```
|
||||
|
||||
### Element Selectors
|
||||
|
||||
You can combine multiple attributes:
|
||||
|
||||
26
loop-test.bat
Normal file
26
loop-test.bat
Normal file
@@ -0,0 +1,26 @@
|
||||
@echo off
|
||||
echo PlayWrong Test Loop Script
|
||||
echo ========================
|
||||
echo.
|
||||
echo This script will run the PlayWrong test in a loop until it fails.
|
||||
echo Press Ctrl+C to stop the loop manually.
|
||||
echo.
|
||||
|
||||
if "%~1"=="" (
|
||||
echo Usage: loop-test.bat ^<test-file^> [profile] [options]
|
||||
echo.
|
||||
echo Examples:
|
||||
echo loop-test.bat step1.test Chrome
|
||||
echo loop-test.bat step1.test Chrome --delay 1000
|
||||
echo loop-test.bat step1.test Chrome --log my-test.log
|
||||
echo.
|
||||
echo Press any key to exit...
|
||||
pause >nul
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Starting test loop for: %1 with profile: %2
|
||||
echo Log will be saved to a timestamped file
|
||||
echo.
|
||||
|
||||
node loop-test.js %*
|
||||
204
loop-test.js
Normal file
204
loop-test.js
Normal file
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class TestLooper {
|
||||
constructor(testFile, profile = 'Chrome', options = {}) {
|
||||
this.testFile = testFile;
|
||||
this.profile = profile;
|
||||
this.options = options;
|
||||
this.runCount = 0;
|
||||
this.successCount = 0;
|
||||
this.failureCount = 0;
|
||||
this.startTime = Date.now();
|
||||
this.logFile = options.logFile || `test-loop-${Date.now()}.log`;
|
||||
}
|
||||
|
||||
log(message) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logMessage = `[${timestamp}] ${message}`;
|
||||
console.log(logMessage);
|
||||
|
||||
// Also write to log file
|
||||
fs.appendFileSync(this.logFile, logMessage + '\n');
|
||||
}
|
||||
|
||||
async runSingleTest() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.runCount++;
|
||||
const startTime = Date.now();
|
||||
|
||||
this.log(`Starting test run #${this.runCount}`);
|
||||
|
||||
const child = spawn('node', ['src/cli.js', this.testFile, this.profile], {
|
||||
stdio: 'pipe',
|
||||
env: process.env
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
stdout += output;
|
||||
// Show real-time progress by forwarding key output lines
|
||||
const lines = output.split('\n').filter(line => line.trim());
|
||||
for (const line of lines) {
|
||||
if (line.includes('Executing:') || line.includes('✓ Completed') || line.includes('Running test file:')) {
|
||||
console.log(` ${line}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
stderr += output;
|
||||
// Show errors in real-time
|
||||
const lines = output.split('\n').filter(line => line.trim());
|
||||
for (const line of lines) {
|
||||
console.log(` ERROR: ${line}`);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
const duration = Date.now() - startTime;
|
||||
const minutes = Math.floor(duration / 60000);
|
||||
const seconds = ((duration % 60000) / 1000).toFixed(1);
|
||||
|
||||
if (code === 0) {
|
||||
this.successCount++;
|
||||
this.log(`✅ Test run #${this.runCount} PASSED (${minutes}m ${seconds}s) - Total passed: ${this.successCount}`);
|
||||
resolve({ success: true, code, stdout, stderr, duration });
|
||||
} else {
|
||||
this.failureCount++;
|
||||
this.log(`❌ Test run #${this.runCount} FAILED with exit code ${code} (${minutes}m ${seconds}s) - Total passed: ${this.successCount}`);
|
||||
this.log(`STDOUT: ${stdout}`);
|
||||
this.log(`STDERR: ${stderr}`);
|
||||
resolve({ success: false, code, stdout, stderr, duration });
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
this.failureCount++;
|
||||
this.log(`❌ Test run #${this.runCount} ERROR: ${error.message}`);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
printStatistics() {
|
||||
const totalTime = Date.now() - this.startTime;
|
||||
const totalMinutes = Math.floor(totalTime / 60000);
|
||||
const totalSeconds = ((totalTime % 60000) / 1000).toFixed(1);
|
||||
|
||||
this.log('\n=== FINAL STATISTICS ===');
|
||||
this.log(`Total runs: ${this.runCount}`);
|
||||
this.log(`Successful runs: ${this.successCount}`);
|
||||
this.log(`Failed runs: ${this.failureCount}`);
|
||||
this.log(`Success rate: ${this.runCount > 0 ? ((this.successCount / this.runCount) * 100).toFixed(1) : 0}%`);
|
||||
this.log(`Total time: ${totalMinutes}m ${totalSeconds}s`);
|
||||
this.log(`Average time per run: ${this.runCount > 0 ? (totalTime / this.runCount / 1000).toFixed(1) : 0}s`);
|
||||
this.log(`Log file: ${this.logFile}`);
|
||||
}
|
||||
|
||||
async loop() {
|
||||
this.log(`Starting test loop for: ${this.testFile} with profile: ${this.profile}`);
|
||||
this.log(`Log file: ${this.logFile}`);
|
||||
this.log(`Press Ctrl+C to stop the loop\n`);
|
||||
|
||||
// Handle Ctrl+C gracefully
|
||||
process.on('SIGINT', () => {
|
||||
this.log('\n\n🛑 Loop interrupted by user');
|
||||
this.printStatistics();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const result = await this.runSingleTest();
|
||||
|
||||
if (!result.success) {
|
||||
this.log('\n🔴 Test failed! Stopping loop...');
|
||||
this.printStatistics();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Optional: Add a small delay between runs
|
||||
if (this.options.delay) {
|
||||
await new Promise(resolve => setTimeout(resolve, this.options.delay));
|
||||
}
|
||||
|
||||
// Print periodic statistics
|
||||
if (this.runCount % 10 === 0) {
|
||||
const currentTime = Date.now() - this.startTime;
|
||||
const minutes = Math.floor(currentTime / 60000);
|
||||
const seconds = ((currentTime % 60000) / 1000).toFixed(1);
|
||||
this.log(`📊 Progress: ${this.runCount} runs completed, ${this.successCount} successful (${minutes}m ${seconds}s elapsed)`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.log(`💥 Unexpected error: ${error.message}`);
|
||||
this.printStatistics();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CLI handling
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.log('Usage: node loop-test.js <test-file> [profile] [options]');
|
||||
console.log('');
|
||||
console.log('Arguments:');
|
||||
console.log(' test-file Path to the test file (e.g., step1.test)');
|
||||
console.log(' profile Browser profile (Chrome, Mobile, MobileSmall) [default: Chrome]');
|
||||
console.log('');
|
||||
console.log('Options:');
|
||||
console.log(' --delay <ms> Add delay between test runs in milliseconds');
|
||||
console.log(' --log <file> Custom log file name');
|
||||
console.log('');
|
||||
console.log('Examples:');
|
||||
console.log(' node loop-test.js step1.test Chrome');
|
||||
console.log(' node loop-test.js step1.test Chrome --delay 1000');
|
||||
console.log(' node loop-test.js step1.test Chrome --log my-test-log.txt');
|
||||
console.log('');
|
||||
console.log('The script will run the test in a loop until it fails.');
|
||||
console.log('Press Ctrl+C to stop the loop manually.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const testFile = args[0];
|
||||
const profile = args[1] || 'Chrome';
|
||||
|
||||
// Parse options
|
||||
const options = {};
|
||||
const delayIndex = args.indexOf('--delay');
|
||||
if (delayIndex !== -1 && args[delayIndex + 1]) {
|
||||
options.delay = parseInt(args[delayIndex + 1]);
|
||||
}
|
||||
|
||||
const logIndex = args.indexOf('--log');
|
||||
if (logIndex !== -1 && args[logIndex + 1]) {
|
||||
options.logFile = args[logIndex + 1];
|
||||
}
|
||||
|
||||
// Check if test file exists
|
||||
if (!fs.existsSync(testFile)) {
|
||||
console.error(`❌ Test file not found: ${testFile}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const looper = new TestLooper(testFile, profile, options);
|
||||
await looper.loop();
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
main().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = TestLooper;
|
||||
25
loop-test.sh
Normal file
25
loop-test.sh
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "PlayWrong Test Loop Script"
|
||||
echo "========================"
|
||||
echo ""
|
||||
echo "This script will run the PlayWrong test in a loop until it fails."
|
||||
echo "Press Ctrl+C to stop the loop manually."
|
||||
echo ""
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: ./loop-test.sh <test-file> [profile] [options]"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " ./loop-test.sh step1.test Chrome"
|
||||
echo " ./loop-test.sh step1.test Chrome --delay 1000"
|
||||
echo " ./loop-test.sh step1.test Chrome --log my-test.log"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Starting test loop for: $1 with profile: $2"
|
||||
echo "Log will be saved to a timestamped file"
|
||||
echo ""
|
||||
|
||||
node loop-test.js "$@"
|
||||
65
src/cli.js
65
src/cli.js
@@ -79,7 +79,46 @@ class TestRunner {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
if (this.executor) {
|
||||
await this.executor.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global reference to runner for cleanup
|
||||
let currentRunner = null;
|
||||
|
||||
// Signal handler for clean shutdown
|
||||
async function handleShutdown(signal) {
|
||||
console.log(`\n🛑 Received ${signal}, shutting down gracefully...`);
|
||||
|
||||
if (currentRunner) {
|
||||
// Signal the executor to stop
|
||||
if (currentRunner.executor) {
|
||||
currentRunner.executor.shouldStop = true;
|
||||
}
|
||||
|
||||
try {
|
||||
await currentRunner.cleanup();
|
||||
console.log('✅ Browser cleanup completed');
|
||||
} catch (error) {
|
||||
console.log('⚠️ Browser cleanup had issues:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('👋 Goodbye!');
|
||||
|
||||
// Give a moment for any remaining cleanup, then exit
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Register signal handlers
|
||||
process.on('SIGINT', handleShutdown); // Ctrl+C
|
||||
process.on('SIGTERM', handleShutdown); // Termination signal
|
||||
|
||||
// CLI handling
|
||||
async function main() {
|
||||
@@ -95,12 +134,14 @@ async function main() {
|
||||
console.log(' --full-page Take full page screenshots instead of viewport only');
|
||||
console.log(' --lint Run linter before execution');
|
||||
console.log(' --strict Treat linter warnings as errors');
|
||||
console.log(' --strict-variables Treat undefined variables as errors');
|
||||
console.log('Examples:');
|
||||
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 --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 --strict-variables');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -111,20 +152,40 @@ async function main() {
|
||||
const fullPageScreenshots = args.includes('--full-page');
|
||||
const lint = args.includes('--lint');
|
||||
const strict = args.includes('--strict');
|
||||
const strictVariables = args.includes('--strict-variables');
|
||||
|
||||
if (!await fs.pathExists(testFile)) {
|
||||
console.error(`Test file not found: ${testFile}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const runner = new TestRunner({ headless, disableAnimations, fullPageScreenshots, lint, strict });
|
||||
const runner = new TestRunner({ headless, disableAnimations, fullPageScreenshots, lint, strict, strictVariables });
|
||||
currentRunner = runner; // Store reference for cleanup
|
||||
|
||||
try {
|
||||
await runner.runTestFile(testFile, profile);
|
||||
} catch (error) {
|
||||
console.error('❌ Test execution failed:', error.message);
|
||||
|
||||
// Clean up on error
|
||||
try {
|
||||
await runner.cleanup();
|
||||
} catch (cleanupError) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
} finally {
|
||||
currentRunner = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
main().catch(console.error);
|
||||
main().catch((error) => {
|
||||
console.error('❌ Unexpected error:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = TestRunner;
|
||||
672
src/executor.js
672
src/executor.js
@@ -15,7 +15,9 @@ class TestExecutor {
|
||||
this.disableAnimations = options.disableAnimations !== false; // Default to disable animations
|
||||
this.fullPageScreenshots = options.fullPageScreenshots || false; // Default to viewport screenshots
|
||||
this.enableScreenshots = options.enableScreenshots !== false; // Default to enable screenshots
|
||||
this.strictVariables = options.strictVariables || false; // Default to allow undefined variables
|
||||
this.variables = {}; // Store extracted variables
|
||||
this.shouldStop = false; // Flag to stop execution on shutdown
|
||||
|
||||
this.profiles = {
|
||||
Chrome: {
|
||||
@@ -43,23 +45,107 @@ class TestExecutor {
|
||||
await this.updateStatus({ type: 'initializing' }, false);
|
||||
}
|
||||
|
||||
for (const command of commands) {
|
||||
// Update status before executing command
|
||||
if (!this.headless) {
|
||||
await this.updateStatus(command, false);
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
// Check if we should stop execution (e.g., due to Ctrl+C)
|
||||
if (this.shouldStop) {
|
||||
console.log('🛑 Stopping test execution due to shutdown request');
|
||||
break;
|
||||
}
|
||||
|
||||
await this.executeCommand(command);
|
||||
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) {
|
||||
// Find next interactive command for display when paused
|
||||
let nextInteractiveCommand = null;
|
||||
for (let j = i; j < commands.length; j++) {
|
||||
if (this.isInteractiveCommand(commands[j])) {
|
||||
nextInteractiveCommand = commands[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await this.updateStatus(command, false, nextInteractiveCommand);
|
||||
}
|
||||
|
||||
// Check for manual dump requests
|
||||
if (!this.headless) {
|
||||
await this.checkDumpRequests();
|
||||
}
|
||||
|
||||
// Check again if we should stop before executing the command
|
||||
if (this.shouldStop) {
|
||||
console.log('🛑 Stopping test execution due to shutdown request');
|
||||
break;
|
||||
}
|
||||
|
||||
let jumpCount = 0;
|
||||
try {
|
||||
jumpCount = await this.executeCommand(command);
|
||||
} catch (error) {
|
||||
// Check if this is a browser disconnection error
|
||||
if (error.message.includes('Target page, context or browser has been closed') ||
|
||||
error.message.includes('Browser has been closed') ||
|
||||
error.message.includes('Context was closed')) {
|
||||
console.log('🔔 Browser was closed during command execution - stopping gracefully');
|
||||
this.shouldStop = true;
|
||||
break;
|
||||
} else {
|
||||
throw error; // Re-throw other errors
|
||||
}
|
||||
}
|
||||
|
||||
// Handle jump commands
|
||||
if (jumpCount > 0) {
|
||||
console.log(`🔄 Jumping ${jumpCount} commands`);
|
||||
i += jumpCount; // Skip the specified number of commands
|
||||
}
|
||||
|
||||
// Update status after completing command
|
||||
if (!this.headless) {
|
||||
await this.updateStatus(command, true);
|
||||
// Find next interactive command for display when paused
|
||||
let nextInteractiveCommand = null;
|
||||
for (let j = i + 1; j < commands.length; j++) {
|
||||
if (this.isInteractiveCommand(commands[j])) {
|
||||
nextInteractiveCommand = commands[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await this.updateStatus(command, true, nextInteractiveCommand);
|
||||
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// Check if this is a browser disconnection error
|
||||
if (error.message.includes('Target page, context or browser has been closed') ||
|
||||
error.message.includes('Browser has been closed') ||
|
||||
error.message.includes('Context was closed')) {
|
||||
console.log('🔔 Browser was closed by user - stopping test execution gracefully');
|
||||
this.shouldStop = true;
|
||||
} else {
|
||||
console.error('Test execution failed:', error);
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
await this.cleanup();
|
||||
}
|
||||
@@ -78,6 +164,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',
|
||||
@@ -85,6 +172,12 @@ class TestExecutor {
|
||||
]
|
||||
});
|
||||
|
||||
// Handle main browser disconnection
|
||||
this.browser.on('disconnected', () => {
|
||||
console.log('🔔 Main browser disconnected - stopping test execution');
|
||||
this.shouldStop = true;
|
||||
});
|
||||
|
||||
this.context = await this.browser.newContext({
|
||||
viewport: profile.viewport,
|
||||
userAgent: profile.userAgent,
|
||||
@@ -92,11 +185,21 @@ class TestExecutor {
|
||||
reducedMotion: 'reduce',
|
||||
// Force consistent viewport
|
||||
screen: profile.viewport,
|
||||
deviceScaleFactor: 1
|
||||
deviceScaleFactor: 1,
|
||||
// Allow popups to be opened
|
||||
javaScriptEnabled: true,
|
||||
// Handle popups properly
|
||||
ignoreHTTPSErrors: true
|
||||
});
|
||||
|
||||
this.page = await this.context.newPage();
|
||||
|
||||
// Handle popup events for follow command
|
||||
this.page.on('popup', async (popup) => {
|
||||
console.log('🔄 Popup detected, adding to context');
|
||||
// The popup is automatically added to the context.pages() array
|
||||
});
|
||||
|
||||
// Create status window for headed mode AFTER main page so it appears on top
|
||||
if (!this.headless) {
|
||||
await this.createStatusWindow();
|
||||
@@ -161,6 +264,19 @@ class TestExecutor {
|
||||
}
|
||||
|
||||
async executeCommand(command) {
|
||||
// Check if we should stop before executing any command
|
||||
if (this.shouldStop) {
|
||||
console.log('🛑 Skipping command execution due to shutdown request');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if browser is still connected
|
||||
if (!this.browser || !this.browser.isConnected()) {
|
||||
console.log('🛑 Skipping command execution - browser disconnected');
|
||||
this.shouldStop = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Create a clean one-line representation of the command
|
||||
const commandStr = this.formatCommandForOutput(command);
|
||||
console.log(`Executing: ${commandStr}`);
|
||||
@@ -239,9 +355,17 @@ class TestExecutor {
|
||||
await this.extractToVariable(command);
|
||||
break;
|
||||
|
||||
case 'jumpIf':
|
||||
return await this.jumpIf(command);
|
||||
|
||||
case 'jumpIfNot':
|
||||
return await this.jumpIfNot(command);
|
||||
|
||||
default:
|
||||
console.warn(`Unknown command type: ${command.type}`);
|
||||
}
|
||||
|
||||
return 0; // No jump by default
|
||||
}
|
||||
|
||||
async waitForElement(command) {
|
||||
@@ -699,14 +823,46 @@ 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'
|
||||
]
|
||||
});
|
||||
|
||||
// Handle status browser disconnection
|
||||
this.statusBrowser.on('disconnected', () => {
|
||||
console.log('🔔 Status browser disconnected - stopping test execution');
|
||||
this.shouldStop = true;
|
||||
this.statusPage = null;
|
||||
this.statusContext = null;
|
||||
this.statusBrowser = null;
|
||||
});
|
||||
|
||||
// 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();
|
||||
|
||||
// Handle status window close event
|
||||
this.statusPage.on('close', () => {
|
||||
console.log('🔔 Status window closed by user - stopping test execution');
|
||||
this.shouldStop = true;
|
||||
this.statusPage = null;
|
||||
});
|
||||
|
||||
// Create the status window HTML
|
||||
const statusHTML = `
|
||||
<!DOCTYPE html>
|
||||
@@ -801,12 +957,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>
|
||||
@@ -843,7 +1067,102 @@ 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');
|
||||
const titleEl = document.querySelector('.status-section .status-title');
|
||||
|
||||
if (isPaused) {
|
||||
button.textContent = '▶️ Resume';
|
||||
button.className = 'control-button resume-btn';
|
||||
indicator.className = 'status-indicator status-paused';
|
||||
stepBtn.disabled = false;
|
||||
|
||||
// Update title to show next command
|
||||
if (titleEl) {
|
||||
titleEl.innerHTML = '<span id="status-indicator" class="status-indicator status-paused"></span>Next Command (+1 Step)';
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// Update title to show current command
|
||||
if (titleEl) {
|
||||
titleEl.innerHTML = '<span id="status-indicator" class="status-indicator status-running"></span>Current Command';
|
||||
}
|
||||
|
||||
// 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>
|
||||
`;
|
||||
|
||||
@@ -863,7 +1182,132 @@ class TestExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
async updateStatus(command, isCompleted = false) {
|
||||
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) {
|
||||
// Check if we should stop execution (e.g., due to Ctrl+C)
|
||||
if (this.shouldStop) {
|
||||
console.log('🛑 Breaking out of pause due to shutdown request');
|
||||
return false;
|
||||
}
|
||||
|
||||
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, nextInteractiveCommand = null) {
|
||||
if (!this.statusPage || this.headless) return;
|
||||
|
||||
try {
|
||||
@@ -882,10 +1326,27 @@ class TestExecutor {
|
||||
|
||||
const progress = this.totalCommands > 0 ? (this.completedCommands / this.totalCommands) * 100 : 0;
|
||||
|
||||
await this.statusPage.evaluate(({ command, completed, total, elapsed, progress }) => {
|
||||
// Update current command
|
||||
await this.statusPage.evaluate(({ command, nextInteractiveCommand, completed, total, elapsed, progress }) => {
|
||||
// Update command display based on pause state
|
||||
const commandEl = document.getElementById('current-command');
|
||||
const indicator = document.getElementById('status-indicator');
|
||||
const titleEl = document.querySelector('.status-section .status-title');
|
||||
|
||||
if (window.playwrongPaused && nextInteractiveCommand) {
|
||||
// When paused, show next interactive command
|
||||
if (commandEl) commandEl.textContent = nextInteractiveCommand;
|
||||
if (indicator) indicator.className = 'status-indicator status-paused';
|
||||
if (titleEl) {
|
||||
titleEl.innerHTML = '<span id="status-indicator" class="status-indicator status-paused"></span>Next Command (+1 Step)';
|
||||
}
|
||||
} else {
|
||||
// When running, show current command
|
||||
if (commandEl) commandEl.textContent = command;
|
||||
if (indicator) indicator.className = 'status-indicator status-running';
|
||||
if (titleEl) {
|
||||
titleEl.innerHTML = '<span id="status-indicator" class="status-indicator status-running"></span>Current Command';
|
||||
}
|
||||
}
|
||||
|
||||
// Update statistics
|
||||
const completedEl = document.getElementById('completed-count');
|
||||
@@ -901,12 +1362,9 @@ class TestExecutor {
|
||||
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),
|
||||
nextInteractiveCommand: nextInteractiveCommand ? this.formatCommandForOutput(nextInteractiveCommand) : null,
|
||||
completed: this.completedCommands,
|
||||
total: this.totalCommands,
|
||||
elapsed: elapsedSeconds,
|
||||
@@ -922,15 +1380,69 @@ class TestExecutor {
|
||||
async followToNewWindow() {
|
||||
console.log('🔄 Switching to new tab...');
|
||||
|
||||
// Get all current pages/tabs
|
||||
const pages = this.context.pages();
|
||||
console.log(`🔄 Debug: Found ${pages.length} pages total`);
|
||||
pages.forEach((page, index) => {
|
||||
console.log(`🔄 Page ${index}: ${page.url()}`);
|
||||
});
|
||||
|
||||
if (pages.length < 2) {
|
||||
throw new Error('No additional tabs found to switch to');
|
||||
// Check if we already have multiple pages (popup already created)
|
||||
if (pages.length >= 2) {
|
||||
// Find the newest tab (not the current one)
|
||||
let newTab = null;
|
||||
const currentUrl = this.page.url();
|
||||
|
||||
// Look for a tab that's different from the current one
|
||||
for (let i = pages.length - 1; i >= 0; i--) {
|
||||
if (pages[i].url() !== currentUrl) {
|
||||
newTab = pages[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Switch to the last (most recently opened) tab
|
||||
const newTab = pages[pages.length - 1];
|
||||
if (newTab) {
|
||||
console.log(`🔄 Found existing new tab: ${newTab.url()}`);
|
||||
|
||||
// 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');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Wait for a new tab to be created
|
||||
const initialPageCount = pages.length;
|
||||
let newTab = null;
|
||||
let retryCount = 0;
|
||||
const maxRetries = 20; // 10 seconds total (500ms * 20)
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
const currentPages = this.context.pages();
|
||||
|
||||
if (currentPages.length > initialPageCount) {
|
||||
// New tab found, get the most recently opened one
|
||||
newTab = currentPages[currentPages.length - 1];
|
||||
break;
|
||||
}
|
||||
|
||||
// Wait a bit before retrying
|
||||
await this.page.waitForTimeout(500);
|
||||
retryCount++;
|
||||
|
||||
if (retryCount % 5 === 0) {
|
||||
console.log(`🔄 Still waiting for new tab... (${retryCount}/${maxRetries})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!newTab) {
|
||||
throw new Error('No additional tabs found to switch to after waiting');
|
||||
}
|
||||
|
||||
console.log(`🔄 New tab found: ${newTab.url()}`);
|
||||
|
||||
// Wait for the tab to be ready
|
||||
await newTab.waitForLoadState('networkidle');
|
||||
@@ -988,6 +1500,34 @@ class TestExecutor {
|
||||
console.log(`✅ Variable "${variableName}" set to: "${value}"`);
|
||||
}
|
||||
|
||||
async jumpIf(command) {
|
||||
const selector = this.buildSelector(command);
|
||||
|
||||
try {
|
||||
// Check if element exists (with a short timeout)
|
||||
await this.page.locator(selector).first().waitFor({ timeout: 1000 });
|
||||
console.log(`🔄 jumpIf: Element found, jumping ${command.jumpCount} commands`);
|
||||
return command.jumpCount;
|
||||
} catch (error) {
|
||||
console.log(`🔄 jumpIf: Element not found, continuing normally`);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
async jumpIfNot(command) {
|
||||
const selector = this.buildSelector(command);
|
||||
|
||||
try {
|
||||
// Check if element exists (with a short timeout)
|
||||
await this.page.locator(selector).first().waitFor({ timeout: 1000 });
|
||||
console.log(`🔄 jumpIfNot: Element found, continuing normally`);
|
||||
return 0;
|
||||
} catch (error) {
|
||||
console.log(`🔄 jumpIfNot: Element not found, jumping ${command.jumpCount} commands`);
|
||||
return command.jumpCount;
|
||||
}
|
||||
}
|
||||
|
||||
formatCommandForOutput(command) {
|
||||
switch (command.type) {
|
||||
case 'use':
|
||||
@@ -1017,6 +1557,10 @@ class TestExecutor {
|
||||
return `switchToTab tabIndex=${command.tabIndex}`;
|
||||
case 'extract':
|
||||
return `extract ${this.formatSelector(command)} variableName="${command.variableName}"`;
|
||||
case 'jumpIf':
|
||||
return `jumpIf ${this.formatSelector(command)} jump=${command.jumpCount}`;
|
||||
case 'jumpIfNot':
|
||||
return `jumpIfNot ${this.formatSelector(command)} jump=${command.jumpCount}`;
|
||||
default:
|
||||
return `${command.type} ${JSON.stringify(command)}`;
|
||||
}
|
||||
@@ -1045,6 +1589,18 @@ class TestExecutor {
|
||||
selector += ` href="${command.href}"`;
|
||||
}
|
||||
|
||||
if (command.htmlType) {
|
||||
selector += ` type="${command.htmlType}"`;
|
||||
}
|
||||
|
||||
if (command.for) {
|
||||
selector += ` for="${command.for}"`;
|
||||
}
|
||||
|
||||
if (command.ariaLabel) {
|
||||
selector += ` aria-label="${command.ariaLabel}"`;
|
||||
}
|
||||
|
||||
if (command.child) {
|
||||
// Handle new parentheses syntax
|
||||
if (command.child.startsWith('child=')) {
|
||||
@@ -1113,6 +1669,14 @@ class TestExecutor {
|
||||
selector += `[type="${params.htmlType}"]`;
|
||||
}
|
||||
|
||||
if (params.for) {
|
||||
selector += `[for="${params.for}"]`;
|
||||
}
|
||||
|
||||
if (params.ariaLabel) {
|
||||
selector += `[aria-label="${params.ariaLabel}"]`;
|
||||
}
|
||||
|
||||
// Handle child selectors (nested elements)
|
||||
if (params.child) {
|
||||
// Parse child selector like "span class="MuiBadge-badge""
|
||||
@@ -1168,6 +1732,20 @@ class TestExecutor {
|
||||
selector += `[href="${href}"]`;
|
||||
}
|
||||
|
||||
// Extract for attribute
|
||||
const forMatch = childString.match(/for=(?:"([^"]*)"|([^\s]+))/);
|
||||
if (forMatch) {
|
||||
const forValue = forMatch[1] || forMatch[2];
|
||||
selector += `[for="${forValue}"]`;
|
||||
}
|
||||
|
||||
// Extract aria-label attribute
|
||||
const ariaLabelMatch = childString.match(/aria-label=(?:"([^"]*)"|([^\s]+))/);
|
||||
if (ariaLabelMatch) {
|
||||
const ariaLabelValue = ariaLabelMatch[1] || ariaLabelMatch[2];
|
||||
selector += `[aria-label="${ariaLabelValue}"]`;
|
||||
}
|
||||
|
||||
// Extract childText for the child element
|
||||
const childTextMatch = childString.match(/childText=(?:"([^"]*)"|([^\s]+))/);
|
||||
if (childTextMatch) {
|
||||
@@ -1246,10 +1824,10 @@ class TestExecutor {
|
||||
// Extract simplified DOM structure with only essential elements and attributes
|
||||
const simplifiedStructure = await this.page.evaluate(() => {
|
||||
// Elements we want to keep
|
||||
const keepElements = ['button', 'input', 'textarea', 'span', 'a'];
|
||||
const keepElements = ['button', 'input', 'textarea', 'span', 'a', 'label'];
|
||||
|
||||
// Attributes we want to keep
|
||||
const keepAttributes = ['id', 'name', 'value', 'type'];
|
||||
const keepAttributes = ['id', 'name', 'value', 'type', 'for', 'aria-label'];
|
||||
|
||||
function processElement(element) {
|
||||
const tagName = element.tagName.toLowerCase();
|
||||
@@ -1492,9 +2070,34 @@ class TestExecutor {
|
||||
// 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();
|
||||
if (this.statusBrowser && !this.headless) {
|
||||
try {
|
||||
await this.statusBrowser.close();
|
||||
} catch (error) {
|
||||
// Ignore errors when closing status browser
|
||||
}
|
||||
}
|
||||
if (this.page) {
|
||||
try {
|
||||
await this.page.close();
|
||||
} catch (error) {
|
||||
// Ignore errors when closing page
|
||||
}
|
||||
}
|
||||
if (this.context) {
|
||||
try {
|
||||
await this.context.close();
|
||||
} catch (error) {
|
||||
// Ignore errors when closing context
|
||||
}
|
||||
}
|
||||
if (this.browser) {
|
||||
try {
|
||||
await this.browser.close();
|
||||
} catch (error) {
|
||||
// Ignore errors when closing browser
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolveEnvironmentVariables(value) {
|
||||
@@ -1512,7 +2115,18 @@ class TestExecutor {
|
||||
// Then check environment variables
|
||||
const envValue = process.env[varName];
|
||||
if (envValue === undefined) {
|
||||
console.warn(`Warning: Variable ${varName} is not defined in stored variables or environment`);
|
||||
const warningMessage = `⚠️ WARNING: Variable ${varName} is not defined in stored variables or environment`;
|
||||
console.warn(warningMessage);
|
||||
|
||||
// Log additional helpful information
|
||||
console.warn(` Variable '${varName}' will be used as literal text: '${match}'`);
|
||||
console.warn(` To fix this, set the environment variable or use the 'extract' command to store the variable`);
|
||||
|
||||
// In strict mode, throw an error for undefined variables
|
||||
if (this.strictVariables) {
|
||||
throw new Error(`Undefined variable: ${varName}. In strict variables mode, all variables must be defined.`);
|
||||
}
|
||||
|
||||
return match; // Return original if not found
|
||||
}
|
||||
return envValue;
|
||||
|
||||
@@ -6,7 +6,7 @@ class TestLinter {
|
||||
this.info = [];
|
||||
|
||||
// Valid commands
|
||||
this.validCommands = ['use', 'open', 'wait', 'click', 'scroll', 'fill', 'break', 'sleep', 'dump', 'follow', 'switchToTab', 'extract'];
|
||||
this.validCommands = ['use', 'open', 'wait', 'click', 'scroll', 'fill', 'break', 'sleep', 'dump', 'follow', 'switchToTab', 'extract', 'jumpIf', 'jumpIfNot'];
|
||||
|
||||
// Valid HTML elements
|
||||
this.validElements = [
|
||||
@@ -32,7 +32,9 @@ class TestLinter {
|
||||
'dump': ['name'],
|
||||
'follow': [],
|
||||
'switchToTab': ['tabIndex'],
|
||||
'extract': ['element', 'attribute']
|
||||
'extract': ['element', 'attribute'],
|
||||
'jumpIf': ['element', 'jump'],
|
||||
'jumpIfNot': ['element', 'jump']
|
||||
};
|
||||
|
||||
// Initialize rules
|
||||
@@ -268,6 +270,14 @@ class TestLinter {
|
||||
continue; // This is valid
|
||||
}
|
||||
|
||||
if (line.cleaned.startsWith('jumpIf ')) {
|
||||
continue; // This is valid
|
||||
}
|
||||
|
||||
if (line.cleaned.startsWith('jumpIfNot ')) {
|
||||
continue; // This is valid
|
||||
}
|
||||
|
||||
if (!this.validCommands.includes(command)) {
|
||||
this.addError(`Invalid command '${command}'. Valid commands: ${this.validCommands.join(', ')}`, line.lineNumber);
|
||||
}
|
||||
@@ -373,9 +383,14 @@ class TestLinter {
|
||||
this.addWarning(`Variable '${variable}' should use UPPER_CASE naming convention`, line.lineNumber);
|
||||
}
|
||||
|
||||
// Info about common variables
|
||||
if (['$PASSWORD', '$PASSWORDMAIL', '$EMAIL', '$USERNAME'].includes(variable)) {
|
||||
// Check if variable is commonly defined
|
||||
const commonVariables = ['$PASSWORD', '$PASSWORDMAIL', '$EMAIL', '$USERNAME', '$API_KEY', '$TOKEN', '$BASE_URL'];
|
||||
|
||||
if (commonVariables.includes(variable)) {
|
||||
this.addInfo(`Using environment variable '${variable}' - ensure it's defined in your environment`, line.lineNumber);
|
||||
} else {
|
||||
// Warn about potentially undefined variables
|
||||
this.addWarning(`Variable '${variable}' may not be defined. Ensure it's set as an environment variable or stored variable`, line.lineNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -385,23 +400,33 @@ class TestLinter {
|
||||
validateFlowLogic(lines) {
|
||||
let hasUseCommand = false;
|
||||
let useLineNumber = 0;
|
||||
let firstCommandLineIndex = -1;
|
||||
|
||||
for (const line of lines) {
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const command = line.cleaned.split(' ')[0];
|
||||
|
||||
// Track the first command line (index in the cleaned lines array)
|
||||
if (firstCommandLineIndex === -1) {
|
||||
firstCommandLineIndex = i;
|
||||
}
|
||||
|
||||
if (command === 'use') {
|
||||
if (hasUseCommand) {
|
||||
this.addWarning(`Multiple 'use' commands found. Consider using multi-profile syntax instead.`, line.lineNumber);
|
||||
}
|
||||
hasUseCommand = true;
|
||||
useLineNumber = line.lineNumber;
|
||||
|
||||
// Check if this is NOT the first command in the cleaned lines
|
||||
if (i > 0) {
|
||||
this.addInfo(`'use' command found at line ${useLineNumber}. Consider placing it at the beginning of the test.`, useLineNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasUseCommand) {
|
||||
this.addWarning(`No 'use' command found. Tests should specify browser profile.`, 1);
|
||||
} else if (useLineNumber > 1) {
|
||||
this.addInfo(`'use' command found at line ${useLineNumber}. Consider placing it at the beginning of the test.`, useLineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -107,6 +107,8 @@ class TestParser {
|
||||
class: params.class,
|
||||
href: params.href,
|
||||
htmlType: params.type, // Store HTML type as htmlType to avoid conflict
|
||||
for: params.for,
|
||||
ariaLabel: params['aria-label'],
|
||||
child: params.child,
|
||||
childText: params.childText
|
||||
};
|
||||
@@ -124,6 +126,8 @@ class TestParser {
|
||||
class: params.class,
|
||||
href: params.href,
|
||||
htmlType: params.type, // Store HTML type as htmlType to avoid conflict
|
||||
for: params.for,
|
||||
ariaLabel: params['aria-label'],
|
||||
child: params.child,
|
||||
childText: params.childText
|
||||
};
|
||||
@@ -141,6 +145,8 @@ class TestParser {
|
||||
class: params.class,
|
||||
href: params.href,
|
||||
htmlType: params.type, // Store HTML type as htmlType to avoid conflict
|
||||
for: params.for,
|
||||
ariaLabel: params['aria-label'],
|
||||
child: params.child,
|
||||
childText: params.childText
|
||||
};
|
||||
@@ -159,6 +165,8 @@ class TestParser {
|
||||
class: params.class,
|
||||
href: params.href,
|
||||
htmlType: params.type, // Store HTML type as htmlType to avoid conflict
|
||||
for: params.for,
|
||||
ariaLabel: params['aria-label'],
|
||||
child: params.child,
|
||||
childText: params.childText
|
||||
};
|
||||
@@ -193,6 +201,56 @@ class TestParser {
|
||||
};
|
||||
}
|
||||
|
||||
// Parse jumpIf command: jumpIf element=span childText="Server-Warenkorb" jump=4
|
||||
if (line.startsWith('jumpIf ')) {
|
||||
const jumpMatch = line.match(/jump=(\d+)/);
|
||||
if (jumpMatch) {
|
||||
const jumpCount = parseInt(jumpMatch[1]);
|
||||
const selectorPart = line.substring(7).replace(/\s+jump=\d+/, ''); // Remove 'jumpIf ' and jump=X
|
||||
const params = this.parseParameters(selectorPart);
|
||||
|
||||
return {
|
||||
type: 'jumpIf',
|
||||
jumpCount: jumpCount,
|
||||
element: params.element,
|
||||
name: params.name,
|
||||
id: params.id,
|
||||
class: params.class,
|
||||
href: params.href,
|
||||
htmlType: params.type,
|
||||
for: params.for,
|
||||
ariaLabel: params['aria-label'],
|
||||
child: params.child,
|
||||
childText: params.childText
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Parse jumpIfNot command: jumpIfNot element=span childText="Server-Warenkorb" jump=4
|
||||
if (line.startsWith('jumpIfNot ')) {
|
||||
const jumpMatch = line.match(/jump=(\d+)/);
|
||||
if (jumpMatch) {
|
||||
const jumpCount = parseInt(jumpMatch[1]);
|
||||
const selectorPart = line.substring(10).replace(/\s+jump=\d+/, ''); // Remove 'jumpIfNot ' and jump=X
|
||||
const params = this.parseParameters(selectorPart);
|
||||
|
||||
return {
|
||||
type: 'jumpIfNot',
|
||||
jumpCount: jumpCount,
|
||||
element: params.element,
|
||||
name: params.name,
|
||||
id: params.id,
|
||||
class: params.class,
|
||||
href: params.href,
|
||||
htmlType: params.type,
|
||||
for: params.for,
|
||||
ariaLabel: params['aria-label'],
|
||||
child: params.child,
|
||||
childText: params.childText
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Parse switch to tab command: switch to tab 0
|
||||
if (line.startsWith('switch to tab ')) {
|
||||
const match = line.match(/switch to tab (\d+)/);
|
||||
@@ -223,6 +281,8 @@ class TestParser {
|
||||
class: params.class,
|
||||
href: params.href,
|
||||
htmlType: params.type,
|
||||
for: params.for,
|
||||
ariaLabel: params['aria-label'],
|
||||
child: params.child,
|
||||
childText: params.childText,
|
||||
target: params.target
|
||||
@@ -277,7 +337,7 @@ class TestParser {
|
||||
}
|
||||
|
||||
parseSimpleParameters(paramString, params) {
|
||||
const regex = /(\w+)=(?:"([^"]*)"|([^\s]+))/g;
|
||||
const regex = /([\w-]+)=(?:"([^"]*)"|([^\s]+))/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(paramString)) !== null) {
|
||||
|
||||
73
step1.test
73
step1.test
@@ -31,11 +31,16 @@ 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"
|
||||
sleep 2000 "anmelden click"
|
||||
#dump "isServer"
|
||||
jumpIfNot element=span childText="Server-Warenkorb löschen" jump=2
|
||||
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"
|
||||
@@ -53,7 +58,9 @@ 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"
|
||||
scroll element=button childText="Bestellung abschließen"
|
||||
@@ -62,65 +69,5 @@ 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 3000 "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"
|
||||
77
step1_dhl.test
Normal file
77
step1_dhl.test
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
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://dev.seedheads.de"
|
||||
sleep 200 "page load"
|
||||
wait element=a href=/Kategorie/Seeds
|
||||
click element=a href=/Kategorie/Seeds
|
||||
sleep 200 "seed click"
|
||||
wait element=button childText="In den Korb"
|
||||
click element=button childText="In den Korb"
|
||||
sleep 200 "in korb click"
|
||||
wait element=span class="MuiBadge-badge" childText="1"
|
||||
click element=button child=span(class="MuiBadge-badge" childText="1")
|
||||
sleep 200 "korb click"
|
||||
wait element=button childText="Weiter zur Kasse"
|
||||
click element=button childText="Weiter zur Kasse"
|
||||
sleep 200 "weiter click"
|
||||
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 2000 "anmelden click"
|
||||
#dump "isServer"
|
||||
jumpIfNot element=span childText="Server-Warenkorb löschen" jump=2
|
||||
click element=button childText="Weiter"
|
||||
sleep 2000 "anmelden click"
|
||||
|
||||
# Part 2 - Fill in the checkout form
|
||||
|
||||
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"
|
||||
fill element=input name="lastName" value="Muster"
|
||||
sleep 100 "nachname fill"
|
||||
wait element=input name="street"
|
||||
fill element=input name="street" value="Muster"
|
||||
sleep 100 "strasse fill"
|
||||
wait element=input name="houseNumber"
|
||||
fill element=input name="houseNumber" value="420"
|
||||
sleep 100 "hausnummer fill"
|
||||
wait element=input name="postalCode"
|
||||
fill element=input name="postalCode" value="42023"
|
||||
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"
|
||||
wait element=input id="DHL" name="deliveryMethod" value="DHL" type="radio"
|
||||
scroll element=input id="DHL" name="deliveryMethod" value="DHL" type="radio"
|
||||
click element=input id="DHL" name="deliveryMethod" value="DHL" type="radio"
|
||||
sleep 100 "dhl click"
|
||||
wait element=label childText="Bestimmungen"
|
||||
scroll element=button childText="Bestellung abschließen"
|
||||
click element=label childText="Bestimmungen"
|
||||
sleep 100 "checkbox checked"
|
||||
wait element=button childText="Bestellung abschließen"
|
||||
click element=button childText="Bestellung abschließen"
|
||||
sleep 3000 "order completion"
|
||||
|
||||
80
step1_dhl_nach.test
Normal file
80
step1_dhl_nach.test
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
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://dev.seedheads.de"
|
||||
sleep 200 "page load"
|
||||
wait element=a href=/Kategorie/Seeds
|
||||
click element=a href=/Kategorie/Seeds
|
||||
sleep 200 "seed click"
|
||||
wait element=button childText="In den Korb"
|
||||
click element=button childText="In den Korb"
|
||||
sleep 200 "in korb click"
|
||||
wait element=span class="MuiBadge-badge" childText="1"
|
||||
click element=button child=span(class="MuiBadge-badge" childText="1")
|
||||
sleep 200 "korb click"
|
||||
wait element=button childText="Weiter zur Kasse"
|
||||
click element=button childText="Weiter zur Kasse"
|
||||
sleep 200 "weiter click"
|
||||
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 2000 "anmelden click"
|
||||
#dump "isServer"
|
||||
jumpIfNot element=span childText="Server-Warenkorb löschen" jump=2
|
||||
click element=button childText="Weiter"
|
||||
sleep 2000 "anmelden click"
|
||||
|
||||
# Part 2 - Fill in the checkout form
|
||||
|
||||
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"
|
||||
fill element=input name="lastName" value="Muster"
|
||||
sleep 100 "nachname fill"
|
||||
wait element=input name="street"
|
||||
fill element=input name="street" value="Muster"
|
||||
sleep 100 "strasse fill"
|
||||
wait element=input name="houseNumber"
|
||||
fill element=input name="houseNumber" value="420"
|
||||
sleep 100 "hausnummer fill"
|
||||
wait element=input name="postalCode"
|
||||
fill element=input name="postalCode" value="42023"
|
||||
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"
|
||||
wait element=input id="DHL" name="deliveryMethod" value="DHL" type="radio"
|
||||
scroll element=input id="DHL" name="deliveryMethod" value="DHL" type="radio"
|
||||
click element=input id="DHL" name="deliveryMethod" value="DHL" type="radio"
|
||||
sleep 100 "dhl click"
|
||||
wait element=input id="onDelivery" name="paymentMethod" value="onDelivery" type="radio"
|
||||
scroll element=input id="onDelivery" name="paymentMethod" value="onDelivery" type="radio"
|
||||
click element=input id="onDelivery" name="paymentMethod" value="onDelivery" type="radio"
|
||||
sleep 100 "on delivery click"
|
||||
scroll element=button childText="Bestellung abschließen"
|
||||
click element=label childText="Bestimmungen"
|
||||
sleep 100 "checkbox checked"
|
||||
wait element=button childText="Bestellung abschließen"
|
||||
click element=button childText="Bestellung abschließen"
|
||||
sleep 3000 "order completion"
|
||||
|
||||
96
step1_dhl_nach2.test
Normal file
96
step1_dhl_nach2.test
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
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
|
||||
|
||||
node src/cli.js step1_dhl_nach2.test Chrome --headed
|
||||
*/
|
||||
use "Chrome"
|
||||
|
||||
# Part 1 - Load Growheads, put one item in the cart and go to checkout
|
||||
open "https://dev.seedheads.de"
|
||||
sleep 200 "page load"
|
||||
wait element=a href=/Kategorie/Seeds
|
||||
click element=a href=/Kategorie/Seeds
|
||||
sleep 200 "seed click"
|
||||
wait element=button childText="In den Korb"
|
||||
click element=button childText="In den Korb"
|
||||
sleep 200 "in korb click"
|
||||
|
||||
|
||||
click element=a href=/Kategorie/Zelte
|
||||
sleep 200 "seed click"
|
||||
wait element=button childText="In den Korb"
|
||||
click element=button childText="In den Korb"
|
||||
sleep 200 "in korb click"
|
||||
|
||||
wait element=button type="button" aria-label="Menge erhöhen"
|
||||
click element=button type="button" aria-label="Menge erhöhen"
|
||||
sleep 200 "menge erhoehen click"
|
||||
|
||||
|
||||
|
||||
wait element=span class="MuiBadge-badge" childText="3"
|
||||
click element=button child=span(class="MuiBadge-badge" childText="3")
|
||||
sleep 200 "korb click"
|
||||
wait element=button childText="Weiter zur Kasse"
|
||||
click element=button childText="Weiter zur Kasse"
|
||||
sleep 200 "weiter click"
|
||||
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 2000 "anmelden click"
|
||||
#dump "isServer"
|
||||
jumpIfNot element=span childText="Server-Warenkorb löschen" jump=2
|
||||
click element=button childText="Weiter"
|
||||
sleep 2000 "anmelden click"
|
||||
|
||||
# Part 2 - Fill in the checkout form
|
||||
|
||||
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"
|
||||
fill element=input name="lastName" value="Muster"
|
||||
sleep 100 "nachname fill"
|
||||
wait element=input name="street"
|
||||
fill element=input name="street" value="Muster"
|
||||
sleep 100 "strasse fill"
|
||||
wait element=input name="houseNumber"
|
||||
fill element=input name="houseNumber" value="420"
|
||||
sleep 100 "hausnummer fill"
|
||||
wait element=input name="postalCode"
|
||||
fill element=input name="postalCode" value="42023"
|
||||
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"
|
||||
wait element=input id="DHL" name="deliveryMethod" value="DHL" type="radio"
|
||||
scroll element=input id="DHL" name="deliveryMethod" value="DHL" type="radio"
|
||||
click element=input id="DHL" name="deliveryMethod" value="DHL" type="radio"
|
||||
sleep 100 "dhl click"
|
||||
wait element=input id="onDelivery" name="paymentMethod" value="onDelivery" type="radio"
|
||||
scroll element=input id="onDelivery" name="paymentMethod" value="onDelivery" type="radio"
|
||||
click element=input id="onDelivery" name="paymentMethod" value="onDelivery" type="radio"
|
||||
sleep 100 "on delivery click"
|
||||
scroll element=button childText="Bestellung abschließen"
|
||||
click element=label childText="Bestimmungen"
|
||||
sleep 100 "checkbox checked"
|
||||
wait element=button childText="Bestellung abschließen"
|
||||
click element=button childText="Bestellung abschließen"
|
||||
sleep 3000 "order completion"
|
||||
|
||||
77
step1_dpd.test
Normal file
77
step1_dpd.test
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
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://dev.seedheads.de"
|
||||
sleep 200 "page load"
|
||||
wait element=a href=/Kategorie/Seeds
|
||||
click element=a href=/Kategorie/Seeds
|
||||
sleep 200 "seed click"
|
||||
wait element=button childText="In den Korb"
|
||||
click element=button childText="In den Korb"
|
||||
sleep 200 "in korb click"
|
||||
wait element=span class="MuiBadge-badge" childText="1"
|
||||
click element=button child=span(class="MuiBadge-badge" childText="1")
|
||||
sleep 200 "korb click"
|
||||
wait element=button childText="Weiter zur Kasse"
|
||||
click element=button childText="Weiter zur Kasse"
|
||||
sleep 200 "weiter click"
|
||||
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 2000 "anmelden click"
|
||||
#dump "isServer"
|
||||
jumpIfNot element=span childText="Server-Warenkorb löschen" jump=2
|
||||
click element=button childText="Weiter"
|
||||
sleep 2000 "anmelden click"
|
||||
|
||||
# Part 2 - Fill in the checkout form
|
||||
|
||||
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"
|
||||
fill element=input name="lastName" value="Muster"
|
||||
sleep 100 "nachname fill"
|
||||
wait element=input name="street"
|
||||
fill element=input name="street" value="Muster"
|
||||
sleep 100 "strasse fill"
|
||||
wait element=input name="houseNumber"
|
||||
fill element=input name="houseNumber" value="420"
|
||||
sleep 100 "hausnummer fill"
|
||||
wait element=input name="postalCode"
|
||||
fill element=input name="postalCode" value="42023"
|
||||
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"
|
||||
wait element=input id="DPD" name="deliveryMethod" value="DPD" type="radio"
|
||||
scroll element=input id="DPD" name="deliveryMethod" value="DPD" type="radio"
|
||||
click element=input id="DPD" name="deliveryMethod" value="DPD" type="radio"
|
||||
sleep 100 "dpd click"
|
||||
wait element=label childText="Bestimmungen"
|
||||
scroll element=button childText="Bestellung abschließen"
|
||||
click element=label childText="Bestimmungen"
|
||||
sleep 100 "checkbox checked"
|
||||
wait element=button childText="Bestellung abschließen"
|
||||
click element=button childText="Bestellung abschließen"
|
||||
sleep 3000 "order completion"
|
||||
|
||||
62
step2.test
Normal file
62
step2.test
Normal 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"
|
||||
Reference in New Issue
Block a user