Refactor test scripts by removing unused files, updating test commands, and adding new functionality for window switching and variable extraction
This commit is contained in:
@@ -59,7 +59,7 @@
|
||||
"patterns": [
|
||||
{
|
||||
"name": "keyword.control.playwrong",
|
||||
"match": "\\b(use|open|wait|click|fill|scroll|sleep|dump|break)\\b"
|
||||
"match": "\\b(use|open|wait|click|fill|scroll|sleep|dump|break|switch to new window|switch to tab|extract)\\b"
|
||||
},
|
||||
{
|
||||
"name": "entity.name.function.playwrong",
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
use "Chrome"
|
||||
|
||||
# Part 1 - Load Google and accept cookies
|
||||
open "https://google.de"
|
||||
wait element=button childText="Alle akzeptieren"
|
||||
click element=button childText="Alle akzeptieren"
|
||||
sleep 1000 "after cookie accept"
|
||||
dump "after_cookie_accept"
|
||||
|
||||
@@ -4,12 +4,8 @@
|
||||
"description": "Custom test language using Playwright",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"test": "node src/cli.js step1.test Chrome --headed --screenshot-none",
|
||||
"install-browsers": "playwright install chromium",
|
||||
"lint": "node src/linter-cli.js step1.test",
|
||||
"lint:strict": "node src/linter-cli.js --strict step1.test",
|
||||
"lint:verbose": "node src/linter-cli.js --verbose step1.test",
|
||||
"lint:all": "node src/linter-cli.js step1.test"
|
||||
"test": "node src/cli.js step1.test Chrome --headed --screenshot --full-page",
|
||||
"install-browsers": "playwright install chromium"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.0",
|
||||
|
||||
@@ -92,12 +92,14 @@ async function main() {
|
||||
console.log(' --headed Run in headed mode (show browser)');
|
||||
console.log(' --headless Run in headless mode (default)');
|
||||
console.log(' --enable-animations Enable CSS animations (default: disabled)');
|
||||
console.log(' --full-page Take full page screenshots instead of viewport only');
|
||||
console.log(' --lint Run linter before execution');
|
||||
console.log(' --strict Treat linter warnings 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');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -106,6 +108,7 @@ async function main() {
|
||||
const profile = args[1] || 'Chrome';
|
||||
const headless = !args.includes('--headed');
|
||||
const disableAnimations = !args.includes('--enable-animations');
|
||||
const fullPageScreenshots = args.includes('--full-page');
|
||||
const lint = args.includes('--lint');
|
||||
const strict = args.includes('--strict');
|
||||
|
||||
@@ -114,7 +117,8 @@ async function main() {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const runner = new TestRunner({ headless, disableAnimations, lint, strict });
|
||||
const runner = new TestRunner({ headless, disableAnimations, fullPageScreenshots, lint, strict });
|
||||
|
||||
await runner.runTestFile(testFile, profile);
|
||||
}
|
||||
|
||||
|
||||
102
src/executor.js
102
src/executor.js
@@ -15,6 +15,8 @@ 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.variables = {}; // Store extracted variables
|
||||
|
||||
this.profiles = {
|
||||
Chrome: {
|
||||
viewport: { width: 1280, height: 720 },
|
||||
@@ -148,7 +150,8 @@ class TestExecutor {
|
||||
break;
|
||||
|
||||
case 'open':
|
||||
await this.page.goto(command.url, { waitUntil: 'networkidle' });
|
||||
const resolvedUrl = this.resolveEnvironmentVariables(command.url);
|
||||
await this.page.goto(resolvedUrl, { waitUntil: 'networkidle' });
|
||||
// Small delay to ensure page is stable
|
||||
if (!this.headless) {
|
||||
await this.page.waitForTimeout(200);
|
||||
@@ -203,6 +206,18 @@ class TestExecutor {
|
||||
await this.sleep(command);
|
||||
break;
|
||||
|
||||
case 'switchToNewWindow':
|
||||
await this.switchToNewWindow();
|
||||
break;
|
||||
|
||||
case 'switchToTab':
|
||||
await this.switchToTab(command);
|
||||
break;
|
||||
|
||||
case 'extract':
|
||||
await this.extractToVariable(command);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`Unknown command type: ${command.type}`);
|
||||
}
|
||||
@@ -648,6 +663,75 @@ class TestExecutor {
|
||||
});
|
||||
}
|
||||
|
||||
async switchToNewWindow() {
|
||||
console.log('🔄 Switching to new tab...');
|
||||
|
||||
// Get all current pages/tabs
|
||||
const pages = this.context.pages();
|
||||
|
||||
if (pages.length < 2) {
|
||||
throw new Error('No additional tabs found to switch to');
|
||||
}
|
||||
|
||||
// Switch to the last (most recently opened) tab
|
||||
const newTab = pages[pages.length - 1];
|
||||
|
||||
// Wait for the tab to be ready
|
||||
await newTab.waitForLoadState('networkidle');
|
||||
|
||||
// Switch to the new tab
|
||||
this.page = newTab;
|
||||
|
||||
console.log('🔄 Switched to new tab');
|
||||
}
|
||||
|
||||
async switchToTab(command) {
|
||||
const tabIndex = command.tabIndex;
|
||||
if (tabIndex === undefined || tabIndex < 0 || tabIndex >= this.context.pages().length) {
|
||||
throw new Error(`Invalid tab index: ${tabIndex}`);
|
||||
}
|
||||
|
||||
const targetPage = this.context.pages()[tabIndex];
|
||||
this.page = targetPage;
|
||||
console.log(`🔄 Switched to tab index ${tabIndex}`);
|
||||
}
|
||||
|
||||
async extractToVariable(command) {
|
||||
const selector = this.buildSelector(command);
|
||||
const attribute = command.attribute;
|
||||
const variableName = command.variableName;
|
||||
|
||||
if (!variableName) {
|
||||
throw new Error('extract command requires a variableName');
|
||||
}
|
||||
|
||||
let value = '';
|
||||
try {
|
||||
const element = await this.page.locator(selector).first();
|
||||
|
||||
if (attribute === 'text') {
|
||||
value = await element.textContent();
|
||||
} else if (attribute === 'innerHTML') {
|
||||
value = await element.innerHTML();
|
||||
} else {
|
||||
// Extract attribute value
|
||||
value = await element.getAttribute(attribute);
|
||||
}
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
throw new Error(`Attribute "${attribute}" not found on element`);
|
||||
}
|
||||
|
||||
console.log(`📋 Extracted ${attribute} from "${selector}" to variable "${variableName}": "${value}"`);
|
||||
} catch (e) {
|
||||
console.warn(`Could not extract ${attribute} from "${selector}" to variable "${variableName}":`, e.message);
|
||||
throw new Error(`Could not extract ${attribute} from "${selector}" to variable "${variableName}"`);
|
||||
}
|
||||
|
||||
this.variables[variableName] = value;
|
||||
console.log(`✅ Variable "${variableName}" set to: "${value}"`);
|
||||
}
|
||||
|
||||
formatCommandForOutput(command) {
|
||||
switch (command.type) {
|
||||
case 'use':
|
||||
@@ -671,6 +755,12 @@ class TestExecutor {
|
||||
return `break "${command.message}"`;
|
||||
case 'sleep':
|
||||
return command.message ? `sleep ${command.milliseconds} "${command.message}"` : `sleep ${command.milliseconds}`;
|
||||
case 'switchToNewWindow':
|
||||
return 'switchToNewWindow';
|
||||
case 'switchToTab':
|
||||
return `switchToTab tabIndex=${command.tabIndex}`;
|
||||
case 'extract':
|
||||
return `extract ${this.formatSelector(command)} variableName="${command.variableName}"`;
|
||||
default:
|
||||
return `${command.type} ${JSON.stringify(command)}`;
|
||||
}
|
||||
@@ -1141,11 +1231,17 @@ class TestExecutor {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Replace $VARIABLE or ${VARIABLE} with environment variable values
|
||||
// Replace $VARIABLE or ${VARIABLE} with environment variable values or stored variables
|
||||
return value.replace(/\$\{?([A-Z_][A-Z0-9_]*)\}?/g, (match, varName) => {
|
||||
// First check stored variables
|
||||
if (this.variables[varName] !== undefined) {
|
||||
return this.variables[varName];
|
||||
}
|
||||
|
||||
// Then check environment variables
|
||||
const envValue = process.env[varName];
|
||||
if (envValue === undefined) {
|
||||
console.warn(`Warning: Environment variable ${varName} is not defined`);
|
||||
console.warn(`Warning: Variable ${varName} is not defined in stored variables or environment`);
|
||||
return match; // Return original if not found
|
||||
}
|
||||
return envValue;
|
||||
|
||||
@@ -6,7 +6,7 @@ class TestLinter {
|
||||
this.info = [];
|
||||
|
||||
// Valid commands
|
||||
this.validCommands = ['use', 'open', 'wait', 'click', 'scroll', 'fill', 'break', 'sleep', 'dump'];
|
||||
this.validCommands = ['use', 'open', 'wait', 'click', 'scroll', 'fill', 'break', 'sleep', 'dump', 'switchToNewWindow', 'switchToTab', 'extract'];
|
||||
|
||||
// Valid HTML elements
|
||||
this.validElements = [
|
||||
@@ -29,7 +29,10 @@ class TestLinter {
|
||||
'fill': ['element', 'value'],
|
||||
'break': [],
|
||||
'sleep': ['milliseconds'],
|
||||
'dump': ['name']
|
||||
'dump': ['name'],
|
||||
'switchToNewWindow': [],
|
||||
'switchToTab': ['tabIndex'],
|
||||
'extract': ['element', 'attribute']
|
||||
};
|
||||
|
||||
// Initialize rules
|
||||
@@ -252,6 +255,19 @@ class TestLinter {
|
||||
for (const line of lines) {
|
||||
const command = line.cleaned.split(' ')[0];
|
||||
|
||||
// Handle multi-word commands
|
||||
if (line.cleaned.startsWith('switch to new window')) {
|
||||
continue; // This is valid
|
||||
}
|
||||
|
||||
if (line.cleaned.startsWith('extract ')) {
|
||||
continue; // This is valid
|
||||
}
|
||||
|
||||
if (line.cleaned.startsWith('switch to tab')) {
|
||||
continue; // This is valid
|
||||
}
|
||||
|
||||
if (!this.validCommands.includes(command)) {
|
||||
this.addError(`Invalid command '${command}'. Valid commands: ${this.validCommands.join(', ')}`, line.lineNumber);
|
||||
}
|
||||
@@ -312,6 +328,10 @@ class TestLinter {
|
||||
if (!line.cleaned.match(/\d+/)) {
|
||||
this.addError(`Command '${command}' requires numeric milliseconds`, line.lineNumber);
|
||||
}
|
||||
} else if (reqParam === 'tabIndex' && command === 'switchToTab') {
|
||||
if (!line.cleaned.match(/\d+/)) {
|
||||
this.addError(`Command '${command}' requires numeric tabIndex`, line.lineNumber);
|
||||
}
|
||||
} else if (!params[reqParam]) {
|
||||
this.addError(`Command '${command}' missing required parameter '${reqParam}'`, line.lineNumber);
|
||||
}
|
||||
|
||||
@@ -186,6 +186,50 @@ class TestParser {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse switch to new window command
|
||||
if (line.startsWith('switch to new window')) {
|
||||
return {
|
||||
type: 'switchToNewWindow'
|
||||
};
|
||||
}
|
||||
|
||||
// Parse switch to tab command: switch to tab 0
|
||||
if (line.startsWith('switch to tab ')) {
|
||||
const match = line.match(/switch to tab (\d+)/);
|
||||
if (match) {
|
||||
return {
|
||||
type: 'switchToTab',
|
||||
tabIndex: parseInt(match[1])
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Parse extract command: extract href from element=a target="_blank" to variable="ORDER_URL"
|
||||
if (line.startsWith('extract ')) {
|
||||
const match = line.match(/extract\s+(\w+)\s+from\s+(.+?)\s+to\s+variable="([^"]+)"/);
|
||||
if (match) {
|
||||
const attribute = match[1];
|
||||
const selectorPart = match[2];
|
||||
const variableName = match[3];
|
||||
const params = this.parseParameters(selectorPart);
|
||||
|
||||
return {
|
||||
type: 'extract',
|
||||
attribute: attribute,
|
||||
variableName: variableName,
|
||||
element: params.element,
|
||||
name: params.name,
|
||||
id: params.id,
|
||||
class: params.class,
|
||||
href: params.href,
|
||||
htmlType: params.type,
|
||||
child: params.child,
|
||||
childText: params.childText,
|
||||
target: params.target
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
58
step1.test
58
step1.test
@@ -9,7 +9,7 @@ Part 3: Login to the email account
|
||||
use "Chrome"
|
||||
|
||||
# Part 1 - Load Growheads, put one item in the cart and go to checkout
|
||||
open "https://growheads.de"
|
||||
/*open "https://growheads.de"
|
||||
sleep 2000 "page load"
|
||||
wait element=a href=/Kategorie/Seeds
|
||||
click element=a href=/Kategorie/Seeds
|
||||
@@ -61,19 +61,61 @@ wait element=label childText="Bestimmungen"
|
||||
click element=label childText="Bestimmungen"
|
||||
sleep 1000 "checkbox checked"
|
||||
wait element=button childText="Bestellung abschließen"
|
||||
#click element=button childText="Bestellung abschließen"
|
||||
click element=button childText="Bestellung abschließen"
|
||||
sleep 3000 "order completion"
|
||||
|
||||
*/
|
||||
# Part 3 - Login to the email account
|
||||
open "https://mail.growbnb.de/"
|
||||
sleep 2000 "page load"
|
||||
sleep 100 "page load"
|
||||
wait element=input name="_user" id="rcmloginuser"
|
||||
fill element=input name="_user" id="rcmloginuser" value="autotest@growheads.de"
|
||||
sleep 1000 "username fill"
|
||||
sleep 100 "username fill"
|
||||
wait element=input name="_pass" id="rcmloginpwd"
|
||||
fill element=input name="_pass" id="rcmloginpwd" value="$PASSWORDMAIL"
|
||||
sleep 1000 "password fill"
|
||||
sleep 100 "password fill"
|
||||
wait element=button type="submit" id="rcmloginsubmit"
|
||||
click element=button type="submit" id="rcmloginsubmit"
|
||||
sleep 3000 "login submit"
|
||||
dump "email_logged_in"
|
||||
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"
|
||||
sleep 3000 "new window open"
|
||||
|
||||
# Switch to the new window that was opened
|
||||
switch to new window
|
||||
|
||||
# Wait a bit more for the new window to fully load
|
||||
sleep 2000 "new window load"
|
||||
|
||||
# Verify that "Musteranmerkung" exists in the content
|
||||
wait element=p childText="Musteranmerkung"
|
||||
|
||||
# Extract the order URL from the link
|
||||
extract href from element=a childText="https://growheads.de/profile#W-" to variable="ORDER_URL"
|
||||
|
||||
sleep 300 "bestellbestätigung click"
|
||||
dump "after_bestellbestaetigung_click"
|
||||
sleep 3000 "bestellbestätigung click"
|
||||
# Delete the email by clicking "Löschen"
|
||||
wait element=a id="rcmbtn105"
|
||||
click element=a id="rcmbtn105"
|
||||
sleep 1000 "email deleted"
|
||||
|
||||
# Now open the extracted URL
|
||||
open "$ORDER_URL"
|
||||
sleep 2000 "order page load"
|
||||
dump "order_page_from_extracted_url"
|
||||
|
||||
dump "after_new_window_click"
|
||||
|
||||
sleep 30000 "login submit"
|
||||
65
step2.test
65
step2.test
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
PlayWrong Test - Complete E-commerce Flow
|
||||
This test demonstrates the full purchase flow from product selection to order completion
|
||||
with beautiful animations and environment variable support
|
||||
Part 1: Load Growheads, put one item in the cart and go to checkout
|
||||
Part 2: Fill in the checkout form
|
||||
Part 3: Login to the email account
|
||||
*/
|
||||
use "Chrome"
|
||||
|
||||
# Part 1 - Load Growheads, put one item in the cart and go to checkout
|
||||
open "https://growheads.de"
|
||||
sleep 2000 "page load"
|
||||
wait element=a href=/Kategorie/Seeds
|
||||
click element=a href=/Kategorie/Seeds
|
||||
sleep 2000 "seed click"
|
||||
wait element=button childText="In den Korb"
|
||||
click element=button childText="In den Korb"
|
||||
sleep 2000 "in korb click"
|
||||
wait element=span class="MuiBadge-badge" childText="1"
|
||||
click element=button child=span(class="MuiBadge-badge" childText="1")
|
||||
sleep 2000 "korb click"
|
||||
wait element=button childText="Weiter zur Kasse"
|
||||
click element=button childText="Weiter zur Kasse"
|
||||
sleep 2000 "weiter click"
|
||||
wait element=input type="email"
|
||||
fill element=input type="email" value="autotest@growheads.de"
|
||||
sleep 2000 "email fill"
|
||||
wait element=input type="password"
|
||||
fill element=input type="password" value="$PASSWORD"
|
||||
sleep 2000 "password fill"
|
||||
wait element=button childText="ANMELDEN" class="MuiButton-fullWidth"
|
||||
click element=button childText="ANMELDEN" class="MuiButton-fullWidth"
|
||||
sleep 3000 "anmelden click"
|
||||
|
||||
# Part 2 - Fill in the checkout form
|
||||
scroll element=span childText="Vorname"
|
||||
wait element=input name="firstName"
|
||||
fill element=input name="firstName" value="Max"
|
||||
sleep 1000 "vorname fill"
|
||||
wait element=input name="lastName"
|
||||
fill element=input name="lastName" value="Muster"
|
||||
sleep 1000 "nachname fill"
|
||||
wait element=input name="street"
|
||||
fill element=input name="street" value="Muster"
|
||||
sleep 1000 "strasse fill"
|
||||
wait element=input name="houseNumber"
|
||||
fill element=input name="houseNumber" value="420"
|
||||
sleep 1000 "hausnummer fill"
|
||||
wait element=input name="postalCode"
|
||||
fill element=input name="postalCode" value="42023"
|
||||
sleep 1000 "plz fill"
|
||||
wait element=input name="city"
|
||||
fill element=input name="city" value="Muster"
|
||||
sleep 1000 "stadt fill"
|
||||
wait element=textarea name="note"
|
||||
fill element=textarea name="note" value="Musteranmerkung"
|
||||
sleep 1000 "note fill"
|
||||
scroll element=button childText="Bestellung abschließen"
|
||||
wait element=label childText="Bestimmungen"
|
||||
click element=label childText="Bestimmungen"
|
||||
sleep 1000 "checkbox checked"
|
||||
wait element=button childText="Bestellung abschließen"
|
||||
sleep 1000 "order completion"
|
||||
dump "order_completed"
|
||||
Reference in New Issue
Block a user