/** * Copyright 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.waveprotocol.wave.client.editor.testtools; import org.waveprotocol.wave.model.document.util.Range; import org.waveprotocol.wave.model.util.ValueUtils; /** * Some assertion methods useful for testing editor * */ public class EditorAssert { /** * A list of tagnames of inline elements */ private static final String inlineTagNames = "b|u|i|span|a"; /** * Returns whether the strings are equal after {@link #normalizeXml(String)} * Helper method (used in delayed assert) to predict the outcome of * {@link #assertXmlEquals(String, String, String)} * * @param expectedXml * @param actualXml * @return Whether the normalized xml strings are equivalent. */ public static boolean xmlEquals(String expectedXml, String actualXml) { String expected = normalizeXml(expectedXml); String actual = normalizeXml(actualXml); if ((expected == null) && (actual == null)) { return true; } return (actual != null) && actual.equals(expected); } /** * Asserts that two xml strings are equal after {@link #normalizeXml(String)} * * @param msg * @param expectedXml * @param actualXml */ public static void assertXmlEquals( String msg, String expectedXml, String actualXml) { String normalizedExpectedXml = normalizeXml(expectedXml); String normalizedActualXml = normalizeXml(actualXml); System.err.println("Expected:[" + normalizedExpectedXml + "]"); System.err.println("Actual :[" + normalizedActualXml + "]"); assertEquals(msg, normalizedExpectedXml, normalizedActualXml); } /** * 'Normalises' an xml string to avoid fickleness in testing. Currently * we only perform a few normalisation steps: * * Self-close all empty elements, e.g., <p></p> -> <p/> * Replace " with ' * * More can be added, e.g., to normalise whitespace, if + when needed. * * @param xml * @return normalised xml string */ private static String normalizeXml(String xml) { if (xml == null) { return null; } int index = 0; int last = 0; StringBuilder out = new StringBuilder(); while ((index = xml.indexOf("></", index)) != -1) { if (xml.charAt(index - 1) == '/') { index += 3; // Already self-closing. continue; } int open = xml.lastIndexOf('<', index); if (open != -1 && xml.charAt(open + 1) != '/') { out.append(xml.subSequence(last, index)).append("/>"); index = last = xml.indexOf('>', index + 1) + 1; } else { index += 3; } } out.append(xml.subSequence(last, xml.length())); return out.toString().replaceAll("\"", "'"); } /** * @param msg * @param expected * @param actual */ private static void failLocationsDiffer(String msg, int expected, int actual) { fail(msg + ". expected=" + expected + ". actual=" + actual); } /** * @param msg * @param expected * @param actual */ private static void failSelectionsDiffer(String msg, Range expected, Range actual) { fail(msg + ". expected=" + expected + ". actual=" + actual); } /** * Asserts that two selection ranges are equal modulo the fact that some browsers * move selections/carets we set outside or inside inline elements in * various ways, and (absent a way to fix that), we want tests still to pass. * * If for example our code sets <p>a<i>|bc</i>d</p>, some browsers may in * fact do <p>a|<i>bc</i>d</p>. Likewise, <p>a<i>bc|</i>d</p> may become * <p>a<i>bc</i>|d</p>. Some browsers do the opposite: <p>a|<i>bc</i>d</p> * becomes <p>a<i>|bc</i>d</p> and so on. * * This assert should fail only if the difference between the two points * cannot be explained by the problems above. Note that to pass the second * example above (i.e., [0, 1, 2] should be considered equal to [0, 2]) we * need the content present in order to check that the 2 index indeed sits at * the right end of the <i>bc</i> element. * * Note also that we allow these adjustments to the selection *only* around * inline elements. For example, <p>|</p> is *not* he same as <p></p>|. Again * we need the content to determine this, along with a white-list of inline * element tag names. * * @param msg * @param expected * @param actual * @param content */ public static void assertSelectionEquals( String msg, Range expected, Range actual, String content) { if (expected == null && actual == null) { return; } if (expected != null && expected.equals(actual)) { return; } if (expected == null || actual == null || actual.isCollapsed() != expected.isCollapsed()) { failSelectionsDiffer(msg, expected, actual); } assertLocationsEquals(msg, expected.getStart(), actual.getStart(), content); if (!expected.isCollapsed()) { assertLocationsEquals(msg, expected.getEnd(), actual.getEnd(), content); } } /** * Asserts that two locations are equal modulo the facts described * for {@link #assertSelectionEquals(String, Range, Range, String)} * * @param msg * @param expected * @param actual * @param content */ public static void assertLocationsEquals( String msg, int expected, int actual, String content) { // First test for strict equality and nulls if (expected == actual) { return; } if (expected == -1 || actual == -1) { failLocationsDiffer(msg, expected, actual); } // We don't have strict equality, and neither location is -1. // Test if the left-most location can be made to match the right-most // one only by moving past tags of inline elements int left = location2Offset( (expected < actual) ? expected : actual, content); int right = location2Offset( (expected < actual) ? actual : expected, content); while (left != -1 && left < right) { left = maybeMovePastInlineTag(left, content); } if (left != right) { failLocationsDiffer(msg, expected, actual); } } /** * @param location e.g. 3 * @param content e.g. <p>ab</p> * @return the char offset in content of location, e.g., 4. * Calls fail if location is not inside content */ private static int location2Offset(int location, String content) { int offset = 0; for (int i = 1; i < location; i++) { if (offset > content.length()) { fail("Location " + location + " is not in " + content); } if (content.charAt(offset) == '<') { offset = content.indexOf('>', offset); if (offset == -1) { fail("Missing '>' in " + content); } } offset++; } return offset; } /** * Attempts to move offset past an inline tag * * @param offset * @param content * @return the new offset, or -1 if offset is not immediately before an inline tag */ private static int maybeMovePastInlineTag(int offset, String content) { if (content.charAt(offset) != '<') { return -1; } int tagStart = (content.charAt(offset + 1) == '/') ? offset + 2 : offset + 1; int space = content.indexOf(' ', tagStart); int close = content.indexOf('>', tagStart); int tagEnd = (space == -1) ? close : Math.min(space, close); String tag = content.substring(tagStart, tagEnd); return inlineTagNames.contains(tag) ? close + 1 : -1; } private static void assertEquals(String message, Object a, Object b) { if (!ValueUtils.equal(a, b)) { throw new AssertionError(message); } } private static void fail(String message) { throw new AssertionError(message); } }