mirror of
https://github.com/simonw/datasette.git
synced 2026-06-19 07:17:52 +02:00
parent
6cd65cf4fb
commit
047b69e87f
2 changed files with 68 additions and 200 deletions
|
|
@ -7,206 +7,6 @@ REPO_ROOT = Path(__file__).resolve().parents[1]
|
|||
STATIC_DIR = REPO_ROOT / "datasette" / "static"
|
||||
|
||||
|
||||
def test_navigation_search_tracks_and_renders_recent_items():
|
||||
script = textwrap.dedent("""
|
||||
const fs = require("fs");
|
||||
const vm = require("vm");
|
||||
const navigationSearchJs = __NAVIGATION_SEARCH_JS__;
|
||||
|
||||
class FakeElement {
|
||||
constructor() {
|
||||
this.innerHTML = "";
|
||||
this.value = "";
|
||||
this.dataset = {};
|
||||
this.open = false;
|
||||
}
|
||||
addEventListener() {}
|
||||
close() { this.open = false; }
|
||||
focus() {}
|
||||
querySelector() {
|
||||
return { scrollIntoView() {} };
|
||||
}
|
||||
showModal() { this.open = true; }
|
||||
}
|
||||
|
||||
class FakeShadowRoot {
|
||||
constructor() {
|
||||
this.innerHTML = "";
|
||||
this.dialog = new FakeElement();
|
||||
this.input = new FakeElement();
|
||||
this.results = new FakeElement();
|
||||
}
|
||||
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.options = options;
|
||||
}
|
||||
};
|
||||
global.customElements = {
|
||||
registry: new Map(),
|
||||
define(name, cls) {
|
||||
this.registry.set(name, cls);
|
||||
},
|
||||
};
|
||||
global.document = {
|
||||
addEventListener() {},
|
||||
activeElement: null,
|
||||
createElement() {
|
||||
return {
|
||||
set textContent(value) {
|
||||
this.innerHTML = String(value)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
global.localStorage = {
|
||||
store: {},
|
||||
getItem(key) {
|
||||
return Object.prototype.hasOwnProperty.call(this.store, key)
|
||||
? this.store[key]
|
||||
: null;
|
||||
},
|
||||
setItem(key, value) {
|
||||
this.store[key] = String(value);
|
||||
},
|
||||
removeItem(key) {
|
||||
delete this.store[key];
|
||||
},
|
||||
};
|
||||
global.window = { location: { href: "" } };
|
||||
|
||||
vm.runInThisContext(
|
||||
fs.readFileSync(navigationSearchJs, "utf8"),
|
||||
{ filename: "navigation-search.js" }
|
||||
);
|
||||
|
||||
const Component = customElements.registry.get("navigation-search");
|
||||
const element = new Component();
|
||||
const items = Array.from({ length: 6 }, (_, index) => ({
|
||||
name: `Item ${index + 1}`,
|
||||
url: `/item-${index + 1}`,
|
||||
type: "table",
|
||||
description: "Table",
|
||||
}));
|
||||
items[5].name = "content: recent_datasette_releases";
|
||||
items[5].display_name = "Recent Datasette releases";
|
||||
|
||||
for (const item of items) {
|
||||
element.matches = [item];
|
||||
element.renderedMatches = [item];
|
||||
element.selectedIndex = 0;
|
||||
element.selectCurrentItem();
|
||||
}
|
||||
|
||||
const stored = JSON.parse(
|
||||
Object.values(localStorage.store).find((value) => value.includes("/item-6"))
|
||||
);
|
||||
if (stored.length !== 5) {
|
||||
throw new Error(`Expected 5 recent items, got ${stored.length}`);
|
||||
}
|
||||
if (stored[0].url !== "/item-6" || stored[4].url !== "/item-2") {
|
||||
throw new Error(`Unexpected recent order: ${JSON.stringify(stored)}`);
|
||||
}
|
||||
if (stored[0].display_name !== "Recent Datasette releases") {
|
||||
throw new Error(`Missing display_name in recent item: ${JSON.stringify(stored[0])}`);
|
||||
}
|
||||
|
||||
element.matches = [
|
||||
items[5],
|
||||
items[4],
|
||||
{
|
||||
name: "Other",
|
||||
url: "/other",
|
||||
type: "database",
|
||||
description: "Database",
|
||||
},
|
||||
];
|
||||
element.shadowRoot.input.value = "";
|
||||
element.renderResults();
|
||||
|
||||
const html = element.shadowRoot.results.innerHTML;
|
||||
if (!html.includes("Recent")) {
|
||||
throw new Error(`Missing Recent heading: ${html}`);
|
||||
}
|
||||
if (!html.includes("Recent Datasette releases") || !html.includes("Item 5")) {
|
||||
throw new Error(`Missing recent items: ${html}`);
|
||||
}
|
||||
if (!html.includes("content: recent_datasette_releases")) {
|
||||
throw new Error(`Missing canonical item name for display_name item: ${html}`);
|
||||
}
|
||||
if (!html.includes("Item 4") || !html.includes("Item 2")) {
|
||||
throw new Error(`Expected all stored recent items in empty state: ${html}`);
|
||||
}
|
||||
if (html.includes("Other")) {
|
||||
throw new Error(`Rendered non-recent item in empty state: ${html}`);
|
||||
}
|
||||
if (!html.includes("Clear recent")) {
|
||||
throw new Error(`Missing Clear recent control: ${html}`);
|
||||
}
|
||||
|
||||
element.clearRecentItems();
|
||||
if (localStorage.getItem(element.recentItemsStorageKey()) !== null) {
|
||||
throw new Error("Expected recent items to be cleared");
|
||||
}
|
||||
element.renderResults();
|
||||
if (element.shadowRoot.results.innerHTML.includes("Clear recent")) {
|
||||
throw new Error("Clear recent should disappear after clearing");
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify(stored));
|
||||
""").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 [item["url"] for item in json.loads(result.stdout)] == [
|
||||
"/item-6",
|
||||
"/item-5",
|
||||
"/item-4",
|
||||
"/item-3",
|
||||
"/item-2",
|
||||
]
|
||||
assert json.loads(result.stdout)[0]["display_name"] == "Recent Datasette releases"
|
||||
|
||||
|
||||
def test_navigation_search_renders_jump_sections_from_javascript_plugins():
|
||||
script = (
|
||||
textwrap.dedent("""
|
||||
|
|
|
|||
|
|
@ -77,3 +77,71 @@ def datasette_server(tmp_path):
|
|||
def test_datasette_homepage_contains_datasette(page, datasette_server):
|
||||
page.goto(datasette_server)
|
||||
assert "Datasette" in page.locator("body").inner_text()
|
||||
|
||||
|
||||
@pytest.mark.playwright
|
||||
def test_navigation_search_tracks_and_renders_recent_items(page, datasette_server):
|
||||
page.goto(datasette_server)
|
||||
result = page.evaluate("""
|
||||
async () => {
|
||||
await customElements.whenDefined("navigation-search");
|
||||
const element = document.querySelector("navigation-search");
|
||||
const key = element.recentItemsStorageKey();
|
||||
localStorage.removeItem(key);
|
||||
|
||||
const items = Array.from({ length: 6 }, (_, index) => ({
|
||||
name: `Item ${index + 1}`,
|
||||
url: `/item-${index + 1}`,
|
||||
type: "table",
|
||||
description: "Table",
|
||||
}));
|
||||
items[5].name = "content: recent_datasette_releases";
|
||||
items[5].display_name = "Recent Datasette releases";
|
||||
|
||||
for (const item of items) {
|
||||
element.saveRecentItem(item);
|
||||
}
|
||||
|
||||
const stored = JSON.parse(localStorage.getItem(key));
|
||||
element.matches = [
|
||||
items[5],
|
||||
items[4],
|
||||
{
|
||||
name: "Other",
|
||||
url: "/other",
|
||||
type: "database",
|
||||
description: "Database",
|
||||
},
|
||||
];
|
||||
element.shadowRoot.querySelector(".search-input").value = "";
|
||||
element.renderResults();
|
||||
const html = element.shadowRoot.querySelector(".results-container").innerHTML;
|
||||
|
||||
element.clearRecentItems();
|
||||
const clearedValue = localStorage.getItem(key);
|
||||
element.renderResults();
|
||||
const htmlAfterClear = element.shadowRoot
|
||||
.querySelector(".results-container")
|
||||
.innerHTML;
|
||||
|
||||
return { stored, html, clearedValue, htmlAfterClear };
|
||||
}
|
||||
""")
|
||||
assert [item["url"] for item in result["stored"]] == [
|
||||
"/item-6",
|
||||
"/item-5",
|
||||
"/item-4",
|
||||
"/item-3",
|
||||
"/item-2",
|
||||
]
|
||||
assert result["stored"][0]["display_name"] == "Recent Datasette releases"
|
||||
assert "Recent" in result["html"]
|
||||
assert "Recent Datasette releases" in result["html"]
|
||||
assert "Item 5" in result["html"]
|
||||
assert "content: recent_datasette_releases" in result["html"]
|
||||
assert "Item 4" in result["html"]
|
||||
assert "Item 2" in result["html"]
|
||||
assert "Other" not in result["html"]
|
||||
assert "Clear recent" in result["html"]
|
||||
assert result["clearedValue"] is None
|
||||
assert "Clear recent" not in result["htmlAfterClear"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue