1073 lines
44 KiB
Java
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 & 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");
|
|
}
|
|
}
|