Web Automation Patterns
This comprehensive guide covers modern web application automation using Ranorex, including handling of SPAs, dynamic content, modern frameworks, and browser-specific scenarios.
🌐 Web Technology Coverage
| Technology | Description | Key Challenges |
|---|---|---|
| Modern SPAs | React, Angular, Vue.js applications | Dynamic DOM, AJAX loading |
| Dynamic Content | Async loading, lazy rendering | Timing, synchronization |
| Cross-Browser | Chrome, Firefox, Edge, Safari | Browser differences, compatibility |
| JavaScript Integration | Custom JS execution, DOM manipulation | Complex interactions |
SPA Patterns
React Application Automation
using Ranorex;
using Ranorex.Core;
using System;
using System.Threading.Tasks;
public class ReactAutomation
{
private WebDocument _webDocument;
public async Task AutomateReactApp(string applicationUrl)
{
Host.Initialize();
try
{
// Launch browser and navigate
await LaunchAndNavigate(applicationUrl);
// Wait for React app to load
await WaitForReactAppReady();
// Perform React-specific operations
await HandleReactComponents();
Report.Success("React Automation", "Successfully automated React application");
}
catch (Exception ex)
{
Report.Failure("React Automation", $"Failed: {ex.Message}");
ScreenshotHelper.CaptureOnFailure("ReactApp", ex);
throw;
}
finally
{
Host.Shutdown();
}
}
private async Task LaunchAndNavigate(string url)
{
// Launch browser
WebDocument.CreateApplication("chrome.exe");
// Wait for browser and navigate
_webDocument = Host.Local.FindSingle<WebDocument>("/dom[@domain]", 10000);
_webDocument.NavigateTo(url);
// Wait for basic page load
await WaitForPageLoad();
}
private async Task WaitForReactAppReady()
{
// Wait for React to be loaded
var reactReady = await WaitHelper.WaitForCondition(() =>
{
try
{
var result = _webDocument.ExecuteScript("return window.React !== undefined");
return result != null && (bool)result;
}
catch
{
return false;
}
}, "React framework to load", TimeSpan.FromSeconds(30));
if (!reactReady)
{
throw new TimeoutException("React framework did not load in time");
}
// Wait for main React component to mount
await WaitForElement("[data-testid='app-root']", TimeSpan.FromSeconds(20));
// Wait for any loading spinners to disappear
await WaitForLoadingComplete();
}
private async Task HandleReactComponents()
{
// Handle React Button component
await ClickReactButton("[data-testid='submit-button']");
// Handle React Input component
await FillReactInput("[data-testid='username-input']", "testuser");
// Handle React Modal
await HandleReactModal("[data-testid='confirmation-modal']");
// Handle React List rendering
await ValidateReactList("[data-testid='user-list']");
}
private async Task ClickReactButton(string selector)
{
// Wait for button to be enabled (React might disable during loading)
var button = await WaitForElement(selector, TimeSpan.FromSeconds(10));
// Ensure button is enabled
await WaitHelper.WaitForCondition(() =>
{
var isEnabled = _webDocument.ExecuteScript($"return !document.querySelector('{selector}').disabled");
return isEnabled != null && (bool)isEnabled;
}, "button to be enabled");
button.Click();
Report.Info("React Button", $"Clicked React button: {selector}");
}
private async Task FillReactInput(string selector, string value)
{
var input = await WaitForElement(selector, TimeSpan.FromSeconds(10));
// React inputs often need focus and change events
_webDocument.ExecuteScript($@"
const input = document.querySelector('{selector}');
input.focus();
input.value = '{value}';
input.dispatchEvent(new Event('input', {{ bubbles: true }}));
input.dispatchEvent(new Event('change', {{ bubbles: true }}));
");
Report.Info("React Input", $"Filled React input {selector} with: {value}");
}
private async Task HandleReactModal(string modalSelector)
{
// Wait for modal to appear
var modal = await WaitForElement(modalSelector, TimeSpan.FromSeconds(5));
if (modal.Exists)
{
// Find and click confirm button within modal
var confirmButton = modal.FindSingle<Button>(".//button[contains(@class, 'confirm')]");
confirmButton.Click();
// Wait for modal to disappear
await WaitHelper.WaitForCondition(() => !modal.Exists, "modal to close");
Report.Success("React Modal", "Successfully handled React modal");
}
}
private async Task ValidateReactList(string listSelector)
{
var list = await WaitForElement(listSelector, TimeSpan.FromSeconds(10));
// Wait for list items to load (React might render empty first)
await WaitHelper.WaitForCondition(() =>
{
var items = list.Find<ListItem>(".//*[@role='listitem']");
return items.Count > 0;
}, "React list items to load");
var listItems = list.Find<ListItem>(".//*[@role='listitem']");
Report.Success("React List", $"Found {listItems.Count} items in React list");
}
private async Task<WebElement> WaitForElement(string selector, TimeSpan timeout)
{
var endTime = DateTime.Now.Add(timeout);
while (DateTime.Now < endTime)
{
try
{
var element = _webDocument.FindSingle<WebElement>($".//*[@data-testid or contains(@class, '') or @id][{CssSelectorToXPath(selector)}]");
if (element.Exists && element.Visible)
{
return element;
}
}
catch (RanorexException)
{
// Element not found yet
}
await Task.Delay(250);
}
throw new ElementNotFoundException($"Element not found: {selector}");
}
private string CssSelectorToXPath(string cssSelector)
{
// Simple CSS to XPath conversion for common patterns
if (cssSelector.StartsWith("[data-testid='"))
{
var testId = cssSelector.Substring(14, cssSelector.Length - 16);
return $"@data-testid='{testId}'";
}
if (cssSelector.StartsWith("#"))
{
return $"@id='{cssSelector.Substring(1)}'";
}
if (cssSelector.StartsWith("."))
{
return $"contains(@class, '{cssSelector.Substring(1)}')";
}
return $"name()='{cssSelector}'";
}
}
Dynamic Content
Handling AJAX and Dynamic Loading
using Ranorex;
using System;
using System.Threading.Tasks;
public class DynamicContentAutomation
{
private WebDocument _webDocument;
/// <summary>
/// Handles AJAX requests and dynamic content loading
/// </summary>
public async Task HandleAjaxContent(string url)
{
try
{
_webDocument = Host.Local.FindSingle<WebDocument>("/dom");
_webDocument.NavigateTo(url);
// Wait for initial page load
await WaitForPageLoad();
// Trigger AJAX request
var loadButton = _webDocument.FindSingle<Button>("//button[@id='load-data']");
loadButton.Click();
// Wait for AJAX to complete using multiple strategies
await WaitForAjaxComplete();
// Validate dynamically loaded content
await ValidateDynamicContent();
Report.Success("AJAX Content", "Successfully handled dynamic content loading");
}
catch (Exception ex)
{
Report.Failure("AJAX Content", $"Failed: {ex.Message}");
throw;
}
}
private async Task WaitForAjaxComplete()
{
// Strategy 1: Wait for jQuery.active to be 0
var jqueryReady = await WaitHelper.WaitForCondition(() =>
{
try
{
var active = _webDocument.ExecuteScript("return jQuery.active || 0");
return active != null && (long)active == 0;
}
catch
{
return true; // jQuery not available, assume ready
}
}, "jQuery AJAX to complete", TimeSpan.FromSeconds(10));
// Strategy 2: Wait for custom loading indicator to disappear
await WaitHelper.WaitForElementToDisappear("//*[@class='loading-spinner']",
TimeSpan.FromSeconds(15), "loading spinner");
// Strategy 3: Wait for specific content to appear
await WaitHelper.WaitForElement("//*[@id='dynamic-content']//div[@class='loaded-item']",
timeout: TimeSpan.FromSeconds(20), description: "dynamic content items");
// Strategy 4: Wait for network idle using Performance API
await WaitForNetworkIdle();
}
private async Task WaitForNetworkIdle()
{
var networkIdle = await WaitHelper.WaitForCondition(() =>
{
try
{
// Check if there are active fetch requests
var script = @"
return window.fetch.activeRequests ?
window.fetch.activeRequests.length === 0 :
true;
";
var result = _webDocument.ExecuteScript(script);
return result != null && (bool)result;
}
catch
{
return true; // Assume idle if we can't check
}
}, "network to be idle", TimeSpan.FromSeconds(15));
if (networkIdle)
{
Report.Info("Network", "Network requests completed");
}
}
private async Task ValidateDynamicContent()
{
// Get all dynamically loaded items
var dynamicItems = _webDocument.Find<DivTag>("//*[@id='dynamic-content']//div[@class='loaded-item']");
if (dynamicItems.Count == 0)
{
throw new Exception("No dynamic content was loaded");
}
// Validate each item
foreach (var item in dynamicItems)
{
var itemText = item.InnerText?.Trim();
if (string.IsNullOrEmpty(itemText))
{
throw new Exception("Dynamic item has no content");
}
Report.Info("Dynamic Content", $"Validated item: {itemText}");
}
Report.Success("Dynamic Content", $"Validated {dynamicItems.Count} dynamic items");
}
/// <summary>
/// Handles infinite scroll and lazy loading
/// </summary>
public async Task HandleInfiniteScroll(string containerSelector)
{
try
{
var container = _webDocument.FindSingle<DivTag>($"//*[{CssSelectorToXPath(containerSelector)}]");
int previousItemCount = 0;
int currentItemCount = 0;
int unchangedCount = 0;
do
{
previousItemCount = currentItemCount;
// Scroll to bottom of container
_webDocument.ExecuteScript($@"
const container = document.querySelector('{containerSelector}');
container.scrollTop = container.scrollHeight;
");
// Wait for new content to load
await Task.Delay(2000);
// Count current items
var items = container.Find<DivTag>(".//*[@class='scroll-item']");
currentItemCount = items.Count;
if (currentItemCount == previousItemCount)
{
unchangedCount++;
}
else
{
unchangedCount = 0;
}
Report.Info("Infinite Scroll", $"Loaded {currentItemCount} items");
} while (unchangedCount < 3 && currentItemCount < 100); // Stop after 3 unchanged iterations or 100 items
Report.Success("Infinite Scroll", $"Completed infinite scroll. Total items: {currentItemCount}");
}
catch (Exception ex)
{
Report.Failure("Infinite Scroll", $"Failed: {ex.Message}");
throw;
}
}
}
Cross-Browser Automation
Multi-Browser Testing Framework
using Ranorex;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public class CrossBrowserAutomation
{
private readonly Dictionary<string, string> _browserPaths = new Dictionary<string, string>
{
["chrome"] = "chrome.exe",
["firefox"] = "firefox.exe",
["edge"] = "msedge.exe",
["safari"] = "safari.exe"
};
/// <summary>
/// Executes test across multiple browsers
/// </summary>
public async Task RunCrossBrowserTest(string testUrl, List<string> browsers)
{
var results = new Dictionary<string, bool>();
foreach (var browser in browsers)
{
try
{
Report.Info("Cross-Browser", $"Starting test in {browser}");
var success = await RunTestInBrowser(browser, testUrl);
results[browser] = success;
// Close browser before next test
await CloseBrowser();
Report.Info("Cross-Browser", $"Completed test in {browser}: {(success ? "PASS" : "FAIL")}");
}
catch (Exception ex)
{
results[browser] = false;
Report.Failure("Cross-Browser", $"Test failed in {browser}: {ex.Message}");
}
}
// Report summary
ReportCrossBrowserResults(results);
}
private async Task<bool> RunTestInBrowser(string browserName, string testUrl)
{
try
{
// Launch specific browser
var browserPath = _browserPaths[browserName.ToLower()];
WebDocument.CreateApplication(browserPath);
var webDoc = Host.Local.FindSingle<WebDocument>("/dom", 15000);
webDoc.NavigateTo(testUrl);
// Wait for page load with browser-specific handling
await WaitForPageLoadByBrowser(webDoc, browserName);
// Perform browser-agnostic test operations
await PerformStandardTest(webDoc, browserName);
return true;
}
catch (Exception ex)
{
Report.Error($"{browserName} Test", $"Browser test failed: {ex.Message}");
return false;
}
}
private async Task WaitForPageLoadByBrowser(WebDocument webDoc, string browserName)
{
// Browser-specific waiting strategies
switch (browserName.ToLower())
{
case "chrome":
await WaitForChromeReady(webDoc);
break;
case "firefox":
await WaitForFirefoxReady(webDoc);
break;
case "edge":
await WaitForEdgeReady(webDoc);
break;
case "safari":
await WaitForSafariReady(webDoc);
break;
default:
await WaitForStandardReady(webDoc);
break;
}
}
private async Task WaitForChromeReady(WebDocument webDoc)
{
// Chrome-specific ready state
await WaitHelper.WaitForCondition(() =>
{
var readyState = webDoc.ExecuteScript("return document.readyState");
return readyState?.ToString() == "complete";
}, "Chrome page to load");
// Wait for Chrome's paint events
await Task.Delay(1000);
}
private async Task WaitForFirefoxReady(WebDocument webDoc)
{
// Firefox may need longer for complex JavaScript
await WaitHelper.WaitForCondition(() =>
{
var readyState = webDoc.ExecuteScript("return document.readyState");
return readyState?.ToString() == "complete";
}, "Firefox page to load", TimeSpan.FromSeconds(20));
// Additional wait for Firefox rendering
await Task.Delay(1500);
}
private async Task WaitForEdgeReady(WebDocument webDoc)
{
// Edge (Chromium) similar to Chrome but with some differences
await WaitForChromeReady(webDoc);
// Edge-specific stability wait
await Task.Delay(800);
}
private async Task WaitForSafariReady(WebDocument webDoc)
{
// Safari needs more time for script execution
await WaitHelper.WaitForCondition(() =>
{
var readyState = webDoc.ExecuteScript("return document.readyState");
return readyState?.ToString() == "complete";
}, "Safari page to load", TimeSpan.FromSeconds(25));
await Task.Delay(2000);
}
private async Task PerformStandardTest(WebDocument webDoc, string browserName)
{
// Perform the same test actions across all browsers
// Test 1: Basic form interaction
await TestFormInteraction(webDoc, browserName);
// Test 2: JavaScript execution
await TestJavaScriptExecution(webDoc, browserName);
// Test 3: CSS selector compatibility
await TestCssSelectorCompatibility(webDoc, browserName);
// Test 4: Event handling
await TestEventHandling(webDoc, browserName);
}
private async Task TestFormInteraction(WebDocument webDoc, string browserName)
{
try
{
var nameField = webDoc.FindSingle<InputTag>("//input[@name='username']");
nameField.Click();
nameField.PressKeys("testuser");
var submitButton = webDoc.FindSingle<Button>("//button[@type='submit']");
submitButton.Click();
Report.Success($"{browserName} Form", "Form interaction successful");
}
catch (Exception ex)
{
Report.Failure($"{browserName} Form", $"Form interaction failed: {ex.Message}");
throw;
}
}
private async Task TestJavaScriptExecution(WebDocument webDoc, string browserName)
{
try
{
var result = webDoc.ExecuteScript("return navigator.userAgent.includes('" + browserName + "')");
if (result != null && (bool)result)
{
Report.Success($"{browserName} JS", "JavaScript execution verified");
}
else
{
Report.Info($"{browserName} JS", "Browser name not found in user agent (expected for some browsers)");
}
}
catch (Exception ex)
{
Report.Failure($"{browserName} JS", $"JavaScript execution failed: {ex.Message}");
throw;
}
}
private void ReportCrossBrowserResults(Dictionary<string, bool> results)
{
var passed = 0;
var total = results.Count;
foreach (var result in results)
{
if (result.Value) passed++;
Report.Info("Cross-Browser Summary", $"{result.Key}: {(result.Value ? "PASS" : "FAIL")}");
}
var passRate = (passed * 100.0) / total;
Report.Info("Cross-Browser Summary", $"Overall: {passed}/{total} browsers passed ({passRate:F1}%)");
if (passRate >= 80)
{
Report.Success("Cross-Browser Test", $"Cross-browser test completed successfully: {passRate:F1}% pass rate");
}
else
{
Report.Failure("Cross-Browser Test", $"Cross-browser test failed: {passRate:F1}% pass rate (below 80% threshold)");
}
}
}
JavaScript Integration
Advanced JavaScript Automation
using Ranorex;
using System;
using System.Collections.Generic;
public class JavaScriptIntegration
{
private WebDocument _webDocument;
/// <summary>
/// Executes complex JavaScript operations
/// </summary>
public void ExecuteAdvancedJavaScript(WebDocument webDoc)
{
_webDocument = webDoc;
// DOM manipulation
ManipulateDom();
// Event simulation
SimulateComplexEvents();
// Data extraction
ExtractDataWithJavaScript();
// Performance monitoring
MonitorPerformance();
}
private void ManipulateDom()
{
// Create new elements dynamically
var createElementScript = @"
const newDiv = document.createElement('div');
newDiv.id = 'ranorex-created';
newDiv.textContent = 'Created by Ranorex';
newDiv.style.cssText = 'background: yellow; padding: 10px; margin: 10px;';
document.body.appendChild(newDiv);
return newDiv.id;
";
var elementId = _webDocument.ExecuteScript(createElementScript);
Report.Info("DOM Manipulation", $"Created element with ID: {elementId}");
// Modify existing elements
var modifyScript = @"
const elements = document.querySelectorAll('button');
let modified = 0;
elements.forEach(btn => {
if (!btn.hasAttribute('data-ranorex-processed')) {
btn.setAttribute('data-ranorex-processed', 'true');
btn.style.border = '2px solid red';
modified++;
}
});
return modified;
";
var modifiedCount = _webDocument.ExecuteScript(modifyScript);
Report.Info("DOM Manipulation", $"Modified {modifiedCount} buttons");
}
private void SimulateComplexEvents()
{
// Simulate drag and drop
var dragDropScript = @"
function simulateDragDrop(sourceSelector, targetSelector) {
const source = document.querySelector(sourceSelector);
const target = document.querySelector(targetSelector);
if (!source || !target) return false;
const dragStartEvent = new DragEvent('dragstart', {
bubbles: true,
cancelable: true,
dataTransfer: new DataTransfer()
});
const dropEvent = new DragEvent('drop', {
bubbles: true,
cancelable: true,
dataTransfer: dragStartEvent.dataTransfer
});
source.dispatchEvent(dragStartEvent);
target.dispatchEvent(dropEvent);
return true;
}
return simulateDragDrop('.drag-source', '.drop-target');
";
var dragDropResult = _webDocument.ExecuteScript(dragDropScript);
if (dragDropResult != null && (bool)dragDropResult)
{
Report.Success("Event Simulation", "Drag and drop simulated successfully");
}
// Simulate complex mouse events
var complexMouseScript = @"
const element = document.querySelector('#complex-target');
if (element) {
['mouseenter', 'mouseover', 'mousedown', 'mouseup', 'click', 'mouseleave'].forEach(eventType => {
const event = new MouseEvent(eventType, {
bubbles: true,
cancelable: true,
clientX: element.getBoundingClientRect().left + 50,
clientY: element.getBoundingClientRect().top + 50
});
element.dispatchEvent(event);
});
return true;
}
return false;
";
_webDocument.ExecuteScript(complexMouseScript);
Report.Info("Event Simulation", "Complex mouse events simulated");
}
private void ExtractDataWithJavaScript()
{
// Extract table data
var tableDataScript = @"
const tables = document.querySelectorAll('table');
const data = [];
tables.forEach(table => {
const rows = table.querySelectorAll('tr');
const tableData = [];
rows.forEach(row => {
const cells = row.querySelectorAll('td, th');
const rowData = Array.from(cells).map(cell => cell.textContent.trim());
if (rowData.length > 0) tableData.push(rowData);
});
if (tableData.length > 0) data.push(tableData);
});
return JSON.stringify(data);
";
var tableDataJson = _webDocument.ExecuteScript(tableDataScript)?.ToString();
if (!string.IsNullOrEmpty(tableDataJson))
{
Report.Info("Data Extraction", $"Extracted table data: {tableDataJson.Length} characters");
}
// Extract form data
var formDataScript = @"
const forms = document.querySelectorAll('form');
const formData = [];
forms.forEach(form => {
const formInfo = {
action: form.action,
method: form.method,
fields: []
};
const inputs = form.querySelectorAll('input, select, textarea');
inputs.forEach(input => {
formInfo.fields.push({
name: input.name,
type: input.type,
value: input.value,
required: input.required
});
});
formData.push(formInfo);
});
return JSON.stringify(formData);
";
var formDataJson = _webDocument.ExecuteScript(formDataScript)?.ToString();
Report.Info("Data Extraction", "Extracted form data");
}
private void MonitorPerformance()
{
// Monitor page load performance
var performanceScript = @"
const perfData = performance.getEntriesByType('navigation')[0];
return {
domContentLoaded: perfData.domContentLoadedEventEnd - perfData.domContentLoadedEventStart,
loadComplete: perfData.loadEventEnd - perfData.loadEventStart,
domInteractive: perfData.domInteractive - perfData.navigationStart,
firstPaint: performance.getEntriesByType('paint').find(entry => entry.name === 'first-paint')?.startTime || 0
};
";
var perfData = _webDocument.ExecuteScript(performanceScript);
Report.Info("Performance", $"Performance data collected: {perfData}");
// Monitor memory usage
var memoryScript = @"
if (performance.memory) {
return {
used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024),
total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024),
limit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024)
};
}
return null;
";
var memoryData = _webDocument.ExecuteScript(memoryScript);
if (memoryData != null)
{
Report.Info("Memory Usage", $"Memory data: {memoryData}");
}
}
}
Best Practices for Web Automation
1. Wait Strategies
- Use explicit waits for dynamic content
- Implement multiple wait strategies for different scenarios
- Monitor network activity and AJAX requests
2. Element Location
- Prefer data-testid attributes for test stability
- Use CSS selectors for modern web applications
- Implement fallback selection strategies
3. Browser Compatibility
- Test across multiple browsers and versions
- Handle browser-specific timing differences
- Use feature detection over browser detection
4. Performance Considerations
- Monitor page load times and responsiveness
- Optimize wait times and polling intervals
- Use JavaScript execution judiciously
5. Error Handling
- Implement retry mechanisms for flaky web elements
- Capture browser console logs on failures
- Handle common web automation exceptions
Next Steps
For additional web automation patterns:
- Mobile Patterns - Mobile web and app automation
- Advanced Integration - API and backend integration
- Code Cookbook - Reusable web automation utilities
This web automation framework provides comprehensive coverage for modern web application testing across browsers and frameworks.