name: mre-playwright description: Automatically create a Minimum Reproducible Example (MRE) using Playwright to reproduce a browser-based bug from a GitHub issue.
Create a Minimum Reproducible Example (MRE) using Playwright to reproduce a browser-based bug from a GitHub issue.
Usage
/mre-playwright <issue_url_or_number>
Instructions
1. Fetch and Understand the Issue
gh issue view title,body,comments <number >--json
Extract:
- Bug description
- Code snippets from the issue
- Steps to reproduce
- Expected vs actual behavior
2. Create MRE Directory
claude/<issue_name>/
├── reproduce_<number>.py
└── screenshot_*.png (generated when run)
3. MRE Script Structure
"""
Playwright reproducer for GitHub issue #<NUMBER>:
<ISSUE_TITLE>
<ISSUE_URL>
Run with: python reproduce_<NUMBER>.py
The bug: <ONE_LINE_DESCRIPTION>
"""
import time
from pathlib import Path
import holoviews as hv
import panel as pn
from playwright.sync_api import sync_playwright
hv.extension("bokeh")
pn.extension()
SCREENSHOT_DIR = Path(__file__).parent
def create_app():
"""Create minimal app that demonstrates the bug.
Use EXACT code from issue when possible, or simplify while
preserving the bug trigger conditions.
"""
# ... app setup
return app
def serve_app(app, port=5006):
"""Start the app server."""
return pn.serve(app, port=port, show=False, threaded=True)
def main():
print("=" * 60)
print("Reproducer for GitHub issue #<NUMBER>")
print("<SHORT_DESCRIPTION>")
print("=" * 60)
app = create_app()
port = 5006
server = serve_app(app, port)
time.sleep(2)
try:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, slow_mo=100)
page = browser.new_page()
page.goto(f"http://localhost:{port}")
# Wait for app to render.
# Bokeh renders everything inside Shadow DOM — use .bk-Canvas (capitalized).
# Playwright auto-pierces open Shadow DOM for CSS selectors, so locator() works.
# Do NOT use .bk-canvas (lowercase) or .bk-events — they live inside Shadow DOM
# but won't be found by wait_for_selector (use locator().wait_for() instead).
page.locator(".bk-Canvas").wait_for(state="visible", timeout=15000)
time.sleep(2)
# === STEP 1: Initial State ===
print("\n--- Step 1: Initial state ---")
page.screenshot(path=str(SCREENSHOT_DIR / "step1_initial.png"))
# Capture/print relevant state
# === STEP 2: Trigger the Bug ===
print("\n--- Step 2: <ACTION_DESCRIPTION> ---")
# Interact with the page:
# page.locator('button:text("Click")').click()
# page.mouse.click(x, y)
# page.keyboard.press("Enter")
time.sleep(0.5)
page.screenshot(path=str(SCREENSHOT_DIR / "step2_action.png"))
# === STEP 3: Verify Bug ===
print("\n--- Step 3: Check for bug ---")
page.screenshot(path=str(SCREENSHOT_DIR / "step3_result.png"))
# Bug detection - adapt to your specific bug
bug_detected = check_for_bug(page)
print("\n" + "=" * 60)
if bug_detected:
print("BUG CONFIRMED!")
print(" - <DESCRIBE_WHAT_IS_WRONG>")
else:
print("Bug not detected (may be fixed)")
print("=" * 60)
print(f"\nScreenshots saved in: {SCREENSHOT_DIR}")
time.sleep(2)
browser.close()
finally:
server.stop()
print("\nDone.")
def check_for_bug(page):
"""Programmatically verify the bug exists.
Return True if bug is present, False if not.
"""
# Example: Check via JavaScript
# result = page.evaluate("() => { return someCheck(); }")
# return result["buggy_condition"]
# Example: Check DOM state
# element = page.locator(".problematic-element")
# return element.is_visible() when it shouldn't be
return False
if __name__ == "__main__":
main()
4. Common Interaction Patterns
Click buttons/elements:
page.locator('button:text("Submit")').click()
page.locator('[data-testid="my-button"]').click()
page.locator('input[type="checkbox"]').check()
Mouse interactions:
bbox = page.locator(".canvas").bounding_box()
page.mouse.click(bbox["x"] + 100, bbox["y"] + 100)
page.mouse.dblclick(x, y)
page.mouse.move(x, y)
page.mouse.down()
page.mouse.up()
Keyboard:
page.keyboard.press("Enter")
page.keyboard.type("text")
page.locator("input").fill("value")
Wait for state:
page.wait_for_selector(".element")
page.wait_for_timeout(500)
page.locator(".element").wait_for(state="visible")
5. Bug Detection Patterns
JavaScript inspection for Bokeh renderers:
GET_RENDERERS_JS = """() => {
const doc = window.Bokeh?.documents?.[0];
if (!doc) return {error: 'No Bokeh document'};
const renderers = [];
for (const model of doc._all_models.values()) {
if (model.type === 'GlyphRenderer') {
let dataLen = 0;
if (model.data_source?.data) {
const keys = Object.keys(model.data_source.data);
if (keys.length > 0) {
dataLen = model.data_source.data[keys[0]].length;
}
}
renderers.push({
name: model.name || 'unnamed',
visible: model.visible,
glyph_type: model.glyph?.type || 'unknown',
data_length: dataLen
});
}
}
return {renderers: renderers};
}"""
result = page.evaluate(JS_GET_RENDERERS)
# Check renderer visibility states
patches_hidden = any(
r["glyph_type"] == "Patches" and not r["visible"] for r in result["renderers"]
)
DOM-based checks:
element = page.locator(".should-be-hidden")
bug_detected = element.is_visible() # Bug if visible when shouldn't be
Screenshot comparison (visual bugs):
import numpy as np
from PIL import Image
# Pixel-diff two screenshots — large diff means rendering changed (e.g. initial vs reset)
def screenshot_diff(path_a, path_b, threshold=10):
a = np.array(Image.open(path_a).convert("RGB"))
b = np.array(Image.open(path_b).convert("RGB"))
diff = np.abs(a.astype(int) - b.astype(int)).max(axis=2)
return int((diff > threshold).sum())
page.screenshot(path="before.png")
# ... trigger action ...
page.screenshot(path="after.png")
diff = screenshot_diff("before.png", "after.png")
bug_detected = diff > 5000 # tune threshold to the expected change size
DOM queries inside Bokeh Shadow DOM:
Bokeh renders inside a Shadow DOM.
page.locator() CSS selectors auto-pierce Shadow DOM; page.evaluate() JS
does not. To query the DOM from JS you must walk shadowRoot manually:
# Works — locator() pierces Shadow DOM automatically
page.locator(".bk-tool-icon-reset").click()
canvas_bb = page.locator(".bk-Canvas").bounding_box()
# Fails — querySelectorAll does not pierce Shadow DOM
# page.evaluate("() => document.querySelectorAll('.bk-Canvas').length") # returns 0
# Works — walk shadowRoot in JS
all_text = page.evaluate("""() => {
function collect(root) {
const texts = [];
root.querySelectorAll('text').forEach(t => texts.push(t.textContent.trim()));
root.querySelectorAll('*').forEach(el => {
if (el.shadowRoot) texts.push(...collect(el.shadowRoot));
});
return texts;
}
return collect(document);
}""")
Bokeh model data is on window.Bokeh.documents[0]._all_models (not in the DOM), so
page.evaluate() works fine for inspecting model state without touching the Shadow DOM.
6. Principles for Good MREs
- Minimal - Remove all code not needed to trigger the bug
- Complete - Single file, runs with
python reproduce.py - Exact - Use issue's code when possible, preserving the trigger
- Visual - Screenshots at each step
- Programmatic - Code that confirms bug exists (not just visual)
- Documented - Clear comments explaining each step
7. After Creating
- Run the script to verify it reproduces the bug
- Check screenshots show the problem clearly
- Confirm bug detection logic works
- Show user the output and screenshots for verification