Validate preview sessions against real classpath and module-path fixtures

This commit is contained in:
2026-11-12 09:47:59 +01:00
parent e97af99412
commit 28c1e47c95
52 changed files with 1867 additions and 9 deletions
@@ -0,0 +1,24 @@
<?xml version="1.0" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.neitzel.fxdesigner.fixtures</groupId>
<artifactId>preview-classpath</artifactId>
<version>1</version>
</parent>
<artifactId>app-classpath</artifactId>
<dependencies>
<dependency>
<groupId>de.neitzel.lib</groupId>
<artifactId>fx</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>de.neitzel.fxdesigner</groupId>
<artifactId>designer-target-tools</artifactId>
<version>0.1.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,51 @@
package fixture.components;
@jakarta.inject.Singleton
public class Card extends javafx.scene.layout.VBox implements de.neitzel.fx.view.HasViewModel {
public final Greeting greeting;
private final javafx.beans.property.ObjectProperty<Object> model =
new javafx.beans.property.SimpleObjectProperty<>();
@javafx.fxml.FXML private javafx.scene.control.Label captionLabel;
@javafx.fxml.FXML private javafx.scene.control.TextField street;
public de.neitzel.fx.view.ViewModel received;
public boolean injected;
public Card(Greeting greeting) {
this.greeting = greeting;
}
@javafx.fxml.FXML
public void initialize() {
injected = true;
captionLabel.setText(greeting.prefix);
}
public String getCaption() {
return captionLabel.getText();
}
public void setCaption(String value) {
if (!injected) throw new IllegalStateException("Caption applied before injection");
captionLabel.setText(value);
}
public javafx.beans.property.ObjectProperty<Object> modelProperty() {
return model;
}
public Object getModel() {
return model.get();
}
public void setModel(Object value) {
model.set(value);
}
public void setViewModel(de.neitzel.fx.view.ViewModel vm) {
received = vm;
}
public javafx.scene.control.TextField street() {
return street;
}
}
@@ -0,0 +1,16 @@
package fixture.components;
@jakarta.inject.Singleton
public class Greeting implements AutoCloseable {
public final String identity = java.util.UUID.randomUUID().toString();
public String prefix = "sample";
public int closes;
public void close() {
closes++;
}
public String text() {
return prefix + "-" + identity;
}
}
@@ -0,0 +1,22 @@
package fixture.components;
@jakarta.inject.Singleton
@de.neitzel.fx.view.ViewModelClass(PersonViewModel.class)
public class PersonController implements de.neitzel.fx.view.HasViewModel {
public final Greeting greeting;
public de.neitzel.fx.view.ViewModel received;
public PersonController(Greeting greeting) {
this.greeting = greeting;
System.setProperty("fixture.controllers", "constructed");
}
public void setViewModel(de.neitzel.fx.view.ViewModel vm) {
received = vm;
}
public String greeting() {
if (received == null) throw new IllegalStateException("No ViewModel before bindings");
return greeting.text();
}
}
@@ -0,0 +1,7 @@
package fixture.components;
public class PersonViewModel extends de.neitzel.fx.view.ViewModel {
public PersonViewModel(Object model) {
super(model);
}
}
@@ -0,0 +1,13 @@
package fixture.model;
public class Address {
private String street = "First Street";
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
}
@@ -0,0 +1,28 @@
package fixture.model;
public class Person {
private String name = "Ada";
private Address home = new Address();
public Person() {
System.setProperty(
"fixture.models",
Integer.toString(Integer.parseInt(System.getProperty("fixture.models", "0")) + 1));
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Address getHome() {
return home;
}
public void setHome(Address home) {
this.home = home;
}
}
@@ -0,0 +1,2 @@
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import javafx.scene.control.TextField?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?>
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml/1"><fx:define><BindingList><DirectBinding control="street" property="text" path="street" direction="both"/></BindingList></fx:define><Label fx:id="captionLabel"/><TextField fx:id="street"/></fx:root>
@@ -0,0 +1,2 @@
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import javafx.scene.control.TextField?><?import fixture.components.Card?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?><?import de.neitzel.fx.view.ComputedBinding?>
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="fixture.components.PersonController" prefWidth="420" prefHeight="240"><fx:define><BindingList><DirectBinding control="name" property="text" path="name" direction="both"/><DirectBinding control="card" property="model" path="home" direction="both"/><DirectBinding control="second" property="model" path="home" direction="both"/><ComputedBinding control="greeting" property="text" method="#greeting"/></BindingList></fx:define><TextField fx:id="name"/><Label fx:id="greeting"/><Card fx:id="card" caption="FXML caption" prefWidth="not-a-number" alignment="CENTER_LEFT"/><Card fx:id="second" caption="Second"/></VBox>
@@ -0,0 +1,2 @@
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import javafx.scene.control.TextField?><?import fixture.components.Card?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?><?import de.neitzel.fx.view.ComputedBinding?>
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="fixture.components.PersonController" prefWidth="420" prefHeight="240"><fx:define><BindingList><DirectBinding control="name" property="text" path="name" direction="both"/><DirectBinding control="card" property="model" path="home" direction="both"/><DirectBinding control="second" property="model" path="home" direction="both"/><ComputedBinding control="greeting" property="text" method="#greeting"/></BindingList></fx:define><TextField fx:id="name"/><Label fx:id="greeting"/><Card fx:id="card" caption="FXML caption" prefWidth="310" alignment="CENTER_LEFT"/><Card fx:id="second" caption="Second"/></VBox>
@@ -0,0 +1,197 @@
package fixture.checks;
import de.neitzel.fxdesigner.preview.*;
import de.neitzel.fxdesigner.target.*;
import fixture.components.*;
import fixture.model.*;
import fixture.preview.Examples;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.control.*;
public final class PreviewChecks {
static int checks;
static void check(boolean b, String message) {
if (!b) throw new AssertionError(message);
checks++;
}
static <T> T fx(Callable<T> action) throws Exception {
var result = new CompletableFuture<T>();
Platform.runLater(
() -> {
try {
result.complete(action.call());
} catch (Throwable e) {
result.completeExceptionally(e);
}
});
return result.get(10, TimeUnit.SECONDS);
}
static PreviewFailure failure(Throwable error) {
for (Throwable t = error; t != null; t = t.getCause())
if (t instanceof PreviewFailure f) return f;
throw new AssertionError("No structured preview diagnostic", error);
}
public static void main(String[] args) throws Exception {
String mode = args[0];
ClassLoader loader = PreviewChecks.class.getClassLoader();
var descriptors = ExampleCatalog.discover(loader);
check(descriptors.size() == 9, "Descriptor discovery");
check(
System.getProperty("fixture.provider.initialized") == null,
"Discovery initialized provider");
check(System.getProperty("fixture.models") == null, "Discovery constructed Model");
check(System.getProperty("fixture.controllers") == null, "Discovery constructed Controller");
if (mode.equals("discovery")) {
System.out.println("PASS discovery " + checks);
return;
}
var started = new CountDownLatch(1);
Platform.startup(
() -> {
Platform.setImplicitExit(false);
started.countDown();
});
check(started.await(10, TimeUnit.SECONDS), "FX startup");
TargetPreview preview = null;
try {
String id = mode.startsWith("missing-") ? "standard" : mode;
var descriptor =
descriptors.stream().filter(d -> d.id().equals(id)).findFirst().orElseThrow();
preview =
TargetPreview.open(
descriptor, loader, Duration.ofMillis(mode.equals("timeout") ? 2000 : 10000));
if (mode.equals("async") || mode.equals("cancel")) {
for (int i = 0; i < 100 && Examples.pending == null; i++) Thread.sleep(20);
check(Examples.pending != null, "Provider readiness created");
check(!preview.ready().toCompletableFuture().isDone(), "Premature readiness");
if (mode.equals("async")) Examples.pending.complete(null);
else preview.cancel();
}
if (java.util.Set.of(
"timeout",
"cancel",
"readiness-failure",
"wrong-model",
"invalid-attribute",
"factory-failure",
"missing-components",
"missing-model")
.contains(mode)) {
try {
preview.ready().toCompletableFuture().get(15, TimeUnit.SECONDS);
throw new AssertionError("Failure accepted: " + mode);
} catch (ExecutionException error) {
var f = failure(error);
var expected =
switch (mode) {
case "timeout" -> PreviewFailure.Code.TIMEOUT;
case "cancel" -> PreviewFailure.Code.CANCELLED;
case "readiness-failure" -> PreviewFailure.Code.READINESS;
case "wrong-model" -> PreviewFailure.Code.MODEL_TYPE;
case "missing-components", "missing-model" -> PreviewFailure.Code.MODULE_ACCESS;
default -> PreviewFailure.Code.LOAD;
};
check(f.code() == expected, "Wrong diagnostic: " + f.code() + " expected " + expected);
}
preview.cleanup().toCompletableFuture().get(5, TimeUnit.SECONDS);
if (!mode.equals("missing-components")) {
check(Examples.service.closes == 1, "Failure did not clean services once");
if (!mode.equals("factory-failure"))
check(Examples.closes == 1, "Session cleanup missing");
}
} else {
TargetPreview ready = preview.ready().toCompletableFuture().get(15, TimeUnit.SECONDS);
fx(
() -> {
var view = ready.loadedView();
check(view.getViewModel() instanceof PersonViewModel, "Named ViewModel not selected");
var controller = Examples.context.getComponent(PersonController.class);
check(controller.received == view.getViewModel(), "Controller ViewModel handoff");
check(controller.greeting == Examples.service, "Configured service not injected");
var card = (Card) view.getRoot().lookup("#card");
var second = (Card) view.getRoot().lookup("#second");
check(card != second, "Component instances reused");
check(
card.getModel() == Examples.person.getHome()
&& second.getModel() == card.getModel(),
"Nested Model identity not shared");
check(card.received == second.received, "Nested ViewModel identity changed");
check(
card.greeting == Examples.service && second.greeting == Examples.service,
"Component injection");
check(card.getCaption().equals("FXML caption"), "Custom caption lost");
check(
card.getPrefWidth() == 310 && card.getAlignment() == Pos.CENTER_LEFT,
"Inherited/coerced attributes lost");
check(card.injected, "Component initialization");
TextField name = (TextField) view.getRoot().lookup("#name");
check(name.getText().equals("Ada"), "Initial parent binding");
name.setText("Grace");
check(Examples.person.getName().equals("Grace"), "Parent write-back");
card.street().setText("Changed Street");
check(
Examples.person.getHome().getStreet().equals("Changed Street")
&& second.street().getText().equals("Changed Street"),
"Component write-back and shared Model");
check(
((Label) view.getRoot().lookup("#greeting")).getText().startsWith("configured-"),
"Computed controller binding");
byte[] rgba = ready.render(420, 240);
check(rgba.length == 420 * 240 * 4, "Render size");
boolean nonzero = false;
for (byte value : rgba)
if (value != 0) {
nonzero = true;
break;
}
check(nonzero, "Blank rendering");
try {
ready.render(8192, 8192);
throw new AssertionError("Oversized image allowed");
} catch (IllegalArgumentException expected) {
checks++;
}
return null;
});
System.out.println(
"SESSION "
+ Examples.service.identity
+ " models="
+ System.getProperty("fixture.models"));
preview.close();
try {
preview.cleanup().toCompletableFuture().get(5, TimeUnit.SECONDS);
check(!mode.equals("cleanup-failure"), "Cleanup failure hidden");
} catch (ExecutionException error) {
check(
mode.equals("cleanup-failure")
&& failure(error).code() == PreviewFailure.Code.CLEANUP,
"Wrong cleanup diagnostic");
}
check(
Examples.closes == 1 && Examples.service.closes == 1,
"Owned resources not cleaned exactly once");
preview.close();
check(Examples.closes == 1, "Repeated close cleaned twice");
}
try {
TargetPreview.open(descriptor, loader, Duration.ofSeconds(1));
throw new AssertionError("Process session reused");
} catch (PreviewFailure e) {
check(e.code() == PreviewFailure.Code.SESSION_REUSE, "Wrong reuse diagnostic");
}
System.out.println("PASS " + mode + " " + checks);
} finally {
if (preview != null) preview.close();
Platform.exit();
}
}
}
@@ -0,0 +1,62 @@
package fixture.preview;
import de.neitzel.fxdesigner.preview.*;
import de.neitzel.injection.ApplicationContext;
import fixture.components.*;
import fixture.model.Person;
import java.util.concurrent.*;
public final class Examples implements PreviewProvider<Person, ApplicationContext> {
static {
System.setProperty("fixture.provider.initialized", "true");
}
public static Person person;
public static ApplicationContext context;
public static Greeting service;
public static volatile CompletableFuture<Void> pending;
public static int closes;
public Class<Person> modelType() {
return Person.class;
}
public PreviewSession<Person, ApplicationContext> open(String id) throws Exception {
if (!javafx.application.Platform.isFxApplicationThread())
throw new AssertionError("Provider not on FX");
person = new Person();
context = new ApplicationContext("fixture.components");
service = context.getComponent(Greeting.class);
service.prefix = "configured";
if (id.equals("factory-failure")) {
service.close();
throw new IllegalStateException("Factory failed after cleaning partial resources");
}
pending = new CompletableFuture<>();
if (!java.util.Set.of("async", "timeout", "cancel", "readiness-failure").contains(id))
pending.complete(null);
return new PreviewSession<>() {
public Person model() {
return id.equals("wrong-model") ? null : person;
}
public ApplicationContext context() {
return context;
}
public CompletionStage<Void> ready() {
if (id.equals("readiness-failure"))
return CompletableFuture.failedFuture(new IllegalStateException("Readiness failed"));
return pending;
}
public void close() {
if (!javafx.application.Platform.isFxApplicationThread())
throw new AssertionError("Cleanup not on FX");
closes++;
service.close();
if (id.equals("cleanup-failure")) throw new IllegalStateException("Cleanup failure");
}
};
}
}
@@ -0,0 +1,56 @@
format=1
count=9
example.0.id=standard
example.0.label=standard
example.0.modelType=fixture.model.Person
example.0.providerClass=fixture.preview.Examples
example.0.viewResource=fixture/components/Person.fxml
example.0.componentPackage=fixture.components
example.1.id=async
example.1.label=async
example.1.modelType=fixture.model.Person
example.1.providerClass=fixture.preview.Examples
example.1.viewResource=fixture/components/Person.fxml
example.1.componentPackage=fixture.components
example.2.id=timeout
example.2.label=timeout
example.2.modelType=fixture.model.Person
example.2.providerClass=fixture.preview.Examples
example.2.viewResource=fixture/components/Person.fxml
example.2.componentPackage=fixture.components
example.3.id=cancel
example.3.label=cancel
example.3.modelType=fixture.model.Person
example.3.providerClass=fixture.preview.Examples
example.3.viewResource=fixture/components/Person.fxml
example.3.componentPackage=fixture.components
example.4.id=readiness-failure
example.4.label=readiness-failure
example.4.modelType=fixture.model.Person
example.4.providerClass=fixture.preview.Examples
example.4.viewResource=fixture/components/Person.fxml
example.4.componentPackage=fixture.components
example.5.id=wrong-model
example.5.label=wrong-model
example.5.modelType=fixture.model.Person
example.5.providerClass=fixture.preview.Examples
example.5.viewResource=fixture/components/Person.fxml
example.5.componentPackage=fixture.components
example.6.id=invalid-attribute
example.6.label=invalid-attribute
example.6.modelType=fixture.model.Person
example.6.providerClass=fixture.preview.Examples
example.6.viewResource=fixture/components/Invalid.fxml
example.6.componentPackage=fixture.components
example.7.id=cleanup-failure
example.7.label=cleanup-failure
example.7.modelType=fixture.model.Person
example.7.providerClass=fixture.preview.Examples
example.7.viewResource=fixture/components/Person.fxml
example.7.componentPackage=fixture.components
example.8.id=factory-failure
example.8.label=factory-failure
example.8.modelType=fixture.model.Person
example.8.providerClass=fixture.preview.Examples
example.8.viewResource=fixture/components/Person.fxml
example.8.componentPackage=fixture.components
@@ -0,0 +1,15 @@
{
"schemaVersion": "1.0",
"contexts": {
"fixture": {
"applicationModule": "app",
"launchMode": "classpath",
"buildRuntime": "java21",
"targetRuntime": "java21",
"preview": {
"sourceSet": "test",
"provider": "fixture.preview.Examples"
}
}
}
}
@@ -0,0 +1,41 @@
<?xml version="1.0" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>de.neitzel.fxdesigner.fixtures</groupId>
<artifactId>preview-classpath</artifactId>
<version>1</version>
<packaging>pom</packaging>
<modules>
<module>app</module>
</modules>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
@@ -0,0 +1,18 @@
<?xml version="1.0" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.neitzel.fxdesigner.fixtures</groupId>
<artifactId>preview-fixture-modulepath</artifactId>
<version>1</version>
</parent>
<artifactId>app-modulepath</artifactId>
<dependencies>
<dependency>
<groupId>de.neitzel.lib</groupId>
<artifactId>fx</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,51 @@
package fixture.components;
@jakarta.inject.Singleton
public class Card extends javafx.scene.layout.VBox implements de.neitzel.fx.view.HasViewModel {
public final Greeting greeting;
private final javafx.beans.property.ObjectProperty<Object> model =
new javafx.beans.property.SimpleObjectProperty<>();
@javafx.fxml.FXML private javafx.scene.control.Label captionLabel;
@javafx.fxml.FXML private javafx.scene.control.TextField street;
public de.neitzel.fx.view.ViewModel received;
public boolean injected;
public Card(Greeting greeting) {
this.greeting = greeting;
}
@javafx.fxml.FXML
public void initialize() {
injected = true;
captionLabel.setText(greeting.prefix);
}
public String getCaption() {
return captionLabel.getText();
}
public void setCaption(String value) {
if (!injected) throw new IllegalStateException("Caption applied before injection");
captionLabel.setText(value);
}
public javafx.beans.property.ObjectProperty<Object> modelProperty() {
return model;
}
public Object getModel() {
return model.get();
}
public void setModel(Object value) {
model.set(value);
}
public void setViewModel(de.neitzel.fx.view.ViewModel vm) {
received = vm;
}
public javafx.scene.control.TextField street() {
return street;
}
}
@@ -0,0 +1,16 @@
package fixture.components;
@jakarta.inject.Singleton
public class Greeting implements AutoCloseable {
public final String identity = java.util.UUID.randomUUID().toString();
public String prefix = "sample";
public int closes;
public void close() {
closes++;
}
public String text() {
return prefix + "-" + identity;
}
}
@@ -0,0 +1,22 @@
package fixture.components;
@jakarta.inject.Singleton
@de.neitzel.fx.view.ViewModelClass(PersonViewModel.class)
public class PersonController implements de.neitzel.fx.view.HasViewModel {
public final Greeting greeting;
public de.neitzel.fx.view.ViewModel received;
public PersonController(Greeting greeting) {
this.greeting = greeting;
System.setProperty("fixture.controllers", "constructed");
}
public void setViewModel(de.neitzel.fx.view.ViewModel vm) {
received = vm;
}
public String greeting() {
if (received == null) throw new IllegalStateException("No ViewModel before bindings");
return greeting.text();
}
}
@@ -0,0 +1,7 @@
package fixture.components;
public class PersonViewModel extends de.neitzel.fx.view.ViewModel {
public PersonViewModel(Object model) {
super(model);
}
}
@@ -0,0 +1,13 @@
package fixture.model;
public class Address {
private String street = "First Street";
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
}
@@ -0,0 +1,28 @@
package fixture.model;
public class Person {
private String name = "Ada";
private Address home = new Address();
public Person() {
System.setProperty(
"fixture.models",
Integer.toString(Integer.parseInt(System.getProperty("fixture.models", "0")) + 1));
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Address getHome() {
return home;
}
public void setHome(Address home) {
this.home = home;
}
}
@@ -0,0 +1,18 @@
module fixture.app {
requires fx;
requires injection;
requires jakarta.inject;
requires javafx.controls;
requires javafx.fxml;
exports fixture.model to
fixture.preview,
fixture.checks;
exports fixture.components to
fixture.preview,
fixture.checks;
opens fixture.components;
opens fixture.model to
fx;
}
@@ -0,0 +1,2 @@
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import javafx.scene.control.TextField?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?>
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml/1"><fx:define><BindingList><DirectBinding control="street" property="text" path="street" direction="both"/></BindingList></fx:define><Label fx:id="captionLabel"/><TextField fx:id="street"/></fx:root>
@@ -0,0 +1,2 @@
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import javafx.scene.control.TextField?><?import fixture.components.Card?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?><?import de.neitzel.fx.view.ComputedBinding?>
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="fixture.components.PersonController" prefWidth="420" prefHeight="240"><fx:define><BindingList><DirectBinding control="name" property="text" path="name" direction="both"/><DirectBinding control="card" property="model" path="home" direction="both"/><DirectBinding control="second" property="model" path="home" direction="both"/><ComputedBinding control="greeting" property="text" method="#greeting"/></BindingList></fx:define><TextField fx:id="name"/><Label fx:id="greeting"/><Card fx:id="card" caption="FXML caption" prefWidth="not-a-number" alignment="CENTER_LEFT"/><Card fx:id="second" caption="Second"/></VBox>
@@ -0,0 +1,2 @@
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.Label?><?import javafx.scene.control.TextField?><?import fixture.components.Card?><?import de.neitzel.fx.view.BindingList?><?import de.neitzel.fx.view.DirectBinding?><?import de.neitzel.fx.view.ComputedBinding?>
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="fixture.components.PersonController" prefWidth="420" prefHeight="240"><fx:define><BindingList><DirectBinding control="name" property="text" path="name" direction="both"/><DirectBinding control="card" property="model" path="home" direction="both"/><DirectBinding control="second" property="model" path="home" direction="both"/><ComputedBinding control="greeting" property="text" method="#greeting"/></BindingList></fx:define><TextField fx:id="name"/><Label fx:id="greeting"/><Card fx:id="card" caption="FXML caption" prefWidth="310" alignment="CENTER_LEFT"/><Card fx:id="second" caption="Second"/></VBox>
@@ -0,0 +1,24 @@
<?xml version="1.0" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.neitzel.fxdesigner.fixtures</groupId>
<artifactId>preview-fixture-modulepath</artifactId>
<version>1</version>
</parent>
<artifactId>checks-modulepath</artifactId>
<dependencies>
<dependency>
<groupId>de.neitzel.fxdesigner.fixtures</groupId>
<artifactId>preview-modulepath</artifactId>
<version>1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>de.neitzel.fxdesigner</groupId>
<artifactId>designer-target-tools</artifactId>
<version>0.1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,197 @@
package fixture.checks;
import de.neitzel.fxdesigner.preview.*;
import de.neitzel.fxdesigner.target.*;
import fixture.components.*;
import fixture.model.*;
import fixture.preview.Examples;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.control.*;
public final class PreviewChecks {
static int checks;
static void check(boolean b, String message) {
if (!b) throw new AssertionError(message);
checks++;
}
static <T> T fx(Callable<T> action) throws Exception {
var result = new CompletableFuture<T>();
Platform.runLater(
() -> {
try {
result.complete(action.call());
} catch (Throwable e) {
result.completeExceptionally(e);
}
});
return result.get(10, TimeUnit.SECONDS);
}
static PreviewFailure failure(Throwable error) {
for (Throwable t = error; t != null; t = t.getCause())
if (t instanceof PreviewFailure f) return f;
throw new AssertionError("No structured preview diagnostic", error);
}
public static void main(String[] args) throws Exception {
String mode = args[0];
ClassLoader loader = PreviewChecks.class.getClassLoader();
var descriptors = ExampleCatalog.discover(loader);
check(descriptors.size() == 9, "Descriptor discovery");
check(
System.getProperty("fixture.provider.initialized") == null,
"Discovery initialized provider");
check(System.getProperty("fixture.models") == null, "Discovery constructed Model");
check(System.getProperty("fixture.controllers") == null, "Discovery constructed Controller");
if (mode.equals("discovery")) {
System.out.println("PASS discovery " + checks);
return;
}
var started = new CountDownLatch(1);
Platform.startup(
() -> {
Platform.setImplicitExit(false);
started.countDown();
});
check(started.await(10, TimeUnit.SECONDS), "FX startup");
TargetPreview preview = null;
try {
String id = mode.startsWith("missing-") ? "standard" : mode;
var descriptor =
descriptors.stream().filter(d -> d.id().equals(id)).findFirst().orElseThrow();
preview =
TargetPreview.open(
descriptor, loader, Duration.ofMillis(mode.equals("timeout") ? 2000 : 10000));
if (mode.equals("async") || mode.equals("cancel")) {
for (int i = 0; i < 100 && Examples.pending == null; i++) Thread.sleep(20);
check(Examples.pending != null, "Provider readiness created");
check(!preview.ready().toCompletableFuture().isDone(), "Premature readiness");
if (mode.equals("async")) Examples.pending.complete(null);
else preview.cancel();
}
if (java.util.Set.of(
"timeout",
"cancel",
"readiness-failure",
"wrong-model",
"invalid-attribute",
"factory-failure",
"missing-components",
"missing-model")
.contains(mode)) {
try {
preview.ready().toCompletableFuture().get(15, TimeUnit.SECONDS);
throw new AssertionError("Failure accepted: " + mode);
} catch (ExecutionException error) {
var f = failure(error);
var expected =
switch (mode) {
case "timeout" -> PreviewFailure.Code.TIMEOUT;
case "cancel" -> PreviewFailure.Code.CANCELLED;
case "readiness-failure" -> PreviewFailure.Code.READINESS;
case "wrong-model" -> PreviewFailure.Code.MODEL_TYPE;
case "missing-components", "missing-model" -> PreviewFailure.Code.MODULE_ACCESS;
default -> PreviewFailure.Code.LOAD;
};
check(f.code() == expected, "Wrong diagnostic: " + f.code() + " expected " + expected);
}
preview.cleanup().toCompletableFuture().get(5, TimeUnit.SECONDS);
if (!mode.equals("missing-components")) {
check(Examples.service.closes == 1, "Failure did not clean services once");
if (!mode.equals("factory-failure"))
check(Examples.closes == 1, "Session cleanup missing");
}
} else {
TargetPreview ready = preview.ready().toCompletableFuture().get(15, TimeUnit.SECONDS);
fx(
() -> {
var view = ready.loadedView();
check(view.getViewModel() instanceof PersonViewModel, "Named ViewModel not selected");
var controller = Examples.context.getComponent(PersonController.class);
check(controller.received == view.getViewModel(), "Controller ViewModel handoff");
check(controller.greeting == Examples.service, "Configured service not injected");
var card = (Card) view.getRoot().lookup("#card");
var second = (Card) view.getRoot().lookup("#second");
check(card != second, "Component instances reused");
check(
card.getModel() == Examples.person.getHome()
&& second.getModel() == card.getModel(),
"Nested Model identity not shared");
check(card.received == second.received, "Nested ViewModel identity changed");
check(
card.greeting == Examples.service && second.greeting == Examples.service,
"Component injection");
check(card.getCaption().equals("FXML caption"), "Custom caption lost");
check(
card.getPrefWidth() == 310 && card.getAlignment() == Pos.CENTER_LEFT,
"Inherited/coerced attributes lost");
check(card.injected, "Component initialization");
TextField name = (TextField) view.getRoot().lookup("#name");
check(name.getText().equals("Ada"), "Initial parent binding");
name.setText("Grace");
check(Examples.person.getName().equals("Grace"), "Parent write-back");
card.street().setText("Changed Street");
check(
Examples.person.getHome().getStreet().equals("Changed Street")
&& second.street().getText().equals("Changed Street"),
"Component write-back and shared Model");
check(
((Label) view.getRoot().lookup("#greeting")).getText().startsWith("configured-"),
"Computed controller binding");
byte[] rgba = ready.render(420, 240);
check(rgba.length == 420 * 240 * 4, "Render size");
boolean nonzero = false;
for (byte value : rgba)
if (value != 0) {
nonzero = true;
break;
}
check(nonzero, "Blank rendering");
try {
ready.render(8192, 8192);
throw new AssertionError("Oversized image allowed");
} catch (IllegalArgumentException expected) {
checks++;
}
return null;
});
System.out.println(
"SESSION "
+ Examples.service.identity
+ " models="
+ System.getProperty("fixture.models"));
preview.close();
try {
preview.cleanup().toCompletableFuture().get(5, TimeUnit.SECONDS);
check(!mode.equals("cleanup-failure"), "Cleanup failure hidden");
} catch (ExecutionException error) {
check(
mode.equals("cleanup-failure")
&& failure(error).code() == PreviewFailure.Code.CLEANUP,
"Wrong cleanup diagnostic");
}
check(
Examples.closes == 1 && Examples.service.closes == 1,
"Owned resources not cleaned exactly once");
preview.close();
check(Examples.closes == 1, "Repeated close cleaned twice");
}
try {
TargetPreview.open(descriptor, loader, Duration.ofSeconds(1));
throw new AssertionError("Process session reused");
} catch (PreviewFailure e) {
check(e.code() == PreviewFailure.Code.SESSION_REUSE, "Wrong reuse diagnostic");
}
System.out.println("PASS " + mode + " " + checks);
} finally {
if (preview != null) preview.close();
Platform.exit();
}
}
}
@@ -0,0 +1,10 @@
module fixture.checks {
requires fixture.app;
requires fixture.preview;
requires de.neitzel.fxdesigner.target;
requires de.neitzel.fxdesigner.preview;
requires fx;
requires injection;
requires javafx.controls;
requires javafx.fxml;
}
@@ -0,0 +1,19 @@
{
"schemaVersion": "1.0",
"contexts": {
"fixture": {
"applicationModule": "app",
"launchMode": "module-path",
"buildRuntime": "java21",
"targetRuntime": "java21",
"preview": {
"sourceSet": "module",
"module": "preview",
"provider": "fixture.preview.Examples"
},
"modulePath": {
"entryModule": "fixture.checks"
}
}
}
}
@@ -0,0 +1,43 @@
<?xml version="1.0" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>de.neitzel.fxdesigner.fixtures</groupId>
<artifactId>preview-fixture-modulepath</artifactId>
<version>1</version>
<packaging>pom</packaging>
<modules>
<module>app</module>
<module>preview</module>
<module>checks</module>
</modules>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
@@ -0,0 +1,24 @@
<?xml version="1.0" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.neitzel.fxdesigner.fixtures</groupId>
<artifactId>preview-fixture-modulepath</artifactId>
<version>1</version>
</parent>
<artifactId>preview-modulepath</artifactId>
<dependencies>
<dependency>
<groupId>de.neitzel.fxdesigner.fixtures</groupId>
<artifactId>app-modulepath</artifactId>
<version>1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>de.neitzel.fxdesigner</groupId>
<artifactId>designer-preview-api</artifactId>
<version>0.1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,62 @@
package fixture.preview;
import de.neitzel.fxdesigner.preview.*;
import de.neitzel.injection.ApplicationContext;
import fixture.components.*;
import fixture.model.Person;
import java.util.concurrent.*;
public final class Examples implements PreviewProvider<Person, ApplicationContext> {
static {
System.setProperty("fixture.provider.initialized", "true");
}
public static Person person;
public static ApplicationContext context;
public static Greeting service;
public static volatile CompletableFuture<Void> pending;
public static int closes;
public Class<Person> modelType() {
return Person.class;
}
public PreviewSession<Person, ApplicationContext> open(String id) throws Exception {
if (!javafx.application.Platform.isFxApplicationThread())
throw new AssertionError("Provider not on FX");
person = new Person();
context = new ApplicationContext("fixture.components");
service = context.getComponent(Greeting.class);
service.prefix = "configured";
if (id.equals("factory-failure")) {
service.close();
throw new IllegalStateException("Factory failed after cleaning partial resources");
}
pending = new CompletableFuture<>();
if (!java.util.Set.of("async", "timeout", "cancel", "readiness-failure").contains(id))
pending.complete(null);
return new PreviewSession<>() {
public Person model() {
return id.equals("wrong-model") ? null : person;
}
public ApplicationContext context() {
return context;
}
public CompletionStage<Void> ready() {
if (id.equals("readiness-failure"))
return CompletableFuture.failedFuture(new IllegalStateException("Readiness failed"));
return pending;
}
public void close() {
if (!javafx.application.Platform.isFxApplicationThread())
throw new AssertionError("Cleanup not on FX");
closes++;
service.close();
if (id.equals("cleanup-failure")) throw new IllegalStateException("Cleanup failure");
}
};
}
}
@@ -0,0 +1,10 @@
module fixture.preview {
requires fixture.app;
requires injection;
requires javafx.graphics;
requires de.neitzel.fxdesigner.preview;
exports fixture.preview to
fixture.checks,
de.neitzel.fxdesigner.target;
}
@@ -0,0 +1,56 @@
format=1
count=9
example.0.id=standard
example.0.label=standard
example.0.modelType=fixture.model.Person
example.0.providerClass=fixture.preview.Examples
example.0.viewResource=fixture/components/Person.fxml
example.0.componentPackage=fixture.components
example.1.id=async
example.1.label=async
example.1.modelType=fixture.model.Person
example.1.providerClass=fixture.preview.Examples
example.1.viewResource=fixture/components/Person.fxml
example.1.componentPackage=fixture.components
example.2.id=timeout
example.2.label=timeout
example.2.modelType=fixture.model.Person
example.2.providerClass=fixture.preview.Examples
example.2.viewResource=fixture/components/Person.fxml
example.2.componentPackage=fixture.components
example.3.id=cancel
example.3.label=cancel
example.3.modelType=fixture.model.Person
example.3.providerClass=fixture.preview.Examples
example.3.viewResource=fixture/components/Person.fxml
example.3.componentPackage=fixture.components
example.4.id=readiness-failure
example.4.label=readiness-failure
example.4.modelType=fixture.model.Person
example.4.providerClass=fixture.preview.Examples
example.4.viewResource=fixture/components/Person.fxml
example.4.componentPackage=fixture.components
example.5.id=wrong-model
example.5.label=wrong-model
example.5.modelType=fixture.model.Person
example.5.providerClass=fixture.preview.Examples
example.5.viewResource=fixture/components/Person.fxml
example.5.componentPackage=fixture.components
example.6.id=invalid-attribute
example.6.label=invalid-attribute
example.6.modelType=fixture.model.Person
example.6.providerClass=fixture.preview.Examples
example.6.viewResource=fixture/components/Invalid.fxml
example.6.componentPackage=fixture.components
example.7.id=cleanup-failure
example.7.label=cleanup-failure
example.7.modelType=fixture.model.Person
example.7.providerClass=fixture.preview.Examples
example.7.viewResource=fixture/components/Person.fxml
example.7.componentPackage=fixture.components
example.8.id=factory-failure
example.8.label=factory-failure
example.8.modelType=fixture.model.Person
example.8.providerClass=fixture.preview.Examples
example.8.viewResource=fixture/components/Person.fxml
example.8.componentPackage=fixture.components