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