241 lines
10 KiB
Java
241 lines
10 KiB
Java
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());
|
|
}
|
|
}
|