
Webapp Testing
- 5 installs
- 22 repo stars
- Updated December 29, 2025
- tencent/awesome-devbuddy
Helps with testing & qa tasks during AI-assisted development.
About
webapp-testing is a Claude Code skill for testing & qa. It helps solo builders move faster with AI-assisted coding.
- webapp-testing
- Testing & QA
- AI-coding skill
Webapp Testing by the numbers
- 5 all-time installs (skills.sh)
- +1 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #1,611 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tencent/awesome-devbuddy --skill webapp-testingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5 |
|---|---|
| repo stars | ★ 22 |
| Last updated | December 29, 2025 |
| Repository | tencent/awesome-devbuddy ↗ |
What it does
Helps with testing & qa tasks during AI-assisted development.
Files
Web 应用测试
要测试本地 Web 应用,请编写原生的 Python Playwright 脚本。
可用辅助脚本:
scripts/with_server.py- 管理服务器生命周期(支持多个服务器)
务必先使用 `--help` 运行脚本 以查看用法。在你先尝试直接运行脚本并确认必须定制之前,不要阅读源码。这些脚本可能非常庞大,会污染你的上下文窗口。它们的设计目的是作为黑盒脚本被直接调用,而不是被纳入你的上下文窗口。
决策树:选择你的方法
用户任务 → 是静态 HTML 吗?
├─ 是 → 直接读取 HTML 文件以识别选择器
│ ├─ 成功 → 使用这些选择器编写 Playwright 脚本
│ └─ 失败/不完整 → 按动态应用处理(见下)
│
└─ 否(动态 Web 应用) → 服务器是否已在运行?
├─ 否 → 运行:python scripts/with_server.py --help
│ 然后使用该助手 + 编写精简的 Playwright 脚本
│
└─ 是 → 先侦察,后操作:
1. 导航并等待 networkidle
2. 截图或检查 DOM
3. 从渲染状态中识别选择器
4. 使用发现的选择器执行操作示例:使用 with_server.py
要启动服务器,先运行 --help,然后使用该助手:
单个服务器:
python scripts/with_server.py --server "npm run dev" --port 5173 -- python your_automation.py多个服务器(例如后端 + 前端):
python scripts/with_server.py \
--server "cd backend && python server.py" --port 3000 \
--server "cd frontend && npm run dev" --port 5173 \
-- python your_automation.py编写自动化脚本时,只包含 Playwright 逻辑(服务器由助手自动管理):
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True) # 始终以无头模式启动 chromium
page = browser.new_page()
page.goto('http://localhost:5173') # 服务器已运行且就绪
page.wait_for_load_state('networkidle') # 关键:等待 JS 执行
# ... 你的自动化逻辑
browser.close()“先侦察,后操作”模式
1. 检查渲染后的 DOM:
page.screenshot(path='/tmp/inspect.png', full_page=True)
content = page.content()
page.locator('button').all()2. 根据检查结果识别选择器
3. 使用发现的选择器执行操作
常见陷阱
❌ 在动态应用中,等待 networkidle 之前不要检查 DOM ✅ 在检查之前请等待 page.wait_for_load_state('networkidle')
最佳实践
- 将捆绑脚本作为黑盒使用 - 处理任务时,考虑
scripts/中是否已有脚本可用。这些脚本可以可靠地处理常见且复杂的工作流,同时不污染你的上下文窗口。使用--help查看用法,然后直接调用。 - 对同步脚本使用
sync_playwright() - 完成后务必关闭浏览器
- 使用描述性选择器:
text=、role=、CSS 选择器或 ID - 添加适当的等待:
page.wait_for_selector()或page.wait_for_timeout()
参考文件
- examples/ - 展示常见模式的示例:
element_discovery.py- 发现页面上的按钮、链接和输入框static_html_automation.py- 使用 file:// URL 操作本地 HTMLconsole_logging.py- 在自动化过程中捕获控制台日志
from playwright.sync_api import sync_playwright
# Example: Capturing console logs during browser automation
url = 'http://localhost:5173' # Replace with your URL
console_logs = []
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(viewport={'width': 1920, 'height': 1080})
# Set up console log capture
def handle_console_message(msg):
console_logs.append(f"[{msg.type}] {msg.text}")
print(f"Console: [{msg.type}] {msg.text}")
page.on("console", handle_console_message)
# Navigate to page
page.goto(url)
page.wait_for_load_state('networkidle')
# Interact with the page (triggers console logs)
page.click('text=Dashboard')
page.wait_for_timeout(1000)
browser.close()
# Save console logs to file
with open('/mnt/user-data/outputs/console.log', 'w') as f:
f.write('\n'.join(console_logs))
print(f"\nCaptured {len(console_logs)} console messages")
print(f"Logs saved to: /mnt/user-data/outputs/console.log")from playwright.sync_api import sync_playwright
# Example: Discovering buttons and other elements on a page
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Navigate to page and wait for it to fully load
page.goto('http://localhost:5173')
page.wait_for_load_state('networkidle')
# Discover all buttons on the page
buttons = page.locator('button').all()
print(f"Found {len(buttons)} buttons:")
for i, button in enumerate(buttons):
text = button.inner_text() if button.is_visible() else "[hidden]"
print(f" [{i}] {text}")
# Discover links
links = page.locator('a[href]').all()
print(f"\nFound {len(links)} links:")
for link in links[:5]: # Show first 5
text = link.inner_text().strip()
href = link.get_attribute('href')
print(f" - {text} -> {href}")
# Discover input fields
inputs = page.locator('input, textarea, select').all()
print(f"\nFound {len(inputs)} input fields:")
for input_elem in inputs:
name = input_elem.get_attribute('name') or input_elem.get_attribute('id') or "[unnamed]"
input_type = input_elem.get_attribute('type') or 'text'
print(f" - {name} ({input_type})")
# Take screenshot for visual reference
page.screenshot(path='/tmp/page_discovery.png', full_page=True)
print("\nScreenshot saved to /tmp/page_discovery.png")
browser.close()from playwright.sync_api import sync_playwright
import os
# Example: Automating interaction with static HTML files using file:// URLs
html_file_path = os.path.abspath('path/to/your/file.html')
file_url = f'file://{html_file_path}'
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(viewport={'width': 1920, 'height': 1080})
# Navigate to local HTML file
page.goto(file_url)
# Take screenshot
page.screenshot(path='/mnt/user-data/outputs/static_page.png', full_page=True)
# Interact with elements
page.click('text=Click Me')
page.fill('#name', 'John Doe')
page.fill('#email', 'john@example.com')
# Submit form
page.click('button[type="submit"]')
page.wait_for_timeout(500)
# Take final screenshot
page.screenshot(path='/mnt/user-data/outputs/after_submit.png', full_page=True)
browser.close()
print("Static HTML automation completed!")<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SPA Testing Demo</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
nav {
background: rgba(255, 255, 255, 0.95);
padding: 20px 0;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
position: sticky;
top: 0;
z-index: 1000;
}
.nav-container {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
}
.logo {
font-size: 1.5em;
font-weight: bold;
color: #667eea;
}
.nav-links {
display: flex;
gap: 30px;
list-style: none;
}
.nav-links a {
text-decoration: none;
color: #333;
font-weight: 500;
padding: 8px 16px;
border-radius: 5px;
transition: all 0.3s;
}
.nav-links a:hover {
background: #667eea;
color: white;
}
.nav-links a.active {
background: #667eea;
color: white;
}
.container {
max-width: 1200px;
margin: 40px auto;
padding: 0 20px;
}
.page-content {
background: white;
padding: 60px 40px;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
min-height: 500px;
}
.page-content h1 {
color: #667eea;
font-size: 3em;
margin-bottom: 20px;
}
.page-content p {
color: #666;
font-size: 1.2em;
line-height: 1.8;
margin-bottom: 30px;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 25px;
margin: 40px 0;
}
.feature-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 12px;
text-align: center;
transition: transform 0.3s;
}
.feature-card:hover {
transform: translateY(-5px);
}
.feature-card h3 {
font-size: 1.5em;
margin-bottom: 15px;
}
.feature-icon {
font-size: 3em;
margin-bottom: 15px;
}
.btn {
display: inline-block;
padding: 15px 30px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: all 0.3s;
}
.btn:hover {
background: #764ba2;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.team-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 30px;
margin-top: 40px;
}
.team-member {
text-align: center;
}
.avatar {
width: 150px;
height: 150px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
margin: 0 auto 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 3em;
color: white;
}
.contact-form {
max-width: 600px;
margin: 40px 0;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 5px;
font-size: 1em;
font-family: inherit;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #667eea;
}
.route-indicator {
background: #f0f0f0;
padding: 10px 20px;
border-radius: 5px;
margin-bottom: 30px;
font-family: 'Courier New', monospace;
color: #667eea;
}
</style>
</head>
<body>
<!-- Navigation -->
<nav>
<div class="nav-container">
<div class="logo">⚡ SPA Demo</div>
<ul class="nav-links">
<li><a href="#/" data-route="home">Home</a></li>
<li><a href="#/about" data-route="about">About</a></li>
<li><a href="#/features" data-route="features">Features</a></li>
<li><a href="#/contact" data-route="contact">Contact</a></li>
</ul>
</div>
</nav>
<!-- Main Content -->
<div class="container">
<div id="app" class="page-content">
<!-- Content will be rendered here by JavaScript -->
</div>
</div>
<script>
// Simple SPA Router
class SPARouter {
constructor() {
this.routes = {};
this.currentRoute = null;
// Listen for hash changes
window.addEventListener('hashchange', () => this.handleRoute());
// Handle initial load
this.handleRoute();
}
// Register a route
addRoute(path, handler) {
this.routes[path] = handler;
}
// Handle route changes
handleRoute() {
const hash = window.location.hash.slice(1) || '/';
const route = hash.split('?')[0];
console.log(`Navigating to route: ${route}`);
// Update active nav link
document.querySelectorAll('.nav-links a').forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === `#${route}`) {
link.classList.add('active');
}
});
// Execute route handler
if (this.routes[route]) {
this.currentRoute = route;
this.routes[route]();
} else {
console.warn(`Route not found: ${route}`);
this.routes['/']();
}
}
}
// Initialize router
const router = new SPARouter();
// Define routes and their content
router.addRoute('/', () => {
document.getElementById('app').innerHTML = `
<div class="route-indicator">Current Route: /</div>
<h1>Welcome to SPA Testing</h1>
<p>This is a Single Page Application (SPA) built with vanilla JavaScript.
It demonstrates client-side routing without page reloads.</p>
<p>Click the navigation links above to see different "pages" load instantly
without refreshing the browser. This is how modern frameworks like React,
Vue, and Angular work.</p>
<div class="feature-grid">
<div class="feature-card">
<div class="feature-icon">🚀</div>
<h3>Fast Navigation</h3>
<p>No page reloads, instant transitions</p>
</div>
<div class="feature-card">
<div class="feature-icon">🎯</div>
<h3>State Management</h3>
<p>Maintain application state across routes</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔗</div>
<h3>Deep Linking</h3>
<p>Share URLs to specific app states</p>
</div>
</div>
<a href="#/features" class="btn">Explore Features →</a>
`;
});
router.addRoute('/about', () => {
document.getElementById('app').innerHTML = `
<div class="route-indicator">Current Route: /about</div>
<h1>About This Demo</h1>
<p>This SPA demonstrates hash-based routing, which is perfect for testing
with Playwright. The URL changes (via the hash) but the page never reloads.</p>
<h2 style="margin-top: 40px; color: #667eea;">How It Works</h2>
<p>When you click a navigation link:</p>
<ol style="line-height: 2; color: #666; margin-left: 20px;">
<li>The URL hash changes (e.g., #/about)</li>
<li>JavaScript detects the hashchange event</li>
<li>The router executes the corresponding route handler</li>
<li>New content is rendered without a page reload</li>
</ol>
<h2 style="margin-top: 40px; color: #667eea;">Our Team</h2>
<div class="team-grid">
<div class="team-member">
<div class="avatar">👨💻</div>
<h3>John Doe</h3>
<p>Lead Developer</p>
</div>
<div class="team-member">
<div class="avatar">👩💻</div>
<h3>Jane Smith</h3>
<p>QA Engineer</p>
</div>
<div class="team-member">
<div class="avatar">👨🎨</div>
<h3>Bob Johnson</h3>
<p>UI/UX Designer</p>
</div>
</div>
`;
});
router.addRoute('/features', () => {
document.getElementById('app').innerHTML = `
<div class="route-indicator">Current Route: /features</div>
<h1>Features</h1>
<p>Our application offers a comprehensive set of features designed to
make your testing experience smooth and efficient.</p>
<div class="feature-grid">
<div class="feature-card">
<div class="feature-icon">🧪</div>
<h3>Easy Testing</h3>
<p>Test navigation, state changes, and interactions with Playwright</p>
</div>
<div class="feature-card">
<div class="feature-icon">📱</div>
<h3>Responsive</h3>
<p>Works seamlessly on desktop, tablet, and mobile</p>
</div>
<div class="feature-card">
<div class="feature-icon">⚡</div>
<h3>Performance</h3>
<p>Instant page transitions and optimized rendering</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔒</div>
<h3>Secure</h3>
<p>Built with security best practices</p>
</div>
<div class="feature-card">
<div class="feature-icon">🎨</div>
<h3>Beautiful UI</h3>
<p>Modern, clean design with smooth animations</p>
</div>
<div class="feature-card">
<div class="feature-icon">📊</div>
<h3>Analytics</h3>
<p>Track user behavior and application performance</p>
</div>
</div>
<a href="#/contact" class="btn">Get In Touch →</a>
`;
});
router.addRoute('/contact', () => {
document.getElementById('app').innerHTML = `
<div class="route-indicator">Current Route: /contact</div>
<h1>Contact Us</h1>
<p>Have questions? We'd love to hear from you. Send us a message and
we'll respond as soon as possible.</p>
<form class="contact-form" onsubmit="handleContactSubmit(event)">
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="message">Message</label>
<textarea id="message" name="message" rows="5" required></textarea>
</div>
<button type="submit" class="btn">Send Message</button>
</form>
`;
});
// Handle contact form submission
window.handleContactSubmit = function(event) {
event.preventDefault();
console.log('Contact form submitted');
document.getElementById('app').innerHTML = `
<div class="route-indicator">Current Route: /contact (success)</div>
<h1>✅ Message Sent!</h1>
<p style="font-size: 1.3em;">Thank you for contacting us. We'll get back to you soon!</p>
<a href="#/" class="btn">← Back to Home</a>
`;
};
console.log('SPA Router initialized');
console.log('Available routes: /, /about, /features, /contact');
</script>
</body>
</html>
#!/usr/bin/env python3
"""
Advanced Example 07: SPA Testing
Learning Objectives:
- Navigate single-page applications with client-side routing
- Handle hash-based routing (#/route)
- Verify URL changes without page reloads
- Test application state changes
- Navigate between different "pages" in an SPA
SPAs are everywhere (React, Vue, Angular). This example shows you how to
test them effectively with Playwright.
"""
import os
from playwright.sync_api import sync_playwright
def test_spa_navigation():
"""Demonstrate testing a Single Page Application"""
# Setup
current_dir = os.path.dirname(os.path.abspath(__file__))
html_file_path = os.path.join(current_dir, 'spa_app.html')
file_url = f'file://{html_file_path}'
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
print("=" * 70)
print("SPA TESTING DEMONSTRATION")
print("=" * 70)
print()
# ===== LOAD SPA =====
print("Step 1: Loading Single Page Application")
print("-" * 70)
page.goto(file_url)
page.wait_for_load_state('networkidle')
print(f"✓ SPA loaded: {file_url}")
# Check initial route
current_url = page.url
print(f"✓ Initial URL: {current_url}")
# Check which page is displayed
route_indicator = page.locator('.route-indicator').inner_text()
print(f"✓ {route_indicator}")
print()
# ===== VERIFY HOME PAGE =====
print("Test 1: Verifying Home Page (/) Content")
print("-" * 70)
# Check if we're on home page
heading = page.locator('h1').inner_text()
print(f"Page heading: {heading}")
# Count feature cards
feature_cards = page.locator('.feature-card').all()
print(f"✓ Found {len(feature_cards)} feature cards on home page")
# Verify active nav link
active_link = page.locator('.nav-links a.active').inner_text()
print(f"✓ Active navigation: {active_link}")
print()
# ===== NAVIGATE TO ABOUT =====
print("Test 2: Navigating to About Page")
print("-" * 70)
# Click the "About" link
page.click('a[href="#/about"]')
# Wait a moment for JavaScript to update the DOM
# For SPAs, we don't get a page reload, so we wait for content change
page.wait_for_selector('.route-indicator')
# Verify URL changed
new_url = page.url
print(f"✓ URL changed to: {new_url}")
assert '#/about' in new_url, "URL should contain #/about"
# Verify route indicator
route_indicator = page.locator('.route-indicator').inner_text()
print(f"✓ {route_indicator}")
# Verify page content
heading = page.locator('h1').inner_text()
print(f"✓ Page heading: {heading}")
# Check for team members (specific to About page)
team_members = page.locator('.team-member').all()
print(f"✓ Found {len(team_members)} team members")
# Verify active nav link changed
active_link = page.locator('.nav-links a.active').inner_text()
print(f"✓ Active navigation: {active_link}")
print()
# ===== NAVIGATE TO FEATURES =====
print("Test 3: Navigating to Features Page")
print("-" * 70)
page.click('a[href="#/features"]')
page.wait_for_selector('.route-indicator')
new_url = page.url
print(f"✓ URL: {new_url}")
assert '#/features' in new_url
route_indicator = page.locator('.route-indicator').inner_text()
print(f"✓ {route_indicator}")
# Verify more feature cards on this page
feature_cards = page.locator('.feature-card').all()
print(f"✓ Found {len(feature_cards)} feature cards")
heading = page.locator('h1').inner_text()
print(f"✓ Page heading: {heading}")
print()
# ===== NAVIGATE TO CONTACT =====
print("Test 4: Navigating to Contact Page")
print("-" * 70)
page.click('a[href="#/contact"]')
page.wait_for_selector('.route-indicator')
new_url = page.url
print(f"✓ URL: {new_url}")
assert '#/contact' in new_url
route_indicator = page.locator('.route-indicator').inner_text()
print(f"✓ {route_indicator}")
# Verify contact form is present
form = page.locator('.contact-form')
is_visible = form.is_visible()
print(f"✓ Contact form visible: {'Yes' if is_visible else 'No'}")
# Count form inputs
inputs = form.locator('input, textarea').all()
print(f"✓ Form has {len(inputs)} input fields")
print()
# ===== TEST FORM SUBMISSION IN SPA =====
print("Test 5: Submitting Contact Form (SPA State Change)")
print("-" * 70)
# Fill form
page.fill('#name', 'Test User')
page.fill('#email', 'test@example.com')
page.fill('#message', 'Testing SPA form submission')
print("✓ Form filled")
# Submit form
page.click('button[type="submit"]')
# Wait for success message (content changes but URL stays same)
page.wait_for_selector('h1:has-text("Message Sent")')
# Verify success state
success_heading = page.locator('h1').inner_text()
print(f"✓ Success message: {success_heading}")
# URL should still be #/contact
current_url = page.url
print(f"✓ URL (unchanged): {current_url}")
print()
# ===== TEST BROWSER BACK BUTTON =====
print("Test 6: Testing Browser Back Button")
print("-" * 70)
# Go back
page.go_back()
page.wait_for_timeout(200) # Brief wait for hash change
current_url = page.url
print(f"✓ After back: {current_url}")
# Should be back at features
assert '#/features' in current_url
print("✓ Successfully navigated back to Features page")
print()
# ===== TEST BROWSER FORWARD BUTTON =====
print("Test 7: Testing Browser Forward Button")
print("-" * 70)
# Go forward
page.go_forward()
page.wait_for_timeout(200)
current_url = page.url
print(f"✓ After forward: {current_url}")
# Should be back at contact success
assert '#/contact' in current_url
print("✓ Successfully navigated forward to Contact page")
print()
# ===== TEST DIRECT URL NAVIGATION =====
print("Test 8: Direct URL Navigation (Deep Linking)")
print("-" * 70)
# Navigate directly to /about by changing URL
page.goto(f"{file_url}#/about")
page.wait_for_selector('.route-indicator')
route_indicator = page.locator('.route-indicator').inner_text()
print(f"✓ {route_indicator}")
heading = page.locator('h1').inner_text()
print(f"✓ Loaded directly to: {heading}")
print()
# ===== VERIFY NO PAGE RELOADS OCCURRED =====
print("Test 9: Verifying No Page Reloads (SPA Characteristic)")
print("-" * 70)
print("Key Observation:")
print(" Throughout all these navigation tests, the page was NEVER reloaded.")
print(" Only the URL hash changed, and JavaScript updated the content.")
print(" This is the defining characteristic of a Single Page Application.")
print()
print("Evidence:")
print(" • URL changes used hash routing: #/route")
print(" • wait_for_load_state('networkidle') only called once")
print(" • Content updates were instantaneous (no loading spinner)")
print(" • Browser back/forward buttons work correctly")
print()
# ===== SUMMARY =====
print("=" * 70)
print("TEST SUMMARY")
print("=" * 70)
print("SPA Navigation Tests Completed:")
print(" ✓ Loaded SPA initial state")
print(" ✓ Navigated to About page")
print(" ✓ Navigated to Features page")
print(" ✓ Navigated to Contact page")
print(" ✓ Submitted form (state change)")
print(" ✓ Used browser back button")
print(" ✓ Used browser forward button")
print(" ✓ Tested direct URL navigation (deep linking)")
print()
print("Key SPA Testing Patterns:")
print(" 🎯 Wait for content selectors, not page loads")
print(" 🎯 Verify URL hash changes")
print(" 🎯 Use wait_for_selector() after navigation clicks")
print(" 🎯 Test browser navigation (back/forward)")
print(" 🎯 Verify state changes without page reloads")
print()
print("Real-World Applications:")
print(" • React Router")
print(" • Vue Router")
print(" • Angular Router")
print(" • Any hash-based or history-based SPA routing")
browser.close()
print()
print("✅ SPA testing completed successfully!")
if __name__ == '__main__':
test_spa_navigation()
#!/usr/bin/env python3
"""
Simple Flask Application for Server Integration Testing
This is a minimal Flask app that demonstrates:
- Serving HTML pages
- RESTful API endpoints
- Dynamic content rendering
"""
from flask import Flask, jsonify, render_template_string
import time
import random
app = Flask(__name__)
# HTML template
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flask Integration Demo</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background: #f0f0f0;
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
}
button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
margin: 5px;
}
button:hover {
background: #0056b3;
}
#data-container {
margin-top: 20px;
padding: 20px;
background: #f8f9fa;
border-radius: 5px;
min-height: 100px;
}
.data-item {
padding: 10px;
margin: 5px 0;
background: #e9ecef;
border-radius: 3px;
}
</style>
</head>
<body>
<div class="container">
<h1>Flask Integration Testing</h1>
<p>This page is served by a Flask backend and demonstrates full-stack testing.</p>
<div>
<button onclick="loadData()">Load Data from API</button>
<button onclick="clearData()">Clear Data</button>
</div>
<div id="data-container">
<p>Click "Load Data" to fetch data from the API...</p>
</div>
</div>
<script>
async function loadData() {
console.log('Loading data from API...');
const container = document.getElementById('data-container');
container.innerHTML = '<p>Loading...</p>';
try {
const response = await fetch('/api/data');
const data = await response.json();
container.innerHTML = '<h3>API Response:</h3>';
data.items.forEach(item => {
const div = document.createElement('div');
div.className = 'data-item';
div.textContent = `${item.id}: ${item.name}`;
container.appendChild(div);
});
console.log('Data loaded successfully');
} catch (error) {
console.error('Error loading data:', error);
container.innerHTML = '<p style="color: red;">Error loading data</p>';
}
}
function clearData() {
document.getElementById('data-container').innerHTML =
'<p>Click "Load Data" to fetch data from the API...</p>';
console.log('Data cleared');
}
</script>
</body>
</html>
"""
@app.route('/')
def home():
"""Serve the main page"""
return render_template_string(HTML_TEMPLATE)
@app.route('/api/data')
def get_data():
"""API endpoint that returns JSON data"""
# Simulate some processing time
time.sleep(0.2)
items = [
{'id': i, 'name': f'Item {i}', 'value': random.randint(1, 100)}
for i in range(1, 6)
]
return jsonify({
'status': 'success',
'items': items,
'timestamp': time.time()
})
@app.route('/api/status')
def status():
"""Health check endpoint"""
return jsonify({
'status': 'ok',
'message': 'Server is running'
})
if __name__ == '__main__':
print("=" * 60)
print("Flask Integration Test Server")
print("=" * 60)
print("Starting server on http://localhost:5000")
print("Press Ctrl+C to stop")
print("=" * 60)
app.run(host='localhost', port=5000, debug=False)
Server Integration Testing
This example demonstrates how to test full-stack applications (frontend + backend) using Playwright with the with_server.py helper script.
Overview
Testing applications with backend servers requires: 1. Starting the server before tests run 2. Waiting for the server to be ready 3. Running your tests 4. Stopping the server after tests complete
The with_server.py helper automates this entire lifecycle!
Files
flask_app.py- Simple Flask web server with HTML page and APItest_with_server.py- Playwright tests for the Flask applicationREADME.md- This file
Prerequisites
Install Flask:
pip install flaskPlaywright should already be installed from the tutorial prerequisites.
Running the Tests
Option 1: Using the with_server.py Helper (Recommended)
The helper script automatically starts the server, waits for it to be ready, runs your tests, and stops the server:
# From this directory
python ../../../../scripts/with_server.py \
--server "python flask_app.py" \
--port 5000 \
-- python test_with_server.pyThat's it! The helper handles everything.
Option 2: Manual Server Start
If you prefer to manually control the server:
Terminal 1 - Start the server:
python flask_app.pyTerminal 2 - Run the tests:
python test_with_server.pyPress Ctrl+C in Terminal 1 to stop the server when done.
What Gets Tested
1. Server Connectivity - Verify the server is running and accessible 2. Page Content - Check that HTML is served correctly 3. API Endpoints - Test direct API calls (GET /api/status, GET /api/data) 4. Frontend-Backend Integration - Click button → API call → DOM update 5. Console Monitoring - Capture browser console logs during testing
Understanding with_server.py
The helper script usage:
python scripts/with_server.py \
--server "command to start server" \
--port port_number \
-- your_test_commandArguments:
--server: Command to start your server (can be used multiple times for multiple servers)--port: Port number to poll for server readiness (can be used multiple times)--: Everything after this is your test command
Multiple Servers Example:
python scripts/with_server.py \
--server "cd backend && python api_server.py" --port 5000 \
--server "cd frontend && npm run dev" --port 3000 \
-- python test_integration.pyHow It Works
1. with_server.py starts the server process(es) 2. Polls the specified port(s) until the server responds (default 30s timeout) 3. Once ready, executes your test command 4. When tests finish, automatically terminates all server processes
Troubleshooting
Port Already in Use
Error: Address already in useSolution: Stop any existing Flask servers:
# macOS/Linux
lsof -ti:5000 | xargs kill
# Windows
netstat -ano | findstr :5000
taskkill /PID <pid> /FServer Not Ready
Error: Server did not become ready within timeoutSolutions:
- Increase timeout: The helper has a 30s default timeout
- Check server logs for startup errors
- Verify the port number is correct
- Ensure Flask is installed:
pip install flask
Connection Refused
Error: Could not connect to serverSolutions:
- Make sure the server is running (check the other terminal)
- Verify you're using the correct port (5000)
- Check firewall settings
Next Steps
After mastering this example:
1. Try with your own backend - Replace Flask with Django, FastAPI, Express, etc. 2. Test multiple endpoints - Add more API routes and test them 3. Test authentication - Add login flows and session management 4. Test database operations - Verify CRUD operations work correctly 5. Test error handling - Trigger errors and verify proper error messages
Related Examples
- 05_dynamic_content - Learn proper wait strategies for async operations
- 09_comprehensive - See a complete test suite for a complex application
---
Pro Tip: In CI/CD environments, always use the with_server.py pattern to ensure clean server startup and shutdown. This prevents port conflicts and zombie processes!
#!/usr/bin/env python3
"""
Advanced Example 08: Server Integration Testing
Learning Objectives:
- Test applications with backend servers
- Use the with_server.py helper script for server lifecycle management
- Test full-stack applications (frontend + backend)
- Verify API integration
- Handle server startup delays
This example demonstrates testing a real Flask application.
The with_server.py helper manages the server lifecycle automatically.
USAGE:
Run this with the helper script:
python ../../../../scripts/with_server.py \\
--server "python flask_app.py" \\
--port 5000 \\
-- python test_with_server.py
Or run flask_app.py manually in another terminal, then run this script.
"""
import os
from playwright.sync_api import sync_playwright
def test_flask_integration():
"""Test a Flask application with Playwright"""
# This test assumes the Flask server is running on localhost:5000
# When using with_server.py, it will be started automatically
base_url = 'http://localhost:5000'
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
print("=" * 70)
print("FLASK INTEGRATION TESTING")
print("=" * 70)
print()
# ===== TEST 1: VERIFY SERVER IS RUNNING =====
print("Test 1: Verifying Server is Running")
print("-" * 70)
try:
# Navigate to the home page
page.goto(base_url, timeout=10000)
page.wait_for_load_state('networkidle')
print(f"✓ Successfully connected to: {base_url}")
# Verify page title
title = page.title()
print(f"✓ Page title: {title}")
print()
except Exception as e:
print(f"❌ Error: Could not connect to server")
print(f" Make sure Flask server is running on port 5000")
print(f" Error details: {e}")
browser.close()
return
# ===== TEST 2: VERIFY PAGE CONTENT =====
print("Test 2: Verifying Page Content")
print("-" * 70)
# Check heading
heading = page.locator('h1').inner_text()
print(f"✓ Page heading: {heading}")
# Count buttons
buttons = page.locator('button').all()
print(f"✓ Found {len(buttons)} buttons")
# Verify initial message
data_container = page.locator('#data-container')
initial_text = data_container.inner_text()
print(f"✓ Initial message: {initial_text[:50]}...")
print()
# ===== TEST 3: API HEALTH CHECK =====
print("Test 3: Testing API Endpoint Directly")
print("-" * 70)
# Use Playwright's request context to call API directly
response = page.request.get(f'{base_url}/api/status')
print(f"✓ API response status: {response.status}")
if response.status == 200:
data = response.json()
print(f"✓ API status: {data['status']}")
print(f"✓ API message: {data['message']}")
print()
# ===== TEST 4: LOAD DATA FROM API =====
print("Test 4: Loading Data via Button Click")
print("-" * 70)
# Click the "Load Data" button
page.click('button:has-text("Load Data")')
print("✓ Clicked 'Load Data' button")
# Wait for data to load
# The API has a 0.2s delay, so we need to wait
page.wait_for_selector('.data-item', timeout=5000)
print("✓ Data loaded from API")
# Count data items
data_items = page.locator('.data-item').all()
print(f"✓ Received {len(data_items)} items from API")
# Display some items
print("\nSample data items:")
for i, item in enumerate(data_items[:3], 1):
text = item.inner_text()
print(f" {i}. {text}")
print()
# ===== TEST 5: CLEAR DATA =====
print("Test 5: Clearing Data")
print("-" * 70)
# Click clear button
page.click('button:has-text("Clear Data")')
print("✓ Clicked 'Clear Data' button")
# Verify data was cleared
data_container_text = page.locator('#data-container').inner_text()
print(f"✓ Data container: {data_container_text[:50]}...")
# Data items should be gone
remaining_items = len(page.locator('.data-item').all())
print(f"✓ Remaining data items: {remaining_items}")
print()
# ===== TEST 6: RELOAD AND TEST AGAIN =====
print("Test 6: Testing Data Load Again")
print("-" * 70)
# Load data again
page.click('button:has-text("Load Data")')
page.wait_for_selector('.data-item')
new_items = page.locator('.data-item').all()
print(f"✓ Loaded {len(new_items)} items (second load)")
# Verify the data changed (random values should be different)
print("✓ API returned fresh data")
print()
# ===== TEST 7: CONSOLE MONITORING =====
print("Test 7: Monitoring Console Logs")
print("-" * 70)
# Set up console listener for next test
console_messages = []
def capture_console(msg):
console_messages.append(f"[{msg.type}] {msg.text}")
page.on("console", capture_console)
# Trigger actions that log to console
page.click('button:has-text("Clear Data")')
page.wait_for_timeout(100)
page.click('button:has-text("Load Data")')
page.wait_for_timeout(500)
print(f"✓ Captured {len(console_messages)} console messages")
print("\nRecent console messages:")
for msg in console_messages[-3:]:
print(f" {msg}")
print()
# ===== SUMMARY =====
print("=" * 70)
print("TEST SUMMARY")
print("=" * 70)
print("Integration Tests Completed:")
print(" ✓ Connected to Flask server")
print(" ✓ Verified page content")
print(" ✓ Tested API health endpoint")
print(" ✓ Loaded data from API via button click")
print(" ✓ Cleared data")
print(" ✓ Reloaded data (verified fresh data)")
print(" ✓ Monitored console logs")
print()
print("Key Learnings:")
print(" 🔧 Full-stack testing combines frontend and backend")
print(" 🔧 Use with_server.py for automatic server lifecycle")
print(" 🔧 Test both UI interactions AND direct API calls")
print(" 🔧 Verify data flows correctly from backend to frontend")
print()
print("Production Applications:")
print(" • Flask + React")
print(" • Django + Vue")
print(" • FastAPI + Angular")
print(" • Express + any frontend framework")
browser.close()
print()
print("✅ Flask integration testing completed successfully!")
if __name__ == '__main__':
print("\nIMPORTANT: This test requires a Flask server running on port 5000")
print("Use the with_server.py helper or start flask_app.py manually\n")
test_flask_integration()
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TechShop - E-Commerce Demo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; position: sticky; top: 0; z-index: 1000; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.header-content { max-width: 1200px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; }
.logo { font-size: 1.8em; font-weight: bold; }
.cart-icon { cursor: pointer; padding: 10px 20px; background: rgba(255,255,255,0.2); border-radius: 5px; }
.cart-count { background: #ff4757; padding: 2px 8px; border-radius: 10px; font-size: 0.9em; margin-left: 5px; }
.search-bar { width: 100%; max-width: 500px; padding: 10px; border: 2px solid #ddd; border-radius: 5px; font-size: 1em; margin: 20px auto; display: block; }
.container { max-width: 1200px; margin: 30px auto; padding: 0 20px; }
.product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 25px; }
.product-card { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); transition: transform 0.3s; }
.product-card:hover { transform: translateY(-5px); }
.product-image { width: 100%; height: 200px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 3em; margin-bottom: 15px; }
.product-name { font-size: 1.2em; font-weight: 600; margin-bottom: 10px; }
.product-price { font-size: 1.3em; color: #667eea; font-weight: bold; margin-bottom: 10px; }
.product-description { color: #666; font-size: 0.95em; margin-bottom: 15px; }
.btn { padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; font-weight: 600; transition: all 0.3s; }
.btn-primary { background: #667eea; color: white; width: 100%; }
.btn-primary:hover { background: #764ba2; }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 2000; }
.modal-content { background: white; max-width: 600px; margin: 50px auto; padding: 40px; border-radius: 15px; max-height: 80vh; overflow-y: auto; }
.modal-close { float: right; font-size: 2em; cursor: pointer; color: #999; }
.cart-item { padding: 15px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
.cart-total { font-size: 1.5em; text-align: right; margin-top: 20px; padding-top: 20px; border-top: 2px solid #eee; }
.empty-cart { text-align: center; padding: 40px; color: #999; }
.checkout-form { margin-top: 30px; }
.form-group { margin-bottom: 15px; }
.form-group label { display: block; margin-bottom: 5px; font-weight: 600; }
.form-group input { width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px; }
.success-message { background: #d4edda; border: 2px solid #c3e6cb; color: #155724; padding: 20px; border-radius: 8px; text-align: center; margin-top: 20px; }
</style>
</head>
<body>
<header>
<div class="header-content">
<div class="logo">🛒 TechShop</div>
<div class="cart-icon" onclick="openCart()">
Cart <span class="cart-count" id="cartCount">0</span>
</div>
</div>
</header>
<div class="container">
<input type="text" class="search-bar" id="searchBar" placeholder="Search products..." onkeyup="searchProducts()">
<div class="product-grid" id="productGrid"></div>
</div>
<div class="modal" id="cartModal">
<div class="modal-content">
<span class="modal-close" onclick="closeCart()">×</span>
<h2>Shopping Cart</h2>
<div id="cartItems"></div>
<div class="cart-total" id="cartTotal"></div>
<div id="checkoutSection"></div>
</div>
</div>
<script>
const products = [
{ id: 1, name: 'Laptop Pro', price: 1299, category: 'laptop', icon: '💻', description: 'High-performance laptop for professionals' },
{ id: 2, name: 'Wireless Mouse', price: 29, category: 'accessory', icon: '🖱️', description: 'Ergonomic wireless mouse' },
{ id: 3, name: 'Mechanical Keyboard', price: 129, category: 'accessory', icon: '⌨️', description: 'RGB mechanical gaming keyboard' },
{ id: 4, name: 'USB-C Hub', price: 49, category: 'accessory', icon: '🔌', description: '7-in-1 USB-C hub with HDMI' },
{ id: 5, name: 'Wireless Headphones', price: 199, category: 'audio', icon: '🎧', description: 'Noise-cancelling headphones' },
{ id: 6, name: 'Webcam HD', price: 79, category: 'accessory', icon: '📷', description: '1080p webcam with microphone' },
{ id: 7, name: 'Monitor 27"', price: 349, category: 'display', icon: '🖥️', description: '4K IPS display' },
{ id: 8, name: 'External SSD 1TB', price: 159, category: 'storage', icon: '💾', description: 'Portable solid-state drive' },
{ id: 9, name: 'Laptop Stand', price: 39, category: 'accessory', icon: '📐', description: 'Aluminum laptop stand' },
{ id: 10, name: 'Desk Lamp', price: 59, category: 'accessory', icon: '💡', description: 'LED desk lamp with USB port' },
];
let cart = [];
let allProducts = [...products];
function renderProducts(productsToRender = allProducts) {
const grid = document.getElementById('productGrid');
grid.innerHTML = productsToRender.map(p => `
<div class="product-card">
<div class="product-image">${p.icon}</div>
<div class="product-name">${p.name}</div>
<div class="product-price">$${p.price}</div>
<div class="product-description">${p.description}</div>
<button class="btn btn-primary" onclick="addToCart(${p.id})">Add to Cart</button>
</div>
`).join('');
}
function searchProducts() {
const query = document.getElementById('searchBar').value.toLowerCase();
const filtered = query ? products.filter(p =>
p.name.toLowerCase().includes(query) ||
p.description.toLowerCase().includes(query) ||
p.category.toLowerCase().includes(query)
) : products;
allProducts = filtered;
renderProducts(filtered);
console.log(`Search: "${query}" - Found ${filtered.length} products`);
}
function addToCart(productId) {
const product = products.find(p => p.id === productId);
const existing = cart.find(item => item.id === productId);
if (existing) {
existing.quantity++;
} else {
cart.push({ ...product, quantity: 1 });
}
updateCartCount();
console.log(`Added ${product.name} to cart`);
}
function updateCartCount() {
const count = cart.reduce((sum, item) => sum + item.quantity, 0);
document.getElementById('cartCount').textContent = count;
}
function openCart() {
renderCart();
document.getElementById('cartModal').style.display = 'block';
}
function closeCart() {
document.getElementById('cartModal').style.display = 'none';
}
function renderCart() {
const cartItems = document.getElementById('cartItems');
const cartTotal = document.getElementById('cartTotal');
if (cart.length === 0) {
cartItems.innerHTML = '<div class="empty-cart"><p>Your cart is empty</p></div>';
cartTotal.innerHTML = '';
document.getElementById('checkoutSection').innerHTML = '';
return;
}
cartItems.innerHTML = cart.map(item => `
<div class="cart-item">
<div>
<strong>${item.name}</strong><br>
$${item.price} × ${item.quantity}
</div>
<div>
<button class="btn" onclick="updateQuantity(${item.id}, -1)">-</button>
${item.quantity}
<button class="btn" onclick="updateQuantity(${item.id}, 1)">+</button>
<button class="btn" onclick="removeFromCart(${item.id})">Remove</button>
</div>
</div>
`).join('');
const total = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
cartTotal.innerHTML = `<strong>Total: $${total.toFixed(2)}</strong>`;
document.getElementById('checkoutSection').innerHTML = `
<button class="btn btn-primary" onclick="showCheckout()">Proceed to Checkout</button>
`;
}
function updateQuantity(productId, change) {
const item = cart.find(i => i.id === productId);
if (item) {
item.quantity += change;
if (item.quantity <= 0) {
removeFromCart(productId);
} else {
renderCart();
updateCartCount();
}
}
}
function removeFromCart(productId) {
cart = cart.filter(item => item.id !== productId);
renderCart();
updateCartCount();
}
function showCheckout() {
document.getElementById('checkoutSection').innerHTML = `
<div class="checkout-form">
<h3>Checkout</h3>
<div class="form-group">
<label>Name</label>
<input type="text" id="checkoutName" required>
</div>
<div class="form-group">
<label>Email</label>
<input type="email" id="checkoutEmail" required>
</div>
<div class="form-group">
<label>Card Number</label>
<input type="text" id="checkoutCard" placeholder="**** **** **** ****" required>
</div>
<button class="btn btn-primary" onclick="completeCheckout()">Complete Order</button>
</div>
`;
}
function completeCheckout() {
const name = document.getElementById('checkoutName').value;
const email = document.getElementById('checkoutEmail').value;
const orderId = 'ORD-' + Math.random().toString(36).substr(2, 9).toUpperCase();
const total = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
console.log('Order completed:', { orderId, name, email, total });
document.getElementById('checkoutSection').innerHTML = `
<div class="success-message">
<h2>✅ Order Confirmed!</h2>
<p>Order ID: <strong>${orderId}</strong></p>
<p>Thank you, ${name}!</p>
<p>Total: $${total.toFixed(2)}</p>
<p>A confirmation email has been sent to ${email}</p>
</div>
`;
cart = [];
updateCartCount();
}
// Initialize
renderProducts();
console.log('TechShop initialized with', products.length, 'products');
</script>
</body>
</html>
#!/usr/bin/env python3
"""
Advanced Example 09: Comprehensive Test Suite
Learning Objectives:
- Build complete end-to-end tests
- Combine all previous techniques
- Organize test code effectively
- Handle complex user workflows
- Create reusable test helper functions
This is a COMPLETE test suite for an e-commerce application.
It demonstrates professional-level test organization and coverage.
"""
import os
from playwright.sync_api import sync_playwright
class TestHelpers:
"""Reusable helper functions for testing"""
@staticmethod
def get_cart_count(page):
"""Get the current cart item count"""
return int(page.locator('#cartCount').inner_text())
@staticmethod
def get_product_count(page):
"""Get the number of products displayed"""
return len(page.locator('.product-card').all())
@staticmethod
def add_product_to_cart(page, product_name):
"""Add a specific product to cart by name"""
# Find the product card by name and click its Add to Cart button
product_card = page.locator(f'.product-card:has-text("{product_name}")')
product_card.locator('button:has-text("Add to Cart")').click()
@staticmethod
def open_cart(page):
"""Open the shopping cart modal"""
page.click('.cart-icon')
page.wait_for_selector('#cartModal[style*="display: block"]')
@staticmethod
def save_screenshot(page, name, screenshots_dir):
"""Save a screenshot with consistent naming"""
path = os.path.join(screenshots_dir, f'{name}.png')
page.screenshot(path=path, full_page=True)
return path
def test_ecommerce_comprehensive():
"""
Comprehensive E-Commerce Test Suite
Tests:
1. Product browsing
2. Search functionality
3. Add to cart
4. Cart quantity management
5. Remove from cart
6. Checkout flow
"""
# Setup
current_dir = os.path.dirname(os.path.abspath(__file__))
html_file_path = os.path.join(current_dir, 'ecommerce_app.html')
file_url = f'file://{html_file_path}'
# Create directories for outputs
screenshots_dir = os.path.join(current_dir, 'screenshots')
os.makedirs(screenshots_dir, exist_ok=True)
helpers = TestHelpers()
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Setup console monitoring
console_messages = []
page.on("console", lambda msg: console_messages.append(f"[{msg.type}] {msg.text}"))
print("=" * 70)
print("E-COMMERCE COMPREHENSIVE TEST SUITE")
print("=" * 70)
print()
# ===== TEST 1: PRODUCT BROWSING =====
print("Test 1: Product Browsing and Initial State")
print("-" * 70)
page.goto(file_url)
page.wait_for_load_state('networkidle')
print("✓ Page loaded successfully")
# Verify page title
title = page.title()
print(f"✓ Page title: {title}")
# Count products
product_count = helpers.get_product_count(page)
print(f"✓ Products displayed: {product_count}")
# Verify cart is empty
cart_count = helpers.get_cart_count(page)
assert cart_count == 0, "Cart should be empty initially"
print(f"✓ Cart items: {cart_count}")
# Screenshot initial state
screenshot_path = helpers.save_screenshot(page, '01_initial_products', screenshots_dir)
print(f"📸 Screenshot: {screenshot_path}")
print()
# ===== TEST 2: SEARCH FUNCTIONALITY =====
print("Test 2: Search Functionality")
print("-" * 70)
# Search for "laptop"
search_term = "laptop"
page.fill('#searchBar', search_term)
page.keyboard.press('Enter')
# Wait briefly for filtering
page.wait_for_timeout(200)
filtered_count = helpers.get_product_count(page)
print(f"✓ Search term: '{search_term}'")
print(f"✓ Results: {filtered_count} product(s)")
# Get product names
products = page.locator('.product-name').all()
print(" Products found:")
for product in products:
print(f" - {product.inner_text()}")
screenshot_path = helpers.save_screenshot(page, '02_search_results', screenshots_dir)
print(f"📸 Screenshot: {screenshot_path}")
print()
# Clear search
page.fill('#searchBar', '')
page.keyboard.press('Enter')
page.wait_for_timeout(200)
# ===== TEST 3: ADD TO CART =====
print("Test 3: Adding Products to Cart")
print("-" * 70)
# Add first product
product1_name = "Laptop Pro"
helpers.add_product_to_cart(page, product1_name)
page.wait_for_timeout(100)
cart_count = helpers.get_cart_count(page)
print(f"✓ Added '{product1_name}' to cart")
print(f"✓ Cart count: {cart_count}")
# Add second product
product2_name = "Wireless Mouse"
helpers.add_product_to_cart(page, product2_name)
page.wait_for_timeout(100)
cart_count = helpers.get_cart_count(page)
print(f"✓ Added '{product2_name}' to cart")
print(f"✓ Cart count: {cart_count}")
# Add third product
product3_name = "Mechanical Keyboard"
helpers.add_product_to_cart(page, product3_name)
page.wait_for_timeout(100)
cart_count = helpers.get_cart_count(page)
print(f"✓ Added '{product3_name}' to cart")
print(f"✓ Cart count: {cart_count}")
assert cart_count == 3, "Cart should have 3 items"
screenshot_path = helpers.save_screenshot(page, '03_cart_updated', screenshots_dir)
print(f"📸 Screenshot: {screenshot_path}")
print()
# ===== TEST 4: VIEW CART =====
print("Test 4: Viewing Cart Contents")
print("-" * 70)
helpers.open_cart(page)
print("✓ Opened cart modal")
# Verify cart items
cart_items = page.locator('.cart-item').all()
print(f"✓ Cart items displayed: {len(cart_items)}")
for i, item in enumerate(cart_items, 1):
item_text = item.inner_text()
print(f" {i}. {item_text.split('Remove')[0].strip()}")
# Verify total
total = page.locator('.cart-total').inner_text()
print(f"✓ {total}")
screenshot_path = helpers.save_screenshot(page, '04_cart_view', screenshots_dir)
print(f"📸 Screenshot: {screenshot_path}")
print()
# ===== TEST 5: QUANTITY MANAGEMENT =====
print("Test 5: Managing Item Quantities")
print("-" * 70)
# Increase quantity of first item
first_item = page.locator('.cart-item').first
first_item.locator('button:has-text("+")').click()
page.wait_for_timeout(100)
print("✓ Increased quantity of first item")
cart_count = helpers.get_cart_count(page)
print(f"✓ New cart count: {cart_count}")
assert cart_count == 4, "Cart should have 4 items (1+1+1+1)"
# Decrease quantity
first_item.locator('button:has-text("-")').click()
page.wait_for_timeout(100)
print("✓ Decreased quantity back to 1")
cart_count = helpers.get_cart_count(page)
print(f"✓ Cart count: {cart_count}")
print()
# ===== TEST 6: REMOVE FROM CART =====
print("Test 6: Removing Items from Cart")
print("-" * 70)
# Remove last item
last_item = page.locator('.cart-item').last
last_item.locator('button:has-text("Remove")').click()
page.wait_for_timeout(100)
print("✓ Removed last item from cart")
cart_count = helpers.get_cart_count(page)
print(f"✓ Cart count: {cart_count}")
assert cart_count == 2, "Cart should have 2 items"
remaining_items = page.locator('.cart-item').all()
print(f"✓ Remaining items: {len(remaining_items)}")
screenshot_path = helpers.save_screenshot(page, '05_item_removed', screenshots_dir)
print(f"📸 Screenshot: {screenshot_path}")
print()
# ===== TEST 7: CHECKOUT FLOW =====
print("Test 7: Checkout Process")
print("-" * 70)
# Add one more item for a complete checkout test
page.click('.modal-close') # Close cart
page.wait_for_timeout(200)
helpers.add_product_to_cart(page, "Wireless Headphones")
page.wait_for_timeout(100)
print("✓ Added another item for checkout")
# Reopen cart
helpers.open_cart(page)
# Click checkout
page.click('button:has-text("Proceed to Checkout")')
page.wait_for_timeout(200)
print("✓ Clicked 'Proceed to Checkout'")
# Verify checkout form appeared
checkout_form = page.locator('.checkout-form')
assert checkout_form.is_visible(), "Checkout form should be visible"
print("✓ Checkout form displayed")
screenshot_path = helpers.save_screenshot(page, '06_checkout_form', screenshots_dir)
print(f"📸 Screenshot: {screenshot_path}")
print()
# ===== TEST 8: COMPLETE ORDER =====
print("Test 8: Completing Order")
print("-" * 70)
# Fill checkout form
page.fill('#checkoutName', 'John Doe')
page.fill('#checkoutEmail', 'john.doe@example.com')
page.fill('#checkoutCard', '4111 1111 1111 1111')
print("✓ Filled checkout form")
# Complete order
page.click('button:has-text("Complete Order")')
page.wait_for_timeout(300)
# Verify success message
success_message = page.locator('.success-message')
assert success_message.is_visible(), "Success message should be visible"
success_text = success_message.inner_text()
print("✓ Order completed successfully")
# Extract order ID
order_id_match = success_text
print(f"✓ Order confirmed")
# Verify cart is empty
cart_count = helpers.get_cart_count(page)
assert cart_count == 0, "Cart should be empty after checkout"
print(f"✓ Cart cleared (count: {cart_count})")
screenshot_path = helpers.save_screenshot(page, '07_order_success', screenshots_dir)
print(f"📸 Screenshot: {screenshot_path}")
print()
# ===== TEST 9: CONSOLE LOGS ANALYSIS =====
print("Test 9: Console Logs Analysis")
print("-" * 70)
print(f"✓ Total console messages: {len(console_messages)}")
# Show sample messages
print("\nSample console messages:")
for msg in console_messages[-5:]:
print(f" {msg}")
print()
# ===== SUMMARY =====
print("=" * 70)
print("TEST SUITE SUMMARY")
print("=" * 70)
print()
print("Tests Completed:")
print(" ✅ Test 1: Product browsing (10 products)")
print(" ✅ Test 2: Search functionality (filter by keyword)")
print(" ✅ Test 3: Add to cart (3 products added)")
print(" ✅ Test 4: View cart contents")
print(" ✅ Test 5: Quantity management (+/-)")
print(" ✅ Test 6: Remove from cart")
print(" ✅ Test 7: Checkout flow")
print(" ✅ Test 8: Complete order")
print(" ✅ Test 9: Console monitoring")
print()
print("Artifacts Generated:")
print(f" 📸 7 screenshots saved to: {screenshots_dir}")
print(f" 📝 {len(console_messages)} console messages captured")
print()
print("Test Coverage:")
print(" • Product catalog display")
print(" • Search and filtering")
print(" • Shopping cart operations")
print(" • Quantity adjustments")
print(" • Item removal")
print(" • Complete checkout workflow")
print(" • Form validation")
print(" • State management")
print(" • Console logging")
print()
print("Best Practices Demonstrated:")
print(" 🏗️ Organized test structure with numbered tests")
print(" 🔧 Reusable helper functions (TestHelpers class)")
print(" 📸 Screenshot capture at key points")
print(" 🐛 Console monitoring throughout")
print(" ✅ Assertions to verify expected behavior")
print(" 📊 Clear test output with progress indicators")
browser.close()
print()
print("=" * 70)
print("✅ COMPREHENSIVE TEST SUITE COMPLETED SUCCESSFULLY!")
print("=" * 70)
if __name__ == '__main__':
test_ecommerce_comprehensive()
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to Web Testing</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #2c3e50;
border-bottom: 3px solid #3498db;
padding-bottom: 10px;
}
.info {
margin: 20px 0;
padding: 15px;
background-color: #e8f4f8;
border-left: 4px solid #3498db;
}
.footer {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #ddd;
color: #7f8c8d;
font-size: 0.9em;
}
</style>
</head>
<body>
<div class="container">
<h1>Hello, Playwright!</h1>
<div class="info">
<p><strong>Welcome to your first web testing example!</strong></p>
<p>This is a simple static HTML page that you can test with Playwright.</p>
</div>
<h2>What you'll learn:</h2>
<ul>
<li>Loading local HTML files with the file:// protocol</li>
<li>Navigating to pages and waiting for them to load</li>
<li>Extracting page title and content</li>
<li>Understanding the Playwright sync API</li>
</ul>
<h2>Key Concepts:</h2>
<p>Static HTML files don't require a web server. You can test them directly by converting the file path to a <code>file://</code> URL.</p>
<div class="footer">
<p>webapp-testing tutorial • Beginner Level • Example 01</p>
</div>
</div>
</body>
</html>
#!/usr/bin/env python3
"""
Beginner Example 01: Static HTML Testing
Learning Objectives:
- Load local HTML files using file:// URLs
- Navigate to pages and extract basic information
- Understand the Playwright context manager pattern
- Verify page content programmatically
This is your first Playwright test! It demonstrates the fundamentals
of loading a static HTML file and inspecting its content.
"""
import os
from playwright.sync_api import sync_playwright
def test_static_html():
"""Test basic navigation and content extraction from a static HTML file"""
# Step 1: Get the absolute path to our HTML file
# Playwright needs an absolute path for file:// URLs
current_dir = os.path.dirname(os.path.abspath(__file__))
html_file_path = os.path.join(current_dir, 'sample.html')
# Convert to file:// URL format
file_url = f'file://{html_file_path}'
# Step 2: Use the Playwright context manager
# This ensures proper cleanup even if errors occur
with sync_playwright() as p:
# Step 3: Launch browser in headless mode
# headless=True means no visible browser window (good for automation)
browser = p.chromium.launch(headless=True)
# Step 4: Create a new page (tab)
page = browser.new_page()
# Step 5: Navigate to our HTML file
page.goto(file_url)
print(f"✓ Loaded: {file_url}")
# Step 6: Extract the page title
# The <title> tag in the HTML <head>
title = page.title()
print(f"✓ Page title: {title}")
# Step 7: Extract text from the main heading
# Using a CSS selector to find the <h1> element
heading = page.locator('h1').inner_text()
print(f"✓ Main heading: {heading}")
# Step 8: Extract text from the info box
# .inner_text() gets the visible text content
info_text = page.locator('.info strong').inner_text()
print(f"✓ Info message: {info_text}")
# Step 9: Count list items
# .all() returns a list of all matching elements
list_items = page.locator('ul li').all()
print(f"✓ Found {len(list_items)} learning objectives")
# Step 10: Print each learning objective
print("\nLearning objectives:")
for i, item in enumerate(list_items, 1):
text = item.inner_text()
print(f" {i}. {text}")
# Step 11: Close the browser
# Good practice to clean up resources
browser.close()
print("\n✓ Test completed successfully!")
if __name__ == '__main__':
test_static_html()
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Element Discovery Demo</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 900px;
margin: 40px auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
background-color: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
h1 {
color: #667eea;
margin-bottom: 10px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
}
.section {
margin: 30px 0;
padding: 20px;
border: 2px solid #e0e0e0;
border-radius: 8px;
}
.section h2 {
margin-top: 0;
color: #764ba2;
}
.buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.btn-primary {
background-color: #667eea;
color: white;
}
.btn-secondary {
background-color: #95a5a6;
color: white;
}
.btn-danger {
background-color: #e74c3c;
color: white;
}
.btn-hidden {
display: none;
}
button:hover:not(.btn-hidden) {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.links {
list-style: none;
padding: 0;
}
.links li {
margin: 10px 0;
}
.links a {
color: #667eea;
text-decoration: none;
padding: 5px 10px;
border-radius: 3px;
transition: background-color 0.3s;
}
.links a:hover {
background-color: #f0f0f0;
}
input, textarea, select {
width: 100%;
padding: 10px;
margin: 8px 0;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
font-size: 14px;
}
label {
font-weight: 600;
color: #333;
display: block;
margin-top: 15px;
}
.form-group {
margin-bottom: 15px;
}
</style>
</head>
<body>
<div class="container">
<h1>Element Discovery</h1>
<p class="subtitle">Practice finding and inspecting different types of HTML elements</p>
<!-- Button Section -->
<div class="section">
<h2>Buttons</h2>
<p>Different button types and states:</p>
<div class="buttons">
<button class="btn-primary">Submit Form</button>
<button class="btn-secondary">Cancel</button>
<button class="btn-danger">Delete</button>
<button class="btn-primary">Save</button>
<button class="btn-hidden">Hidden Button</button>
</div>
</div>
<!-- Links Section -->
<div class="section">
<h2>Navigation Links</h2>
<p>Common website navigation:</p>
<ul class="links">
<li><a href="/">Home</a></li>
<li><a href="/about">About Us</a></li>
<li><a href="/products">Products</a></li>
<li><a href="/contact">Contact</a></li>
<li><a href="https://example.com" target="_blank">External Link</a></li>
</ul>
</div>
<!-- Form Inputs Section -->
<div class="section">
<h2>Form Inputs</h2>
<p>Various input field types:</p>
<div class="form-group">
<label for="username">Username (text input)</label>
<input type="text" id="username" name="username" placeholder="Enter username">
</div>
<div class="form-group">
<label for="email">Email (email input)</label>
<input type="email" id="email" name="user_email" placeholder="user@example.com">
</div>
<div class="form-group">
<label for="password">Password (password input)</label>
<input type="password" id="password" name="password" placeholder="••••••••">
</div>
<div class="form-group">
<label for="country">Country (dropdown)</label>
<select id="country" name="country">
<option value="">-- Select --</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="ca">Canada</option>
</select>
</div>
<div class="form-group">
<label for="comments">Comments (textarea)</label>
<textarea id="comments" name="comments" rows="4" placeholder="Enter your comments..."></textarea>
</div>
</div>
<!-- Hidden Elements Section -->
<div class="section">
<h2>Visibility Testing</h2>
<p>Some elements are hidden with CSS:</p>
<div>
<span>This is visible</span>
<span style="display: none;">This is hidden</span>
<span>This is also visible</span>
</div>
</div>
</div>
</body>
</html>
#!/usr/bin/env python3
"""
Beginner Example 02: Element Discovery
Learning Objectives:
- Discover all interactive elements on a page
- Use different selector strategies (tag, class, ID, attribute)
- Filter visible vs hidden elements
- Extract element attributes
This example demonstrates how to explore a page before automating it.
This "scout first, then act" approach is a best practice in web automation.
"""
import os
from playwright.sync_api import sync_playwright
def discover_elements():
"""Discover and catalog all interactive elements on the page"""
# Setup file URL
current_dir = os.path.dirname(os.path.abspath(__file__))
html_file_path = os.path.join(current_dir, 'sample.html')
file_url = f'file://{html_file_path}'
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Navigate to the page
page.goto(file_url)
print(f"Loaded: {file_url}\n")
# ===== DISCOVER BUTTONS =====
print("=" * 50)
print("BUTTONS")
print("=" * 50)
# Find all buttons on the page
buttons = page.locator('button').all()
print(f"✓ Found {len(buttons)} total buttons\n")
# Inspect each button
for i, button in enumerate(buttons, 1):
# Check if button is visible
is_visible = button.is_visible()
# Get button text
text = button.inner_text() if is_visible else "[hidden]"
# Get button classes
classes = button.get_attribute('class') or "no classes"
# Display results
visibility = "👁️ visible" if is_visible else "🚫 hidden"
print(f"Button {i}:")
print(f" Text: {text}")
print(f" Classes: {classes}")
print(f" Visibility: {visibility}\n")
# ===== DISCOVER LINKS =====
print("=" * 50)
print("LINKS")
print("=" * 50)
# Find all links with href attribute
links = page.locator('a[href]').all()
print(f"✓ Found {len(links)} links\n")
for i, link in enumerate(links, 1):
text = link.inner_text()
href = link.get_attribute('href')
target = link.get_attribute('target') or "(same window)"
print(f"Link {i}:")
print(f" Text: {text}")
print(f" URL: {href}")
print(f" Target: {target}\n")
# ===== DISCOVER INPUT FIELDS =====
print("=" * 50)
print("INPUT FIELDS")
print("=" * 50)
# Find different types of inputs
text_inputs = page.locator('input[type="text"], input[type="email"], input[type="password"]').all()
print(f"✓ Found {len(text_inputs)} text-based inputs\n")
for i, input_field in enumerate(text_inputs, 1):
input_type = input_field.get_attribute('type')
name = input_field.get_attribute('name')
placeholder = input_field.get_attribute('placeholder')
input_id = input_field.get_attribute('id')
print(f"Input {i}:")
print(f" Type: {input_type}")
print(f" ID: {input_id}")
print(f" Name: {name}")
print(f" Placeholder: {placeholder}\n")
# ===== DISCOVER SELECT DROPDOWNS =====
print("=" * 50)
print("DROPDOWNS")
print("=" * 50)
selects = page.locator('select').all()
print(f"✓ Found {len(selects)} dropdown(s)\n")
for i, select in enumerate(selects, 1):
name = select.get_attribute('name')
select_id = select.get_attribute('id')
# Find all options in the dropdown
options = select.locator('option').all()
print(f"Select {i}:")
print(f" ID: {select_id}")
print(f" Name: {name}")
print(f" Options ({len(options)}):")
for option in options:
value = option.get_attribute('value')
text = option.inner_text()
print(f" - {text} (value: {value})")
print()
# ===== DISCOVER TEXTAREAS =====
print("=" * 50)
print("TEXTAREAS")
print("=" * 50)
textareas = page.locator('textarea').all()
print(f"✓ Found {len(textareas)} textarea(s)\n")
for i, textarea in enumerate(textareas, 1):
name = textarea.get_attribute('name')
rows = textarea.get_attribute('rows')
placeholder = textarea.get_attribute('placeholder')
print(f"Textarea {i}:")
print(f" Name: {name}")
print(f" Rows: {rows}")
print(f" Placeholder: {placeholder}\n")
# ===== DISCOVER HEADINGS =====
print("=" * 50)
print("HEADINGS")
print("=" * 50)
# Find all headings (h1, h2, h3, etc.)
headings = page.locator('h1, h2').all()
print(f"✓ Found {len(headings)} heading(s)\n")
for heading in headings:
# Get the tag name by checking which selector matches
tag = "h1" if heading.evaluate('el => el.tagName') == "H1" else "h2"
text = heading.inner_text()
print(f"<{tag}>: {text}")
# ===== TEST VISIBILITY =====
print("\n" + "=" * 50)
print("VISIBILITY TESTING")
print("=" * 50)
# Find all spans in the visibility section
visibility_section = page.locator('.section').nth(3) # 4th section (0-indexed)
spans = visibility_section.locator('span').all()
print(f"✓ Found {len(spans)} span elements\n")
for i, span in enumerate(spans, 1):
is_visible = span.is_visible()
text = span.inner_text() if is_visible else "[cannot get text - hidden]"
visibility = "✓ Visible" if is_visible else "✗ Hidden"
print(f"Span {i}: {visibility}")
if is_visible:
print(f" Text: {text}\n")
else:
print()
# ===== SUMMARY =====
print("=" * 50)
print("DISCOVERY SUMMARY")
print("=" * 50)
print(f"Total buttons: {len(buttons)}")
print(f"Total links: {len(links)}")
print(f"Total text inputs: {len(text_inputs)}")
print(f"Total dropdowns: {len(selects)}")
print(f"Total textareas: {len(textareas)}")
print(f"Total headings: {len(headings)}")
browser.close()
print("\n✓ Element discovery completed!")
if __name__ == '__main__':
discover_elements()
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Screenshot Demo</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(to bottom, #0f2027, #203a43, #2c5364);
color: white;
min-height: 100vh;
}
header {
background-color: rgba(0, 0, 0, 0.3);
padding: 30px;
text-align: center;
border-bottom: 3px solid #00d4ff;
}
header h1 {
font-size: 3em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
header p {
font-size: 1.2em;
color: #00d4ff;
}
.container {
max-width: 1200px;
margin: 40px auto;
padding: 0 20px;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
margin: 40px 0;
}
.card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 30px;
border: 1px solid rgba(255, 255, 255, 0.2);
transition: transform 0.3s, box-shadow 0.3s;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0, 212, 255, 0.3);
}
.card h2 {
color: #00d4ff;
margin-bottom: 15px;
font-size: 1.8em;
}
.card p {
line-height: 1.6;
color: #e0e0e0;
}
.feature-list {
margin: 20px 0;
}
.feature-list li {
padding: 10px;
margin: 10px 0;
background: rgba(0, 212, 255, 0.1);
border-left: 3px solid #00d4ff;
border-radius: 5px;
}
.stats {
display: flex;
justify-content: space-around;
margin: 50px 0;
flex-wrap: wrap;
}
.stat-box {
text-align: center;
padding: 20px;
min-width: 200px;
}
.stat-number {
font-size: 3em;
color: #00d4ff;
font-weight: bold;
}
.stat-label {
font-size: 1.2em;
color: #b0b0b0;
margin-top: 10px;
}
footer {
background-color: rgba(0, 0, 0, 0.5);
padding: 30px;
text-align: center;
margin-top: 60px;
border-top: 3px solid #00d4ff;
}
.highlight-box {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40px;
border-radius: 20px;
margin: 40px 0;
text-align: center;
}
.highlight-box h2 {
font-size: 2.5em;
margin-bottom: 20px;
}
.button-group {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 20px;
}
button {
padding: 15px 30px;
font-size: 1.1em;
border: none;
border-radius: 25px;
cursor: pointer;
transition: all 0.3s;
font-weight: bold;
}
.btn-primary {
background-color: #00d4ff;
color: #0f2027;
}
.btn-secondary {
background-color: transparent;
color: white;
border: 2px solid white;
}
button:hover {
transform: scale(1.05);
box-shadow: 0 5px 15px rgba(255, 255, 255, 0.3);
}
</style>
</head>
<body>
<header id="main-header">
<h1>📸 Screenshot Testing</h1>
<p>Visual Regression Testing with Playwright</p>
</header>
<div class="container">
<div class="highlight-box">
<h2>Capture Every Pixel</h2>
<p>Learn how to take full-page screenshots, element-specific captures, and test across different viewport sizes.</p>
<div class="button-group">
<button class="btn-primary">Get Started</button>
<button class="btn-secondary">Learn More</button>
</div>
</div>
<h2 style="margin-top: 50px; font-size: 2.5em; text-align: center;">Key Features</h2>
<div class="card-grid">
<div class="card">
<h2>Full Page Capture</h2>
<p>Capture entire pages including content below the fold. Perfect for documenting complete page layouts.</p>
</div>
<div class="card">
<h2>Element Screenshots</h2>
<p>Take screenshots of specific elements like headers, cards, or sections for targeted visual testing.</p>
</div>
<div class="card">
<h2>Responsive Testing</h2>
<p>Test how your page looks on different devices by changing viewport sizes programmatically.</p>
</div>
<div class="card">
<h2>Visual Regression</h2>
<p>Compare screenshots over time to detect unintended visual changes in your application.</p>
</div>
<div class="card">
<h2>Before/After Comparison</h2>
<p>Capture states before and after interactions to verify UI changes work as expected.</p>
</div>
<div class="card">
<h2>Documentation</h2>
<p>Generate visual documentation of your application automatically with screenshot automation.</p>
</div>
</div>
<h2 style="margin-top: 50px; font-size: 2.5em; text-align: center;">Testing Statistics</h2>
<div class="stats">
<div class="stat-box">
<div class="stat-number">99.9%</div>
<div class="stat-label">Accuracy</div>
</div>
<div class="stat-box">
<div class="stat-number">1000+</div>
<div class="stat-label">Tests Run Daily</div>
</div>
<div class="stat-box">
<div class="stat-number">50ms</div>
<div class="stat-label">Avg. Screenshot Time</div>
</div>
</div>
<h2 style="margin-top: 50px; font-size: 2.5em;">What You'll Learn</h2>
<ul class="feature-list">
<li>📷 Taking full-page screenshots with <code>full_page=True</code></li>
<li>🎯 Capturing specific elements using locators</li>
<li>📱 Testing different viewport sizes (desktop, tablet, mobile)</li>
<li>💾 Organizing screenshot outputs in directories</li>
<li>🔍 Using screenshots for debugging and visual verification</li>
</ul>
</div>
<footer>
<p>webapp-testing tutorial • Beginner Level • Example 03</p>
<p style="margin-top: 10px; color: #00d4ff;">This page is designed to look great in screenshots!</p>
</footer>
</body>
</html>
#!/usr/bin/env python3
"""
Beginner Example 03: Screenshots
Learning Objectives:
- Capture full-page screenshots
- Take element-specific screenshots
- Set custom viewport sizes for responsive testing
- Organize screenshot outputs
- Use screenshots for debugging and documentation
Screenshots are one of the most valuable tools in web testing:
- Visual verification of UI state
- Before/after comparisons
- Debugging what went wrong
- Generating documentation
- Visual regression testing
"""
import os
from playwright.sync_api import sync_playwright
def test_screenshots():
"""Demonstrate various screenshot techniques"""
# Setup
current_dir = os.path.dirname(os.path.abspath(__file__))
html_file_path = os.path.join(current_dir, 'sample.html')
file_url = f'file://{html_file_path}'
# Create screenshots directory if it doesn't exist
screenshots_dir = os.path.join(current_dir, 'screenshots')
os.makedirs(screenshots_dir, exist_ok=True)
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
print("=" * 60)
print("SCREENSHOT TESTING DEMO")
print("=" * 60)
print()
# ===== TEST 1: Full-Page Screenshot (Default Viewport) =====
print("Test 1: Full-Page Screenshot (Desktop)")
print("-" * 60)
page = browser.new_page()
page.goto(file_url)
# Take a full-page screenshot
# full_page=True captures everything, even content below the fold
screenshot_path = os.path.join(screenshots_dir, '01_full_page_desktop.png')
page.screenshot(path=screenshot_path, full_page=True)
print(f"✓ Screenshot saved: {screenshot_path}")
print(f" Viewport: {page.viewport_size['width']}x{page.viewport_size['height']}")
print()
page.close()
# ===== TEST 2: Mobile Viewport Screenshot =====
print("Test 2: Mobile Viewport Screenshot")
print("-" * 60)
# Create a new page with mobile viewport
page = browser.new_page()
# Set viewport to mobile dimensions (iPhone 12 Pro)
page.set_viewport_size({"width": 390, "height": 844})
page.goto(file_url)
screenshot_path = os.path.join(screenshots_dir, '02_full_page_mobile.png')
page.screenshot(path=screenshot_path, full_page=True)
print(f"✓ Screenshot saved: {screenshot_path}")
print(f" Viewport: 390x844 (iPhone 12 Pro)")
print()
page.close()
# ===== TEST 3: Tablet Viewport Screenshot =====
print("Test 3: Tablet Viewport Screenshot")
print("-" * 60)
page = browser.new_page()
# Set viewport to tablet dimensions (iPad)
page.set_viewport_size({"width": 768, "height": 1024})
page.goto(file_url)
screenshot_path = os.path.join(screenshots_dir, '03_full_page_tablet.png')
page.screenshot(path=screenshot_path, full_page=True)
print(f"✓ Screenshot saved: {screenshot_path}")
print(f" Viewport: 768x1024 (iPad)")
print()
page.close()
# ===== TEST 4: Element-Specific Screenshots =====
print("Test 4: Element-Specific Screenshots")
print("-" * 60)
page = browser.new_page()
page.goto(file_url)
# Screenshot the header only
header = page.locator('#main-header')
header_path = os.path.join(screenshots_dir, '04_element_header.png')
header.screenshot(path=header_path)
print(f"✓ Header screenshot: {header_path}")
# Screenshot the highlight box
highlight = page.locator('.highlight-box')
highlight_path = os.path.join(screenshots_dir, '05_element_highlight_box.png')
highlight.screenshot(path=highlight_path)
print(f"✓ Highlight box screenshot: {highlight_path}")
# Screenshot the first card
first_card = page.locator('.card').first
card_path = os.path.join(screenshots_dir, '06_element_first_card.png')
first_card.screenshot(path=card_path)
print(f"✓ First card screenshot: {card_path}")
print()
# ===== TEST 5: Screenshot Before/After Pattern =====
print("Test 5: Before/After Pattern")
print("-" * 60)
print("This pattern is useful for verifying UI changes")
print()
# Take "before" screenshot
before_path = os.path.join(screenshots_dir, '07_before_interaction.png')
page.screenshot(path=before_path, full_page=True)
print(f"✓ Before screenshot: {before_path}")
# Simulate an interaction (in a real test, you might click a button)
# Here we'll just scroll to demonstrate
page.evaluate('window.scrollTo(0, document.body.scrollHeight / 2)')
# Take "after" screenshot
after_path = os.path.join(screenshots_dir, '08_after_interaction.png')
page.screenshot(path=after_path) # Not full_page - just visible area
print(f"✓ After screenshot: {after_path}")
print()
# ===== TEST 6: Ultra-Wide Desktop Screenshot =====
print("Test 6: Ultra-Wide Desktop Screenshot")
print("-" * 60)
page.set_viewport_size({"width": 1920, "height": 1080})
page.goto(file_url)
ultrawide_path = os.path.join(screenshots_dir, '09_ultrawide_desktop.png')
page.screenshot(path=ultrawide_path, full_page=True)
print(f"✓ Screenshot saved: {ultrawide_path}")
print(f" Viewport: 1920x1080")
print()
# ===== SUMMARY =====
print("=" * 60)
print("SUMMARY")
print("=" * 60)
# Count screenshots
screenshot_files = [f for f in os.listdir(screenshots_dir) if f.endswith('.png')]
print(f"✓ Total screenshots taken: {len(screenshot_files)}")
print(f"✓ Screenshots directory: {screenshots_dir}")
print()
print("Screenshot Types Demonstrated:")
print(" • Full-page screenshots (desktop, tablet, mobile)")
print(" • Element-specific screenshots")
print(" • Before/after comparison screenshots")
print(" • Multiple viewport sizes")
print()
print("Common Use Cases:")
print(" 📸 Visual regression testing")
print(" 🐛 Debugging UI issues")
print(" 📱 Responsive design verification")
print(" 📚 Automated documentation generation")
print(" ✅ Before/after interaction verification")
page.close()
browser.close()
print()
print("✓ Screenshot testing completed successfully!")
print(f" View your screenshots in: {screenshots_dir}")
if __name__ == '__main__':
test_screenshots()
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Contact Form - Form Automation Demo</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.form-container {
background: white;
max-width: 600px;
width: 100%;
padding: 40px;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.form-header {
text-align: center;
margin-bottom: 30px;
}
.form-header h1 {
color: #667eea;
font-size: 2.5em;
margin-bottom: 10px;
}
.form-header p {
color: #666;
font-size: 1.1em;
}
.form-group {
margin-bottom: 25px;
}
label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 600;
font-size: 0.95em;
}
label .required {
color: #e74c3c;
}
input[type="text"],
input[type="email"],
input[type="tel"],
select,
textarea {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1em;
font-family: inherit;
transition: all 0.3s;
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
textarea {
resize: vertical;
min-height: 120px;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 10px;
}
input[type="checkbox"],
input[type="radio"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.radio-group {
display: flex;
gap: 20px;
margin-top: 10px;
}
.radio-option {
display: flex;
align-items: center;
gap: 8px;
}
.radio-option label {
margin-bottom: 0;
font-weight: normal;
cursor: pointer;
}
.button-group {
display: flex;
gap: 15px;
margin-top: 30px;
}
button {
flex: 1;
padding: 15px;
font-size: 1.1em;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.btn-submit {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-submit:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.btn-reset {
background: #e0e0e0;
color: #666;
}
.btn-reset:hover {
background: #d0d0d0;
}
.success-message {
display: none;
background: #d4edda;
border: 2px solid #c3e6cb;
color: #155724;
padding: 20px;
border-radius: 8px;
margin-top: 20px;
text-align: center;
}
.success-message.show {
display: block;
}
.success-message h2 {
margin-bottom: 10px;
color: #155724;
}
.submitted-data {
text-align: left;
margin-top: 15px;
background: white;
padding: 15px;
border-radius: 5px;
}
.submitted-data p {
margin: 5px 0;
font-size: 0.95em;
}
.submitted-data strong {
color: #155724;
}
</style>
</head>
<body>
<div class="form-container">
<div class="form-header">
<h1>📝 Contact Form</h1>
<p>Fill out the form below to get in touch with us</p>
</div>
<form id="contactForm">
<div class="form-group">
<label for="name">Full Name <span class="required">*</span></label>
<input type="text" id="name" name="name" required placeholder="John Doe">
</div>
<div class="form-group">
<label for="email">Email Address <span class="required">*</span></label>
<input type="email" id="email" name="email" required placeholder="john@example.com">
</div>
<div class="form-group">
<label for="phone">Phone Number</label>
<input type="tel" id="phone" name="phone" placeholder="+1 (555) 123-4567">
</div>
<div class="form-group">
<label for="country">Country <span class="required">*</span></label>
<select id="country" name="country" required>
<option value="">-- Select a country --</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="ca">Canada</option>
<option value="au">Australia</option>
<option value="de">Germany</option>
<option value="fr">France</option>
<option value="jp">Japan</option>
<option value="other">Other</option>
</select>
</div>
<div class="form-group">
<label>How did you hear about us? <span class="required">*</span></label>
<div class="radio-group">
<div class="radio-option">
<input type="radio" id="source-search" name="source" value="search" required>
<label for="source-search">Search Engine</label>
</div>
<div class="radio-option">
<input type="radio" id="source-social" name="source" value="social">
<label for="source-social">Social Media</label>
</div>
<div class="radio-option">
<input type="radio" id="source-friend" name="source" value="friend">
<label for="source-friend">Friend</label>
</div>
</div>
</div>
<div class="form-group">
<label for="message">Message <span class="required">*</span></label>
<textarea id="message" name="message" required placeholder="Tell us what you're interested in..."></textarea>
</div>
<div class="form-group">
<div class="checkbox-group">
<input type="checkbox" id="newsletter" name="newsletter">
<label for="newsletter">Subscribe to our newsletter</label>
</div>
</div>
<div class="form-group">
<div class="checkbox-group">
<input type="checkbox" id="terms" name="terms" required>
<label for="terms">I agree to the terms and conditions <span class="required">*</span></label>
</div>
</div>
<div class="button-group">
<button type="reset" class="btn-reset">Reset Form</button>
<button type="submit" class="btn-submit">Submit Form</button>
</div>
</form>
<div class="success-message" id="successMessage">
<h2>✅ Thank You!</h2>
<p>Your message has been successfully submitted.</p>
<div class="submitted-data" id="submittedData"></div>
</div>
</div>
<script>
document.getElementById('contactForm').addEventListener('submit', function(e) {
e.preventDefault();
// Get form data
const formData = new FormData(this);
const data = {};
for (let [key, value] of formData.entries()) {
data[key] = value;
}
// Display submitted data
const submittedDataDiv = document.getElementById('submittedData');
submittedDataDiv.innerHTML = `
<p><strong>Name:</strong> ${data.name || 'N/A'}</p>
<p><strong>Email:</strong> ${data.email || 'N/A'}</p>
<p><strong>Phone:</strong> ${data.phone || 'Not provided'}</p>
<p><strong>Country:</strong> ${document.getElementById('country').selectedOptions[0].text}</p>
<p><strong>Source:</strong> ${document.querySelector('input[name="source"]:checked')?.nextElementSibling?.textContent || 'N/A'}</p>
<p><strong>Newsletter:</strong> ${data.newsletter ? 'Yes' : 'No'}</p>
<p><strong>Message:</strong> ${data.message || 'N/A'}</p>
`;
// Hide form and show success message
this.style.display = 'none';
document.getElementById('successMessage').classList.add('show');
});
</script>
</body>
</html>
#!/usr/bin/env python3
"""
Intermediate Example 04: Form Automation
Learning Objectives:
- Fill text inputs programmatically
- Select dropdown options
- Check/uncheck checkboxes and radio buttons
- Submit forms and verify submission
- Capture before/after states with screenshots
Forms are one of the most common elements to automate in web testing.
This example demonstrates all the key form interaction patterns.
"""
import os
from playwright.sync_api import sync_playwright
def test_form_automation():
"""Demonstrate comprehensive form automation"""
# Setup
current_dir = os.path.dirname(os.path.abspath(__file__))
html_file_path = os.path.join(current_dir, 'form_app.html')
file_url = f'file://{html_file_path}'
# Create screenshots directory
screenshots_dir = os.path.join(current_dir, 'screenshots')
os.makedirs(screenshots_dir, exist_ok=True)
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
print("=" * 70)
print("FORM AUTOMATION TEST")
print("=" * 70)
print()
# Navigate to the form
page.goto(file_url)
print(f"✓ Loaded form: {file_url}")
print()
# Take a screenshot of the empty form
before_screenshot = os.path.join(screenshots_dir, '01_form_empty.png')
page.screenshot(path=before_screenshot, full_page=True)
print(f"📸 Screenshot (empty form): {before_screenshot}")
print()
# ===== FILL TEXT INPUTS =====
print("Step 1: Filling Text Inputs")
print("-" * 70)
# Fill the name field
name_value = "John Doe"
page.fill('#name', name_value)
print(f"✓ Name field filled: '{name_value}'")
# Fill the email field
email_value = "john.doe@example.com"
page.fill('#email', email_value)
print(f"✓ Email field filled: '{email_value}'")
# Fill the phone field (optional field)
phone_value = "+1 (555) 123-4567"
page.fill('#phone', phone_value)
print(f"✓ Phone field filled: '{phone_value}'")
print()
# ===== SELECT DROPDOWN OPTION =====
print("Step 2: Selecting Dropdown Option")
print("-" * 70)
# Select a country from the dropdown
# You can select by value, label, or index
country_value = "us"
page.select_option('#country', value=country_value)
# Verify the selection
selected_option = page.locator('#country option:checked').inner_text()
print(f"✓ Country selected: '{selected_option}' (value: {country_value})")
print()
# ===== SELECT RADIO BUTTON =====
print("Step 3: Selecting Radio Button")
print("-" * 70)
# Select a radio button
# Radio buttons are exclusive - only one can be selected
page.check('#source-social')
print(f"✓ Radio button selected: 'Social Media'")
# Verify which radio is selected
selected_radio = page.locator('input[name="source"]:checked')
radio_value = selected_radio.get_attribute('value')
print(f" Verified selection: {radio_value}")
print()
# ===== FILL TEXTAREA =====
print("Step 4: Filling Textarea")
print("-" * 70)
message_value = """Hi there!
I'm interested in learning more about your web testing services.
I found your website through social media and would love to discuss
how you can help automate our testing processes.
Thanks!"""
page.fill('#message', message_value)
print(f"✓ Message textarea filled ({len(message_value)} characters)")
print()
# ===== CHECK CHECKBOXES =====
print("Step 5: Checking Checkboxes")
print("-" * 70)
# Check the newsletter checkbox
page.check('#newsletter')
is_newsletter_checked = page.is_checked('#newsletter')
print(f"✓ Newsletter checkbox: {'✅ Checked' if is_newsletter_checked else '❌ Unchecked'}")
# Check the terms checkbox (required)
page.check('#terms')
is_terms_checked = page.is_checked('#terms')
print(f"✓ Terms checkbox: {'✅ Checked' if is_terms_checked else '❌ Unchecked'}")
print()
# ===== SCREENSHOT BEFORE SUBMISSION =====
print("Step 6: Capturing Filled Form")
print("-" * 70)
filled_screenshot = os.path.join(screenshots_dir, '02_form_filled.png')
page.screenshot(path=filled_screenshot, full_page=True)
print(f"📸 Screenshot (filled form): {filled_screenshot}")
print()
# ===== VERIFY FORM DATA =====
print("Step 7: Verifying Form Data Before Submission")
print("-" * 70)
# Read back the values to verify they were set correctly
name_verify = page.input_value('#name')
email_verify = page.input_value('#email')
phone_verify = page.input_value('#phone')
country_verify = page.input_value('#country')
message_verify = page.input_value('#message')
print("Form Data Summary:")
print(f" Name: {name_verify}")
print(f" Email: {email_verify}")
print(f" Phone: {phone_verify}")
print(f" Country: {country_verify}")
print(f" Source: {radio_value}")
print(f" Newsletter: {is_newsletter_checked}")
print(f" Terms: {is_terms_checked}")
print(f" Message: {len(message_verify)} characters")
print()
# ===== SUBMIT THE FORM =====
print("Step 8: Submitting the Form")
print("-" * 70)
# Click the submit button
submit_button = page.locator('button[type="submit"]')
submit_button.click()
# Wait a moment for JavaScript to process the submission
page.wait_for_timeout(500)
print("✓ Form submitted")
print()
# ===== VERIFY SUBMISSION SUCCESS =====
print("Step 9: Verifying Submission Success")
print("-" * 70)
# Check if the success message is visible
success_message = page.locator('#successMessage')
is_visible = success_message.is_visible()
print(f"Success message visible: {'✅ Yes' if is_visible else '❌ No'}")
if is_visible:
# Get the success message text
heading = page.locator('#successMessage h2').inner_text()
message = page.locator('#successMessage > p').inner_text()
print(f" Heading: {heading}")
print(f" Message: {message}")
# Verify the submitted data is displayed correctly
submitted_data = page.locator('#submittedData').inner_text()
print(f"\n Submitted data preview:")
for line in submitted_data.split('\n')[:3]:
print(f" {line}")
print()
# ===== SCREENSHOT AFTER SUBMISSION =====
print("Step 10: Capturing Success State")
print("-" * 70)
success_screenshot = os.path.join(screenshots_dir, '03_form_submitted.png')
page.screenshot(path=success_screenshot, full_page=True)
print(f"📸 Screenshot (after submission): {success_screenshot}")
print()
# ===== SUMMARY =====
print("=" * 70)
print("TEST SUMMARY")
print("=" * 70)
print("Actions Completed:")
print(" ✓ Filled 3 text inputs (name, email, phone)")
print(" ✓ Selected dropdown option (country)")
print(" ✓ Selected radio button (source)")
print(" ✓ Filled textarea (message)")
print(" ✓ Checked 2 checkboxes (newsletter, terms)")
print(" ✓ Submitted form")
print(" ✓ Verified success message")
print()
print("Screenshots Captured:")
print(f" 1. Empty form")
print(f" 2. Filled form (before submission)")
print(f" 3. Success page (after submission)")
print()
print(f"📁 Screenshots saved to: {screenshots_dir}")
browser.close()
print()
print("✅ Form automation test completed successfully!")
if __name__ == '__main__':
test_form_automation()
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamic Content Demo</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(to right, #6a11cb 0%, #2575fc 100%);
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
header {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
text-align: center;
margin-bottom: 30px;
}
header h1 {
color: #6a11cb;
font-size: 2.5em;
margin-bottom: 10px;
}
header p {
color: #666;
font-size: 1.1em;
}
.control-panel {
background: white;
padding: 25px;
border-radius: 15px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
margin-bottom: 30px;
}
.button-group {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
button {
flex: 1;
min-width: 150px;
padding: 15px 25px;
font-size: 1em;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-secondary {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.btn-success {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.content-area {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
min-height: 300px;
}
.loading {
text-align: center;
padding: 60px 20px;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #6a11cb;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
color: #666;
font-size: 1.2em;
}
.data-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
margin-top: 20px;
}
.data-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.data-card h3 {
margin-bottom: 10px;
font-size: 1.3em;
}
.data-card p {
font-size: 0.95em;
line-height: 1.5;
opacity: 0.9;
}
.timestamp {
background: #e8f4f8;
border-left: 4px solid #2575fc;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
color: #333;
}
.timestamp strong {
color: #2575fc;
}
.error-message {
background: #f8d7da;
border: 2px solid #f5c6cb;
color: #721c24;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state-icon {
font-size: 4em;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>⚡ Dynamic Content</h1>
<p>Testing JavaScript-Rendered Content with Playwright</p>
</header>
<div class="control-panel">
<div class="button-group">
<button class="btn-primary" id="loadData">Load Data (2s delay)</button>
<button class="btn-secondary" id="loadFast">Load Fast (instant)</button>
<button class="btn-success" id="loadMore">Load More Items</button>
<button id="clearData">Clear All Data</button>
</div>
</div>
<div class="content-area" id="contentArea">
<div class="empty-state">
<div class="empty-state-icon">📦</div>
<h2>No Data Loaded</h2>
<p>Click one of the buttons above to load dynamic content</p>
</div>
</div>
</div>
<script>
let currentData = [];
let loadCount = 0;
// Sample data generator
function generateData(count = 6) {
const items = [];
const topics = ['Web Testing', 'Automation', 'Performance', 'Security', 'API Testing', 'CI/CD', 'DevOps', 'Cloud'];
const adjectives = ['Advanced', 'Essential', 'Modern', 'Effective', 'Powerful', 'Comprehensive'];
for (let i = 0; i < count; i++) {
const topic = topics[Math.floor(Math.random() * topics.length)];
const adjective = adjectives[Math.floor(Math.random() * adjectives.length)];
items.push({
id: Date.now() + i,
title: `${adjective} ${topic}`,
description: `Learn about ${topic.toLowerCase()} strategies and best practices for modern web development.`,
timestamp: new Date().toISOString()
});
}
return items;
}
// Render data to the DOM
function renderData(data) {
const contentArea = document.getElementById('contentArea');
if (data.length === 0) {
contentArea.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📦</div>
<h2>No Data Loaded</h2>
<p>Click one of the buttons above to load dynamic content</p>
</div>
`;
return;
}
const timestamp = new Date().toLocaleString();
let html = `
<div class="timestamp">
<strong>Last Updated:</strong> ${timestamp}
</div>
<h2>Loaded Items (${data.length})</h2>
<div class="data-grid">
`;
data.forEach(item => {
html += `
<div class="data-card">
<h3>${item.title}</h3>
<p>${item.description}</p>
</div>
`;
});
html += '</div>';
contentArea.innerHTML = html;
}
// Show loading state
function showLoading() {
const contentArea = document.getElementById('contentArea');
contentArea.innerHTML = `
<div class="loading">
<div class="spinner"></div>
<div class="loading-text">Loading data...</div>
</div>
`;
}
// Load data with delay
document.getElementById('loadData').addEventListener('click', function() {
console.log('Loading data with 2-second delay...');
showLoading();
setTimeout(() => {
currentData = generateData(6);
renderData(currentData);
loadCount++;
console.log(`Data loaded successfully! (Load count: ${loadCount})`);
}, 2000);
});
// Load data instantly
document.getElementById('loadFast').addEventListener('click', function() {
console.log('Loading data instantly...');
showLoading();
// Use setTimeout with 0 to still trigger async behavior
setTimeout(() => {
currentData = generateData(4);
renderData(currentData);
loadCount++;
console.log(`Data loaded successfully! (Load count: ${loadCount})`);
}, 0);
});
// Load more items
document.getElementById('loadMore').addEventListener('click', function() {
console.log('Loading more items...');
const newItems = generateData(3);
currentData = [...currentData, ...newItems];
renderData(currentData);
console.log(`Added 3 more items. Total: ${currentData.length}`);
});
// Clear all data
document.getElementById('clearData').addEventListener('click', function() {
console.log('Clearing all data...');
currentData = [];
renderData(currentData);
loadCount = 0;
console.log('Data cleared.');
});
// Log initial page load
console.log('Dynamic content app initialized.');
console.log('Available actions: Load Data, Load Fast, Load More, Clear Data');
</script>
</body>
</html>