seamlessfield_test.js

// Copyright 2009 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.

/**
 * @fileoverview Trogedit unit tests for goog.editor.SeamlessField.
 *
 * @author nicksantos@google.com (Nick Santos)
 * @suppress {missingProperties} There are many mocks in this unit test,
 *     and the mocks don't fit well in the type system.
 */

/** @suppress {extraProvide} */
goog.provide('goog.editor.seamlessfield_test');

goog.require('goog.dom');
goog.require('goog.dom.DomHelper');
goog.require('goog.dom.Range');
goog.require('goog.editor.BrowserFeature');
goog.require('goog.editor.Field');
goog.require('goog.editor.SeamlessField');
goog.require('goog.events');
goog.require('goog.functions');
goog.require('goog.style');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.MockRange');
goog.require('goog.testing.jsunit');

goog.setTestOnly('seamlessfield_test');

var fieldElem;
var fieldElemClone;

function setUp() {
  fieldElem = goog.dom.getElement('field');
  fieldElemClone = fieldElem.cloneNode(true);
}

function tearDown() {
  fieldElem.parentNode.replaceChild(fieldElemClone, fieldElem);
}

// the following tests check for blended iframe positioning. They really
// only make sense on browsers without contentEditable.
function testBlankField() {
  if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
    assertAttachSeamlessIframeSizesCorrectly(
        initSeamlessField(' ', {}), createSeamlessIframe());
  }
}

function testFieldWithContent() {
  if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
    assertAttachSeamlessIframeSizesCorrectly(
        initSeamlessField('Hi!', {}), createSeamlessIframe());
  }
}

function testFieldWithPadding() {
  if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
    assertAttachSeamlessIframeSizesCorrectly(
        initSeamlessField('Hi!', {'padding': '2px 5px'}),
        createSeamlessIframe());
  }
}

function testFieldWithMargin() {
  if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
    assertAttachSeamlessIframeSizesCorrectly(
        initSeamlessField('Hi!', {'margin': '2px 5px'}),
        createSeamlessIframe());
  }
}

function testFieldWithBorder() {
  if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
    assertAttachSeamlessIframeSizesCorrectly(
        initSeamlessField('Hi!', {'border': '2px 5px'}),
        createSeamlessIframe());
  }
}

function testFieldWithOverflow() {
  if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
    assertAttachSeamlessIframeSizesCorrectly(
        initSeamlessField(['1', '2', '3', '4', '5', '6', '7'].join('<p/>'),
        {'overflow': 'auto', 'position': 'relative', 'height': '20px'}),
        createSeamlessIframe());
    assertEquals(20, fieldElem.offsetHeight);
  }
}

function testFieldWithOverflowAndPadding() {
  if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
    var blendedField = initSeamlessField(
        ['1', '2', '3', '4', '5', '6', '7'].join('<p/>'),
        {
          'overflow': 'auto',
          'position': 'relative',
          'height': '20px',
          'padding': '2px 3px'
        });
    var blendedIframe = createSeamlessIframe();
    assertAttachSeamlessIframeSizesCorrectly(blendedField, blendedIframe);
    assertEquals(24, fieldElem.offsetHeight);
  }
}

function testIframeHeightGrowsOnWrap() {
  if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
    var clock = new goog.testing.MockClock(true);
    var blendedField;
    try {
      blendedField = initSeamlessField('',
          {'border': '1px solid black', 'height': '20px'});
      blendedField.makeEditable();
      blendedField.setHtml(false, 'Content that should wrap after resize.');

      // Ensure that the field was fully loaded and sized before measuring.
      clock.tick(1);

      // Capture starting heights.
      var unwrappedIframeHeight = blendedField.getEditableIframe().offsetHeight;

      // Resize the field such that the text should wrap.
      fieldElem.style.width = '200px';
      blendedField.doFieldSizingGecko();

      // Iframe should grow as a result.
      var wrappedIframeHeight = blendedField.getEditableIframe().offsetHeight;
      assertTrue('Wrapped text should cause iframe to grow - initial height: ' +
          unwrappedIframeHeight + ', wrapped height: ' + wrappedIframeHeight,
          wrappedIframeHeight > unwrappedIframeHeight);
    } finally {
      blendedField.dispose();
      clock.dispose();
    }
  }
}

