http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/src/org/waveprotocol/wave/model/testing/WaveletDataFactory.java
----------------------------------------------------------------------
diff --git a/src/org/waveprotocol/wave/model/testing/WaveletDataFactory.java 
b/src/org/waveprotocol/wave/model/testing/WaveletDataFactory.java
deleted file mode 100644
index bf8ff93..0000000
--- a/src/org/waveprotocol/wave/model/testing/WaveletDataFactory.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.testing;
-
-
-import org.waveprotocol.wave.model.id.IdGenerator;
-import org.waveprotocol.wave.model.id.WaveId;
-import org.waveprotocol.wave.model.id.WaveletId;
-import org.waveprotocol.wave.model.version.HashedVersion;
-import org.waveprotocol.wave.model.wave.ParticipantId;
-import org.waveprotocol.wave.model.wave.data.ObservableWaveletData;
-import org.waveprotocol.wave.model.wave.data.WaveletData;
-import org.waveprotocol.wave.model.wave.data.impl.EmptyWaveletSnapshot;
-
-/**
- * Exposes any {@link ObservableWaveletData.Factory} as a {@link Factory}, by
- * injecting suitable dependencies for testing.
- *
- */
-public final class WaveletDataFactory<T extends WaveletData> implements 
Factory<T> {
-  private final static WaveId WAVE_ID;
-  private final static WaveletId WAVELET_ID;
-  private static final ParticipantId PARTICIPANT_ID = new 
ParticipantId("[email protected]");
-
-  static {
-    IdGenerator gen = FakeIdGenerator.create();
-    WAVE_ID = gen.newWaveId();
-    WAVELET_ID = gen.newConversationWaveletId();
-  }
-
-  private final WaveletData.Factory<T> factory;
-
-  private WaveletDataFactory(WaveletData.Factory<T> factory) {
-    this.factory = factory;
-  }
-
-  public static <T extends WaveletData> Factory<T> of(WaveletData.Factory<T> 
factory) {
-    return new WaveletDataFactory<T>(factory);
-  }
-
-  @Override
-  public T create() {
-    return factory.create(new EmptyWaveletSnapshot(WAVE_ID, WAVELET_ID, 
PARTICIPANT_ID,
-        HashedVersion.unsigned(0), 0));
-  }
-}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/box/server/robots/operations/BlipOperationServicesTest.java
----------------------------------------------------------------------
diff --git 
a/test/org/waveprotocol/box/server/robots/operations/BlipOperationServicesTest.java
 
b/test/org/waveprotocol/box/server/robots/operations/BlipOperationServicesTest.java
index 591fa83..dfddfb7 100644
--- 
a/test/org/waveprotocol/box/server/robots/operations/BlipOperationServicesTest.java
+++ 
b/test/org/waveprotocol/box/server/robots/operations/BlipOperationServicesTest.java
@@ -33,7 +33,7 @@ import com.google.wave.api.data.ApiView;
 import org.waveprotocol.box.server.robots.OperationContext;
 import org.waveprotocol.box.server.robots.OperationContextImpl;
 import org.waveprotocol.box.server.robots.RobotsTestBase;
-import org.waveprotocol.box.server.robots.testing.OperationServiceHelper;
+import 
org.waveprotocol.box.server.robots.operations.testing.OperationServiceHelper;
 import org.waveprotocol.box.server.robots.util.ConversationUtil;
 import org.waveprotocol.wave.model.conversation.ConversationBlip;
 import org.waveprotocol.wave.model.conversation.ObservableConversation;

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/box/server/robots/operations/DocumentModifyServiceTest.java
----------------------------------------------------------------------
diff --git 
a/test/org/waveprotocol/box/server/robots/operations/DocumentModifyServiceTest.java
 
b/test/org/waveprotocol/box/server/robots/operations/DocumentModifyServiceTest.java
index 3ba955b..68651c1 100644
--- 
a/test/org/waveprotocol/box/server/robots/operations/DocumentModifyServiceTest.java
+++ 
b/test/org/waveprotocol/box/server/robots/operations/DocumentModifyServiceTest.java
@@ -39,7 +39,7 @@ import 
com.google.wave.api.impl.DocumentModifyAction.ModifyHow;
 import com.google.wave.api.impl.DocumentModifyQuery;
 
 import org.waveprotocol.box.server.robots.RobotsTestBase;
-import org.waveprotocol.box.server.robots.testing.OperationServiceHelper;
+import 
org.waveprotocol.box.server.robots.operations.testing.OperationServiceHelper;
 import org.waveprotocol.wave.model.conversation.ObservableConversationBlip;
 import org.waveprotocol.wave.model.document.Document;
 import org.waveprotocol.wave.model.document.util.LineContainers;

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/box/server/robots/operations/FetchWaveServiceTest.java
----------------------------------------------------------------------
diff --git 
a/test/org/waveprotocol/box/server/robots/operations/FetchWaveServiceTest.java 
b/test/org/waveprotocol/box/server/robots/operations/FetchWaveServiceTest.java
index 7779bcc..fe6a587 100644
--- 
a/test/org/waveprotocol/box/server/robots/operations/FetchWaveServiceTest.java
+++ 
b/test/org/waveprotocol/box/server/robots/operations/FetchWaveServiceTest.java
@@ -30,7 +30,7 @@ import com.google.wave.api.OperationType;
 
 import org.waveprotocol.box.server.robots.OperationContextImpl;
 import org.waveprotocol.box.server.robots.RobotsTestBase;
-import org.waveprotocol.box.server.robots.testing.OperationServiceHelper;
+import 
org.waveprotocol.box.server.robots.operations.testing.OperationServiceHelper;
 import org.waveprotocol.box.server.waveserver.WaveletProvider;
 import org.waveprotocol.wave.model.conversation.ObservableConversation;
 

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/box/server/robots/operations/ParticipantServicesTest.java
----------------------------------------------------------------------
diff --git 
a/test/org/waveprotocol/box/server/robots/operations/ParticipantServicesTest.java
 
b/test/org/waveprotocol/box/server/robots/operations/ParticipantServicesTest.java
index 582ae54..2595808 100644
--- 
a/test/org/waveprotocol/box/server/robots/operations/ParticipantServicesTest.java
+++ 
b/test/org/waveprotocol/box/server/robots/operations/ParticipantServicesTest.java
@@ -30,7 +30,7 @@ import com.google.wave.api.OperationRequest.Parameter;
 import org.waveprotocol.box.server.robots.RobotsTestBase;
 import org.waveprotocol.box.server.robots.OperationContext;
 import org.waveprotocol.box.server.robots.OperationContextImpl;
-import org.waveprotocol.box.server.robots.testing.OperationServiceHelper;
+import 
org.waveprotocol.box.server.robots.operations.testing.OperationServiceHelper;
 import org.waveprotocol.box.server.robots.util.ConversationUtil;
 import org.waveprotocol.wave.model.conversation.ObservableConversation;
 import org.waveprotocol.wave.model.wave.ParticipantId;

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/box/server/robots/operations/WaveletSetTitleServiceTest.java
----------------------------------------------------------------------
diff --git 
a/test/org/waveprotocol/box/server/robots/operations/WaveletSetTitleServiceTest.java
 
b/test/org/waveprotocol/box/server/robots/operations/WaveletSetTitleServiceTest.java
index d995638..bdc12b0 100644
--- 
a/test/org/waveprotocol/box/server/robots/operations/WaveletSetTitleServiceTest.java
+++ 
b/test/org/waveprotocol/box/server/robots/operations/WaveletSetTitleServiceTest.java
@@ -26,7 +26,7 @@ import com.google.wave.api.OperationRequest.Parameter;
 import com.google.wave.api.OperationType;
 
 import org.waveprotocol.box.server.robots.RobotsTestBase;
-import org.waveprotocol.box.server.robots.testing.OperationServiceHelper;
+import 
org.waveprotocol.box.server.robots.operations.testing.OperationServiceHelper;
 import org.waveprotocol.wave.model.conversation.ObservableConversationBlip;
 import org.waveprotocol.wave.model.conversation.TitleHelper;
 import org.waveprotocol.wave.model.document.util.LineContainers;

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/box/server/robots/operations/testing/OperationServiceHelper.java
----------------------------------------------------------------------
diff --git 
a/test/org/waveprotocol/box/server/robots/operations/testing/OperationServiceHelper.java
 
