/** * Copyright 2009 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.conversation; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyLong; import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.waveprotocol.wave.model.testing.ExtraAsserts.assertStructureEquivalent; import junit.framework.TestCase; import org.mockito.InOrder; import org.waveprotocol.wave.model.conversation.Conversation.Anchor; import org.waveprotocol.wave.model.conversation.ConversationBlip.LocatedReplyThread; import org.waveprotocol.wave.model.document.MutableDocument; import org.waveprotocol.wave.model.document.MutableDocument.Action; import org.waveprotocol.wave.model.document.util.DocHelper; import org.waveprotocol.wave.model.document.util.DocIterate; import org.waveprotocol.wave.model.document.util.LineContainers; import org.waveprotocol.wave.model.document.util.Point; import org.waveprotocol.wave.model.document.util.XmlStringBuilder; import org.waveprotocol.wave.model.operation.wave.WorthyChangeChecker; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.wave.ParticipantId; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Tests for abstract conversation, thread and blip interfaces. * * @author anorth@google.com (Alex North) */ public abstract class ConversationTestBase extends TestCase { private ObservableConversation target; private ObservableConversation alternate; protected ObservableConversation.Listener convListener; @Override protected void setUp() throws Exception { super.setUp(); target = makeConversation(); alternate = makeConversation(); convListener = mock(ObservableConversation.Listener.class); } /** * Creates a new, empty conversation object. All created conversations must be * from the same conversation view. */ protected abstract ObservableConversation makeConversation(); /** * Creates a new conversation object backed by the same data as another. * Changes made to one conversation should trigger events in the other. */ protected abstract ObservableConversation mirrorConversation(ObservableConversation toMirror); /** Checks that a blip is still valid. */ protected abstract void assertBlipValid(ConversationBlip blip); /** Checks that a blip is invalid. */ protected abstract void assertBlipInvalid(ConversationBlip blip); /** Checks that a thread is invalid. */ protected abstract void assertThreadInvalid(ConversationThread thread); /** Checks that a thread is valid. */ protected abstract void assertThreadValid(ConversationThread thread); // // Anchoring // public void testEmptyConversationIsNotAnchored() { assertFalse(target.hasAnchor()); } public void testCreateAnchor() { populate(alternate); ConversationBlip blip = getFirstBlip(alternate); Anchor anchor = alternate.createAnchor(blip); assertTrue(alternate == anchor.getConversation()); assertTrue(blip == anchor.getBlip()); } public void testSetAnchor() { populate(alternate); ConversationBlip blip = getFirstBlip(alternate); Anchor anchor = alternate.createAnchor(blip); target.setAnchor(anchor); assertTrue(target.hasAnchor()); assertEquals(anchor, target.getAnchor()); } public void testAnchorToSelfFails() { populate(target); ConversationBlip blip = getFirstBlip(target); Anchor anchor = target.createAnchor(blip); try { target.setAnchor(anchor); fail("Expected an IllegalArgumentException"); } catch (IllegalArgumentException expected) { } assertFalse(target.hasAnchor()); } public void testClearAnchor() { populate(alternate); ConversationBlip blip = getFirstBlip(alternate); Anchor anchor = alternate.createAnchor(blip); target.setAnchor(anchor); target.setAnchor(null); assertFalse(target.hasAnchor()); } // Regression test for a bug where the manifest was forgotten after // any element removed. public void testAnchorStillAccessibleAfterBlipAdded() { populate(alternate); ConversationBlip blip = getFirstBlip(alternate); Anchor anchor = alternate.createAnchor(blip); target.setAnchor(anchor); target.getRootThread().appendBlip(); target.getRootThread().getFirstBlip().delete(); assertTrue(target.hasAnchor()); assertEquals(anchor, target.getAnchor()); } // // Participants. // public void testAddedParticipantIsRetreived() { ParticipantId creator = target.getParticipantIds().iterator().next(); ParticipantId fake = new ParticipantId("bill@foo.com"); target.addParticipant(fake); assertEquals(Arrays.asList(creator, fake), CollectionUtils.newArrayList(target.getParticipantIds())); } public void testRemovedParticipantNoLongerRetrieved() { ParticipantId creator = target.getParticipantIds().iterator().next(); ParticipantId fake = new ParticipantId("bill@foo.com"); target.addParticipant(fake); target.removeParticipant(fake); assertEquals(Collections.singletonList(creator), CollectionUtils.newArrayList(target.getParticipantIds())); } public void testParticipantsAreASet() { ParticipantId creator = target.getParticipantIds().iterator().next(); ParticipantId fake1 = new ParticipantId("joe"); ParticipantId fake2 = new ParticipantId("bill"); List<ParticipantId> participants = CollectionUtils.newArrayList(creator, fake1, fake2); target.addParticipant(fake1); target.addParticipant(fake2); assertEquals(participants, CollectionUtils.newArrayList(target.getParticipantIds())); target.addParticipant(fake2); assertEquals(participants, CollectionUtils.newArrayList(target.getParticipantIds())); } // // Threads and blips // public void testEmptyRootThreadHasNoBlips() { assertNotNull(target.getRootThread()); assertSame(target, target.getRootThread().getConversation()); assertNull(target.getRootThread().getFirstBlip()); assertNull(target.getRootThread().getParentBlip()); } public void testAppendBlipAppendsBlipsToThread() { ConversationThread thread = target.getRootThread(); ConversationBlip b1 = thread.appendBlip(); ConversationBlip b2 = thread.appendBlip(); ConversationBlip b3 = thread.appendBlip(); assertSame(b1, thread.getFirstBlip()); assertSame(target, b1.getConversation()); assertSame(thread, b1.getThread()); assertSame(b1, target.getBlip(b1.getId())); assertEquals(Arrays.asList(b1, b2, b3), getBlipList(thread)); } public void testInsetBlipBeforeFirstBlipCreatesNewFirstBlip() { ConversationThread thread = target.getRootThread(); ConversationBlip oldFirst = thread.appendBlip(); ConversationBlip newFirst = thread.insertBlip(oldFirst); assertSame(thread.getFirstBlip(), newFirst); assertSame(newFirst, target.getBlip(newFirst.getId())); assertSame(oldFirst, target.getBlip(oldFirst.getId())); assertEquals(Arrays.asList(newFirst, oldFirst), getBlipList(thread)); } public void testInsertBlipBetweenBlipsInserts() { ConversationThread thread = target.getRootThread(); ConversationBlip first = thread.appendBlip(); ConversationBlip last = thread.appendBlip(); ConversationBlip middle = thread.insertBlip(last); assertEquals(Arrays.asList(first, middle, last), getBlipList(thread)); } public void testAppendRepliesAppendsRepliesToBlip() { ConversationBlip blip = target.getRootThread().appendBlip(); ConversationThread t1 = blip.addReplyThread(); // Append blips to get a new ID for the next thread. t1.appendBlip(); ConversationThread t2 = blip.addReplyThread(); t2.appendBlip(); ConversationThread t3 = blip.addReplyThread(); t3.appendBlip(); assertSame(blip, t1.getParentBlip()); assertEquals(Arrays.asList(t1, t2, t3), CollectionUtils.newArrayList(blip.getReplyThreads())); assertThreadChildrenConsistent(blip); } public void testAppendInlineReplyCreatesInlineThread() { ConversationBlip blip = target.getRootThread().appendBlip(); MutableDocument<?, ?, ?> doc = blip.getContent(); int location = locateAfterLineElement(doc); ConversationThread thread = blip.addReplyThread(location); assertSame(blip, thread.getParentBlip()); assertEquals(Collections.singletonList(LocatedReplyThread.of(thread, location)), blip.locateReplyThreads()); assertThreadChildrenConsistent(blip); } public void testInlineReplyWithMultipleAnchorsUsesFirst() { ConversationBlip blip = target.getRootThread().appendBlip(); MutableDocument<?, ?, ?> doc = blip.getContent(); final int location = locateAfterLineElement(doc); ConversationThread thread = blip.addReplyThread(location); // Duplicate the anchor. doc.with(new Action() { public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) { E anchor = Point.elementAfter(doc, doc.locate(location)); E anchorParent = doc.getParentElement(anchor); doc.createChildElement(anchorParent, doc.getTagName(anchor), doc.getAttributes(anchor)); } }); assertEquals(Collections.singletonList(LocatedReplyThread.of(thread, location)), blip.locateReplyThreads()); } public void testInlineReplyPointUpdatesWithDocContent() { final ConversationBlip blip = target.getRootThread().appendBlip(); MutableDocument<?, ?, ?> doc = blip.getContent(); doc.with(new Action() { public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) { Point<N> startText = doc.locate(locateAfterLineElement(doc)); doc.insertText(startText, "cd"); // Insert reply between c|d. N bodyNode = DocHelper.getElementWithTagName(doc, Blips.BODY_TAGNAME); N textNode = doc.getFirstChild(bodyNode); textNode = doc.getNextSibling(textNode); int replyLocation = doc.getLocation(Point.inText(textNode, 1)); blip.addReplyThread(replyLocation); // Insert text to give abc|d startText = Point.before(doc, textNode); doc.insertText(startText, "ab"); int newLocation = blip.locateReplyThreads().iterator().next().getLocation(); assertEquals(replyLocation + 2, newLocation); } }); } public void testInlineReplyWithDeletedAnchorHasInvalidLocation() { final ConversationBlip blip = target.getRootThread().appendBlip(); MutableDocument<?, ?, ?> doc = blip.getContent(); doc.with(new Action() { public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) { Point<N> startText = doc.locate(locateAfterLineElement(doc)); doc.insertText(startText, "cd"); // Insert reply between c|d. N bodyNode = DocHelper.getElementWithTagName(doc, Blips.BODY_TAGNAME); N textNode = doc.getFirstChild(bodyNode); textNode = doc.getNextSibling(textNode); int replyLocation = doc.getLocation(Point.inText(textNode, 1)); ConversationThread replyThread = blip.addReplyThread(replyLocation); // Delete text and anchor. doc.deleteRange(Point.before(doc, textNode), Point.inElement(bodyNode, null)); int newLocation = blip.locateReplyThreads().iterator().next().getLocation(); assertEquals(Blips.INVALID_INLINE_LOCATION, newLocation); } }); } public void testInlineRepliesInLocationOrder() { final ConversationBlip blip = target.getRootThread().appendBlip(); MutableDocument<?, ?, ?> doc = blip.getContent(); doc.with(new Action() { public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) { Point<N> startText = doc.locate(locateAfterLineElement(doc)); int replyLocation = doc.getLocation(startText); ConversationThread t1 = blip.addReplyThread(replyLocation); t1.appendBlip(); // In front of t1. ConversationThread t2 = blip.addReplyThread(replyLocation); t2.appendBlip(); // In front of the others. ConversationThread t3 = blip.addReplyThread(replyLocation); t3.appendBlip(); // Delete t3's anchor. E anchorToDelete = Point.elementAfter(doc, doc.locate(replyLocation)); doc.deleteNode(anchorToDelete); List<LocatedReplyThread<ConversationThread>> expected = new ArrayList<LocatedReplyThread<ConversationThread>>(); expected.add(LocatedReplyThread.of(t2, replyLocation)); expected.add(LocatedReplyThread.of(t1, replyLocation + 2)); expected.add(LocatedReplyThread.of(t3, Blips.INVALID_INLINE_LOCATION)); List<LocatedReplyThread<? extends ConversationThread>> threads = CollectionUtils.newArrayList(blip.locateReplyThreads()); assertEquals(expected, threads); } }); } public void testDeleteSingleRootThreadBlipRemovesIt() { ConversationBlip blip = target.getRootThread().appendBlip(); blip.delete(); assertNull(target.getRootThread().getFirstBlip()); assertBlipInvalid(blip); } public void testDeleteSingleNonRootThreadBlipRemovesIt() { ConversationThread thread = target.getRootThread().appendBlip().addReplyThread(); ConversationBlip unDeleted = thread.appendBlip(); ConversationBlip blip = thread.appendBlip(); blip.delete(); assertEquals(Arrays.asList(unDeleted), getBlipList(thread)); assertBlipInvalid(blip); } public void testCanAppendAfterDeletingOnlyRootThreadBlip() { ConversationBlip first = target.getRootThread().appendBlip(); first.delete(); ConversationBlip second = target.getRootThread().appendBlip(); assertBlipInvalid(first); assertBlipValid(second); } public void testCanAppendAfterDeletingRootThreadReplies() { ConversationBlip first = target.getRootThread().appendBlip(); ConversationBlip second = target.getRootThread().appendBlip(); ConversationThread reply = first.addReplyThread(); reply.appendBlip(); second.delete(); first.delete(); ConversationBlip newFirst = target.getRootThread().appendBlip(); assertBlipValid(newFirst); } public void testDeleteBlipInThreadLeavesSiblings() { ConversationBlip b1 = target.getRootThread().appendBlip(); ConversationBlip b2 = target.getRootThread().appendBlip(); ConversationBlip b3 = target.getRootThread().appendBlip(); b2.delete(); assertEquals(Arrays.asList(b1, b3), getBlipList(target.getRootThread())); b1.delete(); assertEquals(Arrays.asList(b3), getBlipList(target.getRootThread())); } public void testDeleteBlipWithInlineReplyDeletesReply() { ConversationBlip blip = target.getRootThread().appendBlip(); MutableDocument<?, ?, ?> doc = blip.getContent(); ConversationThread reply = blip.addReplyThread(locateAfterLineElement(doc)); ConversationBlip replyBlip = reply.appendBlip(); blip.delete(); assertNull(target.getRootThread().getFirstBlip()); assertThreadInvalid(reply); assertBlipInvalid(replyBlip); } public void testDeleteBlipWithManyRepliesDeletesReplies() { ConversationBlip blip = target.getRootThread().appendBlip(); MutableDocument<?, ?, ?> doc = blip.getContent(); ConversationThread reply1 = blip.addReplyThread(); // Append blips to get a new ID for the next thread. reply1.appendBlip(); ConversationThread inlineReply1 = blip.addReplyThread(locateAfterLineElement(doc)); inlineReply1.appendBlip(); ConversationThread reply2 = blip.addReplyThread(); reply2.appendBlip(); ConversationThread inlineReply2 = blip.addReplyThread(locateAfterLineElement(doc)); inlineReply2.appendBlip(); blip.delete(); assertNull(target.getRootThread().getFirstBlip()); assertBlipInvalid(blip); assertThreadInvalid(reply1); assertThreadInvalid(reply2); assertThreadInvalid(inlineReply1); assertThreadInvalid(inlineReply2); } public void testDeletedConversationIsUnusable() { target.delete(); assertConversationAccessible(target); assertConversationUnusable(target); } public void testDeleteConversationInvalidatesBlips() { target.getRootThread(); ObservableConversationBlip blip1 = target.getRootThread().appendBlip(); ObservableConversationBlip blip2 = target.getRootThread().appendBlip(); target.addListener(convListener); target.delete(); assertConversationUnusable(target); assertBlipInvalid(blip1); assertBlipInvalid(blip2); verify(convListener).onBlipDeleted(blip1); verify(convListener).onBlipDeleted(blip2); } public void testDeleteConversationInvalidatesNonRootThreads() { ObservableConversationBlip outerBlip = target.getRootThread().appendBlip(); ObservableConversationThread inlineThread = outerBlip.addReplyThread(locateAfterLineElement(outerBlip.getContent())); ObservableConversationBlip innerBlip = inlineThread.appendBlip(); target.addListener(convListener); target.delete(); assertBlipInvalid(outerBlip); assertBlipInvalid(innerBlip); assertThreadInvalid(inlineThread); } public void testDeleteConversationEvents() { ObservableConversationBlip outerBlip = target.getRootThread().appendBlip(); ObservableConversationThread inlineThread = outerBlip.addReplyThread(locateAfterLineElement(outerBlip.getContent())); ObservableConversationBlip innerBlip = inlineThread.appendBlip(); target.addListener(convListener); target.delete(); assertBlipInvalid(outerBlip); assertBlipInvalid(innerBlip); assertThreadInvalid(inlineThread); verify(convListener).onBlipDeleted(innerBlip); verify(convListener).onThreadDeleted(inlineThread); verify(convListener).onBlipDeleted(outerBlip); verifyNoMoreInteractions(convListener); } /** * Tests that non-inline replies to an inline reply are deleted * completely when the inline reply's parent blip is deleted. No * tombstones remain. */ public void testDeleteBlipDeletesRepliesToInlineReply() { ConversationBlip blip = target.getRootThread().appendBlip(); ConversationThread inlineReply = blip.addReplyThread(locateAfterLineElement( blip.getContent())); ConversationBlip inlineReplyBlip = inlineReply.appendBlip(); ConversationThread nonInlineReplyToReply = inlineReplyBlip.addReplyThread(); ConversationBlip nonInlineReplyBlip = nonInlineReplyToReply.appendBlip(); blip.delete(); assertNull(target.getRootThread().getFirstBlip()); assertBlipInvalid(nonInlineReplyBlip); assertThreadInvalid(nonInlineReplyToReply); assertBlipInvalid(inlineReplyBlip); assertThreadInvalid(inlineReply); } public void testDeleteLastBlipInNonRootThreadDeletesThread() { ConversationBlip blip = target.getRootThread().appendBlip(); ConversationThread replyThread = blip.addReplyThread(); ConversationBlip replyBlip = replyThread.appendBlip(); replyBlip.delete(); assertFalse(blip.getReplyThreads().iterator().hasNext()); assertThreadChildrenConsistent(blip); assertThreadInvalid(replyThread); } // Bug 2220263. public void testCanReplyAfterDeletingReplyThread() { ConversationThread topThread = target.getRootThread().appendBlip().addReplyThread(); ConversationBlip topBlip = topThread.appendBlip(); // Add two reply threads. Delete the second (by deleting its blip). ConversationThread firstReply = topBlip.addReplyThread(); firstReply.appendBlip(); ConversationThread secondReply = topBlip.addReplyThread(); secondReply.appendBlip().delete(); // Reply again. This used to throw IndexOutOfBounds. ConversationThread replacementReply = topBlip.addReplyThread(); ConversationBlip replacementBlip = replacementReply.appendBlip(); assertBlipValid(replacementBlip); assertEquals(Arrays.asList(firstReply, replacementReply), CollectionUtils.newArrayList(topBlip.getReplyThreads())); } public void testDeleteInlineReplyDeletesAnchor() { ConversationBlip blip = target.getRootThread().appendBlip(); XmlStringBuilder xmlBefore = XmlStringBuilder.innerXml(blip.getContent()); ConversationThread inlineReply = blip.addReplyThread(locateAfterLineElement( blip.getContent())); ConversationBlip inlineReplyBlip = inlineReply.appendBlip(); inlineReplyBlip.delete(); assertBlipInvalid(inlineReplyBlip); assertThreadInvalid(inlineReply); assertStructureEquivalent(xmlBefore, blip.getContent()); } public void testDeleteRootThreadRemovesAllBlips() { ConversationThread rootThread = target.getRootThread(); ConversationBlip first = rootThread.appendBlip(); ConversationBlip second = rootThread.appendBlip(); rootThread.delete(); assertBlipInvalid(first); assertBlipInvalid(second); assertEquals(CollectionUtils.newArrayList(), getBlipList(rootThread)); assertThreadValid(rootThread); } public void testDeleteNonRootThreadRemovesAllBlipsAndThread() { ConversationBlip blip = target.getRootThread().appendBlip(); ConversationThread replyThread = blip.addReplyThread(); ConversationBlip replyBlip1 = replyThread.appendBlip(); ConversationBlip replyBlip2 = replyThread.appendBlip(); replyThread.delete(); assertFalse(blip.getReplyThreads().iterator().hasNext()); assertThreadChildrenConsistent(blip); assertBlipInvalid(replyBlip1); assertBlipInvalid(replyBlip2); assertThreadInvalid(replyThread); } public void testDeleteEmptyThread() { ConversationBlip blip = target.getRootThread().appendBlip(); ConversationThread replyThread = blip.addReplyThread(); replyThread.delete(); assertFalse(blip.getReplyThreads().iterator().hasNext()); assertThreadChildrenConsistent(blip); assertThreadInvalid(replyThread); } /** * Tests that methods which access the state of a blip without changing it * are correct after blip deletion. */ public void testBlipCanBeAccessedAfterDeletion() { ConversationBlip blip = target.getRootThread().appendBlip(); blip.delete(); assertBlipInvalid(blip); assertBlipAccessible(blip); assertEquals(target.getRootThread(), blip.getThread()); assertEquals(Collections.emptyList(), getBlipList(target.getRootThread())); assertEquals(Collections.emptyList(), getAllReplyList(blip)); } /** * Tests that methods which access the state of a blip (this time with a * child thread) without changing it are correct after blip deletion. */ public void testBlipWithThreadCanBeAccessedAfterDeletion() { ConversationBlip blip = target.getRootThread().appendBlip(); ConversationThread thread = blip.addReplyThread(); blip.delete(); assertBlipInvalid(blip); assertBlipAccessible(blip); assertEquals(target.getRootThread(), blip.getThread()); assertEquals(Collections.emptyList(), getBlipList(target.getRootThread())); assertEquals(blip, thread.getParentBlip()); assertEquals(Collections.emptyList(), getAllReplyList(blip)); } /** * Tests that methods which access the state of a thread without changing it * are correct after thread deletion. */ public void testThreadCanBeAccessedAfterDeletion() { ConversationBlip blip = target.getRootThread().appendBlip(); ConversationThread thread = blip.addReplyThread(); ConversationBlip replyBlip = thread.appendBlip(); thread.delete(); assertBlipInvalid(replyBlip); assertBlipAccessible(replyBlip); assertThreadInvalid(thread); assertThreadAccessible(thread); assertEquals(blip, thread.getParentBlip()); assertFalse(blip.getReplyThreads().iterator().hasNext()); assertEquals(thread, replyBlip.getThread()); assertEquals(Collections.emptyList(), getBlipList(thread)); } // // Tests for ObservableConversation. // public void testSetAnchorEventsAreFired() { populate(alternate); ObservableConversation.AnchorListener listener = mock(ObservableConversation.AnchorListener.class); target.addListener(listener); Anchor anchor1 = alternate.createAnchor(getFirstBlip(alternate)); // Set anchor from null. target.setAnchor(anchor1); verify(listener).onAnchorChanged(null, anchor1); // Change anchor to different blip. Anchor anchor11 = alternate.createAnchor(alternate.getRootThread().getFirstBlip() .getReplyThreads().iterator().next().getFirstBlip()); target.setAnchor(anchor11); verify(listener).onAnchorChanged(anchor1, anchor11); // Change anchor to different wavelet. ObservableConversation alternate2 = makeConversation(); populate(alternate2); Anchor anchor2 = alternate2.createAnchor(getFirstBlip(alternate2)); target.setAnchor(anchor2); verify(listener).onAnchorChanged(anchor11, anchor2); // Set anchor to null. target.setAnchor(null); verify(listener).onAnchorChanged(anchor2, null); // Remove listener. target.removeListener(listener); target.setAnchor(anchor1); verifyNoMoreInteractions(listener); } // These methods test that local modifications cause events via the // blip and thread listeners. They test that modifications to the underlying // data cause events via the conversation listener on a mirror conversation. public void testParticipantChangesFireEvents() { ParticipantId p1 = new ParticipantId("someone@example.com"); ParticipantId p2 = new ParticipantId("else@example.com"); ObservableConversation mirror = mirrorConversation(target); mirror.addListener(convListener); target.addParticipant(p1); target.addParticipant(p2); verify(convListener).onParticipantAdded(p1); verify(convListener).onParticipantAdded(p2); target.addParticipant(p1); verifyNoMoreInteractions(convListener); target.removeParticipant(p2); verify(convListener).onParticipantRemoved(p2); } public void testThreadAppendInsertBlipFiresEvent() { ObservableConversation mirror = mirrorConversation(target); mirror.addListener(convListener); ObservableConversationBlip b1 = target.getRootThread().appendBlip(); ObservableConversationBlip b1mirror = mirror.getRootThread().getFirstBlip(); verify(convListener).onBlipAdded(b1mirror); target.getRootThread().insertBlip(b1); ObservableConversationBlip b2mirror = mirror.getRootThread().getFirstBlip(); verify(convListener).onBlipAdded(b2mirror); allowBlipTimestampChanged(convListener); verifyNoMoreInteractions(convListener); } public void testThreadRemovalFiresEvent() { ObservableConversation mirror = mirrorConversation(target); ObservableConversationBlip b1 = target.getRootThread().appendBlip(); ObservableConversationThread t1 = b1.addReplyThread(); ObservableConversationThread t1mirror = mirror.getRootThread().getFirstBlip() .getReplyThreads().iterator().next(); t1.appendBlip(); ObservableConversationBlip b3mirror = t1mirror.getFirstBlip(); mirror.addListener(convListener); // Trigger thread deletion. t1.delete(); verify(convListener).onBlipDeleted(b3mirror); verify(convListener).onThreadDeleted(t1mirror); allowBlipTimestampChanged(convListener); verifyNoMoreInteractions(convListener); } public void testRootThreadRemovalDoesntFireEvent() { ObservableConversation mirror = mirrorConversation(target); target.getRootThread().appendBlip(); ObservableConversationBlip b1mirror = mirror.getRootThread().getFirstBlip(); mirror.addListener(convListener); // Trigger thread deletion. target.getRootThread().delete(); verify(convListener).onBlipDeleted(b1mirror); allowBlipTimestampChanged(convListener); verifyNoMoreInteractions(convListener); } public void testBlipAppendReplyFiresEvent() { ObservableConversation mirror = mirrorConversation(target); ObservableConversationBlip b1 = target.getRootThread().appendBlip(); ObservableConversationBlip b1mirror = mirror.getRootThread().getFirstBlip(); mirror.addListener(convListener); b1.addReplyThread(); ObservableConversationThread t1mirror = b1mirror.getReplyThreads().iterator().next(); verify(convListener).onThreadAdded(t1mirror); verifyNoMoreInteractions(convListener); } public void testBlipRemovalFiresEvent() { ObservableConversation mirror = mirrorConversation(target); ObservableConversationBlip b1 = target.getRootThread().appendBlip(); ObservableConversationBlip b1mirror = mirror.getRootThread().getFirstBlip(); mirror.addListener(convListener); b1.delete(); verify(convListener).onBlipDeleted(b1mirror); allowBlipTimestampChanged(convListener); verifyNoMoreInteractions(convListener); } public void testCompoundEventsFireBottomUp() { ObservableConversation mirror = mirrorConversation(target); // Build tall structure. // rootThread // |- b1 (deleted) // |- t1 // |- b2 ObservableConversationBlip b1 = target.getRootThread().appendBlip(); ObservableConversationThread t1 = b1.addReplyThread(); ObservableConversationBlip b2 = t1.appendBlip(); ObservableConversationBlip b1mirror = mirror.getRootThread().getFirstBlip(); ObservableConversationThread t1mirror = b1mirror.getReplyThreads().iterator().next(); ObservableConversationBlip b2mirror = t1mirror.getFirstBlip(); mirror.addListener(convListener); // Trigger cascading deletion. b1.delete(); // Timestamp changed events may have also occurred on the blip listeners. // Mockito doesn't support atMost on inOrder verifications, hence we cannot // verify those events then verifyNoMoreInteractions on the blip listeners. // TODO(anorth): verifyNoMoreInteractions when the CWM injects a clock. InOrder order = inOrder(convListener); order.verify(convListener).onBlipDeleted(b2mirror); order.verify(convListener).onThreadDeleted(t1mirror); order.verify(convListener).onBlipDeleted(b1mirror); allowBlipTimestampChanged(convListener); verifyNoMoreInteractions(convListener); } public void testRemovedListenersReceiveNoEvents() { ObservableConversation mirror = mirrorConversation(target); ObservableConversationBlip b1 = target.getRootThread().appendBlip(); ObservableConversationThread t1 = b1.addReplyThread(); ObservableConversationBlip b2 = t1.appendBlip(); ObservableConversationBlip b1mirror = mirror.getRootThread().getFirstBlip(); ObservableConversationThread t1mirror = b1mirror.getReplyThreads().iterator().next(); t1mirror.getFirstBlip(); mirror.addListener(convListener); mirror.removeListener(convListener); b1.delete(); verifyNoMoreInteractions(convListener); } // // Data documents // public void testCanGetDataDocument() { MutableDocument<?, ?, ?> doc = target.getDataDocument("some-doc-id"); assertNotNull(doc); } public void testCannotGetBlipAsDataDocument() { ConversationBlip blip = target.getRootThread().appendBlip(); try { target.getDataDocument(blip.getId()); fail("Expected an exception fetching a blip document as a data doc"); } catch (IllegalArgumentException expected) { } } public void testCannotGetManifestAsDataDocument() { try { target.getDataDocument("conversation"); fail("Expected an exception fetching manifest as a data doc"); } catch (IllegalArgumentException expected) { } } public void testWorthynessConstant() { assertEquals(Blips.THREAD_INLINE_ANCHOR_TAGNAME, WorthyChangeChecker.THREAD_INLINE_ANCHOR_TAGNAME); } protected static ConversationBlip getFirstBlip(Conversation conv) { return conv.getRootThread().getFirstBlip(); } /** * Appends a blip to the root thread, and adds a reply to that blip with one * blip. */ protected static void populate(Conversation conv) { ConversationBlip blip = conv.getRootThread().appendBlip(); blip.addReplyThread().appendBlip(); } protected static <N> int locateAfterLineElement(MutableDocument<N, ?, ?> doc) { return locateAfterLineElementInner(doc); } private static <N, E extends N, T extends N> int locateAfterLineElementInner( MutableDocument<N, E, T> doc) { for (E el : DocIterate.deepElementsReverse(doc, doc.getDocumentElement(), null)) { if (LineContainers.isLineContainer(doc, el)) { Point<N> point = Point.inElement((N) el, null); return doc.getLocation(point); } } LineContainers.appendLine(doc, XmlStringBuilder.createEmpty()); return locateAfterLineElement(doc); } /** * Convenience function that returns the blips in a thread as a List. */ protected static List<ConversationBlip> getBlipList(ConversationThread thread) { return CollectionUtils.newArrayList(thread.getBlips()); } /** * Convenience function that returns all reply threads to a blip as a List. */ protected static List<ConversationThread> getAllReplyList(ConversationBlip blip) { return CollectionUtils.newArrayList(blip.getReplyThreads()); } /** * Verifies any number of method invocations on a mock. */ protected static <T> T allow(T mock) { return verify(mock, atMost(Integer.MAX_VALUE)); } /** * Allows any invocations of onBlipTimestampChanged on a mock. */ protected static void allowBlipTimestampChanged(ObservableConversation.Listener mock) { allow(mock).onBlipTimestampChanged(any(ObservableConversationBlip.class), anyLong(), anyLong()); } /** * Checks that the set of all reply threads of a blip is the same as the union * of the inline reply and non-inline reply threads. */ private static void assertThreadChildrenConsistent(ConversationBlip blip) { Set<ConversationThread> allChildren = new HashSet<ConversationThread>(); for (ConversationThread thread : blip.getReplyThreads()) { assertFalse(allChildren.contains(thread)); allChildren.add(thread); } for (ConversationThread child : blip.getReplyThreads()) { assertTrue(allChildren.contains(child)); allChildren.remove(child); } // make sure they are exactly equals assertEquals(0, allChildren.size()); } /** * Checks that a conversation is unusable by attempting mutation. */ protected static void assertConversationUnusable(Conversation conversation) { try { conversation.setAnchor(null); fail("Expected conversation to be unusable"); } catch (IllegalStateException expected) { } try { conversation.getRootThread().appendBlip(); fail("Expected conversation items to be unusable"); } catch (IllegalStateException expected) { } } /** * Checks that a conversation is accessible by examining some state. */ protected static void assertConversationAccessible(Conversation conversation) { conversation.getAnchor(); assertThreadAccessible(conversation.getRootThread()); } /** * Asserts that the state-querying methods on a blip can be called. */ protected static void assertBlipAccessible(ConversationBlip blip) { blip.getReplyThreads(); blip.getAuthorId(); blip.getContent(); blip.getContributorIds(); blip.getConversation(); blip.getId(); blip.locateReplyThreads(); blip.getLastModifiedTime(); blip.getLastModifiedVersion(); blip.getReplyThreads(); blip.getThread(); blip.hackGetRaw(); blip.isRoot(); } /** * Asserts that the state-querying methods on a thread can be called. */ protected static void assertThreadAccessible(ConversationThread thread) { thread.getBlips(); thread.getConversation(); thread.getFirstBlip(); thread.getId(); thread.getParentBlip(); } }