/* global AppWindow, HomescreenWindow, MockAppWindow, MockLazyLoader, MockL10n, MockService, MockSettingsListener, MocksHelper, MockStackManager, TaskManager, TaskManagerUtils, WheelEvent */ 'use strict'; requireApp('system/test/unit/mock_app_window.js'); requireApp('system/test/unit/mock_homescreen_window.js'); requireApp('system/test/unit/mock_stack_manager.js'); require('/shared/js/event_safety.js'); require('/shared/js/sanitizer.js'); require('/shared/test/unit/mocks/mock_service.js'); require('/shared/test/unit/mocks/mock_l10n.js'); require('/shared/test/unit/mocks/mock_settings_listener.js'); require('/shared/test/unit/mocks/mock_lazy_loader.js'); var mocksForTaskManager = new MocksHelper([ 'StackManager', 'HomescreenWindow', 'AppWindow', 'Service', 'SettingsListener' ]).init(); var TICK_SHOW_HIDE_MS = 2000; suite('system/TaskManager >', function() { suiteSetup(mocksForTaskManager.suiteSetup); setup(mocksForTaskManager.setup); window.LazyLoader = MockLazyLoader; suiteSetup((done) => { function overrideProperty(obj, propName, newConfig) { var previousDescriptor = Object.getOwnPropertyDescriptor(obj, propName); if (previousDescriptor) { suiteTeardown(() => { Object.defineProperty(obj, propName, previousDescriptor); }); } newConfig.configurable = true; Object.defineProperty(obj, propName, newConfig); } overrideProperty(window, 'innerHeight', { value: 640 }); overrideProperty(window, 'innerWidth', { value: 320 }); overrideProperty(document, 'mozFullScreen', { writable: true, value: false }); sinon.stub(AppWindow.prototype, 'getSiteIconUrl') .returns(Promise.resolve('data:image/png;base64,abc+')); document.body.innerHTML = `
    `; navigator.mozL10n = MockL10n; requireApp('system/js/task_manager_utils.js'); requireApp('system/js/base_ui.js'); requireApp('system/js/card.js'); requireApp('system/js/task_manager.js', function() { TaskManager.prototype._TEST_currentIndex = 0; TaskManager.prototype.getCurrentIndex = function() { return this._TEST_currentIndex; }; TaskManager.prototype.panToApp = function(app) { var index = this.stack.indexOf(app); if (index === -1) { index = this.stack.length - 1; } this._TEST_currentIndex = index; this.updateScrollPosition(); return Promise.resolve(); }; done(); }); }); var clock; setup(function() { clock = this.sinon.useFakeTimers(); }); suiteTeardown(mocksForTaskManager.suiteTeardown); teardown(mocksForTaskManager.teardown); test('Hierarchy', function(done) { var tm = new TaskManager(); this.sinon.stub(MockService, 'request'); tm.start().then(() => { assert.isTrue( MockService.request.calledWith('registerHierarchy', tm)); }).then(() => { return tm.stop(); }).then(() => { assert.isTrue( MockService.request.calledWith('unregisterHierarchy', tm)); }).then(done, done); clock.tick(TICK_SHOW_HIDE_MS); }); function spyEvent(obj, name) { var spy = sinon.stub(); obj.addEventListener(name, spy); teardown(() => { obj.removeEventListener(name, spy); }); return spy; } suite('With Empty Task Manager', function() { var tm; var isActivated; var homescreenApp; setup(function(done) { MockStackManager.mStack = []; homescreenApp = new HomescreenWindow('home'); MockService.mockQueryWith('getHomescreen', homescreenApp); MockService.mockQueryWith('fetchCurrentOrientation', 'portrait-primary'); MockService.mockQueryWith('defaultOrientation', 'portrait-primary'); isActivated = false; window.addEventListener('taskmanager-activated', () => { isActivated = true; }); window.addEventListener('taskmanager-deactivated', () => { isActivated = false; }); this.sinon.spy(document, 'mozCancelFullScreen'); document.mozFullScreen = true; // Made overridable in suiteSetup tm = new TaskManager(); tm.start().then(() => tm.show()).then(() => { done(); }, done); clock.tick(TICK_SHOW_HIDE_MS); }); teardown(function(done) { tm.hide().then(() => tm.stop()).then(() => { done(); }, done); clock.tick(TICK_SHOW_HIDE_MS); }); test('Proper state after show and hide', (done) => { assert.isTrue(tm.isShown()); assert.isFalse(tm._isTransitioning); assert.isTrue( document.querySelector('#screen').classList.contains('cards-view')); assert.isTrue(isActivated); assert.ok(document.querySelector('#task-manager.empty.active')); // We're pretending to be in fullscreen mode. assert.isTrue(document.mozCancelFullScreen.calledOnce); var startHide = tm.hide(); assert.isTrue(tm._isTransitioning); startHide.then(() => { assert.isFalse(tm._isTransitioning); assert.isFalse(tm.isShown()); assert.isFalse(isActivated); assert.ok(document.querySelector('#screen:not(.cards-view)')); assert.equal(document.querySelectorAll('.card').length, 0); done(); }); clock.tick(TICK_SHOW_HIDE_MS); }); test('Should emit "cardviewclosed" after hiding', (done) => { var spyCardViewClosed = spyEvent(window, 'cardviewclosed'); tm.hide().then(() => { assert.isTrue(spyCardViewClosed.called); }).then(done, done); clock.tick(TICK_SHOW_HIDE_MS); }); test('Should only emit "cardviewclosed" after !isActive', (done) => { var wasActive; function onclosed() { window.removeEventListener('cardviewclosed', onclosed); wasActive = tm.isActive(); } window.addEventListener('cardviewclosed', onclosed); tm.hide().then(() => { assert.isFalse(wasActive); }).then(done, done); clock.tick(TICK_SHOW_HIDE_MS); }); test('Should emit "cardviewprepare", "cardviewbeforeshow" ' + 'and "cardviewshown"', (done) => { var spyCardViewPrepare = spyEvent(window, 'cardviewprepare'); tm.hide().then(() => { tm.show(); assert.isTrue(spyCardViewPrepare.called); function onBeforeShow() { window.removeEventListener('cardviewbeforeshow', onBeforeShow); function onShown() { window.removeEventListener('cardviewshown', onShown); done(); } window.addEventListener('cardviewshown', onShown); } window.addEventListener('cardviewbeforeshow', onBeforeShow); }); clock.tick(TICK_SHOW_HIDE_MS); }); test('Should hide the overflow during the opening transition', (done) => { sinon.stub(TaskManagerUtils, 'waitForScreenToBeReady') .returns(Promise.resolve()); tm.hide().then(() => { var show = tm.show(); clock.tick(TICK_SHOW_HIDE_MS); return Promise.resolve().then(() => { assert.equal(tm.scrollElement.style.overflowX, 'hidden'); return show; }); }).then(() => { assert.equal(tm.scrollElement.style.overflowX, 'scroll'); }).then(done, done); clock.tick(TICK_SHOW_HIDE_MS); }); test('respondToHierarchyEvent should return true during waitForAppToClose', (done) => { tm.hide().then(() => { var waitForAppToClose = TaskManagerUtils.waitForAppToClose; TaskManagerUtils.waitForAppToClose = function() { TaskManagerUtils.waitForAppToClose = waitForAppToClose; assert.isTrue(tm.respondToHierarchyEvent({ type: 'home' })); return Promise.resolve(); }; var show = tm.show(); clock.tick(TICK_SHOW_HIDE_MS); return show; }).then(done, done); clock.tick(TICK_SHOW_HIDE_MS); }); test('Should not show if already showing', (done) => { var spyBeforeShow = spyEvent(window, 'cardviewbeforeshow'); assert.isTrue(tm.isShown()); // (noting that we're already shown) tm.show().then(() => { assert.isFalse(spyBeforeShow.called); done(); }); }); test('Should not respond to holdhome if already showing', () => { var showSpy = sinon.spy(tm, 'show'); tm.respondToHierarchyEvent(new CustomEvent('holdhome')); assert.isFalse(showSpy.called); }); test('Should open in response to holdhome from homescreen', (done) => { clock.restore(); tm.hide().then(() => { MockService.mockQueryWith( 'AppWindowManager.getActiveWindow', homescreenApp); var showSpy = sinon.spy(tm, 'show'); sinon.spy(homescreenApp, 'close'); var handleShown = () => { window.removeEventListener('cardviewshown', handleShown); try { assert.ok(homescreenApp.close.calledWith('home-to-cardview')); assert.isTrue(showSpy.called); assert.ok(tm.element.classList.contains('from-home')); done(); } catch(e) { done(e); } }; window.addEventListener('cardviewshown', handleShown); tm.respondToHierarchyEvent(new CustomEvent('holdhome')); }); }); test('tapping the home button when open opens the homescreen', (done) => { sinon.spy(tm, 'hide'); homescreenApp.open = function(why) { try { assert.ok(tm.hide.calledOnce); assert.equal(why, 'home-from-cardview'); done(); } catch(e) { done(e); } }; tm.respondToHierarchyEvent(new CustomEvent('home')); }); test('hit "home" while already closing, ignore event (bug 1203772)', () => { sinon.spy(tm, 'hide'); tm.hide(); // If we receive a "home" event while already going home, we should // ignore it. (i.e. in this implementation, _isTransitioning is true) var result = tm.respondToHierarchyEvent(new CustomEvent('home')); assert.equal(result, true); assert.ok(tm.hide.calledOnce); }); test('click when empty takes you home', () => { sinon.spy(tm, 'hide'); tm.element.dispatchEvent(new CustomEvent('click')); clock.tick(TICK_SHOW_HIDE_MS); assert.ok(tm.hide.calledOnce); }); }); suite('With Populated Task Manager', function() { var tm; var apps; var homescreenApp; var APP_CONFIGS = {}; var APP_NAMES = [ 'sms', 'game', 'browser1', 'game2', 'browser2', 'search' ]; APP_NAMES.forEach((name) => { APP_CONFIGS[name] = { launchTime: 5, name: name, origin: `http://${name}.gaiamobile.org`, frame: document.createElement('div'), manifest: { orientation: 'portrait-primary' }, rotatingDegree: 0, requestScreenshotURL() { return null; }, killable() { return true; }, getScreenshot(cb) { cb(); }, blur: function() {} }; }); setup(function(done) { MockStackManager.mStack = []; homescreenApp = new HomescreenWindow('home'); MockService.mockQueryWith('getHomescreen', homescreenApp); MockService.mockQueryWith('fetchCurrentOrientation', 'portrait-primary'); MockService.mockQueryWith('defaultOrientation', 'portrait-primary'); apps = {}; var app; APP_NAMES.forEach((appName) => { app = apps[appName] = new AppWindow(APP_CONFIGS[appName]); MockStackManager.mStack.push(app); }); apps.search.manifest.role = 'search'; apps.browser1.isBrowser = () => true; apps.browser2.isBrowser = () => true; MockService.mockQueryWith('getTopMostWindow', apps.search); MockStackManager.mCurrent = MockStackManager.mStack.length - 1; tm = new TaskManager(); this.sinon.spy(tm, 'getCurrentIndex'); tm.start().then(() => tm.show()).then(() => { done(); }, done); clock.tick(TICK_SHOW_HIDE_MS); }); teardown(function(done) { tm.hide().then(() => tm.stop()).then(done, done); clock.tick(TICK_SHOW_HIDE_MS); }); test('Proper state', () => { assert.isTrue(tm.isShown()); assert.ok(document.querySelector('#task-manager.active:not(.empty)')); assert.equal( MockStackManager.getCurrent(), tm.currentCard.app); }); test('should load icons before "cardviewshown"', done => { tm.hide().then(() => tm.stop()); clock.tick(TICK_SHOW_HIDE_MS); tm = new TaskManager(); var realUpdateStack = tm.updateStack.bind(tm); tm.updateStack = arg => { var result = realUpdateStack(arg); tm.appToCardMap.forEach((card, app) => { sinon.stub(card, 'loadIcon'); }); return result; }; var onShow = () => { window.removeEventListener('cardviewshown', onShow); done(() => { tm.updateStack = realUpdateStack; tm.appToCardMap.forEach((card, app) => { assert.isTrue(card.loadIcon.calledOnce); card.loadIcon.restore(); }); }); }; window.addEventListener('cardviewshown', onShow); tm.start().then(() => tm.show()); clock.tick(TICK_SHOW_HIDE_MS); }); test('should not query the currentIndex for the initial launch (reflow)', function() { // Called once for the scrollEvent once we set the overflow sinon.assert.calledOnce(tm.getCurrentIndex); }); test('Proper accessibility attributes for cards', function() { assert.equal( tm.currentCard.element.getAttribute('aria-labelledby'), tm.currentCard.titleId); var allCards = document.querySelectorAll('.card'); assert.equal(allCards.length, MockStackManager.mStack.length); for (var i = 0; i < allCards.length; i++) { var cardEl = allCards[i]; assert.equal( cardEl.getAttribute('aria-hidden'), cardEl === tm.currentCard.element ? 'false' : 'true'); assert.equal(cardEl.getAttribute('aria-setsize'), allCards.length + ''); assert.equal(cardEl.getAttribute('aria-posinset'), (i + 1).toString()); assert.equal(cardEl.getAttribute('role'), 'presentation'); } }); test('Wheel handling for accessibility', (done) => { var card = tm.currentCard; var killAppStub = sinon.stub(card.app, 'kill'); sinon.stub(card.app, 'killable', () => true); window.dispatchEvent(new WheelEvent('wheel', { deltaMode: 2, deltaY: 1 })); assert.isTrue(killAppStub.called); done(); }); test('wheel left/right event should change the current card', function() { var index = tm.stack.indexOf(tm.currentCard.app); assert.equal(index, tm.stack.length - 1); var previousCard = tm.currentCard; // Go left first, since we're at the end of the stack window.dispatchEvent(new WheelEvent('wheel', { deltaMode: 2, deltaX: -1 })); assert.equal(tm.currentCard.app, tm.stack[index - 1]); assert.equal(tm.currentCard.element.getAttribute('aria-hidden'), 'false'); assert.equal(previousCard.element.getAttribute('aria-hidden'), 'true'); previousCard = tm.currentCard; // Go right window.dispatchEvent(new WheelEvent('wheel', { deltaMode: 2, deltaX: 1 })); assert.equal(tm.currentCard.app, tm.stack[index]); assert.equal(tm.currentCard.element.getAttribute('aria-hidden'), 'false'); assert.equal(previousCard.element.getAttribute('aria-hidden'), 'true'); previousCard = tm.currentCard; // Cannot scroll past the final card window.dispatchEvent(new WheelEvent('wheel', { deltaMode: 2, deltaX: 1 })); assert.equal(tm.currentCard.app, tm.stack[index]); assert.equal(tm.currentCard.element.getAttribute('aria-hidden'), 'false'); }); test('hide destroys each card properly', function(done) { var spies = []; tm.stack.forEach((app) => { spies.push(sinon.spy(app, 'leaveTaskManager')); }); tm.hide().then(() => { spies.forEach((spy) => { assert.ok(spy.called); }); }).then(done, done); clock.tick(TICK_SHOW_HIDE_MS); }); test('close and reopen', function(done) { tm.hide().then(() => { MockStackManager.mCurrent = 0; var promise = tm.show(); clock.tick(TICK_SHOW_HIDE_MS); return promise; }).then(() => { assert.equal( MockStackManager.getCurrent(), tm.currentCard.app); }).then(done, done); clock.tick(TICK_SHOW_HIDE_MS); }); suite('settings > ', function() { test('observes settings at startup', function(done) { tm.stop(); var spy = this.sinon.spy(MockSettingsListener, 'observe'); tm.start().then(() => { assert.equal(spy.callCount, 1); done(); }); }); test('observes screenshots setting updates', function() { var SETTING_KEY = tm.USE_SCREENSHOTS_SETTING; MockSettingsListener.mTriggerCallback(SETTING_KEY, true); assert.isFalse(tm.disableScreenshots, 'disableScreenshots is true when setting is false'); MockSettingsListener.mTriggerCallback(SETTING_KEY, false); assert.isTrue(tm.disableScreenshots, 'disableScreenshots is false when setting is true'); }); }); test('disableScreenshots = true', function(done) { tm.hide().then(() => { tm.disableScreenshots = true; MockStackManager.mCurrent = 0; var promise = tm.show(); clock.tick(TICK_SHOW_HIDE_MS); return promise; }).then(() => { assert.ok(tm.currentCard.element.classList.contains('appIconPreview')); }).then(done, done); clock.tick(TICK_SHOW_HIDE_MS); }); test('disableScreenshots = false', function(done) { tm.hide().then(() => { tm.disableScreenshots = false; MockStackManager.mCurrent = 0; var promise = tm.show(); clock.tick(TICK_SHOW_HIDE_MS); return promise; }).then(() => { assert.ok(!tm.currentCard.element.classList.contains('appIconPreview')); }).then(done, done); clock.tick(TICK_SHOW_HIDE_MS); }); function getExpectedCardPlacement(element, position) { var margins = window.innerWidth - tm.cardWidth; return margins / 2 + position * (tm.cardWidth + tm.CARD_GUTTER); } test('placement', function() { var numCards = tm.cardsList.children.length; var margins = window.innerWidth - tm.cardWidth; var expectedWidth = numCards * tm.cardWidth + (numCards - 1) * tm.CARD_GUTTER + margins; assert.equal(tm.cardsList.style.width, expectedWidth + 'px'); for(var idx = 0; idx < tm.cardsList.children.length; idx++) { var expected = getExpectedCardPlacement(tm.cardsList.children[idx], idx); assert.equal( tm.cardsList.children[idx].style.transform, `translate(${expected}px, calc(50% + 0px))`); } }); test('StackManager.outOfStack', function(done) { var outOfStackApp = apps[APP_NAMES[5]]; sinon.spy(outOfStackApp, 'open'); tm.hide().then(() => { MockStackManager.mOutOfStack = true; MockStackManager.mStack.length = 3; MockStackManager.mCurrent = 2; var promise = tm.show(); clock.tick(TICK_SHOW_HIDE_MS); return promise; }).then(() => { // current card should be the last in the stack assert.equal( MockStackManager.getCurrent(), tm.currentCard.app); var promise = tm.hide(outOfStackApp); clock.tick(TICK_SHOW_HIDE_MS); return promise; }).then(() => { assert.ok(outOfStackApp.open.calledOnce); assert.ok(MockStackManager.position, -1); }).then(done, done); clock.tick(TICK_SHOW_HIDE_MS); }); suite('center apps', function() { setup(function(done) { tm.hide().then(() => { MockService.mockQueryWith('getTopMostWindow', apps.search); MockStackManager.mCurrent = MockStackManager.mStack.length - 1; return tm.show(); }).then(() => { done(); }, done); clock.tick(TICK_SHOW_HIDE_MS); }); test('initial centering', function() { // test that the current card gets centered var currentApp = MockStackManager.mStack[MockStackManager.mCurrent]; var elem = tm.currentCard.element; assert.equal(currentApp, tm.currentCard.app); var expectedLeft = getExpectedCardPlacement( elem, MockStackManager.mCurrent ); assert.equal(tm.currentCard.element.style.transform, `translate(${expectedLeft}px, calc(50% + 0px))`); }); test('centering at different stack position after closing', function(done) { // close and re-open the task manager at a different stack position tm.hide(apps.game).then(() => { // open again at position 0. MockStackManager.mCurrent = 0; return tm.show(); }).then(() => { var currentApp = MockStackManager.mStack[0]; var elem = tm.currentCard.element; assert.equal(currentApp, tm.currentCard.app); var expectedLeft = getExpectedCardPlacement(elem, MockStackManager.mCurrent); assert.equal(tm.currentCard.element.style.transform, `translate(${expectedLeft}px, calc(50% + 0px))`); }).then(done, done); clock.tick(TICK_SHOW_HIDE_MS); }); }); test('takes you home after closing all cards', (done) => { MockStackManager.mStack.length = 0; sinon.spy(tm, 'hide'); window.dispatchEvent(new CustomEvent('appterminated')); clock.tick(TICK_SHOW_HIDE_MS); assert.ok(tm.hide.calledOnce); done(); }); test('kill cards and update in response to StackManager', function() { MockStackManager.mStack.length = 2; window.dispatchEvent(new CustomEvent('appterminated')); clock.tick(TICK_SHOW_HIDE_MS); assert.equal( document.querySelectorAll('.card').length, MockStackManager.mStack.length ); }); test('hide by tapping on the current card', function(done) { // Tapping on the current card should trigger app.open('from-cardview') // as well as hide the task manager. var card = tm.currentCard; this.sinon.spy(tm, 'hide'); card.app.open = function(how) { try { assert.equal(how, 'from-cardview'); assert.ok(tm.hide.calledWith(card.app)); done(); } catch(e) { done(e); } }; card.element.click(); }); test('hide by tapping on a _different_ (non-current) card', function(done) { // Tapping on a non-current card, we should first scroll to that app, // and then open the app and hide the task manager. var card = tm.appToCardMap.get(tm.stack[0]); this.sinon.spy(tm, 'hide'); this.sinon.spy(tm, 'panToApp'); // Sanity: make sure we didn't choose the current card by mistake assert.notEqual(card, tm.currentCard); card.app.open = function(how) { try { // When we finally open the card, we'll know that we've scrolled // to make it front-and-center when (card === tm.currentCard). assert.equal(how, 'from-cardview'); assert.equal(card, tm.currentCard); assert.ok(tm.panToApp.calledWith(card.app)); assert.ok(tm.hide.calledWith(card.app)); done(); } catch(e) { done(e); } }; card.element.click(); }); test('kill a card by clicking on the close button', function(done) { var card = tm.currentCard; card.app.kill = function() { done(); }; card.element.querySelector('.close-button').click(); clock.tick(TICK_SHOW_HIDE_MS); }); // These events should trigger an exit: [ 'lockscreen-appopened', 'attentionopened', 'appopen' ].forEach((NAME) => { test(`${NAME} should trigger hide`, function() { this.sinon.spy(tm, 'hide'); window.dispatchEvent(new CustomEvent(NAME)); clock.tick(TICK_SHOW_HIDE_MS); assert.ok(tm.hide.called); }); }); test('opening from homescreen', function(done) { // NOTE: The promise chains activated here make using sinon's clock // unnecessarily difficult, because sinon's clock does not magically make // Promises synchronous. To compensate for the increased intermittent // potential, we increase this test's timeout. clock.restore(); this.timeout(20000); // Ensure that the selected app ('game') is opened. var openStub = sinon.stub(apps.game, 'open'); tm.hide().then(() => { MockService.mockQueryWith( 'AppWindowManager.getActiveWindow', MockService.query('getHomescreen', true)); MockStackManager.mCurrent = -1; return tm.show(); }).then(() => { return tm.hide(apps.game); }).then(() => { assert.ok(openStub.calledOnce); }).then(done, done); }); test('no touch input handled while opening selected app', function(done) { this.sinon.spy(tm, 'handleEvent'); tm.hide(apps.game).then(() => { assert.isFalse(tm.handleEvent.called, 'handleEvent not called'); }).then(function() { done(); }, done); tm.element.dispatchEvent(new CustomEvent('touchstart')); clock.tick(TICK_SHOW_HIDE_MS); }); suite('card-will-drag / scroll axis lock', function() { test('handling card-will-drag (prevent scrolling)', function() { tm.scrollElement.style.overflowX = 'scroll'; // In this test, they did not scroll, so the card-will-drag event // should be passed through as-is, and we should set 'overflow: hidden'. var willDragEvent = new CustomEvent('card-will-drag', { detail: { firstTouchTimestamp: Date.now() }, bubbles: true, cancelable: true }); tm.currentCard.element.dispatchEvent(willDragEvent); assert.isFalse(willDragEvent.defaultPrevented); assert.equal(tm.scrollElement.style.overflowX, 'hidden'); }); test('handling card-will-drag (cancel event)', function() { tm.scrollElement.style.overflowX = 'scroll'; // In this test, they scrolled AFTER the first touch, meaning we // should prevent the card-will-drag event. tm.scrollElement.dispatchEvent(new CustomEvent('scroll')); var willDragEvent = new CustomEvent('card-will-drag', { detail: { firstTouchTimestamp: Date.now() - 1000 }, bubbles: true, cancelable: true }); tm.currentCard.element.dispatchEvent(willDragEvent); assert.isTrue(willDragEvent.defaultPrevented); assert.equal(tm.scrollElement.style.overflowX, 'scroll'); }); }); test('card-dropped, not killed', function() { tm.scrollElement.style.overflowX = 'hidden'; var dropEvent = new CustomEvent('card-dropped', { detail: { willKill: false }, bubbles: true }); tm.currentCard.element.dispatchEvent(dropEvent); assert.equal(tm.scrollElement.style.overflowX, 'scroll'); }); test('card-dropped, kill the app', function(done) { tm.scrollElement.style.overflowX = 'hidden'; var dropEvent = new CustomEvent('card-dropped', { detail: { willKill: true }, bubbles: true }); tm.currentCard.app.kill = function() { done(); }; tm.currentCard.element.dispatchEvent(dropEvent); assert.equal(tm.scrollElement.style.overflowX, 'scroll'); clock.tick(TICK_SHOW_HIDE_MS); }); test('browser-only filtering', function(done) { var allBrowserApps = [apps.browser1, apps.browser2, apps.search]; clock.restore(); this.timeout(20000); var newApp; tm.hide().then(() => { return tm.show({ browserOnly: true }); }).then(() => { assert.equal(tm.stack.length, allBrowserApps.length); window.addEventListener('cardviewclosed', (evt) => { newApp = evt.detail; }); return tm.hide(apps.browser1); }).then(() => { assert.equal( newApp, apps.browser1 ); }).then(done, done); }); suite('new sheet buttons >', function() { var NEW_SHEET_SELECTOR = '#task-manager-new-sheet-button'; var NEW_PRIVATE_SHEET_SELECTOR = '#task-manager-new-private-sheet-button'; setup(function() { window.AppWindow = function(config) { var app = new MockAppWindow(config); MockStackManager.mStack.push(app); return app; }; window.BrowserConfigHelper = function(config) { return config; }; }); function testSheetButton(name, { selector, isPrivate }) { test(name, function() { this.sinon.spy(tm, 'hide'); var originalStack = tm.stack.slice(); tm.element.querySelector(selector).click(); var newStack = tm.stack.slice(); var lastApp = newStack[newStack.length - 1]; assert.equal(newStack.length, originalStack.length + 1); assert.equal(lastApp.isPrivate ? true : false, isPrivate); assert.equal(tm.currentCard.app, lastApp); assert.ok(tm.hide.calledWith(lastApp, 'from-new-card')); }); } testSheetButton('open new sheet', { selector: NEW_SHEET_SELECTOR, isPrivate: false }); testSheetButton('open new private sheet', { selector: NEW_PRIVATE_SHEET_SELECTOR, isPrivate: true }); test('only opens one sheet, even if called multiple times', function() { this.sinon.spy(tm, 'hide'); var button = tm.element.querySelector(NEW_SHEET_SELECTOR); button.click(); button.click(); button.click(); assert.ok(tm.hide.calledOnce); }); }); }); });