/* global StackManager, AppWindow, Event, MocksHelper, MockService, MockSheetsTransition */ 'use strict'; requireApp('system/js/stack_manager.js'); requireApp('system/test/unit/mock_app_window.js'); requireApp('system/shared/test/unit/mocks/mock_service.js'); requireApp('system/test/unit/mock_sheets_transition.js'); var mocksForStackManager = new MocksHelper([ 'AppWindow', 'SheetsTransition', 'Service' ]).init(); suite('system/StackManager >', function() { var dialer, contact, settings, google, system, operatorVariant; var contact_sheet_1, contact_sheet_2; var settings_sheet_1, settings_sheet_2, settings_sheet_3; mocksForStackManager.attachTestHelpers(); setup(function() { StackManager.start(); this.sinon.useFakeTimers(); dialer = new AppWindow({ url: 'app://communications.gaiamobile.org/dialer/index.html', origin: 'app://communications.gaiamobile.org/', manifestURL: 'app://communications.gaiamobile.org/dialer/manifest.webapp', name: 'Dialer' }); contact = new AppWindow({ url: 'app://communications.gaiamobile.org/contact/index.html', origin: 'app://communications.gaiamobile.org/', manifestURL: 'app://communications.gaiamobile.org/contact/manifest.webapp', name: 'Contact' }); settings = new AppWindow({ url: 'app://settings.gaiamobile.org/index.html', origin: 'app://settings.gaiamobile.org/', manifestURL: 'app://settings.gaiamobile.org/manifest.webapp', name: 'Settings' }); google = new AppWindow({ url: 'http://google.com/index.html', origin: 'http://google.com' }); system = new AppWindow({ url: 'app://system.gaiamobile.org/index.html', origin: 'app://system.gaiamobile.org/', manifestURL: 'app://system.gaiamobile.org/contact/manifest.webapp', name: 'System', manifest: { role: 'system' } }); operatorVariant = new AppWindow({ url: 'app://opvariant.gaiamobile.org/index.html', origin: 'app://opvariant.gaiamobile.org/', manifestURL: 'app://opvariant.gaiamobile.org/contact/manifest.webapp', name: 'Operator Variant', manifest: { role: 'system' } }); contact_sheet_1 = new AppWindow({ url: 'app://communications.gaiamobile.org/contact/sheet1.html', origin: 'app://communications.gaiamobile.org/', manifestURL: 'app://communications.gaiamobile.org/contact/manifest.webapp', name: 'Contact', previousWindow: contact }); contact_sheet_2 = new AppWindow({ url: 'app://communications.gaiamobile.org/contact/sheet1.html', origin: 'app://communications.gaiamobile.org/', manifestURL: 'app://communications.gaiamobile.org/contact/manifest.webapp', name: 'Contact', previousWindow: contact_sheet_1 }); settings_sheet_1 = new AppWindow({ url: 'app://settings.gaiamobile.org/sheet1.html', origin: 'app://settings.gaiamobile.org/', manifestURL: 'app://settings.gaiamobile.org/manifest.webapp', name: 'Settings', previousWindow: settings }); settings_sheet_2 = new AppWindow({ url: 'app://settings.gaiamobile.org/sheet1.html', origin: 'app://settings.gaiamobile.org/', manifestURL: 'app://settings.gaiamobile.org/manifest.webapp', name: 'Settings', previousWindow: settings_sheet_1 }); settings_sheet_3 = new AppWindow({ url: 'app://settings.gaiamobile.org/sheet1.html', origin: 'app://settings.gaiamobile.org/', manifestURL: 'app://settings.gaiamobile.org/manifest.webapp', name: 'Settings', previousWindow: settings_sheet_2 }); contact_sheet_1.groupID = contact.groupID; contact_sheet_2.groupID = contact.groupID; settings_sheet_1.groupID = settings.groupID; settings_sheet_2.groupID = settings.groupID; settings_sheet_3.groupID = settings.groupID; }); teardown(function() { this.sinon.clock.tick(800); // Making sure everything got broadcasted StackManager.__clearAll(); }); function appLaunch(app, warm) { if (!warm) { var evt = document.createEvent('CustomEvent'); evt.initCustomEvent('appcreated', true, false, app); window.dispatchEvent(evt); } var evt2 = document.createEvent('CustomEvent'); evt2.initCustomEvent('launchapp', true, false, app.config); window.dispatchEvent(evt2); } function wrapperLaunch(app, warm) { if (!warm) { var evt = document.createEvent('CustomEvent'); evt.initCustomEvent('appcreated', true, false, app); window.dispatchEvent(evt); } var evt2 = document.createEvent('CustomEvent'); evt2.initCustomEvent('launchapp', true, false, app.config); window.dispatchEvent(evt2); } function appCrash(app) { var evt = document.createEvent('CustomEvent'); evt.initCustomEvent('appterminated', true, false, { origin: app.origin, manifestURL: app.manifestURL, instanceID: app.instanceID }); window.dispatchEvent(evt); } function home() { window.dispatchEvent(new Event('homescreenopening')); } function appOpening(app) { var evt = document.createEvent('CustomEvent'); evt.initCustomEvent('appopening', true, false, app); window.dispatchEvent(evt); } function appOpened(app) { var evt = document.createEvent('CustomEvent'); evt.initCustomEvent('appopened', true, false, app); window.dispatchEvent(evt); } suite('Homescreen', function() { setup(function() { home(); }); test('the position indicates we are on the homescreen', function() { assert.equal(StackManager.position, -1, 'position should be -1'); }); }); suite('Stack vs hidden apps', function() { setup(function() { appLaunch(system); appLaunch(operatorVariant); }); test('role=system apps should never be in the stack', function() { StackManager.snapshot().forEach(function(app) { if (app.manifest) { assert.notEqual(app.manifest.role, 'system'); } }); }); suite('when the app currently displayed is not part of the stack', function() { setup(function() { appLaunch(settings); appLaunch(operatorVariant); MockService.mockQueryWith('AppWindowManager.getActiveWindow', operatorVariant); this.sinon.stub(settings, 'getActiveWindow').returns(null); }); test('getCurrent should still work to allow event forwarding', function() { assert.equal(StackManager.getCurrent(), operatorVariant); }); test('outOfStack should be true since we don\'t know where we are', function() { assert.isTrue(StackManager.outOfStack()); }); test('outOfStack should not throw if this.position === -1', function() { StackManager.position = -1; assert.isTrue(StackManager.outOfStack()); }); suite('but to prevent undefined swiping behaviors', function() { test('getPrev should return undefined', function() { assert.isUndefined(StackManager.getPrev()); }); test('getNext should return undefined', function() { assert.isUndefined(StackManager.getNext()); }); }); }); teardown(function() { StackManager.__clearAll(); }); }); test('_didntMove by default is true', function() { appLaunch(dialer); this.sinon.stub(dialer, 'setNFCFocus'); StackManager.commit(); assert.isTrue(dialer.setNFCFocus.calledWith(true)); }); suite('Cards View Events', function() { setup(function() { appLaunch(dialer); appLaunch(contact); appLaunch(settings); }); test('stack position updates on cardviewclosed', function() { StackManager.position = 0; assert.equal(StackManager.position, 0, 'wrong starting position'); var cardClosedEvent = new CustomEvent('cardviewclosed', { 'detail': dialer }); StackManager.handleEvent(cardClosedEvent); assert.equal(StackManager.position, 2, 'new position is end of stack'); assert.equal(StackManager.getCurrent(), dialer, 'app is at top of stack'); }); test('and does not cause the position to stringify', function() { assert.equal(StackManager.position, 2, 'wrong starting position'); StackManager.position = '2'; StackManager.goPrev(); StackManager.goNext(); assert.deepEqual(StackManager.getCurrent(), settings); assert.equal(StackManager.position, 2, 'wrong end position'); }); teardown(function() { StackManager.__clearAll(); }); }); suite('Moving through history', function() { setup(function() { appLaunch(dialer); appLaunch(contact); appLaunch(settings); appLaunch(system); }); suite('> snapshot()', function() { test('snapshot returned should equal stack internal value', function() { var snapshot = StackManager.snapshot(); assert.deepEqual(snapshot, StackManager._stack); }); }); suite('> goPrev()', function() { test('should move back in the stack without modifying it', function() { StackManager.goPrev(); assert.deepEqual(StackManager.getCurrent().config, contact.config); assert.deepEqual(StackManager.getNext().config, settings.config); assert.deepEqual(StackManager.getPrev().config, dialer.config); }); test('and the position should be updated properly', function() { assert.equal(StackManager.position, 2); }); test('should do nothing when we\'re at the bottom of the stack', function() { StackManager.goPrev(); StackManager.goPrev(); assert.deepEqual(StackManager.getCurrent().config, dialer.config); StackManager.goPrev(); assert.deepEqual(StackManager.getCurrent().config, dialer.config); }); test('and the position should not change', function() { assert.equal(StackManager.position, 2); }); }); suite('> goNext()', function() { setup(function() { StackManager.goPrev(); assert.isFalse(StackManager._didntMove); StackManager.goPrev(); assert.isFalse(StackManager._didntMove); }); test('should move forward in the stack without modifying it', function() { StackManager.goNext(); assert.deepEqual(StackManager.getCurrent().config, contact.config); assert.deepEqual(StackManager.getNext().config, settings.config); assert.deepEqual(StackManager.getPrev().config, dialer.config); }); test('it should dispatch a stackchanged event after a delay', function(done) { window.addEventListener('stackchanged', function onStackChanged(evt) { window.removeEventListener('stackchanged', onStackChanged); var detail = evt.detail; assert.equal(detail.position, 1); assert.equal(detail.sheets.length, 3); assert.deepEqual(detail.sheets[0].config, dialer.config); assert.deepEqual(detail.sheets[1].config, contact.config); assert.deepEqual(detail.sheets[2].config, settings.config); done(); }); StackManager.goNext(); this.sinon.clock.tick(800); }); test('the position should be updated properly', function() { assert.equal(StackManager.position, 0); }); test('should do nothing when we\'re at the top of the stack', function() { StackManager.goNext(); assert.isFalse(StackManager._didntMove); StackManager.goNext(); assert.isTrue(StackManager._didntMove); assert.deepEqual(StackManager.getCurrent().config, settings.config); StackManager.goNext(); assert.deepEqual(StackManager.getCurrent().config, settings.config); }); }); test('going back to the start app', function() { var onSheetsGestureEnd = this.sinon.spy(); window.addEventListener('sheets-gesture-end', onSheetsGestureEnd); StackManager.goPrev(); StackManager.goNext(); assert.isTrue(StackManager._didntMove); StackManager.commit(); sinon.assert.calledOnce(onSheetsGestureEnd); this.sinon.clock.tick(800); sinon.assert.calledOnce(onSheetsGestureEnd); window.removeEventListener('sheets-gesture-end', onSheetsGestureEnd); }); suite('> blasting through history', function() { var dialerBroadcast, contactBroadcast, settingsBroadcast; var dialerQueueShow, contactCancelQueuedShow, settingsQueueHide; setup(function() { dialerBroadcast = this.sinon.stub(dialer, 'broadcast'); contactBroadcast = this.sinon.stub(contact, 'broadcast'); settingsBroadcast = this.sinon.stub(settings, 'broadcast'); dialerQueueShow = this.sinon.stub(dialer, 'queueShow'); contactCancelQueuedShow = this.sinon.stub(contact, 'cancelQueuedShow'); settingsQueueHide = this.sinon.stub(settings, 'queueHide'); this.sinon.stub(MockService, 'request'); StackManager.goPrev(); assert.isFalse(StackManager._didntMove); StackManager.goPrev(); assert.isFalse(StackManager._didntMove); }); test('it should flag the next active app', function() { sinon.assert.calledOnce(dialerQueueShow); }); test('it should unflag an app we passed over', function() { sinon.assert.calledOnce(contactCancelQueuedShow); }); test('it should flag the app that will become inactive', function() { sinon.assert.calledOnce(settingsQueueHide); }); test('it should broadcast only 1 app change', function() { sinon.assert.notCalled(dialerBroadcast); sinon.assert.notCalled(contactBroadcast); sinon.assert.notCalled(settingsBroadcast); this.sinon.clock.tick(800); sinon.assert.calledWith(dialerBroadcast, 'swipein'); sinon.assert.notCalled(contactBroadcast); sinon.assert.calledWith(settingsBroadcast, 'swipeout'); }); test('it should request stopRecording', function() { assert.isTrue(MockService.request.calledWith('stopRecording')); }); suite('if we\'re back to the same place', function() { setup(function() { StackManager.goNext(); StackManager.goNext(); assert.isTrue(StackManager._didntMove); }); test('it should just cleanup the transition classes', function() { var clearSpy = this.sinon.spy(settings.transitionController, 'clearTransitionClasses'); this.sinon.clock.tick(800); sinon.assert.calledOnce(clearSpy); }); }); suite('if we\'re still transitioning after the timeout', function() { setup(function() { MockSheetsTransition.transitioning = true; this.sinon.clock.tick(800); }); test('commit should then broadcast', function() { sinon.assert.notCalled(dialerBroadcast); sinon.assert.notCalled(contactBroadcast); sinon.assert.notCalled(settingsBroadcast); MockSheetsTransition.transitioning = false; StackManager.commit(); sinon.assert.calledWith(dialerBroadcast, 'swipein'); sinon.assert.notCalled(contactBroadcast); sinon.assert.calledWith(settingsBroadcast, 'swipeout'); }); }); }); }); suite('When an app is launched', function() { setup(function() { appLaunch(dialer); }); test('it should become the current stack item', function() { assert.deepEqual(StackManager.getCurrent().config, dialer.config); }); test('it should set the position when opened', function() { var fakePosition = 10; assert.equal(StackManager.position, 0); this.sinon.stub(StackManager, '_indexOfInstanceID').returns(fakePosition); appOpened(dialer); assert.equal(StackManager.position, fakePosition); }); suite('then another app is launched', function() { setup(function() { appLaunch(contact); }); test('it should go on top of the stack', function() { assert.deepEqual(StackManager.getPrev().config, dialer.config); assert.deepEqual(StackManager.getCurrent().config, contact.config); }); suite('then we go back and launch a third app', function() { setup(function() { StackManager.goPrev(); appLaunch(settings); }); test('the current app at the time of the launch should move to the top', function() { assert.deepEqual(StackManager.getPrev(), dialer); }); test('the new app should go on the top', function() { assert.deepEqual(StackManager.getCurrent(), settings); }); }); }); suite('if it\'s already in the stack', function() { setup(function() { appLaunch(contact); appLaunch(settings); }); test('it should go on top of the stack', function() { appLaunch(dialer, true); assert.deepEqual(StackManager.getCurrent().config, dialer.config); assert.deepEqual(StackManager.getPrev().config, settings.config); }); test('and the position should be updated properly', function() { assert.equal(StackManager.position, 2); }); test('it should bring the current app on top too', function() { StackManager.goPrev(); appLaunch(dialer, true); assert.deepEqual(StackManager.getPrev(), contact); }); test('it should not be duplicated', function() { appLaunch(dialer, true); assert.deepEqual(StackManager.getCurrent().config, dialer.config); assert.deepEqual(StackManager.getPrev().config, settings.config); StackManager.goPrev(); assert.deepEqual(StackManager.getCurrent().config, settings.config); assert.deepEqual(StackManager.getPrev().config, contact.config); StackManager.goPrev(); assert.deepEqual(StackManager.getCurrent().config, contact.config); assert.isUndefined(StackManager.getPrev()); }); test('it should dispatch a stackchanged event', function(done) { window.addEventListener('stackchanged', function onStackChanged(evt) { window.removeEventListener('stackchanged', onStackChanged); var detail = evt.detail; assert.equal(detail.position, 2); assert.equal(detail.sheets.length, 3); assert.deepEqual(detail.sheets[0].config, contact.config); assert.deepEqual(detail.sheets[1].config, settings.config); assert.deepEqual(detail.sheets[2].config, dialer.config); done(); }); appLaunch(dialer, true); }); }); suite('if it\'s launched in background', function() { setup(function() { settings.stayBackground = true; settings.config.stayBackground = true; appLaunch(settings); }); teardown(function() { settings.stayBackground = false; settings.config.stayBackground = false; }); test('it should go at the bottom of the stack while keeping the current', function() { assert.deepEqual(StackManager.getCurrent().config, dialer.config); assert.deepEqual(StackManager.getPrev().config, settings.config); }); test('and the position should remain the same', function() { assert.equal(StackManager.position, 1); }); suite('and the stack is empty', function() { setup(function() { StackManager.__clearAll(); appLaunch(settings); }); test('it should go at the bottom of the stack and become the current', function() { assert.deepEqual(StackManager.getCurrent().config, settings.config); assert.isUndefined(StackManager.getPrev()); assert.isUndefined(StackManager.getNext()); }); test('and the position should be 0', function() { assert.equal(StackManager.position, 0); }); }); }); }); suite('When a wrapper is launched', function() { setup(function() { wrapperLaunch(google); }); test('it should become the current stack item', function() { assert.deepEqual(StackManager.getCurrent().config, google.config); }); suite('if it\'s already in the stack', function() { setup(function() { appLaunch(settings); }); test('it should go on top of the stack', function() { wrapperLaunch(google, true); assert.deepEqual(StackManager.getCurrent().config, google.config); assert.deepEqual(StackManager.getPrev().config, settings.config); }); test('it should not be duplicated', function() { wrapperLaunch(google, true); assert.deepEqual(StackManager.getCurrent().config, google.config); assert.deepEqual(StackManager.getPrev().config, settings.config); StackManager.goPrev(); assert.deepEqual(StackManager.getCurrent().config, settings.config); assert.isUndefined(StackManager.getPrev()); }); }); }); suite('When an app terminates', function() { setup(function() { appLaunch(dialer); appLaunch(settings); appLaunch(contact); }); suite('if it\'s the current app', function() { setup(function() { appCrash(contact); }); test('it should be removed from the stack', function() { assert.equal(StackManager.length, 2); }); test('the position should be updated properly', function() { assert.equal(StackManager.position, 1); }); test('the current should go back', function() { assert.deepEqual(StackManager.getPrev().config, dialer.config); assert.deepEqual(StackManager.getCurrent().config, settings.config); }); }); suite('if it\'s an app deeper in the stack', function() { setup(function() { appCrash(dialer); }); test('it should be removed from the stack', function() { assert.equal(StackManager.length, 2); }); test('the position should be updated properly', function() { assert.equal(StackManager.position, 1); }); test('the current should go back', function() { assert.deepEqual(StackManager.getPrev().config, settings.config); assert.deepEqual(StackManager.getCurrent().config, contact.config); }); }); suite('if it\'s an app above in the stack', function() { setup(function() { StackManager.goPrev(); appCrash(contact); }); test('it should be removed from the stack', function() { assert.equal(StackManager.length, 2); }); test('the position should not change', function() { assert.equal(StackManager.position, 1); }); test('the current should not move', function() { assert.deepEqual(StackManager.getPrev().config, dialer.config); assert.deepEqual(StackManager.getCurrent().config, settings.config); }); }); }); suite('When the last app terminates', function() { setup(function() { appLaunch(contact); appCrash(contact); }); test('the position should be -1', function() { assert.equal(StackManager.position, -1); }); test('the stack should be empty', function() { assert.equal(StackManager._stack.length, 0); }); }); test('open app from card should update the ordering', function() { appLaunch(dialer); appLaunch(contact); appLaunch(settings); appOpening(dialer); assert.deepEqual(StackManager.getCurrent().config, dialer.config); }); suite('In-app sheets support', function() { setup(function() { appLaunch(dialer); appLaunch(contact); appLaunch(settings); }); test('goPrev() should go the the parent window if there is one', function() { var stub1 = this.sinon.stub(settings, 'getActiveWindow'); stub1.returns(settings_sheet_2); var stub2 = this.sinon.stub(settings_sheet_2, 'getPrev'); stub2.returns(settings_sheet_1); var stubBroadcast1 = this.sinon.stub(settings_sheet_1, 'broadcast'); var stubBroadcast2 = this.sinon.stub(settings_sheet_2, 'broadcast'); StackManager.goPrev(); this.sinon.clock.tick(800); assert.isTrue(stubBroadcast1.calledWith('swipein')); assert.isTrue(stubBroadcast2.calledWith('swipeout')); }); test('goNext() should go to the child window if there is one', function() { var stub1 = this.sinon.stub(settings, 'getActiveWindow'); stub1.returns(settings_sheet_2); var stub2 = this.sinon.stub(settings_sheet_2, 'getNext'); stub2.returns(settings_sheet_3); var stubBroadcast1 = this.sinon.stub(settings_sheet_2, 'broadcast'); var stubBroadcast2 = this.sinon.stub(settings_sheet_3, 'broadcast'); StackManager.goNext(); this.sinon.clock.tick(800); assert.isTrue(stubBroadcast1.calledWith('swipeout')); assert.isTrue(stubBroadcast2.calledWith('swipein')); }); test('goNext() should go to the next app root window if we are on a leaf', function() { StackManager.goPrev(); this.sinon.clock.tick(800); var stub1 = this.sinon.stub(settings, 'getRootWindow'); stub1.returns(settings); var stub2 = this.sinon.stub(contact, 'getActiveWindow'); stub2.returns(contact_sheet_2); var stubBroadcast1 = this.sinon.stub(settings, 'broadcast'); var stubBroadcast2 = this.sinon.stub(contact_sheet_2, 'broadcast'); StackManager.goNext(); this.sinon.clock.tick(800); assert.isTrue(stubBroadcast2.calledWith('swipeout')); assert.isTrue(stubBroadcast1.calledWith('swipein')); }); test('goPrev() should go to the previous app leaf if we are on a root', function() { var stub1 = this.sinon.stub(contact, 'getLeafWindow'); stub1.returns(contact_sheet_2); var stub2 = this.sinon.stub(settings, 'getActiveWindow'); stub2.returns(settings); var stubBroadcast1 = this.sinon.stub(settings, 'broadcast'); var stubBroadcast2 = this.sinon.stub(contact_sheet_2, 'broadcast'); StackManager.goPrev(); this.sinon.clock.tick(800); assert.isTrue(stubBroadcast1.calledWith('swipeout')); assert.isTrue(stubBroadcast2.calledWith('swipein')); }); }); suite('When the home button is pressed', function() { setup(function() { appLaunch(dialer); appLaunch(contact); StackManager.goPrev(); }); test('the current position should be set to null', function() { home(); assert.isUndefined(StackManager.getCurrent()); }); test('the current sheet should move to the top of the stack', function() { assert.deepEqual(StackManager.getCurrent().config, dialer.config); home(); appLaunch(settings); assert.deepEqual(StackManager.getPrev().config, dialer.config); }); test('it should dispatch a stackchanged event', function(done) { window.addEventListener('stackchanged', function onStackChanged(evt) { window.removeEventListener('stackchanged', onStackChanged); var detail = evt.detail; assert.equal(detail.position, -1); assert.equal(detail.sheets.length, 2); assert.deepEqual(detail.sheets[0].config, contact.config); assert.deepEqual(detail.sheets[1].config, dialer.config); done(); }); home(); }); test('it should do an emergency broadcast to prevent race conditions', function() { var clearSpy = this.sinon.spy(settings.transitionController, 'clearTransitionClasses'); var broadcastSpy = this.sinon.spy(dialer, 'broadcast'); StackManager.goPrev(); home(); sinon.assert.calledWith(broadcastSpy, 'closed'); sinon.assert.calledOnce(clearSpy); this.sinon.clock.tick(800); sinon.assert.calledOnce(broadcastSpy); }); suite('if the stack is empty', function() { setup(function() { StackManager.__clearAll(); home(); }); test('it shouldn\'t do anything', function() { assert.equal(StackManager.length, 0); }); test('the position should be -1', function() { assert.equal(StackManager.position, -1); }); test('setting the position should do nothing', function() { StackManager.position = 2; assert.equal(StackManager.position, -1); }); suite('and we press home a second time', function() { setup(function() { home(); }); test('the stack should still be empty', function() { assert.equal(StackManager.length, 0); }); }); }); }); });