diff --git a/datasette_apps/agent_tools.py b/datasette_apps/agent_tools.py
index 338e141..98bb10a 100644
--- a/datasette_apps/agent_tools.py
+++ b/datasette_apps/agent_tools.py
@@ -74,7 +74,8 @@
"loads invisibly in the user's open browser tab, so this requires an "
"interactive conversation and edit permission for that app. The "
"javascript argument is the body of an async function executed in "
- "the app's global scope; its awaited return value is the result and "
+ "the app's global scope once the page has parsed and its own scripts "
+ "have run; its awaited return value is the result and "
"must be JSON-serializable - return .textContent or measurements, "
"not DOM elements. Apps render asynchronously, so wait for readiness "
"with await debug.waitFor(fn, {timeout, interval}), which polls fn "
@@ -85,7 +86,10 @@
"await debug.waitFor(() => window.innerWidth). Captured "
"events include JavaScript errors, unhandled rejections, CSP "
"violations, failed fetches, console output and datasette.query() "
- "calls, from page load through script completion. Caveat: synthetic "
+ "calls, from page load through script completion. Some browsers "
+ "report errors against app-script-N - the app's Nth
+""",
+ name="Error sampler",
+ )
+ )
+ job, payload, harness = asyncio.run(
+ _create_debug_job_with_task(
+ server,
+ app["id"],
+ """
+await debug.waitFor(() => document.getElementById("title").dataset.ready);
+document.getElementById("listener-button").click();
+document.getElementById("attribute-button").click();
+await new Promise((resolve) => setTimeout(resolve, 200));
+return {count: shared.count};
+""",
)
+ )
+
+ with server, _browser_page() as page:
+ _run_debug_task(server, page, "01TASK0000000000000000RCVR", payload, harness)
+ envelope = _wait_for_task_result(page)[0]["envelope"]
+
+ # The app ran exactly as it would without the debug bridge: its
+ # scripts share top-level bindings with each other and with the
+ # debug script
+ assert envelope["ok"] is True, envelope
+ assert envelope["result"] == {"count": 3}
+ js_errors = {
+ error["message"]: error
+ for error in envelope["events"]["errors"]
+ if error["kind"] == "javascript-error"
+ }
+ # A str, so pytest reports it in full rather than truncated
+ all_errors = json.dumps(list(js_errors.values()), indent=1)
+ # WebKit sanitizes every one of these to "Script error." in a
+ # sandboxed frame; debug frames recover each message
+ for expected in (
+ "top-level code failed",
+ "click listener failed",
+ "inline attribute handler failed",
+ "timer callback failed",
+ ):
+ assert any(expected in message for message in js_errors), all_errors
+ assert not any("Script error" in message for message in js_errors), all_errors
+ top_level = next(
+ error
+ for message, error in js_errors.items()
+ if "top-level code failed" in message
+ )
+ if top_level["filename"].startswith("app-script-"):
+ # Engines that sanitize run inline scripts as blob: scripts,
+ # reported against the app's Nth