Add shared source-preserving Computed binding inspector
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
package de.neitzel.fxdesigner.kit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import de.neitzel.fxdesigner.contracts.*;
|
||||
import java.net.URI;
|
||||
import java.util.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class BindingDocumentTest {
|
||||
private static final String P = "de.neitzel.fx.view.";
|
||||
private static final BindingDocument.Values VALUE =
|
||||
new BindingDocument.Values("label", "text", "name", "view", "");
|
||||
|
||||
private String apply(String text, List<EditBatch.Replacement> edits) throws Exception {
|
||||
String result =
|
||||
new EditBatch(7, "binding", edits)
|
||||
.applyTo(new DocumentSnapshot(URI.create("file:///test.fxml"), 7, text, true, false));
|
||||
new BindingDocument(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsRequiredContainersWithoutSerializingTheDocument() throws Exception {
|
||||
for (String root :
|
||||
List.of(
|
||||
"<VBox/>",
|
||||
"<VBox xmlns:fx='urn:other'/>",
|
||||
"<VBox><!-- untouched -->\r\n</VBox>",
|
||||
"<VBox xmlns:f='http://javafx.com/fxml/1'><f:define/></VBox>",
|
||||
"<VBox xmlns:f='http://javafx.com/fxml/1'><f:define><"
|
||||
+ P
|
||||
+ "BindingList/></f:define></VBox>")) {
|
||||
String text = "<?neitzel-model app.Person?>\n" + root;
|
||||
var doc = new BindingDocument(text);
|
||||
String result = apply(text, doc.write(null, VALUE, false));
|
||||
var created = new BindingDocument(result);
|
||||
assertEquals(1, created.entries.size());
|
||||
assertEquals(VALUE, created.entries.getFirst().values());
|
||||
assertTrue(result.startsWith("<?neitzel-model app.Person?>\n"));
|
||||
assertFalse(result.contains("<?import"));
|
||||
if (text.contains("untouched")) assertTrue(result.contains("<!-- untouched -->\r\n"));
|
||||
if (text.contains("urn:other"))
|
||||
assertTrue(result.contains("xmlns:fx1=\"http://javafx.com/fxml/1\""));
|
||||
}
|
||||
}
|
||||
|
||||
private String existing() {
|
||||
return "<?neitzel-model app.Person?>\n"
|
||||
+ "<?import de.neitzel.fx.view.*?>\n"
|
||||
+ "<VBox xmlns:fx='http://javafx.com/fxml/1'><!-- before"
|
||||
+ " --><fx:define><BindingList><!-- preserve --><DirectBinding control = 'label'"
|
||||
+ " property='text' path = 'name' direction='view' converter='app.Old'"
|
||||
+ " custom='untouched'/><!-- after --><app.Unknown"
|
||||
+ " value='keep'/></BindingList></fx:define><Label fx:id='label'/></VBox>";
|
||||
}
|
||||
|
||||
@Test
|
||||
void changesOnlyRequestedAttributeRangesAndRetainsUnknownContent() throws Exception {
|
||||
String text = existing();
|
||||
var doc = new BindingDocument(text);
|
||||
int index = doc.entries.getFirst().index();
|
||||
var value = new BindingDocument.Values("label", "text", "home.street", "both", "app.New");
|
||||
String updated = apply(text, doc.write(index, value, false));
|
||||
assertEquals(
|
||||
text.replace("path = 'name'", "path = 'home.street'")
|
||||
.replace("direction='view'", "direction='both'")
|
||||
.replace("converter='app.Old'", "converter='app.New'"),
|
||||
updated);
|
||||
assertTrue(new BindingDocument(updated).write(index, value, false).isEmpty());
|
||||
String escaped =
|
||||
apply(
|
||||
text,
|
||||
doc.write(
|
||||
index, new BindingDocument.Values("label", "text", "a'&<", "view", ""), false));
|
||||
assertEquals("a'&<", new BindingDocument(escaped).entries.getFirst().values().path());
|
||||
assertFalse(escaped.contains("converter='app.Old'"));
|
||||
assertFalse(new BindingDocument(escaped).errors().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void removesOnlyTheSelectedElementIncludingPairedTags() throws Exception {
|
||||
String text =
|
||||
existing()
|
||||
.replace("custom='untouched'/>", "custom='untouched'><!-- inner --></DirectBinding>");
|
||||
var doc = new BindingDocument(text);
|
||||
var tag = doc.source.tags.get(doc.entries.getFirst().index());
|
||||
String result = apply(text, doc.remove(doc.entries.getFirst().index()));
|
||||
assertEquals(text.substring(0, tag.start()) + text.substring(tag.end()), result);
|
||||
assertTrue(result.contains("<!-- preserve --><!-- after -->"));
|
||||
assertTrue(result.contains("app.Unknown"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void replacesAllConflictingDirectAndComputedBindingsAtomically() throws Exception {
|
||||
String text =
|
||||
existing()
|
||||
.replace(
|
||||
"<!-- after -->",
|
||||
"<ComputedBinding control='label' property='text' method='#caption'/><!-- after"
|
||||
+ " -->");
|
||||
var doc = new BindingDocument(text);
|
||||
assertTrue(doc.errors().stream().anyMatch(s -> s.contains("Duplicate")));
|
||||
assertThrows(BindingDocument.Conflict.class, () -> doc.write(null, VALUE, false));
|
||||
String replaced = apply(text, doc.write(null, VALUE, true));
|
||||
var parsed = new BindingDocument(replaced);
|
||||
assertEquals(1, parsed.entries.size());
|
||||
assertFalse(replaced.contains("<ComputedBinding"));
|
||||
assertTrue(parsed.errors().isEmpty());
|
||||
assertTrue(replaced.contains("app.Unknown"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void supportsExplicitElementsWrapperAndRefusesNestedPropertyEdits() throws Exception {
|
||||
String text =
|
||||
"<?import de.neitzel.fx.view.*?><VBox"
|
||||
+ " xmlns:fx='http://javafx.com/fxml/1'><fx:define><BindingList><elements><DirectBinding"
|
||||
+ " control='label'><property>text</property></DirectBinding></elements></BindingList></fx:define></VBox>";
|
||||
var doc = new BindingDocument(text);
|
||||
assertFalse(doc.entries.getFirst().editable());
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> doc.write(doc.entries.getFirst().index(), VALUE, false));
|
||||
String result =
|
||||
apply(
|
||||
text,
|
||||
doc.write(
|
||||
null, new BindingDocument.Values("other", "text", "name", "view", ""), false));
|
||||
assertEquals(2, new BindingDocument(result).entries.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void usesOnlyTheLibraryDeclarationsAndRejectsMalformedXml() throws Exception {
|
||||
String text =
|
||||
existing().replace("<?import de.neitzel.fx.view.*?>", "<?import app.DirectBinding?>");
|
||||
assertTrue(new BindingDocument(text).entries.isEmpty());
|
||||
assertThrows(Exception.class, () -> new BindingDocument("<VBox>"));
|
||||
assertThrows(
|
||||
Exception.class, () -> new BindingDocument("<!DOCTYPE x SYSTEM 'file:///etc/passwd'><x/>"));
|
||||
}
|
||||
|
||||
private BindingDocument computed(String text) throws Exception {
|
||||
return new BindingDocument(text, BindingDocument.Kind.COMPUTED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void computedCreatesContainersAndRoundTripsEscapedMethods() 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.computed("label", "text", "#a'&<");
|
||||
String result = apply(text, computed(text).write(null, value, false));
|
||||
assertEquals(value, computed(result).entries.getFirst().values());
|
||||
assertTrue(result.startsWith("<?neitzel-model app.Person?>\r\n"));
|
||||
assertFalse(result.contains(" direction="));
|
||||
assertFalse(result.contains(" path="));
|
||||
assertTrue(computed(result).errors().stream().anyMatch(e -> e.contains("Controller method")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void computedChangesOnlyItsFieldsAndRemovesPairedDeclaration() throws Exception {
|
||||
String text =
|
||||
existing()
|
||||
.replace(
|
||||
"<app.Unknown",
|
||||
"<ComputedBinding control = 'other' property='text' method = '#old'"
|
||||
+ " custom='keep'><!-- inner --></ComputedBinding><Label"
|
||||
+ " fx:id='other'/><app.Unknown");
|
||||
var doc = computed(text);
|
||||
int index = doc.entries.getFirst().index();
|
||||
var value = BindingDocument.Values.computed("label", "accessibleText", "greeting");
|
||||
String changed = apply(text, doc.write(index, value, false));
|
||||
assertEquals(
|
||||
text.replace("control = 'other'", "control = 'label'")
|
||||
.replace("property='text' method", "property='accessibleText' method")
|
||||
.replace("method = '#old'", "method = 'greeting'"),
|
||||
changed);
|
||||
assertTrue(computed(changed).write(index, value, false).isEmpty());
|
||||
String removed = apply(changed, computed(changed).remove(index));
|
||||
assertTrue(computed(removed).entries.isEmpty());
|
||||
assertEquals(1, new BindingDocument(removed).entries.size());
|
||||
assertTrue(removed.contains("<!-- preserve -->"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void computedReplacementRemovesDirectAndComputedConflictsTogether() throws Exception {
|
||||
String text =
|
||||
existing()
|
||||
.replace(
|
||||
"<!-- after -->",
|
||||
"<ComputedBinding control='label' property='text' method='#caption'/><!-- after"
|
||||
+ " -->");
|
||||
var doc = computed(text);
|
||||
var value = BindingDocument.Values.computed("label", "text", "#greeting");
|
||||
assertThrows(BindingDocument.Conflict.class, () -> doc.write(null, value, false));
|
||||
String result = apply(text, doc.write(null, value, true));
|
||||
assertTrue(new BindingDocument(result).entries.isEmpty());
|
||||
assertEquals(1, computed(result).entries.size());
|
||||
assertTrue(computed(result).errors().isEmpty());
|
||||
assertTrue(result.contains("<!-- preserve -->"));
|
||||
// Reverse replacement uses the same conflict policy.
|
||||
assertThrows(
|
||||
BindingDocument.Conflict.class,
|
||||
() -> new BindingDocument(result).write(null, VALUE, false));
|
||||
String direct = apply(result, new BindingDocument(result).write(null, VALUE, true));
|
||||
assertTrue(computed(direct).entries.isEmpty());
|
||||
assertEquals(1, new BindingDocument(direct).entries.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void computedNestedSyntaxIsVisibleRemovableButNotFlattened() throws Exception {
|
||||
String text =
|
||||
existing()
|
||||
.replace(
|
||||
"<app.Unknown",
|
||||
"<ComputedBinding control='other'"
|
||||
+ " property='text'><method>#caption</method></ComputedBinding><app.Unknown");
|
||||
var doc = computed(text);
|
||||
var entry = doc.entries.getFirst();
|
||||
assertFalse(entry.editable());
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
doc.write(
|
||||
entry.index(), BindingDocument.Values.computed("other", "text", "#new"), false));
|
||||
assertTrue(computed(apply(text, doc.remove(entry.index()))).entries.isEmpty());
|
||||
assertTrue(
|
||||
computed(text.replace("<?import de.neitzel.fx.view.*?>", "<?import app.ComputedBinding?>"))
|
||||
.entries
|
||||
.isEmpty());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user