mirror of
https://github.com/simonw/datasette.git
synced 2026-07-11 01:54:44 +02:00
parent
b5fa485a9f
commit
3cfdca026a
2 changed files with 329 additions and 387 deletions
|
|
@ -79,6 +79,20 @@ def test_datasette_homepage_contains_datasette(page, datasette_server):
|
|||
assert "Datasette" in page.locator("body").inner_text()
|
||||
|
||||
|
||||
def load_edit_tools(page, datasette_server):
|
||||
page.goto(datasette_server)
|
||||
page.evaluate("""
|
||||
() => {
|
||||
window._datasetteTableData = {
|
||||
database: "data",
|
||||
table: "projects",
|
||||
tableUrl: "/data/projects",
|
||||
};
|
||||
}
|
||||
""")
|
||||
page.add_script_tag(url=f"{datasette_server}-/static/edit-tools.js")
|
||||
|
||||
|
||||
@pytest.mark.playwright
|
||||
def test_navigation_search_tracks_and_renders_recent_items(page, datasette_server):
|
||||
page.goto(datasette_server)
|
||||
|
|
@ -213,3 +227,318 @@ def test_datasette_manager_make_column_field(page, datasette_server):
|
|||
"pluginName": "handles",
|
||||
"useTextarea": True,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.playwright
|
||||
def test_table_plugin_column_field_api(page, datasette_server):
|
||||
load_edit_tools(page, datasette_server)
|
||||
page.evaluate("""
|
||||
() => {
|
||||
const assert = (condition, message) => {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
};
|
||||
|
||||
const context = columnFormControlContext(
|
||||
"logo",
|
||||
true,
|
||||
{ type: "file", config: null },
|
||||
{
|
||||
mode: "edit",
|
||||
defaultExpression: "lower(hex(randomblob(4)))",
|
||||
useSqliteDefault: true,
|
||||
},
|
||||
);
|
||||
const expectedContextKeys = [
|
||||
"mode",
|
||||
"database",
|
||||
"table",
|
||||
"tableUrl",
|
||||
"column",
|
||||
"columnType",
|
||||
"sqliteType",
|
||||
"notNull",
|
||||
"isPk",
|
||||
"defaultExpression",
|
||||
"form",
|
||||
"dialog",
|
||||
].join(",");
|
||||
assert(
|
||||
Object.keys(context).join(",") === expectedContextKeys,
|
||||
`Unexpected context keys: ${Object.keys(context).join(",")}`,
|
||||
);
|
||||
assert(
|
||||
context.defaultExpression === "lower(hex(randomblob(4)))",
|
||||
"context.defaultExpression was not set",
|
||||
);
|
||||
assert(
|
||||
JSON.stringify(context.columnType) === '{"type":"file","config":{}}',
|
||||
"context.columnType should expose type and object config",
|
||||
);
|
||||
assert(
|
||||
context.isPk,
|
||||
"context.isPk should say whether the column is a primary key",
|
||||
);
|
||||
|
||||
const control = document.createElement("input");
|
||||
control.name = "logo";
|
||||
control.value = "df-old";
|
||||
control.dataset.initialValue = "df-old";
|
||||
control.dataset.initialValueKind = "string";
|
||||
control.dataset.currentValueKind = "string";
|
||||
control.dataset.useSqliteDefault = "1";
|
||||
control.disabled = true;
|
||||
const dispatchedEvents = [];
|
||||
control.addEventListener("input", (event) =>
|
||||
dispatchedEvents.push(event.type),
|
||||
);
|
||||
control.addEventListener("change", (event) =>
|
||||
dispatchedEvents.push(event.type),
|
||||
);
|
||||
|
||||
const field = createColumnFieldApi({
|
||||
id: "row-edit-field-0",
|
||||
labelId: "row-edit-field-label-0",
|
||||
descriptionId: "row-edit-field-meta-0",
|
||||
control,
|
||||
meta: document.createElement("span"),
|
||||
context,
|
||||
});
|
||||
|
||||
let renderArgumentCount = null;
|
||||
let renderField = null;
|
||||
const wrapper = renderColumnField(
|
||||
{
|
||||
pluginName: "test-plugin",
|
||||
render(field) {
|
||||
renderArgumentCount = arguments.length;
|
||||
renderField = field;
|
||||
return document.createElement("button");
|
||||
},
|
||||
},
|
||||
field,
|
||||
);
|
||||
assert(
|
||||
renderArgumentCount === 1 && renderField === field,
|
||||
"plugin render should receive the field object only",
|
||||
);
|
||||
assert(field.root === wrapper, "field.root should be the plugin wrapper");
|
||||
assert(
|
||||
wrapper.children.length === 1 &&
|
||||
wrapper.children[0].nodeName === "BUTTON",
|
||||
"plugin render should append returned DOM nodes to field.root",
|
||||
);
|
||||
|
||||
field.setValue(null);
|
||||
assert(field.getValue() === null, "field.setValue(null) should round-trip as null");
|
||||
assert(
|
||||
!field.isUsingSqliteDefault(),
|
||||
"field.setValue() should stop using the SQLite default",
|
||||
);
|
||||
assert(
|
||||
control.dataset.currentValueKind === "null",
|
||||
"null values should update currentValueKind",
|
||||
);
|
||||
|
||||
field.setValue("df-new");
|
||||
assert(
|
||||
field.getValue() === "df-new",
|
||||
"field.setValue() should update the current value",
|
||||
);
|
||||
assert(
|
||||
field.getInitialValue() === "df-old",
|
||||
"field.getInitialValue() should remain stable",
|
||||
);
|
||||
assert(
|
||||
field.hasChanged(),
|
||||
"field.hasChanged() should notice plugin value changes",
|
||||
);
|
||||
assert(
|
||||
dispatchedEvents.length === 0,
|
||||
`field.setValue() should not dispatch events: ${dispatchedEvents}`,
|
||||
);
|
||||
|
||||
const dirtyRowField = document.createElement("div");
|
||||
dirtyRowField.className = "row-edit-field";
|
||||
dirtyRowField._datasetteColumnFormField = field;
|
||||
const dirtyFields = document.createElement("div");
|
||||
dirtyFields.appendChild(dirtyRowField);
|
||||
const dirtyState = {
|
||||
hasLoaded: true,
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
mode: "edit",
|
||||
fields: dirtyFields,
|
||||
dialog: {
|
||||
closeCalled: false,
|
||||
close() {
|
||||
this.closeCalled = true;
|
||||
},
|
||||
},
|
||||
shouldRestoreFocus: false,
|
||||
};
|
||||
const confirmMessages = [];
|
||||
window.confirm = (message) => {
|
||||
confirmMessages.push(message);
|
||||
return false;
|
||||
};
|
||||
assert(
|
||||
rowEditDialogHasChanges(dirtyState),
|
||||
"row edit dialog should notice changed field values",
|
||||
);
|
||||
assert(
|
||||
!closeRowEditDialogIfConfirmed(dirtyState),
|
||||
"dirty row edit dialog should stay open when discard is rejected",
|
||||
);
|
||||
assert(
|
||||
!dirtyState.dialog.closeCalled,
|
||||
"dirty row edit dialog should not close when discard is rejected",
|
||||
);
|
||||
assert(
|
||||
confirmMessages[0] === "Discard unsaved changes to this row?",
|
||||
`Unexpected discard confirmation: ${confirmMessages[0]}`,
|
||||
);
|
||||
dirtyState.mode = "insert";
|
||||
window.confirm = (message) => {
|
||||
confirmMessages.push(message);
|
||||
return true;
|
||||
};
|
||||
assert(
|
||||
closeRowEditDialogIfConfirmed(dirtyState),
|
||||
"dirty row edit dialog should close when discard is confirmed",
|
||||
);
|
||||
assert(
|
||||
dirtyState.dialog.closeCalled && dirtyState.shouldRestoreFocus,
|
||||
"confirmed dirty row edit dialog should close and restore focus",
|
||||
);
|
||||
assert(
|
||||
confirmMessages[1] === "Discard this new row?",
|
||||
`Unexpected insert discard confirmation: ${confirmMessages[1]}`,
|
||||
);
|
||||
|
||||
const cleanContext = columnFormControlContext("title", false, null, {
|
||||
mode: "edit",
|
||||
});
|
||||
assert(
|
||||
cleanContext.defaultExpression === null,
|
||||
"context.defaultExpression should be null without a SQLite default",
|
||||
);
|
||||
const cleanControl = document.createElement("input");
|
||||
cleanControl.name = "title";
|
||||
cleanControl.value = "clean";
|
||||
cleanControl.dataset.initialValue = "clean";
|
||||
cleanControl.dataset.initialValueKind = "string";
|
||||
cleanControl.dataset.currentValueKind = "string";
|
||||
const cleanField = createColumnFieldApi({
|
||||
id: "row-edit-field-1",
|
||||
labelId: "row-edit-field-label-1",
|
||||
descriptionId: "row-edit-field-meta-1",
|
||||
control: cleanControl,
|
||||
meta: document.createElement("span"),
|
||||
context: cleanContext,
|
||||
});
|
||||
const cleanRowField = document.createElement("div");
|
||||
cleanRowField.className = "row-edit-field";
|
||||
cleanRowField._datasetteColumnFormField = cleanField;
|
||||
const cleanFields = document.createElement("div");
|
||||
cleanFields.appendChild(cleanRowField);
|
||||
const cleanState = {
|
||||
hasLoaded: true,
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
mode: "edit",
|
||||
fields: cleanFields,
|
||||
dialog: {
|
||||
closeCalled: false,
|
||||
close() {
|
||||
this.closeCalled = true;
|
||||
},
|
||||
},
|
||||
shouldRestoreFocus: false,
|
||||
};
|
||||
confirmMessages.length = 0;
|
||||
window.confirm = (message) => {
|
||||
confirmMessages.push(message);
|
||||
return false;
|
||||
};
|
||||
assert(
|
||||
!rowEditDialogHasChanges(cleanState),
|
||||
"row edit dialog should ignore unchanged field values",
|
||||
);
|
||||
assert(
|
||||
closeRowEditDialogIfConfirmed(cleanState),
|
||||
"clean row edit dialog should close without confirmation",
|
||||
);
|
||||
assert(
|
||||
cleanState.dialog.closeCalled && cleanState.shouldRestoreFocus,
|
||||
"clean row edit dialog should close and restore focus",
|
||||
);
|
||||
assert(
|
||||
confirmMessages.length === 0,
|
||||
"clean row edit dialog should not ask for confirmation",
|
||||
);
|
||||
|
||||
dirtyState.dialog.closeCalled = false;
|
||||
dirtyState.shouldRestoreFocus = false;
|
||||
confirmMessages.length = 0;
|
||||
field.setValue("<p></p>");
|
||||
field.markClean();
|
||||
assert(
|
||||
!field.hasChanged(),
|
||||
"field.markClean() should update the clean baseline",
|
||||
);
|
||||
assert(
|
||||
!rowEditDialogHasChanges(dirtyState),
|
||||
"row edit dialog should ignore clean plugin normalization",
|
||||
);
|
||||
assert(
|
||||
closeRowEditDialogIfConfirmed(dirtyState),
|
||||
"normalized row edit dialog should close without confirmation",
|
||||
);
|
||||
assert(
|
||||
confirmMessages.length === 0,
|
||||
"normalized row edit dialog should not ask for confirmation",
|
||||
);
|
||||
field.setValue("<p>Hello</p>");
|
||||
assert(
|
||||
field.hasChanged() && rowEditDialogHasChanges(dirtyState),
|
||||
"later plugin value changes should still count as dirty",
|
||||
);
|
||||
|
||||
try {
|
||||
field.setValue({ id: "df-object" });
|
||||
throw new Error("field.setValue() should reject object values");
|
||||
} catch (error) {
|
||||
if (!String(error.message).includes("serialize objects")) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
field.setValidity("Pick a file");
|
||||
assert(
|
||||
control.validationMessage === "Pick a file",
|
||||
"field.setValidity() should set custom validity",
|
||||
);
|
||||
assert(
|
||||
control.getAttribute("aria-invalid") === "true",
|
||||
"field.setValidity() should set aria-invalid",
|
||||
);
|
||||
assert(
|
||||
field.validationMessageElement && !field.validationMessageElement.hidden,
|
||||
"field.setValidity() should show a field validation message",
|
||||
);
|
||||
field.clearValidity();
|
||||
assert(
|
||||
control.validationMessage === "" &&
|
||||
control.getAttribute("aria-invalid") === null,
|
||||
"field.clearValidity() should clear custom validity",
|
||||
);
|
||||
|
||||
field.useSqliteDefault();
|
||||
assert(
|
||||
field.isUsingSqliteDefault() && control.disabled,
|
||||
"field.useSqliteDefault() should mark and disable control",
|
||||
);
|
||||
}
|
||||
""")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue