// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License.txt in the project root for license information. // // /// /// /// /// module CorsicaTests { var _ElementResizeInstrument = Helper.require("WinJS/Controls/ElementResizeInstrument/_ElementResizeInstrument")._ElementResizeInstrument; var resizeEvent = _ElementResizeInstrument.EventNames.resize; var readyEvent = _ElementResizeInstrument.EventNames._ready; function disposeAndRemoveElement(element: HTMLElement) { if (element.winControl) { element.winControl.dispose(); } WinJS.Utilities.disposeSubTree(element); if (element.parentElement) { element.parentElement.removeChild(element); } } function awaitInitialResizeEvent(resizeInstrument: WinJS.UI.PrivateElementResizeInstrument): WinJS.Promise { // When the _ElementResizeHandler finishes loading, it will fire an initial resize event. Some tests may want // to account for that to avoid false positives in tests. return new WinJS.Promise((c) => { resizeInstrument.addEventListener(resizeEvent, function handleInitialResize() { resizeInstrument.removeEventListener(resizeEvent, handleInitialResize); c(); }); }); } function allowTimeForAdditionalResizeEvents(): WinJS.Promise { // Helper function used to let enough time pass for a resize event to occur, // usually this is to capture any additional resize events that may have been pending, // particularly when we want to verify that no redundant events will be fired. return WinJS.Promise.timeout(300); } export class ElementResizeInstrumentTests { "use strict"; _element: HTMLElement; _parent: HTMLElement; _child: HTMLElement; _parentInstrument: WinJS.UI.PrivateElementResizeInstrument; _childInstrument: WinJS.UI.PrivateElementResizeInstrument; setUp() { // Setup creates a subTree of two elements "parent" and "child", // and gives each its own _ElementResizeInstrument. LiveUnit.LoggingCore.logComment("In setup"); // Host element for our subTree. this._element = document.createElement("DIV"); // Create two elements (parent & child), each styled with percentage heights & widths and each with its own _ElementResizeInstrument. this._parent = document.createElement("DIV"); this._child = document.createElement("DIV"); this._parent.appendChild(this._child); this._element.appendChild(this._parent); document.body.appendChild(this._element); // Let host element be the nearest positioned ancestor of the parent element this._element.style.cssText = "position: relative; height: 800px; width: 800px;"; // Parent and Child need to be positioned in order to have resizes detected by the resizeInstruments. Not necessary to set CSS offsets. var parentStyleText = "position: relative; width: 65%; maxWidth: inherit; minWidth: inherit; height: 65%; maxHeight: inherit; minHeight: inherit; padding: 0px;"; var childStyleText = parentStyleText; this._parent.id = "parent"; this._parent.style.cssText = parentStyleText; this._parentInstrument = new _ElementResizeInstrument(); this._parent.appendChild(this._parentInstrument.element); this._child.id = "child"; this._child.style.cssText = childStyleText; this._childInstrument = new _ElementResizeInstrument(); this._child.appendChild(this._childInstrument.element); } tearDown() { if (this._element) { disposeAndRemoveElement(this._element) this._element = null; this._parent = null; this._child = null; this._parentInstrument = null; this._childInstrument = null; } } testInitialResizeEvent(complete) { // Verify that an _ElementResizeInstrument will asynchronously fire a "resize" event after it has // been initialized and added to the DOM. // The _ElementResizeInstrument uses an element and its contentWindow to detect resize events in whichever element the // _ElementResizeInstrument is appended to. Some browsers will fire an async "resize" event for the element automatically when // it gets added to the DOM, others won't. In both cases it is up to the _ElementResizeHandler to make sure that an initial async "resize" // event is always fired in all browsers. var parentInstrumentReadyPromise = new WinJS.Promise((c) => { this._parentInstrument.addEventListener(readyEvent, c); this._parentInstrument.addedToDom(); }) var childInstrumentReadyPromise = new WinJS.Promise((c) => { this._childInstrument.addEventListener(readyEvent, c); this._childInstrument.addedToDom(); }) // The ready event is a private event used for unit tests. The ready event fires whenever the _ElementResizeInstrument's underlying // element has successfully loaded and the _ElementResizeInstrument has successfully hooked up a "resize" event listener // to the element's contentWindow. WinJS.Promise .join([ // Verify that everything was hooked up correctly. parentInstrumentReadyPromise, childInstrumentReadyPromise, ]).then(() => { // If everything was hooked up correctly, we expect an initial resize event from both instruments. var parentInstrumentResizePromise = awaitInitialResizeEvent(this._parentInstrument); var childInstrumentResizePromise = awaitInitialResizeEvent(this._childInstrument); return WinJS.Promise .join([ parentInstrumentResizePromise, childInstrumentResizePromise, ]); }).done(complete); } testInitialResizeEventFiresOnlyOnce(complete) { // Verify that in all browsers each _ElementResizeInstrument fires exactly one initial resize event. // The _ElementResizeInstrument uses an element and its contentWindow to detect resize events in whichever element the // _ElementResizeInstrument is appended to. Some browsers will fire an async "resize" event for the element automatically when // it gets added to the DOM, others won't. In both cases it is up to the _ElementResizeHandler to make sure that an initial aysnc "resize" // event is always fired in all browsers. this._parentInstrument.addedToDom(); this._childInstrument.addedToDom(); var expectedResizeCount = 1; var parentResizeCount = 0; var childResizeCount = 0; this._parentInstrument.addEventListener(resizeEvent, () => { parentResizeCount++; }); this._childInstrument.addEventListener(resizeEvent, () => { childResizeCount++; }) allowTimeForAdditionalResizeEvents() .done(() => { LiveUnit.Assert.areEqual(expectedResizeCount, parentResizeCount, "Only 1 resize event should have been detected by the parent instrument"); LiveUnit.Assert.areEqual(expectedResizeCount, childResizeCount, "Only 1 resize event should have been detected by the child instrument"); complete(); }); } testChildElementResize(complete) { // Verifies that when both the parent and child elements have _ElementResizeInstruments, resizing // the child element will trigger child resize events, but will not trigger parent resize events. function parentFailEvent(): void { LiveUnit.Assert.fail("Size changes to the child element should not trigger resize events in the parent element."); } var childStyle = this._child.style; var childResizedCounter = 0; var expectedChildResizeEvents = 7; var childResizedSignal: WinJS._Signal; function childResizeHandler(): void { childResizedCounter++; childResizedSignal.complete(); } this._parentInstrument.addedToDom(); this._childInstrument.addedToDom(); WinJS.Promise .join([ awaitInitialResizeEvent(this._parentInstrument), awaitInitialResizeEvent(this._childInstrument) ]) .then(() => { this._childInstrument.addEventListener(resizeEvent, childResizeHandler); this._parentInstrument.addEventListener(resizeEvent, parentFailEvent); childResizedSignal = new WinJS._Signal(); childStyle.width = "50%"; return childResizedSignal.promise; }) .then(() => { childResizedSignal = new WinJS._Signal(); childStyle.height = "50%"; return childResizedSignal.promise; }) .then(() => { childResizedSignal = new WinJS._Signal(); childStyle.padding = "5px"; return childResizedSignal.promise; }) .then(() => { childResizedSignal = new WinJS._Signal(); childStyle.maxWidth = "40%"; return childResizedSignal.promise; }) .then(() => { childResizedSignal = new WinJS._Signal(); childStyle.maxHeight = "40%"; return childResizedSignal.promise; }) .then(() => { childResizedSignal = new WinJS._Signal(); childStyle.minWidth = "60%"; return childResizedSignal.promise; }) .then(() => { childResizedSignal = new WinJS._Signal(); childStyle.minHeight = "60%"; return childResizedSignal.promise; }).done(function () { LiveUnit.Assert.areEqual(expectedChildResizeEvents, childResizedCounter, "Incorrect number of resize events fired for child element"); complete(); }); } testResizeEventsAreBatched(complete) { var childStyle = this._child.style; var childResizedCounter = 0; var expectedChildResizeEvents = 1; function childResizeHandler(): void { childResizedCounter++; } this._childInstrument.addedToDom(); awaitInitialResizeEvent(this._childInstrument) .then(() => { this._childInstrument.addEventListener(resizeEvent, childResizeHandler); childStyle.width = "50%"; getComputedStyle(this._child); childStyle.height = "50%"; getComputedStyle(this._child); childStyle.padding = "5px"; // Wait long enough to make sure only one resize event was fired. return allowTimeForAdditionalResizeEvents(); }) .done(() => { LiveUnit.Assert.areEqual(expectedChildResizeEvents, childResizedCounter, "Batched 'resize' events should cause the event handler to fire EXACTLY once."); complete(); }); } testParentElementResize(complete) { // Verifies that changes to the dimensions of the parent element trigger resize events for both the parent and the child element. // Test expects child element to be styled with percentage height and width and that both the child element and the parent element // each have their own _ElementResizeInstrument. var parentStyle = this._parent.style; var parentHandlerCounter = 0; var expectedParentHandlerCalls = 2; var parentResizedSignal: WinJS._Signal; function parentResizeHandler(): void { parentHandlerCounter++; parentResizedSignal.complete(); } var childHandlerCounter = 0; var expectedChildHandlerCalls = 2; var childResizedSignal: WinJS._Signal; function childResizeHandler(): void { childHandlerCounter++; childResizedSignal.complete(); } this._parentInstrument.addedToDom(); this._childInstrument.addedToDom(); WinJS.Promise .join([ awaitInitialResizeEvent(this._parentInstrument), awaitInitialResizeEvent(this._childInstrument) ]) .then(() => { this._parentInstrument.addEventListener(resizeEvent, parentResizeHandler); this._childInstrument.addEventListener(resizeEvent, childResizeHandler); parentResizedSignal = new WinJS._Signal(); childResizedSignal = new WinJS._Signal(); parentStyle.height = "50%"; return WinJS.Promise.join([ parentResizedSignal.promise, childResizedSignal.promise, ]); }) .then(() => { parentResizedSignal = new WinJS._Signal(); childResizedSignal = new WinJS._Signal(); parentStyle.width = "50%"; return WinJS.Promise.join([ parentResizedSignal.promise, childResizedSignal.promise, ]); }) .done(() => { LiveUnit.Assert.areEqual(expectedParentHandlerCalls, parentHandlerCounter, "Batched 'resize' events should cause the parent handler to fire EXACTLY once."); LiveUnit.Assert.areEqual(expectedChildHandlerCalls, childHandlerCounter, "Batched 'resize' events should cause the child handler to fire EXACTLY once."); complete(); }); } testDispose(complete) { function parentFailResizeHandler(): void { LiveUnit.Assert.fail("disposed parentIstrument should never fire resize events"); } function childFailResizeHandler(): void { LiveUnit.Assert.fail("disposed childInstrument should never fires resize events"); } // Test disposing parent instrument immediately after addedToDom is called, some browsers may still be loading the element at this point and we want to // make sure that we don't still try to hook the 's content window asyncronously once the finishes loading, if its already been disposed. // Verify that the parent instrument never fires an initial resize event. this._parentInstrument.addEventListener(resizeEvent, parentFailResizeHandler); this._parentInstrument.addedToDom(); this._parentInstrument.dispose(); LiveUnit.Assert.isTrue(this._parentInstrument._disposed); // Test that by disposing the child instrument after it is ready, // it wont fire an initial resize event. new WinJS.Promise((c) => { this._childInstrument.addEventListener(readyEvent, c); this._childInstrument.addedToDom(); }) .then(() => { this._childInstrument.addEventListener(resizeEvent, childFailResizeHandler); this._childInstrument.dispose(); LiveUnit.Assert.isTrue(this._childInstrument._disposed); // Now that both Instruments have been disposed, resizing the parent or child element should no longer fire events this._parent.style.height = "10px"; this._child.style.height = "10px"; // Wait long enough to ensure events aren't being handled. return allowTimeForAdditionalResizeEvents(); }) .done(() => { // Disposing again should not cause any bad behavior this._parentInstrument.dispose(); this._childInstrument.dispose(); complete(); }); } testReAppendToDomAndResizeAsynchronously(complete) { // Make sure that removing and reappending an initialized _ElementResizeInstrument // Doesn't permanently stop our _ElementResizeInstrument from firing resize events. // This test is partially testing the browser to make sure that the "resize" listener // we've added to the element's contentWindow doesn't become permanently // broken if it leaves and renters the DOM. // We understand that right now there is a period of time after the control has // been re-appended into the DOM before it will start responding to size change // events. The period of time varies depending on the browser, presumably this // is because the browser hasn't run layout yet. We expect that developers can call // forceLayout() on any controls using _ElementResizeInstruments to force the control // to respond to size changes during this period of time where resize events are not // fired when size changes are made immediately after appending it to the DOM. var childResizeSignal: WinJS._Signal; function childResizeHandler() { childResizeSignal.complete(); } var parentResizeSignal: WinJS._Signal; function parentResizeHandler() { parentResizeSignal.complete(); } var parent = this._parent; var parentInstrument = this._parentInstrument; var childInstrument = this._childInstrument; parentInstrument.addedToDom(); childInstrument.addedToDom(); WinJS.Promise .join([ awaitInitialResizeEvent(this._parentInstrument), awaitInitialResizeEvent(this._childInstrument) ]) .then(() => { parentInstrument.addEventListener(resizeEvent, parentResizeHandler); childInstrument.addEventListener(resizeEvent, childResizeHandler); // Test both instruments still fire "resize" after re-appending them to the // DOM and then asynchronously updating the width of the parent element. parentResizeSignal = new WinJS._Signal(); childResizeSignal = new WinJS._Signal(); this._element.removeChild(parent); return new WinJS.Promise((c) => { window.requestAnimationFrame(c); }); }) .then(() => { this._element.appendChild(parent); // Timeout long enough for the browser to acknowledge the element is back in the DOM. return WinJS.Promise.timeout(100); }) .then(() => { parent.style.width = "43%" return WinJS.Promise.join([ parentResizeSignal.promise, childResizeSignal.promise, ]); }) .done(() => { complete(); }); } testDisposeDoesntThrowAnException(complete) { // There is an issue in Safari and iOS where the element's contentWindow and // contentDocument properties are no longer accessible and any previous stored references // to either object will have have lost their prototype chains. When we would try to unregister // the contentWindow "resize" handler a DOM exception would be thrown because frame // contentWindows in iOS and Safari don't have add and remove eventListener methods // while they are not in the DOM. https://bugs.webkit.org/show_bug.cgi?id=149251 // Make sure that disposing a fully loaded _ElementResizeInstrument while its no longer in the DOM, // doesn't throw an exception. Additionally, even if we were unable to unregister the event handler // from the contentWindow, make sure that re-appending a disposed _ElementResizeInstrument and // resizing it will not cause it to notify listeners of any resize events, since it has been disposed. function parentFailResizeHandler(): void { LiveUnit.Assert.fail("disposed parentIstrument should never fire resize events"); } var readyPromise = new WinJS.Promise((c) => { this._parentInstrument.addEventListener(readyEvent, c); this._parentInstrument.addedToDom(); }); readyPromise .then(() => { return awaitInitialResizeEvent(this._parentInstrument); }) .then(() => { this._element.removeChild(this._parent); try { this._parentInstrument.dispose(); } catch (e) { LiveUnit.Assert.fail("Disposing an _ElementResizeInstrument that is not in the DOM, should not throw an exception"); } this._parentInstrument.addEventListener(resizeEvent, parentFailResizeHandler); document.body.appendChild(this._parent); // Timeout long enough for the browser to acknowledge the element is back in the DOM. return WinJS.Promise.timeout(100); }).then(() => { this._parent.style.width = "101px"; return allowTimeForAdditionalResizeEvents(); }).done(() => { complete(); }); } } } LiveUnit.registerTestClass("CorsicaTests.ElementResizeInstrumentTests");