| package org.wordpress.android.editor; |
| |
| import android.text.Editable; |
| import android.text.Spannable; |
| import android.text.SpannableStringBuilder; |
| import android.text.style.ForegroundColorSpan; |
| import android.text.style.RelativeSizeSpan; |
| import android.text.style.StyleSpan; |
| |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.robolectric.RobolectricTestRunner; |
| import org.robolectric.annotation.Config; |
| |
| import static org.junit.Assert.assertEquals; |
| |
| @Config(sdk = 18) |
| @RunWith(RobolectricTestRunner.class) |
| public class HtmlStyleTextWatcherTest { |
| |
| private HtmlStyleTextWatcherForTests mWatcher; |
| private Editable mContent; |
| private boolean mUpdateSpansWasCalled; |
| private HtmlStyleTextWatcher.SpanRange mSpanRange; |
| |
| @Before |
| public void setUp() { |
| mWatcher = new HtmlStyleTextWatcherForTests(); |
| mUpdateSpansWasCalled = false; |
| } |
| |
| @Test |
| public void testTypingNormalText() { |
| // -- Test typing in normal text (non-HTML) in an empty document |
| mContent = new SpannableStringBuilder("a"); |
| |
| mWatcher.onTextChanged(mContent, 0, 0, 1); // Typed "a" |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(false, mUpdateSpansWasCalled); |
| |
| mContent = new SpannableStringBuilder("ab"); |
| |
| mWatcher.onTextChanged(mContent, 1, 0, 1); // Typed "b" |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(false, mUpdateSpansWasCalled); |
| |
| |
| // -- Test typing in normal text after exiting tags |
| mContent = new SpannableStringBuilder("text <b>bold</b> a"); |
| |
| mWatcher.onTextChanged(mContent, 17, 0, 1); // Typed "a" |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(false, mUpdateSpansWasCalled); |
| |
| |
| // -- Test typing in normal text before exiting tags |
| mContent = new SpannableStringBuilder("text a <b>bold</b>"); |
| |
| mWatcher.onTextChanged(mContent, 5, 0, 1); // Typed "a" |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(false, mUpdateSpansWasCalled); |
| } |
| |
| @Test |
| public void testTypingInOpeningTag() { |
| // Test with several different cases of pre-existing text |
| String[] previousTextCases = new String[]{"", "plain text", "<i>", |
| "<blockquote>some existing content</blockquote> "}; |
| for (String initialText : previousTextCases) { |
| int offset = initialText.length(); |
| mUpdateSpansWasCalled = false; |
| |
| // -- Test typing in an opening tag symbol |
| mContent = new SpannableStringBuilder(initialText + "<"); |
| |
| mWatcher.onTextChanged(mContent, offset, 0, 1); |
| mWatcher.afterTextChanged(mContent); |
| |
| // No formatting should be applied/removed |
| assertEquals(false, mUpdateSpansWasCalled); |
| |
| |
| // -- Test typing in the tag name |
| mContent = new SpannableStringBuilder(initialText + "<b"); |
| |
| mWatcher.onTextChanged(mContent, offset + 1, 0, 1); |
| mWatcher.afterTextChanged(mContent); |
| |
| // No formatting should be applied/removed |
| assertEquals(false, mUpdateSpansWasCalled); |
| |
| |
| // -- Test typing in a closing tag symbol |
| mContent = new SpannableStringBuilder(initialText + "<b>"); |
| |
| mWatcher.onTextChanged(mContent, offset + 2, 0, 1); |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(offset, mSpanRange.getOpeningTagLoc()); |
| assertEquals(offset + 3, mSpanRange.getClosingTagLoc()); |
| } |
| } |
| |
| @Test |
| public void testTypingInClosingTag() { |
| // Test with several different cases of pre-existing text |
| String[] previousTextCases = new String[]{"<b>stuff", "plain text <b>stuff", "<i><b>stuff", |
| "<blockquote>some existing content</blockquote> <b>stuff"}; |
| |
| for (String initialText : previousTextCases) { |
| int offset = initialText.length(); |
| mUpdateSpansWasCalled = false; |
| |
| // -- Test typing in an opening tag symbol |
| mContent = new SpannableStringBuilder(initialText + "<"); |
| |
| mWatcher.onTextChanged(mContent, offset, 0, 1); |
| mWatcher.afterTextChanged(mContent); |
| |
| // No formatting should be applied/removed |
| assertEquals(false, mUpdateSpansWasCalled); |
| |
| |
| // -- Test typing in the closing tag slash |
| mContent = new SpannableStringBuilder(initialText + "</"); |
| |
| mWatcher.onTextChanged(mContent, offset + 1, 0, 1); |
| mWatcher.afterTextChanged(mContent); |
| |
| // No formatting should be applied/removed |
| assertEquals(false, mUpdateSpansWasCalled); |
| |
| // -- Test typing in the tag name |
| mContent = new SpannableStringBuilder(initialText + "</b"); |
| |
| mWatcher.onTextChanged(mContent, offset + 2, 0, 1); |
| mWatcher.afterTextChanged(mContent); |
| |
| // No formatting should be applied/removed |
| assertEquals(false, mUpdateSpansWasCalled); |
| |
| |
| // -- Test typing in a closing tag symbol |
| mContent = new SpannableStringBuilder(initialText + "</b>"); |
| |
| mWatcher.onTextChanged(mContent, offset + 3, 0, 1); |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(offset, mSpanRange.getOpeningTagLoc()); |
| assertEquals(offset + 4, mSpanRange.getClosingTagLoc()); |
| } |
| } |
| |
| @Test |
| public void testTypingInTagWithSurroundingTags() { |
| // Spans in this case will be applied until the end of the next tag |
| // This fixes a pasting bug and might be refined later |
| // -- Test typing in the opening tag symbol |
| mContent = new SpannableStringBuilder("some <del>text</del> < <b>bold text</b>"); |
| |
| mWatcher.onTextChanged(mContent, 21, 0, 1); // Added lone "<" |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(21, mSpanRange.getOpeningTagLoc()); |
| assertEquals(26, mSpanRange.getClosingTagLoc()); |
| |
| |
| // -- Test typing in the tag name |
| mContent = new SpannableStringBuilder("some <del>text</del> <i <b>bold text</b>"); |
| |
| mWatcher.onTextChanged(mContent, 22, 0, 1); |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(21, mSpanRange.getOpeningTagLoc()); |
| assertEquals(27, mSpanRange.getClosingTagLoc()); |
| |
| |
| // -- Test typing in the closing tag symbol |
| mContent = new SpannableStringBuilder("some <del>text</del> <i> <b>bold text</b>"); |
| |
| mWatcher.onTextChanged(mContent, 23, 0, 1); |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(21, mSpanRange.getOpeningTagLoc()); |
| assertEquals(28, mSpanRange.getClosingTagLoc()); |
| } |
| |
| @Test |
| public void testTypingInLoneClosingSymbol() { |
| // -- Test typing in an isolated closing tag symbol |
| mContent = new SpannableStringBuilder("some text >"); |
| |
| mWatcher.onTextChanged(mContent, 10, 0, 1); |
| mWatcher.afterTextChanged(mContent); |
| |
| // No formatting should be applied/removed |
| assertEquals(false, mUpdateSpansWasCalled); |
| |
| |
| // -- Test typing in an isolated closing tag symbol with surrounding tags |
| mContent = new SpannableStringBuilder("some <b>tex>t</b>"); |
| |
| mWatcher.onTextChanged(mContent, 11, 0, 1); // Added lone ">" |
| mWatcher.afterTextChanged(mContent); |
| |
| // The span in this case will be applied from the start of the previous tag to the end of the next tag |
| assertEquals(5, mSpanRange.getOpeningTagLoc()); |
| assertEquals(17, mSpanRange.getClosingTagLoc()); |
| } |
| |
| @Test |
| public void testTypingInEntity() { |
| // Test with several different cases of pre-existing text |
| String[] previousTextCases = new String[]{"", "plain text", "ρ", |
| "<blockquote>some existing content †</blockquote> "}; |
| for (String initialText : previousTextCases) { |
| int offset = initialText.length(); |
| mUpdateSpansWasCalled = false; |
| |
| // -- Test typing in the entity's opening '&' |
| mContent = new SpannableStringBuilder(initialText + "&"); |
| |
| mWatcher.onTextChanged(mContent, offset, 0, 1); |
| mWatcher.afterTextChanged(mContent); |
| |
| // No formatting should be applied/removed |
| assertEquals(false, mUpdateSpansWasCalled); |
| |
| |
| // -- Test typing in the entity's main text |
| mContent = new SpannableStringBuilder(initialText + "&"); |
| |
| mWatcher.onTextChanged(mContent, offset + 3, 0, 1); |
| mWatcher.afterTextChanged(mContent); |
| |
| // No formatting should be applied/removed |
| assertEquals(false, mUpdateSpansWasCalled); |
| |
| |
| // -- Test typing in the entity's closing ';' |
| mContent = new SpannableStringBuilder(initialText + "&"); |
| |
| mWatcher.onTextChanged(mContent, offset + 4, 0, 1); |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(offset, mSpanRange.getOpeningTagLoc()); |
| assertEquals(offset + 5, mSpanRange.getClosingTagLoc()); |
| } |
| } |
| |
| @Test |
| public void testAddingTagFromFormatBar() { |
| // -- Test adding a tag to an empty document |
| mContent = new SpannableStringBuilder("<b>"); |
| |
| mWatcher.onTextChanged(mContent, 0, 0, 3); |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(0, mSpanRange.getOpeningTagLoc()); |
| assertEquals(3, mSpanRange.getClosingTagLoc()); |
| |
| |
| // -- Test adding a tag at the end of a document with text |
| mContent = new SpannableStringBuilder("stuff<b>"); |
| |
| mWatcher.onTextChanged(mContent, 5, 0, 3); |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(5, mSpanRange.getOpeningTagLoc()); |
| assertEquals(8, mSpanRange.getClosingTagLoc()); |
| |
| |
| // -- Test adding a tag at the end of a document containing other html |
| mContent = new SpannableStringBuilder("some text <i>italics</i> <b>"); |
| |
| mWatcher.onTextChanged(mContent, 25, 0, 3); // Added "<b>" |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(25, mSpanRange.getOpeningTagLoc()); |
| assertEquals(28, mSpanRange.getClosingTagLoc()); |
| |
| |
| // -- Test adding a tag at the start of a document with text |
| mContent = new SpannableStringBuilder("<b>some text"); |
| |
| mWatcher.onTextChanged(mContent, 0, 0, 3); |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(0, mSpanRange.getOpeningTagLoc()); |
| assertEquals(3, mSpanRange.getClosingTagLoc()); |
| |
| |
| // -- Test adding a tag at the start of a document containing other html |
| mContent = new SpannableStringBuilder("<b>some text <i>italics</i>"); |
| |
| mWatcher.onTextChanged(mContent, 0, 0, 3); |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(0, mSpanRange.getOpeningTagLoc()); |
| assertEquals(3, mSpanRange.getClosingTagLoc()); |
| |
| |
| // -- Test adding a tag within another tag pair |
| mContent = new SpannableStringBuilder("<b>some <i>text</b>"); |
| |
| mWatcher.onTextChanged(mContent, 8, 0, 3); // Added <i> |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(8, mSpanRange.getOpeningTagLoc()); |
| assertEquals(11, mSpanRange.getClosingTagLoc()); |
| |
| |
| // -- Test adding a closing tag within another tag pair |
| mContent = new SpannableStringBuilder("<b>some <i>text</i></b>"); |
| |
| mWatcher.onTextChanged(mContent, 15, 0, 4); // Added "</i>" |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(15, mSpanRange.getOpeningTagLoc()); |
| assertEquals(19, mSpanRange.getClosingTagLoc()); |
| } |
| |
| @Test |
| public void testAddingListTagsFromFormatBar() { |
| // -- Test adding a list tag to an empty document |
| mContent = new SpannableStringBuilder("<ul>\n\t<li>"); |
| |
| mWatcher.onTextChanged(mContent, 0, 0, 10); |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(0, mSpanRange.getOpeningTagLoc()); |
| assertEquals(10, mSpanRange.getClosingTagLoc()); |
| |
| |
| // -- Test adding a closing list tag |
| mContent = new SpannableStringBuilder("<ul>\n" + //5 |
| "\t<li>list item</li>\n" + //20 |
| "\t<li>another list item</li>\n" + //22 |
| "</ul>"); |
| |
| mWatcher.onTextChanged(mContent, 47, 0, 11); // Added "</li>\n</ul>" |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(47, mSpanRange.getOpeningTagLoc()); |
| assertEquals(58, mSpanRange.getClosingTagLoc()); |
| } |
| |
| @Test |
| public void testDeletingPartsOfTag() { |
| // -- Test deleting different characters within a tag |
| mContent = new SpannableStringBuilder("<b>stuff</b>"); |
| |
| int deletedChar = 0; |
| mWatcher.beforeTextChanged(mContent, deletedChar, 1, 0); |
| // Deleted characters are removed from the string between beforeTextChanged() and onTextChanged() |
| mContent.delete(deletedChar, deletedChar + 1); |
| mWatcher.onTextChanged(mContent, deletedChar, 1, 0); |
| mWatcher.afterTextChanged(mContent); |
| |
| // "b>" should be re-styled |
| assertEquals(0, mSpanRange.getOpeningTagLoc()); |
| assertEquals(2, mSpanRange.getClosingTagLoc()); |
| |
| for (int i = 8; i < 12; i++) { |
| mContent = new SpannableStringBuilder("<b>stuff</b>"); |
| |
| mWatcher.beforeTextChanged(mContent, i, 1, 0); |
| mContent.delete(i, i + 1); |
| mWatcher.afterTextChanged(mContent); |
| |
| // Style should be updated starting from the end of 'stuff' |
| assertEquals(8, mSpanRange.getOpeningTagLoc()); |
| assertEquals(mContent.length(), mSpanRange.getClosingTagLoc()); |
| } |
| } |
| |
| @Test |
| public void testPasteTagPair() { |
| // -- Test pasting in a set of opening and closing tags at the end of the document |
| mContent = new SpannableStringBuilder("text <b></b>"); |
| |
| mWatcher.onTextChanged(mContent, 5, 0, 7); |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(5, mSpanRange.getOpeningTagLoc()); |
| assertEquals(12, mSpanRange.getClosingTagLoc()); |
| } |
| |
| @Test |
| public void testCutAndPasteTagPart() { |
| // -- Test cutting a tag and part of another tag from the document |
| mContent = new SpannableStringBuilder("test <b></b> <i>italics</i>"); |
| |
| mWatcher.beforeTextChanged(mContent, 5, 4, 0); // Deleted "<b><" |
| mContent.delete(5, 9); |
| mWatcher.onTextChanged(mContent, 5, 4, 0); |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(5, mSpanRange.getOpeningTagLoc()); |
| assertEquals(8, mSpanRange.getClosingTagLoc()); |
| |
| |
| // -- Test pasting the cut text back in |
| mContent = new SpannableStringBuilder("test <b></b> <i>italics</i>"); |
| mWatcher.onTextChanged(mContent, 5, 0, 4); // Pasted "<b><" back in |
| mWatcher.afterTextChanged(mContent); |
| |
| assertEquals(5, mSpanRange.getOpeningTagLoc()); |
| assertEquals(12, mSpanRange.getClosingTagLoc()); |
| } |
| |
| @Test |
| public void testCutAndPasteTagPartReplacingText() { |
| // -- Test pasting cut text while text is selected |
| // Pasted "<b><", replacing "st " of "test " |
| mContent = new SpannableStringBuilder("test /b> <i>italics</i>"); |
| mWatcher.beforeTextChanged(mContent, 2, 3, 4); |
| mContent = new SpannableStringBuilder("te<b></b> <i>italics</i>"); |
| mWatcher.onTextChanged(mContent, 2, 3, 4); |
| mWatcher.afterTextChanged(mContent); |
| |
| // Should re-style whole document |
| assertEquals(0, mSpanRange.getOpeningTagLoc()); |
| assertEquals(mContent.length(), mSpanRange.getClosingTagLoc()); |
| |
| |
| // -- Test pasting cut text while text is selected, case 2 |
| // Pasted "i>", replacing "test " |
| mContent = new SpannableStringBuilder("<test italics</i>"); |
| mWatcher.beforeTextChanged(mContent, 1, 5, 2); |
| mContent = new SpannableStringBuilder("<i>italics</i>"); |
| mWatcher.onTextChanged(mContent, 1, 5, 2); |
| mWatcher.afterTextChanged(mContent); |
| |
| // Should re-style whole document |
| assertEquals(0, mSpanRange.getOpeningTagLoc()); |
| assertEquals(mContent.length(), mSpanRange.getClosingTagLoc()); |
| } |
| |
| @Test |
| public void testNoChange() { |
| |
| mWatcher.beforeTextChanged("sample", 0, 0, 0); |
| mWatcher.onTextChanged("sample", 0, 0, 0); |
| mWatcher.afterTextChanged(null); |
| |
| // No formatting should be applied/removed |
| assertEquals(false, mUpdateSpansWasCalled); |
| } |
| |
| @Test |
| public void testUpdateSpans() { |
| // -- Test tag styling |
| HtmlStyleTextWatcher watcher = new HtmlStyleTextWatcher(); |
| Spannable content = new SpannableStringBuilder("<b>stuff</b>"); |
| watcher.updateSpans(content, new HtmlStyleTextWatcher.SpanRange(0, 3)); |
| |
| assertEquals(1, content.getSpans(0, 3, ForegroundColorSpan.class).length); |
| |
| // -- Test entity styling |
| content = new SpannableStringBuilder("text & more text"); |
| watcher.updateSpans(content, new HtmlStyleTextWatcher.SpanRange(5, 10)); |
| |
| assertEquals(1, content.getSpans(5, 10, ForegroundColorSpan.class).length); |
| assertEquals(1, content.getSpans(5, 10, StyleSpan.class).length); |
| assertEquals(1, content.getSpans(5, 10, RelativeSizeSpan.class).length); |
| |
| // -- Test comment styling |
| content = new SpannableStringBuilder("text <!--comment--> more text"); |
| watcher.updateSpans(content, new HtmlStyleTextWatcher.SpanRange(5, 19)); |
| |
| assertEquals(1, content.getSpans(5, 19, ForegroundColorSpan.class).length); |
| assertEquals(1, content.getSpans(5, 19, StyleSpan.class).length); |
| assertEquals(1, content.getSpans(5, 19, RelativeSizeSpan.class).length); |
| |
| content = new SpannableStringBuilder("<b>stuff</b>"); |
| watcher.updateSpans(content, new HtmlStyleTextWatcher.SpanRange(0, 3)); |
| |
| watcher.updateSpans(content, new HtmlStyleTextWatcher.SpanRange(0, 42)); |
| assertEquals(1, content.getSpans(0, 3, ForegroundColorSpan.class).length); |
| |
| } |
| |
| private class HtmlStyleTextWatcherForTests extends HtmlStyleTextWatcher { |
| @Override |
| protected void updateSpans(Spannable s, SpanRange spanRange) { |
| mSpanRange = spanRange; |
| mUpdateSpansWasCalled = true; |
| } |
| } |
| } |