/** * Copyright 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.waveprotocol.wave.model.document; import junit.framework.TestCase; import org.waveprotocol.wave.model.document.MutableAnnotationSet.CompareRangedValueByStartThenEnd; import org.waveprotocol.wave.model.document.MutableAnnotationSet.RangedValue; import org.waveprotocol.wave.model.document.indexed.IndexedDocument; import org.waveprotocol.wave.model.document.operation.DocOp; import org.waveprotocol.wave.model.document.operation.Nindo; import org.waveprotocol.wave.model.document.operation.automaton.DocumentSchema; import org.waveprotocol.wave.model.document.operation.impl.AttributesImpl; import org.waveprotocol.wave.model.document.operation.impl.DocOpUtil; import org.waveprotocol.wave.model.document.raw.impl.Element; import org.waveprotocol.wave.model.document.raw.impl.Node; import org.waveprotocol.wave.model.document.raw.impl.Text; import org.waveprotocol.wave.model.document.util.DocProviders; import org.waveprotocol.wave.model.document.util.Point; import org.waveprotocol.wave.model.document.util.PointRange; import org.waveprotocol.wave.model.document.util.Range; import org.waveprotocol.wave.model.operation.OperationException; import org.waveprotocol.wave.model.operation.OperationRuntimeException; import org.waveprotocol.wave.model.operation.OperationSequencer; import java.util.Collections; import java.util.LinkedList; /** * Test cases for MutableDocument implementations * * @author danilatos@google.com (Daniel Danilatos) */ public class MutableDocumentImplTest extends TestCase { /** * A parser for documents. */ public static final DocumentTestCases.DocumentParser< IndexedDocument<Node, Element, Text>> documentParser = new DocumentTestCases.DocumentParser<IndexedDocument< Node, Element, Text>>() { public IndexedDocument<Node, Element, Text> parseDocument(String innerXml) { return DocProviders.POJO.parse(innerXml); } @Override public IndexedDocument<Node, Element, Text> copyDocument( IndexedDocument<Node, Element, Text> other) { return DocProviders.POJO.build(other.asOperation(), DocumentSchema.NO_SCHEMA_CONSTRAINTS); } public String asString(IndexedDocument<Node, Element, Text> document) { return document.toXmlString(); } }; /** Indexed document to feed into a MutableDocument and apply ops to */ IndexedDocument<Node, Element, Text> indexed; /** MutableDocument that gets tested */ MutableDocumentImpl<Node, Element, Text> doc; /** Latest document mutation to have been applied */ DocOp latestOp; /** * Creates and returns a sequencer which applies incoming ops to the given document */ OperationSequencer<Nindo> createSequencer( final IndexedDocument<Node, Element, Text> document) { return new OperationSequencer<Nindo>() { @Override public void begin() { } @Override public void end() { } @Override public void consume(Nindo op) { try { latestOp = document.consumeAndReturnInvertible(op); } catch (OperationException oe) { throw new OperationRuntimeException("sequencer consume failed.", oe); } } }; } /** * Tests for the delete range method. * Tests correct deletion behaviour, and returned point range value */ public void testDeleteRange() { // deletes nothing String str = "123<b>asdf</b>34<x/>5"; init(str); for (int i = 0; i <= 14; i++) { assertCollapsedAt(i, doc.deleteRange(l(i), l(i))); assertResult(str); } // delete start or end tag does nothing assertRangeAt(3, 4, doc.deleteRange(l(3), l(4))); assertResult(str); assertRangeAt(8, 9, doc.deleteRange(l(8), l(9))); assertResult(str); // text only init("12345678"); assertCollapsedAt(1, doc.deleteRange(l(1), l(3))); // middle assertResult("145678"); assertCollapsedAt(0, doc.deleteRange(l(0), l(1))); // start assertResult("45678"); assertCollapsedAt(3, doc.deleteRange(l(3), l(5))); // end assertResult("456"); assertCollapsedAt(0, doc.deleteRange(l(0), l(3))); // all assertResult(""); // within single element, but multiple nodes being deleted init("123<b>5</b><i>6</i>78"); assertCollapsedAt(3, doc.deleteRange(l(3), l(9))); // middle assertResult("12378"); // into element init("123<b>456</b>"); assertRangeAt(2, 3, doc.deleteRange(l(2), l(5))); assertResult("12<b>56</b>"); // into elements, depth 2 init("123<b>4<i>56</i></b>"); assertRangeAt(2, 4, doc.deleteRange(l(2), l(7))); assertResult("12<b><i>6</i></b>"); // out of element init("<b>123</b>456"); assertRangeAt(3, 4, doc.deleteRange(l(3), l(6))); assertResult("<b>12</b>56"); // out of element, depth 2 init("<b><i>12</i>3</b>456"); assertRangeAt(3, 5, doc.deleteRange(l(3), l(8))); assertResult("<b><i>1</i></b>56"); // across elements init("<b>123</b>456<i>789</i>"); assertRangeAt(3, 5, doc.deleteRange(l(3), l(10))); assertResult("<b>12</b><i>89</i>"); // across elements with extra element in the middle init("<b>123</b>4<x></x>6<i>789</i>"); assertRangeAt(3, 5, doc.deleteRange(l(3), l(11))); assertResult("<b>12</b><i>89</i>"); // across elements with elements as the bounding content init("<b>123<x></x></b>456<i><y></y>789</i>"); assertRangeAt(4, 6, doc.deleteRange(l(4), l(13))); assertResult("<b>123</b><i>789</i>"); } public void testDeleteRangeIndices() { String str = "123<b>asdf</b>34<x/>5"; init(str); for (int i = 0; i <= 14; i++) { assertCollapsedAt(i, doc.deleteRange(i, i)); assertResult(str); } // delete start or end tag does nothing assertRangeAt(3, 4, doc.deleteRange(3, 4)); assertResult(str); assertRangeAt(8, 9, doc.deleteRange(8, 9)); assertResult(str); // text only init("12345678"); assertCollapsedAt(1, doc.deleteRange(1, 3)); // middle assertResult("145678"); assertCollapsedAt(0, doc.deleteRange(0, 1)); // start assertResult("45678"); assertCollapsedAt(3, doc.deleteRange(3, 5)); // end assertResult("456"); assertCollapsedAt(0, doc.deleteRange(0, 3)); // all assertResult(""); } /** * Test basic get attribute. */ public void testGetAttributes() { init("<p t=\"0\" s=\"hi\">hello</p>"); Element e = (Element) doc.getFirstChild(doc.getDocumentElement()); assertEquals("0", doc.getAttribute(e, "t")); assertEquals("hi", doc.getAttribute(e, "s")); } /** * Test set attribute overrides and removes old attributes, as opposed to * update. */ public void testSetAttributes() { init("<p t=\"0\" s=\"hi\">hello</p>"); Element e = (Element) doc.getFirstChild(doc.getDocumentElement()); doc.setElementAttributes(e, new AttributesImpl("just", "this")); assertEquals(null, doc.getAttribute(e, "t")); assertEquals(null, doc.getAttribute(e, "s")); assertEquals("this", doc.getAttribute(e, "just")); } protected Point<Node> l(int location) { return doc.locate(location); } /** Init document state */ protected void init(String initialContent) { indexed = DocProviders.POJO.parse(initialContent); // Get a mutable doc view of our target and hook it up with the // "remote" document as the sink of outgoing ops. doc = new MutableDocumentImpl<Node, Element, Text>( createSequencer(indexed), indexed); } /** * Check the content of both indexed documents is as expected * @param expectedContent */ protected void assertResult(String expectedContent) { String result = DocOpUtil.toXmlString(indexed.asOperation()); // Check the paste happened correctly assertEquals(expectedContent, result); } /** * Check the content of both indexed documents is as expected * @param expectedContent */ protected void assertOperationResult(String expectedContent) { String result = DocOpUtil.toXmlString(indexed.asOperation()); // Check the ops have been applied correctly assertEquals(expectedContent, result); } protected void assertCollapsedAt( int location, Range actual) { assertEquals(new Range(location, location), actual); } protected void assertCollapsedAt( int location, PointRange<Node> actual) { Point<Node> expected = l(location); assertEquals(new PointRange<Node>(expected, expected), actual); } protected void assertRangeAt( int start, int end, Range actual) { assertEquals(start, actual.getStart()); assertEquals(end, actual.getEnd()); } protected void assertRangeAt( int start, int end, PointRange<Node> actual) { PointRange<Node> expected = new PointRange<Node>(l(start), l(end)); assertEquals(expected, actual); } /** Test a simple set annotation */ public void testSetAnnotation() { init("<p>abcdef</p>"); doc.setAnnotation(3, 6, "style/color", "stix"); assertOperationResult( "<p>ab<?a \"style/color\"=\"stix\"?>cde<?a \"style/color\"?>f</p>"); } /** Test adding two non-overlapping settings of the same annotation */ public void testTwoNonOverlappingAnnotations() { init("<p>abcdef</p>"); doc.setAnnotation(2, 4, "style/color", "lola"); doc.setAnnotation(5, 6, "style/color", "lola"); assertOperationResult( "<p>a<?a \"style/color\"=\"lola\"?>bc<?a \"style/color\"?>d" + "<?a \"style/color\"=\"lola\"?>e<?a \"style/color\"?>f</p>"); } /** Test adding two overlapping settings of the same annotation */ public void testTwoOverlappingAnnotations() { init("<p>abcdef</p>"); doc.setAnnotation(2, 4, "style/color", "charlie"); doc.setAnnotation(3, 6, "style/color", "charlie"); assertOperationResult( "<p>a<?a \"style/color\"=\"charlie\"?>bcde<?a \"style/color\"?>f</p>"); } /** Test adding an annotation over the whole document */ public void testSetAnnotationOverWholeDocument() { init("<p>abcdef</p>"); doc.setAnnotation(0, doc.size(), "style/color", "flim"); assertOperationResult( "<?a \"style/color\"=\"flim\"?><p>abcdef</p><?a \"style/color\"?>"); } /** Test a simple set and unset of an annotation */ public void testSetAndUnsetAnnotation() { init("<p>abcdef</p>"); doc.setAnnotation(3, 6, "style/color", "maisy"); doc.setAnnotation(0, doc.size(), "style/color", null); assertOperationResult("<p>abcdef</p>"); } /** Test that trying to add a zero range annotation does nothing */ public void testZeroRangeSetAnnotation() { init("<p>abcdef</p>"); doc.setAnnotation(3, 3, "style/color", "blum"); assertOperationResult("<p>abcdef</p>"); } /** * Test that trying to add an annotation with a negative start throws * an IndexOutOfBoundsException. */ public void testNegativeStartSetAnnotationThrowsException() throws Exception { init("<p>abcdef</p>"); try { doc.setAnnotation(-1, 4, "style/color", "frub"); // Doh - no exception thrown. Fail the test assert false; } catch (IndexOutOfBoundsException iae) { // expected } } /** * Test that trying to add an annotation with an end past the size throws * an IndexOutOfBoundsException. */ public void testSetAnnotationPastDocEndThrowsException() throws Exception { init("<p>abcdef</p>"); try { doc.setAnnotation(1, doc.size() + 1, "style/color", "frub"); // Doh - no exception thrown. Fail the test fail(); } catch (IndexOutOfBoundsException iae) { // expected } } /** * Test that trying to add an annotation with a negative range throws * an IndexOutOfBoundsException. */ public void testNegativeRangeSetAnnotationThrowsException() throws Exception { init("<p>abcdef</p>"); try { doc.setAnnotation(4, 1, "style/color", "slarken"); // Doh - no exception thrown. Fail the test fail(); } catch (IndexOutOfBoundsException iae) { // expected } } /** Test a simple reset annotation */ public void testResetAnnotation() { init("<p>abcdef</p>"); doc.resetAnnotation(3, 6, "style/color", "pocoyo"); assertOperationResult( "<p>ab<?a \"style/color\"=\"pocoyo\"?>cde<?a \"style/color\"?>f</p>"); } /** Test a simple set and reset annotation */ public void testSetAndResetAnnotation() { init("<p>abcdef</p>"); doc.setAnnotation(0, doc.size(), "style/color", "pato"); doc.resetAnnotation(3, 6, "style/color", "pato"); assertOperationResult( "<p>ab<?a \"style/color\"=\"pato\"?>cde<?a \"style/color\"?>f</p>"); } /** * Test that using a zero range reset annotation clears the annotation over * the whole document. */ public void testZeroRangeResetAnnotationClearsDocument() { init("<p>abcdef</p>"); doc.setAnnotation(1, 4, "style/color", "spot"); doc.resetAnnotation(0, 0, "style/color", "spot"); assertOperationResult("<p>abcdef</p>"); init("<p>abcdef</p>"); doc.setAnnotation(1, 4, "style/color", "spot"); doc.resetAnnotation(2, 2, "style/color", "spot"); assertOperationResult("<p>abcdef</p>"); init("<p>abcdef</p>"); doc.setAnnotation(1, 4, "style/color", "spot"); doc.resetAnnotation(doc.size(), doc.size(), "style/color", "spot"); assertOperationResult("<p>abcdef</p>"); } /** * Test that trying to reset an annotation with a negative start throws * an IndexOutOfBoundsException. */ public void testNegativeStartResetAnnotationThrowsException() throws Exception { init("<p>abcdef</p>"); try { doc.resetAnnotation(-1, 4, "style/color", "frub"); // Doh - no exception thrown. Fail the test assert false; } catch (IndexOutOfBoundsException iae) { // expected } } /** * Test that trying to reset an annotation with an end bigger than the document * an IndexOutOfBoundsException. */ public void testResetAnnotationPastDocEndThrowsException() throws Exception { init("<p>abcdef</p>"); try { doc.resetAnnotation(1, doc.size() + 1, "style/color", "frub"); // Doh - no exception thrown. Fail the test assert false; } catch (IndexOutOfBoundsException iae) { // expected } } public void testMoveNodes() throws Exception { // simple move init("<root><before/><from/></root>"); Element root = doc.getDocumentElement().getFirstChild().asElement(); Node from = root.getLastChild(); doc.moveSiblings(Point.start(doc, root), from, null); assertOperationResult("<root><from/><before/></root>"); // move with attributes and children init("<root><before/> stuff <from> child <sub/></from> more <attr x=\"x\" y=\"z\"/> end</root>"); root = doc.getDocumentElement().getFirstChild().asElement(); Node stuff = root.getFirstChild().getNextSibling(); from = stuff.getNextSibling(); doc.moveSiblings(Point.before(doc, stuff), from, root.getLastChild()); assertOperationResult( "<root><before/><from> child <sub/></from> more <attr x=\"x\" y=\"z\"/> stuff end</root>"); // move with annotations // 0 1 234 567 8 9 10 11 12 init("<root><b>bo<i>ld</i></b><after/></root>"); doc.setAnnotation(1, 9, "b", "B"); // around the bs doc.setAnnotation(4, 8, "i", "I"); // around the is doc.setAnnotation(0, 3, "s", "S"); // overlaps the start doc.setAnnotation(7, 12, "e", "E"); // overlaps the end, AND covers the new range /* <root><b>bo<i>ld</i></b><after/></root> B BB B BB B B I II I S S S E E E E E <root><after/><b>bo<i>ld</i></b></root> B BB B BB B B I II I S S S E E E E E <?a "s"="S"?><root><?a "e"="E" "s"?><after/><?a "b"="B" "e" "s"="S"?><b>b<?a "s"?>o<?a "i"="I"?><i>ld<?a "e"="E"?></i><?a "i"?></b><?a "b"?></root><?a "e"?> */ root = doc.getDocumentElement().getFirstChild().asElement(); doc.moveSiblings(Point.end((Node) root), root.getFirstChild(), root.getLastChild()); assertOperationResult("<?a \"s\"=\"S\"?><root><?a \"e\"=\"E\" \"s\"?><after/>" + "<?a \"b\"=\"B\" \"e\" \"s\"=\"S\"?><b>b<?a \"s\"?>o<?a \"i\"=\"I\"?><i>ld" + "<?a \"e\"=\"E\"?></i><?a \"i\"?></b><?a \"b\"?></root><?a \"e\"?>"); } /** * Test we can atomically reset multiple annotations within a range. */ @SuppressWarnings("deprecation") // resetAnnotationsInRange (method under test) is deprecated public void xtestSimpleResetAnnotations() { // TODO(user): Fix this test. init("<p>abcdef</p>"); LinkedList<RangedValue<String>> annos = new LinkedList<RangedValue<String>>(); annos.add(new RangedValue<String>(2, 4, "cyril")); annos.add(new RangedValue<String>(5, 6, "tallulah")); doc.resetAnnotationsInRange(0, doc.size(), "style/color", annos); assertOperationResult( "<p>a<?a \"style/color\"=\"cyril\"?>bc<?a \"style/color\"?>d" + "<?a \"style/color\"=\"tallulah\"?>e<?a \"style/color\"?>f</p>"); // Just fail for now, so that we remember to come back to fix up this test. fail(); // DocumentOperationChecker.Recorder recorder = new DocumentOperationChecker.Recorder(); // recorder.begin(); // recorder.skip(3); // recorder.startAnnotation("style/color", "cyril"); // recorder.skip(2); // recorder.endAnnotation("style/color"); // recorder.skip(1); // recorder.startAnnotation("style/color", "tallulah"); // recorder.skip(1); // recorder.endAnnotation("style/color"); // recorder.finish(); // DocumentOperationChecker checker = recorder.finishRecording(); // latestOp.apply(checker); // checker.checkCompleted(); } /** * Test we can atomically extend an annotation to the right */ @SuppressWarnings("deprecation") // resetAnnotationsInRange (method under test) is deprecated public void xtestExtendAnnotationsRight() { // TODO(user): Fix this test. // Test extending to the right init("<p>23456789</p>"); LinkedList<RangedValue<String>> annos = new LinkedList<RangedValue<String>>(); doc.setAnnotation(1, 2, "style/color", "marv"); doc.setAnnotation(3, 4, "style/color", "eddie"); annos.add(new RangedValue<String>(1, 2, "marv")); annos.add(new RangedValue<String>(3, 6, "eddie")); doc.resetAnnotationsInRange(1, 8, "style/color", annos); assertOperationResult( "<p>" + "<?a \"style/color\"=\"marv\"?>2<?a \"style/color\"?>" + "3" + "<?a \"style/color\"=\"eddie\"?>456<?a \"style/color\"?>" + "789" + "</p>"); // Just fail for now, so that we remember to come back to fix up this test. fail(); // DocumentOperationChecker.Recorder recorder = new DocumentOperationChecker.Recorder(); // recorder.begin(); // recorder.skip(5); // recorder.startAnnotation("style/color", "eddie"); // recorder.skip(2); // recorder.endAnnotation("style/color"); // recorder.finish(); // DocumentOperationChecker checker = recorder.finishRecording(); // latestOp.apply(checker); // checker.checkCompleted(); } /** * Test we can atomically extend an annotation to the left */ @SuppressWarnings("deprecation") // resetAnnotationsInRange (method under test) is deprecated public void xtestExtendAnnotationsLeft() { // TODO(user): Fix this test. init("<p>23456789</p>"); LinkedList<RangedValue<String>> annos = new LinkedList<RangedValue<String>>(); doc.setAnnotation(1, 2, "style/color", "lotta"); doc.setAnnotation(3, 4, "style/color", "sizzles"); annos.add(new RangedValue<String>(1, 2, "lotta")); annos.add(new RangedValue<String>(2, 4, "sizzles")); doc.resetAnnotationsInRange(0, 7, "style/color", annos); assertOperationResult( "<p>" + "<?a \"style/color\"=\"lotta\"?>2" + "<?a \"style/color\"=\"sizzles\"?>34<?a \"style/color\"?>" + "56789" + "</p>"); // Just fail for now, so that we remember to come back to fix up this test. fail(); // DocumentOperationChecker.Recorder recorder = new DocumentOperationChecker.Recorder(); // recorder.begin(); // recorder.skip(3); // recorder.startAnnotation("style/color", "sizzles"); // recorder.skip(1); // recorder.endAnnotation("style/color"); // recorder.finish(); // DocumentOperationChecker checker = recorder.finishRecording(); // latestOp.apply(checker); // checker.checkCompleted(); } /** * Test we can atomically reset multiple annotations within a range that also clear * other existing annotations. */ @SuppressWarnings("deprecation") // resetAnnotationsInRange (method under test) is deprecated public void xtestResetAnnotations() { // TODO(user): Fix this test. init("<p>23456789</p>"); LinkedList<RangedValue<String>> annos = new LinkedList<RangedValue<String>>(); doc.setAnnotation(0, 1, "style/color", "marv"); doc.setAnnotation(3, 5, "style/color", "eddie"); annos.add(new RangedValue<String>(2, 3, "charley")); annos.add(new RangedValue<String>(4, 6, "morten")); doc.resetAnnotationsInRange(0, 6, "style/color", annos); assertOperationResult( "<p>" + "2" + "<?a \"style/color\"=\"charley\"?>3<?a \"style/color\"?>" + "4" + "<?a \"style/color\"=\"morten\"?>56<?a \"style/color\"?>" + "789" + "</p></blip>"); // Just fail for now, so that we remember to come back to fix up this test. fail(); // DocumentOperationChecker.Recorder recorder = new DocumentOperationChecker.Recorder(); // recorder.begin(); // recorder.skip(1); // recorder.startAnnotation("style/color", null); // recorder.skip(1); // recorder.endAnnotation("style/color"); // recorder.skip(1); // recorder.startAnnotation("style/color", "charley"); // recorder.skip(1); // recorder.endAnnotation("style/color"); // recorder.startAnnotation("style/color", null); // recorder.skip(1); // recorder.endAnnotation("style/color"); // // TODO(user): optimise the below sequence to combine the two sets // // of the same value // recorder.startAnnotation("style/color", "morten"); // recorder.skip(1); // recorder.endAnnotation("style/color"); // recorder.startAnnotation("style/color", "morten"); // recorder.skip(1); // recorder.endAnnotation("style/color"); // recorder.finish(); // DocumentOperationChecker checker = recorder.finishRecording(); // latestOp.apply(checker); // checker.checkCompleted(); } // TODO(danilatos): test all the other content manipulation methods. /** * Tests that createChildElement does as it says. */ public void testCreateChildElement() { init("<p>first child</p>"); Element root = doc.getDocumentElement(); doc.createChildElement(root, "child", Collections.<String, String> emptyMap()); assertOperationResult("<p>first child</p><child/>"); } public void testCompareRangedValueByStartThenEndButIgnoreValue() { CompareRangedValueByStartThenEnd<String> comp = new CompareRangedValueByStartThenEnd<String>(); { // first wholly to the left of second RangedValue<String> first = new RangedValue<String>(0, 3, "a"); RangedValue<String> second = new RangedValue<String>(5, 6, null); assert(comp.compare(first, second) < 0); } { // End of first touching start of second RangedValue<String> first = new RangedValue<String>(0, 3, "a"); RangedValue<String> second = new RangedValue<String>(3, 6, null); assert(comp.compare(first, second) < 0); } { // End of first within second RangedValue<String> first = new RangedValue<String>(0, 4, "a"); RangedValue<String> second = new RangedValue<String>(3, 6, null); assert(comp.compare(first, second) < 0); } { // First and second start at the same place, first ends first RangedValue<String> first = new RangedValue<String>(3, 4, "a"); RangedValue<String> second = new RangedValue<String>(3, 6, null); assert(comp.compare(first, second) < 0); } { // First equal to second RangedValue<String> first = new RangedValue<String>(3, 4, "a"); RangedValue<String> second = new RangedValue<String>(3, 4, null); assert(comp.compare(first, second) == 0); } { // First starts within second, ends are equal RangedValue<String> first = new RangedValue<String>(3, 4, "a"); RangedValue<String> second = new RangedValue<String>(2, 4, null); assert(comp.compare(first, second) > 0); } { // First starts within second, first ends after second RangedValue<String> first = new RangedValue<String>(3, 5, "a"); RangedValue<String> second = new RangedValue<String>(2, 4, null); assert(comp.compare(first, second) > 0); } { // First starts where second ends RangedValue<String> first = new RangedValue<String>(4, 5, "a"); RangedValue<String> second = new RangedValue<String>(2, 4, null); assert(comp.compare(first, second) > 0); } { // First wholly to the right of second RangedValue<String> first = new RangedValue<String>(5, 6, "a"); RangedValue<String> second = new RangedValue<String>(2, 4, null); assert(comp.compare(first, second) > 0); } } }