From fe4ce936c622512bf21c38e25b9dede6cb7bd432 Mon Sep 17 00:00:00 2001 From: seb Date: Thu, 17 Jul 2025 08:16:17 +0200 Subject: [PATCH] Refactor test scripts by removing unused files, updating test commands, and adding new functionality for window switching and variable extraction --- .../syntaxes/playwrong.tmLanguage.json | 2 +- google.test | 9 -- package.json | 8 +- src/cli.js | 6 +- src/executor.js | 102 +++++++++++++++++- src/linter.js | 24 ++++- src/parser.js | 44 ++++++++ step1.test | 58 ++++++++-- step2.test | 65 ----------- 9 files changed, 223 insertions(+), 95 deletions(-) delete mode 100644 google.test delete mode 100644 step2.test diff --git a/.vscode/extensions/playwrong-syntax/syntaxes/playwrong.tmLanguage.json b/.vscode/extensions/playwrong-syntax/syntaxes/playwrong.tmLanguage.json index 89355d4..6e935ea 100644 --- a/.vscode/extensions/playwrong-syntax/syntaxes/playwrong.tmLanguage.json +++ b/.vscode/extensions/playwrong-syntax/syntaxes/playwrong.tmLanguage.json @@ -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", diff --git a/google.test b/google.test deleted file mode 100644 index c44cd41..0000000 --- a/google.test +++ /dev/null @@ -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" - diff --git a/package.json b/package.json index 1744935..9945ee4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/cli.js b/src/cli.js index 6e608f2..ae0517e 100644 --- a/src/cli.js +++ b/src/cli.js @@ -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); } diff --git a/src/executor.js b/src/executor.js index 71cae12..d309272 100644 --- a/src/executor.js +++ b/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; diff --git a/src/linter.js b/src/linter.js index 51e9240..80fbbf9 100644 --- a/src/linter.js +++ b/src/linter.js @@ -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); } diff --git a/src/parser.js b/src/parser.js index acd984a..d15e13f 100644 --- a/src/parser.js +++ b/src/parser.js @@ -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; } diff --git a/step1.test b/step1.test index c946fad..e4caca2 100644 --- a/step1.test +++ b/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" \ No newline at end of file +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" \ No newline at end of file diff --git a/step2.test b/step2.test deleted file mode 100644 index d4ae75a..0000000 --- a/step2.test +++ /dev/null @@ -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" \ No newline at end of file