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 fx(Callable task) throws Exception { var f = new CompletableFuture(); Platform.runLater( () -> { try { f.complete(task.call()); } catch (Throwable e) { f.completeExceptionally(e); } }); return f.get(15, TimeUnit.SECONDS); } private static void await(Callable 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 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 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) 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 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(); 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) 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) 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) 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) 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) pane.lookup("#binding-path")) .getItems().stream().anyMatch(a -> a.path().equals("badge")))); check( fx( () -> ((ListView) 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) 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 = "\n" + original .replace( "", "caption=\"Second\"/>"); 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 & 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( "", ""); 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 = ""; 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(""); return null; }); await( () -> document .snapshot() .toCompletableFuture() .get() .text() .equals(root.replace("root text", "root changed") + "")); 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) 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) 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 + ""; 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"); } }