function testDispatchIframeResizedForWrapperHeight() {
  if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
    var clock = new goog.testing.MockClock(true);
    var blendedField = initSeamlessField('Hi!', {'border': '2px 5px'});
    var iframe = createSeamlessIframe();
    blendedField.attachIframe(iframe);

    var resizeCalled = false;
    goog.events.listenOnce(
        blendedField,
        goog.editor.Field.EventType.IFRAME_RESIZED,
        function() {
          resizeCalled = true;
        });

    try {
      blendedField.makeEditable();
      blendedField.setHtml(false, 'Content that should wrap after resize.');

      // Ensure that the field was fully loaded and sized before measuring.
      clock.tick(1);

      assertFalse('Iframe resize must not be dispatched yet', resizeCalled);

      // Resize the field such that the text should wrap.
      fieldElem.style.width = '200px';
      blendedField.sizeIframeToWrapperGecko_();
      assertTrue('Iframe resize must be dispatched for Wrapper', resizeCalled);
    } finally {
      blendedField.dispose();
      clock.dispose();
    }
  }
}

function testDispatchIframeResizedForBodyHeight() {
  if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
    var clock = new goog.testing.MockClock(true);
    var blendedField = initSeamlessField('Hi!', {'border': '2px 5px'});
    var iframe = createSeamlessIframe();
    blendedField.attachIframe(iframe);

    var resizeCalled = false;
    goog.events.listenOnce(
        blendedField,
        goog.editor.Field.EventType.IFRAME_RESIZED,
        function() {
          resizeCalled = true;
        });

    try {
      blendedField.makeEditable();
      blendedField.setHtml(false, 'Content that should wrap after resize.');

      // Ensure that the field was fully loaded and sized before measuring.
      clock.tick(1);

      assertFalse('Iframe resize must not be dispatched yet', resizeCalled);

      // Resize the field to a different body height.
      var bodyHeight = blendedField.getIframeBodyHeightGecko_();
      blendedField.getIframeBodyHeightGecko_ = function() {
        return bodyHeight + 1;
      };
      blendedField.sizeIframeToBodyHeightGecko_();
      assertTrue('Iframe resize must be dispatched for Body', resizeCalled);
    } finally {
      blendedField.dispose();
      clock.dispose();
    }
  }
}

function testDispatchBlur() {
  if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE &&
      !goog.editor.BrowserFeature.CLEARS_SELECTION_WHEN_FOCUS_LEAVES) {
    var blendedField = initSeamlessField('Hi!', {'border': '2px 5px'});
    var iframe = createSeamlessIframe();
    blendedField.attachIframe(iframe);

    var blurCalled = false;
    goog.events.listenOnce(blendedField, goog.editor.Field.EventType.BLUR,
        function() {
          blurCalled = true;
        });

    var clearSelection = goog.dom.Range.clearSelection;
    var cleared = false;
    var clearedWindow;
    blendedField.editableDomHelper = new goog.dom.DomHelper();
    blendedField.editableDomHelper.getWindow =
        goog.functions.constant(iframe.contentWindow);
    var mockRange = new goog.testing.MockRange();
    blendedField.getRange = function() {
      return mockRange;
    };
    goog.dom.Range.clearSelection = function(opt_window) {
      clearSelection(opt_window);
      cleared = true;
      clearedWindow = opt_window;
    };
    var clock = new goog.testing.MockClock(true);

    mockRange.collapse(true);
    mockRange.select();
    mockRange.$replay();
    blendedField.dispatchBlur();
    clock.tick(1);

    assertTrue('Blur must be dispatched.', blurCalled);
    assertTrue('Selection must be cleared.', cleared);
    assertEquals('Selection must be cleared in iframe',
        iframe.contentWindow, clearedWindow);
    mockRange.$verify();
    clock.dispose();
  }
}

function testSetMinHeight() {
  if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
    var clock = new goog.testing.MockClock(true);
    try {
      var field = initSeamlessField(
          ['1', '2', '3', '4', '5', '6', '7'].join('<p/>'),
          {'position': 'relative', 'height': '60px'});

      // Initially create and size iframe.
      var iframe = createSeamlessIframe();
      field.attachIframe(iframe);
      field.iframeFieldLoadHandler(iframe, '', {});
      // Need to process timeouts set by load handlers.
      clock.tick(1000);

      var normalHeight = goog.style.getSize(iframe).height;

      var delayedChangeCalled = false;
      goog.events.listen(field, goog.editor.Field.EventType.DELAYEDCHANGE,
          function() {
            delayedChangeCalled = true;
          });

      // Test that min height is obeyed.
      field.setMinHeight(30);
      clock.tick(1000);
      assertEquals('Iframe height must match min height.',
          30, goog.style.getSize(iframe).height);
      assertFalse('Setting min height must not cause delayed change event.',
          delayedChangeCalled);

      // Test that min height doesn't shrink field.
      field.setMinHeight(0);
      clock.tick(1000);
      assertEquals(normalHeight, goog.style.getSize(iframe).height);
      assertFalse('Setting min height must not cause delayed change event.',
          delayedChangeCalled);
    } finally {
      field.dispose();
      clock.dispose();
    }
  }
}


