// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License.txt in the project root for license information. // // // // /// module CorsicaTests { "use strict"; var _Constants = Helper.require("WinJS/Controls/_LegacyAppBar/_Constants"), _LightDismissService = Helper.require("WinJS/_LightDismissService"), Key = WinJS.Utilities.Key, Flyout = WinJS.UI.Flyout, Menu = WinJS.UI.Menu, MenuCommand = WinJS.UI.MenuCommand, _defaultAnchor: HTMLElement, cascadeManager = Flyout._cascadeManager, chainCounter; var DEFAULT_CHAIN_SIZE = 6; interface IObservable { addEventListener(eventName: string, eventHandler: Function, useCapture?: boolean): void; removeEventListener(eventName: string, eventHandler: Function, useCapture?: boolean): void; } var listenOnce = (observable: IObservable, eventName: string, callback: () => any): void => { observable.addEventListener(eventName, function handler() { observable.removeEventListener(eventName, handler, false); callback(); }, false); }; // Private test class provides Helpers and tests that every implementing test class will need. export class _BaseCascadingTests { private abstractMethodFail() { LiveUnit.Assert.fail("Test Error: This method is abstract. Descendant classes need to provide implementation."); } // // Abstract Helper methods that need to be implemented by each derivative class. // showFlyoutInCascade(flyout: WinJS.UI.PrivateFlyout): WinJS.Promise { this.abstractMethodFail(); return WinJS.Promise.wrapError(null); // Appease the compiler. } generateFlyoutChain(numFlyouts?: number): Array { this.abstractMethodFail(); return []; // Appease the compiler. } chainFlyouts(parentFlyout: WinJS.UI.PrivateFlyout, subFlyout: WinJS.UI.PrivateFlyout): void { this.abstractMethodFail(); } // // Concrete Helper methods // hideFlyoutInCascade(flyout: WinJS.UI.PrivateFlyout): WinJS.Promise { // Hides the specified flyout and returns a promise that completes when // it and all of its subFlyouts in the cascade are hidden. var p: WinJS.Promise; var index = cascadeManager.indexOf(flyout); if (index >= 0) { // Identify all the subFlyouts that should hide when the specified flyout is hidden. var hidingFlyouts: Array = cascadeManager._cascadingStack.slice(index, cascadeManager.length); var hidingPromises: Array> = hidingFlyouts.map((flyout): WinJS.Promise => { return new WinJS.Promise((c, e, p) => { function afterHide() { flyout.removeEventListener("afterhide", afterHide, false); c(); }; flyout.addEventListener("afterhide", afterHide, false); }); }); hidingFlyouts[0].hide(); p = WinJS.Promise.join(hidingPromises); } else { p = WinJS.Promise.wrap(); } return p; } showFlyoutChain(flyoutChain: Array, sentinelFlyout?: WinJS.UI.PrivateFlyout): WinJS.Promise { // Shows all flyouts in the specified flyoutChain until the sentinel flyout is shown. // If no sentinel is specified, the entire chain is shown. // Returns a promise that is completed when the last flyout is finished showing. var verifyFlyoutContainsFocusAfterShowing = (flyout: WinJS.UI.PrivateFlyout) => { LiveUnit.Assert.isTrue(flyout.element.contains(document.activeElement), "Flyout should contain focus after showing"); } var index = flyoutChain.indexOf(sentinelFlyout); flyoutChain = (index < 0) ? flyoutChain : flyoutChain.slice(0, index + 1); return Helper.Promise.forEach(flyoutChain, (flyout) => { return this.showFlyoutInCascade(flyout).then(() => { verifyFlyoutContainsFocusAfterShowing(flyout); }); }); } verifyCascade(expectedCascade: Array): void { // Verifies that the Flyouts currently in the cascade and the Flyouts that are currently visible line up with the chain of flyouts we are expecting. var msg = "The Flyouts in the cascade should match the chain of Flyouts we were expecting."; LiveUnit.LoggingCore.logComment("Test: " + msg); LiveUnit.Assert.areEqual(expectedCascade.length, cascadeManager.length, msg); for (var i = 0, len = expectedCascade.length; i < len; i++) { LiveUnit.Assert.areEqual(expectedCascade[i], cascadeManager.getAt(i), msg); } msg = "The Flyouts that are visible should match the chain of Flyouts we were expecting."; LiveUnit.LoggingCore.logComment("Test: " + msg); var visibleFlyoutElements: Array = Array.prototype.filter.call(document.querySelectorAll(".win-flyout"), function (flyoutElement) { return !flyoutElement.winControl.hidden; }); LiveUnit.Assert.areEqual(expectedCascade.length, visibleFlyoutElements.length, msg); for (var i = 0, len = expectedCascade.length; i < len; i++) { LiveUnit.Assert.isTrue(visibleFlyoutElements.indexOf(expectedCascade[i].element) >= 0, msg); } } verifyDismissableLayer(expectedDismissables: WinJS.UI.PrivateFlyout[]): void { var dismissableLayer = cascadeManager.dismissableLayer; var dismissableFlyouts = dismissableLayer.clients.map(function (client) { return client.element.winControl; }); if (expectedDismissables.length === 0) { LiveUnit.Assert.isFalse(_LightDismissService.isShown(dismissableLayer), "CascadingManager's dismissable layer is empty and should not be shown."); } else { LiveUnit.Assert.isTrue(_LightDismissService.isShown(dismissableLayer), "CascadingManager's dismissable layer is non-empty and should be shown."); } Helper.Assert.areArraysEqual(expectedDismissables, dismissableFlyouts, "Unexpected set of Flyouts in dismissable layer"); } setUp() { LiveUnit.LoggingCore.logComment("In setup"); chainCounter = 0; _defaultAnchor = document.createElement('button'); _defaultAnchor.id = "defaultanchor"; _defaultAnchor.textContent = "defaultanchor"; _defaultAnchor.tabIndex = 1; document.body.appendChild(_defaultAnchor); _defaultAnchor.focus(); } tearDown() { LiveUnit.LoggingCore.logComment("In tearDown"); chainCounter = 0; cascadeManager.collapseAll(); var flyouts = document.querySelectorAll(".win-flyout"); Array.prototype.forEach.call(flyouts, (element: HTMLElement) => { OverlayHelpers.disposeAndRemove(element); element = null; }); OverlayHelpers.disposeAndRemove(_defaultAnchor); } // // Unit Tests // testSingleFlyoutInTheCascade = function (complete) { // Verifies that showing and hiding a flyout will always add and remove it from the cascade. function checkAfterShow() { flyout.removeEventListener("aftershow", checkAfterShow, false); var msg = "Shown flyout should take focus"; LiveUnit.LoggingCore.logComment("Test: " + msg); LiveUnit.Assert.isTrue(flyout.element.contains(document.activeElement), msg); msg = "Showing a flyout should always add it to the cascade"; LiveUnit.LoggingCore.logComment("Test: " + msg); LiveUnit.Assert.isTrue(cascadeManager.indexOf(flyout) >= 0, msg); LiveUnit.Assert.areEqual(cascadeManager.length, 1); flyout.hide(); var msg = "Flyouts should be removed from the cascade synchronously when hide() is called."; LiveUnit.LoggingCore.logComment("Test: " + msg); LiveUnit.Assert.areEqual(0, cascadeManager.length, msg); }; function checkAfterHide() { flyout.removeEventListener("afterhide", checkAfterHide, false); var msg = "Hiding a flyout should always remove it from the cascade"; LiveUnit.LoggingCore.logComment("Test: " + msg); LiveUnit.Assert.isFalse(cascadeManager.indexOf(flyout) >= 0, msg); LiveUnit.Assert.areEqual(cascadeManager.length, 0) var msg = "Hiding all flyouts in the cascade should leave focus in the app."; LiveUnit.LoggingCore.logComment("Test: " + msg); LiveUnit.Assert.isTrue(_defaultAnchor.contains(document.activeElement), msg); complete(); }; var flyout = this.generateFlyoutChain(1)[0]; var msg = "The cascade should be empty when no flyouts are showing"; LiveUnit.LoggingCore.logComment("Test: " + msg); LiveUnit.Assert.areEqual(0, cascadeManager.length, msg); flyout.addEventListener("aftershow", checkAfterShow, false); flyout.addEventListener("afterhide", checkAfterHide, false); flyout.show(); var msg = "Flyouts should be added to the cascade synchronously when show() is called."; LiveUnit.LoggingCore.logComment("Test: " + msg); LiveUnit.Assert.areEqual(1, cascadeManager.length, msg); } testChainedFlyoutsWillAppendToTheCascadeWhenShownInOrder = function (complete) { // Verifies that showing chained flyouts, one after the other, in order, will cause them all show in the cascade, in order. var flyoutChain = this.generateFlyoutChain(); this.showFlyoutChain(flyoutChain).then(() => { this.verifyCascade(flyoutChain); complete(); }); } testHidingAFlyoutAlsoCollapsesItsSubFlyoutsAndRestoresFocus = function (complete) { // Verifies that hiding a flyout will also hide its cascading subFlyouts. // Verifies that each time a flyout is hidden, focus is restored to whichever element the specified flyout originally took focus from. // Explicitly set initial focus: _defaultAnchor.focus(); var requiredSize = 3, flyoutChain = this.generateFlyoutChain(); LiveUnit.Assert.isTrue(flyoutChain.length >= requiredSize, "ERROR: Test requires input size of at least " + requiredSize); var index: number, flyout: WinJS.UI.PrivateFlyout, expectedFocusTarget: HTMLElement, expectedCascadeAfterHiding: Array; return this.showFlyoutChain(flyoutChain).then(() => { // Hide Flyout at the end of the cascade index = flyoutChain.length - 1; flyout = flyoutChain[index]; expectedFocusTarget = flyoutChain[index - 1].element, expectedCascadeAfterHiding = flyoutChain.slice(0, index); return this.hideFlyoutInCascade(flyout); }).then(() => { this.verifyCascade(expectedCascadeAfterHiding); LiveUnit.Assert.areEqual(document.activeElement, expectedFocusTarget, "The flyout specified to hide should have put focus on whatever element it had originally taken it from."); // Hide Flyout in the middle of the cascade index = Math.floor(flyoutChain.length / 2) flyout = flyoutChain[index]; expectedFocusTarget = flyoutChain[index - 1].element; expectedCascadeAfterHiding = flyoutChain.slice(0, index); return this.hideFlyoutInCascade(flyout); }).then(() => { this.verifyCascade(expectedCascadeAfterHiding); LiveUnit.Assert.areEqual(document.activeElement, expectedFocusTarget, "The flyout specified to hide should have put focus on whatever element it had originally taken it from."); // Hide Flyout at the beginning of the cascade index = 0; flyout = flyoutChain[index]; expectedFocusTarget = _defaultAnchor; expectedCascadeAfterHiding = flyoutChain.slice(0, index); return this.hideFlyoutInCascade(flyout); }).then(() => { this.verifyCascade(expectedCascadeAfterHiding); LiveUnit.Assert.areEqual(document.activeElement, expectedFocusTarget, "The flyout specified to hide should have put focus on whatever element it had originally taken it from."); complete(); }); } testShowingAFlyout_AnchoredToAFlyoutInTheMiddleOfTheCascade_HidesOtherSubFlyouts = function (complete) { // Verifies that, showing a flyout "A" whose anchor is an element contained within a flyout "B", while "B" is already showing in the cascade will: // 1) Removes all subflyouts after "B" from the cascade, making "B" the new end. // 2) Appends "A" to the end of the cascade after "B". var flyoutChain = this.generateFlyoutChain(), requiredSize = 2; LiveUnit.Assert.isTrue(flyoutChain.length >= requiredSize, "ERROR: Test requires input size of at least " + requiredSize); this.showFlyoutChain(flyoutChain).then(() => { // Create a single Flyout and chain it to a flyout in the middle of the cascade. var otherFlyout = this.generateFlyoutChain(1)[0]; this.chainFlyouts(flyoutChain[requiredSize - 2], otherFlyout); this.showFlyoutInCascade(otherFlyout).then(() => { var expectedCascade = flyoutChain.slice(0, requiredSize - 1).concat(otherFlyout); this.verifyCascade(expectedCascade); complete(); }); }); } testShowingAFlyout_NotAnchoredToAFlyoutInTheTheCascade_ReplacesTheCurrentCascadeWithItself = function (complete) { // Verifies that, showing a flyout (A), that is not anchored to a flyout already in the cascade should replace all subflyouts in the cascade with flyout (A). // Also Verifies that then hiding (A) will restore focus back to the element in the App that had focus before the any of the flyouts were opened. // Explicitly set initial focus: _defaultAnchor.focus(); // Chain of flyouts to initially show in the cascade. var flyoutChain = this.generateFlyoutChain(); // Single flyout from a new chain. var otherFlyout = this.generateFlyoutChain(1)[0]; this.showFlyoutChain(flyoutChain).then(() => { return this.showFlyoutInCascade(otherFlyout); }).then(() => { this.verifyCascade([otherFlyout]); return this.hideFlyoutInCascade(otherFlyout); }).done(() => { LiveUnit.Assert.isTrue(_defaultAnchor.contains(document.activeElement), "Hiding all flyouts in the cascade should return focus to the element that originally had it."); complete(); }); } testFlyoutAlwaysHidesSubFlyoutsWhenItReceivesFocus = function (complete) { // Verifies that when focus moves into a flyout from somewhere that was outside of that flyout, all of it's subflyout descendants get removed from the cascade. var flyoutChain = this.generateFlyoutChain(), requiredSize = 3; LiveUnit.Assert.isTrue(flyoutChain.length >= requiredSize, "ERROR: Test requires input size of at least " + requiredSize); this.showFlyoutChain(flyoutChain).then(() => { var index = 1, flyoutToFocus = flyoutChain[index], firstSubFlyoutToHide = flyoutChain[index + 1], expectedChain = flyoutChain.slice(0, index + 1); listenOnce(firstSubFlyoutToHide, "afterhide", () => { this.verifyCascade(expectedChain); complete(); }); LiveUnit.Assert.isFalse(flyoutToFocus.element.contains(document.activeElement), "Test Error: focus needs to be outside of the element, before we focus it."); flyoutToFocus.element.focus(); }); } testEntireCascadeHidesWhenAllFlyoutsLoseFocus = function (complete) { // Verifies that the entire cascade hides when all flyouts lose focus. var flyoutChain = this.generateFlyoutChain(); this.showFlyoutChain(flyoutChain).then(() => { listenOnce(flyoutChain[0], "afterhide", () => { this.verifyCascade([]); complete(); }); LiveUnit.Assert.isTrue(cascadeManager.indexOfElement(document.activeElement) >= 0, "Test Error: focus needs to be inside of one of the flyouts in the cascade before we move focus outside of the cascade."); _defaultAnchor.focus(); }); } testDisposeOfCascade = function (complete) { // Verifies cascade cleans up properly when each of its flyouts gets disposed. var flyoutChain = this.generateFlyoutChain(); this.showFlyoutChain(flyoutChain).then(() => { this.verifyDismissableLayer(flyoutChain); flyoutChain.forEach((flyout) => { flyout.dispose(); }); this.verifyCascade([]); this.verifyDismissableLayer([]); complete(); }); } testLeftArrowKeyHidesCurrentSubFlyout = function (complete) { // Verifies that the left arrow key will hide any flyout that is a subFlyout. var flyoutChain = this.generateFlyoutChain(); this.showFlyoutChain(flyoutChain).then(() => { var endFlyout = flyoutChain[flyoutChain.length - 1], expectedCascade = flyoutChain.slice(0, flyoutChain.length - 1); listenOnce(endFlyout, "afterhide", () => { this.verifyCascade(expectedCascade); complete(); }); Helper.keydown(endFlyout.element, Key.leftArrow); }); } testLeftArrowKeyDoesNotHideFlyoutWhenOnlyOneFlyoutIsShowing = function (complete) { // Verifies that the left arrow key will not hide a Flyout, if that Flyout is not a subFlyout of another shown flyout. var flyout = this.generateFlyoutChain(1)[0]; var msg = "Left arrow key should not hide the current flyout if it is not the subFlyout of another shown flyout."; function beforeHide() { flyout.removeEventListener("beforehide", beforeHide, false); LiveUnit.Assert.fail(msg); } this.showFlyoutInCascade(flyout).then(() => { this.verifyCascade([flyout]); LiveUnit.LoggingCore.logComment("Test: " + msg); flyout.addEventListener("beforehide", beforeHide, false); Helper.keydown(flyout.element, Key.leftArrow); return WinJS.Promise.timeout(); }).then(function () { flyout.removeEventListener("beforehide", beforeHide, false); complete(); }); } testAltAndF10WillCollapseTheEntireCascade = function (complete) { // Verifies that both "alt" and "F10" keys when pressed inside a flyout will collapse the entire cascade. var flyoutChain = this.generateFlyoutChain(); var verifyKeyCollapsesTheCascade = (keyCode: number, keyName: string) => { return new WinJS.Promise((completePromise) => { this.showFlyoutChain(flyoutChain).then(() => { var headFlyout = flyoutChain[0], tailFlyout = flyoutChain[flyoutChain.length - 1]; listenOnce(headFlyout, "afterhide", () => { this.verifyCascade([]); completePromise(); }); var msg = "The entire cascade should hide whenever " + keyName + " is pressed inside a Flyout"; LiveUnit.LoggingCore.logComment("Test: " + msg); Helper.keydown(tailFlyout.element, keyCode); }); }); }; verifyKeyCollapsesTheCascade(Key.alt, "alt").then(() => { return verifyKeyCollapsesTheCascade(Key.F10, "F10"); }).done(complete); } testFlyoutsBlockedFromShowingDuringReEntrancy_WillBeShownAsyncronously = function (complete) { // Regression test: https://github.com/winjs/winjs/issues/882 // Verifies that showing a 2nd Flyout chain at the beginning of hiding the 1st Flyout chain, // will cause the 2nd Flyout chain to show once the 1st cascade is finished collapsing. var chain1 = this.generateFlyoutChain(), chain2 = this.generateFlyoutChain(); this.showFlyoutChain(chain1).then(() => { chain1[0].onbeforehide = () => { // Sanity Check to make sure we are actually testing against the reentrancyLock LiveUnit.Assert.isTrue(Flyout._cascadeManager.reentrancyLock, "TEST ERROR: Test is only valid when reentrancyLock is enabled"); this.showFlyoutChain(chain2).then(() => { this.verifyCascade(chain2); complete(); }); }; chain1[0].hide(); }); } testFocusMovesWithinCascadeSynchronously = function (complete) { // Verifies Overlay.show and Overlay.hide move focus synchronously // when focus is being moved between Overlays within the cascade. var testShow = (overlay) => { var promise = OverlayHelpers.show(overlay); LiveUnit.Assert.areEqual(overlay.element, document.activeElement, "Overlay should have received focus synchronously during show"); return promise; }; var chain = this.generateFlyoutChain(); testShow(chain[0]).then(() => { return testShow(chain[1]); }).then(() => { OverlayHelpers.hide(chain[1]); LiveUnit.Assert.areEqual(chain[0].element, document.activeElement, "Hidden Overlay should have synchronously moved focus to its parent Overlay during hide"); OverlayHelpers.hide(chain[0]); complete(); }); } testCascadeDoesNotLoseTrackOfShowingFlyouts1 = function (complete) { // Regression test for https://github.com/winjs/winjs/issues/1256 // Verifies that synchronously showing, hiding, and showing a flyout // will not invalidate the cascadeManager's model of the cascade. var flyout = this.generateFlyoutChain(1)[0]; var afterShow = () => { flyout.removeEventListener("aftershow", afterShow, false); this.verifyCascade([flyout]); complete(); }; flyout.addEventListener("aftershow", afterShow, false); var msg = "The cascade should be empty when no flyouts are showing"; LiveUnit.LoggingCore.logComment("Test: " + msg); LiveUnit.Assert.areEqual(0, cascadeManager.length, msg); flyout.show(); flyout.hide(); flyout.show(); } testCascadeDoesNotLoseTrackOfShowingFlyouts2 = function (complete) { // Regression test for https://github.com/winjs/winjs/issues/1256 // Verifies that synchronously showing and hiding various flyouts in the // cascade will not invalidate the cascadeManager's model of the cascade. var requiredSize = 2; var flyoutChain = this.generateFlyoutChain(); if (flyoutChain.length < requiredSize) { LiveUnit.Assert.fail("TEST ERROR: test requires a minimum flyout chain size of " + requiredSize); } var msg = "The cascade should be empty when no flyouts are showing"; LiveUnit.LoggingCore.logComment("Test: " + msg); LiveUnit.Assert.areEqual(0, cascadeManager.length, msg); // Begin by showing the entire chain except for the last flyout. this.showFlyoutChain(flyoutChain, flyoutChain[flyoutChain.length - 2]).then(() => { var headFlyout = flyoutChain[0]; var tailFlyout = flyoutChain[flyoutChain.length - 1]; var afterShow = () => { tailFlyout.removeEventListener("aftershow", afterShow, false); // Verify tailFlyout is the lone flyout in the cascade. this.verifyCascade([tailFlyout]); complete(); }; tailFlyout.addEventListener("aftershow", afterShow, false); // (1) This will synchronously add the tailFlyout to the end of the cascadingStack and start its // asynchronous show animation. tailFlyout.show(); // (2) This will synchronously remove the headFlyout and all its subFlyouts from the cascadingStack // and call hide() on each of them them. All of the flyouts except for the tail flyout were already // visible, so they will begin an asynchronous hide animation. The tail flyout is still doing a show // animation, so it will set its _doNext operation to "hide", which will be resolved asynchronously // after the tail flyout's current animation completes. headFlyout.hide(); // (3) This will synchronously add the tail flyout into the cascade. Tail flyout is still doing the // original show animation from (1), so this will override the _doNext operation from "hide" to "show", // which will be resolved asynchronously after the tail flyout's current animation completes. In the // case that the tail flyout is already shown, the resolution of _checkDoNext will nop. tailFlyout.show(); }); } } // Test Class for Cascading Flyout unit tests. export class CascadingFlyoutTests extends _BaseCascadingTests { // Implementation of Abstract showFlyoutInCascade Method. showFlyoutInCascade(flyout: WinJS.UI.PrivateFlyout): WinJS.Promise { return OverlayHelpers.show(flyout); } // Implementation of Abstract generateFlyoutChain Method. generateFlyoutChain(numFlyouts?: number): Array { // Creates and returns an Array of Flyouts. Each Flyout in the chain has its anchor property set to the HTMLElement of the previous flyout. var flyoutChain = [], chainClass = "chain_" + ++chainCounter, anchor, prevFlyout; // Default fallback. numFlyouts = numFlyouts || DEFAULT_CHAIN_SIZE; for (var i = 0; i < numFlyouts; i++) { anchor = prevFlyout ? prevFlyout.element : _defaultAnchor; var flyout = new Flyout(null, { anchor: anchor }); document.body.appendChild(flyout.element); WinJS.Utilities.addClass(flyout.element, chainClass); flyout.element.id = (i + 1) + "of" + numFlyouts; flyoutChain.push(flyout); prevFlyout = flyout; } return flyoutChain; } // Implementation of Abstract chainFlyouts Method. chainFlyouts(parentFlyout: WinJS.UI.PrivateFlyout, subFlyout: WinJS.UI.PrivateFlyout): void { // Chain the subFlyout to the parentFlyout. subFlyout.anchor = parentFlyout.element; } } // Test Class for Cascading Menu unit tests. export class CascadingMenuTests extends _BaseCascadingTests { private firstCommandId = "flyoutCmd1"; private secondCommandId = "flyoutCmd2"; // Implementation of Abstract showFlyoutInCascade Method. showFlyoutInCascade(flyout: WinJS.UI.PrivateFlyout): WinJS.Promise { // If my anchor isn't in the cascade, just call overlayhelpers.show // else call menucommand._activateFlyoutCommand(flyout) var cascadingStack = cascadeManager._cascadingStack; for (var cascadeIndex = cascadingStack.length - 1; cascadeIndex >= 0; cascadeIndex--) { var currentFlyout = cascadingStack[cascadeIndex], currentFlyoutCommands = currentFlyout.element.querySelectorAll("." + _Constants.menuCommandFlyoutClass), parentFlyoutCommand; for (var i = 0, len = currentFlyoutCommands.length; i < len; i++) { var flyoutCommand = currentFlyoutCommands[i].winControl; if (flyoutCommand && flyoutCommand.flyout === flyout) { parentFlyoutCommand = flyoutCommand; } } if (parentFlyoutCommand) { break; } } var result; if (parentFlyoutCommand) { result = MenuCommand._activateFlyoutCommand(parentFlyoutCommand); } else { result = OverlayHelpers.show(flyout); } return result; } // Implementation of Abstract generateFlyoutChain Method. generateFlyoutChain(numMenus?: number): Array { // Creates and returns an Array of Menu Flyouts. Each Menu in the chain has its anchor property set to the HTMLElement of parent Menu's flyout MenuCommand var flyoutChain = [], chainClass = "chain_" + ++chainCounter, anchor, prevMenu; // Default fallback. numMenus = numMenus || DEFAULT_CHAIN_SIZE; for (var i = 0; i < numMenus; i++) { var menu = new Menu(null, {}); document.body.appendChild(menu.element); WinJS.Utilities.addClass(menu.element, chainClass); menu.element.id = (i + 1) + "of" + numMenus; if (prevMenu) { // Set commands in the previous Menu in order to chain it to the current Menu, via the MenuCommand 'flyout' property. var prevMenuCommands = [ // First command opens the current Menu. // Second command can be used by tests for any reason. new MenuCommand(null, { id: this.firstCommandId, label: this.firstCommandId, type: _Constants.typeFlyout, flyout: menu }), new MenuCommand(null, { id: this.secondCommandId, label: this.secondCommandId, type: _Constants.typeFlyout, flyout: null }), ]; prevMenu.commands = prevMenuCommands; menu.anchor = prevMenuCommands[0].element; } else { menu.anchor = _defaultAnchor; } flyoutChain.push(menu); prevMenu = menu; } return flyoutChain; } // Implementation of Abstract chainFlyouts Method. chainFlyouts(parentMenu: WinJS.UI.PrivateFlyout, subMenu: WinJS.UI.PrivateFlyout): void { // Chain the subMenu to the parentMenu. var commandInParentMenu = parentMenu.element.querySelector("#" + this.secondCommandId).winControl; subMenu.anchor = commandInParentMenu; commandInParentMenu.flyout = subMenu; } // // Unit Tests // testMenuCommandActionCommittedCollapsesEntireCascade = function (complete) { var flyoutChain = this.generateFlyoutChain(); var buttonCmd = new MenuCommand(null, { type: 'button' }); flyoutChain[flyoutChain.length - 1].commands = [buttonCmd]; this.showFlyoutChain(flyoutChain).then(() => { var pArr = []; flyoutChain.forEach((flyout: WinJS.UI.PrivateFlyout) => { pArr.push(new WinJS.Promise((c) => { listenOnce(flyout.element, "afterhide", c); })); }) WinJS.Promise.join(pArr).then(() => { this.verifyCascade([]); complete(); }); buttonCmd._invoke(); // We expect this to trigger collapse of entire cascade. }); } testHorizontalLayoutOfCascadedSubMenus = function (complete) { // Verifies the following Horizontal placement logic for cascading menus. // 1. PREFERRED: When there is enough room to fit a subMenu on either side of the parentMenu, // the subMenu prefers to go on the right hand side. // 2. FALLBACK: When there is only enough room to fit a subMenu on the right side of the parentMenu, // the subMenu is placed to the left of the parent menu. // 3. LASTRESORT: When there is not enough room to fit a subMenu on either side of the parentMenu, // the subMenu is pinned to the right edge of the window. var iframe = document.createElement("iframe"); iframe.src = "$(TESTDATA)/WinJSSandbox.html"; iframe.onload = function () { // This test requires the WinJS loaded inside of the iframe to ensure that private // WinJS internal helper functions identify the edge of the iframe's visual // viewport as the edge of the visible document, so that Cascading Menu's will // correctly avoid clipping through the edge of their contentwindow when showing. var iframeWinJS = iframe.contentWindow["WinJS"]; var iframeMenu = iframeWinJS.UI.Menu; var iframeMenuCommand = iframeWinJS.UI.MenuCommand; var iframeDocument = iframe.contentDocument; var defaultAnchor = iframeDocument.createElement("DIV"); var parentMenu = new iframeMenu(); var subMenu = new iframeMenu(); var flyoutCommand = new iframeMenuCommand(null, { type: 'flyout', flyout: subMenu, label: 'show submenu' }); var subMenuCommands = [ new iframeMenuCommand(null, { type: 'button', label: 'cmd1' }), new iframeMenuCommand(null, { type: 'button', label: 'cmd2' }), new iframeMenuCommand(null, { type: 'button', label: 'cmd3' }), ]; parentMenu.anchor = defaultAnchor; parentMenu.commands = [flyoutCommand]; subMenu.anchor = flyoutCommand.element; subMenu.commands = subMenuCommands; iframeDocument.body.appendChild(defaultAnchor); iframeDocument.body.appendChild(parentMenu.element); iframeDocument.body.appendChild(subMenu.element); var cachedParentMenuBorderBoxWidth: number; // content, padding, border; var cachedParentMenuMargins: { left: number; right: number; top: number; bottom: number; }; var cachedSubMenuBorderBoxWidth: number; // content, padding, border, var cachedSubMenuMargins: { left: number; right: number; top: number; bottom: number; }; var iframeWidth: number; var expectedOverlap = 4; var minimumSpaceForLeftSubMenu: number; var minimumSpaceForRightSubMenu: number; function asyncShow(menu: WinJS.UI.PrivateMenu, anchor, placement?, alignment?): WinJS.Promise { return new WinJS.Promise(function (c, e, p): void { if (!menu.hidden) { c(); } else { listenOnce(menu.element, "aftershow", c); menu.show(anchor, placement, alignment); } }); } function asyncHide(menu: WinJS.UI.PrivateMenu): WinJS.Promise { return new WinJS.Promise(function (c, e, p): void { if (menu.hidden) { c(); } else { listenOnce(menu.element, "afterhide", c); menu.hide(); } }); } function cacheHorizontalMeasurements(): WinJS.Promise { return new WinJS.Promise((c) => { asyncShow(parentMenu, defaultAnchor) .then(() => { cachedParentMenuBorderBoxWidth = parentMenu.element.getBoundingClientRect().width; cachedParentMenuMargins = WinJS.Utilities._getPreciseMargins(parentMenu.element); return iframeMenuCommand._activateFlyoutCommand(flyoutCommand); }) .then(() => { cachedSubMenuBorderBoxWidth = subMenu.element.getBoundingClientRect().width; cachedSubMenuMargins = WinJS.Utilities._getPreciseMargins(subMenu.element); minimumSpaceForLeftSubMenu = cachedSubMenuMargins.left + cachedSubMenuBorderBoxWidth - expectedOverlap; minimumSpaceForRightSubMenu = cachedSubMenuBorderBoxWidth - expectedOverlap + cachedSubMenuMargins.right; return iframeMenuCommand._deactivateFlyoutCommand(flyoutCommand); }) .then(() => { return asyncHide(parentMenu); }) .done(c); }); } function configureHorizontalPositionOfParentMenuInIframe(visibleSpaceLeftOfParentMenu, visibleSpaceRightOfParentMenu): WinJS.Promise { // Sizes the iframe to the specified dimensions, then positions the parentMenu at the correct location to perform the test. return new WinJS.Promise((c) => { function configureAfterContentWindowResize() { // PRECONDITION: Sanity check that Iframe width is the value we intended. Helper.Assert.areFloatsEqual(iframeWidth, iframe.offsetWidth, "TEST ERROR: Test expects iframe width of " + iframeWidth + "px", 1); // PRECONDITION: Sanity check visualViewportWidth matches iframeWidth var iframeVisualViewportWidth = iframeWinJS.UI._Overlay._keyboardInfo._visualViewportWidth; Helper.Assert.areFloatsEqual(iframe.offsetWidth, iframeVisualViewportWidth, "TEST ERROR: Iframe's WinJS should report that the visual viewport width matches the iframe width", 1); asyncShow(parentMenu, defaultAnchor) .then(() => { parentMenu.element.style.left = (visibleSpaceLeftOfParentMenu - cachedParentMenuMargins.left) + "px"; }) .done(c); } // Sizing the iframe will trigger an async "resize" event in the iframe contentWindow. Wait until the "resize" event // fires to avoid light dismissing the menus before we've finished. iframeWidth = visibleSpaceLeftOfParentMenu + cachedParentMenuBorderBoxWidth + visibleSpaceRightOfParentMenu; listenOnce(iframe.contentWindow, "resize", configureAfterContentWindowResize); iframe.style.width = iframeWidth + "px"; }); } function verifySubMenuWithinHorizontalBounds(subMenu) { var subMenuRect = subMenu.element.getBoundingClientRect(); var tolerance = 1; LiveUnit.Assert.isTrue(subMenuRect.left - cachedSubMenuMargins.left > 0 - tolerance, "left edge of subMenu marginbox should not overrun left edge of iframe visual viewport"); LiveUnit.Assert.isTrue(subMenuRect.right + cachedSubMenuMargins.right < iframeWidth + tolerance, "right edge of subMenu marginbox should not overrun right edge of iframe visual viewport"); } function verifyCascadePreferredPlacement(): WinJS.Promise { // Verifies that subMenus will cascade to the right of their parent menu, as long as there is enough room. return new WinJS.Promise((c) => { var parentMenuRect: ClientRect; var subMenuRect: ClientRect; var visibleSpaceToTheLeft = minimumSpaceForLeftSubMenu + 1; var visibleSpaceToTheRight = minimumSpaceForRightSubMenu + 1; configureHorizontalPositionOfParentMenuInIframe( visibleSpaceToTheLeft, visibleSpaceToTheRight) .then(() => { // PRECONDITION: Sanity check that parent menu has enough room to fit a subMenu on either side. parentMenuRect = parentMenu.element.getBoundingClientRect(); LiveUnit.Assert.isTrue(parentMenuRect.left >= minimumSpaceForLeftSubMenu, "TEST ERROR: Test requires more room between left edge of parent menu and the left edge of the visual viewport"); LiveUnit.Assert.isTrue(iframeWidth - parentMenuRect.right >= minimumSpaceForRightSubMenu, "TEST ERROR: Test requires more room between right edge of parent menu and the right edge of the visual viewport"); // Show the subMenu return iframeMenuCommand._activateFlyoutCommand(subMenu.anchor.winControl); }) .then(() => { // Verify subMenu cascades to the right when there is enough space on either side. subMenuRect = subMenu.element.getBoundingClientRect(); Helper.Assert.areFloatsEqual(parentMenuRect.right - subMenuRect.left, expectedOverlap, "left edge of subMenu should overlap right edge of parent menu by " + expectedOverlap + "px", 1); verifySubMenuWithinHorizontalBounds(subMenu); // Hide subMenu return iframeMenuCommand._deactivateFlyoutCommand(subMenu.anchor.winControl) }) .done(c); }); } function verifyCascadeFallBackPlacement(): WinJS.Promise { // Verifies that subMenus will cascade to the left of their parent menu, // if there is only enough room to fit a subMenu to the left. return new WinJS.Promise((c) => { var parentMenuRect: ClientRect; var subMenuRect: ClientRect; var visibleSpaceToTheLeft = minimumSpaceForLeftSubMenu + 1; var visibleSpaceToTheRight = minimumSpaceForRightSubMenu - 1; configureHorizontalPositionOfParentMenuInIframe( visibleSpaceToTheLeft, visibleSpaceToTheRight) .then(() => { // PRECONDITION: Sanity check that there is only enough room to fit a subMenu on the left side of the parentMenu. parentMenuRect = parentMenu.element.getBoundingClientRect(); LiveUnit.Assert.isTrue(parentMenuRect.left >= minimumSpaceForLeftSubMenu, "TEST ERROR: Test requires that there NOT be enough space on the right hand side to fit a subMenu"); LiveUnit.Assert.isTrue(iframeWidth - parentMenuRect.right < minimumSpaceForRightSubMenu, "TEST ERROR: Test requires that there be enough space on the left hand side to fit a subMenu"); // Show the subMenu return iframeMenuCommand._activateFlyoutCommand(subMenu.anchor.winControl); }) .then(() => { // Verify subMenu cascades to the left when there is only enough room to the left. subMenuRect = subMenu.element.getBoundingClientRect(); Helper.Assert.areFloatsEqual(subMenuRect.right - parentMenuRect.left, expectedOverlap, "right edge of subMenu should overlap left edge of parent menu by " + expectedOverlap + "px", 1); verifySubMenuWithinHorizontalBounds(subMenu); // Hide subMenu return iframeMenuCommand._deactivateFlyoutCommand(subMenu.anchor.winControl) }) .done(c); }); } function verifyCascadeLastResortPlacement(): WinJS.Promise { // Verifies that subMenus will be pinned to the right edge of the viewport, // when there is not enough room to cascade on either side of the parent menu. return new WinJS.Promise((c) => { var parentMenuRect: ClientRect; var subMenuRect: ClientRect; var visibleSpaceToTheLeft = minimumSpaceForLeftSubMenu - 1; var visibleSpaceToTheRight = minimumSpaceForRightSubMenu - 1; configureHorizontalPositionOfParentMenuInIframe( visibleSpaceToTheLeft, visibleSpaceToTheRight) .then(() => { // PRECONDITION: Sanity check that there is not enough room to fit a subMenu on either side of the parentMenu. parentMenuRect = parentMenu.element.getBoundingClientRect(); LiveUnit.Assert.isTrue(parentMenuRect.left < minimumSpaceForLeftSubMenu, "TEST ERROR: Test requires that there NOT be enough space on the right hand side to fit a subMenu"); LiveUnit.Assert.isTrue(iframeWidth - parentMenuRect.right < minimumSpaceForRightSubMenu, "TEST ERROR: Test requires that there NOT be enough space on the left hand side to fit a subMenu"); // Show the subMenu return iframeMenuCommand._activateFlyoutCommand(subMenu.anchor.winControl); }) .then(() => { subMenuRect = subMenu.element.getBoundingClientRect(); Helper.Assert.areFloatsEqual(iframeWidth, subMenuRect.right + cachedSubMenuMargins.right, "right edge of subMenu marginbox should pin to the right edge of the iframe if there " + "isn't enough space to cascade on either side of the parentMenu", 1); verifySubMenuWithinHorizontalBounds(subMenu); // Hide subMenu return iframeMenuCommand._deactivateFlyoutCommand(subMenu.anchor.winControl) }) .done(c); }); } cacheHorizontalMeasurements() .then(verifyCascadePreferredPlacement) .then(verifyCascadeFallBackPlacement) .then(verifyCascadeLastResortPlacement) .done(() => { // Clean up parentMenu.dispose(); subMenu.dispose(); document.body.removeChild(iframe); complete(); }); }; document.body.appendChild(iframe); } testVerticalAlignmentOfCascadedSubMenus = function (complete) { // Verifies the following vertical alignment logic for cascading menus. // 1. PREFERRED: When there is enough room to align a subMenu to either the top or the bottom of its anchor element, // the subMenu prefers to be top aligned. // 2. FALLBACK: When there is enough room to bottom align a subMenu but not enough room to top align it, // then the subMenu will be aligned to the bottom of its anchor element. // 3. LASTRESORT: When there is not enough room to top align or bottom align the subMenu to its anchor, // then the subMenu will be center aligned to it's anchor's vertical midpoint. var iframe = document.createElement("iframe"); iframe.src = "$(TESTDATA)/WinJSSandbox.html"; iframe.onload = function () { // This test requires the WinJS loaded inside of the iframe to ensure that private // WinJS internal helper functions identify the edge of the iframe's visual // viewport as the edge of the visible document, so that Cascading Menu's will // correctly avoid clipping through the edge of their contentwindow when showing. var iframeWinJS = iframe.contentWindow["WinJS"]; var iframeMenu = iframeWinJS.UI.Menu; var iframeMenuCommand = iframeWinJS.UI.MenuCommand; var iframeDocument = iframe.contentDocument; var defaultAnchor = iframeDocument.createElement("DIV"); var parentMenu = new iframeMenu(); var subMenu = new iframeMenu(); var flyoutCommand = new iframeMenuCommand(null, { type: 'flyout', flyout: subMenu, label: 'show submenu' }); var subMenuCommands = [ new iframeMenuCommand(null, { type: 'button', label: 'cmd1' }), new iframeMenuCommand(null, { type: 'button', label: 'cmd2' }), new iframeMenuCommand(null, { type: 'button', label: 'cmd3' }), ]; parentMenu.anchor = defaultAnchor; parentMenu.commands = [flyoutCommand]; subMenu.anchor = flyoutCommand.element; subMenu.commands = subMenuCommands; iframeDocument.body.appendChild(defaultAnchor); iframeDocument.body.appendChild(parentMenu.element); iframeDocument.body.appendChild(subMenu.element); var cachedParentMenuBorderBoxHeight: number; // content, padding, border; var cachedParentMenuMargins: { left: number; right: number; top: number; bottom: number; }; var cachedFlyoutCommandBorderBoxHeight: number; var cachedSubMenuBorderBoxHeight: number; // content, padding, border, var cachedSubMenuMargins: { left: number; right: number; top: number; bottom: number; }; var iframeHeight: number; var additionalSpaceRequiredBelow: number; var additionalSpaceRequiredAbove: number; function asyncShow(menu: WinJS.UI.PrivateMenu, anchor, placement?, alignment?): WinJS.Promise { return new WinJS.Promise(function (c, e, p): void { if (!menu.hidden) { c(); } else { listenOnce(menu.element, "aftershow", c); menu.show(anchor, placement, alignment); } }); } function asyncHide(menu: WinJS.UI.PrivateMenu): WinJS.Promise { return new WinJS.Promise(function (c, e, p): void { if (menu.hidden) { c(); } else { listenOnce(menu.element, "afterhide", c); menu.hide(); } }); } function cacheVerticalMeasurements(): WinJS.Promise { // Caches the unmodified height of (1) the parentMenu, (2) the parentMenu's flyoutCommand and (3) the subMenu, // so that we can refer to them while configuring and laying out the rest of the test since the menu's are prone // to light dismiss, and don't maintain their size in the DOM when they are hidden. return new WinJS.Promise((c) => { function measureAfterContentWindowResize() { asyncShow(parentMenu, defaultAnchor) .then(() => { cachedParentMenuBorderBoxHeight = parentMenu.element.getBoundingClientRect().height; cachedParentMenuMargins = WinJS.Utilities._getPreciseMargins(parentMenu.element); cachedFlyoutCommandBorderBoxHeight = flyoutCommand.element.getBoundingClientRect().height; return iframeMenuCommand._activateFlyoutCommand(flyoutCommand); }) .then(() => { cachedSubMenuBorderBoxHeight = subMenu.element.getBoundingClientRect().height; // PRECONDITION: subMenu must be taller than the parentMenu LiveUnit.Assert.isTrue(cachedSubMenuBorderBoxHeight > cachedParentMenuBorderBoxHeight, "TEST ERROR: Test requires that the subMenu be taller than the parentMenu"); cachedSubMenuMargins = WinJS.Utilities._getPreciseMargins(subMenu.element); // The additional space required between the bottom of the flyoutCommand and the bottom of the iframe in order to fit a top aligned subMenu. additionalSpaceRequiredBelow = cachedSubMenuBorderBoxHeight + cachedSubMenuMargins.bottom - cachedFlyoutCommandBorderBoxHeight; // The additional space required between the top of the flyoutCommand and the top of the iframe in order fit a bottom aligned subMenu. additionalSpaceRequiredAbove = cachedSubMenuMargins.top + cachedSubMenuBorderBoxHeight - cachedFlyoutCommandBorderBoxHeight; return iframeMenuCommand._deactivateFlyoutCommand(flyoutCommand); }) .then(() => { return asyncHide(parentMenu); }) .done(c); } // We need to show each Menu in order to cache its height. Menu's can truncate themselves and add a scroll bar upon // showing if they don't have enough vertical space. Make sure that the iframe is big enough during the initial // measure to avoid them having to truncate their height. // // Sizing the iframe will trigger an async "resize" event in the iframe contentWindow. Hold off on trying to measure // until the "resize" event fires, in order to avoid light dismissing the menu's before we've finished. listenOnce(iframe.contentWindow, "resize", measureAfterContentWindowResize); iframe.style.height = "500px"; iframe.style.width = "500px"; }); } function configureVerticalPositionOfFlyoutComandInIframe(visibleSpaceAboveFlyoutCommand, visibleSpaceBelowFlyoutCommand): WinJS.Promise { // Sizes the height of the iframe to be the sum of the two arguments plus the height of flyoutCommandBorderBox, // then positions the parentMenu so that its flyoutCommand is located at the coordinates to perform the test. return new WinJS.Promise((c) => { function configureAfterContentWindowResize() { // PRECONDITION: Sanity check that Iframe height is the value we intended. Helper.Assert.areFloatsEqual(iframeHeight, iframe.offsetHeight, "TEST ERROR: Test expects iframe height of " + iframeHeight + "px", 1); // PRECONDITION: Sanity check visualViewportHeight matches iframeHeight var iframeVisualViewportHeight = iframeWinJS.UI._Overlay._keyboardInfo._visualViewportHeight; Helper.Assert.areFloatsEqual(iframe.offsetHeight, iframeVisualViewportHeight, "TEST ERROR: Iframe's WinJS should report that the visual viewport height matches the iframe height", 1); asyncShow(parentMenu, defaultAnchor) .then(() => { // Find the distance between top of flyoutCommand and top of parentMenu's marginbox. var parentMenuMarginBoxTop = (parentMenu.element.getBoundingClientRect().top - cachedParentMenuMargins.top); var distanceBetween = flyoutCommand.element.getBoundingClientRect().top - parentMenuMarginBoxTop; // Position the top of the parentMenu's marginBox so that the flyoutCommand is in the exact vertical location that was specified parentMenu.element.style.top = (visibleSpaceAboveFlyoutCommand - distanceBetween) + "px"; }).done(c); } // Sizing the iframe will trigger an async "resize" event in the iframe contentWindow. Wait until the "resize" event // fires to avoid light dismissing the menu's. iframeHeight = visibleSpaceAboveFlyoutCommand + cachedFlyoutCommandBorderBoxHeight + visibleSpaceBelowFlyoutCommand; listenOnce(iframe.contentWindow, "resize", configureAfterContentWindowResize); iframe.style.height = iframeHeight + "px"; }); } function verifySubMenuWithinVerticalBounds(subMenu) { var subMenuRect = subMenu.element.getBoundingClientRect(); var tolerance = 1; LiveUnit.Assert.isTrue(subMenuRect.top - cachedSubMenuMargins.top > 0 - tolerance, "top edge of subMenu marginbox should not overrun top edge of iframe visual viewport"); LiveUnit.Assert.isTrue(subMenuRect.bottom + cachedSubMenuMargins.bottom < iframeHeight + tolerance, "bottom edge of subMenu marginbox should not overrun bottom edge of iframe visual viewport"); } function verifyCascadePreferredAlignment(): WinJS.Promise { // Verifies that cascading subMenus will top align to their ancestor flyoutCommand, as long as there is enough // room to stay untruncated. return new WinJS.Promise((c) => { var flyoutCommandRect: ClientRect; var subMenuRect: ClientRect; var visibleSpaceAbove = additionalSpaceRequiredAbove + 1; var visibleSpaceBelow = additionalSpaceRequiredBelow + 1; configureVerticalPositionOfFlyoutComandInIframe( visibleSpaceAbove, visibleSpaceBelow) .then(() => { // PRECONDITION: Sanity check our configuration has enough room to top align or bottom align the subMenu // without overrunning any edge of the iframe flyoutCommandRect = flyoutCommand.element.getBoundingClientRect(); LiveUnit.Assert.isTrue(iframeHeight - flyoutCommandRect.bottom >= additionalSpaceRequiredBelow, "TEST ERROR: Test requires enough room to fit a top aligned subMenu"); LiveUnit.Assert.isTrue(flyoutCommandRect.top >= additionalSpaceRequiredAbove, "TEST ERROR: Test requires enough room to fit a bottom aligned subMenu"); // Show the subMenu return iframeMenuCommand._activateFlyoutCommand(subMenu.anchor.winControl); }) .then(() => { // Verify subMenu prefers top aligment subMenuRect = subMenu.element.getBoundingClientRect(); Helper.Assert.areFloatsEqual(flyoutCommandRect.top, subMenuRect.top, "Cascading subMenu should prefer to align with top edge of flyoutCommand", 1); verifySubMenuWithinVerticalBounds(subMenu); // Hide subMenu return iframeMenuCommand._deactivateFlyoutCommand(subMenu.anchor.winControl) }) .done(c); }); } function verifyCascadeFallBackAlignment(): WinJS.Promise { // Verifies that cascading subMenus will attempt to bottom align to their ancestor flyoutCommand when // there is not enough room to top align and stay untruncated. return new WinJS.Promise((c) => { var flyoutCommandRect: ClientRect; var subMenuRect: ClientRect; var visibleSpaceAbove = additionalSpaceRequiredAbove + 1; var visibleSpaceBelow = additionalSpaceRequiredBelow - 1; configureVerticalPositionOfFlyoutComandInIframe( visibleSpaceAbove, visibleSpaceBelow) .then(() => { // PRECONDITION: Sanity check our configuration has enough room to bottom align the subMenu // but not enough room to top align. flyoutCommandRect = flyoutCommand.element.getBoundingClientRect(); LiveUnit.Assert.isTrue(iframeHeight - flyoutCommandRect.bottom < additionalSpaceRequiredBelow, "TEST ERROR: Test requires that there NOT be enough room for a top aligned subMenu "); LiveUnit.Assert.isTrue(flyoutCommandRect.top >= additionalSpaceRequiredAbove, "TEST ERROR: Test requires enough room to fit a bottom aligned subMenu"); // Show the subMenu return iframeMenuCommand._activateFlyoutCommand(subMenu.anchor.winControl); }) .then(() => { // Verify subMenu falls back to bottom alignment when there isn't enough room to top align. subMenuRect = subMenu.element.getBoundingClientRect(); Helper.Assert.areFloatsEqual(flyoutCommandRect.bottom, subMenuRect.bottom, "Cascading subMenu should fallback to align with bottom edge of flyoutCommand.", 1); verifySubMenuWithinVerticalBounds(subMenu); // Hide subMenu return iframeMenuCommand._deactivateFlyoutCommand(subMenu.anchor.winControl) }) .done(c); }); } function verifyCascadeLastResortAlignment(): WinJS.Promise { // Verifies that cascading subMenus will center align to their ancestor flyoutCommand when // there is not enough room to either, top align or bottom align, and stay untruncated. return new WinJS.Promise((c) => { var flyoutCommandRect: ClientRect; var subMenuRect: ClientRect; var visibleSpaceAbove = additionalSpaceRequiredAbove - 1; var visibleSpaceBelow = additionalSpaceRequiredBelow - 1; configureVerticalPositionOfFlyoutComandInIframe( visibleSpaceAbove, visibleSpaceBelow) .then(() => { // PRECONDITION: Sanity check our configuration doesn't have enough room to top align the subMenu // nor enough room to bottom align. flyoutCommandRect = flyoutCommand.element.getBoundingClientRect(); LiveUnit.Assert.isTrue(iframeHeight - flyoutCommandRect.bottom < additionalSpaceRequiredBelow, "TEST ERROR: Test requires that there NOT be enough room for a top aligned subMenu "); LiveUnit.Assert.isTrue(flyoutCommandRect.top < additionalSpaceRequiredAbove, "TEST ERROR: Test requires that there NOT be enough room to bottom align a subMenu"); // Show the subMenu return iframeMenuCommand._activateFlyoutCommand(subMenu.anchor.winControl); }) .then(() => { subMenuRect = subMenu.element.getBoundingClientRect(); Helper.Assert.areFloatsEqual(flyoutCommandRect.top + flyoutCommandRect.height / 2, subMenuRect.top + subMenuRect.height / 2, "Center aligned subMenu should have the same vertical midpoint as the ancestor flyoutCommand", 1); verifySubMenuWithinVerticalBounds(subMenu); // Hide subMenu return iframeMenuCommand._deactivateFlyoutCommand(subMenu.anchor.winControl); }) .done(c); }); } cacheVerticalMeasurements() .then(verifyCascadePreferredAlignment) .then(verifyCascadeFallBackAlignment) .then(verifyCascadeLastResortAlignment) .done(() => { // Clean up parentMenu.dispose(); subMenu.dispose(); document.body.removeChild(iframe); complete(); }); }; document.body.appendChild(iframe); } } } // register the object as a test class by passing in the name LiveUnit.registerTestClass("CorsicaTests.CascadingFlyoutTests"); LiveUnit.registerTestClass("CorsicaTests.CascadingMenuTests");