Add 'jumpIf' and 'jumpIfNot' commands to README and executor; implement parsing, execution, and linter support for conditional command jumps in test scripts.
This commit is contained in:
@@ -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` - Pause execution (press any key to continue)
|
||||||
- `break "waiting for user input"` - Pause with message
|
- `break "waiting for user input"` - Pause with message
|
||||||
- `dump "step_name"` - Take screenshot
|
- `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 Selectors
|
||||||
- `element=tagname` - HTML tag (div, span, button, input, a, form, etc.)
|
- `element=tagname` - HTML tag (div, span, button, input, a, form, etc.)
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -103,6 +103,20 @@ sleep 2500 "waiting for animation"
|
|||||||
sleep 500 "let page settle"
|
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
|
### Element Selectors
|
||||||
|
|
||||||
You can combine multiple attributes:
|
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 "$@"
|
||||||
128
src/executor.js
128
src/executor.js
@@ -43,13 +43,21 @@ class TestExecutor {
|
|||||||
await this.updateStatus({ type: 'initializing' }, false);
|
await this.updateStatus({ type: 'initializing' }, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const command of commands) {
|
for (let i = 0; i < commands.length; i++) {
|
||||||
|
const command = commands[i];
|
||||||
|
|
||||||
// Update status before executing command
|
// Update status before executing command
|
||||||
if (!this.headless) {
|
if (!this.headless) {
|
||||||
await this.updateStatus(command, false);
|
await this.updateStatus(command, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.executeCommand(command);
|
const jumpCount = await this.executeCommand(command);
|
||||||
|
|
||||||
|
// Handle jump commands
|
||||||
|
if (jumpCount > 0) {
|
||||||
|
console.log(`🔄 Jumping ${jumpCount} commands`);
|
||||||
|
i += jumpCount; // Skip the specified number of commands
|
||||||
|
}
|
||||||
|
|
||||||
// Update status after completing command
|
// Update status after completing command
|
||||||
if (!this.headless) {
|
if (!this.headless) {
|
||||||
@@ -92,11 +100,21 @@ class TestExecutor {
|
|||||||
reducedMotion: 'reduce',
|
reducedMotion: 'reduce',
|
||||||
// Force consistent viewport
|
// Force consistent viewport
|
||||||
screen: profile.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();
|
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
|
// Create status window for headed mode AFTER main page so it appears on top
|
||||||
if (!this.headless) {
|
if (!this.headless) {
|
||||||
await this.createStatusWindow();
|
await this.createStatusWindow();
|
||||||
@@ -239,9 +257,17 @@ class TestExecutor {
|
|||||||
await this.extractToVariable(command);
|
await this.extractToVariable(command);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'jumpIf':
|
||||||
|
return await this.jumpIf(command);
|
||||||
|
|
||||||
|
case 'jumpIfNot':
|
||||||
|
return await this.jumpIfNot(command);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn(`Unknown command type: ${command.type}`);
|
console.warn(`Unknown command type: ${command.type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return 0; // No jump by default
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForElement(command) {
|
async waitForElement(command) {
|
||||||
@@ -922,15 +948,69 @@ class TestExecutor {
|
|||||||
async followToNewWindow() {
|
async followToNewWindow() {
|
||||||
console.log('🔄 Switching to new tab...');
|
console.log('🔄 Switching to new tab...');
|
||||||
|
|
||||||
// Get all current pages/tabs
|
|
||||||
const pages = this.context.pages();
|
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) {
|
// Check if we already have multiple pages (popup already created)
|
||||||
throw new Error('No additional tabs found to switch to');
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Switch to the last (most recently opened) tab
|
// Fallback: Wait for a new tab to be created
|
||||||
const newTab = pages[pages.length - 1];
|
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
|
// Wait for the tab to be ready
|
||||||
await newTab.waitForLoadState('networkidle');
|
await newTab.waitForLoadState('networkidle');
|
||||||
@@ -988,6 +1068,34 @@ class TestExecutor {
|
|||||||
console.log(`✅ Variable "${variableName}" set to: "${value}"`);
|
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) {
|
formatCommandForOutput(command) {
|
||||||
switch (command.type) {
|
switch (command.type) {
|
||||||
case 'use':
|
case 'use':
|
||||||
@@ -1017,6 +1125,10 @@ class TestExecutor {
|
|||||||
return `switchToTab tabIndex=${command.tabIndex}`;
|
return `switchToTab tabIndex=${command.tabIndex}`;
|
||||||
case 'extract':
|
case 'extract':
|
||||||
return `extract ${this.formatSelector(command)} variableName="${command.variableName}"`;
|
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:
|
default:
|
||||||
return `${command.type} ${JSON.stringify(command)}`;
|
return `${command.type} ${JSON.stringify(command)}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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', 'follow', 'switchToTab', 'extract'];
|
this.validCommands = ['use', 'open', 'wait', 'click', 'scroll', 'fill', 'break', 'sleep', 'dump', 'follow', 'switchToTab', 'extract', 'jumpIf', 'jumpIfNot'];
|
||||||
|
|
||||||
// Valid HTML elements
|
// Valid HTML elements
|
||||||
this.validElements = [
|
this.validElements = [
|
||||||
@@ -32,7 +32,9 @@ class TestLinter {
|
|||||||
'dump': ['name'],
|
'dump': ['name'],
|
||||||
'follow': [],
|
'follow': [],
|
||||||
'switchToTab': ['tabIndex'],
|
'switchToTab': ['tabIndex'],
|
||||||
'extract': ['element', 'attribute']
|
'extract': ['element', 'attribute'],
|
||||||
|
'jumpIf': ['element', 'jump'],
|
||||||
|
'jumpIfNot': ['element', 'jump']
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize rules
|
// Initialize rules
|
||||||
@@ -268,6 +270,14 @@ class TestLinter {
|
|||||||
continue; // This is valid
|
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)) {
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,6 +193,52 @@ 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,
|
||||||
|
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,
|
||||||
|
child: params.child,
|
||||||
|
childText: params.childText
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Parse switch to tab command: switch to tab 0
|
// Parse switch to tab command: switch to tab 0
|
||||||
if (line.startsWith('switch to tab ')) {
|
if (line.startsWith('switch to tab ')) {
|
||||||
const match = line.match(/switch to tab (\d+)/);
|
const match = line.match(/switch to tab (\d+)/);
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ fill element=input type="password" value="$PASSWORD"
|
|||||||
sleep 200 "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 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
|
# Part 2 - Fill in the checkout form
|
||||||
scroll element=span childText="Vorname"
|
scroll element=span childText="Vorname"
|
||||||
|
|||||||
Reference in New Issue
Block a user