b/test/org/waveprotocol/box/server/robots/operations/testing/OperationServiceHelper.java
new file mode 100644
index 0000000..a5708a4
--- /dev/null
+++ 
b/test/org/waveprotocol/box/server/robots/operations/testing/OperationServiceHelper.java
@@ -0,0 +1,120 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.box.server.robots.operations.testing;
+
+import static org.mockito.Mockito.mock;
+
+import com.google.wave.api.data.converter.EventDataConverter;
+import com.google.wave.api.data.converter.v22.EventDataConverterV22;
+
+import org.waveprotocol.box.server.robots.OperationContextImpl;
+import org.waveprotocol.box.server.robots.RobotWaveletData;
+import org.waveprotocol.box.server.robots.operations.OperationService;
+import org.waveprotocol.box.server.robots.util.ConversationUtil;
+import org.waveprotocol.box.server.waveserver.WaveletProvider;
+import org.waveprotocol.wave.model.conversation.ObservableConversation;
+import org.waveprotocol.wave.model.conversation.WaveletBasedConversation;
+import org.waveprotocol.wave.model.id.IdURIEncoderDecoder;
+import org.waveprotocol.wave.model.id.WaveletName;
+import org.waveprotocol.wave.model.operation.SilentOperationSink;
+import 
org.waveprotocol.wave.model.operation.wave.BasicWaveletOperationContextFactory;
+import org.waveprotocol.wave.model.operation.wave.WaveletOperation;
+import org.waveprotocol.wave.model.testing.BasicFactories;
+import org.waveprotocol.wave.model.testing.FakeIdGenerator;
+import org.waveprotocol.wave.model.version.HashedVersionFactory;
+import org.waveprotocol.wave.model.version.HashedVersionFactoryImpl;
+import org.waveprotocol.wave.model.wave.ParticipantId;
+import org.waveprotocol.wave.model.wave.ParticipationHelper;
+import org.waveprotocol.wave.model.wave.data.DocumentFactory;
+import org.waveprotocol.wave.model.wave.data.DocumentOperationSink;
+import org.waveprotocol.wave.model.wave.data.ObservableWaveletData;
+import org.waveprotocol.wave.model.wave.data.WaveletData;
+import org.waveprotocol.wave.model.wave.data.impl.EmptyWaveletSnapshot;
+import org.waveprotocol.wave.model.wave.data.impl.WaveletDataImpl;
+import org.waveprotocol.wave.model.wave.opbased.OpBasedWavelet;
+import org.waveprotocol.wave.util.escapers.jvm.JavaUrlCodec;
+
+/**
+ * Helper for testing {@link OperationService}. Puts a single empty
+ * conversational wavelet with one participant in the operation context.
+ *
+ * @author [email protected] (Lennard de Rijk)
+ */
+public class OperationServiceHelper {
+
+  private static final IdURIEncoderDecoder URI_CODEC =
+      new IdURIEncoderDecoder(new JavaUrlCodec());
+  private static final HashedVersionFactory HASH_FACTORY = new 
HashedVersionFactoryImpl(URI_CODEC);
+  private static final DocumentFactory<? extends DocumentOperationSink> 
DOCUMENT_FACTORY =
+      BasicFactories.observablePluggableMutableDocumentFactory();
+
+  private final WaveletProvider waveletProvider;
+  private final OperationContextImpl context;
+
+  /**
+   * Constructs a new {@link OperationServiceHelper} with a wavelet with the
+   * name and participant that are passed in.
+   *
+   * @param waveletName the name of the empty wavelet to open in the context.
+   * @param participant the participant that should be on that empty wavelet.
+   */
+  public OperationServiceHelper(WaveletName waveletName, ParticipantId 
participant) {
+    waveletProvider = mock(WaveletProvider.class);
+    EventDataConverter converter = new EventDataConverterV22();
+
+    ObservableWaveletData waveletData = 
WaveletDataImpl.Factory.create(DOCUMENT_FACTORY).create(
+        new EmptyWaveletSnapshot(waveletName.waveId, waveletName.waveletId, 
participant,
+            HASH_FACTORY.createVersionZero(waveletName), 0L));
+    waveletData.addParticipant(participant);
+
+    BasicWaveletOperationContextFactory CONTEXT_FACTORY =
+        new BasicWaveletOperationContextFactory(participant);
+
+    SilentOperationSink<WaveletOperation> executor =
+        SilentOperationSink.Executor.<WaveletOperation, 
WaveletData>build(waveletData);
+    OpBasedWavelet wavelet =
+        new OpBasedWavelet(waveletData.getWaveId(), waveletData, 
CONTEXT_FACTORY,
+            ParticipationHelper.DEFAULT, executor, SilentOperationSink.VOID);
+
+    // Make a conversation with an empty root blip
+    WaveletBasedConversation.makeWaveletConversational(wavelet);
+    ConversationUtil conversationUtil = new 
ConversationUtil(FakeIdGenerator.create());
+    ObservableConversation conversation = 
conversationUtil.buildConversation(wavelet).getRoot();
+    conversation.getRootThread().appendBlip();
+
+    context = new OperationContextImpl(waveletProvider, converter, 
conversationUtil);
+    context.putWavelet(waveletName.waveId, waveletName.waveletId,
+        new RobotWaveletData(waveletData, 
HASH_FACTORY.createVersionZero(waveletName)));
+  }
+
+  /**
+   * @return the {@link WaveletProvider} mock
+   */
+  public WaveletProvider getWaveletProvider() {
+    return waveletProvider;
+  }
+
+  /**
+   * @return the {@link OperationContextImpl} with the empty wavelet opened.
+   */
+  public OperationContextImpl getContext() {
+    return context;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/box/server/rpc/testing/FakeServerRpcController.java
----------------------------------------------------------------------
diff --git 
a/test/org/waveprotocol/box/server/rpc/testing/FakeServerRpcController.java 
b/test/org/waveprotocol/box/server/rpc/testing/FakeServerRpcController.java
new file mode 100644
index 0000000..fa45cd6
--- /dev/null
+++ b/test/org/waveprotocol/box/server/rpc/testing/FakeServerRpcController.java
@@ -0,0 +1,84 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.box.server.rpc.testing;
+
+import static org.waveprotocol.box.server.util.testing.TestingConstants.USER;
+
+import com.google.protobuf.RpcCallback;
+
+import org.waveprotocol.box.server.rpc.ServerRpcController;
+import org.waveprotocol.wave.model.wave.ParticipantId;
+
+
+/**
+  * An {@code RpcController} that just handles error text and failure 
condition.
+  */
+public class FakeServerRpcController implements ServerRpcController {
+  private boolean failed = false;
+  private String errorText = null;
+
+  @Override
+  public String errorText() {
+    return errorText;
+  }
+
+  @Override
+  public boolean failed() {
+    return failed;
+  }
+
+  @Override
+  public boolean isCanceled() {
+    return false;
+  }
+
+  @Override
+  public void notifyOnCancel(RpcCallback<Object> arg) {
+  }
+
+  @Override
+  public void reset() {
+    failed = false;
+    errorText = null;
+  }
+
+  @Override
+  public void setFailed(String error) {
+    failed = true;
+    errorText = error;
+  }
+
+  @Override
+  public void startCancel() {
+  }
+
+  @Override
+  public ParticipantId getLoggedInUser() {
+    return ParticipantId.ofUnsafe(USER);
+  }
+
+  @Override
+  public void cancel() {
+  }
+
+  @Override
+  public void run() {
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/box/server/util/testing/ExceptionLogHandler.java
----------------------------------------------------------------------
diff --git 
a/test/org/waveprotocol/box/server/util/testing/ExceptionLogHandler.java 
b/test/org/waveprotocol/box/server/util/testing/ExceptionLogHandler.java
new file mode 100644
index 0000000..7ea3550
--- /dev/null
+++ b/test/org/waveprotocol/box/server/util/testing/ExceptionLogHandler.java
@@ -0,0 +1,65 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.box.server.util.testing;
+
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+
+/**
+ * A log handler which throws a runtime exception at and above a set log level.
+ * Useful for catching erroneous conditions during testing, which are only
+ * reported to the logs.
+ *
+ * @author [email protected] (Michael Kuntzman)
+ */
+public class ExceptionLogHandler extends Handler {
+  /** The integer value of the minimum fatal log level. */
+  private final int fatalLevel;
+
+  /**
+   * Constructs an ExceptionLogHandler that throws a runtime exception at and 
above the specified
+   * log level.
+   *
+   * @param fatalLevel the minimum log level for which to throw an exception.
+   */
+  public ExceptionLogHandler(Level fatalLevel) {
+    this.fatalLevel = fatalLevel.intValue();
+  }
+
+  /**
+   * @throws RuntimeException if the log record is at or above the fatal log 
level.
+   */
+  @Override
+  public void publish(LogRecord record) {
+    if (record.getLevel().intValue() >= fatalLevel) {
+      throw new RuntimeException(record.getLevel() + ": " + 
record.getMessage(),
+          record.getThrown());
+    }
+  }
+
+  @Override
+  public void flush() {
+  }
+
+  @Override
+  public void close() {
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/box/server/util/testing/Matchers.java
----------------------------------------------------------------------
diff --git a/test/org/waveprotocol/box/server/util/testing/Matchers.java 
b/test/org/waveprotocol/box/server/util/testing/Matchers.java
new file mode 100644
index 0000000..72be27a
--- /dev/null
+++ b/test/org/waveprotocol/box/server/util/testing/Matchers.java
@@ -0,0 +1,172 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.box.server.util.testing;
+
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+
+import java.util.Collection;
+
+/**
+ * Additional matchers to go with JUnit4's assertThat and assumeThat.
+ *
+ * @author [email protected] (Michael Kuntzman)
+ */
+// TODO(Michael): Maybe move this class to the libraries repository/branch.
+public class Matchers {
+  /**
+   * Nicer aliases for some of the methods in this class, which may conflict 
with methods in other
+   * packages (potential conficts noted for each alias).
+   */
+  public static class Aliases {
+    /**
+     * Alias for "containsString". May conflict with 
"org.mockito.Mockito.contains".
+     *
+     * @param substring to look for.
+     * @return a matcher for checking that a string contains the specified 
substring.
+     */
+    public static TypeSafeMatcher<String> contains(final String substring) {
+      return containsString(substring);
+    }
+
+    /**
+     * Alias for "matchesRegex". May conflict with 
"org.mockito.Mockito.matches".
+     *
+     * @param regularExpression to match against.
+     * @return a matcher for checking that a string matches the specified 
regular expression.
+     */
+    public static TypeSafeMatcher<String> matches(final String 
regularExpression) {
+      return matchesRegex(regularExpression);
+    }
+  }
+
+  /**
+   * A more user-friendly version of 
org.junit.matchers.JUnitMatchers.hasItem(T element). Allows a
+   * more verbose failure than assertTrue(collection.contains(item)). The 
matcher produces
+   * "Expected: a collection containing '...' got: '...'", whereas assertTrue 
produces merely
+   * "AssertionFailedError".
+   * Usage: static import, then assertThat(collection, contains(item)).
+   *
+   * @param item to look for.
+   * @return a matcher for checking that a collection contains the specified 
item.
+   */
+  public static <T> TypeSafeMatcher<Collection<? super T>> contains(final T 
item) {
+    return new TypeSafeMatcher<Collection<? super T>>() {
+          @Override
+          public boolean matchesSafely(Collection<? super T> collection) {
+            return collection.contains(item);
+          }
+
+          @Override
+          public void describeTo(Description description) {
+            description.appendText("a collection containing 
").appendValue(item);
+          }
+        };
+  }
+
+  /**
+   * Same as JUnitMatchers.containsString. Allows a more verbose failure than
+   * assertTrue(str.contains(substring)).
+   * Usage: static import, then assertThat(str, containsString(substring)).
+   *
+   * @param substring to look for.
+   * @return a matcher for checking that a string contains the specified 
substring.
+   */
+  public static TypeSafeMatcher<String> containsString(final String substring) 
{
+    return new TypeSafeMatcher<String>() {
+          @Override
+          public boolean matchesSafely(String str) {
+            return str.contains(substring);
+          }
+
+          @Override
+          public void describeTo(Description description) {
+            description.appendText("a string containing 
").appendValue(substring);
+          }
+        };
+  }
+
+  /**
+   * The negative version of "contains" for a collection. Allows a more 
verbose failure than
+   * assertFalse(collection.contains(item)).
+   * Usage: static import, then assertThat(collection, doesNotContain(item)).
+   *
+   * @param item to look for.
+   * @return a matcher for checking that a collection does not contain the 
specified item.
+   */
+  public static <T> TypeSafeMatcher<Collection<? super T>> 
doesNotContain(final T item) {
+    return new TypeSafeMatcher<Collection<? super T>>() {
+          @Override
+          public boolean matchesSafely(Collection<? super T> collection) {
+            return !collection.contains(item);
+          }
+
+          @Override
+          public void describeTo(Description description) {
+            description.appendText("a collection NOT containing 
").appendValue(item);
+          }
+        };
+  }
+
+  /**
+   * The negative version of "contains" for a string (or "containsString"). 
Allows a more verbose
+   * failure than assertFalse(str.contains(substring)).
+   * Usage: static import, then assertThat(str, doesNotContain(substring)).
+   *
+   * @param substring to look for.
+   * @return a matcher for checking that a string contains the specified 
substring.
+   */
+  public static TypeSafeMatcher<String> doesNotContain(final String substring) 
{
+    return new TypeSafeMatcher<String>() {
+          @Override
+          public boolean matchesSafely(String str) {
+            return !str.contains(substring);
+          }
+
+          @Override
+          public void describeTo(Description description) {
+            description.appendText("a string NOT containing 
").appendValue(substring);
+          }
+        };
+  }
+
+  /**
+   * Allows a more verbose failure than assertTrue(str.matches(regex)). The 
matcher produces
+   * "Expected: a string matching regex '...' got: '...'", whereas assertTrue 
produces merely
+   * "AssertionFailedError".
+   * Usage: static import, then assertThat(str, matchesRegex(regex)).
+   *
+   * @param regularExpression to match against.
+   * @return a matcher for checking that a string matches the specified 
regular expression.
+   */
+  public static TypeSafeMatcher<String> matchesRegex(final String 
regularExpression) {
+    return new TypeSafeMatcher<String>() {
+          @Override
+          public boolean matchesSafely(String str) {
+            return str.matches(regularExpression);
+          }
+
+          @Override
+          public void describeTo(Description description) {
+            description.appendText("a string matching regex 
").appendValue(regularExpression);
+          }
+        };
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/box/server/util/testing/TestingConstants.java
----------------------------------------------------------------------
diff --git 
a/test/org/waveprotocol/box/server/util/testing/TestingConstants.java 
b/test/org/waveprotocol/box/server/util/testing/TestingConstants.java
new file mode 100644
index 0000000..ed0d95f
--- /dev/null
+++ b/test/org/waveprotocol/box/server/util/testing/TestingConstants.java
@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.box.server.util.testing;
+
+import org.waveprotocol.wave.model.id.WaveId;
+import org.waveprotocol.wave.model.id.WaveletId;
+import org.waveprotocol.wave.model.id.WaveletName;
+import org.waveprotocol.wave.model.wave.ParticipantId;
+
+/**
+ * Commonly used constants for unit testing. Some constants taken from
+ * previously existing test cases.
+ *
+ * @author [email protected] (Michael Kuntzman)
+ */
+// TODO(Michael): Maybe move this class to the libraries repository/branch.
+public interface TestingConstants {
+  public static final String BLIP_ID = "b+blip";
+
+  public static final String MESSAGE = "The quick brown fox jumps over the 
lazy dog";
+
+  public static final String MESSAGE2 = "Why's the rum gone?";
+
+  public static final String MESSAGE3 = "There is no spoon";
+
+  public static final String DOMAIN = "host.com";
+
+  public static final String OTHER_USER_NAME = "other";
+
+  public static final String OTHER_USER = OTHER_USER_NAME + "@" + DOMAIN;
+
+  public static final ParticipantId OTHER_PARTICIPANT = new 
ParticipantId(OTHER_USER);
+
+  public static final int PORT = 9876;
+
+  public static final String USER_NAME = "user";
+
+  public static final String USER = USER_NAME + "@" + DOMAIN;
+
+  public static final char[] PASSWORD = "password".toCharArray();
+
+  public static final ParticipantId PARTICIPANT = new ParticipantId(USER);
+
+  public static final WaveId WAVE_ID = WaveId.of(DOMAIN, "w+wave");
+
+  public static final WaveletId WAVELET_ID = WaveletId.of(DOMAIN, "wavelet");
+
+  public static final WaveletName WAVELET_NAME = WaveletName.of(WAVE_ID, 
WAVELET_ID);
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/wave/client/editor/testing/ContentSerialisationUtil.java
----------------------------------------------------------------------
diff --git 
a/test/org/waveprotocol/wave/client/editor/testing/ContentSerialisationUtil.java
 
b/test/org/waveprotocol/wave/client/editor/testing/ContentSerialisationUtil.java
new file mode 100644
index 0000000..81d88ce
--- /dev/null
+++ 
b/test/org/waveprotocol/wave/client/editor/testing/ContentSerialisationUtil.java
@@ -0,0 +1,58 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.client.editor.testing;
+
+import org.waveprotocol.wave.client.editor.Editor;
+import org.waveprotocol.wave.model.schema.conversation.ConversationSchemas;
+
+import org.waveprotocol.wave.model.document.util.DocProviders;
+import org.waveprotocol.wave.model.document.util.XmlStringBuilder;
+
+/**
+ * Utility that decorates editors by adding the ability to get/set content 
using types
+ *   other than DocInitialisation ops.
+ *
+ * TODO(patcoleman): figure out which class(/es) should be natively supported 
inside Editor
+ * the interface, and which should be decorated within this class.
+ *
+ * @author [email protected] (Pat Coleman)
+ */
+public class ContentSerialisationUtil {
+  /** Static utility, hence private constructor. */
+  private ContentSerialisationUtil() {}
+
+  /// String support
+
+  /** Gets the editor's persistent document as a String. */
+  public static String getContentString(Editor editor) {
+    return 
XmlStringBuilder.innerXml(editor.getPersistentDocument()).toString();
+  }
+
+  /**
+   * Sets the editor's content to a given string - the accepted format 
currently is XML,
+   *   note to be careful that attribute values are surrounded by ", not '.
+   */
+  public static void setContentString(Editor editor, String content) {
+    editor.setContent(DocProviders.POJO.parse(content).asOperation(),
+        ConversationSchemas.BLIP_SCHEMA_CONSTRAINTS);
+  }
+
+  /// Other classes to come...
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/wave/client/editor/testing/DocumentFreeSelectionHelper.java
----------------------------------------------------------------------
diff --git 
a/test/org/waveprotocol/wave/client/editor/testing/DocumentFreeSelectionHelper.java
 
b/test/org/waveprotocol/wave/client/editor/testing/DocumentFreeSelectionHelper.java
new file mode 100644
index 0000000..c540ca0
--- /dev/null
+++ 
b/test/org/waveprotocol/wave/client/editor/testing/DocumentFreeSelectionHelper.java
@@ -0,0 +1,106 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.client.editor.testing;
+
+import org.waveprotocol.wave.client.editor.content.ContentNode;
+import org.waveprotocol.wave.client.editor.content.ContentRange;
+import org.waveprotocol.wave.client.editor.content.FocusedContentRange;
+import org.waveprotocol.wave.client.editor.selection.content.SelectionHelper;
+import org.waveprotocol.wave.model.document.util.FocusedRange;
+import org.waveprotocol.wave.model.document.util.Point;
+import org.waveprotocol.wave.model.document.util.Range;
+
+/**
+ * A simple selection helper implementation with no knowledge of a content tree
+ *
+ * @author [email protected] (Pat Coleman)
+ */
+public class DocumentFreeSelectionHelper implements SelectionHelper {
+  /** Track the simple range directly. */
+  FocusedRange selection = null;
+
+  public DocumentFreeSelectionHelper(int start, int end) {
+    selection = new FocusedRange(start, end);
+  }
+
+  public DocumentFreeSelectionHelper(FocusedRange range) {
+    selection = range;
+  }
+
+  @Override
+  public void clearSelection() {
+    selection = null;
+  }
+
+  @Override
+  public FocusedRange getSelectionRange() {
+    return selection;
+  }
+
+  @Override
+  public Range getOrderedSelectionRange() {
+    return selection != null ? selection.asRange() : null;
+  }
+
+  @Override
+  public void setSelectionRange(FocusedRange selection) {
+    this.selection = selection;
+  }
+
+  @Override
+  public Point<ContentNode> getFirstValidSelectionPoint() {
+    throw new UnsupportedOperationException("DocumentFree SelectionHelper has 
no document content");
+  }
+
+  @Override
+  public Point<ContentNode> getLastValidSelectionPoint() {
+    throw new UnsupportedOperationException("DocumentFree SelectionHelper has 
no document content");
+  }
+
+  @Override
+  public FocusedContentRange getSelectionPoints() {
+    throw new UnsupportedOperationException("DocumentFree SelectionHelper has 
no document content");
+  }
+
+  @Override
+  public ContentRange getOrderedSelectionPoints() {
+    throw new UnsupportedOperationException("DocumentFree SelectionHelper has 
no document content");
+  }
+
+  @Override
+  public boolean isValidSelectionPoint(Point<ContentNode> cp) {
+    throw new UnsupportedOperationException("DocumentFree SelectionHelper has 
no document content");
+  }
+
+  @Override
+  public void setCaret(Point<ContentNode> caret) {
+    throw new UnsupportedOperationException("DocumentFree SelectionHelper has 
no document content");
+  }
+
+  @Override
+  public void setSelectionPoints(Point<ContentNode> start, Point<ContentNode> 
end) {
+    throw new UnsupportedOperationException("DocumentFree SelectionHelper has 
no document content");
+  }
+
+  @Override
+  public void setCaret(int caret) {
+    throw new UnsupportedOperationException("DocumentFree SelectionHelper has 
no document content");
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/wave/client/editor/testing/FakeEditorContext.java
----------------------------------------------------------------------
diff --git 
a/test/org/waveprotocol/wave/client/editor/testing/FakeEditorContext.java 
b/test/org/waveprotocol/wave/client/editor/testing/FakeEditorContext.java
new file mode 100644
index 0000000..d1dc06c
--- /dev/null
+++ b/test/org/waveprotocol/wave/client/editor/testing/FakeEditorContext.java
@@ -0,0 +1,105 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.client.editor.testing;
+
+import org.waveprotocol.wave.client.editor.EditorContext;
+import 
org.waveprotocol.wave.client.editor.EditorUpdateEvent.EditorUpdateListener;
+import org.waveprotocol.wave.client.editor.Responsibility;
+import org.waveprotocol.wave.client.editor.ResponsibilityManagerImpl;
+import org.waveprotocol.wave.client.editor.content.CMutableDocument;
+import org.waveprotocol.wave.client.editor.content.misc.CaretAnnotations;
+import org.waveprotocol.wave.client.editor.selection.content.SelectionHelper;
+
+/**
+ * Representing a (possibly-POJO) implementation of the editor context 
interface,
+ * by using members supplied on construction.
+ *
+ * @author [email protected] (Pat Coleman)
+ */
+public class FakeEditorContext implements EditorContext {
+  private final CMutableDocument document;
+  private final CaretAnnotations caretAnnotations;
+  private final String imeCompositionState;
+  private final SelectionHelper selectionHelper;
+  private final Responsibility.Manager responsibility = new 
ResponsibilityManagerImpl();
+
+  public FakeEditorContext(CMutableDocument doc, CaretAnnotations caret, 
String imeState,
+      SelectionHelper selection) {
+    this.document = doc;
+    this.caretAnnotations = caret;
+    this.imeCompositionState = imeState;
+    this.selectionHelper = selection;
+  }
+
+  @Override
+  public void blur() {
+    // NO-OP
+  }
+
+  @Override
+  public void focus(boolean collapsed) {
+    // NO-OP
+  }
+
+  @Override
+  public CaretAnnotations getCaretAnnotations() {
+    return caretAnnotations;
+  }
+
+  @Override
+  public CMutableDocument getDocument() {
+    return document;
+  }
+
+  @Override
+  public String getImeCompositionState() {
+    return imeCompositionState;
+  }
+
+  @Override
+  public SelectionHelper getSelectionHelper() {
+    return selectionHelper;
+  }
+
+  @Override
+  public boolean isEditing() {
+    return false;
+  }
+
+  @Override
+  public Responsibility.Manager getResponsibilityManager() {
+    return responsibility;
+  }
+
+  @Override
+  public void addUpdateListener(EditorUpdateListener listener) {
+    throw new AssertionError("Not implemented");
+  }
+
+  @Override
+  public void removeUpdateListener(EditorUpdateListener listener) {
+    throw new AssertionError("Not implemented");
+  }
+
+  @Override
+  public void undoableSequence(Runnable cmd) {
+    cmd.run();
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/wave/client/editor/testing/FakeEditorEvent.java
----------------------------------------------------------------------
diff --git 
a/test/org/waveprotocol/wave/client/editor/testing/FakeEditorEvent.java 
b/test/org/waveprotocol/wave/client/editor/testing/FakeEditorEvent.java
new file mode 100755
index 0000000..d31cbe8
--- /dev/null
+++ b/test/org/waveprotocol/wave/client/editor/testing/FakeEditorEvent.java
@@ -0,0 +1,104 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.client.editor.testing;
+
+import org.waveprotocol.wave.client.common.util.FakeSignalEvent;
+import org.waveprotocol.wave.client.editor.constants.BrowserEvents;
+import org.waveprotocol.wave.client.editor.content.ContentPoint;
+import org.waveprotocol.wave.client.editor.event.EditorEvent;
+
+/**
+ * Use this class to mock events for EditorImpl methods
+ *
+ */
+public class FakeEditorEvent extends FakeSignalEvent implements EditorEvent {
+
+  public static SignalEventFactory<FakeEditorEvent> ED_FACTORY =
+    new SignalEventFactory<FakeEditorEvent>() {
+      @Override public FakeEditorEvent create() {
+        return new FakeEditorEvent();
+      }
+    };
+
+  /**
+   * @param type
+   * @return a fake event of the given type
+   */
+  public static FakeEditorEvent create(String type) {
+    return FakeSignalEvent.createEvent(ED_FACTORY, type);
+  }
+
+
+  /**
+   * Construct from a KeySignalType and a key code
+   */
+  public static FakeEditorEvent create(KeySignalType type, int keyCode) {
+    return FakeSignalEvent.createKeyPress(ED_FACTORY, type, keyCode, null);
+  }
+
+  /**
+   * @return A fake paste event
+   */
+  public static FakeEditorEvent createPasteEvent() {
+    return create("paste");
+  }
+
+  /**
+   * Creates a composition start, some composition updates, and a composition 
end
+   *
+   * @param numUpdates
+   * @return the events in order
+   */
+  public static FakeEditorEvent[] compositionSequence(int numUpdates) {
+    FakeEditorEvent[] evts = new FakeEditorEvent[numUpdates + 2];
+
+    evts[0] = create(BrowserEvents.COMPOSITIONSTART);
+    for (int i = 1; i <= numUpdates; i++) {
+      evts[i] = create(BrowserEvents.COMPOSITIONUPDATE);
+    }
+    evts[numUpdates + 1] = create(BrowserEvents.COMPOSITIONEND);
+    return evts;
+  }
+
+  private boolean shouldAllowDefault = false;
+  private ContentPoint caret;
+
+  @Override
+  public void allowBrowserDefault() {
+    shouldAllowDefault = true;
+  }
+
+
+  @Override
+  public ContentPoint getCaret() {
+    return caret;
+  }
+
+  @Override
+  public void setCaret(ContentPoint caret) {
+    this.caret = caret;
+  }
+
+  @Override
+  public boolean shouldAllowBrowserDefault() {
+    return shouldAllowDefault;
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/wave/client/editor/testing/FakeUser.java
----------------------------------------------------------------------
diff --git a/test/org/waveprotocol/wave/client/editor/testing/FakeUser.java 
b/test/org/waveprotocol/wave/client/editor/testing/FakeUser.java
new file mode 100644
index 0000000..e24762d
--- /dev/null
+++ b/test/org/waveprotocol/wave/client/editor/testing/FakeUser.java
@@ -0,0 +1,198 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.client.editor.testing;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.dom.client.Text;
+import org.waveprotocol.wave.client.common.util.DomHelper;
+import 
org.waveprotocol.wave.client.editor.extract.InconsistencyException.HtmlInserted;
+import 
org.waveprotocol.wave.client.editor.extract.InconsistencyException.HtmlMissing;
+import org.waveprotocol.wave.client.editor.extract.TypingExtractor;
+import 
org.waveprotocol.wave.client.editor.extract.TypingExtractor.SelectionSource;
+import org.waveprotocol.wave.client.editor.impl.HtmlView;
+
+import org.waveprotocol.wave.model.document.util.Point;
+
+/**
+ * Simulates the behaviour of a browser updating the DOM due to a user typing
+ *
+ * @author [email protected] (Daniel Danilatos)
+ */
+public class FakeUser implements SelectionSource{
+
+  public enum Action {
+    MOVE,
+    TYPE,
+    DELETE,
+    BACKSPACE,
+    SPLIT
+  }
+
+  private final HtmlView htmlView;
+
+  private Point<Node> caret;
+
+  public FakeUser(HtmlView htmlView) {
+    this.htmlView = htmlView;
+  }
+
+  @SuppressWarnings("unchecked")  // NOTE(zdwang): This is for (Point<Node>) 
action[1]
+  public void run(TypingExtractor extractor, Object... actions) throws 
HtmlMissing, HtmlInserted {
+    for (Object a : actions) {
+      Object[] action = (Object[]) a;
+      Point<Node> sel = getSelectionStart();
+      Text txt = sel == null ? null : sel.getContainer().<Text>cast();
+      Action type = (Action) action[0];
+      switch (type) {
+        case MOVE:
+          //extractor.flush();
+          setCaret((Point<Node>) action[1]);
+          break;
+        case TYPE:
+          String typed = (String) action[1];
+          extractor.somethingHappened(getSelectionStart());
+          if (sel.isInTextNode()) {
+            txt.insertData(sel.getTextOffset(), (String) action[1]);
+            moveCaret(((String) action[1]).length());
+          } else {
+            txt = Document.get().createTextNode(typed);
+            sel.getContainer().insertBefore(txt, sel.getNodeAfter());
+            setCaret(Point.inText((Node)txt, typed.length()));
+          }
+          break;
+        case BACKSPACE:
+        case DELETE:
+          extractor.somethingHappened(getSelectionStart());
+          int amount = (Integer) action[1];
+          if (type == Action.BACKSPACE) {
+            moveCaret(-amount);
+          }
+          deleteText(amount);
+          break;
+        case SPLIT:
+          sel.getContainer().<Text>cast().splitText(sel.getTextOffset());
+          moveCaret(0);
+          break;
+      }
+    }
+  }
+
+  private void deleteText(int amount) {
+    Point<Node> sel = getSelectionStart();
+    Text txt = sel == null ? null : sel.getContainer().<Text>cast();
+    int startIndex = sel.getTextOffset(), len;
+    while (amount > 0) {
+      if (txt == null || !DomHelper.isTextNode(txt)) {
+        throw new RuntimeException("Action ran off end of text node");
+      }
+      String data = txt.getData();
+      int remainingInNode = data.length() - startIndex;
+      if (remainingInNode >= amount) {
+        len = amount;
+      } else {
+        len = remainingInNode;
+      }
+      txt.setData(data.substring(0, startIndex) + data.substring(startIndex + 
len));
+      amount -= len;
+      startIndex = 0;
+      txt = htmlView.getNextSibling(txt).cast();
+    }
+    moveCaret(0);
+  }
+
+  public void moveCaret(int distance) {
+    Point<Node> caret = getSelectionStart();
+    if (!caret.isInTextNode()) {
+      Node before = Point.nodeBefore(htmlView, caret.asElementPoint());
+      if (DomHelper.isTextNode(before)) {
+        caret = Point.inText(before, before.<Text>cast().getLength());
+      } else if (DomHelper.isTextNode(caret.getNodeAfter())) {
+        caret = Point.inText(caret.getNodeAfter(), 0);
+      } else {
+        throw new RuntimeException("Unimplemented/Invalid");
+      }
+    }
+    Text nodelet = caret.getContainer().cast();
+    int offset = caret.getTextOffset() + distance;
+    while (offset < 0) {
+      nodelet = htmlView.getPreviousSibling(nodelet).cast();
+      if (nodelet == null || !DomHelper.isTextNode(nodelet)) {
+        throw new RuntimeException("Action ran off end of text node");
+      }
+      offset += nodelet.getLength();
+    }
+    while (offset > nodelet.getLength()) {
+      offset -= nodelet.getLength();
+      nodelet = htmlView.getPreviousSibling(nodelet).cast();
+      if (nodelet == null || !DomHelper.isTextNode(nodelet)) {
+        throw new RuntimeException("Action ran off end of text node");
+      }
+    }
+    setCaret(Point.inText((Node)nodelet, offset));
+  }
+
+  private void setCaret(Point<Node> point) {
+    caret = point;
+  }
+
+  public static Object move(Point<Node> caret) {
+    return new Object[]{Action.MOVE, caret, caret};
+  }
+
+//TODO(danilatos): Support non-collapsed selections
+//  public static Object move(Point<Node> start, Point<Node> end) {
+//    return new Object[]{Action.MOVE, start, end};
+//  }
+
+  public static Object type(String text) {
+    return new Object[]{Action.TYPE, text};
+  }
+
+  public static Object del() {
+    return del(1);
+  }
+
+  public static Object del(int len) {
+    return new Object[]{Action.DELETE, len};
+  }
+
+  public static Object bksp() {
+    return bksp(1);
+  }
+
+  public static Object bksp(int len) {
+    return new Object[]{Action.BACKSPACE, len};
+  }
+
+  public static Object split() {
+    return new Object[]{Action.SPLIT};
+  }
+
+  @Override
+  public Point<Node> getSelectionEnd() {
+    return caret;
+  }
+
+  @Override
+  public Point<Node> getSelectionStart() {
+    return caret;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/wave/client/editor/testing/MockTypingSink.java
----------------------------------------------------------------------
diff --git 
a/test/org/waveprotocol/wave/client/editor/testing/MockTypingSink.java 
b/test/org/waveprotocol/wave/client/editor/testing/MockTypingSink.java
new file mode 100644
index 0000000..f82bcc2
--- /dev/null
+++ b/test/org/waveprotocol/wave/client/editor/testing/MockTypingSink.java
@@ -0,0 +1,98 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.client.editor.testing;
+
+import org.waveprotocol.wave.client.editor.RestrictedRange;
+import org.waveprotocol.wave.client.editor.content.ContentNode;
+import org.waveprotocol.wave.client.editor.content.ContentTextNode;
+import org.waveprotocol.wave.client.editor.extract.TypingExtractor.TypingSink;
+
+import junit.framework.TestCase;
+
+import org.waveprotocol.wave.model.document.util.Point;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A mocked typing sink providing assertion methods.
+ *
+ * @author [email protected] (Daniel Danilatos)
+ */
+public class MockTypingSink implements TypingSink {
+  static class Op { }
+  static class Ins extends Op {
+    Point<ContentNode> start;
+    String text;
+    private Ins(Point<ContentNode> start, String text) {
+      this.start = start;
+      this.text = text;
+    }
+  }
+  static class Del extends Op {
+    Point<ContentNode> start;
+    int deleteSize;
+    private Del(Point<ContentNode> start, int deleteSize) {
+      this.start = start;
+      this.deleteSize = deleteSize;
+    }
+  }
+
+  final List<Op> expectedOps = new ArrayList<Op>();
+  final Set<ContentTextNode> affectedNodes = new HashSet<ContentTextNode>();
+  boolean finished = true;
+
+  public void expectDelete(Point<ContentNode> start, int deleteSize) {
+    expectedOps.add(new Del(start, deleteSize));
+  }
+
+  public void expectInsert(Point<ContentNode> start, String text) {
+    expectedOps.add(new Ins(start, text));
+  }
+
+  public void expectFinished() {
+    TestCase.assertTrue(finished && expectedOps.isEmpty());
+  }
+
+  @Override
+  public void typingReplace(Point<ContentNode> start, int length, String text,
+      RestrictedRange<ContentNode> range) {
+
+    if (length > 0) {
+      Del delOp = (Del)expectedOps.remove(0);
+      TestCase.assertEquals(delOp.start, start);
+      TestCase.assertEquals(delOp.deleteSize, length);
+    }
+
+    if (text.length() > 0) {
+      Ins insOp = (Ins)expectedOps.remove(0);
+      TestCase.assertEquals(insOp.start, start);
+      TestCase.assertEquals(insOp.text, text);
+    }
+
+    // TODO(danilatos): Test the range is correct
+    finished = true;
+  }
+
+  @Override
+  public void aboutToFlush() { }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/wave/client/editor/testing/StubDocumentOperationSink.java
----------------------------------------------------------------------
diff --git 
a/test/org/waveprotocol/wave/client/editor/testing/StubDocumentOperationSink.java
 
b/test/org/waveprotocol/wave/client/editor/testing/StubDocumentOperationSink.java
new file mode 100644
index 0000000..0e05705
--- /dev/null
+++ 
b/test/org/waveprotocol/wave/client/editor/testing/StubDocumentOperationSink.java
@@ -0,0 +1,38 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.client.editor.testing;
+
+import org.waveprotocol.wave.model.document.operation.DocOp;
+import org.waveprotocol.wave.model.operation.SilentOperationSink;
+
+/**
+ * For testing
+ * @author [email protected] (Daniel Danilatos)
+ */
+public class StubDocumentOperationSink implements SilentOperationSink<DocOp> {
+
+  /** Handy instance */
+  public static final StubDocumentOperationSink INSTANCE = new 
StubDocumentOperationSink();
+
+  @Override
+  public void consume(DocOp op) {
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/wave/client/editor/testing/StubSelectionHelper.java
----------------------------------------------------------------------
diff --git 
a/test/org/waveprotocol/wave/client/editor/testing/StubSelectionHelper.java 
b/test/org/waveprotocol/wave/client/editor/testing/StubSelectionHelper.java
new file mode 100644
index 0000000..d32fe8a
--- /dev/null
+++ b/test/org/waveprotocol/wave/client/editor/testing/StubSelectionHelper.java
@@ -0,0 +1,90 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.client.editor.testing;
+
+import org.waveprotocol.wave.client.editor.content.ContentNode;
+import org.waveprotocol.wave.client.editor.content.ContentRange;
+import org.waveprotocol.wave.client.editor.content.FocusedContentRange;
+import org.waveprotocol.wave.client.editor.selection.content.SelectionHelper;
+import org.waveprotocol.wave.model.document.util.FocusedRange;
+import org.waveprotocol.wave.model.document.util.Point;
+import org.waveprotocol.wave.model.document.util.Range;
+
+/**
+ * Empty implementation. Suitable for subclassing.
+ *
+ * @author [email protected] (Daniel Danilatos)
+ */
+public class StubSelectionHelper implements SelectionHelper {
+
+  public static final StubSelectionHelper INSTANCE = new StubSelectionHelper();
+
+  public void clearSelection() {
+
+  }
+
+  public FocusedContentRange getSelectionPoints() {
+    return null;
+  }
+
+  @Override
+  public ContentRange getOrderedSelectionPoints() {
+    return null;
+  }
+
+  public FocusedRange getSelectionRange() {
+    return null;
+  }
+
+  @Override
+  public Range getOrderedSelectionRange() {
+    return null;
+  }
+
+  public void setCaret(Point<ContentNode> caret) {
+
+  }
+
+  @Override
+  public void setCaret(int caret) {
+
+  }
+
+  public void setSelectionPoints(Point<ContentNode> start,
+      Point<ContentNode> end) {
+
+  }
+
+  public void setSelectionRange(FocusedRange selection) {
+
+  }
+
+  public Point<ContentNode> getFirstValidSelectionPoint() {
+    return null;
+  }
+
+  public Point<ContentNode> getLastValidSelectionPoint() {
+    return null;
+  }
+
+  public boolean isValidSelectionPoint(Point<ContentNode> cp) {
+    return false;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/wave/client/editor/testing/TestEditors.java
----------------------------------------------------------------------
diff --git a/test/org/waveprotocol/wave/client/editor/testing/TestEditors.java 
b/test/org/waveprotocol/wave/client/editor/testing/TestEditors.java
new file mode 100644
index 0000000..5de369b
--- /dev/null
+++ b/test/org/waveprotocol/wave/client/editor/testing/TestEditors.java
@@ -0,0 +1,92 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.client.editor.testing;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+
+import org.waveprotocol.wave.client.editor.Editor;
+import org.waveprotocol.wave.client.editor.EditorSettings;
+import org.waveprotocol.wave.client.editor.EditorStaticDeps;
+import org.waveprotocol.wave.client.editor.Editors;
+import org.waveprotocol.wave.client.editor.ElementHandlerRegistry;
+import org.waveprotocol.wave.client.editor.content.ContentDocument;
+import org.waveprotocol.wave.client.editor.content.PainterRegistry;
+import org.waveprotocol.wave.client.editor.content.Registries;
+import org.waveprotocol.wave.client.editor.content.Renderer;
+import org.waveprotocol.wave.client.editor.content.misc.StyleAnnotationHandler;
+import org.waveprotocol.wave.client.editor.content.paragraph.LineRendering;
+import org.waveprotocol.wave.client.editor.keys.KeyBindingRegistry;
+import org.waveprotocol.wave.client.widget.popup.simple.Popup;
+import org.waveprotocol.wave.model.conversation.Blips;
+import org.waveprotocol.wave.model.document.operation.automaton.DocumentSchema;
+import org.waveprotocol.wave.model.document.util.AnnotationRegistry;
+import org.waveprotocol.wave.model.document.util.LineContainers;
+
+/**
+ * Utility to set up basic editors for integration testing.
+ *
+ * @author [email protected] (Daniel Danilatos)
+ */
+public class TestEditors {
+  /**
+   * Gets a realistic editor for testing.
+   */
+  public static Editor getMinimalEditor() {
+    registerHandlers(Editor.ROOT_REGISTRIES);
+
+    EditorStaticDeps.setPopupProvider(Popup.LIGHTWEIGHT_POPUP_PROVIDER);
+    Editor editor = Editors.create();
+    editor.init(Editor.ROOT_REGISTRIES, KeyBindingRegistry.NONE, 
EditorSettings.DEFAULT);
+    editor.setEditing(true);
+    return editor;
+  }
+
+  private static void registerHandlers(Registries registries) {
+    AnnotationRegistry annotationRegistry = 
registries.getAnnotationHandlerRegistry();
+    PainterRegistry paintRegistry = registries.getPaintRegistry();
+    ElementHandlerRegistry elementHandlerRegistry = 
registries.getElementHandlerRegistry();
+
+    LineContainers.setTopLevelContainerTagname(Blips.BODY_TAGNAME);
+    LineRendering.registerContainer(Blips.BODY_TAGNAME, 
elementHandlerRegistry);
+    TestInlineDoodad.register(elementHandlerRegistry);
+    StyleAnnotationHandler.register(registries);
+  }
+
+  /** For testing purposes only. */
+  public static ContentDocument createTestDocument() {
+    ContentDocument doc = new 
ContentDocument(DocumentSchema.NO_SCHEMA_CONSTRAINTS);
+    Registries registries = Editor.ROOT_REGISTRIES.createExtension();
+    for (String t : new String[] {"q", "a", "b", "c", "x"}) {
+      final String tag = t;
+      registries.getElementHandlerRegistry().registerRenderer(tag,
+          new Renderer() {
+            @Override
+            public Element createDomImpl(Renderable element) {
+              return 
element.setAutoAppendContainer(Document.get().createElement(tag));
+            }
+          });
+    }
+    doc.setRegistries(registries);
+    Editor editor = getMinimalEditor();
+    editor.setContent(doc);
+    return doc;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/wave/client/editor/testing/TestInlineDoodad.java
----------------------------------------------------------------------
diff --git 
a/test/org/waveprotocol/wave/client/editor/testing/TestInlineDoodad.java 
b/test/org/waveprotocol/wave/client/editor/testing/TestInlineDoodad.java
new file mode 100644
index 0000000..a9ce076
--- /dev/null
+++ b/test/org/waveprotocol/wave/client/editor/testing/TestInlineDoodad.java
@@ -0,0 +1,54 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.client.editor.testing;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.SpanElement;
+
+import org.waveprotocol.wave.client.editor.ElementHandlerRegistry;
+import org.waveprotocol.wave.client.editor.RenderingMutationHandler;
+import org.waveprotocol.wave.client.editor.extract.PasteFormatRenderers;
+
+/**
+ * Inline doodad for testing.
+ *
+ */
+public class TestInlineDoodad {
+  private static class TestRenderer extends RenderingMutationHandler {
+    @Override
+    public Element createDomImpl(Renderable element) {
+      SpanElement domElement = Document.get().createSpanElement();
+      return element.setAutoAppendContainer(domElement);
+    }
+  }
+
+  public static final String FULL_TAGNAME = "span";
+
+  public static void register(ElementHandlerRegistry handlerRegistry) {
+    register(handlerRegistry, FULL_TAGNAME);
+  }
+
+  public static void register(ElementHandlerRegistry handlerRegistry, String 
tagName) {
+    RenderingMutationHandler renderingMutationHandler = new TestRenderer();
+    handlerRegistry.registerRenderingMutationHandler(tagName, 
renderingMutationHandler);
+    handlerRegistry.registerNiceHtmlRenderer(tagName, 
PasteFormatRenderers.SHALLOW_CLONE_RENDERER);
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/wave/client/editor/testing/Testing.gwt.xml
----------------------------------------------------------------------
diff --git a/test/org/waveprotocol/wave/client/editor/testing/Testing.gwt.xml 
b/test/org/waveprotocol/wave/client/editor/testing/Testing.gwt.xml
new file mode 100644
index 0000000..6ad55cc
--- /dev/null
+++ b/test/org/waveprotocol/wave/client/editor/testing/Testing.gwt.xml
@@ -0,0 +1,31 @@
+<?xml version='1.0'?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you 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.
+
+-->
+
+<module>
+<inherits name="com.google.gwt.user.User" />
+<inherits name="org.waveprotocol.wave.client.editor.Editor" />
+<inherits name="org.waveprotocol.wave.client.widget.popup.simple.Simple" />
+<inherits name="org.waveprotocol.wave.model.conversation.Conversation" />
+<inherits name="org.waveprotocol.wave.model.schema.conversation.Conversation" 
/> 
+<source path=""/>
+
+</module>

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/wave/client/testing/UndercurrentHarness.gwt.xml
----------------------------------------------------------------------
diff --git 
a/test/org/waveprotocol/wave/client/testing/UndercurrentHarness.gwt.xml 
b/test/org/waveprotocol/wave/client/testing/UndercurrentHarness.gwt.xml
new file mode 100644
index 0000000..c5d6167
--- /dev/null
+++ b/test/org/waveprotocol/wave/client/testing/UndercurrentHarness.gwt.xml
@@ -0,0 +1,53 @@
+<?xml version='1.0'?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you 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.
+
+-->
+
+<module rename-to="waveharness">
+  <inherits name="com.google.gwt.user.User" />
+  <inherits name="org.waveprotocol.wave.client.Client" />
+  <inherits name="org.waveprotocol.wave.common.bootstrap.FlagConstants" />
+  <inherits name="org.waveprotocol.wave.model.conversation.Conversation" />
+  <inherits name="org.waveprotocol.wave.model.conversation.Testing" />
+  <inherits name="org.waveprotocol.wave.model.util.Util" />
+  <inherits name="com.google.common.collect.Collect"/>
+  <inherits name="org.waveprotocol.box.stat.Stat" />
+  <inherits name='org.waveprotocol.box.webclient.stat.Stat'/>
+
+  <entry-point 
class="org.waveprotocol.wave.client.testing.UndercurrentHarness" />
+  <source path=""/>
+
+  <!-- Those comments beginning with the words "comment" or "Uncomment" have
+     special meaning to the PRESUBMIT.py script. -->
+  <!-- comment out the next line to build all client types -->
+
+  <set-property name="loglevel" value="none"/>
+  <set-property name="compiler.emulatedStack" value="false"/>
+
+  <!-- For make CSS obfuscation more pretty -->
+  <set-configuration-property name="CssResource.style" value="pretty"/>
+
+  <!--  This linker is required for superdev mode -->
+  <add-linker name="xsiframe" />
+
+  <!-- collapse all properties to decrease the amount of time spend compiling 
-->
+  <collapse-all-properties />
+
+</module>

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/wave/client/testing/UndercurrentHarness.java
----------------------------------------------------------------------
diff --git a/test/org/waveprotocol/wave/client/testing/UndercurrentHarness.java 
b/test/org/waveprotocol/wave/client/testing/UndercurrentHarness.java
new file mode 100644
index 0000000..48a6753
--- /dev/null
+++ b/test/org/waveprotocol/wave/client/testing/UndercurrentHarness.java
@@ -0,0 +1,380 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.client.testing;
+
+import com.google.gwt.core.client.Duration;
+import com.google.gwt.core.client.EntryPoint;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.GWT.UncaughtExceptionHandler;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.RepeatingCommand;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.Command;
+
+import org.waveprotocol.wave.client.StageOne;
+import org.waveprotocol.wave.client.StageThree;
+import org.waveprotocol.wave.client.StageTwo;
+import org.waveprotocol.wave.client.StageZero;
+import org.waveprotocol.wave.client.Stages;
+import org.waveprotocol.wave.client.common.safehtml.SafeHtmlBuilder;
+import org.waveprotocol.wave.client.common.util.AsyncHolder;
+import org.waveprotocol.wave.client.concurrencycontrol.MuxConnector;
+import org.waveprotocol.wave.client.doodad.DoodadInstallers;
+import 
org.waveprotocol.wave.client.doodad.attachment.AttachmentManagerProvider;
+import 
org.waveprotocol.wave.client.doodad.attachment.testing.FakeAttachmentsManager;
+import org.waveprotocol.wave.client.util.ClientFlags;
+import org.waveprotocol.wave.client.util.NullTypedSource;
+import org.waveprotocol.wave.client.util.OverridingTypedSource;
+import 
org.waveprotocol.wave.client.wavepanel.impl.toolbar.color.ComplexColorPicker;
+import 
org.waveprotocol.wave.client.wavepanel.impl.toolbar.color.SampleCustomColorPicker;
+import org.waveprotocol.wave.common.bootstrap.FlagConstants;
+import org.waveprotocol.wave.concurrencycontrol.channel.WaveViewService;
+import org.waveprotocol.wave.model.conversation.Conversation;
+import org.waveprotocol.wave.model.conversation.ConversationBlip;
+import org.waveprotocol.wave.model.conversation.ConversationThread;
+import org.waveprotocol.wave.model.conversation.ConversationView;
+import org.waveprotocol.wave.model.conversation.WaveBasedConversationView;
+import org.waveprotocol.wave.model.document.util.XmlStringBuilder;
+import org.waveprotocol.wave.model.id.IdGenerator;
+import org.waveprotocol.wave.model.schema.SchemaProvider;
+import org.waveprotocol.wave.model.schema.conversation.ConversationSchemas;
+import org.waveprotocol.wave.model.testing.BasicFactories;
+import org.waveprotocol.wave.model.testing.FakeIdGenerator;
+import org.waveprotocol.wave.model.util.CollectionUtils;
+import org.waveprotocol.wave.model.util.ReadableStringMap.ProcV;
+import org.waveprotocol.wave.model.util.StringMap;
+import org.waveprotocol.wave.model.wave.ParticipantId;
+import org.waveprotocol.wave.model.wave.data.DocumentFactory;
+import org.waveprotocol.wave.model.wave.data.ObservableWaveletData;
+import org.waveprotocol.wave.model.wave.data.ReadableWaveletData;
+import org.waveprotocol.wave.model.wave.data.WaveViewData;
+import org.waveprotocol.wave.model.wave.data.impl.WaveViewDataImpl;
+import org.waveprotocol.wave.model.wave.data.impl.WaveletDataImpl;
+import org.waveprotocol.wave.model.wave.opbased.OpBasedWavelet;
+import org.waveprotocol.wave.model.wave.opbased.WaveViewImpl;
+import 
org.waveprotocol.wave.model.wave.opbased.WaveViewImpl.WaveletConfigurator;
+import org.waveprotocol.wave.model.wave.opbased.WaveViewImpl.WaveletFactory;
+
+/**
+ * Kicks off some initial actions for development purposes.
+ *
+ */
+public class UndercurrentHarness implements EntryPoint {
+
+  private UndercurrentHarness() {
+  }
+
+  private static boolean loaded;
+
+  /**
+   * Runs the harness script.
+   */
+  @Override
+  public void onModuleLoad() {
+    AttachmentManagerProvider.init(new FakeAttachmentsManager());
+    if (loaded) {
+      return;
+    }
+    loaded = true;
+
+    final Timeline timeline = new Timeline();
+    new Stages() {
+
+      @Override
+      protected AsyncHolder<StageZero> createStageZeroLoader() {
+        return new StageZero.DefaultProvider() {
+
+          @Override
+          protected void onStageInit() {
+            timeline.add("stage0_start");
+          }
+
+          @Override
+          protected void onStageLoaded() {
+            timeline.add("stage0_end");
+          }
+
+          @Override
+          protected UncaughtExceptionHandler createUncaughtExceptionHandler() {
+            return GWT.getUncaughtExceptionHandler();
+          }
+        };
+      }
+
+      @Override
+      protected AsyncHolder<StageOne> createStageOneLoader(StageZero zero) {
+        return new StageOne.DefaultProvider(zero) {
+          @Override
+          protected void onStageInit() {
+            timeline.add("stage1_start");
+          }
+
+          @Override
+          protected void onStageLoaded() {
+            timeline.add("stage1_end");
+          }
+
+          @Override
+          protected Element createWaveHolder() {
+            return Document.get().getElementById("initialHtml");
+          }
+        };
+      }
+
+      @Override
+      protected AsyncHolder<StageTwo> createStageTwoLoader(StageOne one) {
+        return new StageTwo.DefaultProvider(one, null) {
+
+          @Override
+          protected void onStageInit() {
+            timeline.add("stage2_start");
+          }
+
+          @Override
+          protected void onStageLoaded() {
+            timeline.add("stage2_end");
+          }
+
+          @Override
+          protected void fetchWave(Accessor<WaveViewData> whenReady) {
+            timeline.add("fakewave_start");
+            WaveViewData fake = WaveFactory.create(getDocumentRegistry());
+            timeline.add("fakewave_end");
+            whenReady.use(fake);
+          }
+
+          @Override
+          protected ParticipantId createSignedInUser() {
+            return ParticipantId.ofUnsafe("[email protected]");
+          }
+
+          @Override
+          protected String createSessionId() {
+            return "session";
+          }
+
+          @Override
+          protected MuxConnector createConnector() {
+            return new MuxConnector() {
+              @Override
+              public void connect(Command whenOpened) {
+                if (whenOpened != null) {
+                  whenOpened.execute();
+                }
+              }
+
+              @Override
+              public void close() {
+                // Ignore
+              }
+            };
+          }
+
+          @Override
+          protected WaveViewService createWaveViewService() {
+            // The vacuous MuxConnector should avoid the need for a
+            // communication layer.
+            throw new UnsupportedOperationException();
+          }
+
+          @Override
+          protected SchemaProvider createSchemas() {
+            return new ConversationSchemas();
+          }
+        };
+      }
+
+      @Override
+      protected AsyncHolder<StageThree> createStageThreeLoader(StageTwo two) {
+        ClientFlags.resetWithSourceForTesting(OverridingTypedSource.of(new 
NullTypedSource())
+            .withBoolean(FlagConstants.ENABLE_UNDERCURRENT_EDITING, true)
+            .build());
+
+        // Only for test additional color pickers
+        new SampleCustomColorPicker(ComplexColorPicker.getInstance());
+
+        return new StageThree.DefaultProvider(two) {
+          @Override
+          protected void onStageInit() {
+            timeline.add("stage3_start");
+          }
+
+          @Override
+          protected void onStageLoaded() {
+            timeline.add("stage3_end");
+          }
+        };
+      }
+    }.load(new Command() {
+      @Override
+      public void execute() {
+        showInfo(timeline);
+      }
+    });
+  }
+
+  /**
+   * Populates the info box. Continuously reports which element has browser
+   * focus, and reports timing information for the stage loading.
+   *
+   * @param timeline timeline to report
+   */
+  private static void showInfo(Timeline timeline) {
+    Element timeBox = Document.get().getElementById("timeline");
+    timeline.dump(timeBox);
+
+    Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
+      private final Element activeBox = 
Document.get().getElementById("active");
+
+      @Override
+      public boolean execute() {
+        Element e = getActiveElement();
+        String text = (e != null ? e.getTagName() + " id:" + e.getId() : 
"none");
+        activeBox.setInnerText(text);
+        return true;
+      }
+
+      private native Element getActiveElement() /*-{
+        return $doc.activeElement;
+      }-*/;
+    }, 1000);
+  }
+
+  /**
+   * Creates a sample wave with a conversation in it.
+   */
+  private final static class WaveFactory {
+
+    /**
+     * Creates a sample wave.
+     *
+     * @param docFactory factory/registry for documents in the wave
+     * @return the wave state of the sample wave.
+     */
+    public static WaveViewDataImpl create(DocumentFactory<?> docFactory) {
+      // Create a sample wave.
+      WaveViewData sampleData = createSampleWave();
+
+      // Now build one that has the same setup state as that required by
+      // undercurrent (complex issue with the per-document output sinks).
+      WaveViewDataImpl newData = 
WaveViewDataImpl.create(sampleData.getWaveId());
+      WaveletDataImpl.Factory copier = 
WaveletDataImpl.Factory.create(docFactory);
+      for (ReadableWaveletData src : sampleData.getWavelets()) {
+        WaveletDataImpl copied = copier.create(src);
+        for (ParticipantId p : src.getParticipants()) {
+          copied.addParticipant(p);
+        }
+        copied.setVersion(copied.getVersion());
+        copied.setHashedVersion(src.getHashedVersion());
+        copied.setLastModifiedTime(src.getLastModifiedTime());
+        newData.addWavelet(copied);
+      }
+      return newData;
+    }
+
+    /** @return a sample wave with a conversation in it. */
+    private static WaveViewData createSampleWave() {
+      final ParticipantId sampleAuthor = 
ParticipantId.ofUnsafe("[email protected]");
+      IdGenerator gen = FakeIdGenerator.create();
+      final WaveViewDataImpl waveData = 
WaveViewDataImpl.create(gen.newWaveId());
+      final DocumentFactory<?> docFactory = 
BasicFactories.fakeDocumentFactory();
+      final ObservableWaveletData.Factory<?> waveletDataFactory =
+          new ObservableWaveletData.Factory<WaveletDataImpl>() {
+            private final ObservableWaveletData.Factory<WaveletDataImpl> inner 
=
+                WaveletDataImpl.Factory.create(docFactory);
+
+            @Override
+            public WaveletDataImpl create(ReadableWaveletData data) {
+              WaveletDataImpl wavelet = inner.create(data);
+              waveData.addWavelet(wavelet);
+              return wavelet;
+            }
+          };
+      WaveletFactory<OpBasedWavelet> waveletFactory = BasicFactories
+            .opBasedWaveletFactoryBuilder()
+            .with(waveletDataFactory)
+            .with(sampleAuthor)
+            .build();
+
+      WaveViewImpl<?> wave = WaveViewImpl.create(
+          waveletFactory, waveData.getWaveId(), gen, sampleAuthor, 
WaveletConfigurator.ADD_CREATOR);
+
+      // Build a conversation in that wave.
+      ConversationView v = WaveBasedConversationView.create(wave, gen);
+      Conversation c = v.createRoot();
+      ConversationThread root = c.getRootThread();
+      sampleReply(root.appendBlip());
+      write(root.appendBlip());
+      write(root.appendBlip());
+      write(root.appendBlip());
+
+      return waveData;
+    }
+
+    private static void write(ConversationBlip blip) {
+      org.waveprotocol.wave.model.document.Document d = blip.getContent();
+      d.emptyElement(d.getDocumentElement());
+      
d.appendXml(XmlStringBuilder.createFromXmlString("<body><line></line>Hello 
World</body>"));
+    }
+
+    private static void sampleReply(ConversationBlip blip) {
+      write(blip);
+      ConversationThread thread = blip.addReplyThread(8);
+      write(thread.appendBlip());
+    }
+
+    private static void biggerSampleReply(ConversationBlip blip) {
+      write(blip);
+      ConversationThread thread = blip.addReplyThread();
+      sampleReply(thread.appendBlip());
+      sampleReply(thread.appendBlip());
+      write(thread.appendBlip());
+    }
+  }
+
+  private static class Timeline {
+    private final StringMap<Integer> events = 
CollectionUtils.createStringMap();
+    private final Duration duration = new Duration();
+
+    void add(String name) {
+      events.put(name, duration.elapsedMillis());
+    }
+
+    void dump(Element timeBox) {
+      final SafeHtmlBuilder timeHtml = new SafeHtmlBuilder();
+      timeHtml.appendHtmlConstant("<table cellpadding='0' cellspacing='0'>");
+      events.each(new ProcV<Integer>() {
+        @Override
+        public void apply(String key, Integer value) {
+          timeHtml.appendHtmlConstant("<tr><td>");
+          timeHtml.appendEscaped(key);
+          timeHtml.appendHtmlConstant(":</td><td>");
+          timeHtml.appendEscaped("" + value);
+          timeHtml.appendHtmlConstant("</td></tr>");
+        }
+      });
+      timeHtml.appendHtmlConstant("</table>");
+      timeBox.setInnerHTML(timeHtml.toSafeHtml().asString());
+
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/70f39328/test/org/waveprotocol/wave/client/testing/public/UndercurrentHarness.html
----------------------------------------------------------------------
diff --git 
a/test/org/waveprotocol/wave/client/testing/public/UndercurrentHarness.html 
b/test/org/waveprotocol/wave/client/testing/public/UndercurrentHarness.html
new file mode 100644
index 0000000..a7ac1a3
--- /dev/null
+++ b/test/org/waveprotocol/wave/client/testing/public/UndercurrentHarness.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
"http://www.w3.org/TR/html4/loose.dtd";>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<html>
+  <head>
+    <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
+    <meta name="gwt:property" content="locale=en">
+    <title>Undercurrent Harness</title>
+    <style type='text/css'>
+      /* Full screen box. */
+      body {
+        position: absolute;
+        margin: 0;
+        padding: 0;
+        left: 0;
+        right: 0;
+        top: 0;
+        bottom: 0;
+
+        /*
+         * Arial seems to be the only non-ugly sans-serif font that renders 
roughly the same on both
+         * Chrome and Firefox.  e.g., 'sans-serif' on Firefox is a nicer 
mystery font, because it is
+         * wider, but Chrome binds 'sans-serif' to 'arial'.
+         */
+        font-family: arial;
+        font-size: small;
+      }
+
+      /* Fixed-width and horizontally centered, in a way that works on IE. */
+      #initialHtml {
+        position: absolute;
+        left: 50%;
+        right: 50%;
+        top: 4em;  /* Enough space for the header. */
+        bottom: 4em;
+        /* Expand out of the center line, aiming for ~70-75 character blip 
widths. */
+        margin-left: -250px;
+        margin-right: -250px;
+        border: 1px solid lightGray;
+      }
+
+      #info {
+        position: absolute;
+        z-index: 100;
+        right: 20px;
+        top: 20px;
+        padding: 0.2em;
+        width: 10em;
+        border: 1px solid black;
+        background-color: lightGray;
+      }
+
+      #timeline {
+        margin-left: 1em;
+      }
+
+    </style>
+  </head>
+  <body>
+    <h2>Undercurrent Test Harness</h2>
+    <div id='initialHtml'></div>
+    <div id='info'>
+      Active: <span id='active'></span><br/>
+      Timing:
+      <div id='timeline'></div>
+    </div>
+    <script type='text/javascript' src='waveharness.nocache.js'>
+    </script>
+  </body>
+</html>

Reply via email to