Implement retained target sessions, runtime sizing and Component navigation

This commit is contained in:
2026-11-12 14:20:26 +01:00
parent e41b276b13
commit 6e25cf3ac6
71 changed files with 2538 additions and 120 deletions
@@ -41,9 +41,16 @@ public final class PreviewChecks {
public static void main(String[] args) throws Exception {
String mode = args[0];
if (mode.equals("unstable")
|| mode.equals("full")
|| mode.equals("full-empty")
|| mode.equals("card")) {
StageChecks.run(mode);
return;
}
ClassLoader loader = PreviewChecks.class.getClassLoader();
var descriptors = ExampleCatalog.discover(loader);
check(descriptors.size() == 9, "Descriptor discovery");
check(descriptors.size() == 15, "Descriptor discovery");
check(
System.getProperty("fixture.provider.initialized") == null,
"Discovery initialized provider");
@@ -0,0 +1,198 @@
package fixture.checks;
import de.neitzel.fxdesigner.target.*;
import fixture.components.*;
import fixture.preview.Examples;
import java.time.Duration;
import java.util.concurrent.*;
import javafx.application.Platform;
import javafx.scene.control.*;
import javafx.scene.layout.*;
public final class StageChecks {
public static void run(String mode) throws Exception {
var started = new CountDownLatch(1);
Platform.startup(
() -> {
Platform.setImplicitExit(false);
started.countDown();
});
if (!started.await(10, TimeUnit.SECONDS)) throw new AssertionError("FX startup");
var descriptor =
ExampleCatalog.discover(StageChecks.class.getClassLoader()).stream()
.filter(e -> e.id().equals(mode.equals("unstable") ? "full" : mode))
.findFirst()
.orElseThrow();
if (mode.equals("unstable"))
descriptor =
new de.neitzel.fxdesigner.preview.ExampleDescriptor(
descriptor.id(),
descriptor.label(),
descriptor.modelType(),
descriptor.providerClass(),
"fixture/components/Unstable.fxml",
descriptor.componentPackage());
var preview =
TargetPreview.open(descriptor, StageChecks.class.getClassLoader(), Duration.ofSeconds(15));
try {
if (mode.equals("unstable")) {
try {
preview.ready().toCompletableFuture().get(20, TimeUnit.SECONDS);
throw new AssertionError("Unstable layout accepted");
} catch (ExecutionException expected) {
Throwable error = expected;
boolean found = false;
while (error != null) {
if (String.valueOf(error.getMessage()).contains("did not converge")) found = true;
error = error.getCause();
}
PreviewChecks.check(found, "Missing non-convergence diagnostic");
}
System.out.println("PASS unstable " + PreviewChecks.checks);
return;
}
var ready = preview.ready().toCompletableFuture().get(20, TimeUnit.SECONDS);
PreviewChecks.fx(
() -> {
var root = ready.loadedView().getRoot();
if (mode.equals("card")) {
var card = (Card) root;
PreviewChecks.check(
card.street().getText().equals("Independent Card sample"),
"Independent Component Model");
card.street().setText("Edited independently");
PreviewChecks.check(
((fixture.model.Address) card.getModel())
.getStreet()
.equals("Edited independently"),
"Independent write-back");
} else if (mode.equals("full-empty")) {
PreviewChecks.check(
((AddressForm) root.lookup("#different-css-id")).getModel() == null,
"Missing nested Model");
PreviewChecks.check(
((VBox) root.lookup("#visits")).getChildren().isEmpty(), "Empty repeats");
PreviewChecks.check(
ready.occurrences().values().stream().noneMatch(VisitRow.class.getName()::equals),
"Empty table");
} else {
var rank = (TextField) root.lookup("#rankInput");
rank.setText("invalid");
PreviewChecks.check(
Examples.person.getRank() == 7, "Conversion failure changed Model");
rank.setText("42");
PreviewChecks.check(Examples.person.getRank() == 42, "Conversion recovery");
((Button) root.lookup("#action")).fire();
PreviewChecks.check(
Examples.person.getName().equals("Controller action"), "Controller action");
var home = (AddressForm) root.lookup("#different-css-id");
Occurrences.setKey(home, "home");
var note = (NoteForm) home.lookup("#nested");
PreviewChecks.check(
home.getModel() == Examples.person.getHome()
&& note.getModel() == home.getModel(),
"Nested Model identity");
var visits = (VBox) root.lookup("#visits");
PreviewChecks.check(visits.getChildren().size() == 2, "Repeat instances");
PreviewChecks.check(
((AddressForm) visits.getChildren().getFirst()).getModel() == home.getModel(),
"Repeated shared Model");
var narrow = ready.measure("home", 100, 150);
var wide = ready.measure("home", 600, 150);
PreviewChecks.check(
narrow.prefHeight() > wide.prefHeight(), "Width-dependent wrapping");
PreviewChecks.check(
narrow.occurrence().equals(wide.occurrence()),
"Stable occurrence across measurements");
PreviewChecks.check(
ready.renderNode(narrow.occurrence(), 100, 150).length == 60000,
"Occurrence rendering");
PreviewChecks.check(
home.getWidth() == 100 && home.getHeight() == 150,
"Allocated target size lost during capture");
PreviewChecks.check(
home.getId().equals("different-css-id"), "CSS identity overwritten");
var metadata = ready.occurrences();
long notes =
metadata.entrySet().stream()
.filter(
e ->
e.getKey().endsWith(".type")
&& e.getValue().equals(NoteForm.class.getName()))
.count();
PreviewChecks.check(notes == 3, "Nested repeated occurrence discovery");
long rows =
metadata.entrySet().stream()
.filter(
e ->
e.getKey().endsWith(".type")
&& e.getValue().equals(VisitRow.class.getName()))
.count();
PreviewChecks.check(rows == 2, "Table row occurrences");
var table = (de.neitzel.fx.view.Table) root.lookup("#table");
table.setSelection(Examples.person.getVisits().get(1));
PreviewChecks.check(
table.getSelection() == Examples.person.getVisits().get(1), "Table selection");
var firstRow =
(VisitRow)
table.lookupAll("*").stream()
.filter(
n ->
n instanceof VisitRow r
&& r.getModel() == Examples.person.getHome())
.findFirst()
.orElseThrow();
((TextField) firstRow.lookup("#street")).setText("Changed through Table");
PreviewChecks.check(
Examples.person.getHome().getStreet().equals("Changed through Table"),
"Table write-back");
PreviewChecks.check(
((Label) note.lookup("#note")).getText().equals("Changed through Table"),
"Shared nested binding refresh");
var body = (Pane) firstRow.getParent();
((Button) table.lookup(".column-opener")).fire();
table.lookupAll(".button").stream()
.filter(n -> n instanceof Button b && b.getText().equals("Descending"))
.map(n -> (Button) n)
.findFirst()
.orElseThrow()
.fire();
PreviewChecks.check(
((VisitRow) body.getChildren().getFirst()).getModel()
== Examples.person.getVisits().get(1),
"Interactive Table sorting");
((Button) table.lookup(".column-opener")).fire();
table.lookupAll(".check-box").stream()
.filter(n -> n instanceof CheckBox c && c.getText().equals("Second visit"))
.map(n -> (CheckBox) n)
.findFirst()
.orElseThrow()
.setSelected(false);
table.lookupAll(".button").stream()
.filter(n -> n instanceof Button b && b.getText().equals("Confirm"))
.map(n -> (Button) n)
.findFirst()
.orElseThrow()
.fire();
PreviewChecks.check(
body.getChildren().size() == 1
&& ((VisitRow) body.getChildren().getFirst()).getModel()
== Examples.person.getHome(),
"Interactive Table filtering");
PreviewChecks.check(
Examples.person.getVisits().size() == 2, "Filtering changed Model collection");
}
ready.showInteractive("Stage 3 automated interactive fixture", () -> {});
PreviewChecks.check(
javafx.stage.Window.getWindows().stream().anyMatch(javafx.stage.Window::isShowing),
"Interactive window");
return null;
});
} finally {
preview.close();
preview.cleanup().toCompletableFuture().get(5, TimeUnit.SECONDS);
Platform.exit();
}
System.out.println("PASS " + mode + " " + PreviewChecks.checks);
}
}
@@ -2,6 +2,7 @@ module fixture.checks {
requires fixture.app;
requires fixture.preview;
requires de.neitzel.fxdesigner.target;
requires de.neitzel.fxdesigner.contracts;
requires de.neitzel.fxdesigner.preview;
requires fx;
requires injection;