Add shared source-preserving Repeat binding inspector

This commit is contained in:
2026-11-13 14:06:16 +01:00
parent 510ce35b60
commit 0300062fb6
10 changed files with 481 additions and 54 deletions
@@ -10,10 +10,15 @@ final class BindingDocument {
enum Kind {
DIRECT,
COMPUTED;
COMPUTED,
REPEAT;
String element() {
return this == DIRECT ? "DirectBinding" : "ComputedBinding";
return switch (this) {
case DIRECT -> "DirectBinding";
case COMPUTED -> "ComputedBinding";
case REPEAT -> "Repeat";
};
}
}
@@ -23,18 +28,31 @@ final class BindingDocument {
String path,
String direction,
String converter,
String method) {
String method,
String pane,
String component,
String collection) {
Values(String control, String property, String path, String direction, String converter) {
this(control, property, path, direction, converter, "");
this(control, property, path, direction, converter, "", "", "", "");
}
static Values repeat(String pane, String component, String collection) {
return new Values("", "", "", "view", "", "", pane, component, collection);
}
String target() {
return pane.isEmpty() ? control : pane;
}
static Values computed(String control, String property, String method) {
return new Values(control, property, "", "view", "", method);
return new Values(control, property, "", "view", "", method, "", "", "");
}
}
record Entry(int index, Values values, boolean editable) {
public String toString() {
if (!values.pane().isEmpty())
return values.pane() + " ← " + values.collection() + " (" + values.component() + ")";
return values.control()
+ "."
+ values.property()
@@ -115,6 +133,9 @@ final class BindingDocument {
}
private Values values(Element e) {
if (kind == Kind.REPEAT)
return Values.repeat(
e.getAttribute("pane"), e.getAttribute("component"), e.getAttribute("collection"));
if (kind == Kind.COMPUTED)
return Values.computed(
e.getAttribute("control"), e.getAttribute("property"), e.getAttribute("method"));
@@ -170,9 +191,34 @@ final class BindingDocument {
}
}
}
var panes = new HashSet<String>();
for (var e : elements)
if (binding(e, "Repeat")) {
if (hasElementChildren(e)) continue;
String pane = e.getAttribute("pane");
if (!panes.add(pane)) errors.add("Duplicate Repeat pane: " + pane);
if (ids.getOrDefault(pane, 0) != 1) errors.add("Missing or ambiguous Repeat pane: " + pane);
if (!qualifiedName(e.getAttribute("collection")))
errors.add("Invalid or missing Repeat collection: " + pane);
if (!qualifiedName(e.getAttribute("component")))
errors.add("Invalid or missing Repeat Component class: " + pane);
}
return List.copyOf(errors);
}
private static boolean qualifiedName(String value) {
return value.matches(
"[\\p{javaJavaIdentifierStart}][\\p{javaJavaIdentifierPart}]*(\\.[\\p{javaJavaIdentifierStart}][\\p{javaJavaIdentifierPart}]*)*");
}
private boolean conflicts(Element e, Values value) {
if (kind == Kind.REPEAT)
return binding(e, "Repeat") && e.getAttribute("pane").equals(value.pane());
return (binding(e, "DirectBinding") || binding(e, "ComputedBinding"))
&& e.getAttribute("control").equals(value.control())
&& e.getAttribute("property").equals(value.property());
}
List<Replacement> remove(int index) {
Element e = entry(index);
var t = source.tags.get(indexes.get(e));
@@ -180,23 +226,20 @@ final class BindingDocument {
}
List<Replacement> write(Integer index, Values value, boolean replaceConflicts) {
if (value.control().isBlank() || value.property().isBlank())
if (kind == Kind.REPEAT && value.pane().isBlank())
throw new IllegalArgumentException("Choose a pane");
if (kind != Kind.REPEAT && (value.control().isBlank() || value.property().isBlank()))
throw new IllegalArgumentException("Choose a control and property");
Element original = index == null ? null : entry(index);
if (original != null && hasElementChildren(original))
throw new IllegalArgumentException(
"This declaration uses nested property elements; edit it in Source.");
var conflicts =
elements.stream()
.filter(
e ->
e != original
&& (binding(e, "DirectBinding") || binding(e, "ComputedBinding"))
&& e.getAttribute("control").equals(value.control())
&& e.getAttribute("property").equals(value.property()))
.toList();
var conflicts = elements.stream().filter(e -> e != original && conflicts(e, value)).toList();
if (!conflicts.isEmpty() && !replaceConflicts)
throw new Conflict(value.control() + "." + value.property());
throw new Conflict(
kind == Kind.REPEAT
? "Repeat pane " + value.pane()
: value.control() + "." + value.property());
var edits = new ArrayList<Replacement>();
for (var e : conflicts) {
var t = source.tags.get(indexes.get(e));
@@ -205,25 +248,33 @@ final class BindingDocument {
if (original != null) {
var changed = new LinkedHashMap<String, String>();
Map<String, String> fields =
kind == Kind.COMPUTED
kind == Kind.REPEAT
? Map.of(
"control",
value.control(),
"property",
value.property(),
"method",
value.method())
: Map.of(
"control",
value.control(),
"property",
value.property(),
"path",
value.path(),
"direction",
value.direction(),
"converter",
value.converter());
"pane",
value.pane(),
"component",
value.component(),
"collection",
value.collection())
: kind == Kind.COMPUTED
? Map.of(
"control",
value.control(),
"property",
value.property(),
"method",
value.method())
: Map.of(
"control",
value.control(),
"property",
value.property(),
"path",
value.path(),
"direction",
value.direction(),
"converter",
value.converter());
for (var f : fields.entrySet())
if (!original.getAttribute(f.getKey()).equals(f.getValue()))
changed.put(
@@ -240,7 +291,19 @@ final class BindingDocument {
+ "\" property=\""
+ XmlSource.escape(value.property(), '"')
+ "\"";
if (kind == Kind.COMPUTED)
if (kind == Kind.REPEAT)
node =
"<"
+ PACKAGE
+ kind.element()
+ " pane=\""
+ XmlSource.escape(value.pane(), '"')
+ "\" component=\""
+ XmlSource.escape(value.component(), '"')
+ "\" collection=\""
+ XmlSource.escape(value.collection(), '"')
+ "\"";
else if (kind == Kind.COMPUTED)
node += " method=\"" + XmlSource.escape(value.method(), '"') + "\"";
else
node +=
@@ -20,7 +20,10 @@ final class BindingEditor extends VBox {
property = field("property"),
path = field("path"),
converter = field("converter"),
method = field("method");
method = field("method"),
pane = field("pane"),
component = field("component"),
collection = field("collection");
private final ComboBox<BindingMetadata.Computed> methods = new ComboBox<>();
private final BindingDocument.Kind kind;
private final ComboBox<BindingMetadata.Converter> converters = new ComboBox<>();
@@ -43,9 +46,11 @@ final class BindingEditor extends VBox {
Consumer<BindingDocument.Values> discover) {
this.kind = kind;
boolean computed = kind == BindingDocument.Kind.COMPUTED;
String prefix = computed ? "computed-" : "direct-";
boolean repeat = kind == BindingDocument.Kind.REPEAT;
String prefix = repeat ? "repeat-" : computed ? "computed-" : "direct-";
this.navigate = navigate;
for (var field : java.util.List.of(control, property, path, converter, method))
for (var field :
java.util.List.of(control, property, path, converter, method, pane, component, collection))
field.setId(prefix + field.getId());
methods.setId("computed-sources");
methods.setPrefWidth(680);
@@ -60,6 +65,12 @@ final class BindingEditor extends VBox {
apply.setText("Apply Computed binding");
remove.setText("Remove Computed binding");
}
if (repeat) {
inspect.setText("Validate Repeat bindings");
fresh.setText("New Repeat binding");
apply.setText("Apply Repeat binding");
remove.setText("Remove Repeat binding");
}
setSpacing(5);
converters.setId("direct-converters");
converters.setPrefWidth(680);
@@ -83,9 +94,11 @@ final class BindingEditor extends VBox {
fresh.setOnAction(
e ->
newBinding(
computed
? BindingDocument.Values.computed("", "", "")
: new BindingDocument.Values("", "", "", "both", "")));
repeat
? BindingDocument.Values.repeat("", "", "")
: computed
? BindingDocument.Values.computed("", "", "")
: new BindingDocument.Values("", "", "", "both", "")));
apply.setOnAction(
e ->
edit.accept(new Intent(kind, source, selected, values(), false, replace.isSelected())));
@@ -101,7 +114,7 @@ final class BindingEditor extends VBox {
fill(b.values());
replace.setSelected(false);
buttons();
if (!followingCanvas) navigate.accept(b.values().control());
if (!followingCanvas) navigate.accept(b.values().target());
if (!b.editable())
status.setText(
"Nested property elements are preserved. Edit this declaration in Source, or"
@@ -110,7 +123,7 @@ final class BindingEditor extends VBox {
var grid = new GridPane();
grid.setHgap(8);
grid.setVgap(4);
if (!computed)
if (!computed && !repeat)
grid.addRow(
0,
new Label("Control (fx:id)"),
@@ -119,19 +132,23 @@ final class BindingEditor extends VBox {
property,
new Label("Direction"),
direction);
if (!computed)
if (!computed && !repeat)
grid.addRow(1, new Label("Model path"), path, new Label("Converter (optional)"), converter);
else {
else if (computed) {
grid.addRow(0, new Label("Control (fx:id)"), control, new Label("Property"), property);
grid.addRow(1, new Label("Controller method"), method);
for (var field : java.util.List.of(control, property))
field.textProperty().addListener((o, a, b) -> methods.getItems().clear());
}
if (repeat) {
grid.addRow(0, new Label("Pane (fx:id)"), pane, new Label("Collection path"), collection);
grid.addRow(1, new Label("Component class"), component);
}
getChildren()
.addAll(
bindings,
grid,
new HBox(8, inspect, computed ? methods : converters),
repeat ? new HBox(8, inspect) : new HBox(8, inspect, computed ? methods : converters),
new HBox(8, fresh, apply, remove, replace),
status);
buttons();
@@ -145,6 +162,9 @@ final class BindingEditor extends VBox {
}
private BindingDocument.Values values() {
if (kind == BindingDocument.Kind.REPEAT)
return BindingDocument.Values.repeat(
pane.getText().trim(), component.getText().trim(), collection.getText().trim());
if (kind == BindingDocument.Kind.COMPUTED)
return BindingDocument.Values.computed(
control.getText().trim(), property.getText().trim(), method.getText().trim());
@@ -163,6 +183,9 @@ final class BindingEditor extends VBox {
direction.setValue(v.direction());
converter.setText(v.converter());
method.setText(v.method());
pane.setText(v.pane());
component.setText(v.component());
collection.setText(v.collection());
}
void newBinding(BindingDocument.Values v) {
@@ -214,9 +237,9 @@ final class BindingEditor extends VBox {
void selectControl(String id) {
if (loading || id == null) return;
var selected = bindings.getSelectionModel().getSelectedItem();
if (selected != null && selected.values().control().equals(id)) return;
if (selected != null && selected.values().target().equals(id)) return;
bindings.getItems().stream()
.filter(e -> e.values().control().equals(id))
.filter(e -> e.values().target().equals(id))
.findFirst()
.ifPresent(
e -> {
@@ -250,6 +273,21 @@ final class BindingEditor extends VBox {
+ ": "
+ p.message())
.collect(java.util.stream.Collectors.joining("\n"))));
if (kind == BindingDocument.Kind.REPEAT) {
try {
var errors = new BindingDocument(source, kind).errors();
status.setText(
provenance
+ "\n"
+ (errors.isEmpty() ? "No structural binding errors." : String.join("\n", errors))
+ "\n"
+ "Collection and Component compatibility have not been checked. Render drafts to"
+ " test them.");
} catch (Exception error) {
status.setText("Validation unavailable: " + error.getMessage());
}
return;
}
if (requested == null || !values().equals(requested)) return;
if (kind == BindingDocument.Kind.COMPUTED) {
methods.getItems().setAll(metadata.computed());
@@ -44,6 +44,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
String id = hit.getFxId();
if (bindingEditor != null) bindingEditor.selectControl(id);
if (computedEditor != null) computedEditor.selectControl(id);
if (repeatEditor != null) repeatEditor.selectControl(id);
if (id == null || !projection.components().containsKey(id)) return;
componentInspector.getChildren().add(new Label(projection.components().get(id) + " · " + id));
componentInspector
@@ -115,7 +116,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
private DesignerWorkflow interactiveWorkflow;
private DesignerWorkflow bindingWorkflow;
private BindingBrowser bindingBrowser;
private BindingEditor bindingEditor, computedEditor;
private BindingEditor bindingEditor, computedEditor, repeatEditor;
private String validatedSource, validatingSource;
private boolean validationPreview;
private java.util.List<de.neitzel.fxdesigner.protocol.BindingMetadata.Problem> bindingProblems =
@@ -200,6 +201,16 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
if (target != null) kit.editor().getSelection().select(target);
},
values -> validateBindings(values, false));
repeatEditor =
new BindingEditor(
BindingDocument.Kind.REPEAT,
this::editBinding,
id -> {
if (kit.editor().getFxomDocument() == null) return;
var target = new FXOMFxIdIndex(kit.editor().getFxomDocument()).lookup(id);
if (target != null) kit.editor().getSelection().select(target);
},
values -> validateBindings(values, false));
bindingBrowser =
new BindingBrowser(
this::discoverBindings,
@@ -348,8 +359,11 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
directBindingTab.setClosable(false);
var computedTab = new Tab("Computed bindings", computedEditor);
computedTab.setClosable(false);
var repeatTab = new Tab("Repeat bindings", repeatEditor);
repeatTab.setClosable(false);
var bindings =
new TitledPane("Bindings", new TabPane(directBindingTab, computedTab, discoveryTab));
new TitledPane(
"Bindings", new TabPane(directBindingTab, computedTab, repeatTab, discoveryTab));
bindings.setExpanded(false);
center.setBottom(new VBox(properties, bindings));
output.setEditable(false);
@@ -491,6 +505,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
draft = value;
bindingEditor.load(value.text(), value.writable());
computedEditor.load(value.text(), value.writable());
repeatEditor.load(value.text(), value.writable());
submittedSource = null;
commitSource();
return;
@@ -514,6 +529,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
draft = value;
bindingEditor.load(value.text(), value.writable());
computedEditor.load(value.text(), value.writable());
repeatEditor.load(value.text(), value.writable());
if (!source.getText().equals(value.text())) {
int anchor = source.getAnchor(), caret = source.getCaretPosition();
var edits =
@@ -686,7 +702,11 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
? "Remove " + intent.kind().element()
: (intent.index() == null ? "Create " : "Edit ") + intent.kind().element());
} catch (Exception error) {
(intent.kind() == BindingDocument.Kind.COMPUTED ? computedEditor : bindingEditor)
(intent.kind() == BindingDocument.Kind.REPEAT
? repeatEditor
: intent.kind() == BindingDocument.Kind.COMPUTED
? computedEditor
: bindingEditor)
.error(error.getMessage());
}
});
@@ -761,6 +781,14 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
: "Current successful build")
+ " · draft revision "
+ captured.revision());
repeatEditor.metadata(
metadata,
values,
(last
? "Explicit retained build (outdated)"
: "Current successful build")
+ " · draft revision "
+ captured.revision());
status.setText(
"Binding validation complete: "
+ metadata.problems().size()
@@ -237,4 +237,108 @@ class BindingDocumentTest {
.entries
.isEmpty());
}
private BindingDocument repeats(String text) throws Exception {
return new BindingDocument(text, BindingDocument.Kind.REPEAT);
}
@Test
void repeatCreatesContainersAndEscapesFields() throws Exception {
for (String root :
List.of(
"<VBox/>",
"<VBox xmlns:f='http://javafx.com/fxml/1'><f:define/></VBox>",
"<VBox xmlns:f='http://javafx.com/fxml/1'><f:define><"
+ P
+ "BindingList><elements/></"
+ P
+ "BindingList></f:define></VBox>")) {
String text = "<?neitzel-model app.Person?>\r\n" + root;
var value = BindingDocument.Values.repeat("pane", "app.Child", "a'&<");
String result = apply(text, repeats(text).write(null, value, false));
assertEquals(value, repeats(result).entries.getFirst().values());
assertTrue(result.startsWith("<?neitzel-model app.Person?>\r\n"));
assertFalse(result.contains(" control="));
assertTrue(repeats(result).errors().stream().anyMatch(e -> e.contains("Repeat collection")));
}
}
private String repeatSource() {
return existing()
.replace(
"<app.Unknown",
"<Repeat pane = 'rows' component='app.Child' collection = 'visits' custom='keep'><!--"
+ " inner --></Repeat><app.Unknown")
.replace("</VBox>", "<VBox fx:id='rows'/><VBox fx:id='other'/></VBox>");
}
@Test
void repeatChangesAllFieldsWithoutRewritingOtherBindings() throws Exception {
String text = repeatSource();
var doc = repeats(text);
int index = doc.entries.getFirst().index();
var value = BindingDocument.Values.repeat("other", "app.OtherChild", "home.visits");
String changed = apply(text, doc.write(index, value, false));
assertEquals(
text.replace("pane = 'rows'", "pane = 'other'")
.replace("component='app.Child'", "component='app.OtherChild'")
.replace("collection = 'visits'", "collection = 'home.visits'"),
changed);
assertTrue(repeats(changed).write(index, value, false).isEmpty());
String removed = apply(changed, repeats(changed).remove(index));
assertTrue(repeats(removed).entries.isEmpty());
assertEquals(1, new BindingDocument(removed).entries.size());
assertTrue(removed.contains("<!-- preserve -->"));
}
@Test
void repeatConflictsAreScopedToThePaneAndReplacedAtomically() throws Exception {
String text =
repeatSource()
.replace(
"<app.Unknown",
"<Repeat pane='rows' component='app.Child' collection='visits'/><app.Unknown");
var doc = repeats(text);
var value = BindingDocument.Values.repeat("rows", "app.NewChild", "visits");
assertTrue(doc.errors().stream().anyMatch(e -> e.contains("Duplicate Repeat pane")));
assertThrows(BindingDocument.Conflict.class, () -> doc.write(null, value, false));
String result = apply(text, doc.write(null, value, true));
assertEquals(1, repeats(result).entries.size());
assertEquals(1, new BindingDocument(result).entries.size());
assertTrue(repeats(result).errors().isEmpty());
// A DirectBinding to a property of the same pane remains independent.
var direct = new BindingDocument.Values("rows", "visible", "name", "view", "");
String withDirect = apply(result, new BindingDocument(result).write(null, direct, false));
assertEquals(1, repeats(withDirect).entries.size());
}
@Test
void repeatNestedSyntaxAndImportIdentityArePreserved() throws Exception {
String text =
repeatSource()
.replace(
"collection = 'visits' custom='keep'><!-- inner -->",
"custom='keep'><collection>visits</collection>");
var doc = repeats(text);
var entry = doc.entries.getFirst();
assertFalse(entry.editable());
assertThrows(
IllegalArgumentException.class,
() ->
doc.write(
entry.index(),
BindingDocument.Values.repeat("rows", "app.Child", "visits"),
false));
assertTrue(repeats(apply(text, doc.remove(entry.index()))).entries.isEmpty());
assertTrue(
repeats(text.replace("<?import de.neitzel.fx.view.*?>", "<?import app.Repeat?>"))
.entries
.isEmpty());
assertTrue(
repeats(repeatSource().replace("pane = 'rows'", "pane = 'missing'")).errors().stream()
.anyMatch(e -> e.contains("Missing or ambiguous Repeat pane")));
assertTrue(
repeats(repeatSource().replace("component='app.Child'", "component='' ")).errors().stream()
.anyMatch(e -> e.contains("Repeat Component")));
}
}
@@ -644,6 +644,164 @@ public final class ProjectionHostChecks {
await(() -> fx(() -> source(pane).getText().equals(full)));
}
private static void repeatField(DesignerPane pane, String name, String value) throws Exception {
fx(
() -> {
((TextField) pane.lookup("#repeat-" + name)).setText(value);
return null;
});
}
private static void selectRepeat(DesignerPane pane, String name) throws Exception {
fx(
() -> {
var list = (ListView<Object>) pane.lookup("#repeat-bindings");
list.getSelectionModel()
.select(
list.getItems().stream()
.filter(e -> e.toString().startsWith(name + " ←"))
.findFirst()
.orElseThrow());
return null;
});
}
private static void repeatHistory(
DesignerPane pane, DocumentSession document, String before, String after) throws Exception {
click(pane, "Undo");
await(() -> documentText(document).equals(before));
click(pane, "Redo");
await(() -> documentText(document).equals(after));
await(() -> fx(() -> source(pane).getText().equals(after)));
}
private static void repeatBindingEdits(DesignerPane pane, DocumentSession document, String full)
throws Exception {
String base =
full.replace(
"<VBox fx:id=\"visits\"/>",
"<VBox fx:id=\"visits\"/><VBox fx:id=\"otherVisits\"/><!-- Repeat edit sentinel -->");
check(!base.equals(full), "Repeat fixture pane missing");
set(document, base);
await(() -> fx(() -> source(pane).getText().equals(base)));
bindingTab(pane, "Repeat bindings");
selectRepeat(pane, "visits");
click(pane, "Remove Repeat binding");
String removed =
base.replace(
"<Repeat pane=\"visits\" component=\"fixture.components.AddressForm\""
+ " collection=\"visits\"/>",
"");
await(() -> documentText(document).equals(removed));
repeatHistory(pane, document, base, removed);
click(pane, "New Repeat binding");
repeatField(pane, "pane", "visits");
repeatField(pane, "component", "fixture.components.AddressForm");
repeatField(pane, "collection", "visits");
check(documentText(document).equals(removed), "Repeat form changed source before Apply");
click(pane, "Apply Repeat binding");
await(() -> !documentText(document).equals(removed));
String created = documentText(document);
check(
created.contains(
"de.neitzel.fx.view.Repeat pane=\"visits\" component=\"fixture.components.AddressForm\""
+ " collection=\"visits\""),
"Repeat creation failed");
repeatHistory(pane, document, removed, created);
selectRepeat(pane, "visits");
repeatField(pane, "pane", "otherVisits");
repeatField(pane, "component", "fixture.components.VisitRow");
click(pane, "Apply Repeat binding");
String changed =
created.replace(
"Repeat pane=\"visits\" component=\"fixture.components.AddressForm\"",
"Repeat pane=\"otherVisits\" component=\"fixture.components.VisitRow\"");
await(() -> documentText(document).equals(changed));
repeatHistory(pane, document, created, 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),
"Repeat save changed source");
click(pane, "Validate Repeat bindings");
await(
() ->
fx(
() ->
((Label) pane.lookup("#repeat-status"))
.getText()
.contains("Collection and Component compatibility")));
click(pane, "Render drafts");
await(
() ->
fx(
() ->
object(pane, "home").getSceneGraphObject() instanceof SnapshotRegion r
&& r.image() != null
&& r.statusText().equals("Target runtime")));
click(pane, "New Repeat binding");
repeatField(pane, "pane", "otherVisits");
repeatField(pane, "component", "fixture.components.AddressForm");
repeatField(pane, "collection", "visits");
click(pane, "Apply Repeat binding");
await(
() ->
fx(
() ->
((Label) pane.lookup("#repeat-status"))
.getText()
.contains("Replace conflicting bindings")));
check(documentText(document).equals(changed), "Repeat conflict silently replaced");
fx(
() -> {
((CheckBox) pane.lookup("#repeat-replace")).setSelected(true);
return null;
});
click(pane, "Apply Repeat binding");
await(() -> !documentText(document).equals(changed));
String replaced = documentText(document);
check(
!replaced.contains("component=\"fixture.components.VisitRow\""),
"Repeat replacement retained old declaration");
repeatHistory(pane, document, changed, replaced);
selectRepeat(pane, "otherVisits");
repeatField(pane, "collection", "invalid path");
click(pane, "Apply Repeat binding");
String invalid = replaced.replace("collection=\"visits\"/>", "collection=\"invalid path\"/>");
await(() -> documentText(document).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 Repeat was not 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:")
&& l.getText().contains("Repeat collection"))));
repeatHistory(pane, document, replaced, invalid);
set(document, full);
await(() -> fx(() -> source(pane).getText().equals(full)));
System.out.println(
"PASS Repeat inspector: create, change, remove, conflict, exact undo/redo, save, target"
+ " load and invalid draft");
}
private static void viewModelBindings(
DesignerPane pane, DocumentSession document, String original) throws Exception {
bindingTab(pane, "Discover targets");
@@ -1022,6 +1180,7 @@ public final class ProjectionHostChecks {
converterEdits(pane, document, full);
directBindingEdits(pane, document, full);
computedBindingEdits(pane, document, full);
repeatBindingEdits(pane, document, full);
document
.save(document.snapshot().toCompletableFuture().get().revision())
.toCompletableFuture()