Add shared source-preserving DirectBinding inspector

This commit is contained in:
2026-11-13 12:35:20 +01:00
parent 2fe408bb4d
commit 17b1bce704
11 changed files with 939 additions and 12 deletions
@@ -207,6 +207,199 @@ public final class ProjectionHostChecks {
await(() -> fx(() -> source(pane).getText().equals(before)));
}
private static void bindingTab(DesignerPane pane, String name) throws Exception {
fx(
() -> {
pane.lookupAll(".tab-pane").stream()
.filter(n -> n instanceof TabPane)
.map(n -> (TabPane) n)
.forEach(
t ->
t.getTabs().stream()
.filter(tab -> tab.getText().equals(name))
.findFirst()
.ifPresent(tab -> t.getSelectionModel().select(tab)));
return null;
});
}
private static void directField(DesignerPane pane, String field, String value) throws Exception {
fx(
() -> {
((TextField) pane.lookup("#direct-" + field)).setText(value);
return null;
});
}
private static void selectDirect(DesignerPane pane, String control) throws Exception {
fx(
() -> {
var list = (ListView<Object>) pane.lookup("#direct-bindings");
list.getSelectionModel()
.select(
list.getItems().stream()
.filter(e -> e.toString().startsWith(control + "."))
.findFirst()
.orElseThrow());
return null;
});
}
// The live IDE probe uses a separate plugin loader: assert exported UI and XML, not internals.
private static List<org.w3c.dom.Element> directDeclarations(String text) throws Exception {
var factory = javax.xml.parsers.DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
var parsed =
factory
.newDocumentBuilder()
.parse(new org.xml.sax.InputSource(new java.io.StringReader(text)));
var nodes = parsed.getElementsByTagName("*");
var result = new ArrayList<org.w3c.dom.Element>();
for (int i = 0; i < nodes.getLength(); i++) {
var element = (org.w3c.dom.Element) nodes.item(i);
if (Set.of("DirectBinding", "de.neitzel.fx.view.DirectBinding")
.contains(element.getTagName())) result.add(element);
}
return result;
}
private static String documentText(DocumentSession doc) throws Exception {
return doc.snapshot().toCompletableFuture().get().text();
}
private static void directBindingEdits(DesignerPane pane, DocumentSession document, String full)
throws Exception {
bindingTab(pane, "Direct bindings");
click(pane, "New binding");
directField(pane, "control", "action");
directField(pane, "property", "text");
directField(pane, "path", "name");
fx(
() -> {
((ComboBox<String>) pane.lookup("#direct-direction")).setValue("view");
return null;
});
click(pane, "Apply binding");
await(() -> !documentText(document).equals(full));
String created = documentText(document);
check(
directDeclarations(created).stream()
.anyMatch(
e ->
e.getAttribute("control").equals("action")
&& e.getAttribute("path").equals("name")),
"Create binding failed");
click(pane, "Undo");
await(() -> documentText(document).equals(full));
click(pane, "Redo");
await(() -> documentText(document).equals(created));
await(() -> fx(() -> source(pane).getText().equals(created)));
selectDirect(pane, "action");
directField(pane, "path", "home.street");
click(pane, "Apply binding");
String changed =
created.replace(
"path=\"name\" direction=\"view\"", "path=\"home.street\" direction=\"view\"");
await(() -> documentText(document).equals(changed));
click(pane, "Undo");
await(() -> documentText(document).equals(created));
click(pane, "Redo");
await(() -> documentText(document).equals(changed));
await(() -> fx(() -> source(pane).getText().equals(changed)));
document
.save(document.snapshot().toCompletableFuture().get().revision())
.toCompletableFuture()
.get();
check(
java.nio.file.Files.readString(
java.nio.file.Path.of(document.snapshot().toCompletableFuture().get().document()))
.equals(changed),
"Binding save changed source");
click(pane, "Render drafts");
await(
() ->
fx(
() ->
object(pane, "home").getSceneGraphObject() instanceof SnapshotRegion r
&& r.image() != null
&& r.statusText().equals("Target runtime")));
selectDirect(pane, "action");
click(pane, "Remove binding");
await(() -> !documentText(document).equals(changed));
String removed = documentText(document);
check(
directDeclarations(removed).stream()
.noneMatch(e -> e.getAttribute("control").equals("action")),
"Remove binding failed");
click(pane, "Undo");
await(() -> documentText(document).equals(changed));
click(pane, "Redo");
await(() -> documentText(document).equals(removed));
await(() -> fx(() -> source(pane).getText().equals(removed)));
// Conflicting creation requires explicit replacement and remains one history entry.
click(pane, "New binding");
directField(pane, "control", "rankInput");
directField(pane, "property", "text");
directField(pane, "path", "name");
fx(
() -> {
((ComboBox<String>) pane.lookup("#direct-direction")).setValue("view");
return null;
});
click(pane, "Apply binding");
check(documentText(document).equals(removed), "Conflict silently overwrote a binding");
fx(
() -> {
((CheckBox) pane.lookup("#direct-replace")).setSelected(true);
return null;
});
click(pane, "Apply binding");
await(() -> !documentText(document).equals(removed));
String replaced = documentText(document);
var rank =
directDeclarations(replaced).stream()
.filter(e -> e.getAttribute("control").equals("rankInput"))
.toList();
check(
rank.size() == 1 && rank.getFirst().getAttribute("path").equals("name"),
"Explicit replace failed");
click(pane, "Undo");
await(() -> documentText(document).equals(removed));
click(pane, "Redo");
await(() -> documentText(document).equals(replaced));
await(() -> fx(() -> source(pane).getText().equals(replaced)));
selectDirect(pane, "rankInput");
directField(pane, "path", "invalid path");
click(pane, "Apply binding");
await(() -> documentText(document).contains("path=\"invalid path\""));
String invalid = documentText(document);
await(() -> fx(() -> source(pane).getText().equals(invalid)));
document
.save(document.snapshot().toCompletableFuture().get().revision())
.toCompletableFuture()
.get();
check(
java.nio.file.Files.readString(
java.nio.file.Path.of(document.snapshot().toCompletableFuture().get().document()))
.equals(invalid),
"Invalid binding could not be saved");
click(pane, "Render drafts");
await(
() ->
fx(
() ->
pane.lookupAll(".label").stream()
.anyMatch(
n ->
n instanceof Label l
&& l.getText() != null
&& l.getText().startsWith("Preview blocked:"))));
click(pane, "Undo");
await(() -> documentText(document).equals(replaced));
set(document, full);
await(() -> fx(() -> source(pane).getText().equals(full)));
}
public static void run(DesignerPane pane, DocumentSession document) throws Exception {
String original = document.snapshot().toCompletableFuture().get().text();
// The same real application, now with nested, fully-qualified Component declarations.
@@ -429,11 +622,12 @@ public final class ProjectionHostChecks {
fx(
() -> {
pane.lookupAll(".titled-pane").stream()
.filter(n -> n instanceof TitledPane t && t.getText().equals("Bindings — discovery"))
.filter(n -> n instanceof TitledPane t && t.getText().equals("Bindings"))
.map(n -> (TitledPane) n)
.forEach(t -> t.setExpanded(true));
return null;
});
bindingTab(pane, "Discover targets");
click(pane, "Discover bindings");
await(
() ->
@@ -481,6 +675,29 @@ public final class ProjectionHostChecks {
check(
document.snapshot().toCompletableFuture().get().text().equals(full),
"Binding discovery altered source");
fx(
() -> {
var targets =
(TableView<de.neitzel.fxdesigner.protocol.BindingMetadata.Target>)
pane.lookup("#binding-targets");
targets
.getSelectionModel()
.select(
targets.getItems().stream()
.filter(t -> t.control().equals("rankInput") && t.property().equals("text"))
.findFirst()
.orElseThrow());
return null;
});
click(pane, "Use selected target");
check(
fx(
() ->
((TextField) pane.lookup("#direct-control")).getText().equals("rankInput")
&& ((TextField) pane.lookup("#direct-path")).getText().equals("rank")),
"Discovery did not populate binding form");
check(documentText(document).equals(full), "Discovery handoff changed source before Apply");
directBindingEdits(pane, document, full);
document
.save(document.snapshot().toCompletableFuture().get().revision())
.toCompletableFuture()