webapp-testing-patterns by bobmatnyc/claude-mpm-skills
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill webapp-testing-patternsPlaywright 自动化模式、选择器和最佳实践的完整指南。
当文本唯一时,最易读且最易维护的方法:
page.click('text=Login')
page.click('text="Sign Up"') # 精确匹配
page.click('text=/log.*in/i') # 正则表达式,不区分大小写
基于 ARIA 角色的语义选择器:
page.click('role=button[name="Submit"]')
page.fill('role=textbox[name="Email"]', 'user@example.com')
page.click('role=link[name="Learn more"]')
page.check('role=checkbox[name="Accept terms"]')
用于精确定位的传统 CSS 选择器:
page.click('#submit-button')
page.fill('.email-input', 'user@example.com')
page.click('button.primary')
page.click('nav > ul > li:first-child')
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
用于复杂的 DOM 导航:
page.click('xpath=//button[contains(text(), "Submit")]')
page.click('xpath=//div[@class="modal"]//button[@type="submit"]')
用于测试专用选择器的最佳实践:
page.click('[data-testid="submit-btn"]')
page.fill('[data-test="email-input"]', 'test@example.com')
组合选择器以提高精度:
page.locator('div.modal').locator('button.submit').click()
page.locator('role=dialog').locator('text=Confirm').click()
优先级顺序(从最稳定到最不稳定):
data-testid 属性(最稳定)role= 选择器(语义化,可访问)text= 选择器(可读性强,但文本可能更改)id 属性(如果非动态则稳定)对于动态应用程序至关重要:
# 等待网络空闲(最常见)
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
# 等待 DOM 准备就绪
page.wait_for_load_state('domcontentloaded')
# 等待完全加载,包括图片
page.wait_for_load_state('load')
在交互前等待特定元素:
# 等待元素可见
page.wait_for_selector('button.submit', state='visible')
# 等待元素隐藏
page.wait_for_selector('.loading-spinner', state='hidden')
# 等待元素存在于 DOM 中(可能不可见)
page.wait_for_selector('.modal', state='attached')
# 等待元素从 DOM 中移除
page.wait_for_selector('.error-message', state='detached')
固定时间延迟(谨慎使用):
# 等待动画完成
page.wait_for_timeout(500)
# 等待延迟内容(最好使用 wait_for_selector)
page.wait_for_timeout(2000)
等待 JavaScript 条件:
# 等待自定义 JavaScript 条件
page.wait_for_function('() => document.querySelector(".data").innerText !== "Loading..."')
# 等待变量被设置
page.wait_for_function('() => window.appReady === true')
Playwright 自动等待元素变为可操作状态:
# 这些操作会自动等待元素:
# - 可见
# - 稳定(无动画)
# - 启用(未禁用)
# - 未被其他元素遮挡
page.click('button.submit') # 自动等待
page.fill('input.email', 'test@example.com') # 自动等待
# 基本点击
page.click('button.submit')
# 带选项的点击
page.click('button.submit', button='right') # 右键点击
page.click('button.submit', click_count=2) # 双击
page.click('button.submit', modifiers=['Control']) # Ctrl+点击
# 强制点击(绕过可操作性检查)
page.click('button.submit', force=True)
# 文本输入框
page.fill('input[name="email"]', 'user@example.com')
page.type('input[name="search"]', 'query', delay=100) # 带延迟的输入
# 先清空再填写
page.fill('input[name="email"]', '')
page.fill('input[name="email"]', 'new@example.com')
# 按键
page.press('input[name="search"]', 'Enter')
page.press('input[name="text"]', 'Control+A')
# 按标签选择
page.select_option('select[name="country"]', label='United States')
# 按值选择
page.select_option('select[name="country"]', value='us')
# 按索引选择
page.select_option('select[name="country"]', index=2)
# 选择多个选项
page.select_option('select[multiple]', ['option1', 'option2'])
# 勾选复选框
page.check('input[type="checkbox"]')
# 取消勾选复选框
page.uncheck('input[type="checkbox"]')
# 选择单选按钮
page.check('input[value="option1"]')
# 切换复选框状态
if page.is_checked('input[type="checkbox"]'):
page.uncheck('input[type="checkbox"]')
else:
page.check('input[type="checkbox"]')
# 上传单个文件
page.set_input_files('input[type="file"]', '/path/to/file.pdf')
# 上传多个文件
page.set_input_files('input[type="file"]', ['/path/to/file1.pdf', '/path/to/file2.pdf'])
# 清空文件输入框
page.set_input_files('input[type="file"]', [])
# 悬停在元素上
page.hover('button.tooltip-trigger')
# 聚焦元素
page.focus('input[name="email"]')
# 失焦元素
page.evaluate('document.activeElement.blur()')
from playwright.sync_api import expect
# 期望元素可见
expect(page.locator('button.submit')).to_be_visible()
# 期望元素隐藏
expect(page.locator('.error-message')).to_be_hidden()
# 期望精确文本
expect(page.locator('.title')).to_have_text('Welcome')
# 期望包含文本
expect(page.locator('.message')).to_contain_text('success')
# 期望文本匹配模式
expect(page.locator('.code')).to_have_text(re.compile(r'\d{6}'))
# 期望元素启用/禁用
expect(page.locator('button.submit')).to_be_enabled()
expect(page.locator('button.submit')).to_be_disabled()
# 期望复选框被勾选
expect(page.locator('input[type="checkbox"]')).to_be_checked()
# 期望元素可编辑
expect(page.locator('input[name="email"]')).to_be_editable()
# 期望属性值
expect(page.locator('img')).to_have_attribute('src', '/logo.png')
# 期望 CSS 类
expect(page.locator('button')).to_have_class('btn-primary')
# 期望输入值
expect(page.locator('input[name="email"]')).to_have_value('user@example.com')
# 期望特定数量
expect(page.locator('li')).to_have_count(5)
# 获取所有元素并断言
items = page.locator('li').all()
assert len(items) == 5
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# 测试逻辑在此处
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
browser.close()
import pytest
from playwright.sync_api import sync_playwright
@pytest.fixture(scope="session")
def browser():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
yield browser
browser.close()
@pytest.fixture
def page(browser):
page = browser.new_page()
yield page
page.close()
def test_login(page):
page.goto('http://localhost:3000')
page.fill('input[name="email"]', 'user@example.com')
page.fill('input[name="password"]', 'password123')
page.click('button[type="submit"]')
expect(page.locator('.welcome-message')).to_be_visible()
class TestAuthentication:
def test_successful_login(self, page):
# 测试成功登录
pass
def test_failed_login(self, page):
# 测试登录失败
pass
def test_logout(self, page):
# 测试登出
pass
@pytest.fixture(autouse=True)
def setup_and_teardown(page):
# 设置 - 在每个测试前运行
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
yield # 测试在此处运行
# 清理 - 在每个测试后运行
page.evaluate('localStorage.clear()')
# 拦截并模拟 API 响应
def handle_route(route):
route.fulfill(
status=200,
body='{"success": true, "data": "mocked"}',
headers={'Content-Type': 'application/json'}
)
page.route('**/api/data', handle_route)
page.goto('http://localhost:3000')
# 阻止图片和样式表以加速测试
page.route('**/*.{png,jpg,jpeg,gif,svg,css}', lambda route: route.abort())
# 等待特定的 API 调用
with page.expect_response('**/api/users') as response_info:
page.click('button.load-users')
response = response_info.value
assert response.status == 200
# 全页面截图
page.screenshot(path='/tmp/screenshot.png', full_page=True)
# 元素截图
page.locator('.modal').screenshot(path='/tmp/modal.png')
# 自定义尺寸的截图
page.set_viewport_size({'width': 1920, 'height': 1080})
page.screenshot(path='/tmp/desktop.png')
browser = p.chromium.launch(headless=True)
context = browser.new_context(record_video_dir='/tmp/videos/')
page = context.new_page()
# 执行操作...
context.close() # 关闭时保存视频
page.pause() # 打开 Playwright 检查器
def handle_console(msg):
print(f"[{msg.type}] {msg.text}")
page.on("console", handle_console)
browser = p.chromium.launch(headless=False, slow_mo=1000) # 1 秒延迟
# 设置 DEBUG 环境变量
# DEBUG=pw:api python test.py
# 安装 pytest-xdist
pip install pytest-xdist
# 并行运行测试
pytest -n auto # 自动检测 CPU 核心数
pytest -n 4 # 使用 4 个工作进程运行
# 每个测试获得隔离的上下文(cookies、localStorage 等)
@pytest.fixture
def context(browser):
context = browser.new_context()
yield context
context.close()
@pytest.fixture
def page(context):
return context.new_page()
每周安装量
70
仓库
GitHub 星标数
22
首次出现
2026年1月23日
安全审计
安装于
opencode53
claude-code52
gemini-cli52
codex52
github-copilot48
cursor46
Complete guide to Playwright automation patterns, selectors, and best practices.
Most readable and maintainable approach when text is unique:
page.click('text=Login')
page.click('text="Sign Up"') # Exact match
page.click('text=/log.*in/i') # Regex, case-insensitive
Semantic selectors based on ARIA roles:
page.click('role=button[name="Submit"]')
page.fill('role=textbox[name="Email"]', 'user@example.com')
page.click('role=link[name="Learn more"]')
page.check('role=checkbox[name="Accept terms"]')
Traditional CSS selectors for precise targeting:
page.click('#submit-button')
page.fill('.email-input', 'user@example.com')
page.click('button.primary')
page.click('nav > ul > li:first-child')
For complex DOM navigation:
page.click('xpath=//button[contains(text(), "Submit")]')
page.click('xpath=//div[@class="modal"]//button[@type="submit"]')
Best practice for test-specific selectors:
page.click('[data-testid="submit-btn"]')
page.fill('[data-test="email-input"]', 'test@example.com')
Combine selectors for precision:
page.locator('div.modal').locator('button.submit').click()
page.locator('role=dialog').locator('text=Confirm').click()
Priority order (most stable to least stable):
data-testid attributes (most stable)role= selectors (semantic, accessible)text= selectors (readable, but text may change)id attributes (stable if not dynamic)Essential for dynamic applications:
# Wait for network to be idle (most common)
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
# Wait for DOM to be ready
page.wait_for_load_state('domcontentloaded')
# Wait for full load including images
page.wait_for_load_state('load')
Wait for specific elements before interacting:
# Wait for element to be visible
page.wait_for_selector('button.submit', state='visible')
# Wait for element to be hidden
page.wait_for_selector('.loading-spinner', state='hidden')
# Wait for element to exist in DOM (may not be visible)
page.wait_for_selector('.modal', state='attached')
# Wait for element to be removed from DOM
page.wait_for_selector('.error-message', state='detached')
Fixed time delays (use sparingly):
# Wait for animations to complete
page.wait_for_timeout(500)
# Wait for delayed content (better to use wait_for_selector)
page.wait_for_timeout(2000)
Wait for JavaScript conditions:
# Wait for custom JavaScript condition
page.wait_for_function('() => document.querySelector(".data").innerText !== "Loading..."')
# Wait for variable to be set
page.wait_for_function('() => window.appReady === true')
Playwright automatically waits for elements to be actionable:
# These automatically wait for element to be:
# - Visible
# - Stable (not animating)
# - Enabled (not disabled)
# - Not obscured by other elements
page.click('button.submit') # Auto-waits
page.fill('input.email', 'test@example.com') # Auto-waits
# Basic click
page.click('button.submit')
# Click with options
page.click('button.submit', button='right') # Right-click
page.click('button.submit', click_count=2) # Double-click
page.click('button.submit', modifiers=['Control']) # Ctrl+click
# Force click (bypass actionability checks)
page.click('button.submit', force=True)
# Text inputs
page.fill('input[name="email"]', 'user@example.com')
page.type('input[name="search"]', 'query', delay=100) # Type with delay
# Clear then fill
page.fill('input[name="email"]', '')
page.fill('input[name="email"]', 'new@example.com')
# Press keys
page.press('input[name="search"]', 'Enter')
page.press('input[name="text"]', 'Control+A')
# Select by label
page.select_option('select[name="country"]', label='United States')
# Select by value
page.select_option('select[name="country"]', value='us')
# Select by index
page.select_option('select[name="country"]', index=2)
# Select multiple options
page.select_option('select[multiple]', ['option1', 'option2'])
# Check a checkbox
page.check('input[type="checkbox"]')
# Uncheck a checkbox
page.uncheck('input[type="checkbox"]')
# Check a radio button
page.check('input[value="option1"]')
# Toggle checkbox
if page.is_checked('input[type="checkbox"]'):
page.uncheck('input[type="checkbox"]')
else:
page.check('input[type="checkbox"]')
# Upload single file
page.set_input_files('input[type="file"]', '/path/to/file.pdf')
# Upload multiple files
page.set_input_files('input[type="file"]', ['/path/to/file1.pdf', '/path/to/file2.pdf'])
# Clear file input
page.set_input_files('input[type="file"]', [])
# Hover over element
page.hover('button.tooltip-trigger')
# Focus element
page.focus('input[name="email"]')
# Blur element
page.evaluate('document.activeElement.blur()')
from playwright.sync_api import expect
# Expect element to be visible
expect(page.locator('button.submit')).to_be_visible()
# Expect element to be hidden
expect(page.locator('.error-message')).to_be_hidden()
# Expect exact text
expect(page.locator('.title')).to_have_text('Welcome')
# Expect partial text
expect(page.locator('.message')).to_contain_text('success')
# Expect text matching pattern
expect(page.locator('.code')).to_have_text(re.compile(r'\d{6}'))
# Expect element to be enabled/disabled
expect(page.locator('button.submit')).to_be_enabled()
expect(page.locator('button.submit')).to_be_disabled()
# Expect checkbox to be checked
expect(page.locator('input[type="checkbox"]')).to_be_checked()
# Expect element to be editable
expect(page.locator('input[name="email"]')).to_be_editable()
# Expect attribute value
expect(page.locator('img')).to_have_attribute('src', '/logo.png')
# Expect CSS class
expect(page.locator('button')).to_have_class('btn-primary')
# Expect input value
expect(page.locator('input[name="email"]')).to_have_value('user@example.com')
# Expect specific count
expect(page.locator('li')).to_have_count(5)
# Get all elements and assert
items = page.locator('li').all()
assert len(items) == 5
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Test logic here
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
browser.close()
import pytest
from playwright.sync_api import sync_playwright
@pytest.fixture(scope="session")
def browser():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
yield browser
browser.close()
@pytest.fixture
def page(browser):
page = browser.new_page()
yield page
page.close()
def test_login(page):
page.goto('http://localhost:3000')
page.fill('input[name="email"]', 'user@example.com')
page.fill('input[name="password"]', 'password123')
page.click('button[type="submit"]')
expect(page.locator('.welcome-message')).to_be_visible()
class TestAuthentication:
def test_successful_login(self, page):
# Test successful login
pass
def test_failed_login(self, page):
# Test failed login
pass
def test_logout(self, page):
# Test logout
pass
@pytest.fixture(autouse=True)
def setup_and_teardown(page):
# Setup - runs before each test
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle')
yield # Test runs here
# Teardown - runs after each test
page.evaluate('localStorage.clear()')
# Intercept and mock API response
def handle_route(route):
route.fulfill(
status=200,
body='{"success": true, "data": "mocked"}',
headers={'Content-Type': 'application/json'}
)
page.route('**/api/data', handle_route)
page.goto('http://localhost:3000')
# Block images and stylesheets for faster tests
page.route('**/*.{png,jpg,jpeg,gif,svg,css}', lambda route: route.abort())
# Wait for specific API call
with page.expect_response('**/api/users') as response_info:
page.click('button.load-users')
response = response_info.value
assert response.status == 200
# Full page screenshot
page.screenshot(path='/tmp/screenshot.png', full_page=True)
# Element screenshot
page.locator('.modal').screenshot(path='/tmp/modal.png')
# Screenshot with custom dimensions
page.set_viewport_size({'width': 1920, 'height': 1080})
page.screenshot(path='/tmp/desktop.png')
browser = p.chromium.launch(headless=True)
context = browser.new_context(record_video_dir='/tmp/videos/')
page = context.new_page()
# Perform actions...
context.close() # Video saved on close
page.pause() # Opens Playwright Inspector
def handle_console(msg):
print(f"[{msg.type}] {msg.text}")
page.on("console", handle_console)
browser = p.chromium.launch(headless=False, slow_mo=1000) # 1 second delay
# Set DEBUG environment variable
# DEBUG=pw:api python test.py
# Install pytest-xdist
pip install pytest-xdist
# Run tests in parallel
pytest -n auto # Auto-detect CPU cores
pytest -n 4 # Run with 4 workers
# Each test gets isolated context (cookies, localStorage, etc.)
@pytest.fixture
def context(browser):
context = browser.new_context()
yield context
context.close()
@pytest.fixture
def page(context):
return context.new_page()
Weekly Installs
70
Repository
GitHub Stars
22
First Seen
Jan 23, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode53
claude-code52
gemini-cli52
codex52
github-copilot48
cursor46
Skills CLI 使用指南:AI Agent 技能包管理器安装与管理教程
44,900 周安装
Assess技能:AI代码评估工具,提供结构化评分与改进建议 | Claude钩子集成
77 周安装
verify 技能:AI驱动代码验证工具 - 并行代理测试、安全审计与质量评分
77 周安装
Tailwind CSS 4 最佳实践指南:样式决策树、cn()函数使用与核心规则
77 周安装
OpenAI Agents Python 最终发布评审指南:自动化发布流程与风险管理
77 周安装
API安全加固指南:多层防护REST API,防御常见漏洞攻击(Express/Node.js)
77 周安装
前端设计技能:告别AI垃圾美学,创建独特、生产级前端界面与代码
77 周安装