/**
* 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.integration;
import org.waveprotocol.wave.client.debug.logger.LogLevel;
import org.waveprotocol.wave.client.editor.Editor;
import org.waveprotocol.wave.client.editor.EditorTestingUtil;
import org.waveprotocol.wave.model.document.operation.DocOp;
import org.waveprotocol.wave.model.document.operation.Nindo;
import org.waveprotocol.wave.model.document.operation.automaton.DocumentSchema;
import org.waveprotocol.wave.model.document.util.DocProviders;
import org.waveprotocol.wave.model.document.util.LineContainers;
import org.waveprotocol.wave.model.operation.OperationException;
import org.waveprotocol.wave.model.operation.OperationRuntimeException;
/**
* Unit test applying Operations thru {@link Editor#execute(DocOp)}
*
*/
public class OperationGwtTest extends TestBase {
/**
* Tests that applying an operation results in correct
* content and selection
*
* @param startContent
* @param nindo
* @param expectedContent
* @throws OperationException
*/
private void testOperation(String startContent,
Nindo nindo, String expectedContent)
throws OperationException {
// Compute a message identifying the test
String msg = startContent + " + " + nindo + " => " + expectedContent;
logger.trace().logXml("Testing: " + msg);
// Execute the operation on starting content
setContent(editor, startContent);
// Get a docOp out of the nindo
DocOp docOp = getDocOpFromNindo(nindo);
editor.getContent().consume(docOp);
// Assert editor health (editor already does this in debug builds)
if (!LogLevel.showDebug()) {
EditorTestingUtil.checkHealth(editor);
}
// Assert result
assertEditorContent(msg, expectedContent, editor);
}
private DocOp getDocOpFromNindo(Nindo nindo) {
try {
return DocProviders.POJO.build(editor.getDocumentInitialization(),
DocumentSchema.NO_SCHEMA_CONSTRAINTS).consumeAndReturnInvertible(nindo);
} catch (OperationException e) {
throw new OperationRuntimeException("initialization failed", e);
}
}
/**
* Tests that applying an operation causes exception
* TODO(user): consider testing for specific exception types
*
* @param startContent
* @param operation
*/
private void testOperationFailure(
String startContent, Nindo nindo) {
// Compute a message identifying the test
String msg = startContent + " + " + nindo + " => FAIL";
logger.trace().logXml("Testing: " + msg);
try {
// Execute the operation on starting content
setContent(editor, startContent);
DocOp docOp = getDocOpFromNindo(nindo);
editor.getContent().consume(docOp);
} catch (Throwable e) {
// Test succeeds
return;
}
// We didn't get the exception
fail(msg);
}
/**
* {@link #testOperation} specialised to InsertText operation
*
* @param startContent
* @param insertionPoint
* @param insertString
* @param expectedContent
* @throws OperationException
*/
public void testInsertText(String startContent,
int insertionPoint, String insertString, String expectedContent)
throws OperationException {
// Fix to wrap in line container body
startContent = "<body>" + abbreviations.expand(startContent) + "</body>";
expectedContent = "<body>" + abbreviations.expand(expectedContent) + "</body>";
testOperation(
startContent,
Nindo.insertCharacters(insertionPoint, insertString),
expectedContent);
}
/**
* {@link #testOperationFailure} specialised to InsertText operation
*
* @param startContent
* @param insertionPoint
* @param insertString
*/
private void testInsertTextFailure(
String startContent, int insertionPoint, String insertString) {
// Fix to wrap in line container body
startContent = "<body>" + abbreviations.expand(startContent) + "</body>";
testOperationFailure(
startContent,
Nindo.insertCharacters(insertionPoint, insertString));
}
/**
* {@link #testOperation} specialised to InsertXML operation
*
* @param startContent
* @param insertionPoint
* @param insertXML
* @param insertLength
* @param expectedContent
* @throws OperationException
*/
public void testInsertXML(String startContent,
int insertionPoint, String insertXML, int insertLength,
String expectedContent)
throws OperationException {
// TODO(danilatos): Bring this back
/*
testOperation(
startContent,
new XmlInsertXml(insertionPoint, insertXML, insertLength),
expectedContent);
*/
}
/**
* {@link #testOperationFailure} specialised to InsertXML operation
*
* @param startContent
* @param insertionPoint
* @param insertXML
* @param insertLength
*/
@SuppressWarnings("unused") // TODO(user): Add this back.
private void testInsertXMLFailure(String startContent,
int insertionPoint, String insertXML, int insertLength) {
// TODO(danilatos): Bring this back
/*
testOperationFailure(
startContent,
new XmlInsertXml(insertionPoint, insertXML, insertLength));
*/
}
/**
* {@link #testOperation} specialised to Delete operation
*
* @param startContent
* @param start
* @param end
* @param expectedContent
* @throws OperationException
*/
private void testDelete(String startContent,
int start, int end,
String expectedContent)
throws OperationException {
// TODO(danilatos): Bring this back
/*
testOperation(
startContent,
new XmlDelete(start, end),
expectedContent);
*/
}
/**
* {@link #testOperationFailure} specialised to Delete operation
*
* @param startContent
* @param start
* @param end
*/
private void testDeleteFailure(String startContent, int start, int end) {
// TODO(danilatos): Bring this back
/*
testOperationFailure(
startContent,
new XmlDelete(start, end));
*/
}
/**
* Tests insert text operation
*
* @throws OperationException
*/
public void testInsertText()
throws OperationException {
prepareTest();
// Setup abbreviations from <l/> to <tagName/>
abbreviations.add("<l/>", "<" + LineContainers.LINE_TAGNAME + "/>");
abbreviations.add("<u>", "<span>");
abbreviations.add("</u>", "</span>");
abbreviations.add("<i>", "<span>");
abbreviations.add("</i>", "</span>");
// Test inserting X into empty paragraph
testInsertText(
"<l/>",
3, "X",
"<l/>X"
);
// Test inserting X into simple text node
testInsertText(
"<l/>abcd",
3, "X",
"<l/>Xabcd"
);
testInsertText(
"<l/>abcd",
5, "X",
"<l/>abXcd"
);
testInsertText(
"<l/>abcd",
7, "X",
"<l/>abcdX"
);
// Test inserting X into slightly more complex DOM,
// including creating new text node
testInsertText(
"<l/>a<i>bc</i><u>de</u>f",
3, "X",
"<l/>Xa<i>bc</i><u>de</u>f"
);
testInsertText(
"<l/>a<i>bc</i><u>de</u>f",
4, "X",
"<l/>aX<i>bc</i><u>de</u>f"
);
testInsertText(
"<l/>a<i>bc</i><u>de</u>f",
5, "X",
"<l/>a<i>Xbc</i><u>de</u>f"
);
testInsertText(
"<l/>a<i>bc</i><u>de</u>f",
7, "X",
"<l/>a<i>bcX</i><u>de</u>f"
);
testInsertText(
"<l/>a<i>bc</i><u>de</u>f",
8, "X",
"<l/>a<i>bc</i>X<u>de</u>f"
);
testInsertText(
"<l/>a<i>bc</i><u>de</u>f",
9, "X",
"<l/>a<i>bc</i><u>Xde</u>f"
);
testInsertText(
"<l/>a<i>bc</i><u>de</u>f",
10, "X",
"<l/>a<i>bc</i><u>dXe</u>f"
);
testInsertText(
"<l/>a<i>bc</i><u>de</u>f",
12, "X",
"<l/>a<i>bc</i><u>de</u>Xf"
);
testInsertText(
"<l/>a<i>bc</i><u>de</u>f",
13, "X",
"<l/>a<i>bc</i><u>de</u>fX"
);
// Test we can insert special xml chars in text nodes w/o escaping
testInsertText(
"<l/>abcd",
5, "<br/>",
"<l/>ab<br/>cd"
);
// Borrowed from zdwang...
/*
testInsertText(
"<l/>" +
"<i>ab</i>" +
"cd " +
"<b>" +
"e" +
"<i>fg</i>" +
"</b>" +
" h" +
"",
6, "12",
"<l/>" +
"<i>ab</i>" +
"12cd " +
"<b>" +
"e" +
"<i>fg</i>" +
"</b>" +
" h" +
""
);
testInsertText(
"<l/>" +
"<i>ab</i>" +
"cd " +
"<b>" +
"e" +
"<i>fg</i>" +
"</b>" +
" h" +
"",
13, " 12 ",
"<l/>" +
"<i>ab</i>" +
"cd " +
"<b>" +
"e" +
"<i>f 12 g</i>" +
"</b>" +
" h" +
""
);
*/
// Test caret preservation when inserting X into empty paragraph
testInsertText(
"<l/>|",
3, "X",
"<l/>|X"
);
// Test caret preservation when inserting X into simple text node
// We want the caret to stay left of the insertion when the insertion
// point coincides with the caret location.
testInsertText(
"<l/>ab|cd",
3, "X",
"<l/>Xab|cd"
);
testInsertText(
"<l/>ab|cd",
4, "X",
"<l/>aXb|cd"
);
testInsertText(
"<l/>ab|cd",
5, "X",
"<l/>ab|Xcd"
);
testInsertText(
"<l/>ab|cd",
6, "X",
"<l/>ab|cXd"
);
testInsertText(
"<l/>ab|cd",
7, "X",
"<l/>ab|cdX"
);
testInsertText(
"<l/>|abcd",
3, "X",
"<l/>|Xabcd"
);
testInsertText(
"<l/>|abcd",
4, "X",
"<l/>|aXbcd"
);
testInsertText(
"<l/>abcd|",
7, "X",
"<l/>abcd|X"
);
testInsertText(
"<l/>abcd|",
6, "X",
"<l/>abcXd|"
);
// Test selection preservation when inserting XX into simple text node
// We want the insertion to stay outside the selection when the insertion
// point coincides with an end point in the selection
testInsertText(
"<l/>a[bc]d",
3, "XX",
"<l/>XXa[bc]d"
);
// TODO(user): debate outcome with zdwang and alexmah
// "<l/>aXX[bc]d"
testInsertText(
"<l/>a[bc]d",
4, "XX",
"<l/>a[XXbc]d"
);
testInsertText(
"<l/>a[bc]d",
5, "XX",
"<l/>a[bXXc]d"
);
testInsertText(
"<l/>a[bc]d",
6, "XX",
"<l/>a[bc]XXd"
);
testInsertText(
"<l/>a[bc]d",
7, "XX",
"<l/>a[bc]dXX"
);
// TODO(user): debate outcome with zdwang and alexmah
// "<l/>XX[ab]cd"
testInsertText(
"<l/>[ab]cd",
3, "XX",
"<l/>[XXab]cd"
);
testInsertText(
"<l/>[ab]cd",
4, "XX",
"<l/>[aXXb]cd"
);
testInsertText(
"<l/>[ab]cd",
5, "XX",
"<l/>[ab]XXcd"
);
testInsertText(
"<l/>[ab]cd",
6, "XX",
"<l/>[ab]cXXd"
);
testInsertText(
"<l/>[ab]cd",
7, "XX",
"<l/>[ab]cdXX"
);
testInsertText(
"<l/>ab[cd]",
3, "XX",
"<l/>XXab[cd]"
);
testInsertText(
"<l/>ab[cd]",
4, "XX",
"<l/>aXXb[cd]"
);
// TODO(user): debate outcome with zdwang and alexmah
// "<l/>abXX[cd]"
testInsertText(
"<l/>ab[cd]",
5, "XX",
"<l/>ab[XXcd]"
);
testInsertText(
"<l/>ab[cd]",
6, "XX",
"<l/>ab[cXXd]"
);
testInsertText(
"<l/>ab[cd]",
7, "XX",
"<l/>ab[cd]XX"
);
}
public void testInsertInvalidText() {
// Test invalid insertion point
testInsertTextFailure(
"<l/>abcd",
9, "X"
);
testInsertTextFailure(
"<l/>abcd",
-1, "X"
);
testInsertTextFailure(
"<l/><i>abcd</i>",
11, "X"
);
}
/**
* Tests insert xml operation
*
* @throws OperationException
*/
public void testInsertXML()
throws OperationException {
prepareTest();
// Setup abbreviations from <l/> to <tagName/>
abbreviations.add("<l/>", "<" + LineContainers.LINE_TAGNAME + "/>");
abbreviations.add("<u>", "<label>");
abbreviations.add("</u>", "</label>");
abbreviations.add("<i>", "<label>");
abbreviations.add("</i>", "</label>");
// Test inserting simple <br/> element into text node
// TODO(user, lars): XmlInsertXml doesn't parse <br/> correctly
testInsertXML(
"<l/>abcd",
3, "<br/>", 2,
"<l/><br/>abcd"
);
testInsertXML(
"<l/>abcd",
5, "<br/>", 2,
"<l/>ab<br/>cd"
);
testInsertXML(
"<l/>abcd",
7, "<br/>", 2,
"<l/>abcd<br/>"
);
// Test inserting <br/> into slightly more complex DOM
testInsertXML(
"<l/>a<i>bc</i><u>de</u>f",
3, "<br/>", 2,
"<l/><br/>a<i>bc</i><u>de</u>f"
);
testInsertXML(
"<l/>a<i>bc</i><u>de</u>f",
4, "<br/>", 2,
"<l/>a<br/><i>bc</i><u>de</u>f"
);
testInsertXML(
"<l/>a<i>bc</i><u>de</u>f",
5, "<br/>", 2,
"<l/>a<i><br/>bc</i><u>de</u>f"
);
testInsertXML(
"<l/>a<i>bc</i><u>de</u>f",
7, "<br/>", 2,
"<l/>a<i>bc<br/></i><u>de</u>f"
);
testInsertXML(
"<l/>a<i>bc</i><u>de</u>f",
8, "<br/>", 2,
"<l/>a<i>bc</i><br/><u>de</u>f"
);
testInsertXML(
"<l/>a<i>bc</i><u>de</u>f",
9, "<br/>", 2,
"<l/>a<i>bc</i><u><br/>de</u>f"
);
testInsertXML(
"<l/>a<i>bc</i><u>de</u>f",
10, "<br/>", 2,
"<l/>a<i>bc</i><u>d<br/>e</u>f"
);
testInsertXML(
"<l/>a<i>bc</i><u>de</u>f",
12, "<br/>", 2,
"<l/>a<i>bc</i><u>de</u><br/>f"
);
testInsertXML(
"<l/>a<i>bc</i><u>de</u>f",
13, "<br/>", 2,
"<l/>a<i>bc</i><u>de</u>f<br/>"
);
// Test inserting more complex stuff
testInsertXML(
"<l/>abcd",
8, "<l/>efgh", 6,
"<l/>abcd<l/>efgh"
);
testInsertXML(
"<l/>abcd",
3, "V<i>X</i><u>Y</u>ZZ", 9,
"<l/>V<i>X</i><u>Y</u>ZZabcd"
);
testInsertXML(
"<l/>abcd",
5, "V<i>X</i><u>Y</u>ZZ", 9,
"<l/>abV<i>X</i><u>Y</u>ZZcd"
);
testInsertXML(
"<l/>abcd",
7, "V<i>X</i><u>Y</u>ZZ", 9,
"<l/>abcdV<i>X</i><u>Y</u>ZZ"
);
// inserting A with attribute
// TODO(user, lars): getContent fails to report href attribute
// ContentElement.registerSchema("a", ContentElement.Schema.create(new String[] {"href"}));
// testInsertXML(
// "<l/>abc",
// 4, "<a href='http://www.google.com/'>google</a>", 1,
// "<l/>ab<a href=\"http://www.google.com/\">google</a>c"
// );
// Test invalid operations (length wrong)
// TODO(user, lars): XmlInsertXml doesn't fail when length wrong
// testInsertXMLFailure(
// "<l/>abcd",
// 4, "V<i>X</i><u>Y</u>ZZ", 8
// );
// testInsertXMLFailure(
// "<l/>abcd",
// 4, "V<i>X</i><u>Y</u>ZZ", 10
// );
// Test caret preservation when inserting into text node
testInsertXML(
"<l/>ab|cd",
4, "V<i>X</i><u>Y</u>ZZ", 9,
"<l/>aV<i>X</i><u>Y</u>ZZb|cd"
);
testInsertXML(
"<l/>ab|cd",
5, "V<i>X</i><u>Y</u>ZZ", 9,
"<l/>ab|V<i>X</i><u>Y</u>ZZcd"
);
testInsertXML(
"<l/>ab|cd",
6, "V<i>X</i><u>Y</u>ZZ", 9,
"<l/>ab|cV<i>X</i><u>Y</u>ZZd"
);
// Test caret preservation when inserting outside text node
testInsertXML(
"<l/>a<i>bc|</i><u>de</u>f",
8, "V<i>X</i><u>Y</u>ZZ", 9,
"<l/>a<i>bc|</i>V<i>X</i><u>Y</u>ZZ<u>de</u>f"
);
testInsertXML(
"<l/>a|<i>bc</i><u>de</u>f",
8, "V<i>X</i><u>Y</u>ZZ", 9,
"<l/>a|<i>bc</i>V<i>X</i><u>Y</u>ZZ<u>de</u>f"
);
/* TODO(user): bring this test back somehow. Problem is that
// browsers don't like placing the caret between the <i> and <u>
// nodes. Different browsers move the caret to different locations
// inside text nodes + the result of the test then differs :-(
testInsertXML(
"<l/>a<i>bc</i>|<u>de</u>f",
new int[] {0, 2}, "V<i>X</i><u>Y</u>ZZ", 5,
"<l/>a<i>bc</i>|V<i>X</i><u>Y</u>ZZ<u>de</u>f"
);*/
testInsertXML(
"<l/>a<i>bc</i><u>d|e</u>f",
8, "V<i>X</i><u>Y</u>ZZ", 9,
"<l/>a<i>bc</i>V<i>X</i><u>Y</u>ZZ<u>d|e</u>f"
);
testInsertXML(
"<l/>a<i>bc</i><u>de</u>|f",
8, "V<i>X</i><u>Y</u>ZZ", 9,
"<l/>a<i>bc</i>V<i>X</i><u>Y</u>ZZ<u>de</u>|f"
);
// Test selection preservation when inserting into text node
testInsertXML(
"<l/>[ab]cd",
5, "V<i>X</i><u>Y</u>ZZ", 9,
"<l/>[ab]V<i>X</i><u>Y</u>ZZcd"
);
testInsertXML(
"<l/>a[bc]d",
5, "V<i>X</i><u>Y</u>ZZ", 9,
"<l/>a[bV<i>X</i><u>Y</u>ZZc]d"
);
// TODO(user): debate outcome with zdwang and alexmah
// "<l/>abV<i>X</i><u>Y</u>ZZ[cd]"
testInsertXML(
"<l/>ab[cd]",
5, "V<i>X</i><u>Y</u>ZZ", 9,
"<l/>ab[V<i>X</i><u>Y</u>ZZcd]"
);
// Test selection preservation when inserting outside text node
testInsertXML(
"<l/>[a<i>bc</i>]<u>de</u>f",
8, "V<i>X</i><u>Y</u>ZZ", 9,
"<l/>[a<i>bc</i>]V<i>X</i><u>Y</u>ZZ<u>de</u>f"
);
testInsertXML(
"<l/>a<i>b[c</i><u>d]e</u>f",
8, "V<i>X</i><u>Y</u>ZZ", 9,
"<l/>a<i>b[c</i>V<i>X</i><u>Y</u>ZZ<u>d]e</u>f"
);
/* TODO(user): bring this test back somehow. Problem is that
// browsers don't like placing the caret between the <i> and <u>
// nodes. Different browsers move the caret to different locations
// inside text nodes + the result of the test then differs :-(
// TODO(user): debate outcome with zdwang and alexmah
// "<l/>a<i>bc</i>V<i>X</i><u>Y</u>ZZ[<u>de</u>f]"
testInsertXML(
"<l/>a<i>bc</i>[<u>de</u>f]",
new int[] {0, 2}, "V<i>X</i><u>Y</u>ZZ", 9,
"<l/>a<i>bc</i>[V<i>X</i><u>Y</u>ZZ<u>de</u>f]"
);*/
testInsertXML(
"<l/>[a<i>b]c</i><u>de</u>f",
8, "V<i>X</i><u>Y</u>ZZ", 9,
"<l/>[a<i>b]c</i>V<i>X</i><u>Y</u>ZZ<u>de</u>f"
);
testInsertXML(
"<l/>a<i>bc</i><u>d[e</u>f]",
8, "V<i>X</i><u>Y</u>ZZ", 9,
"<l/>a<i>bc</i>V<i>X</i><u>Y</u>ZZ<u>d[e</u>f]"
);
}
/**
* Tests delete operation
*
* @throws OperationException
*/
public void testDelete()
throws OperationException {
prepareTest();
// Setup abbreviations from <l/> to <tagName/>
abbreviations.add("<l/>", "<" + LineContainers.LINE_TAGNAME + "/>");
abbreviations.add("<u>", "<label>");
abbreviations.add("</u>", "</label>");
abbreviations.add("<i>", "<label>");
abbreviations.add("</i>", "</label>");
// simple delete all
testDelete(
"<l/>abcd",
3, 7,
"<p/>"
);
// simple delete some
testDelete(
"<l/>abcde",
4, 7,
"<l/>ae"
);
// simply delete some child node
testDelete(
"<l/>ab<i>c</i>de",
4, 5,
"<l/>a<i>c</i>de");
// delete <i> node
testDelete(
"<l/>ab<i>cd</i>ef",
5, 9,
"<l/>abef");
// fail to delete across a tree from shallow to deep
// TODO(user): await answer from alex and maybe bring the delete test below
// back...
// testDelete(
// "<l/>ab<i>cd</i>ef",
// 3, 6,
// "<l/>a<i>d</i>ef"
// );
// fail to delete across a tree from deep to shallow
testDeleteFailure(
"<l/>ab<i>cd</i>ef",
7, 10
);
// fail to delete across a tree at equal depth
testDeleteFailure(
"<l/><i>ab</i><i>cd</i>",
5, 9
);
// fail to delete across a tree where the start point is deeper
testDeleteFailure(
"<l/><i>a<b>ef</b>b</i><i>cd</i>",
7, 13
);
// fail to delete across a tree where the end point is deeper
testDeleteFailure(
"<l/><i>ab</i><i>c<b>ef</b>d</i>",
5, 11
);
// fail to delete across a tree where both points are deep
testDeleteFailure(
"<l/><i>a<b>ef</b>b</i><i>c<b>ef</b>d</i>",
7, 15
);
// fail to delete across a tree where both points are the leftmost points in
// their containers
testDeleteFailure(
"<l/><i>a<b>ef</b>b</i><i>c<b>ef</b>d</i>",
6, 14
);
// fail to delete across a tree where both points are the rightmost points in
// their containers
// TODO(danilatos): this delete silently does nothing, when it should probably
// throw an exception
// testDeleteFailure(
// "<l/><i>a<b>ef</b>b</i><i>c<b>ef</b>d</i>",
// 7, 15
// );
// fail to delete across a tree where the each child path passes through an
// element node that is a leftmost sibling after the two paths diverge
testDeleteFailure(
"<l/><i><b>ef</b>ab</i><i><b>ef</b>cd</i>",
6, 14
);
// fail to delete across a tree where the each child path passes through an
// element node that is a rightmost sibling after the two paths diverge
testDeleteFailure(
"<l/><i>ab<b>ef</b></i><i>cd<b>ef</b></i>",
8, 16
);
// fail to delete from parent to child
testDeleteFailure(
"<l/>ab<i>cd</i>ef",
5, 7
);
// fail to delete from child to parent
testDeleteFailure(
"<l/>ab<i>cd</i>ef",
7, 9
);
// fail to delete inverted range
// testDeleteFailure(
// "<l/>abcde",
// 4, 3
// );
// simple fail, index out of bounds
testDeleteFailure(
"<l/>abcde",
5, 12
);
// simple fail, start not found
testDeleteFailure(
"<l/>abcde",
12, 14
);
}
}