/** * 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.client.editor.util; import org.waveprotocol.wave.client.editor.content.misc.CaretAnnotations; import org.waveprotocol.wave.client.editor.content.misc.CaretAnnotations.AnnotationResolver; import org.waveprotocol.wave.model.document.util.DocProviders; import junit.framework.TestCase; import org.waveprotocol.wave.model.document.AnnotationBehaviour; import org.waveprotocol.wave.model.document.AnnotationBehaviour.AnnotationFamily; import org.waveprotocol.wave.model.document.AnnotationBehaviour.BiasDirection; import org.waveprotocol.wave.model.document.AnnotationBehaviour.ContentType; import org.waveprotocol.wave.model.document.AnnotationBehaviour.CursorDirection; import org.waveprotocol.wave.model.document.AnnotationBehaviour.DefaultAnnotationBehaviour; import org.waveprotocol.wave.model.document.MutableDocument; import org.waveprotocol.wave.model.document.raw.impl.Node; import org.waveprotocol.wave.model.document.util.AnnotationRegistry; import org.waveprotocol.wave.model.document.util.AnnotationRegistryImpl; import org.waveprotocol.wave.model.document.util.LineContainers; import org.waveprotocol.wave.model.util.StringMap; /** Check a number of different inheritence cases. */ public class AnnotationBehaviourLogicTest extends TestCase { private AnnotationRegistry REGISTRY = null; private static final String LINK_KEY = "link"; private static final String STYLE_KEY = "style"; /** Sets up a state with anything needed by all the tests. */ private AnnotationBehaviourLogic<Node> initDefault() { LineContainers.setTopLevelContainerTagname("body"); /** * Create a registry that has two types of behaviours: * - style-like (no bias change, inherits inside) * - link-like (bias away, with priority overriding containers). */ REGISTRY = AnnotationRegistryImpl.ROOT; REGISTRY.registerBehaviour(STYLE_KEY, new DefaultAnnotationBehaviour(AnnotationFamily.CONTENT)); REGISTRY.registerBehaviour(LINK_KEY, new AnnotationBehaviour() { public BiasDirection getBias(StringMap<Object> left, StringMap<Object> right, CursorDirection cursor) { assert left.containsKey(LINK_KEY) && right.containsKey(LINK_KEY); return left.get(LINK_KEY) == null ? BiasDirection.LEFT : BiasDirection.RIGHT; // away } public double getPriority() { return 10.0; // higher than default } public InheritDirection replace(StringMap<Object> inside, StringMap<Object> outside, ContentType type) { return InheritDirection.INSIDE; } public AnnotationFamily getAnnotationFamily() { return AnnotationFamily.CONTENT; } }); /** * Create document with content: key: [] = link annotation, {} = style * <line/>l[ink] blank {style} * <line/> * <line/>[{both}] * <line/>normal */ useDocument("<line/>link blank style<line/><line/>both<line/>normal", "link:L:4:7", "style:S:14:19", "link:L:23:27", "style:S:23:27"); useCaretAnnotations(); return new AnnotationBehaviourLogic<Node>(REGISTRY, doc, caret); } /** Makes sure ranged rebias is always right. */ public void testRangedRebias() { AnnotationBehaviourLogic<Node> logic = initDefault(); useSelection(4, 7); for (CursorDirection dir : CursorDirection.values()) { assertEquals(BiasDirection.RIGHT, logic.rebias(start, end, dir)); } } /** Test collapsed start/end of line, no annotations, bias inwards. */ public void testLineBoundaryRebias() { AnnotationBehaviourLogic<Node> logic = initDefault(); // start useSelection(29, 29); for (CursorDirection dir : CursorDirection.values()) { // always bias right assertEquals(BiasDirection.RIGHT, logic.rebias(start, end, dir)); } // end useSelection(35, 35); for (CursorDirection dir : CursorDirection.values()) { // always bias left assertEquals(BiasDirection.LEFT, logic.rebias(start, end, dir)); } // both useSelection(21, 21); for (CursorDirection dir : CursorDirection.values()) { // don't change bias assertEquals(CursorDirection.toBiasDirection(dir), logic.rebias(start, end, dir)); } } /** Test collapsed start/end of link, middle of a line. */ public void testLinkOutwardsBias() { AnnotationBehaviourLogic<Node> logic = initDefault(); // start of link useSelection(4, 4); for (CursorDirection dir : CursorDirection.values()) { // always bias left assertEquals(BiasDirection.LEFT, logic.rebias(start, end, dir)); } // middle = nothing useSelection(5, 5); for (CursorDirection dir : CursorDirection.values()) { // don't change assertEquals(CursorDirection.toBiasDirection(dir), logic.rebias(start, end, dir)); } // end useSelection(7, 7); for (CursorDirection dir : CursorDirection.values()) { // always bias right assertEquals(BiasDirection.RIGHT, logic.rebias(start, end, dir)); } } /** Priority checking case: test collapsed, start of link, end of line. */ public void testLineLinkPriority() { AnnotationBehaviourLogic<Node> logic = initDefault(); // start of line and link useSelection(23, 23); for (CursorDirection dir : CursorDirection.values()) { // always bias left - link takes priority assertEquals(BiasDirection.LEFT, logic.rebias(start, end, dir)); } } /** Test annotation supplementing when wholly outside/inside a ranged annotation. */ public void testSupplementNotAtBoundary() { // desired behaviour: no supplementing when wholly inside or outside an annotation AnnotationBehaviourLogic<Node> logic = initDefault(); // outside: useSelection(10, 10); for (BiasDirection dir : BiasDirection.values()) { // never inherit logic.supplementAnnotations(start, dir, ContentType.PLAIN_TEXT); assertFalse(caret.hasAnnotation(LINK_KEY)); } // inside: useSelection(5, 5); for (BiasDirection dir : BiasDirection.values()) { // never inherit logic.supplementAnnotations(start, dir, ContentType.PLAIN_TEXT); assertFalse(caret.hasAnnotation(LINK_KEY)); } } /** Test supplementing style-like annotations at start/end with both bias. */ public void testSupplementStyle() { // desired behaviour: only changes caret annotations when right biased, either end AnnotationBehaviourLogic<Node> logic = initDefault(); // start: useSelection(14, 14); for (BiasDirection dir : BiasDirection.values()) { // only inherit when right biased. logic.supplementAnnotations(start, dir, ContentType.PLAIN_TEXT); assertEquals(dir == BiasDirection.RIGHT, caret.hasAnnotation(STYLE_KEY)); caret.removeAnnotation(STYLE_KEY); } // end: useSelection(19, 19); for (BiasDirection dir : BiasDirection.values()) { // only inherit when right biased. logic.supplementAnnotations(start, dir, ContentType.PLAIN_TEXT); assertEquals(dir == BiasDirection.RIGHT, caret.hasAnnotation(STYLE_KEY)); caret.removeAnnotation(STYLE_KEY); } } /** Test supplementing link-like annotations at start/end with both bias. */ public void testSupplementLink() { // desired behaviour: same as styles AnnotationBehaviourLogic<Node> logic = initDefault(); // start: useSelection(4, 4); for (BiasDirection dir : BiasDirection.values()) { logic.supplementAnnotations(start, dir, ContentType.PLAIN_TEXT); assertEquals(dir == BiasDirection.RIGHT, caret.hasAnnotation(LINK_KEY)); caret.removeAnnotation(LINK_KEY); } // end: useSelection(7, 7); for (BiasDirection dir : BiasDirection.values()) { logic.supplementAnnotations(start, dir, ContentType.PLAIN_TEXT); assertEquals(dir == BiasDirection.RIGHT, caret.hasAnnotation(LINK_KEY)); caret.removeAnnotation(LINK_KEY); } } /** Test that supplementing doesn't clobber over caret annotations. */ public void testSupplementCaret() { // desired behaviour: same as styles AnnotationBehaviourLogic<Node> logic = initDefault(); // make sure it tries to add when none exists: useSelection(4, 4); logic.supplementAnnotations(start, BiasDirection.RIGHT, ContentType.PLAIN_TEXT); assertEquals("L", caret.getAnnotation(LINK_KEY)); // supplemented! // change and try again caret.setAnnotation(LINK_KEY, "different"); logic.supplementAnnotations(start, BiasDirection.RIGHT, ContentType.PLAIN_TEXT); assertEquals("different", caret.getAnnotation(LINK_KEY)); // left alone } /// /// Convenience methods for a context, copied from EditorAnnotationUtilTest /// // Parameters to make generating the EditorContext wrappers easier private int start; private int end; private MutableDocument<Node, ?, ?> doc; private CaretAnnotations caret; // Resolve the annotations by left-biasing within the document, stores it in doc. AnnotationResolver annotationResolver = new AnnotationResolver() { public String getAnnotation(String key) { return start == 0 || doc.getAnnotation(start - 1, key) == null ? null : doc.getAnnotation(start - 1, key).toString(); } }; // Converts the document and annotations into a CMutableDocument and stores in doc private void useDocument(String docContent, String... annotations) { MutableDocument<Node, ?, ?> mutable = DocProviders.MOJO.parse("<body>" + docContent + "</body>"); // format: key:value:start:end for (String value : annotations) { String[] vals = value.split(":"); mutable.setAnnotation(Integer.parseInt(vals[2]), Integer.parseInt(vals[3]), vals[0], vals[1]); } doc = mutable; } // Converts the annotations passed into a CaretAnnotations bundle and stores in caret private void useCaretAnnotations(String... annotations) { CaretAnnotations localCaret = new CaretAnnotations(); for (String value : annotations) { String[] dup = value.split(":"); localCaret.setAnnotation(dup[0], dup[1]); } caret = localCaret; caret.setAnnotationResolver(annotationResolver); } // Stores the start and end of the selection range to simulate. private void useSelection(int begin, int finish) { start = begin; end = finish; } }