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