Files
NeitzelFXDesigner/designer-kit-ui/src/test/java/de/neitzel/fxdesigner/kit/ProjectionHostChecks.java
T

1073 lines
44 KiB
Java

package de.neitzel.fxdesigner.kit;
import com.oracle.javafx.scenebuilder.kit.fxom.*;
import de.neitzel.fxdesigner.contracts.*;
import de.neitzel.fxdesigner.host.DocumentSession;
import java.util.*;
import java.util.concurrent.*;
import javafx.application.Platform;
import javafx.scene.control.*;
/**
* Identical live UI/document scenarios used by native standalone and the isolated IDE companion.
*/
public final class ProjectionHostChecks {
private static <T> T fx(Callable<T> task) throws Exception {
var f = new CompletableFuture<T>();
Platform.runLater(
() -> {
try {
f.complete(task.call());
} catch (Throwable e) {
f.completeExceptionally(e);
}
});
return f.get(15, TimeUnit.SECONDS);
}
private static void await(Callable<Boolean> test) throws Exception {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(45);
while (!test.call()) {
if (System.nanoTime() > deadline) throw new AssertionError("Projection UI deadline");
Thread.sleep(50);
}
}
private static void check(boolean ok, String message) {
if (!ok) throw new AssertionError(message);
}
private static TextArea source(DesignerPane pane) {
return (TextArea)
pane.lookupAll(".text-area").stream()
.filter(n -> n instanceof TextArea t && t.isEditable())
.findFirst()
.orElseThrow();
}
private static FXOMInstance object(DesignerPane pane, String id) {
return (FXOMInstance) new FXOMFxIdIndex(pane.kit().editor().getFxomDocument()).lookup(id);
}
private static void click(DesignerPane pane, String text) throws Exception {
fx(
() -> {
((Button)
pane.lookupAll(".button").stream()
.filter(n -> n instanceof Button b && b.getText().equals(text))
.findFirst()
.orElseThrow())
.fire();
return null;
});
}
private static void set(DocumentSession document, String text) throws Exception {
var current = document.snapshot().toCompletableFuture().get();
document
.apply(
new EditBatch(
current.revision(),
"Fixture source edit",
List.of(new EditBatch.Replacement(0, current.text().length(), text))))
.toCompletableFuture()
.get();
}
private static void mouse(
javafx.scene.Node node,
javafx.event.EventType<javafx.scene.input.MouseEvent> type,
double dx,
double dy,
boolean down) {
mouse(node, type, dx, dy, down, node);
}
private static void mouse(
javafx.scene.Node node,
javafx.event.EventType<javafx.scene.input.MouseEvent> type,
double dx,
double dy,
boolean down,
javafx.scene.Node receiver) {
var p =
node.localToScene(
node.getBoundsInLocal().getCenterX(), node.getBoundsInLocal().getCenterY(), true);
var screen =
node.localToScreen(
node.getBoundsInLocal().getCenterX(), node.getBoundsInLocal().getCenterY());
receiver.fireEvent(
new javafx.scene.input.MouseEvent(
type,
p.getX() + dx,
p.getY() + dy,
screen.getX() + dx,
screen.getY() + dy,
javafx.scene.input.MouseButton.PRIMARY,
1,
false,
false,
false,
false,
down,
false,
false,
false,
false,
type != javafx.scene.input.MouseEvent.MOUSE_DRAGGED,
new javafx.scene.input.PickResult(node, p.getX() + dx, p.getY() + dy)));
}
public static void resizeCorner(DesignerPane pane, DocumentSession document) throws Exception {
String before = document.snapshot().toCompletableFuture().get().text();
fx(
() -> {
pane.kit().editor().getSelection().select(object(pane, "second"));
pane.applyCss();
pane.layout();
return null;
});
Thread.sleep(150);
double initialWidth =
fx(() -> ((SnapshotRegion) object(pane, "second").getSceneGraphObject()).getWidth());
double initialHeight =
fx(() -> ((SnapshotRegion) object(pane, "second").getSceneGraphObject()).getHeight());
fx(
() -> {
var handles = pane.content().lookupHandles(object(pane, "second"));
check(
handles
instanceof
com.oracle.javafx.scenebuilder.kit.editor.panel.content.driver.handles
.AbstractGenericHandles<
?>,
"Native resize handles missing");
var handle =
((com.oracle.javafx.scenebuilder.kit.editor.panel.content.driver.handles
.AbstractGenericHandles<
?>)
handles)
.getHandleNode(
com.oracle.javafx.scenebuilder.kit.editor.panel.content.util.CardinalPoint
.SE);
mouse(handle, javafx.scene.input.MouseEvent.MOUSE_PRESSED, 0, 0, true);
mouse(handle, javafx.scene.input.MouseEvent.DRAG_DETECTED, 20, 15, true);
mouse(handle, javafx.scene.input.MouseEvent.MOUSE_DRAGGED, 20, 15, true);
mouse(handle, javafx.scene.input.MouseEvent.MOUSE_RELEASED, 20, 15, false);
return null;
});
await(() -> !document.snapshot().toCompletableFuture().get().text().equals(before));
String resized = document.snapshot().toCompletableFuture().get().text();
check(
resized.contains("prefHeight=") && resized.contains("Card"),
"Resize must preserve real Component declaration");
var parser = javax.xml.parsers.DocumentBuilderFactory.newInstance();
parser.setNamespaceAware(true);
parser.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
var resizedXml =
parser
.newDocumentBuilder()
.parse(new org.xml.sax.InputSource(new java.io.StringReader(resized)));
var resizedNodes = resizedXml.getElementsByTagName("*");
org.w3c.dom.Element resizedCard = null;
for (int i = 0; i < resizedNodes.getLength(); i++) {
var candidate = (org.w3c.dom.Element) resizedNodes.item(i);
if (candidate.getAttributeNS("http://javafx.com/fxml/1", "id").equals("second"))
resizedCard = candidate;
}
String storedHeight = resizedCard.getAttribute("prefHeight");
check(
!storedHeight.isBlank() && Double.parseDouble(storedHeight) > initialHeight,
"Corner resize lost height: before="
+ initialHeight
+ " stored="
+ storedHeight
+ " source="
+ resized);
String storedWidth = resizedCard.getAttribute("prefWidth");
check(
!storedWidth.isBlank() && Double.parseDouble(storedWidth) > initialWidth,
"Corner resize lost width: before=" + initialWidth + " stored=" + storedWidth);
await(
() ->
fx(
() ->
source(pane).getText().equals(resized)
&& ((SnapshotRegion) object(pane, "second").getSceneGraphObject())
.getHeight()
> initialHeight));
click(pane, "Undo");
await(() -> document.snapshot().toCompletableFuture().get().text().equals(before));
await(() -> fx(() -> source(pane).getText().equals(before)));
click(pane, "Redo");
await(() -> document.snapshot().toCompletableFuture().get().text().equals(resized));
await(() -> fx(() -> source(pane).getText().equals(resized)));
click(pane, "Undo");
await(() -> document.snapshot().toCompletableFuture().get().text().equals(before));
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 boolean bindingStatus(DesignerPane pane, String text) throws Exception {
return fx(() -> ((Label) pane.lookup("#direct-status")).getText().contains(text));
}
private static void converterEdits(DesignerPane pane, DocumentSession document, String full)
throws Exception {
bindingTab(pane, "Direct bindings");
selectDirect(pane, "rankInput");
click(pane, "Find Converters / Validate");
await(() -> fx(() -> !((ComboBox<?>) pane.lookup("#direct-converters")).getItems().isEmpty()));
check(
fx(
() ->
((TextField) pane.lookup("#direct-converter"))
.getText()
.equals("fixture.components.RankConverter")),
"Existing Converter changed");
check(documentText(document).equals(full), "Validation altered source");
directField(pane, "converter", "");
click(pane, "Apply binding");
await(() -> !documentText(document).equals(full));
String invalid = documentText(document);
await(() -> fx(() -> source(pane).getText().equals(invalid)));
click(pane, "Render drafts");
await(() -> bindingStatus(pane, "Incompatible value types"));
await(
() ->
fx(
() ->
pane.lookupAll(".label").stream()
.anyMatch(
n ->
n instanceof Label l
&& l.getText() != null
&& l.getText().startsWith("Preview blocked:"))));
selectDirect(pane, "rankInput");
click(pane, "Find Converters / Validate");
await(
() ->
fx(
() ->
((TextField) pane.lookup("#direct-converter"))
.getText()
.equals("fixture.components.RankConverter")));
check(documentText(document).equals(invalid), "Proposal applied without review");
click(pane, "Apply binding");
await(() -> !documentText(document).equals(invalid));
String repaired = documentText(document);
click(pane, "Undo");
await(() -> documentText(document).equals(invalid));
click(pane, "Redo");
await(() -> documentText(document).equals(repaired));
await(() -> fx(() -> source(pane).getText().equals(repaired)));
selectDirect(pane, "rankInput");
directField(pane, "converter", "fixture.components.LegacyRankConverter");
click(pane, "Apply binding");
await(() -> documentText(document).contains("LegacyRankConverter"));
await(() -> fx(() -> source(pane).getText().contains("LegacyRankConverter")));
selectDirect(pane, "rankInput");
click(pane, "Find Converters / Validate");
await(() -> bindingStatus(pane, "WARNING rankInput.text"));
check(
fx(
() ->
((TextField) pane.lookup("#direct-converter"))
.getText()
.equals("fixture.components.LegacyRankConverter")),
"Unverified explicit Converter replaced");
set(document, full);
await(() -> fx(() -> source(pane).getText().equals(full)));
click(pane, "New binding");
directField(pane, "control", "action");
directField(pane, "property", "text");
directField(pane, "path", "name");
click(pane, "Find Converters / Validate");
await(() -> fx(() -> ((ComboBox<?>) pane.lookup("#direct-converters")).getItems().size() > 1));
check(
fx(() -> ((TextField) pane.lookup("#direct-converter")).getText().isEmpty()),
"Ambiguous candidates silently selected");
check(documentText(document).equals(full), "Converter browsing altered source");
}
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)));
}
private static void computedField(DesignerPane pane, String field, String value)
throws Exception {
fx(
() -> {
((TextField) pane.lookup("#computed-" + field)).setText(value);
return null;
});
}
private static void selectComputed(DesignerPane pane, String control) throws Exception {
fx(
() -> {
var list = (ListView<Object>) pane.lookup("#computed-bindings");
list.getSelectionModel()
.select(
list.getItems().stream()
.filter(e -> e.toString().startsWith(control + "."))
.findFirst()
.orElseThrow());
return null;
});
}
private static void computedBindingEdits(DesignerPane pane, DocumentSession document, String full)
throws Exception {
bindingTab(pane, "Computed bindings");
click(pane, "New Computed binding");
computedField(pane, "control", "action");
computedField(pane, "property", "text");
click(pane, "Find methods / Validate");
await(() -> fx(() -> !((ComboBox<?>) pane.lookup("#computed-sources")).getItems().isEmpty()));
fx(
() -> {
var methods =
(ComboBox<de.neitzel.fxdesigner.protocol.BindingMetadata.Computed>)
pane.lookup("#computed-sources");
var greeting =
methods.getItems().stream()
.filter(m -> m.method().equals("#greeting"))
.findFirst()
.orElseThrow();
check(greeting.status().equals("COMPATIBLE"), "Computed method type comparison failed");
methods.setValue(greeting);
return null;
});
check(documentText(document).equals(full), "Method choice changed source before Apply");
click(pane, "Apply Computed binding");
await(() -> !documentText(document).equals(full));
String created = documentText(document);
check(
created.contains(
"ComputedBinding control=\"action\" property=\"text\" method=\"#greeting\""),
"Computed creation failed");
click(pane, "Undo");
await(() -> documentText(document).equals(full));
click(pane, "Redo");
await(() -> documentText(document).equals(created));
await(() -> fx(() -> source(pane).getText().equals(created)));
selectComputed(pane, "action");
computedField(pane, "property", "accessibleText");
computedField(pane, "method", "greeting");
click(pane, "Apply Computed binding");
String changed =
created.replace(
"property=\"text\" method=\"#greeting\"",
"property=\"accessibleText\" method=\"greeting\"");
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),
"Computed save changed source");
click(pane, "Render drafts");
await(
() ->
fx(
() ->
object(pane, "home").getSceneGraphObject() instanceof SnapshotRegion r
&& r.image() != null
&& r.statusText().equals("Target runtime")));
selectComputed(pane, "action");
click(pane, "Remove Computed binding");
await(() -> !documentText(document).equals(changed));
String removed = documentText(document);
check(!removed.contains("ComputedBinding"), "Computed removal failed");
click(pane, "Undo");
await(() -> documentText(document).equals(changed));
click(pane, "Redo");
await(() -> documentText(document).equals(removed));
await(() -> fx(() -> source(pane).getText().equals(removed)));
click(pane, "New Computed binding");
computedField(pane, "control", "rankInput");
computedField(pane, "property", "text");
computedField(pane, "method", "#greeting");
click(pane, "Apply Computed binding");
await(
() ->
fx(
() ->
((Label) pane.lookup("#computed-status"))
.getText()
.contains("Replace conflicting bindings")));
check(documentText(document).equals(removed), "Computed silently replaced Direct binding");
fx(
() -> {
((CheckBox) pane.lookup("#computed-replace")).setSelected(true);
return null;
});
click(pane, "Apply Computed binding");
await(() -> !documentText(document).equals(removed));
String replaced = documentText(document);
check(
directDeclarations(replaced).stream()
.noneMatch(e -> e.getAttribute("control").equals("rankInput")),
"Computed replacement retained Direct conflict");
click(pane, "Undo");
await(() -> documentText(document).equals(removed));
click(pane, "Redo");
await(() -> documentText(document).equals(replaced));
await(() -> fx(() -> source(pane).getText().equals(replaced)));
selectComputed(pane, "rankInput");
computedField(pane, "method", "#invalidComputed");
click(pane, "Apply Computed binding");
await(() -> documentText(document).contains("method=\"#invalidComputed\""));
String invalid = documentText(document);
document
.save(document.snapshot().toCompletableFuture().get().revision())
.toCompletableFuture()
.get();
click(pane, "Render drafts");
await(
() ->
fx(
() ->
pane.lookupAll(".label").stream()
.anyMatch(
n ->
n instanceof Label l
&& l.getText() != null
&& l.getText().startsWith("Preview blocked:"))));
check(
java.nio.file.Files.readString(
java.nio.file.Path.of(document.snapshot().toCompletableFuture().get().document()))
.equals(invalid),
"Invalid Computed draft not saved");
click(pane, "Undo");
await(() -> documentText(document).equals(replaced));
click(pane, "Redo");
await(() -> documentText(document).equals(invalid));
set(document, full);
await(() -> fx(() -> source(pane).getText().equals(full)));
}
private static void viewModelBindings(
DesignerPane pane, DocumentSession document, String original) throws Exception {
bindingTab(pane, "Discover targets");
click(pane, "Discover bindings");
await(
() ->
fx(
() ->
((ComboBox<de.neitzel.fxdesigner.protocol.BindingMetadata.Attribute>)
pane.lookup("#binding-path"))
.getItems().stream().anyMatch(a -> a.path().equals("badge"))));
check(
fx(
() ->
((ListView<de.neitzel.fxdesigner.protocol.BindingMetadata.Computed>)
pane.lookup("#computed-methods"))
.getItems().stream().anyMatch(m -> m.method().equals("#greeting"))),
"Computed inventory missing");
check(documentText(document).equals(original), "ViewModel discovery altered FXML");
String invalid = original.replace("method=\"#greeting\"", "method=\"#invalidComputed\"");
check(!invalid.equals(original), "Computed fixture missing");
set(document, invalid);
await(() -> fx(() -> source(pane).getText().equals(invalid)));
bindingTab(pane, "Direct bindings");
click(pane, "Render drafts");
await(() -> bindingStatus(pane, "Computed return type"));
await(
() ->
fx(
() ->
pane.lookupAll(".label").stream()
.anyMatch(
n ->
n instanceof Label l
&& l.getText() != null
&& l.getText().startsWith("Preview blocked:"))));
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 Computed draft could not be saved");
click(pane, "Undo");
await(() -> documentText(document).equals(original));
click(pane, "Redo");
await(() -> documentText(document).equals(invalid));
click(pane, "Undo");
await(() -> documentText(document).equals(original));
String replacement =
java.nio.file.Files.readString(
java.nio.file.Path.of(document.snapshot().toCompletableFuture().get().document())
.resolveSibling("Replacement.fxml"));
set(document, replacement);
await(() -> fx(() -> source(pane).getText().equals(replacement)));
bindingTab(pane, "Discover targets");
click(pane, "Discover bindings");
await(
() ->
fx(
() ->
((Label) pane.lookup("#binding-status"))
.getText()
.contains("fixture.components.ReplacementViewModel")));
check(
fx(
() ->
((ComboBox<de.neitzel.fxdesigner.protocol.BindingMetadata.Attribute>)
pane.lookup("#binding-path"))
.getItems().stream().noneMatch(a -> a.path().equals("name"))),
"Replacement inherited Model attributes");
check(documentText(document).equals(replacement), "Replacement discovery altered FXML");
set(document, original);
await(() -> fx(() -> source(pane).getText().equals(original)));
}
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.
String nested =
"<?import javafx.scene.layout.AnchorPane?>\n"
+ original
.replace(
"<Card fx:id=\"second\"",
"<javafx.scene.layout.AnchorPane prefHeight='260'><fixture.components.Card"
+ " fx:id=\"second\" AnchorPane.leftAnchor='12' AnchorPane.topAnchor='8'")
.replace(
"caption=\"Second\"/>",
"caption=\"Second\"/></javafx.scene.layout.AnchorPane>");
check(!nested.equals(original), "Fixture must add nested Component");
set(document, nested);
await(() -> fx(() -> source(pane).getText().equals(nested) && object(pane, "second") != null));
fx(
() -> {
pane.kit().editor().getSelection().select(object(pane, "second"));
return null;
});
await(() -> fx(() -> pane.lookup("#component-property-caption") != null));
fx(
() -> {
var text = source(pane);
int at = text.getText().indexOf("path=\"home\"");
text.selectRange(at, at + 11);
var field = (TextField) pane.lookup("#component-property-caption");
field.setText("Nested & selected");
field.fireEvent(new javafx.event.ActionEvent());
return null;
});
String changed = nested.replace("caption=\"Second\"", "caption=\"Nested &amp; selected\"");
await(() -> document.snapshot().toCompletableFuture().get().text().equals(changed));
await(() -> fx(() -> source(pane).getText().equals(changed)));
check(
fx(() -> source(pane).getSelectedText().equals("path=\"home\"")),
"Unaffected source selection lost");
click(pane, "Undo");
await(() -> document.snapshot().toCompletableFuture().get().text().equals(nested));
click(pane, "Redo");
await(() -> document.snapshot().toCompletableFuture().get().text().equals(changed));
await(() -> fx(() -> source(pane).getText().equals(changed)));
click(pane, "Source");
click(pane, "Design");
click(pane, "Split");
check(fx(() -> source(pane).getText().equals(changed)), "Mode switch changed source");
click(pane, "Render drafts");
await(
() ->
fx(
() ->
object(pane, "second").getSceneGraphObject() instanceof SnapshotRegion r
&& r.image() != null
&& r.statusText().equals("Target runtime")));
fx(
() -> {
var item = object(pane, "second");
var node = (javafx.scene.Node) item.getSceneGraphObject();
var point = node.localToScene(node.getBoundsInLocal().getCenterX(), 5, true);
check(
pane.content().pick(point.getX(), point.getY()) == item, "Native Component picking");
pane.kit().editor().getSelection().select(item);
pane.applyCss();
pane.layout();
return null;
});
resizeCorner(pane, document);
// Exercise the exact Kit relocation command used after a canvas move. Native OS
// dragboard delivery is a separate manual/platform qualification, not synthesized here.
fx(
() -> {
var item = object(pane, "second");
pane.kit().editor().getSelection().select(item);
pane.kit()
.editor()
.getJobManager()
.push(
new com.oracle.javafx.scenebuilder.kit.editor.job.RelocateSelectionJob(
Map.of(item, new javafx.geometry.Point2D(42, 18)), pane.kit().editor()));
return null;
});
await(() -> !document.snapshot().toCompletableFuture().get().text().equals(changed));
check(
document.snapshot().toCompletableFuture().get().text().contains("AnchorPane.leftAnchor"),
"Attached constraints lost during move");
click(pane, "Undo");
await(() -> document.snapshot().toCompletableFuture().get().text().equals(changed));
await(() -> fx(() -> source(pane).getText().equals(changed)));
var snapshot = document.snapshot().toCompletableFuture().get();
document.save(snapshot.revision()).toCompletableFuture().get();
check(
java.nio.file.Files.readString(java.nio.file.Path.of(snapshot.document())).equals(changed),
"Save reformatted source: expected=["
+ changed
+ "] actual=["
+ java.nio.file.Files.readString(java.nio.file.Path.of(snapshot.document()))
+ "]");
try (var reopened =
pane.kit().host().documents().open(snapshot.document()).toCompletableFuture().get()) {
check(
reopened.snapshot().toCompletableFuture().get().text().equals(changed),
"Reopen lost source");
}
String anonymous =
changed.replace(
"</javafx.scene.layout.AnchorPane>",
"<fixture.components.Card caption='Anonymous'"
+ " layoutY='240'/></javafx.scene.layout.AnchorPane>");
var anonymousProjection = new FxmlProjection(anonymous);
anonymousProjection.project();
String anonymousId =
anonymousProjection.components().keySet().stream()
.filter(id -> id.startsWith("__designer_"))
.findFirst()
.orElseThrow();
set(document, anonymous);
await(
() ->
fx(
() ->
source(pane).getText().equals(anonymous) && object(pane, anonymousId) != null));
click(pane, "Render drafts");
await(
() ->
fx(
() ->
object(pane, anonymousId).getSceneGraphObject() instanceof SnapshotRegion r
&& r.image() != null
&& r.statusText().equals("Target runtime")));
check(
!document.snapshot().toCompletableFuture().get().text().contains("__designer_"),
"Preview identity leaked into source");
set(document, changed);
await(() -> fx(() -> source(pane).getText().equals(changed)));
// Incomplete source retains the old canvas, but blocks visual editing until recovery.
set(document, changed + "<");
await(() -> fx(() -> source(pane).getText().equals(changed + "<")));
check(
fx(() -> ((javafx.scene.Node) object(pane, "second").getSceneGraphObject()).isDisabled()),
"Invalid source leaves canvas editable");
set(document, original);
await(
() ->
fx(
() ->
source(pane).getText().equals(original)
&& !((javafx.scene.Node) object(pane, "second").getSceneGraphObject())
.isDisabled()));
// fx:root Component editing needs no application class in this host.
String root =
"<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><fx:root"
+ " type='javafx.scene.layout.VBox' xmlns:fx='http://javafx.com/fxml/1'><Label"
+ " text='root text'/></fx:root>";
set(document, root);
await(
() ->
fx(
() ->
source(pane).getText().equals(root)
&& pane.kit()
.editor()
.getFxomDocument()
.getFxmlText(false)
.contains("root text")));
fx(
() -> {
var item = object(pane, "__designer_1");
pane.kit().editor().getSelection().select(item);
return null;
});
await(
() ->
fx(
() ->
pane.lookupAll(".text-field").stream()
.anyMatch(
n -> n instanceof TextField t && t.getText().equals("root text"))));
fx(
() -> {
var field =
(TextField)
pane.lookupAll(".text-field").stream()
.filter(n -> n instanceof TextField t && t.getText().equals("root text"))
.findFirst()
.orElseThrow();
field.setText("root changed");
field.fireEvent(new javafx.event.ActionEvent());
source(pane).appendText("<!-- queued source edit -->");
return null;
});
await(
() ->
document
.snapshot()
.toCompletableFuture()
.get()
.text()
.equals(root.replace("root text", "root changed") + "<!-- queued source edit -->"));
var fullFile =
java.nio.file.Path.of(document.snapshot().toCompletableFuture().get().document())
.resolveSibling("Full.fxml");
String full = java.nio.file.Files.readString(fullFile);
set(document, full);
await(() -> fx(() -> source(pane).getText().equals(full) && object(pane, "home") != null));
click(pane, "Render drafts");
await(
() ->
fx(
() ->
java.util.List.of("home", "visits", "table").stream()
.allMatch(
id ->
object(pane, id).getSceneGraphObject() instanceof SnapshotRegion r
&& r.image() != null
&& r.statusText().equals("Target runtime"))));
check(
document.snapshot().toCompletableFuture().get().text().equals(full),
"Runtime sizing changed source");
fx(
() -> {
pane.lookupAll(".titled-pane").stream()
.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(
() ->
fx(
() ->
((TableView<?>) pane.lookup("#binding-targets"))
.getItems().stream()
.anyMatch(
v ->
v
instanceof
de.neitzel.fxdesigner.protocol.BindingMetadata.Target t
&& t.control().equals("rankInput")
&& t.property().equals("text")
&& t.status().equals("COMPATIBLE"))));
fx(
() -> {
var paths =
(ComboBox<de.neitzel.fxdesigner.protocol.BindingMetadata.Attribute>)
pane.lookup("#binding-path");
check(
paths.getItems().stream().anyMatch(a -> a.path().equals("home.street")),
"Binding browser lacks nested path");
paths.setValue(
paths.getItems().stream()
.filter(a -> a.path().equals("rank"))
.findFirst()
.orElseThrow());
return null;
});
await(
() ->
fx(
() ->
((TableView<?>) pane.lookup("#binding-targets"))
.getItems().stream()
.anyMatch(
v ->
v
instanceof
de.neitzel.fxdesigner.protocol.BindingMetadata.Target t
&& t.control().equals("rankInput")
&& t.property().equals("text")
&& t.status().equals("CONVERTER_REQUIRED"))));
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");
converterEdits(pane, document, full);
directBindingEdits(pane, document, full);
computedBindingEdits(pane, document, full);
document
.save(document.snapshot().toCompletableFuture().get().revision())
.toCompletableFuture()
.get();
click(pane, "Interactive preview");
await(
() ->
pane.interactivePid() > 0
&& ProcessHandle.of(pane.interactivePid())
.map(ProcessHandle::isAlive)
.orElse(false));
long interactivePid = pane.interactivePid();
String editedFull = full + "<!-- unsaved interactive draft -->";
set(document, editedFull);
await(() -> fx(() -> source(pane).getText().equals(editedFull)));
Thread.sleep(500);
check(pane.interactivePid() == interactivePid, "Unsaved edit restarted interactive sample");
document
.save(document.snapshot().toCompletableFuture().get().revision())
.toCompletableFuture()
.get();
await(() -> pane.interactivePid() > 0 && pane.interactivePid() != interactivePid);
await(() -> ProcessHandle.of(interactivePid).map(p -> !p.isAlive()).orElse(true));
long restarted = pane.interactivePid();
click(pane, "Stop preview");
await(() -> ProcessHandle.of(restarted).map(p -> !p.isAlive()).orElse(true));
set(document, original);
await(() -> fx(() -> source(pane).getText().equals(original) && object(pane, "card") != null));
viewModelBindings(pane, document, original);
document
.save(document.snapshot().toCompletableFuture().get().revision())
.toCompletableFuture()
.get();
click(pane, "Render drafts");
await(
() ->
fx(
() ->
object(pane, "card").getSceneGraphObject() instanceof SnapshotRegion r
&& r.image() != null
&& r.statusText().equals("Target runtime")));
System.out.println(
"PASS shared projection: exact source, picking/resize/relocation, nested/FQ/anonymous"
+ " Components, declared properties, selection, undo/redo, modes, target load,"
+ " save/reopen, invalid recovery, fx:root");
}
}