diff --git a/tests/test_navigation_search_js.py b/tests/test_navigation_search_js.py
deleted file mode 100644
index fc3c7a63..00000000
--- a/tests/test_navigation_search_js.py
+++ /dev/null
@@ -1,194 +0,0 @@
-import json
-from pathlib import Path
-import subprocess
-import textwrap
-
-REPO_ROOT = Path(__file__).resolve().parents[1]
-STATIC_DIR = REPO_ROOT / "datasette" / "static"
-
-
-def test_navigation_search_renders_jump_sections_from_javascript_plugins():
- script = (
- textwrap.dedent("""
- const fs = require("fs");
- const vm = require("vm");
- const datasetteManagerJs = __DATASETTE_MANAGER_JS__;
- const navigationSearchJs = __NAVIGATION_SEARCH_JS__;
-
- const documentListeners = {};
-
- class FakeElement {
- constructor(tagName = "div", parent = null) {
- this._innerHTML = "";
- this.value = "";
- this.dataset = {};
- this.open = false;
- this.parent = parent;
- this.tagName = tagName.toUpperCase();
- }
- set textContent(value) {
- this.innerHTML = String(value)
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, """);
- }
- get innerHTML() {
- return this._innerHTML;
- }
- set innerHTML(value) {
- this._innerHTML = String(value);
- if (this.parent) {
- this.parent._innerHTML += this._innerHTML;
- }
- }
- addEventListener() {}
- appendChild(child) {
- this._innerHTML += child.innerHTML || "";
- return child;
- }
- close() { this.open = false; }
- focus() {}
- querySelector(selector) {
- if (selector.startsWith("[data-jump-section-index=")) {
- return new FakeElement("div", this);
- }
- return { scrollIntoView() {} };
- }
- showModal() { this.open = true; }
- }
-
- class FakeShadowRoot {
- constructor() {
- this.innerHTML = "";
- this.dialog = new FakeElement("dialog");
- this.input = new FakeElement("input");
- this.results = new FakeElement("div");
- }
- querySelector(selector) {
- if (selector == "dialog") return this.dialog;
- if (selector == ".search-input") return this.input;
- if (selector == ".results-container") return this.results;
- return new FakeElement();
- }
- }
-
- global.HTMLElement = class {
- constructor() {
- this.attributes = {};
- }
- attachShadow() {
- this.shadowRoot = new FakeShadowRoot();
- return this.shadowRoot;
- }
- dispatchEvent() {}
- getAttribute(name) {
- return this.attributes[name] || null;
- }
- querySelector() {
- return null;
- }
- setAttribute(name, value) {
- this.attributes[name] = value;
- }
- };
- global.CustomEvent = class {
- constructor(name, options) {
- this.name = name;
- this.type = name;
- this.detail = options ? options.detail : undefined;
- }
- };
- global.customElements = {
- registry: new Map(),
- define(name, cls) {
- this.registry.set(name, cls);
- },
- };
- global.document = {
- addEventListener(name, callback) {
- documentListeners[name] = documentListeners[name] || [];
- documentListeners[name].push(callback);
- },
- activeElement: null,
- createElement(tagName) {
- return new FakeElement(tagName);
- },
- dispatchEvent(event) {
- for (const callback of documentListeners[event.type] || []) {
- callback(event);
- }
- },
- querySelectorAll() {
- return [];
- },
- };
- global.localStorage = {
- getItem() { return null; },
- setItem() {},
- removeItem() {},
- };
- global.window = { datasetteVersion: "test", location: { href: "" } };
-
- vm.runInThisContext(
- fs.readFileSync(datasetteManagerJs, "utf8"),
- { filename: "datasette-manager.js" }
- );
- for (const callback of documentListeners.DOMContentLoaded || []) {
- callback();
- }
- window.__DATASETTE__.registerPlugin("agent", {
- version: "0.1",
- makeJumpSections() {
- return [
- {
- id: "agent-chat",
- render(node, context) {
- if (!context.navigationSearch) {
- throw new Error("Expected navigationSearch in render context");
- }
- node.innerHTML = [
- '',
- '',
- '',
- ].join('');
- },
- },
- ];
- },
- });
-
- vm.runInThisContext(
- fs.readFileSync(navigationSearchJs, "utf8"),
- { filename: "navigation-search.js" }
- );
-
- const Component = customElements.registry.get("navigation-search");
- const element = new Component();
- element.shadowRoot.input.value = "";
- element.renderResults();
-
- const html = element.shadowRoot.results.innerHTML;
- if (!html.includes("Start a new agent chat")) {
- throw new Error(`Missing jump section content: ${html}`);
- }
- process.stdout.write("ok");
- """)
- .replace(
- "__DATASETTE_MANAGER_JS__",
- json.dumps(str(STATIC_DIR / "datasette-manager.js")),
- )
- .replace(
- "__NAVIGATION_SEARCH_JS__",
- json.dumps(str(STATIC_DIR / "navigation-search.js")),
- )
- )
- result = subprocess.run(
- ["node", "-e", script],
- cwd=REPO_ROOT,
- text=True,
- capture_output=True,
- check=False,
- )
- assert result.returncode == 0, result.stderr
- assert result.stdout.endswith("ok")
diff --git a/tests/test_playwright.py b/tests/test_playwright.py
index 726536d9..caea8b16 100644
--- a/tests/test_playwright.py
+++ b/tests/test_playwright.py
@@ -145,3 +145,41 @@ def test_navigation_search_tracks_and_renders_recent_items(page, datasette_serve
assert "Clear recent" in result["html"]
assert result["clearedValue"] is None
assert "Clear recent" not in result["htmlAfterClear"]
+
+
+@pytest.mark.playwright
+def test_navigation_search_renders_jump_sections_from_javascript_plugins(
+ page, datasette_server
+):
+ page.goto(datasette_server)
+ html = page.evaluate("""
+ async () => {
+ await customElements.whenDefined("navigation-search");
+ window.__DATASETTE__.registerPlugin("agent", {
+ version: "0.1",
+ makeJumpSections() {
+ return [
+ {
+ id: "agent-chat",
+ render(node, context) {
+ if (!context.navigationSearch) {
+ throw new Error("Expected navigationSearch in render context");
+ }
+ node.innerHTML = [
+ '',
+ '',
+ '',
+ ].join("");
+ },
+ },
+ ];
+ },
+ });
+
+ const element = document.querySelector("navigation-search");
+ element.shadowRoot.querySelector(".search-input").value = "";
+ element.renderResults();
+ return element.shadowRoot.querySelector(".results-container").innerHTML;
+ }
+ """)
+ assert "Start a new agent chat" in html