// 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.ui.TooltipTest'); goog.setTestOnly('goog.ui.TooltipTest'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); goog.require('goog.events.Event'); goog.require('goog.events.EventHandler'); goog.require('goog.events.EventType'); goog.require('goog.events.FocusHandler'); goog.require('goog.html.testing'); goog.require('goog.math.Coordinate'); goog.require('goog.positioning.AbsolutePosition'); goog.require('goog.style'); goog.require('goog.testing.MockClock'); goog.require('goog.testing.TestQueue'); goog.require('goog.testing.events'); goog.require('goog.testing.jsunit'); goog.require('goog.ui.PopupBase'); goog.require('goog.ui.Tooltip'); goog.require('goog.userAgent'); /** * A subclass of Tooltip that overrides {@code getPositioningStrategy} * for testing purposes. * @constructor */ function TestTooltip(el, text, dom) { goog.ui.Tooltip.call(this, el, text, dom); } goog.inherits(TestTooltip, goog.ui.Tooltip); /** @override */ TestTooltip.prototype.getPositioningStrategy = function() { return new goog.positioning.AbsolutePosition(13, 17); }; var tt, clock, handler, eventQueue, dom; // Allow positions to be off by one in gecko as it reports scrolling // offsets in steps of 2. var ALLOWED_OFFSET = goog.userAgent.GECKO ? 1 : 0; function setUp() { // We get access denied error when accessing the iframe in IE on the farm // as IE doesn't have the same window size issues as firefox on the farm // we bypass the iframe and use the current document instead. if (goog.userAgent.EDGE_OR_IE) { dom = goog.dom.getDomHelper(document); } else { var frame = document.getElementById('testframe'); var doc = goog.dom.getFrameContentDocument(frame); dom = goog.dom.getDomHelper(doc); } // Host elements in fixed size iframe to avoid window size problems when // running under Selenium. dom.getDocument().body.innerHTML = '

Content

' + '

Hover Here For Popup

' + '

Secondary target

