// 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.html.testing');
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.setSafeHtml(
      false,
      goog.html.testing.newSafeHtmlForTest(
          '<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.setSafeHtml(
      false,
      goog.html.testing.newSafeHtmlForTest(
          '<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.setSafeHtml(
      false,
      goog.html.testing.newSafeHtmlForTest(
          '<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.setSafeHtml(
      false,
      goog.html.testing.newSafeHtmlForTest(
          '<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.setSafeHtml(
      false,
      goog.html.testing.newSafeHtmlForTest(
          '<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.setSafeHtml(
      false,
      goog.html.testing.newSafeHtmlForTest(
          '<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.setSafeHtml(
      false,
      goog.html.testing.newSafeHtmlForTest(
          '<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.setSafeHtml(
      false,
      goog.html.testing.newSafeHtmlForTest(
          '<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.setSafeHtml(
        false,
        goog.html.testing.newSafeHtmlForTest(
            '<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.setSafeHtml(
        false,
        goog.html.testing.newSafeHtmlForTest('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.setSafeHtml(
        false,
        goog.html.testing.newSafeHtmlForTest('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.setSafeHtml(
        false,
        goog.html.testing.newSafeHtmlForTest(
            '<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.setSafeHtml(
        false,
        goog.html.testing.newSafeHtmlForTest('<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.setSafeHtml(
        false,
        goog.html.testing.newSafeHtmlForTest('<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.setSafeHtml(
        false,
        goog.html.testing.newSafeHtmlForTest(
            '<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.setSafeHtml(
        false, goog.html.testing.newSafeHtmlForTest('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.setSafeHtml(
        false,
        goog.html.testing.newSafeHtmlForTest(
            '<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());
  }
}