/**
 * @bug 1649967 This code used to throw a Javascript error.
 */
function testSetMinHeightWithNoIframe() {
  if (goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
    try {
      var field = initSeamlessField('&nbsp;', {});
      field.makeEditable();
      field.setMinHeight(30);
    } finally {
      field.dispose();
    }
  }
}

function testStartChangeEvents() {
  if (goog.editor.BrowserFeature.USE_MUTATION_EVENTS) {
    var clock = new goog.testing.MockClock(true);

    try {
      var field = initSeamlessField('&nbsp;', {});
      field.makeEditable();

      var changeCalled = false;
      goog.events.listenOnce(field, goog.editor.Field.EventType.CHANGE,
          function() {
            changeCalled = true;
          });

      var delayedChangeCalled = false;
      goog.events.listenOnce(field, goog.editor.Field.EventType.CHANGE,
          function() {
            delayedChangeCalled = true;
          });

      field.stopChangeEvents(true, true);
      if (field.changeTimerGecko_) {
        field.changeTimerGecko_.start();
      }

      field.startChangeEvents();
      clock.tick(1000);

      assertFalse(changeCalled);
      assertFalse(delayedChangeCalled);
    } finally {
      clock.dispose();
      field.dispose();
    }
  }
}

function testManipulateDom() {
  // Test in blended field since that is what fires change events.
  var editableField = initSeamlessField('&nbsp;', {});
  var clock = new goog.testing.MockClock(true);

  var delayedChangeCalled = 0;
  goog.events.listen(editableField, goog.editor.Field.EventType.DELAYEDCHANGE,
      function() {
        delayedChangeCalled++;
      });

  assertFalse(editableField.isLoaded());
  editableField.manipulateDom(goog.nullFunction);
  clock.tick(1000);
  assertEquals('Must not fire delayed change events if field is not loaded.',
      0, delayedChangeCalled);

  editableField.makeEditable();
  var usesIframe = editableField.usesIframe();

  try {
    editableField.manipulateDom(goog.nullFunction);
    clock.tick(1000); // Wait for delayed change to fire.
    assertEquals('By default must fire a single delayed change event.',
        1, delayedChangeCalled);

    editableField.manipulateDom(goog.nullFunction, true);
    clock.tick(1000); // Wait for delayed change to fire.
    assertEquals('Must prevent all delayed change events.',
        1, delayedChangeCalled);

    editableField.manipulateDom(function() {
      this.handleChange();
      this.handleChange();
      if (this.changeTimerGecko_) {
        this.changeTimerGecko_.fire();
      }

      this.dispatchDelayedChange_();
      this.delayedChangeTimer_.fire();
    }, false, editableField);
    clock.tick(1000); // Wait for delayed change to fire.
    assertEquals('Must ignore dispatch delayed change called within func.',
        2, delayedChangeCalled);
  } finally {
    // Ensure we always uninstall the mock clock and dispose of everything.
    editableField.dispose();
    clock.dispose();
  }
}

function testAttachIframe() {
  var blendedField = initSeamlessField('Hi!', {});
  var iframe = createSeamlessIframe();
  try {
    blendedField.attachIframe(iframe);
  } catch (err) {
    fail('Error occurred while attaching iframe.');
  }
}


function createSeamlessIframe() {
  // NOTE(nicksantos): This is a reimplementation of
  // TR_EditableUtil.getIframeAttributes, but untangled for tests, and
  // specifically with what we need for blended mode.
  return goog.dom.createDom('IFRAME',
      { 'frameBorder': '0', 'style': 'padding:0;' });
}


/**
 * Initialize a new editable field for the field id 'field', with the given
 * innerHTML and styles.
 *
 * @param {string} innerHTML html for the field contents.
 * @param {Object} styles Key-value pairs for styles on the field.
 * @return {goog.editor.SeamlessField} The field.
 */
function initSeamlessField(innerHTML, styles) {
  var field = new goog.editor.SeamlessField('field');
  fieldElem.innerHTML = innerHTML;
  goog.style.setStyle(fieldElem, styles);
  return field;
}


/**
 * Make sure that the original field element for the given goog.editor.Field has
 * the same size before and after attaching the given iframe. If this is not
 * true, then the field will fidget while we're initializing the field,
 * and that's not what we want.
 *
 * @param {goog.editor.Field} fieldObj The field.
 * @param {HTMLIFrameElement} iframe The iframe.
 */
function assertAttachSeamlessIframeSizesCorrectly(fieldObj, iframe) {
  var size = goog.style.getSize(fieldObj.getOriginalElement());
  fieldObj.attachIframe(iframe);
  var newSize = goog.style.getSize(fieldObj.getOriginalElement());

  assertEquals(size.width, newSize.width);
  assertEquals(size.height, newSize.height);
}