473 lines
15 KiB
JavaScript
473 lines
15 KiB
JavaScript
// 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() {
|
|
goog.events.removeAllNativeListeners();
|
|
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 {
|
|
goog.events.removeAllNativeListeners();
|
|
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(' ', {});
|
|
field.makeEditable();
|
|
field.setMinHeight(30);
|
|
} finally {
|
|
field.dispose();
|
|
goog.events.removeAllNativeListeners();
|
|
}
|
|
}
|
|
}
|
|
|
|
function testStartChangeEvents() {
|
|
if (goog.editor.BrowserFeature.USE_MUTATION_EVENTS) {
|
|
var clock = new goog.testing.MockClock(true);
|
|
|
|
try {
|
|
var field = initSeamlessField(' ', {});
|
|
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(' ', {});
|
|
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);
|
|
}
|