// Copyright 2008 The Closure Library Authors. All Rights Reserved. // // 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. goog.provide('goog.editor.plugins.TagOnEnterHandlerTest'); goog.setTestOnly('goog.editor.plugins.TagOnEnterHandlerTest'); goog.require('goog.dom'); goog.require('goog.dom.NodeType'); goog.require('goog.dom.Range'); goog.require('goog.dom.TagName'); goog.require('goog.editor.BrowserFeature'); goog.require('goog.editor.Field'); goog.require('goog.editor.Plugin'); goog.require('goog.editor.plugins.TagOnEnterHandler'); goog.require('goog.events.KeyCodes'); goog.require('goog.string.Unicode'); goog.require('goog.testing.dom'); goog.require('goog.testing.editor.TestHelper'); goog.require('goog.testing.events'); goog.require('goog.testing.jsunit'); goog.require('goog.userAgent'); var savedHtml; var editor; var field1; function setUp() { field1 = makeField('field1'); field1.makeEditable(); } /** * Tests that deleting a BR that comes right before a block element works. * @bug 1471096 */ function testDeleteBrBeforeBlock() { // This test only works on Gecko, because it's testing for manual deletion of // BR tags, which is done only for Gecko. For other browsers we fall through // and let the browser do the delete, which can only be tested with a robot // test (see javascript/apps/editor/tests/delete_br_robot.html). if (goog.userAgent.GECKO) { field1.setHtml(false, 'one<br><br><div>two</div>'); var helper = new goog.testing.editor.TestHelper(field1.getElement()); helper.select(field1.getElement(), 2); // Between the two BR's. goog.testing.events.fireKeySequence(field1.getElement(), goog.events.KeyCodes.DELETE); assertEquals('Should have deleted exactly one <br>', 'one<br><div>two</div>', field1.getElement().innerHTML); } // End if GECKO } /** * Tests that deleting a BR is working normally (that the workaround for the * bug is not causing double deletes). * @bug 1471096 */ function testDeleteBrNormal() { // This test only works on Gecko, because it's testing for manual deletion of // BR tags, which is done only for Gecko. For other browsers we fall through // and let the browser do the delete, which can only be tested with a robot // test (see javascript/apps/editor/tests/delete_br_robot.html). if (goog.userAgent.GECKO) { field1.setHtml(false, 'one<br><br><br>two'); var helper = new goog.testing.editor.TestHelper(field1.getElement()); helper.select(field1.getElement(), 2); // Between the first and second BR's. field1.getElement().focus(); goog.testing.events.fireKeySequence(field1.getElement(), goog.events.KeyCodes.DELETE); assertEquals('Should have deleted exactly one <br>', 'one<br><br>two', field1.getElement().innerHTML); } // End if GECKO } /** * Regression test for http://b/1991234 . Tests that when you hit enter and it * creates a blank line with whitespace and a BR, the cursor is placed in the * whitespace text node instead of the BR, otherwise continuing to type will * create adjacent text nodes, which causes browsers to mess up some * execcommands. Fix is in a Gecko-only codepath, thus test runs only for Gecko. * A full test for the entire sequence that reproed the bug is in * javascript/apps/editor/tests/ponenter_robot.html . */ function testEnterCreatesBlankLine() { if (goog.userAgent.GECKO) { field1.setHtml(false, '<p>one <br></p>'); var helper = new goog.testing.editor.TestHelper(field1.getElement()); // Place caret after 'one' but keeping a space and a BR as FF does. helper.select('one ', 3); field1.getElement().focus(); goog.testing.events.fireKeySequence(field1.getElement(), goog.events.KeyCodes.ENTER); var range = field1.getRange(); assertFalse('Selection should not be in BR tag', range.getStartNode().nodeType == goog.dom.NodeType.ELEMENT && range.getStartNode().tagName == goog.dom.TagName.BR); assertEquals('Selection should be in text node to avoid creating adjacent' + ' text nodes', goog.dom.NodeType.TEXT, range.getStartNode().nodeType); var rangeStartNode = goog.dom.Range.createFromNodeContents(range.getStartNode()); assertHTMLEquals('The value of selected text node should be replaced with' + ' ', ' ', rangeStartNode.getHtmlFragment()); } } /** * Regression test for http://b/3051179 . Tests that when you hit enter and it * creates a blank line with a BR and the cursor is placed in P. * Splitting DOM causes to make an empty text node. Then if the cursor is placed * at the text node the cursor is shown at wrong location. * Therefore this test checks that the cursor is not placed at an empty node. * Fix is in a Gecko-only codepath, thus test runs only for Gecko. */ function testEnterNormalizeNodes() { if (goog.userAgent.GECKO) { field1.setHtml(false, '<p>one<br></p>'); var helper = new goog.testing.editor.TestHelper(field1.getElement()); // Place caret after 'one' but keeping a BR as FF does. helper.select('one', 3); field1.getElement().focus(); goog.testing.events.fireKeySequence(field1.getElement(), goog.events.KeyCodes.ENTER); var range = field1.getRange(); assertTrue('Selection should be in P tag', range.getStartNode().nodeType == goog.dom.NodeType.ELEMENT && range.getStartNode().tagName == goog.dom.TagName.P); assertTrue('Selection should be at the head and collapsed', range.getStartOffset() == 0 && range.isCollapsed()); } } /** * Verifies * goog.editor.plugins.TagOnEnterHandler.prototype.handleRegularEnterGecko_ * when we explicitly split anchor elements. This test runs only for Gecko * since this is a Gecko-only codepath. */ function testEnterAtBeginningOfLink() { if (goog.userAgent.GECKO) { field1.setHtml(false, '<a href="/">b<br></a>'); var helper = new goog.testing.editor.TestHelper(field1.getElement()); field1.focusAndPlaceCursorAtStart(); goog.testing.events.fireKeySequence(field1.getElement(), goog.events.KeyCodes.ENTER); helper.assertHtmlMatches( '<p> </p><p><a href="/">b<br></a></p>'); } } /** * Verifies correct handling of pressing enter in an empty list item. */ function testEnterInEmptyListItemInEmptyList() { if (goog.userAgent.GECKO) { field1.setHtml(false, '<ul><li> </li></ul>'); var helper = new goog.testing.editor.TestHelper(field1.getElement()); var li = field1.getElement().getElementsByTagName(goog.dom.TagName.LI)[0]; helper.select(li.firstChild, 0); goog.testing.events.fireKeySequence(field1.getElement(), goog.events.KeyCodes.ENTER); helper.assertHtmlMatches('<p> </p>'); } } function testEnterInEmptyListItemAtBeginningOfList() { if (goog.userAgent.GECKO) { field1.setHtml(false, '<ul style="font-weight: bold">' + '<li> </li>' + '<li>1</li>' + '<li>2</li>' + '</ul>'); var helper = new goog.testing.editor.TestHelper(field1.getElement()); var li = field1.getElement().getElementsByTagName(goog.dom.TagName.LI)[0]; helper.select(li.firstChild, 0); goog.testing.events.fireKeySequence(field1.getElement(), goog.events.KeyCodes.ENTER); helper.assertHtmlMatches( '<p> </p><ul style="font-weight: bold"><li>1</li><li>2</li></ul>'); } } function testEnterInEmptyListItemAtEndOfList() { if (goog.userAgent.GECKO) { field1.setHtml(false, '<ul style="font-weight: bold">' + '<li>1</li>' + '<li>2</li>' + '<li> </li>' + '</ul>'); var helper = new goog.testing.editor.TestHelper(field1.getElement()); var li = field1.getElement().getElementsByTagName(goog.dom.TagName.LI)[2]; helper.select(li.firstChild, 0); goog.testing.events.fireKeySequence(field1.getElement(), goog.events.KeyCodes.ENTER); helper.assertHtmlMatches( '<ul style="font-weight: bold"><li>1</li><li>2</li></ul><p> </p>'); } } function testEnterInEmptyListItemInMiddleOfList() { if (goog.userAgent.GECKO) { field1.setHtml(false, '<ul style="font-weight: bold">' + '<li>1</li>' + '<li> </li>' + '<li>2</li>' + '</ul>'); var helper = new goog.testing.editor.TestHelper(field1.getElement()); var li = field1.getElement().getElementsByTagName(goog.dom.TagName.LI)[1]; helper.select(li.firstChild, 0); goog.testing.events.fireKeySequence(field1.getElement(), goog.events.KeyCodes.ENTER); helper.assertHtmlMatches( '<ul style="font-weight: bold"><li>1</li></ul>' + '<p> </p>' + '<ul style="font-weight: bold"><li>2</li></ul>'); } } function testEnterInEmptyListItemInSublist() { if (goog.userAgent.GECKO) { field1.setHtml(false, '<ul>' + '<li>A</li>' + '<ul style="font-weight: bold">' + '<li>1</li>' + '<li> </li>' + '<li>2</li>' + '</ul>' + '<li>B</li>' + '</ul>'); var helper = new goog.testing.editor.TestHelper(field1.getElement()); var li = field1.getElement().getElementsByTagName(goog.dom.TagName.LI)[2]; helper.select(li.firstChild, 0); goog.testing.events.fireKeySequence(field1.getElement(), goog.events.KeyCodes.ENTER); helper.assertHtmlMatches( '<ul>' + '<li>A</li>' + '<ul style="font-weight: bold"><li>1</li></ul>' + '<li> </li>' + '<ul style="font-weight: bold"><li>2</li></ul>' + '<li>B</li>' + '</ul>'); } } function testEnterInEmptyListItemAtBeginningOfSublist() { if (goog.userAgent.GECKO) { field1.setHtml(false, '<ul>' + '<li>A</li>' + '<ul style="font-weight: bold">' + '<li> </li>' + '<li>1</li>' + '<li>2</li>' + '</ul>' + '<li>B</li>' + '</ul>'); var helper = new goog.testing.editor.TestHelper(field1.getElement()); var li = field1.getElement().getElementsByTagName(goog.dom.TagName.LI)[1]; helper.select(li.firstChild, 0); goog.testing.events.fireKeySequence(field1.getElement(), goog.events.KeyCodes.ENTER); helper.assertHtmlMatches( '<ul>' + '<li>A</li>' + '<li> </li>' + '<ul style="font-weight: bold"><li>1</li><li>2</li></ul>' + '<li>B</li>' + '</ul>'); } } function testEnterInEmptyListItemAtEndOfSublist() { if (goog.userAgent.GECKO) { field1.setHtml(false, '<ul>' + '<li>A</li>' + '<ul style="font-weight: bold">' + '<li>1</li>' + '<li>2</li>' + '<li> </li>' + '</ul>' + '<li>B</li>' + '</ul>'); var helper = new goog.testing.editor.TestHelper(field1.getElement()); var li = field1.getElement().getElementsByTagName(goog.dom.TagName.LI)[3]; helper.select(li.firstChild, 0); goog.testing.events.fireKeySequence(field1.getElement(), goog.events.KeyCodes.ENTER); helper.assertHtmlMatches( '<ul>' + '<li>A</li>' + '<ul style="font-weight: bold"><li>1</li><li>2</li></ul>' + '<li> </li>' + '<li>B</li>' + '</ul>'); } } function testPrepareContentForPOnEnter() { assertPreparedContents('hi', 'hi'); assertPreparedContents( goog.editor.BrowserFeature.COLLAPSES_EMPTY_NODES ? '<p> </p>' : '', ' '); } function testPrepareContentForDivOnEnter() { assertPreparedContents('hi', 'hi', goog.dom.TagName.DIV); assertPreparedContents( goog.editor.BrowserFeature.COLLAPSES_EMPTY_NODES ? '<div><br></div>' : '', ' ', goog.dom.TagName.DIV); } /** * Assert that the prepared contents matches the expected. */ function assertPreparedContents(expected, original, opt_tag) { var field = makeField('field1', opt_tag); field.makeEditable(); assertEquals(expected, field.reduceOp_( goog.editor.Plugin.Op.PREPARE_CONTENTS_HTML, original)); } /** * Selects the node at the given id, and simulates an ENTER keypress. * @param {googe.editor.Field} field The field with the node. * @param {string} id A DOM id. * @return {boolean} Whether preventDefault was called on the event. */ function selectNodeAndHitEnter(field, id) { var cursor = field.getEditableDomHelper().getElement(id); goog.dom.Range.createFromNodeContents(cursor).select(); return goog.testing.events.fireKeySequence( cursor, goog.events.KeyCodes.ENTER); } /** * Creates a field with only the enter handler plugged in, for testing. * @param {string} id A DOM id. * @param {boolean=} opt_tag The block tag to use. Defaults to P. * @return {goog.editor.Field} A field. */ function makeField(id, opt_tag) { var field = new goog.editor.Field(id); field.registerPlugin( new goog.editor.plugins.TagOnEnterHandler(opt_tag || goog.dom.TagName.P)); return field; } /** * Runs a test for splitting the dom. * @param {number} offset Index into the text node to split. * @param {string} firstHalfString What the html of the first half of the DOM * should be. * @param {string} secondHalfString What the html of the 2nd half of the DOM * should be. * @param {boolean} isAppend True if the second half should be appended to the * DOM. * @param {boolean=} opt_goToRoot True if the root argument for splitDom should * be excluded. */ function helpTestSplit_(offset, firstHalfString, secondHalfString, isAppend, opt_goToBody) { var node = document.createElement('div'); node.innerHTML = '<b>begin bold<i>italic</i>end bold</b>'; document.body.appendChild(node); var italic = node.getElementsByTagName('i')[0].firstChild; var splitFn = isAppend ? goog.editor.plugins.TagOnEnterHandler.splitDomAndAppend_ : goog.editor.plugins.TagOnEnterHandler.splitDom_; var secondHalf = splitFn(italic, offset, opt_goToBody ? undefined : node); if (opt_goToBody) { secondHalfString = '<div>' + secondHalfString + '</div>'; } assertEquals('original node should have first half of the html', firstHalfString, node.innerHTML.toLowerCase(). replace(goog.string.Unicode.NBSP, ' ')); assertEquals('new node should have second half of the html', secondHalfString, secondHalf.innerHTML.toLowerCase(). replace(goog.string.Unicode.NBSP, ' ')); if (isAppend) { assertTrue('second half of dom should be the original node\'s next' + 'sibling', node.nextSibling == secondHalf); goog.dom.removeNode(secondHalf); } goog.dom.removeNode(node); } /** * Runs different cases of splitting the DOM. * @param {function(number, string, string)} testFn Function that takes an * offset, firstHalfString and secondHalfString as parameters. */ function splitDomCases_(testFn) { testFn(3, '<b>begin bold<i>ita</i></b>', '<b><i>lic</i>end bold</b>'); testFn(0, '<b>begin bold<i> </i></b>', '<b><i>italic</i>end bold</b>'); testFn(6, '<b>begin bold<i>italic</i></b>', '<b><i> </i>end bold</b>'); } function testSplitDom() { splitDomCases_(function(offset, firstHalfString, secondHalfString) { helpTestSplit_(offset, firstHalfString, secondHalfString, false, true); helpTestSplit_(offset, firstHalfString, secondHalfString, false, false); }); } function testSplitDomAndAppend() { splitDomCases_(function(offset, firstHalfString, secondHalfString) { helpTestSplit_(offset, firstHalfString, secondHalfString, true, false); }); } function testSplitDomAtElement() { var node = document.createElement('div'); node.innerHTML = '<div>abc<br>def</div>'; document.body.appendChild(node); goog.editor.plugins.TagOnEnterHandler.splitDomAndAppend_(node.firstChild, 1, node.firstChild); goog.testing.dom.assertHtmlContentsMatch('<div>abc</div><div><br>def</div>', node); goog.dom.removeNode(node); } function testSplitDomAtElementStart() { var node = document.createElement('div'); node.innerHTML = '<div>abc<br>def</div>'; document.body.appendChild(node); goog.editor.plugins.TagOnEnterHandler.splitDomAndAppend_(node.firstChild, 0, node.firstChild); goog.testing.dom.assertHtmlContentsMatch('<div></div><div>abc<br>def</div>', node); goog.dom.removeNode(node); } function testSplitDomAtChildlessElement() { var node = document.createElement('div'); node.innerHTML = '<div>abc<br>def</div>'; document.body.appendChild(node); var br = node.getElementsByTagName(goog.dom.TagName.BR)[0]; goog.editor.plugins.TagOnEnterHandler.splitDomAndAppend_( br, 0, node.firstChild); goog.testing.dom.assertHtmlContentsMatch('<div>abc</div><div><br>def</div>', node); goog.dom.removeNode(node); } function testReplaceWhiteSpaceWithNbsp() { var node = document.createElement('div'); var textNode = document.createTextNode(''); node.appendChild(textNode); textNode.nodeValue = ' test '; goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_( node.firstChild, true, false); assertHTMLEquals(' test ', node.innerHTML); textNode.nodeValue = ' test '; goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_( node.firstChild, true, false); assertHTMLEquals(' test ', node.innerHTML); textNode.nodeValue = ' test '; goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_( node.firstChild, false, false); assertHTMLEquals(' test ', node.innerHTML); textNode.nodeValue = ' test '; goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_( node.firstChild, false, false); assertHTMLEquals(' test ', node.innerHTML); textNode.nodeValue = ''; goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_( node.firstChild, false, false); assertHTMLEquals(' ', node.innerHTML); textNode.nodeValue = ''; goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_( node.firstChild, false, true); assertHTMLEquals('', node.innerHTML); }