claude-skills/engineering-team/playwright-pro/skills/pw/reference/locators.md
Reza Rezvani 1851c8fb09 fix(plugins): restructure 9 multi-skill domain plugins into ./skills/ layout
Same root cause as #587/#591 — Claude Code's runtime loader rejects
array-form skills paths like ["./content-production", "./ai-seo", ...]
even when each entry is a valid subdirectory containing SKILL.md.
`claude plugin validate` accepts them but the loader does not.

The proven canonical layout (used by self-improving-agent in #536):

  <plugin>/
  ├── .claude-plugin/plugin.json    skills: "./skills"
  └── skills/
      ├── <skill-1>/SKILL.md
      ├── <skill-2>/SKILL.md
      └── ...

Restructured 9 multi-skill domain plugins:
- business-growth (4 skills moved)
- c-level-advisor (28)
- engineering (36)
- engineering-team (32)
- finance (2)
- marketing-skill (43)
- product-team (12)
- project-management (8)
- ra-qm-team (13)

Also fixed standalone plugins that had root SKILL.md + ./skills/ subdir
(agenthub, autoresearch-agent, executive-mentor, playwright-pro). The
loader rejected them despite skills="./skills" because of the conflicting
root SKILL.md (compare self-improving-agent which works because PR #536
moved its root SKILL.md). Moved each root SKILL.md into ./skills/<name>/.

Restored standalone plugin folders to their original paths after the
multi-skill restructure swept them into parent skills/ directories
(marketplace.json source paths require original locations).

Removed 7 orphaned marketplace entries that pointed to skill folders
without their own plugin.json (content-creator, demand-gen,
fullstack-engineer, aws-architect, product-manager, scrum-master,
skill-security-auditor) — these were already non-functional.

Bumped patch versions on every changed plugin and synced
marketplace.json. Marketplace now lists 29 working plugins (down
from 36).

After merge: users run `/plugin marketplace update claude-code-skills`
followed by `/plugin update --all` to pick up the working layout.
2026-05-02 22:51:20 +02:00

2.1 KiB

Locator Priority

Use the first option that works:

Priority Locator Use for
1 getByRole('button', { name: 'Submit' }) Buttons, links, headings, form elements
2 getByLabel('Email address') Form fields with associated labels
3 getByText('Welcome back') Non-interactive text content
4 getByPlaceholder('Search...') Inputs with placeholder text
5 getByAltText('Company logo') Images with alt text
6 getByTitle('Close dialog') Elements with title attribute
7 getByTestId('checkout-summary') When no semantic option exists
8 page.locator('.legacy-widget') CSS/XPath — absolute last resort

Role Locator Cheat Sheet

// Buttons — <button>, <input type="submit">, [role="button"]
page.getByRole('button', { name: 'Save changes' })

// Links — <a href>
page.getByRole('link', { name: 'View profile' })

// Headings — h1-h6
page.getByRole('heading', { name: 'Dashboard', level: 1 })

// Text inputs — by label association
page.getByRole('textbox', { name: 'Email' })

// Checkboxes
page.getByRole('checkbox', { name: 'Remember me' })

// Radio buttons
page.getByRole('radio', { name: 'Monthly billing' })

// Dropdowns — <select>
page.getByRole('combobox', { name: 'Country' })

// Navigation
page.getByRole('navigation', { name: 'Main' })

// Tables
page.getByRole('table', { name: 'Recent orders' })

// Rows within tables
page.getByRole('row', { name: /Order #123/ })

// Tab panels
page.getByRole('tab', { name: 'Settings' })

// Dialogs
page.getByRole('dialog', { name: 'Confirm deletion' })

// Alerts
page.getByRole('alert')

Filtering and Chaining

// Filter by text
page.getByRole('listitem').filter({ hasText: 'Product A' })

// Filter by child locator
page.getByRole('listitem').filter({
  has: page.getByRole('button', { name: 'Buy' })
})

// Chain locators
page.getByRole('navigation').getByRole('link', { name: 'Settings' })

// Nth match
page.getByRole('listitem').nth(0)
page.getByRole('listitem').first()
page.getByRole('listitem').last()