text: // 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.EnterHandlerTest');
  goog.setTestOnly('goog.editor.plugins.EnterHandlerTest');
  
  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.Blockquote');
  goog.require('goog.editor.plugins.EnterHandler');
  goog.require('goog.editor.range');
  goog.require('goog.events');
  goog.require('goog.events.KeyCodes');
  goog.require('goog.testing.ExpectedFailures');
  goog.require('goog.testing.MockClock');
  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 field1;
  var field2;
  var firedDelayedChange;
  var firedBeforeChange;
  var clock;
  var container;
  var EXPECTEDFAILURES;
  
  function setUpPage() {
    container = goog.dom.getElement('container');
  }
  
  function setUp() {
    EXPECTEDFAILURES = new goog.testing.ExpectedFailures();
    savedHtml = goog.dom.getElement('root').innerHTML;
    clock = new goog.testing.MockClock(true);
  }
  
  function setUpFields(classnameRequiredToSplitBlockquote) {
    field1 = makeField('field1', classnameRequiredToSplitBlockquote);
    field2 = makeField('field2', classnameRequiredToSplitBlockquote);
  
    field1.makeEditable();
    field2.makeEditable();
  }
  
  function tearDown() {
    clock.dispose();
  
    EXPECTEDFAILURES.handleTearDown();
  
    goog.dom.getElement('root').innerHTML = savedHtml;
  }
  
  function testEnterInNonSetupBlockquote() {
    setUpFields(true);
    resetChangeFlags();
    var prevented = !selectNodeAndHitEnter(field1, 'field1cursor');
    waitForChangeEvents();
    assertChangeFlags();
  
    // make sure there's just one blockquote, and that the text has been deleted.
    var elem = field1.getElement();
    var dom = field1.getEditableDomHelper();
    EXPECTEDFAILURES.expectFailureFor(
        goog.userAgent.OPERA,
        'The blockquote is overwritten with DIV due to CORE-22104 -- Opera ' +
            'overwrites the BLOCKQUOTE ancestor with DIV when doing FormatBlock ' +
            'for DIV');
    try {
      assertEquals(
          'Blockquote should not be split', 1,
          dom.getElementsByTagNameAndClass(
                 goog.dom.TagName.BLOCKQUOTE, null, elem)
              .length);
    } catch (e) {
      EXPECTEDFAILURES.handleException(e);
    }
    assert(
        'Selection should be deleted', -1 == elem.innerHTML.indexOf('selection'));
  
    assertEquals(
        'The event should have been prevented only on webkit', prevented,
        goog.userAgent.WEBKIT);
  }
  
  function testEnterInSetupBlockquote() {
    setUpFields(true);
    resetChangeFlags();
    var prevented = !selectNodeAndHitEnter(field2, 'field2cursor');
    waitForChangeEvents();
    assertChangeFlags();
  
    // make sure there are two blockquotes, and a DIV with nbsp in the middle.
    var elem = field2.getElement();
    var dom = field2.getEditableDomHelper();
    assertEquals(
        'Blockquote should be split', 2,
        dom.getElementsByTagNameAndClass(goog.dom.TagName.BLOCKQUOTE, null, elem)
            .length);
    assert(
        'Selection should be deleted', -1 == elem.innerHTML.indexOf('selection'));
  
    assert(
        'should have div with &nbsp;',
        -1 != elem.innerHTML.indexOf('>' + getNbsp() + '<'));
    assert('event should have been prevented', prevented);
  }
  
  function testEnterInNonSetupBlockquoteWhenClassnameIsNotRequired() {
    setUpFields(false);
  
    resetChangeFlags();
    var prevented = !selectNodeAndHitEnter(field1, 'field1cursor');
    waitForChangeEvents();
    assertChangeFlags();
  
    // make sure there are two blockquotes, and a DIV with nbsp in the middle.
    var elem = field1.getElement();
    var dom = field1.getEditableDomHelper();
    assertEquals(
        'Blockquote should be split', 2,
        dom.getElementsByTagNameAndClass(goog.dom.TagName.BLOCKQUOTE, null, elem)
            .length);
    assert(
        'Selection should be deleted', -1 == elem.innerHTML.indexOf('selection'));
  
    assert(
        'should have div with &nbsp;',
        -1 != elem.innerHTML.indexOf('>' + getNbsp() + '<'));
    assert('event should have been prevented', prevented);
  }
  
  function testEnterInBlockquoteCreatesDivInBrMode() {
    setUpFields(true);
    selectNodeAndHitEnter(field2, 'field2cursor');
    var elem = field2.getElement();
    var dom = field2.getEditableDomHelper();
  
    var firstBlockquote = dom.getElementsByTagNameAndClass(
        goog.dom.TagName.BLOCKQUOTE, null, elem)[0];
    var div = dom.getNextElementSibling(firstBlockquote);
    assertEquals('Element after blockquote should be a div', 'DIV', div.tagName);
    assertEquals(
        'Element after div should be second blockquote', 'BLOCKQUOTE',
        dom.getNextElementSibling(div).tagName);
  }
  
  
  /**
   * Tests that breaking after a BR doesn't result in unnecessary newlines.
   * @bug 1471047
   */
  function testEnterInBlockquoteRemovesUnnecessaryBrWithCursorAfterBr() {
    setUpFields(true);
  
    // Assume the following HTML snippet:-
    // <blockquote>one<br>|two<br></blockquote>
    //
    // After enter on the cursor position without the fix, the resulting HTML
    // after the blockquote split was:-
    // <blockquote>one</blockquote>
    // <div>&nbsp;</div>
    // <blockquote><br>two<br></blockquote>
    //
    // This creates the impression on an unnecessary newline. The resulting HTML
    // after the fix is:-
    //
    // <blockquote>one<br></blockquote>
    // <div>&nbsp;</div>
    // <blockquote>two<br></blockquote>
    field1.setHtml(
        false, '<blockquote id="quote" class="tr_bq">one<br>' +
            'two<br></blockquote>');
    var dom = field1.getEditableDomHelper();
    goog.dom.Range.createCaret(dom.getElement('quote'), 2).select();
    goog.testing.events.fireKeySequence(
        field1.getElement(), goog.events.KeyCodes.ENTER);
    var elem = field1.getElement();
    var secondBlockquote = dom.getElementsByTagNameAndClass(
        goog.dom.TagName.BLOCKQUOTE, null, elem)[1];
    assertHTMLEquals('two<br>', secondBlockquote.innerHTML);
  
    // Verifies that a blockquote split doesn't happen if it doesn't need to.
    field1.setHtml(
        false, '<blockquote class="tr_bq">one<br id="brcursor"></blockquote>');
    selectNodeAndHitEnter(field1, 'brcursor');
    assertEquals(
        1,
        dom.getElementsByTagNameAndClass(goog.dom.TagName.BLOCKQUOTE, null, elem)
            .length);
  }
  
  
  /**
   * Tests that breaking in a text node before a BR doesn't result in unnecessary
   * newlines.
   * @bug 1471047
   */
  function testEnterInBlockquoteRemovesUnnecessaryBrWithCursorBeforeBr() {
    setUpFields(true);
  
    // Assume the following HTML snippet:-
    // <blockquote>one|<br>two<br></blockquote>
    //
    // After enter on the cursor position, the resulting HTML should be.
    // <blockquote>one<br></blockquote>
    // <div>&nbsp;</div>
    // <blockquote>two<br></blockquote>
    field1.setHtml(
        false, '<blockquote id="quote" class="tr_bq">one<br>' +
            'two<br></blockquote>');
    var dom = field1.getEditableDomHelper();
    var cursor = dom.getElement('quote').firstChild;
    goog.dom.Range.createCaret(cursor, 3).select();
    goog.testing.events.fireKeySequence(
        field1.getElement(), goog.events.KeyCodes.ENTER);
    var elem = field1.getElement();
    var secondBlockquote = dom.getElementsByTagNameAndClass(
        goog.dom.TagName.BLOCKQUOTE, null, elem)[1];
    assertHTMLEquals('two<br>', secondBlockquote.innerHTML);
  
    // Ensures that standard text node split works as expected with the new
    // change.
    field1.setHtml(
        false, '<blockquote id="quote" class="tr_bq">one<b>two</b><br>');
    cursor = dom.getElement('quote').firstChild;
    goog.dom.Range.createCaret(cursor, 3).select();
    goog.testing.events.fireKeySequence(
        field1.getElement(), goog.events.KeyCodes.ENTER);
    secondBlockquote = dom.getElementsByTagNameAndClass(
        goog.dom.TagName.BLOCKQUOTE, null, elem)[1];
    assertHTMLEquals('<b>two</b><br>', secondBlockquote.innerHTML);
  }
  
  
  /**
   * Tests that pressing enter in a blockquote doesn't create unnecessary
   * DOM subtrees.
   *
   * @bug 1991539
   * @bug 1991392
   */
  function testEnterInBlockquoteRemovesExtraNodes() {
    setUpFields(true);
  
    // Let's assume we have the following DOM structure and the
    // cursor is placed after the first numbered list item "one".
    //
    // <blockquote class="tr_bq">
    //   <div><div>a</div><ol><li>one|</li></div>
    //   <div>two</div>
    // </blockquote>
    //
    // After pressing enter, we have the following structure.
    //
    // <blockquote class="tr_bq">
    //   <div><div>a</div><ol><li>one|</li></div>
    // </blockquote>
    // <div>&nbsp;</div>
    // <blockquote class="tr_bq">
    //   <div><ol><li><span id=""></span></li></ol></div>
    //   <div>two</div>
    // </blockquote>
    //
    // This appears to the user as an empty list. After the fix, the HTML
    // will be
    //
    // <blockquote class="tr_bq">
    //   <div><div>a</div><ol><li>one|</li></div>
    // </blockquote>
    // <div>&nbsp;</div>
    // <blockquote class="tr_bq">
    //   <div>two</div>
    // </blockquote>
    //
    field1.setHtml(
        false, '<blockquote class="tr_bq">' +
            '<div><div>a</div><ol><li id="cursor">one</li></div>' +
            '<div>b</div>' +
            '</blockquote>');
    var dom = field1.getEditableDomHelper();
    goog.dom.Range.createCaret(dom.getElement('cursor').firstChild, 3).select();
    goog.testing.events.fireKeySequence(
        field1.getElement(), goog.events.KeyCodes.ENTER);
    var elem = field1.getElement();
    var secondBlockquote = dom.getElementsByTagNameAndClass(
        goog.dom.TagName.BLOCKQUOTE, null, elem)[1];
    assertHTMLEquals('<div>b</div>', secondBlockquote.innerHTML);
  
    // Ensure that we remove only unnecessary subtrees.
    field1.setHtml(
        false, '<blockquote class="tr_bq">' +
            '<div><span>a</span><div id="cursor">one</div><div>two</div></div>' +
            '<div><span>c</span></div>' +
            '</blockquote>');
    goog.dom.Range.createCaret(dom.getElement('cursor').firstChild, 3).select();
    goog.testing.events.fireKeySequence(
        field1.getElement(), goog.events.KeyCodes.ENTER);
    secondBlockquote = dom.getElementsByTagNameAndClass(
        goog.dom.TagName.BLOCKQUOTE, null, elem)[1];
    var expectedHTML = '<div><div>two</div></div>' +
        '<div><span>c</span></div>';
    assertHTMLEquals(expectedHTML, secondBlockquote.innerHTML);
  
    // Place the cursor in the middle of a line.
    field1.setHtml(
        false, '<blockquote id="quote" class="tr_bq">' +
            '<div>one</div><div>two</div>' +
            '</blockquote>');
    goog.dom.Range.createCaret(dom.getElement('quote').firstChild.firstChild, 1)
        .select();
    goog.testing.events.fireKeySequence(
        field1.getElement(), goog.events.KeyCodes.ENTER);
    var blockquotes =
        dom.getElementsByTagNameAndClass(goog.dom.TagName.BLOCKQUOTE, null, elem);
    assertEquals(2, blockquotes.length);
    assertHTMLEquals('<div>o</div>', blockquotes[0].innerHTML);
    assertHTMLEquals('<div>ne</div><div>two</div>', blockquotes[1].innerHTML);
  }
  
  function testEnterInList() {
    setUpFields(true);
  
    // <enter> in a list should *never* be handled by custom code. Lists are
    // just way too complicated to get right.
    field1.setHtml(false, '<ol><li>hi!<span id="field1cursor"></span></li></ol>');
    if (goog.userAgent.OPERA) {
      // Opera doesn't actually place the selection in the empty span
      // unless we add a text node first.
      var dom = field1.getEditableDomHelper();
      dom.getElement('field1cursor').appendChild(dom.createTextNode(''));
    }
    var prevented = !selectNodeAndHitEnter(field1, 'field1cursor');
    assertFalse('<enter> in a list should not be prevented', prevented);
  }
  
  function testEnterAtEndOfBlockInWebkit() {
    setUpFields(true);
  
    if (goog.userAgent.WEBKIT) {
      field1.setHtml(
          false, '<blockquote>hi!<span id="field1cursor"></span></blockquote>');
  
      var cursor = field1.getEditableDomHelper().getElement('field1cursor');
      goog.editor.range.placeCursorNextTo(cursor, false);
      goog.dom.removeNode(cursor);
  
      var prevented = !goog.testing.events.fireKeySequence(
          field1.getElement(), goog.events.KeyCodes.ENTER);
      waitForChangeEvents();
      assertChangeFlags();
      assert('event should have been prevented', prevented);
  
      // Make sure that the block now has two brs.
      var elem = field1.getElement();
      assertEquals(
          'should have inserted two br tags: ' + elem.innerHTML, 2,
          goog.dom.getElementsByTagNameAndClass(goog.dom.TagName.BR, null, elem)
              .length);
    }
  }
  
  
  /**
   * Tests that deleting a BR that comes right before a block element works.
   * @bug 1471096
   * @bug 2056376
   */
  function testDeleteBrBeforeBlock() {
    setUpFields(true);
  
    // 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);
  
      // We test the case where the BR has a previous sibling which is not
      // a block level element.
      field1.setHtml(false, 'one<br><ul><li>two</li></ul>');
      helper.select(field1.getElement(), 1);  // Between one and BR.
      goog.testing.events.fireKeySequence(
          field1.getElement(), goog.events.KeyCodes.DELETE);
      assertEquals(
          'Should have deleted the <br>', 'one<ul><li>two</li></ul>',
          field1.getElement().innerHTML);
      // Verify that the cursor is placed at the end of the text node "one".
      var range = field1.getRange();
      var focusNode = range.getFocusNode();
      assertTrue('The selected range should be collapsed', range.isCollapsed());
      assertTrue(
          'The focus node should be the text node "one"',
          focusNode.nodeType == goog.dom.NodeType.TEXT &&
              focusNode.data == 'one');
      assertEquals(
          'The focus offset should be at the end of the text node "one"',
          focusNode.length, range.getFocusOffset());
      assertTrue(
          'The next sibling of the focus node should be the UL',
          focusNode.nextSibling &&
              focusNode.nextSibling.tagName == goog.dom.TagName.UL);
  
      // We test the case where the previous sibling of the BR is a block
      // level element.
      field1.setHtml(false, '<div>foo</div><br><div><span>bar</span></div>');
      helper.select(field1.getElement(), 1);  // Before the BR.
      goog.testing.events.fireKeySequence(
          field1.getElement(), goog.events.KeyCodes.DELETE);
      assertEquals(
          'Should have deleted the <br>',
          '<div>foo</div><div><span>bar</span></div>',
          field1.getElement().innerHTML);
      range = field1.getRange();
      assertEquals(
          'The selected range should be contained within the <span>',
          String(goog.dom.TagName.SPAN), range.getContainerElement().tagName);
      assertTrue('The selected range should be collapsed', range.isCollapsed());
      // Verify that the cursor is placed inside the span at the beginning of bar.
      focusNode = range.getFocusNode();
      assertTrue(
          'The focus node should be the text node "bar"',
          focusNode.nodeType == goog.dom.NodeType.TEXT &&
              focusNode.data == 'bar');
      assertEquals(
          'The focus offset should be at the beginning ' +
              'of the text node "bar"',
          0, range.getFocusOffset());
  
      // We test the case where the BR does not have a previous sibling.
      field1.setHtml(false, '<br><ul><li>one</li></ul>');
      helper.select(field1.getElement(), 0);  // Before the BR.
      goog.testing.events.fireKeySequence(
          field1.getElement(), goog.events.KeyCodes.DELETE);
      assertEquals(
          'Should have deleted the <br>', '<ul><li>one</li></ul>',
          field1.getElement().innerHTML);
      range = field1.getRange();
      // Verify that the cursor is placed inside the LI at the text node "one".
      assertEquals(
          'The selected range should be contained within the <li>',
          String(goog.dom.TagName.LI), range.getContainerElement().tagName);
      assertTrue('The selected range should be collapsed', range.isCollapsed());
      focusNode = range.getFocusNode();
      assertTrue(
          'The focus node should be the text node "one"',
          (focusNode.nodeType == goog.dom.NodeType.TEXT &&
           focusNode.data == 'one'));
      assertEquals(
          'The focus offset should be at the beginning of ' +
              'the text node "one"',
          0, range.getFocusOffset());
  
      // Testing deleting a BR followed by a block level element and preceded
      // by a BR.
      field1.setHtml(false, '<br><br><ul><li>one</li></ul>');
      helper.select(field1.getElement(), 1);  // Between the BR's.
      goog.testing.events.fireKeySequence(
          field1.getElement(), goog.events.KeyCodes.DELETE);
      assertEquals(
          'Should have deleted the <br>', '<br><ul><li>one</li></ul>',
          field1.getElement().innerHTML);
      // Verify that the cursor is placed inside the LI at the text node "one".
      range = field1.getRange();
      assertEquals(
          'The selected range should be contained within the <li>',
          String(goog.dom.TagName.LI), range.getContainerElement().tagName);
      assertTrue('The selected range should be collapsed', range.isCollapsed());
      focusNode = range.getFocusNode();
      assertTrue(
          'The focus node should be the text node "one"',
          (focusNode.nodeType == goog.dom.NodeType.TEXT &&
           focusNode.data == 'one'));
      assertEquals(
          'The focus offset should be at the beginning of ' +
              'the text node "one"',
          0, range.getFocusOffset());
    }  // End if GECKO
  }
  
  
  /**
   * Tests that deleting a BR before a blockquote doesn't remove quoted text.
   * @bug 1471075
   */
  function testDeleteBeforeBlockquote() {
    setUpFields(true);
  
    if (goog.userAgent.GECKO) {
      field1.setHtml(
          false, '<br><br><div><br><blockquote>foo</blockquote></div>');
      var helper = new goog.testing.editor.TestHelper(field1.getElement());
      helper.select(field1.getElement(), 0);  // Before the first BR.
      // Fire three deletes in quick succession.
      goog.testing.events.fireKeySequence(
          field1.getElement(), goog.events.KeyCodes.DELETE);
      goog.testing.events.fireKeySequence(
          field1.getElement(), goog.events.KeyCodes.DELETE);
      goog.testing.events.fireKeySequence(
          field1.getElement(), goog.events.KeyCodes.DELETE);
      assertEquals(
          'Should have deleted all the <br>\'s and the blockquote ' +
              'isn\'t affected',
          '<div><blockquote>foo</blockquote></div>',
          field1.getElement().innerHTML);
      var range = field1.getRange();
      assertEquals(
          'The selected range should be contained within the ' +
              '<blockquote>',
          String(goog.dom.TagName.BLOCKQUOTE), range.getContainerElement().tagName);
      assertTrue('The selected range should be collapsed', range.isCollapsed());
      var focusNode = range.getFocusNode();
      assertTrue(
          'The focus node should be the text node "foo"',
          (focusNode.nodeType == goog.dom.NodeType.TEXT &&
           focusNode.data == 'foo'));
      assertEquals(
          'The focus offset should be at the ' +
              'beginning of the text node "foo"',
          0, range.getFocusOffset());
    }
  }
  
  
  /**
   * Tests that deleting a BR is working normally (that the workaround for the
   * bug is not causing double deletes).
   * @bug 1471096
   */
  function testDeleteBrNormal() {
    setUpFields(true);
  
    // 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
  }
  
  
  /**
   * Tests that deleteCursorSelectionW3C_ correctly recognizes visually
   * collapsed selections in Opera even if they contain a <br>.
   * See the deleteCursorSelectionW3C_ comment in enterhandler.js.
   */
  function testCollapsedSelectionKeepsBrOpera() {
    setUpFields(true);
  
    if (goog.userAgent.OPERA) {
      field1.setHtml(false, '<div><br id="pleasedontdeleteme"></div>');
      field1.focus();
      goog.testing.events.fireKeySequence(
          field1.getElement(), goog.events.KeyCodes.ENTER);
      assertNotNull(
          'The <br> must not have been deleted',
          goog.dom.getElement('pleasedontdeleteme'));
    }
  }
  
  
  /**
   * Selects the node at the given id, and simulates an ENTER keypress.
   * @param {goog.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 dom = field.getEditableDomHelper();
    var cursor = dom.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.
   * @return {goog.editor.Field} A field.
   */
  function makeField(id, classnameRequiredToSplitBlockquote) {
    var field = new goog.editor.Field(id);
    field.registerPlugin(new goog.editor.plugins.EnterHandler());
    field.registerPlugin(
        new goog.editor.plugins.Blockquote(classnameRequiredToSplitBlockquote));
  
    goog.events.listen(
        field, goog.editor.Field.EventType.BEFORECHANGE, function() {
          // set the global flag that beforechange was fired.
          firedBeforeChange = true;
        });
    goog.events.listen(
        field, goog.editor.Field.EventType.DELAYEDCHANGE, function() {
          // set the global flag that delayed change was fired.
          firedDelayedChange = true;
        });
  
    return field;
  }
  
  
  /**
   * Reset all the global flags related to change events.
   */
  function resetChangeFlags() {
    waitForChangeEvents();
    firedBeforeChange = firedDelayedChange = false;
  }
  
  
  /**
   * Asserts that both change flags were fired since the last reset.
   */
  function assertChangeFlags() {
    assert('Beforechange should have fired', firedBeforeChange);
    assert('Delayedchange should have fired', firedDelayedChange);
  }
  
  
  /**
   * Wait for delayedchange to propagate.
   */
  function waitForChangeEvents() {
    clock.tick(
        goog.editor.Field.DELAYED_CHANGE_FREQUENCY +
        goog.editor.Field.CHANGE_FREQUENCY);
  }
  
  function getNbsp() {
    // On WebKit (pre-528) and Opera, &nbsp; shows up as its unicode character in
    // innerHTML under some circumstances.
    return (goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher('528')) ||
            goog.userAgent.OPERA ?
        '\u00a0' :
        '&nbsp;';
  }
  
  
  function testPrepareContent() {
    setUpFields(true);
    assertPreparedContents('hi', 'hi');
    assertPreparedContents(
        goog.editor.BrowserFeature.COLLAPSES_EMPTY_NODES ? '<br>' : '', '   ');
  }
  
  
  /**
   * Assert that the prepared contents matches the expected.
   */
  function assertPreparedContents(expected, original) {
    assertEquals(
        expected,
        field1.reduceOp_(goog.editor.Plugin.Op.PREPARE_CONTENTS_HTML, original));
  }
  
  // UTILITY FUNCTION TESTS.
  
  function testDeleteW3CSimple() {
    if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
      container.innerHTML = '<div>abcd</div>';
      var range = goog.dom.Range.createFromNodes(
          container.firstChild.firstChild, 1, container.firstChild.firstChild, 3);
      range.select();
      goog.editor.plugins.EnterHandler.deleteW3cRange_(range);
  
      goog.testing.dom.assertHtmlContentsMatch('<div>ad</div>', container);
    }
  }
  
  function testDeleteW3CAll() {
    if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
      container.innerHTML = '<div>abcd</div>';
      var range = goog.dom.Range.createFromNodes(
          container.firstChild.firstChild, 0, container.firstChild.firstChild, 4);
      range.select();
      goog.editor.plugins.EnterHandler.deleteW3cRange_(range);
  
      goog.testing.dom.assertHtmlContentsMatch('<div>&nbsp;</div>', container);
    }
  }
  
  function testDeleteW3CPartialEnd() {
    if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
      container.innerHTML = '<div>ab</div><div>cd</div>';
      var range = goog.dom.Range.createFromNodes(
          container.firstChild.firstChild, 1, container.lastChild.firstChild, 1);
      range.select();
      goog.editor.plugins.EnterHandler.deleteW3cRange_(range);
  
      goog.testing.dom.assertHtmlContentsMatch('<div>ad</div>', container);
    }
  }
  
  function testDeleteW3CNonPartialEnd() {
    if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
      container.innerHTML = '<div>ab</div><div>cd</div>';
      var range = goog.dom.Range.createFromNodes(
          container.firstChild.firstChild, 1, container.lastChild.firstChild, 2);
      range.select();
      goog.editor.plugins.EnterHandler.deleteW3cRange_(range);
  
      goog.testing.dom.assertHtmlContentsMatch('<div>a</div>', container);
    }
  }
  
  function testIsInOneContainer() {
    if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
      container.innerHTML = '<div><br></div>';
      var div = container.firstChild;
      var range = goog.dom.Range.createFromNodes(div, 0, div, 1);
      range.select();
      assertTrue(
          'Selection must be recognized as being in one container',
          goog.editor.plugins.EnterHandler.isInOneContainerW3c_(range));
    }
  }
  
  function testDeletingEndNodesWithNoNewLine() {
    if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
      container.innerHTML =
          'a<div>b</div><div><br></div><div>c</div><div>d</div>';
      var range = goog.dom.Range.createFromNodes(
          container.childNodes[2], 0, container.childNodes[4].childNodes[0], 1);
      range.select();
      var newRange = goog.editor.plugins.EnterHandler.deleteW3cRange_(range);
      goog.testing.dom.assertHtmlContentsMatch('a<div>b</div>', container);
      assertTrue(newRange.isCollapsed());
      assertEquals(container, newRange.getStartNode());
      assertEquals(2, newRange.getStartOffset());
    }
  }
  