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 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( "", "", "\r\n", "", "<" + P + "BindingList/>")) { String text = "\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("\n")); assertFalse(result.contains("\r\n")); if (text.contains("urn:other")) assertTrue(result.contains("xmlns:fx1=\"http://javafx.com/fxml/1\"")); } } private String existing() { return "\n" + "\n" + ""; } @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'>"); 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("")); assertTrue(result.contains("app.Unknown")); } @Test void replacesAllConflictingDirectAndComputedBindingsAtomically() throws Exception { String text = existing() .replace( "", ""); 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("text"; 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("", ""); assertTrue(new BindingDocument(text).entries.isEmpty()); assertThrows(Exception.class, () -> new BindingDocument("")); assertThrows( Exception.class, () -> new BindingDocument("")); } private BindingDocument computed(String text) throws Exception { return new BindingDocument(text, BindingDocument.Kind.COMPUTED); } @Test void computedCreatesContainersAndRoundTripsEscapedMethods() throws Exception { for (String root : List.of( "", "", "<" + P + "BindingList>")) { String text = "\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("\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( "")); } @Test void computedReplacementRemovesDirectAndComputedConflictsTogether() throws Exception { String text = existing() .replace( "", ""); 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("")); // 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( "#caption 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("", "")) .entries .isEmpty()); } }