// 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.
| name | verdex-playwright-complete |
| description | 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.