| name | playwright-tool |
| description | Browser automation library with reusable functions and test scripts |
Playwright Tool
Library Location: ~/.config/opencode/tools/playwright_tool/
Project Entry Points: {project_root}/.opencode/scripts/grant_perm.py, {project_root}/.opencode/scripts/playwright/
Overview
The Playwright Tool provides a browser automation library for testing web applications. It consists of reusable library functions and a structured folder for disposable test scripts.
Key Principles
- Library + Scripts Pattern: Library provides building blocks, scripts are disposable test code
- No Persistent State: Each script starts fresh, browser closes when script ends
- Absolute Imports: Scripts import from library using absolute paths
- Script Discovery: Scripts live in
scripts/ folder with descriptive names
Core Architecture
File Structure
Library (~/.config/opencode/tools/playwright_tool/lib/):
lib/
├── __init__.py # Exports all functions
├── browser.py # Browser management (start, close, login)
├── actions.py # Action functions (goto, click, fill, etc.)
├── utils.py # Utilities (screenshot, inspect_page, etc.)
├── permissions.py # Permission management
└── config.py # Configuration
Project-Specific ({project_root}/.opencode/):
.opencode/
├── scripts/playwright/ # Test scripts (scratchpad)
│ ├── login.py
│ └── test_sales_order.py
└── screenshots/ # Screenshot output
Library Functions
Browser Management (lib/browser.py):
start_browser(headless=False) - Start browser, returns (p, browser, page)
close_browser(browser, p) - Close browser and stop Playwright
login(page, user_type=None) - Login to Django application (user_type="test" or user_type="super")
Actions (lib/actions.py):
goto(page, url) - Navigate to URL (relative or absolute)
click(page, selector) - Click element
fill(page, selector, value) - Fill input field
check(page, selector) - Check checkbox/radio
uncheck(page, selector) - Uncheck checkbox
select(page, selector, value) - Select dropdown option
hover(page, selector) - Hover over element
press(page, selector, key) - Press key on element
wait_for_load_state(page, state) - Wait for load state
wait_for_selector(page, selector, timeout) - Wait for element
Utilities (lib/utils.py):
take_screenshot(page, filename) - Take screenshot
inspect_page(page) - List all clickable elements
debug_page_state(page, step_name) - Debug page state
get_page_html(page, max_length) - Get page HTML
capture_console_errors(page) - Set up console error capture
Permissions Tool
The permissions module (lib/permissions.py) manages Django user permissions for testing.
Cascading Permission Structure
Child apps inherit parent app permissions through MODEL_PERMISSIONS. Grant parent app permissions to access all child apps:
company.change_company cascades to company_location.change_company, company_contact.change_company, etc.
Permission Types
- UserProfile Boolean Fields - Custom
can_* permissions defined on the UserProfile model
- Django Standard Permissions - Standard
view, add, change, delete permissions on models
UserProfile Permissions (lib/permissions.py)
from lib.permissions import (
get_user_permissions,
get_user_permission,
set_user_permission,
reset_user_permissions,
list_permission_fields,
get_test_user_permissions,
)
Django Standard Permissions
from lib.permissions import (
get_django_permissions,
grant_app_permission,
grant_full_app_access,
)
Usage Examples
Configure Credentials (lib/config.py):
SUPER_USER = "stratus"
SUPER_PASSWORD = "stratus"
TEST_USER = "test"
TEST_PASSWORD = "test"
Login as Test User:
from lib.browser import start_browser, close_browser, login
p, browser, page = start_browser(headless=False)
try:
login(page, user_type="test")
finally:
close_browser(browser, p)
Grant Full App Access (includes all CRUD + cascading to child apps):
from lib.permissions import grant_full_app_access, get_django_permissions
grant_full_app_access("test", "company")
print(get_django_permissions("test"))
Grant Single Permission:
from lib.permissions import grant_app_permission
grant_app_permission("test", "view", "company")
Check User Permissions:
from lib.permissions import get_django_permissions, get_user_permissions
print(get_django_permissions("test"))
print(get_user_permissions("test"))
Set UserProfile Permission:
from lib.permissions import set_user_permission
set_user_permission("test", "can_approve_procurement_purchase_orders", True)
Running Permission Scripts (CLI)
Run from project root (where manage.py is). Uses Path.home() to dynamically locate the tool:
python .opencode/scripts/grant_perm.py grant -u test -a asset --action view
python .opencode/scripts/grant_perm.py full -u test -a company
python .opencode/scripts/grant_perm.py list -u test
python .opencode/scripts/grant_perm.py list -u test --app asset
python .opencode/scripts/grant_perm.py check -u test -p view_asset
python .opencode/scripts/grant_perm.py fields
python .opencode/scripts/grant_perm.py set -u test --perm can_approve -v True
Running Playwright Scripts
python .opencode/scripts/playwright/__main__.py login.py
python .opencode/scripts/playwright/__main__.py check_sidebar.py
Implementation Guide
Writing Scripts
Scripts are disposable test code. Write them in scripts/, run them, modify as needed.
DON'T: Create reusable modules or import scripts into other code
from scripts.login import main
main()
DO: Write self-contained scripts that use library functions
from __future__ import annotations
import sys
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(BASE_DIR))
from lib.browser import start_browser, close_browser, login
from lib.actions import goto, click
from lib.utils import inspect_page, take_screenshot
def main():
p, browser, page = start_browser(headless=False)
try:
login(page)
page.wait_for_timeout(2000)
inspect_page(page)
take_screenshot(page, "home.png")
finally:
close_browser(browser, p)
if __name__ == "__main__":
main()
Running Scripts
Execute scripts directly with Python:
python .config/opencode/tools/playwright_tool/scripts/login.py
Configuration
Edit lib/config.py to customize settings:
BASE_URL = "http://localhost:8000"
LOGIN_URL = "/django_spire/auth/login/"
DEFAULT_TIMEOUT = 10000
SCREENSHOT_DIR = ".opencode/screenshots"
SUPER_USER = "stratus"
SUPER_PASSWORD = "stratus"
TEST_USER = "test"
TEST_PASSWORD = "test"
Example: Complete Workflow
from __future__ import annotations
import sys
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(BASE_DIR))
from lib.browser import start_browser, close_browser, login
from lib.actions import goto
from lib.utils import (
inspect_page,
debug_page_state,
take_screenshot,
capture_console_errors,
)
from lib.permissions import grant_full_app_access, get_django_permissions
def main():
p, browser, page = start_browser(headless=False)
get_errors = capture_console_errors(page)
try:
login(page)
goto(page, "/django_spire/companies/")
inspect_page(page)
debug_page_state(page, "company_list")
take_screenshot(page, "company_page.png")
errors = get_errors()
print(f"Console errors: {len(errors)}")
for err in errors:
print(f" - {err['text']}")
finally:
close_browser(browser, p)
if __name__ == "__main__":
main()
Login Redirect Behavior
After login, always let the redirect happen naturally. The login function should:
- Submit the login form
- Wait for
domcontentloaded state (not networkidle)
- Add a small timeout (1-2s) to allow redirects to complete naturally
def main():
p, browser, page = start_browser(headless=False)
try:
login(page)
page.wait_for_timeout(2000)
inspect_page(page)
finally:
close_browser(browser, p)
Why Not Use networkidle?
networkidle waits for all network requests to finish, which can timeout on pages with:
- Live reload connections (
__reload__, __debug__)
- WebSocket connections
- Third-party analytics/tracking scripts
domcontentloaded + timeout is faster and more reliable for SPA navigation
Navigation from Home Page
After login redirect lands on the home page (/), you can navigate using:
inspect_page(page) to see available navigation links
click(page, "text=Link Text") to click sidebar links
- Direct URL navigation via
goto()
Code Review Checklist
When reviewing Playwright scripts, verify the following:
- Absolute Imports: Verify scripts use absolute imports from
lib/, not relative imports
- Self-Contained: Confirm scripts are standalone and not imported by other code
- Proper Cleanup: Check that
close_browser() is called in finally block
- Error Capture: Verify
capture_console_errors() is used for debugging
- Screenshots: Confirm screenshots are taken at key workflow steps
- Descriptive Names: Scripts should have descriptive names (not
test.py, temp.py)
- No Hardcoded URLs: Use
BASE_URL from config, not hardcoded URLs
- Headless Mode: Scripts should default to
headless=False for debugging
- Wait States: Verify
wait_for_load_state() is called after navigation
- Selectors: Prefer specific selectors (
text=, #id, [name='field']) over generic ones
Known Limitations
- No Persistent State: Browser closes when script ends. Cannot maintain state across scripts.
- No Interactive Mode: REPL/interactive mode not supported. Use scripts instead.
- Sequential Execution: Scripts run sequentially, not in parallel.
Related Skills