// 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.dom.TagName'); 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('
'), {'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(''), { '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(''), {'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(' ', {}); 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(' ', {}); 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( goog.dom.TagName.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); }