From 6bbd33d81da6b0b599639e07f212602a3b25f48b Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sun, 14 Jun 2026 16:44:59 -0700 Subject: [PATCH] Port navigation jump sections to Playwright Refs #2779 --- tests/test_navigation_search_js.py | 194 ----------------------------- tests/test_playwright.py | 38 ++++++ 2 files changed, 38 insertions(+), 194 deletions(-) delete mode 100644 tests/test_navigation_search_js.py 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