'; tt = new goog.ui.Tooltip(undefined, undefined, dom); tt.setElement( dom.createDom( goog.dom.TagName.DIV, {id: 'popup', style: 'visibility:hidden'}, 'Hello')); clock = new goog.testing.MockClock(true); eventQueue = new goog.testing.TestQueue(); handler = new goog.events.EventHandler(eventQueue); handler.listen(tt, goog.ui.PopupBase.EventType.SHOW, eventQueue.enqueue); handler.listen(tt, goog.ui.PopupBase.EventType.HIDE, eventQueue.enqueue); } function tearDown() { // tooltip needs to be hidden as well as disposed of so that it doesn't // leave global state hanging around to trip up other tests. tt.onHide(); tt.dispose(); clock.uninstall(); handler.removeAll(); } function testConstructor() { var element = tt.getElement(); assertNotNull('Tooltip should have non-null element', element); assertEquals( 'Tooltip element should be the DIV we created', dom.getElement('popup'), element); assertEquals( 'Tooltip element should be a child of the document body', dom.getDocument().body, element.parentNode); } function testTooltipShowsAndHides() { var hoverTarget = dom.getElement('hovertarget'); var elsewhere = dom.getElement('notpopup'); var element = tt.getElement(); var position = new goog.math.Coordinate(5, 5); assertNotNull('Tooltip should have non-null element', element); assertEquals( 'Initial state should be inactive', goog.ui.Tooltip.State.INACTIVE, tt.getState()); tt.attach(hoverTarget); tt.setShowDelayMs(100); tt.setHideDelayMs(50); goog.testing.events.fireMouseOverEvent(hoverTarget, elsewhere, position); assertEquals(goog.ui.Tooltip.State.WAITING_TO_SHOW, tt.getState()); clock.tick(101); assertEquals('visible', tt.getElement().style.visibility); assertEquals( 'tooltip y position (10px margin below the cursor)', '15px', tt.getElement().style.top); assertEquals(goog.ui.Tooltip.State.SHOWING, tt.getState()); assertEquals(goog.ui.PopupBase.EventType.SHOW, eventQueue.dequeue().type); assertTrue(eventQueue.isEmpty()); goog.testing.events.fireMouseOutEvent(hoverTarget, elsewhere); assertEquals(goog.ui.Tooltip.State.WAITING_TO_HIDE, tt.getState()); clock.tick(51); assertEquals('hidden', tt.getElement().style.visibility); assertEquals(goog.ui.Tooltip.State.INACTIVE, tt.getState()); assertEquals(goog.ui.PopupBase.EventType.HIDE, eventQueue.dequeue().type); assertTrue(eventQueue.isEmpty()); } function testMultipleTargets() { var firstTarget = dom.getElement('hovertarget'); var secondTarget = dom.getElement('second'); var elsewhere = dom.getElement('notpopup'); var element = tt.getElement(); tt.attach(firstTarget); tt.attach(secondTarget); tt.setShowDelayMs(100); tt.setHideDelayMs(50); // Move over first target goog.testing.events.fireMouseOverEvent(firstTarget, elsewhere); clock.tick(101); assertEquals(goog.ui.PopupBase.EventType.SHOW, eventQueue.dequeue().type); assertTrue(eventQueue.isEmpty()); // Move from first to second goog.testing.events.fireMouseOutEvent(firstTarget, secondTarget); goog.testing.events.fireMouseOverEvent(secondTarget, firstTarget); assertEquals(goog.ui.Tooltip.State.UPDATING, tt.getState()); assertTrue(eventQueue.isEmpty()); // Move from second to element (before second shows) goog.testing.events.fireMouseOutEvent(secondTarget, element); goog.testing.events.fireMouseOverEvent(element, secondTarget); assertEquals(goog.ui.Tooltip.State.SHOWING, tt.getState()); assertTrue(eventQueue.isEmpty()); // Move from element to second, and let it show goog.testing.events.fireMouseOutEvent(element, secondTarget); goog.testing.events.fireMouseOverEvent(secondTarget, element); assertEquals(goog.ui.Tooltip.State.UPDATING, tt.getState()); clock.tick(101); assertEquals(goog.ui.Tooltip.State.SHOWING, tt.getState()); assertEquals('Anchor should be second target', secondTarget, tt.anchor); assertEquals(goog.ui.PopupBase.EventType.HIDE, eventQueue.dequeue().type); assertEquals(goog.ui.PopupBase.EventType.SHOW, eventQueue.dequeue().type); assertTrue(eventQueue.isEmpty()); // Move from second to first and then off without first showing goog.testing.events.fireMouseOutEvent(secondTarget, firstTarget); goog.testing.events.fireMouseOverEvent(firstTarget, secondTarget); assertEquals(goog.ui.Tooltip.State.UPDATING, tt.getState()); goog.testing.events.fireMouseOutEvent(firstTarget, elsewhere); assertEquals(goog.ui.Tooltip.State.WAITING_TO_HIDE, tt.getState()); clock.tick(51); assertEquals('hidden', tt.getElement().style.visibility); assertEquals(goog.ui.Tooltip.State.INACTIVE, tt.getState()); assertEquals(goog.ui.PopupBase.EventType.HIDE, eventQueue.dequeue().type); assertTrue(eventQueue.isEmpty()); clock.tick(200); // Move from element to second, but detach second before it shows. goog.testing.events.fireMouseOutEvent(element, secondTarget); goog.testing.events.fireMouseOverEvent(secondTarget, element); assertEquals(goog.ui.Tooltip.State.WAITING_TO_SHOW, tt.getState()); tt.detach(secondTarget); clock.tick(200); assertEquals(goog.ui.Tooltip.State.INACTIVE, tt.getState()); assertEquals('Anchor should be second target', secondTarget, tt.anchor); assertTrue(eventQueue.isEmpty()); } function testRequireInteraction() { var hoverTarget = dom.getElement('hovertarget'); var elsewhere = dom.getElement('notpopup'); tt.attach(hoverTarget); tt.setShowDelayMs(100); tt.setHideDelayMs(50); tt.setRequireInteraction(true); goog.testing.events.fireMouseOverEvent(hoverTarget, elsewhere); clock.tick(101); assertEquals( 'Tooltip should not show without mouse move event', 'hidden', tt.getElement().style.visibility); goog.testing.events.fireMouseMoveEvent(hoverTarget); goog.testing.events.fireMouseOverEvent(hoverTarget, elsewhere); clock.tick(101); assertEquals( 'Tooltip should show because we had mouse move event', 'visible', tt.getElement().style.visibility); goog.testing.events.fireMouseOutEvent(hoverTarget, elsewhere); clock.tick(51); assertEquals('hidden', tt.getElement().style.visibility); goog.testing.events.fireBrowserEvent( new goog.events.Event(goog.events.EventType.FOCUS, hoverTarget)); clock.tick(101); assertEquals( 'Tooltip should show because we had focus event', 'visible', tt.getElement().style.visibility); goog.testing.events.fireBrowserEvent( new goog.events.Event(goog.events.EventType.BLUR, hoverTarget)); clock.tick(51); assertEquals('hidden', tt.getElement().style.visibility); goog.testing.events.fireMouseMoveEvent(hoverTarget); goog.testing.events.fireMouseOverEvent(hoverTarget, elsewhere); goog.testing.events.fireMouseOutEvent(hoverTarget, elsewhere); goog.testing.events.fireMouseOverEvent(hoverTarget, elsewhere); clock.tick(101); assertEquals( 'A cancelled trigger should also cancel the seen interaction', 'hidden', tt.getElement().style.visibility); } function testDispose() { var element = tt.getElement(); tt.dispose(); assertTrue('Tooltip should have been disposed of', tt.isDisposed()); assertNull( 'Tooltip element reference should have been nulled out', tt.getElement()); assertNotEquals( 'Tooltip element should not be a child of the body', document.body, element.parentNode); } function testNested() { var ttNested; tt.getElement().appendChild( dom.createDom(goog.dom.TagName.SPAN, {id: 'nested'}, 'Goodbye')); ttNested = new goog.ui.Tooltip(undefined, undefined, dom); ttNested.setElement( dom.createDom(goog.dom.TagName.DIV, {id: 'nestedPopup'}, 'hi')); tt.setShowDelayMs(100); tt.setHideDelayMs(50); ttNested.setShowDelayMs(75); ttNested.setHideDelayMs(25); var nestedAnchor = dom.getElement('nested'); var hoverTarget = dom.getElement('hovertarget'); var outerTooltip = dom.getElement('popup'); var innerTooltip = dom.getElement('nestedPopup'); var elsewhere = dom.getElement('notpopup'); ttNested.attach(nestedAnchor); tt.attach(hoverTarget); // Test mouse into, out of nested tooltip goog.testing.events.fireMouseOverEvent(hoverTarget, elsewhere); clock.tick(101); goog.testing.events.fireMouseOutEvent(hoverTarget, outerTooltip); goog.testing.events.fireMouseOverEvent(outerTooltip, hoverTarget); clock.tick(51); assertEquals('visible', tt.getElement().style.visibility); goog.testing.events.fireMouseOutEvent(outerTooltip, nestedAnchor); goog.testing.events.fireMouseOverEvent(nestedAnchor, outerTooltip); clock.tick(76); assertEquals('visible', tt.getElement().style.visibility); assertEquals('visible', ttNested.getElement().style.visibility); goog.testing.events.fireMouseOutEvent(nestedAnchor, outerTooltip); goog.testing.events.fireMouseOverEvent(outerTooltip, nestedAnchor); clock.tick(100); assertEquals('visible', tt.getElement().style.visibility); assertEquals('hidden', ttNested.getElement().style.visibility); // Go back in nested tooltip and then out through tooltip element. goog.testing.events.fireMouseOutEvent(outerTooltip, nestedAnchor); goog.testing.events.fireMouseOverEvent(nestedAnchor, outerTooltip); clock.tick(76); goog.testing.events.fireMouseOutEvent(nestedAnchor, innerTooltip); goog.testing.events.fireMouseOverEvent(innerTooltip, nestedAnchor); clock.tick(15); assertEquals('visible', tt.getElement().style.visibility); assertEquals('visible', ttNested.getElement().style.visibility); goog.testing.events.fireMouseOutEvent(innerTooltip, elsewhere); clock.tick(26); assertEquals('hidden', ttNested.getElement().style.visibility); clock.tick(51); assertEquals('hidden', tt.getElement().style.visibility); // Test with focus goog.testing.events.fireBrowserEvent( new goog.events.Event(goog.events.EventType.FOCUS, hoverTarget)); clock.tick(101); goog.testing.events.fireBrowserEvent( new goog.events.Event(goog.events.EventType.BLUR, hoverTarget)); goog.testing.events.fireBrowserEvent( new goog.events.Event(goog.events.EventType.FOCUS, nestedAnchor)); clock.tick(76); assertEquals('visible', tt.getElement().style.visibility); assertEquals('visible', ttNested.getElement().style.visibility); goog.testing.events.fireBrowserEvent( new goog.events.Event(goog.events.EventType.BLUR, nestedAnchor)); goog.testing.events.fireBrowserEvent( new goog.events.Event(goog.events.EventType.FOCUS, hoverTarget)); clock.tick(26); assertEquals('visible', tt.getElement().style.visibility); assertEquals('hidden', ttNested.getElement().style.visibility); ttNested.onHide(); ttNested.dispose(); } function testPosition() { dom.getDocument().body.style.paddingBottom = '150%'; // force scrollbar var scrollEl = dom.getDocumentScrollElement(); var anchor = dom.getElement('hovertarget'); var tooltip = new goog.ui.Tooltip(anchor, 'foo'); tooltip.getElement().style.position = 'absolute'; tooltip.cursorPosition.x = 100; tooltip.cursorPosition.y = 100; tooltip.showForElement(anchor); assertEquals( 'Tooltip should be at cursor position', '(110, 110)', // (100, 100) + padding (10, 10) goog.style.getPageOffset(tooltip.getElement()).toString()); scrollEl.scrollTop = 50; var offset = goog.style.getPageOffset(tooltip.getElement()); assertTrue( 'Tooltip should be at cursor position when scrolled', Math.abs(offset.x - 110) <= ALLOWED_OFFSET); // 100 + padding 10 assertTrue( 'Tooltip should be at cursor position when scrolled', Math.abs(offset.y - 110) <= ALLOWED_OFFSET); // 100 + padding 10 tooltip.dispose(); dom.getDocument().body.style.paddingTop = ''; scrollEl.scrollTop = 0; } function testPositionOverride() { var anchor = dom.getElement('hovertarget'); var tooltip = new TestTooltip(anchor, 'foo', dom); tooltip.showForElement(anchor); assertEquals( 'Tooltip should be at absolute position', '(13, 17)', goog.style.getPageOffset(tooltip.getElement()).toString()); tooltip.dispose(); } function testHtmlContent() { tt.setSafeHtml( goog.html.testing.newSafeHtmlForTest( 'Hello')); var spanEl = goog.dom.getElementByClass('theSpan', tt.getElement()); assertEquals('Hello', goog.dom.getTextContent(spanEl)); } function testSetElementNull() { tt.setElement(null); } function testFocusBlurElementsInTooltip() { var anchorEl = dom.getElement('hovertarget'); goog.dom.setFocusableTabIndex(anchorEl, true); tt.attach(anchorEl); goog.testing.events.fireFocusEvent(anchorEl); clock.tick(1000); assertEquals('visible', tt.getElement().style.visibility); goog.testing.events.fireBlurEvent(anchorEl); tt.tooltipFocusHandler_.dispatchEvent( goog.events.FocusHandler.EventType.FOCUSIN); clock.tick(1000); assertEquals('visible', tt.getElement().style.visibility); // Run blur on the previous element followed by focus on the element being // focused, as would normally happen when focus() is called on an element. tt.tooltipFocusHandler_.dispatchEvent( goog.events.FocusHandler.EventType.FOCUSOUT); tt.tooltipFocusHandler_.dispatchEvent( goog.events.FocusHandler.EventType.FOCUSIN); clock.tick(1000); assertEquals('visible', tt.getElement().style.visibility); tt.tooltipFocusHandler_.dispatchEvent( goog.events.FocusHandler.EventType.FOCUSOUT); clock.tick(1000); assertEquals('hidden', tt.getElement().style.visibility); } function testFocusElementInTooltipThenBackToAnchor() { var anchorEl = dom.getElement('hovertarget'); goog.dom.setFocusableTabIndex(anchorEl, true); tt.attach(anchorEl); goog.testing.events.fireFocusEvent(anchorEl); clock.tick(1000); assertEquals('visible', tt.getElement().style.visibility); // Run blur on the previous element followed by focus on the element being // focused, as would normally happen when focus() is called on an element. goog.testing.events.fireBlurEvent(anchorEl); tt.tooltipFocusHandler_.dispatchEvent( goog.events.FocusHandler.EventType.FOCUSIN); clock.tick(1000); assertEquals('visible', tt.getElement().style.visibility); // Run blur on the previous element followed by focus on the element being // focused, as would normally happen when focus() is called on an element. tt.tooltipFocusHandler_.dispatchEvent( goog.events.FocusHandler.EventType.FOCUSOUT); goog.testing.events.fireFocusEvent(anchorEl); clock.tick(1000); assertEquals('visible', tt.getElement().style.visibility); goog.testing.events.fireBlurEvent(anchorEl); clock.tick(1000); assertEquals('hidden', tt.getElement().style.visibility); }