aboutsummaryrefslogtreecommitdiff
path: root/playwright-py
diff options
context:
space:
mode:
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