aboutsummaryrefslogtreecommitdiff
path: root/playwright-py
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-22 15:48:48 -0500
committerCraig Jennings <c@cjennings.net>2026-05-22 15:48:48 -0500
commit8c291b81cd7fb10479a55fb47e9a9cebcfc1b9b8 (patch)
tree9f4d2fc7d3dff7f5404576759fee9d4a44f74b73 /playwright-py
parente6f8db82fb3e97ebf11866de2166ff4505871c21 (diff)
downloadrulesets-8c291b81cd7fb10479a55fb47e9a9cebcfc1b9b8.tar.gz
rulesets-8c291b81cd7fb10479a55fb47e9a9cebcfc1b9b8.zip
refactor(skills): locator-first playwright guidance, drop emoji markers
Two cleanups to the playwright skills, landed together since they overlap the same files. The skills taught networkidle as the readiness check and leaned on raw page.click/fill/waitForSelector. Playwright discourages networkidle for readiness, so the guidance in both SKILL.md files now waits for a visible app landmark via a web assertion or locator, the login and form examples use getByLabel/getByRole plus expect, the API reference leads with that pattern, and lib/helpers.js defaults waitForPageReady to load (preferring a caller-supplied landmark) and races the success indicator in authenticate instead of waiting on networkidle. The second cleanup strips emoji console markers across run.js, helpers.js, both SKILL.md files, and the py examples, replacing each with a plain ASCII tag like [ok], [error], or [scan]. node --check and py_compile pass, and an emoji grep comes back clean.
Diffstat (limited to 'playwright-py')
-rw-r--r--playwright-py/SKILL.md15
-rw-r--r--playwright-py/examples/broken_links.py6
-rw-r--r--playwright-py/examples/login_flow.py2
-rw-r--r--playwright-py/examples/responsive_sweep.py2
4 files changed, 13 insertions, 12 deletions
diff --git a/playwright-py/SKILL.md b/playwright-py/SKILL.md
index 0ed912b..54e1cb7 100644
--- a/playwright-py/SKILL.md
+++ b/playwright-py/SKILL.md
@@ -26,7 +26,8 @@ User task → Is it static HTML?
│ Then use the helper + write simplified Playwright script
└─ Yes → Reconnaissance-then-action:
- 1. Navigate and wait for networkidle
+ 1. Navigate and wait for a visible app landmark
+ (expect(page.get_by_role('main')).to_be_visible())
2. Take screenshot or inspect DOM
3. Identify selectors from rendered state
4. Execute actions with discovered selectors
@@ -51,13 +52,13 @@ python scripts/with_server.py \
To create an automation script, include only Playwright logic (servers are managed automatically):
```python
-from playwright.sync_api import sync_playwright
+from playwright.sync_api import sync_playwright, expect
with sync_playwright() as p:
browser = p.chromium.launch(headless=True) # headless for CI/pytest; headless=False for interactive debugging
page = browser.new_page()
page.goto('http://localhost:5173') # Server already running and ready
- page.wait_for_load_state('networkidle') # CRITICAL: Wait for JS to execute
+ expect(page.get_by_role('main')).to_be_visible() # wait for a visible app landmark, not network quiet
# ... your automation logic
browser.close()
```
@@ -77,16 +78,16 @@ with sync_playwright() as p:
## Common Pitfall
-❌ **Don't** inspect the DOM before waiting for `networkidle` on dynamic apps
-✅ **Do** wait for `page.wait_for_load_state('networkidle')` before inspection
+**Don't** inspect the DOM before the app has rendered on a dynamic page — you get stale content or an empty skeleton.
+**Do** wait for a visible, app-specific landmark before inspecting: `expect(page.get_by_role('main')).to_be_visible()` or `page.get_by_text('Dashboard').wait_for()`. These auto-wait for the element to appear, which is what "ready" means. Avoid `page.wait_for_load_state('networkidle')` as the readiness check — Playwright discourages it, since a page can be interactive long before the network quiets (or never quiet at all, with polling or analytics).
## Best Practices
- **Use bundled scripts as black boxes** - To accomplish a task, consider whether one of the scripts available in `scripts/` can help. These scripts handle common, complex workflows reliably without cluttering the context window. Use `--help` to see usage, then invoke directly.
- Use `sync_playwright()` for synchronous scripts
- Always close the browser when done
-- Use descriptive selectors: `text=`, `role=`, CSS selectors, or IDs
-- Add appropriate waits: `page.wait_for_selector()` or `page.wait_for_timeout()`
+- Prefer user-visible locators: `page.get_by_role(...)`, `page.get_by_label(...)`, `page.get_by_text(...)`. Fall back to CSS/`text=` selectors only when those don't fit.
+- For readiness, lead with web assertions and locator waits — `expect(locator).to_be_visible()`, `locator.wait_for()` — which auto-wait for a real, visible condition. Reach for `page.wait_for_selector()` only when a locator won't express the wait. Avoid `wait_for_load_state('networkidle')` as a readiness check (Playwright discourages it) and avoid fixed `page.wait_for_timeout()` delays.
- **Choose headed vs headless by purpose, not habit.** This skill defaults to *headless* (`headless=True`) because it targets CI and pytest. The companion `/playwright-js` defaults to *headed* for interactive visual debugging. Pick by what you're doing, and only override when the purpose flips:
| Purpose | Mode |
diff --git a/playwright-py/examples/broken_links.py b/playwright-py/examples/broken_links.py
index c78520f..a292f74 100644
--- a/playwright-py/examples/broken_links.py
+++ b/playwright-py/examples/broken_links.py
@@ -41,13 +41,13 @@ def main() -> int:
status = resp.status
if status < 400:
ok += 1
- print(f"✓ {status} {url}")
+ print(f"[ok] {status} {url}")
else:
bad += 1
- print(f"✗ {status} {url}")
+ print(f"[fail] {status} {url}")
except Exception as ex:
err += 1
- print(f"✗ ERR {url} ({type(ex).__name__}: {ex})")
+ print(f"[fail] ERR {url} ({type(ex).__name__}: {ex})")
print(f"\n{ok} ok, {bad} broken, {err} errored out of {len(urls)} total")
browser.close()
diff --git a/playwright-py/examples/login_flow.py b/playwright-py/examples/login_flow.py
index d114ac6..6d2fa45 100644
--- a/playwright-py/examples/login_flow.py
+++ b/playwright-py/examples/login_flow.py
@@ -45,7 +45,7 @@ def main() -> int:
safe_click(page, 'button[type="submit"]')
page.wait_for_url("**/dashboard", timeout=5000)
- print(f"✓ Logged in; redirected to {page.url}")
+ print(f"[ok] Logged in; redirected to {page.url}")
browser.close()
return 0
diff --git a/playwright-py/examples/responsive_sweep.py b/playwright-py/examples/responsive_sweep.py
index d890d5b..eb6e216 100644
--- a/playwright-py/examples/responsive_sweep.py
+++ b/playwright-py/examples/responsive_sweep.py
@@ -41,7 +41,7 @@ def main() -> int:
page.wait_for_load_state("networkidle")
path = OUTPUT_DIR / f"responsive-{name}.png"
page.screenshot(path=str(path), full_page=True)
- print(f"✓ {name:<8} ({width:>4}x{height:<4}) → {path}")
+ print(f"[ok] {name:<8} ({width:>4}x{height:<4}) -> {path}")
context.close()
browser.close()
return 0