mcpField
W
webapp-testing

webapp-testing

github.com/bobmatnyc测试与质量78445 次安装15 周前更新
代码仓库

关于 webapp-testing

面向 Web 应用测试开发者的 Playwright 参考技能,汇集选择器、交互、断言、调试和执行模式。

主要功能

  1. 01选择器策略
  2. 02等待条件与元素交互
  3. 03断言与测试组织
  4. 04网络拦截
  5. 05截图与调试
  6. 06并行执行测试

适用场景

  1. 01为 Web 测试选择稳定选择器
  2. 02同步动态页面测试时序
  3. 03测试时模拟 API 响应
  4. 04捕获故障以便调试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill webapp-testing

安装后在 Claude Code 中输入/webapp-testing确认已加载。

附带文件metadata.json

详细介绍

Playwright 模式参考

Playwright 自动化模式、选择器和最佳实践完整指南。

目录

选择器

文本选择器

当文本唯一时,这是最易读、最易维护的方式:

page.click('text=Login')
page.click('text="Sign Up"')  # Exact match
page.click('text=/log.*in/i')  # Regex, case-insensitive
基于角色的选择器

基于 ARIA 角色的语义选择器:

page.click('role=button[name="Submit"]')
page.fill('role=textbox[name="Email"]', '[email protected]')
page.click('role=link[name="Learn more"]')
page.check('role=checkbox[name="Accept terms"]')
CSS 选择器

用于精确定位的传统 CSS 选择器:

page.click('#submit-button')
page.fill('.email-input', '[email protected]')
page.click('button.primary')
page.click('nav > ul > li:first-child')
XPath 选择器

用于复杂的 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"]', '[email protected]')
链式选择器

组合选择器以实现精准定位:

page.locator('div.modal').locator('button.submit').click()
page.locator('role=dialog').locator('text=Confirm').click()
选择器最佳实践

优先级顺序(从最稳定到最不稳定):

  1. data-testid 属性(最稳定)
  2. role= 选择器(语义化,易于访问)
  3. text= 选择器(易读,但文本可能会变化)
  4. id 属性(若不是动态值,则较稳定)
  5. CSS 类(稳定性较低,可能随样式变化)
  6. XPath(脆弱,尽可能避免)

等待策略

加载状态等待

对动态应用至关重要:

# 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 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')
超时等待

固定时间延迟(谨慎使用):

# 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)
自定义等待条件

等待 JavaScript 条件:

# 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 会自动等待元素达到可操作状态:

# 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', '[email protected]')  # 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"]', '[email protected]')
page.type('input[name="search"]', 'query', delay=100)  # Type with delay

# Clear then fill
page.fill('input[name="email"]', '')
page.fill('input[name="email"]', '[email protected]')

# 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('[email protected]')
数量和集合
# 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()
使用 Pytest(推荐)
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"]', '[email protected]')
    page.fill('input[name="password"]', 'password123')
    page.click('button[type="submit"]')
    expect(page.locator('.welcome-message')).to_be_visible()
使用 Describe 块对测试分组
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()')

网络拦截

模拟 API 响应
# 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

并行执行

Pytest 并行
# 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()

评论

支持 Markdown