/** * Copyright 2010 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.util; import static org.mockito.Matchers.same; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import junit.framework.TestCase; import org.mockito.Matchers; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.waveprotocol.wave.model.document.Doc; import org.waveprotocol.wave.model.document.DocHandler; import org.waveprotocol.wave.model.document.ObservableDocument; import org.waveprotocol.wave.model.document.ObservableMutableDocument; import org.waveprotocol.wave.model.document.indexed.DocumentHandler; import org.waveprotocol.wave.model.document.operation.automaton.DocumentSchema; import org.waveprotocol.wave.model.document.util.DocumentEventRouter; import org.waveprotocol.wave.model.document.util.ListenerRegistration; import org.waveprotocol.wave.model.document.util.Point; import org.waveprotocol.wave.model.document.util.XmlStringBuilder; import org.waveprotocol.wave.model.testing.BasicFactories; import java.util.Collections; import java.util.Map; /** * Generic tests for event routers. * */ public abstract class DocumentEventRouterTestBase extends TestCase { /** * An empty document. */ private ObservableDocument realDoc; /** * A mock spying on the document. */ private ObservableDocument doc; /** * The list of listeners currently registered on the document. */ private final CopyOnWriteSet<Object> docListeners = CopyOnWriteSet.create(); private Doc.E elmOne; private Doc.E elmTwo; @Override protected void setUp() throws Exception { super.setUp(); this.realDoc = createDocument(); elmOne = realDoc.createChildElement(realDoc.getDocumentElement(), "a", noAttribs()); elmTwo = realDoc.createChildElement(realDoc.getDocumentElement(), "b", noAttribs()); this.doc = spy(realDoc); doAnswer(new Answer<Void>() { @SuppressWarnings("unchecked") @Override public Void answer(InvocationOnMock invocation) throws Throwable { DocumentHandler<Doc.N, Doc.E, Doc.T> listener = (DocumentHandler<Doc.N, Doc.E, Doc.T>) invocation.getArguments()[0]; docListeners.add(listener); realDoc.addListener(listener); return null; } }).when(doc).addListener(Mockito.<DocHandler>any()); doAnswer(new Answer<Void>() { @SuppressWarnings("unchecked") @Override public Void answer(InvocationOnMock invocation) throws Throwable { DocumentHandler<Doc.N, Doc.E, Doc.T> listener = (DocumentHandler<Doc.N, Doc.E, Doc.T>) invocation.getArguments()[0]; docListeners.remove(invocation.getArguments()[0]); realDoc.removeListener(listener); return null; } }).when(doc).removeListener(Mockito.<DocHandler>any()); // Mockito messes with how this works on the mock for some reason so we stub // it here to get the right behavior. doAnswer(new Answer<Point<Doc.N>>() { @Override public Point<Doc.N> answer(InvocationOnMock invocation) throws Throwable { return realDoc.locate((Integer) invocation.getArguments()[0]); } }).when(doc).locate(Mockito.anyInt()); } protected abstract DocumentEventRouter<Doc.N, Doc.E, ?> createRouter( ObservableMutableDocument<Doc.N, Doc.E, Doc.T> doc); /** * Create an empty document. Subclasses can override this if they need a * certain kind of document. */ protected ObservableDocument createDocument() { return BasicFactories.createDocument(DocumentSchema.NO_SCHEMA_CONSTRAINTS); } /** * Test that just one listener is registered per router and only when the router * has active listeners. */ @SuppressWarnings("unchecked") public void testAddedOnDemand() { assertEquals(0, docListeners.size()); DocumentEventRouter<Doc.N, Doc.E, ?> router = createRouter(doc); assertEquals(0, docListeners.size()); ListenerRegistration firstReg = router.addAttributeListener(new DummyElm(), mock(AttributeListener.class)); assertEquals(1, docListeners.size()); ListenerRegistration secondReg = router.addAttributeListener(new DummyElm(), mock(AttributeListener.class)); assertEquals(1, docListeners.size()); firstReg.detach(); assertEquals(1, docListeners.size()); secondReg.detach(); assertEquals(0, docListeners.size()); } /** * Test that removing a node removes all associated listeners and, if possible, * unregisters the router from the document. */ @SuppressWarnings("unchecked") public void testRemoveListenersWhenDeleted() { assertEquals(0, docListeners.size()); DocumentEventRouter<Doc.N, Doc.E, ?> router = createRouter(doc); assertEquals(0, docListeners.size()); router.addAttributeListener(elmOne, mock(AttributeListener.class)); router.addAttributeListener(elmOne, mock(AttributeListener.class)); router.addDeletionListener(elmOne, mock(DeletionListener.class)); router.addDeletionListener(elmOne, mock(DeletionListener.class)); router.addChildListener(elmOne, mock(ElementListener.class)); router.addChildListener(elmOne, mock(ElementListener.class)); assertEquals(1, docListeners.size()); realDoc.deleteNode(elmOne); assertEquals(0, docListeners.size()); } /** * Test that removing yourself during event handling works as expected. */ public void testSelfRemoval() { class SelfRemovalListener implements ElementListener<Doc.E> { private int removeCallCount = 0; private ListenerRegistration reg; @Override public void onElementAdded(Doc.E element) { fail(); } @Override public void onElementRemoved(Doc.E element) { removeCallCount++; if (removeCallCount == 1) { reg.detach(); } } } Doc.E parent = elmOne; Doc.E childOne = realDoc.createChildElement(parent, "a", noAttribs()); Doc.E childTwo = realDoc.createChildElement(parent, "b", noAttribs()); DocumentEventRouter<Doc.N, Doc.E, ?> router = createRouter(doc); SelfRemovalListener util = new SelfRemovalListener(); util.reg = router.addChildListener(parent, util); assertEquals(0, util.removeCallCount); realDoc.deleteNode(childTwo); assertEquals(1, util.removeCallCount); realDoc.deleteNode(childOne); assertEquals(1, util.removeCallCount); } /** * Test that attribute and deletion listeners only receive events for the elements * they're listening to. */ @SuppressWarnings("unchecked") public void testFiltering() { DocumentEventRouter<Doc.N, Doc.E, ?> router = createRouter(doc); AttributeListener<Doc.E> attribListenerOne = mock(AttributeListener.class); router.addAttributeListener(elmOne, attribListenerOne); AttributeListener<Doc.E> attribListenerTwo = mock(AttributeListener.class); router.addAttributeListener(elmTwo, attribListenerTwo); realDoc.setElementAttribute(elmOne, "t", "1"); verify(attribListenerOne).onAttributesChanged(same(elmOne), anyAttribs(), anyAttribs()); verify(attribListenerTwo, never()).onAttributesChanged(anyElement(), anyAttribs(), anyAttribs()); realDoc.setElementAttribute(elmTwo, "s", "2"); Mockito.verify(attribListenerOne).onAttributesChanged(same(elmOne), anyAttribs(), anyAttribs()); Mockito.verify(attribListenerTwo).onAttributesChanged(same(elmTwo), anyAttribs(), anyAttribs()); DeletionListener deleteListenerOne = mock(DeletionListener.class); router.addDeletionListener(elmOne, deleteListenerOne); DeletionListener deleteListenerTwo = mock(DeletionListener.class); router.addDeletionListener(elmTwo, deleteListenerTwo); realDoc.deleteNode(elmTwo); verify(deleteListenerOne, never()).onDeleted(); verify(deleteListenerTwo).onDeleted(); realDoc.deleteNode(elmOne); verify(deleteListenerOne).onDeleted(); verify(deleteListenerTwo).onDeleted(); verify(attribListenerOne).onAttributesChanged(same(elmOne), anyAttribs(), anyAttribs()); verify(attribListenerTwo).onAttributesChanged(same(elmTwo), anyAttribs(), anyAttribs()); verifyNoMoreInteractions(deleteListenerOne); verifyNoMoreInteractions(deleteListenerTwo); verifyNoMoreInteractions(attribListenerOne); verifyNoMoreInteractions(attribListenerTwo); } /** * Test that child listeners are called as appropriate. */ @SuppressWarnings("unchecked") public void testChildListeners() { DocumentEventRouter<Doc.N, Doc.E, ?> router = createRouter(doc); ElementListener<Doc.E> elmListenerOne = mock(ElementListener.class); ElementListener<Doc.E> elmListenerTwo = mock(ElementListener.class); router.addChildListener(elmOne, elmListenerOne); router.addChildListener(elmTwo, elmListenerTwo); Doc.E childOne = realDoc.createChildElement(elmOne, "x", noAttribs()); verify(elmListenerOne).onElementAdded(childOne); verify(elmListenerTwo, never()).onElementAdded(anyElement()); Doc.E childTwo = realDoc.createChildElement(elmTwo, "y", noAttribs()); verify(elmListenerOne).onElementAdded(childOne); verify(elmListenerTwo).onElementAdded(childTwo); realDoc.deleteNode(childTwo); verify(elmListenerOne, never()).onElementRemoved(anyElement()); verify(elmListenerTwo).onElementRemoved(childTwo); realDoc.deleteNode(childOne); verify(elmListenerOne).onElementRemoved(childOne); verify(elmListenerTwo).onElementRemoved(childTwo); verifyNoMoreInteractions(elmListenerOne); verifyNoMoreInteractions(elmListenerTwo); } /** * Test that only the root of a deletion is given child notifications, not * children of parents that are themselves being deleted. */ @SuppressWarnings("unchecked") public void testRecursiveDeletion() { Doc.E parent = elmOne; final Doc.E child = realDoc.createChildElement(parent, "c", noAttribs()); Doc.E grandChild = realDoc.createChildElement(child, "d", noAttribs()); DocumentEventRouter<Doc.N, Doc.E, ?> router = createRouter(doc); ElementListener<Doc.E> childRemovedListener = mock(ElementListener.class); router.addChildListener(parent, childRemovedListener); ElementListener<Doc.E> grandChildRemovedListener = mock(ElementListener.class); router.addChildListener(child, grandChildRemovedListener); realDoc.deleteNode(child); verify(childRemovedListener).onElementRemoved(same(child)); verifyNoMoreInteractions(childRemovedListener); verifyZeroInteractions(grandChildRemovedListener); } /** * Test that only the root of an insertion is given child notifications. */ @SuppressWarnings("unchecked") // Generic mocks. public void testRecursiveInsertion() { final Doc.E parent = elmOne; final DocumentEventRouter<Doc.N, Doc.E, ?> router = createRouter(doc); final ElementListener<Doc.E> childListener = mock(ElementListener.class); ElementListener<Doc.E> parentListener = mock(ElementListener.class); final ElementListener<Doc.E> secondParentListener = mock(ElementListener.class); doAnswer(new Answer<Void>() { public Void answer(InvocationOnMock invocation) throws Throwable { Doc.E child = (Doc.E) invocation.getArguments()[0]; router.addChildListener(child, childListener); router.addChildListener(parent, secondParentListener); return null; } }).when(parentListener).onElementAdded(anyElement()); router.addChildListener(parent, parentListener); Doc.E child = doc.insertXml(Point.start(doc, parent), XmlStringBuilder.createEmpty().wrap("a").wrap("b")); verify(parentListener).onElementAdded(child); verifyNoMoreInteractions(parentListener); verifyZeroInteractions(childListener); verifyZeroInteractions(secondParentListener); } private static Doc.E anyElement() { return Matchers.<Doc.E>any(); } private static Map<String, String> anyAttribs() { return Matchers.<Map<String, String>>any(); } private static Map<String, String> noAttribs() { return Collections.<String, String>emptyMap(); } private static class DummyElm implements Doc.E { } }