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:
128
src/executor.js
128
src/executor.js
@@ -43,13 +43,21 @@ class TestExecutor {
|
||||
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
|
||||
if (!this.headless) {
|
||||
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
|
||||
if (!this.headless) {
|
||||
@@ -92,11 +100,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();
|
||||
@@ -239,9 +257,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) {
|
||||
@@ -922,15 +948,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;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
const newTab = pages[pages.length - 1];
|
||||
// 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 +1068,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 +1125,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)}`;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
if (line.startsWith('switch to tab ')) {
|
||||
const match = line.match(/switch to tab (\d+)/);
|
||||
|
||||
Reference in New Issue
Block a user