datasette/tests/test_datasette_manager_js.py
2026-06-14 16:48:32 -07:00

202 lines
6.7 KiB
Python

import json
from pathlib import Path
import subprocess
import textwrap
STATIC_DIR = Path(__file__).resolve().parents[1] / "datasette" / "static"
def test_builtin_json_column_field_validation():
script = textwrap.dedent("""
const fs = require("fs");
const vm = require("vm");
const editToolsJs = __EDIT_TOOLS_JS__;
class FakeEvent {
constructor(type, options) {
this.type = type;
this.bubbles = !!(options && options.bubbles);
}
}
class FakeElement {
constructor(tagName = "div") {
this.nodeName = tagName.toUpperCase();
this.nodeType = 1;
this.children = [];
this.dataset = {};
this.attributes = {};
this.value = "";
this.name = "";
this.disabled = false;
this.hidden = false;
this.textContent = "";
this.validationMessage = "";
this.eventListeners = {};
this.className = "";
}
appendChild(child) {
this.children.push(child);
child.parentNode = this;
return child;
}
addEventListener(type, callback) {
this.eventListeners[type] = this.eventListeners[type] || [];
this.eventListeners[type].push(callback);
}
dispatchEvent(event) {
event.target = event.target || this;
for (const callback of this.eventListeners[event.type] || []) {
callback(event);
}
return true;
}
setAttribute(name, value) {
this.attributes[name] = String(value);
}
getAttribute(name) {
return this.attributes[name] || null;
}
removeAttribute(name) {
delete this.attributes[name];
}
setCustomValidity(message) {
this.validationMessage = message;
}
}
global.Event = FakeEvent;
global.document = {
addEventListener() {},
createElement(tagName) {
return new FakeElement(tagName);
},
createTextNode(text) {
const node = new FakeElement("#text");
node.textContent = text;
return node;
},
};
global.location = {
href: "http://localhost/data/projects",
pathname: "/data/projects",
search: "",
};
global.window = {
_datasetteTableData: {
database: "data",
table: "projects",
tableUrl: "/data/projects",
},
};
vm.runInThisContext(fs.readFileSync(editToolsJs, "utf8"), {
filename: "edit-tools.js",
});
const plugins = [];
registerBuiltinColumnFieldPlugins({
registerPlugin(name, plugin) {
plugins.push({ name, plugin });
},
});
const jsonPlugin = plugins.find((entry) => entry.name === "datasette-json-column");
if (!jsonPlugin) {
throw new Error("datasette-json-column plugin was not registered");
}
const pluginControl = jsonPlugin.plugin.makeColumnField({
column: "metadata",
columnType: { type: "json", config: {} },
});
if (!pluginControl || pluginControl.useTextarea !== true) {
throw new Error("JSON column plugin should request a textarea");
}
const context = columnFormControlContext(
"metadata",
false,
{ type: "json", config: {} },
{ mode: "edit" }
);
const control = new FakeElement("textarea");
control.name = "metadata";
control.value = '{"ok": true}';
control.dataset.initialValue = '{"ok": true}';
control.dataset.initialValueKind = "string";
control.dataset.currentValueKind = "string";
const meta = new FakeElement("span");
const field = createColumnFieldApi({
id: "row-edit-field-0",
labelId: "row-edit-field-label-0",
descriptionId: "row-edit-field-meta-0",
control,
meta,
context,
});
renderColumnField(
Object.assign({ pluginName: "datasette-json-column" }, pluginControl),
field
);
if (control.validationMessage !== "") {
throw new Error("Initial valid JSON should not be invalid");
}
if (control.dataset.initialValueKind !== "string") {
throw new Error("JSON plugin should keep the original string value kind");
}
if (control.dataset.currentValueKind !== "string") {
throw new Error("JSON plugin should keep the current string value kind");
}
if (!field.validationMessageElement || field.validationMessageElement.hidden !== true) {
throw new Error("JSON validation message should start hidden");
}
control.value = "{";
control.dispatchEvent(new Event("input", { bubbles: true }));
if (!control.validationMessage.startsWith("Invalid JSON")) {
throw new Error("Invalid JSON should set a custom validity message");
}
if (control.getAttribute("aria-invalid") !== "true") {
throw new Error("Invalid JSON should set aria-invalid");
}
if (field.validationMessageElement.hidden) {
throw new Error("Invalid JSON should show the validation message");
}
control.value = '{"ok": true}';
control.dispatchEvent(new Event("input", { bubbles: true }));
if (control.validationMessage !== "") {
throw new Error("Valid JSON should clear the custom validity message");
}
if (control.getAttribute("aria-invalid") !== null) {
throw new Error("Valid JSON should clear aria-invalid");
}
if (!field.validationMessageElement.hidden) {
throw new Error("Valid JSON should hide the validation message");
}
control.dataset.initialValue = '{"ok":';
control.value = '{"ok": true}';
const values = collectRowFormValues({
mode: "edit",
fields: {
querySelectorAll() {
return [control];
},
},
});
if (values.metadata !== '{"ok": true}') {
throw new Error("Corrected JSON should be submitted as a string value");
}
process.stdout.write("ok");
""").replace("__EDIT_TOOLS_JS__", json.dumps(str(STATIC_DIR / "edit-tools.js")))
result = subprocess.run(
["node", "-e", script],
text=True,
capture_output=True,
check=False,
)
assert result.returncode == 0, result.stderr
assert result.stdout == "ok"