Port JSON column field test to Playwright

Refs #2779
This commit is contained in:
Simon Willison 2026-06-14 16:49:30 -07:00
commit 387e309b3b
2 changed files with 125 additions and 202 deletions

View file

@ -1,202 +0,0 @@
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"

View file

@ -542,3 +542,128 @@ def test_table_plugin_column_field_api(page, datasette_server):
);
}
""")
@pytest.mark.playwright
def test_builtin_json_column_field_validation(page, datasette_server):
load_edit_tools(page, datasette_server)
page.evaluate("""
() => {
const assert = (condition, message) => {
if (!condition) {
throw new Error(message);
}
};
const plugins = [];
registerBuiltinColumnFieldPlugins({
registerPlugin(name, plugin) {
plugins.push({ name, plugin });
},
});
const jsonPlugin = plugins.find(
(entry) => entry.name === "datasette-json-column",
);
assert(
jsonPlugin,
"datasette-json-column plugin was not registered",
);
const pluginControl = jsonPlugin.plugin.makeColumnField({
column: "metadata",
columnType: { type: "json", config: {} },
});
assert(
pluginControl && pluginControl.useTextarea === true,
"JSON column plugin should request a textarea",
);
const context = columnFormControlContext(
"metadata",
false,
{ type: "json", config: {} },
{ mode: "edit" },
);
const control = document.createElement("textarea");
control.className = "row-edit-input";
control.name = "metadata";
control.value = '{"ok": true}';
control.dataset.initialValue = '{"ok": true}';
control.dataset.initialValueKind = "string";
control.dataset.currentValueKind = "string";
const meta = document.createElement("span");
const field = createColumnFieldApi({
id: "row-edit-field-0",
labelId: "row-edit-field-label-0",
descriptionId: "row-edit-field-meta-0",
control,
meta,
context,
});
const wrapper = renderColumnField(
Object.assign({ pluginName: "datasette-json-column" }, pluginControl),
field,
);
assert(
control.validationMessage === "",
"Initial valid JSON should not be invalid",
);
assert(
control.dataset.initialValueKind === "string",
"JSON plugin should keep the original string value kind",
);
assert(
control.dataset.currentValueKind === "string",
"JSON plugin should keep the current string value kind",
);
assert(
field.validationMessageElement &&
field.validationMessageElement.hidden === true,
"JSON validation message should start hidden",
);
control.value = "{";
control.dispatchEvent(new Event("input", { bubbles: true }));
assert(
control.validationMessage.startsWith("Invalid JSON"),
"Invalid JSON should set a custom validity message",
);
assert(
control.getAttribute("aria-invalid") === "true",
"Invalid JSON should set aria-invalid",
);
assert(
!field.validationMessageElement.hidden,
"Invalid JSON should show the validation message",
);
control.value = '{"ok": true}';
control.dispatchEvent(new Event("input", { bubbles: true }));
assert(
control.validationMessage === "",
"Valid JSON should clear the custom validity message",
);
assert(
control.getAttribute("aria-invalid") === null,
"Valid JSON should clear aria-invalid",
);
assert(
field.validationMessageElement.hidden,
"Valid JSON should hide the validation message",
);
control.dataset.initialValue = '{"ok":';
control.value = '{"ok": true}';
const fields = document.createElement("div");
fields.appendChild(wrapper);
const values = collectRowFormValues({
mode: "edit",
fields,
});
assert(
values.metadata === '{"ok": true}',
"Corrected JSON should be submitted as a string value",
);
}
""")