Complete Playwright test authoring workflow with Verdex MCP tools. Three phases - explore apps interactively to map user journeys, build stable role-first selectors via progressive DOM exploration, write idiomatic Playwright tests. Includes workflow discovery, selector patterns, and testing best practices.
Complete test authoring in three phases: Explore → Select → Test
Use this skill when you need to:
.nth() selectorsGoal: Understand the application and user journeys before writing any code
When to read the detailed guide: When starting work on a new application or feature
browser_navigate, browser_click, browser_snapshot) with refsGoal: Convert exploration discoveries into production-ready Playwright selectors
When to read the detailed guide: After workflow exploration, when you have refs and need selectors
resolve_container, inspect_pattern, extract_anchors)Goal: Transform selectors and workflows into clean, maintainable Playwright tests
When to read the detailed guide: When you have selectors and workflows ready to code
await browser_initialize()
await browser_navigate("https://your-app.com")
// Snapshot shows interactive elements with refs (e1, e2, e3...)
// Click and interact using refs to explore
await browser_click("e5")
await browser_snapshot() // See what changed
Document what you learn: URLs, actions, expected outcomes
Check uniqueness FIRST:
// Is element unique? If YES → use simple selector
page.getByRole("button", { name: "Proceed to Checkout" })
// If NO → use Container → Content → Role pattern
page
.getByTestId("product-card")
.filter({ hasText: "iPhone 15 Pro" })
.getByRole("button", { name: "Add to Cart" })
Progressive exploration when needed:
resolve_container("e25") // Find stable containers
inspect_pattern("e25", 2) // Understand siblings
extract_anchors("e25", 1) // Mine deep content (optional)
test('should add product to cart', async ({ page }) => {
// Arrange
await page.goto('/products')
// Act
await page
.getByTestId('product-card')
.filter({ hasText: 'iPhone 15 Pro' })
.getByRole('button', { name: 'Add to Cart' })
.click()
// Assert
await expect(page.getByText('Item added to cart')).toBeVisible()
await expect(page.getByRole('link', { name: /Cart \(1\)/ })).toBeVisible()
})
page
.getByTestId("stable-container") // 1. Scope to container
.filter({ hasText: "unique-content" }) // 2. Filter by content
.getByRole("button", { name: "Action" }) // 3. Target by role
// ❌ NEVER: Positional selectors
page.getByRole("button").nth(5)
page.getByRole("button").first() // .first() = .nth(0), still brittle
// ❌ NEVER: Filter in wrong place
page.getByRole("button").filter({ hasText: "text-in-sibling" })
// ❌ NEVER: Parent traversal
page.getByText("text").locator("..")
// ✅ ALWAYS: Check uniqueness first
const count = await page.getByRole("button", { name: "Submit" }).count()
// If count === 1, use simple selector. If > 1, add container scoping.
Are you starting work on a new app/feature?
├─ YES → Read guides/workflow-discovery.md
│ Learn: Interactive exploration, mapping user journeys
│ Tools: browser_navigate, browser_click, browser_snapshot
│ Output: Workflow documentation with refs
│
└─ NO → Do you have refs and need to build selectors?
├─ YES → Read guides/selector-writing.md
│ Learn: Container → Content → Role pattern
│ Tools: resolve_container, inspect_pattern, extract_anchors
│ Output: Stable Playwright selectors
│
└─ NO → Ready to write test code?
└─ YES → Read guides/playwright-patterns.md
Learn: Test structure, assertions, best practices
Output: Production-ready Playwright tests
Take browser_snapshot() first, then ask:
Is my target element unique on the page?
├─ YES → Use simple selector ✅
│ page.getByRole("button", { name: "Unique Text" })
│ Done! No exploration needed.
│
└─ NO → Element appears multiple times
│
Call resolve_container(ref) to find stable containers
│
Found data-testid or semantic element?
├─ YES → Use as container scope
│ │
│ Call inspect_pattern(ref, level) to see siblings
│ │
│ Found unique text to filter by?
│ ├─ YES → Build selector ✅
│ │ page.getByTestId("container")
│ │ .filter({ hasText: "unique" })
│ │ .getByRole("button")
│ │
│ └─ NO → Call extract_anchors(ref, level) for deeper analysis
│ Then build selector with discovered anchors
│
└─ NO → Found only generic divs
Use locator("div").filter() pattern (last resort)
| Scenario | Tools Needed | Avg Cost | When to Use | |----------|-------------|----------|-------------| | Unique element | Snapshot only | ~800 | Element appears once on page | | Test ID scoped | Snapshot + resolve + inspect | ~1,200 | Well-structured apps with test IDs | | Semantic scoped | Snapshot + resolve + inspect | ~1,500 | Apps with good semantic HTML | | Structure-based | Snapshot + resolve + inspect + extract | ~2,500 | Legacy code, poorly structured DOM |
Optimization tip: Always check uniqueness first. Skip extract_anchors if inspect_pattern shows clear unique text.
// === EXPLORATION PHASE ===
await browser_initialize() // Start fresh browser
await browser_navigate("https://app.com") // Go to URL, auto-snapshots
await browser_snapshot() // See current page state with refs
await browser_click("e5") // Click element by ref
await browser_type("e1", "text@example.com") // Type into input by ref
await wait_for_browser(2000) // Wait 2 seconds for dynamic content
// === SELECTOR BUILDING PHASE ===
resolve_container("e25") // Find stable containers (test IDs, landmarks)
inspect_pattern("e25", 2) // Analyze sibling structure at level
extract_anchors("e25", 1) // Mine deep content (use if inspect insufficient)
// Pattern 1: Unique element (simplest)
page.getByRole("button", { name: "Proceed to Checkout" })
// Pattern 2: Test ID container (most common)
page
.getByTestId("product-card")
.filter({ hasText: "iPhone 15 Pro" })
.getByRole("button", { name: "Add to Cart" })
// Pattern 3: Semantic container
page
.getByRole("article", { name: /John Doe/ })
.getByRole("button", { name: "Helpful" })
// Pattern 4: Generic container (last resort)
page
.locator("div")
.filter({ hasText: "Order #ORD-2024-1234" })
.getByRole("button", { name: "Track" })
test.describe('Feature Name', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/starting-point')
})
test('should do specific thing', async ({ page }) => {
// Arrange: Set up state
await expect(page.getByRole('heading', { name: 'Title' })).toBeVisible()
// Act: Perform action
await page.getByRole('button', { name: 'Submit' }).click()
// Assert: Verify outcome
await expect(page).toHaveURL(/success/)
await expect(page.getByText('Success message')).toBeVisible()
})
})
Before finalizing any selector:
.count())getByRole() or semantic locator.nth(), .first(), .last(), or locator('..')Start here and read guides as needed:
guides/workflow-discovery.md - Interactive exploration and journey mapping
guides/selector-writing.md - Building stable selectors
guides/playwright-patterns.md - Writing idiomatic tests
Container → Content → Role beats positional selectors every time.
Complete workflow:
resolve_container for stable scopinginspect_pattern for sibling analysisextract_anchors only when neededDon't dump the entire DOM. Ask targeted questions with progressive disclosure.
npx skills add verdexhq/verdex-playwright-complete下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer