{"action":"created","comment":{"url":"https://api.github.com/repos/mozilla-b2g/gaia/pulls/comments/40320620","id":40320620,"diff_hunk":"@@ -1,1377 +1,570 @@\n-/* global MockStackManager, MockSettingsListener, MockService,\n-          TaskManager, Card, AppWindow,\n-          HomescreenWindow, MocksHelper, MockL10n */\n+/* global MockStackManager, MockService,\n+          TaskManager, AppWindow,\n+          HomescreenWindow, MockSettingsListener, MocksHelper, MockL10n */\n \n 'use strict';\n \n requireApp('system/test/unit/mock_app_window.js');\n requireApp('system/test/unit/mock_homescreen_window.js');\n requireApp('system/test/unit/mock_stack_manager.js');\n-requireApp('system/test/unit/mock_lazy_loader.js');\n \n require('/shared/js/event_safety.js');\n require('/shared/js/sanitizer.js');\n require('/shared/test/unit/mocks/mock_service.js');\n-require('/shared/test/unit/mocks/mock_settings_listener.js');\n require('/shared/test/unit/mocks/mock_l10n.js');\n+require('/shared/test/unit/mocks/mock_settings_listener.js');\n \n var mocksForTaskManager = new MocksHelper([\n   'StackManager',\n   'HomescreenWindow',\n   'AppWindow',\n   'Service',\n-  'LazyLoader',\n   'SettingsListener'\n ]).init();\n \n-function waitForEvent(target, name, timeout) {\n-  if (isNaN(timeout)) {\n-    timeout = 250;\n-  }\n-  var promise = new window.Promise(function(resolve, reject) {\n-    var timer = setTimeout(function() {\n-      reject(new Error('Timeout exceeded waiting for ' + name));\n-    }, timeout);\n-    target.addEventListener(name, function onEvent(evt){\n-      clearTimeout(timer);\n-      target.removeEventListener(name, onEvent);\n-      resolve(evt);\n-    });\n-  });\n-  return promise;\n-}\n-\n-function failOnReject(err) {\n-  if (err) {\n-    return err;\n-  }\n-  assert.isTrue(false, 'Should not reject');\n-}\n-\n suite('system/TaskManager >', function() {\n-  var fakeInnerWidth = 360;\n-  var fakeInnerHeight = 640;\n-\n-  var screenNode, realL10n;\n-  var cardsView, cardsList, noRecentWindowsNode;\n-  var innerHeightDescriptor, innerWidthDescriptor,\n-      scrollLeftDescriptor, scrollTopDescriptor,\n-      scrollToDescriptor;\n-\n-  function sendHoldhome() {\n-    var evt = new CustomEvent('holdhome', { });\n-    taskManager.respondToHierarchyEvent(evt);\n-  }\n \n-  function sendAppopen(detail) {\n-    detail = detail || {\n-      manifestURL: 'http://sms.gaiamobile.org/manifest.webapp',\n-      origin: 'http://sms.gaiamobile.org',\n-      isHomescreen: false\n-    };\n+  suiteSetup(mocksForTaskManager.suiteSetup);\n+  setup(mocksForTaskManager.setup);\n \n-    if (detail.element) {\n-      detail.element.dispatchEvent(new CustomEvent('_opened'));\n+  // MockLazyLoader invokes promise callbacks in the wrong order, but changing\n+  // it causes many other tests to fail. So for now, use a dead-simple shim:\n+  window.LazyLoader = {\n+    load(files) {\n+      return Promise.resolve();\n     }\n-\n-    var evt = new CustomEvent('appopen', { detail: detail });\n-    window.dispatchEvent(evt);\n-  }\n-\n-  function sendAppTerminated(detail) {\n-    var evt = new CustomEvent('appterminated', { detail: detail });\n-    window.dispatchEvent(evt);\n-  }\n-\n-  function showTaskManager(clock, stackFilter) {\n-    window.dispatchEvent(new CustomEvent('taskmanagershow', {\n-      detail: {\n-        filter: stackFilter\n-      }\n-    }));\n-    // We wait for the app to close\n-    window.dispatchEvent(new CustomEvent('appclosed'));\n-    // Then dispatch the cardviewshow event after a tick\n-    clock.tick();\n-  }\n-\n-  function fakeFinish(clock, app) {\n-    clock.tick(100); // smooth timeout\n-    app.element.dispatchEvent(new CustomEvent('_opened'));\n-    clock.tick(); // timeout before close event is dispatched\n-  }\n-\n-  var apps = {}, appConfigs, home;\n-  var taskManager;\n-  var scrollProperties = {};\n-\n-  mocksForTaskManager.attachTestHelpers();\n-\n-  suiteSetup(function cv_suiteSetup(done) {\n-    appConfigs = {\n-      'sms': {\n-        launchTime: 5,\n-        name: 'sms',\n-        frame: document.createElement('div'),\n-        manifest: {\n-          orientation: 'portrait-primary'\n-        },\n-        rotatingDegree: 0,\n-        requestScreenshotURL: function() {\n-          return null;\n-        },\n-        killable: function() {\n-          return true;\n-        },\n-        getScreenshot: function(callback) {\n-          callback();\n-        },\n-        origin: 'http://sms.gaiamobile.org',\n-        blur: function() {}\n-      },\n-      'game': {\n-        launchTime: 4,\n-        name: 'game',\n-        manifest: {\n-          orientation: 'landscape-primary'\n-        },\n-        rotatingDegree: 90,\n-        requestScreenshotURL: function() {\n-          return null;\n-        },\n-        getScreenshot: function(callback) {\n-          callback();\n-        },\n-        origin: 'http://game.gaiamobile.org',\n-        blur: function() {}\n-      },\n-      'browser1': {\n-        launchTime: 4,\n-        name: 'browser1',\n-        rotatingDegree: 0,\n-        isBrowser: function() {\n-          return true;\n-        },\n-        requestScreenshotURL: function() {\n-          return null;\n-        },\n-        getScreenshot: function(callback) {\n-          callback();\n-        },\n-        origin: 'http://browser1.somedomain.tld/path',\n-        blur: function() {}\n-      },\n-      'game2': {\n-        launchTime: 3,\n-        name: 'game2',\n-        manifest: {\n-          orientation: 'landscape-secondary'\n-        },\n-        rotatingDegree: 270,\n-        requestScreenshotURL: function() {\n-          return null;\n-        },\n-        getScreenshot: function(callback) {\n-          callback();\n-        },\n-        origin: 'http://game2.gaiamobile.org',\n-        blur: function() {}\n-      },\n-      'browser2': {\n-        name: 'browser2',\n-        rotatingDegree: 0,\n-        isBrowser: function() {\n-          return true;\n-        },\n-        requestScreenshotURL: function() {\n-          return null;\n-        },\n-        getScreenshot: function(callback) {\n-          callback();\n-        },\n-        origin: 'http://browser2.somedomain.tld/',\n-        blur: function() {}\n-      },\n-      'search': {\n-        name: 'search',\n-        rotatingDegree: 0,\n-        isBrowser: function() {\n-          return true;\n-        },\n-        requestScreenshotURL: function() {\n-          return null;\n-        },\n-        getScreenshot: function(callback) {\n-          callback();\n-        },\n-        origin: 'http://search.gaiamobile.org/',\n-        manifest: {\n-          role: 'search'\n-        },\n-        blur: function() {}\n-      }\n-    };\n-\n-    innerHeightDescriptor = Object\n-                              .getOwnPropertyDescriptor(window, 'innerHeight');\n-    Object.defineProperty(window, 'innerHeight', {\n-      value: fakeInnerHeight,\n-      configurable: true\n-    });\n-    innerWidthDescriptor = Object\n-                            .getOwnPropertyDescriptor(window, 'innerWidth');\n-    Object.defineProperty(window, 'innerWidth', {\n-      value: fakeInnerWidth,\n-      configurable: true\n-    });\n-\n-    scrollLeftDescriptor = Object.getOwnPropertyDescriptor(Element.prototype,\n-                            'scrollLeft');\n-    scrollTopDescriptor = Object.getOwnPropertyDescriptor(Element.prototype,\n-                            'scrollTop');\n-    scrollToDescriptor = Object.getOwnPropertyDescriptor(Element.prototype,\n-                            'scrollTo');\n-    Object.defineProperty(Element.prototype, 'scrollLeft', {\n-      configurable: true,\n-      get: function() { return scrollProperties[this.id].scrollLeft || 0; },\n-      set: function(x) { return (scrollProperties[this.id].scrollLeft = x); }\n-    });\n-\n-    Object.defineProperty(Element.prototype, 'scrollTop', {\n-      configurable: true,\n-      get: function() { return scrollProperties[this.id].scrollTop || 0; },\n-      set: function(y) { return (scrollProperties[this.id].scrollTop = y); }\n-    });\n-\n-    Object.defineProperty(Element.prototype, 'scrollTo', {\n-      configurable: true,\n-      get: function() {\n-        return function scrollTo(x, y) {\n-          if ('object' == typeof x) {\n-            y = x.top;\n-            x = x.left;\n-          }\n-          this.scrollLeft = (isNaN(x) || !x) ? 0 : x;\n-          this.scrollTop = (isNaN(y) || !y) ? 0 : y;\n-        };\n+  };\n+\n+  suiteSetup((done) => {\n+    function overrideProperty(obj, propName, newConfig) {\n+      var previousDescriptor = Object.getOwnPropertyDescriptor(obj, propName);\n+      if (previousDescriptor) {\n+        suiteTeardown(() => {\n+          Object.defineProperty(obj, propName, previousDescriptor);\n+        });\n       }\n-    });\n-\n-    screenNode = document.createElement('div');\n-    screenNode.id = 'screen';\n-    cardsView = document.createElement('div');\n-    cardsView.id = 'cards-view';\n+      newConfig.configurable = true;\n+      Object.defineProperty(obj, propName, newConfig);\n+    }\n \n-    cardsList = document.createElement('ul');\n-    cardsList.id = 'cards-list';\n-    cardsView.appendChild(cardsList);\n+    overrideProperty(window, 'innerHeight', { value: 640 });\n+    overrideProperty(window, 'innerWidth', { value: 320 });\n+    overrideProperty(document, 'mozFullScreen',\n+      { writable: true, value: false });\n \n-    noRecentWindowsNode = document.createElement('div');\n-    noRecentWindowsNode.id = 'cards-no-recent-windows';\n-    cardsView.appendChild(noRecentWindowsNode);\n+    sinon.stub(AppWindow.prototype, 'getSiteIconUrl')\n+      .returns(Promise.resolve('data:image/png;base64,abc+'));\n \n-    screenNode.appendChild(cardsView);\n-    document.body.appendChild(screenNode);\n+    document.body.innerHTML = `\n+    <div id=\"screen\">\n+      <div id=\"cards-view\" data-z-index-level=\"cards-view\">\n+        <ul id=\"cards-list\"></ul>\n+        <span id=\"cards-no-recent-windows\" class=\"no-recent-apps\"></span>\n+      </div>\n+    </div>\n+    `;\n \n-    realL10n = navigator.mozL10n;\n     navigator.mozL10n = MockL10n;\n \n-    home = new HomescreenWindow('fakeHome');\n-    requireApp('system/js/cards_helper.js');\n+    requireApp('system/js/task_manager_utils.js');\n     requireApp('system/js/base_ui.js');\n     requireApp('system/js/card.js');\n-\n     requireApp('system/js/task_manager.js', function() {\n-      done();\n-    });\n \n-  });\n-\n-  suiteTeardown(function() {\n-    Object.defineProperty(window, 'innerHeight', innerHeightDescriptor);\n-    Object.defineProperty(window, 'innerWidth', innerWidthDescriptor);\n-    Object.defineProperty(Element.prototype, 'scrollTop',\n-                          scrollTopDescriptor);\n-    Object.defineProperty(Element.prototype, 'scrollLeft',\n-                          scrollLeftDescriptor);\n-    Object.defineProperty(Element.prototype, 'scrollTo',\n-                          scrollToDescriptor);\n-\n-    screenNode.parentNode.removeChild(screenNode);\n-    navigator.mozL10n = realL10n;\n-  });\n-\n-  setup(function() {\n-    apps = {};\n-    MockStackManager.mStack = [];\n-    MockService.mockQueryWith('getHomescreen', home);\n-    MockService.mockQueryWith('fetchCurrentOrientation', 'portrait-primary');\n-    MockService.mockQueryWith('defaultOrientation', 'portrait-primary');\n-    // The whole suite should use fakeTimers to prevent intemittents\n-    // since the code logic is timer-heavy\n-    this.sinon.useFakeTimers();\n-\n-    scrollProperties['cards-view'] = {\n-      scrollLeft: 0, scrollTop: 0\n-    };\n-\n-    this.sinon.stub(AppWindow.prototype, 'getSiteIconUrl')\n-        .returns(Promise.resolve('data:image/png;base64,abc+'));\n-\n-    taskManager = new TaskManager();\n-    taskManager.start();\n-  });\n-  // We make sure to end each test with a reset DOM,\n-  // hidden+stopped+destroyed task manager and all setTimeouts triggered\n-  teardown(function() {\n-    // Clean up DOM in case a test didn't end up calling hide/_removeCards\n-    cardsList.innerHTML = '';\n-    cardsView.className = '';\n-\n-    if (!taskManager) {\n-      return;\n-    }\n-    taskManager.hide();\n-    // the cardviewbeforeclose event after a tick\n-    // the cardviewclosed event after a tick\n-    this.sinon.clock.tick(2);\n-    taskManager.stop();\n-    this.sinon.clock.tick(500); // 100ms exit + 400ms safety\n-    taskManager = null;\n-  });\n-\n-  suite('Hierarchy functions', function() {\n-    var manager;\n-    teardown(function() {\n-      if (manager) {\n-        manager.stop();\n-        manager = null;\n-      }\n-    });\n-    test('start should register hierarchy', function() {\n-      this.sinon.stub(MockService, 'request');\n-      manager = new TaskManager();\n-      manager.start();\n-      assert.isTrue(\n-        MockService.request.calledWith('registerHierarchy', manager));\n-    });\n-\n-    test('stop should unregister hierarchy', function() {\n-      this.sinon.stub(MockService, 'request');\n-      taskManager.stop();\n-      assert.isTrue(\n-        MockService.request.calledWith('unregisterHierarchy', taskManager));\n-    });\n-\n-    test('setActive to true should publish -activated', function() {\n-      var wasPublished = false;\n-      function onactivated() {\n-        wasPublished = true;\n-      }\n-      window.addEventListener(\n-        taskManager.EVENT_PREFIX + '-activated', onactivated);\n-\n-      taskManager.setActive(true);\n-      this.sinon.clock.tick();\n-      window.removeEventListener(\n-        taskManager.EVENT_PREFIX + '-activated', onactivated);\n-\n-      assert.ok(wasPublished);\n-    });\n-\n-    test('setActive to false should publish -deactivated', function() {\n-      var wasPublished = false;\n-      function ondeactivated() {\n-        wasPublished = true;\n-      }\n-\n-      taskManager.setActive(true);\n-      this.sinon.clock.tick();\n-      window.addEventListener(\n-        taskManager.EVENT_PREFIX + '-deactivated', ondeactivated);\n-      taskManager.setActive(false);\n+      TaskManager.prototype._TEST_currentIndex = 0;\n+      TaskManager.prototype.getCurrentIndex = function() {\n+        return this._TEST_currentIndex;\n+      };\n+      TaskManager.prototype.panToApp = function(app) {\n+        var index = this.stack.indexOf(app);\n+        if (index === -1) {\n+          index = this.stack.length - 1;\n+        }\n+        this._TEST_currentIndex = index;\n+        this.updateScrollPosition();\n+        return Promise.resolve();\n+      };\n \n-      assert.ok(wasPublished);\n+      done();\n     });\n   });\n \n-  suite('sanity check > ', function() {\n-    test('instantiable TaskManager', function(){\n-      assert.isTrue(taskManager instanceof window.TaskManager,\n-                  'taskManager instanceof TaskManager');\n+  var clock;\n \n-      var anotherTaskManager = new TaskManager();\n-      assert.isTrue(anotherTaskManager instanceof window.TaskManager,\n-                  'taskManager instanceof TaskManager');\n-\n-      assert.notEqual(anotherTaskManager, taskManager,\n-                      'TaskManager not expected to exhibit Singleton behavior');\n-    });\n-\n-    test('instantiable Cards', function(){\n-      var card = new Card();\n-      assert.ok(card && card instanceof window.Card,\n-                'Card instantiation');\n-    });\n+  setup(function() {\n+    clock = this.sinon.useFakeTimers();\n   });\n \n-  suite('when a document is fullscreen', function() {\n-    var realFullScreen;\n-    setup(function() {\n-      realFullScreen = document.mozFullScreen;\n-      Object.defineProperty(document, 'mozFullScreen', {\n-        configurable: true,\n-        get: function() { return true; }\n-      });\n-    });\n+  suiteTeardown(mocksForTaskManager.suiteTeardown);\n+  teardown(mocksForTaskManager.teardown);\n \n-    teardown(function() {\n-      Object.defineProperty(document, 'mozFullScreen', {\n-        configurable: true,\n-        get: function() { return realFullScreen; }\n-      });\n-    });\n+  test('Hierarchy', function(done) {\n+    var tm = new TaskManager();\n \n-    test('should exit fullscreen when showing',\n-      function() {\n-        var cancelSpy = this.sinon.spy(document, 'mozCancelFullScreen');\n-        taskManager.show();\n-        sinon.assert.calledOnce(cancelSpy);\n-      });\n+    this.sinon.stub(MockService, 'request');\n+    tm.start().then(() => {\n+      assert.isTrue(\n+        MockService.request.calledWith('registerHierarchy', tm));\n+    }).then(() => {\n+      return tm.stop();\n+    }).then(() => {\n+      assert.isTrue(\n+        MockService.request.calledWith('unregisterHierarchy', tm));\n+    }).then(done, done);\n+    clock.tick(1000);\n   });\n \n-  suite('settings > ', function() {\n-    test('observes settings at startup', function() {\n-      var spy = this.sinon.spy(MockSettingsListener, 'observe');\n-      taskManager.start();\n-      var numSettings = Object.keys(taskManager._settings).length;\n-      var proto = TaskManager.prototype;\n-      assert.equal(numSettings, 1);\n-      assert.equal(spy.callCount, 1);\n-      var call = spy.getCall(0);\n-      assert.equal(call.args[0], proto.SCREENSHOT_PREVIEWS_SETTING_KEY);\n-    });\n-    test('observes screenshots setting updates', function() {\n-      taskManager.start();\n-      var SETTING_KEY = taskManager.SCREENSHOT_PREVIEWS_SETTING_KEY;\n-      MockSettingsListener.mTriggerCallback(SETTING_KEY, false);\n-      assert.ok(!taskManager.useAppScreenshotPreviews,\n-        'useAppScreenshotPreviews is false when setting is false');\n-\n-      MockSettingsListener.mTriggerCallback(SETTING_KEY, true);\n-      assert.ok(taskManager.useAppScreenshotPreviews,\n-        'useAppScreenshotPreviews is true when setting is true');\n+  function spyEvent(obj, name) {\n+    var spy = sinon.stub();\n+    obj.addEventListener(name, spy);\n+    teardown(() => {\n+      obj.removeEventListener(name, spy);\n     });\n-  });\n+    return spy;\n+  }\n \n-  suite('populated task manager >', function() {\n-   function getExpectedCardPlacement(element, position) {\n-      var margins = taskManager.windowWidth - taskManager.cardWidth;\n-      var expectedLeft = margins / 2 +\n-                         position * (taskManager.cardWidth +\n-                                         taskManager.CARD_GUTTER);\n-      return expectedLeft;\n-    }\n+  suite('With Empty Task Manager', function() {\n+    var tm;\n+    var isActivated;\n \n-    setup(function() {\n+    setup(function(done) {\n       MockStackManager.mStack = [];\n-      var app;\n-      for (var key in appConfigs) {\n-        app = new AppWindow(appConfigs[key]);\n-        apps[key] = app;\n-        MockStackManager.mStack.push(app);\n-      }\n-    });\n-\n-    suite('display cardsview >', function() {\n-      setup(function() {\n-        MockService.mockQueryWith('getTopMostWindow', apps.search);\n-        MockStackManager.mCurrent = MockStackManager.mStack.length -1;\n-        showTaskManager(this.sinon.clock);\n-      });\n-\n-      test('fetch elements', function() {\n-        assert.equal(taskManager.element, cardsView);\n-        assert.equal(taskManager.cardsList, cardsList);\n-        assert.equal(taskManager.screenElement, screenNode);\n-      });\n-\n-      test('cardsview should be active once app is closed', function() {\n-        assert.isTrue(taskManager.isShown(), 'taskManager.isShown');\n-        assert.isTrue(screenNode.classList.contains('cards-view'));\n-      });\n-\n-      test('cardsview shouldnt show while already showing', function() {\n-        this.sinon.stub(taskManager, 'setActive');\n-        var onCardViewBeforeShowSpy = sinon.spy();\n-        window.addEventListener('cardviewbeforeshow', onCardViewBeforeShowSpy);\n-\n-        taskManager.show();\n-        assert.isFalse(taskManager.setActive.called);\n-        assert.isFalse(onCardViewBeforeShowSpy.called);\n-        window.removeEventListener('cardviewbeforeshow',\n-                                   onCardViewBeforeShowSpy);\n-      });\n-\n-      test('cardsview shouldnt respond to holdhome while already showing',\n-      function() {\n-        this.sinon.stub(taskManager, 'setActive');\n-        var onCardViewBeforeShowSpy = sinon.spy();\n-        window.addEventListener('cardviewbeforeshow', onCardViewBeforeShowSpy);\n-\n-        sendHoldhome();\n-        assert.isFalse(taskManager.setActive.called);\n-        assert.isFalse(onCardViewBeforeShowSpy.called);\n-        window.removeEventListener('cardviewbeforeshow',\n-                                   onCardViewBeforeShowSpy);\n-      });\n-\n-      test('cardsview shouldnt display no recent apps message', function() {\n-        assert.isFalse(cardsView.classList.contains('empty'));\n-      });\n-\n-      test('initial state', function() {\n-        assert.ok(taskManager.currentCard,\n-                  'has a truthy currentCard property');\n-\n-        var numCards = taskManager.cardsList.children.length;\n-        assert.equal(MockStackManager.snapshot().length, numCards,\n-                     'has correct number of list items');\n-\n-        // not sure we want to expose _stackIndex,\n-        // but we'll sanity-check check it anyway\n-        var currentIndex = MockStackManager.mCurrent;\n-        assert.equal(currentIndex, taskManager._stackIndex);\n-        assert.equal(taskManager.currentCard.position, currentIndex);\n-      });\n-\n-      test('placement', function() {\n-        var numCards = taskManager.cardsList.children.length;\n-        var margins = taskManager.windowWidth - taskManager.cardWidth;\n-        var expectedWidth = numCards * taskManager.cardWidth +\n-                           (numCards - 1) * taskManager.CARD_GUTTER +\n-                           margins;\n-        assert.equal(taskManager.cardsList.style.width, expectedWidth +'px');\n-\n-        var expectedLeft;\n-        for(var idx=0; idx < taskManager.cardsList.children; idx++) {\n-          expectedLeft = getExpectedCardPlacement(cardsList.children[idx], idx);\n-          assert.equal(cardsList.children[idx].style.left,\n-                       expectedLeft+'px');\n-        }\n-      });\n-\n-      test('wheel up event', function() {\n-        var card = taskManager.currentCard;\n-        var killAppStub = this.sinon.stub(card, 'killApp');\n-        this.sinon.stub(card.app, 'killable', function() {\n-          return true;\n-        });\n-        taskManager.handleEvent({\n-          type: 'wheel',\n-          deltaMode: 2,\n-          DOM_DELTA_PAGE: 2,\n-          deltaY: 1\n-        });\n-        assert.isTrue(killAppStub.called);\n-      });\n-\n-      test('wheel left/right event', function() {\n-        var card = taskManager.currentCard;\n-        this.sinon.spy(card, 'setVisibleForScreenReader');\n-\n-        taskManager.handleEvent({\n-          type: 'wheel',\n-          deltaMode: 2,\n-          DOM_DELTA_PAGE: 2,\n-          deltaX: 1\n-        });\n-        assert.ok(card.setVisibleForScreenReader.calledOnce);\n-      });\n-    });\n-\n-    suite('center apps', function() {\n-      setup(function() {\n-        MockService.mockQueryWith('getTopMostWindow', apps.search);\n-        MockStackManager.mCurrent = MockStackManager.mStack.length -1;\n-        showTaskManager(this.sinon.clock);\n-      });\n-\n-      test('initial centering', function() {\n-        // test that the current card gets centered\n-        var currentApp = MockStackManager.mStack[MockStackManager.mCurrent];\n-        var elem = taskManager.currentCard.element;\n-\n-        assert.equal(currentApp, taskManager.currentCard.app);\n-\n-        var expectedLeft = getExpectedCardPlacement(\n-          elem, MockStackManager.mCurrent\n-        );\n-        assert.equal(taskManager.currentCard.element.style.left,\n-                     expectedLeft+'px');\n-      });\n-\n-      test('centering at different stack position after closing',\n-      function(done) {\n-        // close and re-open the task manager at a different stack position\n-        waitForEvent(window, 'cardviewclosed').then(function() {\n-          // open again at position 0.\n-          MockStackManager.mCurrent = 0;\n-          taskManager.show();\n-\n-          var currentApp = MockStackManager.mStack[0];\n-          var elem = taskManager.currentCard.element;\n-\n-          assert.equal(currentApp, taskManager.currentCard.app);\n-\n-          var expectedLeft = getExpectedCardPlacement(\n-            elem, MockStackManager.mCurrent\n-          );\n-          assert.equal(taskManager.currentCard.element.style.left,\n-                       expectedLeft+'px');\n-        }, failOnReject)\n-        .then(function() { done(); }, done);\n-\n-        var targetApp = apps.game;\n-        taskManager.exitToApp(targetApp);\n-        fakeFinish(this.sinon.clock, targetApp);\n-      });\n-    });\n-\n-    suite('when the currently displayed app is out of the stack',\n-    function() {\n-      setup(function() {\n-        MockStackManager.mOutOfStack = true;\n-        MockStackManager.mStack = [\n-          apps.sms,\n-          apps.game,\n-          apps.game2\n-        ];\n-        MockStackManager.mCurrent = MockStackManager.mStack.length -1;\n-        taskManager.show();\n-      });\n-\n-      teardown(function() {\n-        MockStackManager.mOutOfStack = false;\n-      });\n-\n-      test('currentCard should be the last in the stack',\n-      function() {\n-        var currentApp = taskManager.currentCard.app;\n-        var expectedPosition = MockStackManager.mStack.indexOf(currentApp);\n-        assert.equal(expectedPosition, MockStackManager.mCurrent);\n-      });\n-\n-      test('exitToApp handles out-of-stack app', function(done) {\n-        var outOfStackApp = apps.search;\n-        var openStub = this.sinon.stub(outOfStackApp, 'open');\n-        waitForEvent(window, 'cardviewclosed').then(function(evt) {\n-          assert.isTrue(openStub.calledOnce);\n-          assert.equal(evt.detail.newStackPosition, -1);\n-        }, failOnReject)\n-        .then(function() { done(); }, done);\n-\n-        taskManager.exitToApp(outOfStackApp);\n-        this.sinon.clock.tick(500); // allow safety timeout to fire\n-      });\n-    });\n-\n-    suite('display cardsview via holdhome >', function() {\n-      setup(function(done) {\n-        assert.isFalse(taskManager.isShown(), 'taskManager isnt showing yet');\n-\n-        MockStackManager.mStack = [\n-          apps.sms\n-        ];\n-        MockStackManager.mCurrent = MockStackManager.mStack.length -1;\n-\n-        waitForEvent(window, 'cardviewshown')\n-          .then(function() { done(); }, failOnReject);\n-        sendHoldhome();\n-        window.dispatchEvent(new CustomEvent('appclosed'));\n-        this.sinon.clock.tick();\n-      });\n+      MockService.mockQueryWith('getHomescreen', new HomescreenWindow('home'));\n+      MockService.mockQueryWith('fetchCurrentOrientation', 'portrait-primary');\n+      MockService.mockQueryWith('defaultOrientation', 'portrait-primary');\n \n-      test('cardsview should be active', function() {\n-        assert.isTrue(cardsView.classList.contains('active'));\n-        assert.isTrue(taskManager.isShown());\n+      isActivated = false;\n+      window.addEventListener('taskmanager-activated', () => {\n+        isActivated = true;\n       });\n-    });\n-\n-    suite('display cardsview via holdhome > when the keyboard is displayed',\n-    function() {\n-      setup(function(done) {\n-        MockService.mockQueryWith('keyboardEnabled', true);\n-        assert.isFalse(taskManager.isShown(), 'taskManager isnt showing yet');\n-\n-        MockStackManager.mStack = [\n-          apps.sms\n-        ];\n-        MockStackManager.mCurrent = MockStackManager.mStack.length -1;\n-        waitForEvent(window, 'cardviewshown')\n-          .then(function() { done(); }, failOnReject);\n-\n-        sendHoldhome();\n-\n-        window.dispatchEvent(new CustomEvent('appclosed'));\n-        this.sinon.clock.tick();\n-        assert.isFalse(taskManager.isShown());\n-\n-        window.dispatchEvent(new CustomEvent('keyboardhidden'));\n-        this.sinon.clock.tick();\n+      window.addEventListener('taskmanager-deactivated', () => {\n+        isActivated = false;\n       });\n \n-      test('cardsview should be active', function() {\n-        assert.isTrue(cardsView.classList.contains('active'));\n-        assert.isTrue(taskManager.isShown());\n-      });\n-    });\n-\n-    suite('populated task manager in rocketbar >', function() {\n-      setup(function() {\n-        MockStackManager.mStack = [\n-          apps.sms\n-        ];\n-        MockStackManager.mCurrent = MockStackManager.mStack.length -1;\n-        showTaskManager(this.sinon.clock);\n-      });\n+      this.sinon.spy(document, 'mozCancelFullScreen');\n+      document.mozFullScreen = true; // Made overridable in suiteSetup\n \n-      test('has correct classes', function() {\n-        assert.isTrue(cardsView.classList.contains('active'));\n-      });\n+      tm = new TaskManager();\n+      tm.start().then(() => tm.show()).then(() => { done(); }, done);\n+      clock.tick(1000);\n     });\n-  });\n \n-  suite('empty task manager >', function() {\n-    setup(function() {\n-      MockStackManager.mStack = [];\n-      MockStackManager.mCurrent = -1;\n+    teardown(function(done) {\n+      tm.hide().then(() => tm.stop()).then(() => { done(); }, done);\n+      clock.tick(1000);\n     });\n \n-    test('Empty task manager opens', function(done) {\n-      var events = [];\n-      window.Promise.race([\n-        waitForEvent(window, 'cardviewclosed').then(function() {\n-          events.push('cardviewclosed');\n-        }, failOnReject),\n-        waitForEvent(window, 'cardviewshown').then(function() {\n-          events.push('cardviewshown');\n-        }, failOnReject)\n-      ]).then(function() {\n-        assert.equal(events.length, 1, 'sanity check, only one event received');\n-        assert.equal(events[0],\n-                    'cardviewshown',\n-                    'cardviewshown event raised when shown with empty stack');\n-        assert.isTrue(cardsView.classList.contains('active'));\n-        assert.isTrue(taskManager.isShown());\n-      }).then(function() {\n+    test('Proper state after show and hide', (done) => {\n+      assert.isTrue(tm.isShown());\n+      assert.isFalse(tm._isTransitioning);\n+      assert.isTrue(\n+        document.querySelector('#screen').classList.contains('cards-view'));\n+      assert.isTrue(isActivated);\n+      assert.ok(document.querySelector('#cards-view.empty.active'));\n+      // We're pretending to be in fullscreen mode.\n+      assert.isTrue(document.mozCancelFullScreen.calledOnce);\n+\n+      var startHide = tm.hide();\n+      assert.isTrue(tm._isTransitioning);\n+      startHide.then(() => {\n+        assert.isFalse(tm._isTransitioning);\n+        assert.isFalse(tm.isShown());\n+        assert.isFalse(isActivated);\n+        assert.ok(document.querySelector('#screen:not(.cards-view)'));\n+        assert.equal(document.querySelectorAll('.card').length, 0);\n         done();\n-      }, failOnReject);\n-\n-      showTaskManager(this.sinon.clock);\n-    });\n-\n-    suite('display empty cardsview >', function() {\n-      setup(function() {\n-        showTaskManager(this.sinon.clock);\n-      });\n-\n-      test('on click, empty cardsview is closed and back to home screen',\n-      function(done) {\n-        var events = [];\n-        assert.isTrue(cardsView.classList.contains('empty'));\n-        assert.isTrue(cardsView.classList.contains('active'));\n-        assert.isTrue(taskManager.isShown());\n-\n-        waitForEvent(window, 'cardviewclosed', 501).then(function(){\n-          events.push('cardviewclosed');\n-        }, failOnReject).then(function() {\n-          assert.equal(events.length, 1, 'sanity check, only 1 event received');\n-          assert.equal(events[0],\n-                      'cardviewclosed',\n-                      'cardviewclosed event raised when touch starts');\n-          assert.isFalse(cardsView.classList.contains('active'));\n-          assert.isFalse(taskManager.isShown());\n-        }, failOnReject)\n-        .then(done, done);\n-\n-        taskManager.handleEvent({target: taskManager.element,\n-                              type: 'click',\n-                              preventDefault: function(){} });\n-        this.sinon.clock.tick(501);\n       });\n+      clock.tick(1000);\n     });\n-  });\n \n-  suite('hide > ', function() {\n-    setup(function() {\n-      MockStackManager.mStack = [\n-        appConfigs.sms,\n-        appConfigs.game\n-      ].map((config) => {\n-        var app = new AppWindow(config);\n-        apps[app.name] = app;\n-        return app;\n+    test('Should not show if already showing', (done) => {\n+      var spyBeforeShow = spyEvent(window, 'cardviewbeforeshow');\n+      assert.isTrue(tm.isShown()); // (noting that we're already shown)\n+      tm.show().then(() => {\n+        assert.isFalse(spyBeforeShow.called);\n+        done();\n       });\n-      MockStackManager.mCurrent = 0;\n-      MockService.mockQueryWith('AppWindowManager.getActiveWindow', apps.sms);\n-\n-      showTaskManager(this.sinon.clock);\n     });\n \n-    test('taskManager should not be active', function() {\n-      taskManager.hide();\n-      assert.isFalse(taskManager.isShown(), 'isShown is false');\n-      assert.isFalse(cardsView.classList.contains('active'),\n-                    'no .active class');\n+    test('Should not respond to holdhome if already showing', () => {\n+      var showSpy = sinon.spy(tm, 'show');\n+      tm.respondToHierarchyEvent(new CustomEvent('holdhome'));\n+      assert.isFalse(showSpy.called);\n     });","path":"apps/system/test/unit/task_manager_test.js","position":953,"original_position":953,"commit_id":"b4bbcf7967392f42c2a7ea4f5753366fab7d8be7","original_commit_id":"b4bbcf7967392f42c2a7ea4f5753366fab7d8be7","user":{"login":"etiennesegonzac","id":105194,"avatar_url":"https://avatars.githubusercontent.com/u/105194?v=3","gravatar_id":"","url":"https://api.github.com/users/etiennesegonzac","html_url":"https://github.com/etiennesegonzac","followers_url":"https://api.github.com/users/etiennesegonzac/followers","following_url":"https://api.github.com/users/etiennesegonzac/following{/other_user}","gists_url":"https://api.github.com/users/etiennesegonzac/gists{/gist_id}","starred_url":"https://api.github.com/users/etiennesegonzac/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/etiennesegonzac/subscriptions","organizations_url":"https://api.github.com/users/etiennesegonzac/orgs","repos_url":"https://api.github.com/users/etiennesegonzac/repos","events_url":"https://api.github.com/users/etiennesegonzac/events{/privacy}","received_events_url":"https://api.github.com/users/etiennesegonzac/received_events","type":"User","site_admin":false},"body":"and for this test, instead of just spying on `show` I'd like to also assert on the active app (homescreen and not lockscreen) to make sure we're calling `close` properly and setting the special transition classes `from-home` correctly","created_at":"2015-09-24T13:55:37Z","updated_at":"2015-09-24T13:55:37Z","html_url":"https://github.com/mozilla-b2g/gaia/pull/31897#discussion_r40320620","pull_request_url":"https://api.github.com/repos/mozilla-b2g/gaia/pulls/31897","_links":{"self":{"href":"https://api.github.com/repos/mozilla-b2g/gaia/pulls/comments/40320620"},"html":{"href":"https://github.com/mozilla-b2g/gaia/pull/31897#discussion_r40320620"},"pull_request":{"href":"https://api.github.com/repos/mozilla-b2g/gaia/pulls/31897"}}},"pull_request":{"url":"https://api.github.com/repos/mozilla-b2g/gaia/pulls/31897","id":45354121,"html_url":"https://github.com/mozilla-b2g/gaia/pull/31897","diff_url":"https://github.com/mozilla-b2g/gaia/pull/31897.diff","patch_url":"https://github.com/mozilla-b2g/gaia/pull/31897.patch","issue_url":"https://api.github.com/repos/mozilla-b2g/gaia/issues/31897","number":31897,"state":"open","locked":false,"title":"Bug 1206928 - Refactor Task Manager.","user":{"login":"mcav","id":42708,"avatar_url":"https://avatars.githubusercontent.com/u/42708?v=3","gravatar_id":"","url":"https://api.github.com/users/mcav","html_url":"https://github.com/mcav","followers_url":"https://api.github.com/users/mcav/followers","following_url":"https://api.github.com/users/mcav/following{/other_user}","gists_url":"https://api.github.com/users/mcav/gists{/gist_id}","starred_url":"https://api.github.com/users/mcav/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mcav/subscriptions","organizations_url":"https://api.github.com/users/mcav/orgs","repos_url":"https://api.github.com/users/mcav/repos","events_url":"https://api.github.com/users/mcav/events{/privacy}","received_events_url":"https://api.github.com/users/mcav/received_events","type":"User","site_admin":false},"body":"This patch refactors Task Manager to be smaller, smoother, and more\r\nmaintainable.\r\n\r\nA brief video of this patch is here: https://youtu.be/Fkd7EQa0BxM\r\n\r\nAs I began working on Task Manager, I noticed that we've accumulated a lot of\r\ntechnical debt, which would make the planned 2.5 (and beyond) changes difficult\r\nand error-prone. This patch removes most of that debt and should reduce the risk\r\nof problems going forward.\r\n\r\nI've tried to refrain from doing too much in one patch, but in the process of\r\ncleaning up the code, a couple enhancements naturally followed the work:\r\n\r\n- The swipe-up-to-close gesture now feels smoother and natural.\r\n\r\n  * Rather than using a fixed swipe-up threshold, we now use velocity to decide\r\n    whether or not the user intended to kill the app. This better reflects\r\n    the intent of the user.\r\n\r\n  * The logic for swiping up has been extracted into a separate class, improving\r\n    testability. This leaves the `Card` class largely inert.\r\n\r\n  * Cards behave with proper easing, falling naturally back into place.\r\n\r\n  * We have axis-lock, allowing scrolling side-to-side or swipe-up, but not\r\n    both at the same time.\r\n\r\n  * I did not attempt to use native vertical scrolling. While native scrolling\r\n    may provide better FPS, I think a majority of the \"this feels janky\" feeling\r\n    was due to logic/easing problems rather than actual scroll FPS. In my\r\n    testing, swiping while following the finger seems very reasonable on Aries\r\n    and Flame, perhaps because Task Manager is fully opaque.\r\n\r\n- CardsHelper has been renamed to TaskManagerUtils to better reflect its role;\r\n  it now contains additional standalone functions for ease of testing.\r\n\r\n- Our interaction with StackManager is now much more straightforward,\r\n  reducing the need to maintain error-prone integer-based indexes.\r\n\r\n- Filtering (\"browser-only\") is now only a few lines of code.\r\n\r\nThe rest of the changes are straightforward: cleaning up logic flows, reducing\r\ncomplexity, and updating the code to modern standards. Similarly, the unit tests\r\nhave also been combed over to ensure they remain maintainable as well.\r\n\r\nThe diff may be hard to follow due to the nature of the refactoring; for ease of\r\nreview, I would suggest viewing `task_manager.js`, `card.js`, and\r\n`task_manager_utils.js` independently, followed by a scan of the diff to see if\r\nanything stands out. I've tried to structure the code in such a way that it can\r\nbe easily understood from top-to-bottom.\r\n\r\nI have a patch in progress for adding the \"new sheet\" buttons, but I would like\r\nto base that on top of this work.\r\n","created_at":"2015-09-17T23:24:32Z","updated_at":"2015-09-24T13:55:37Z","closed_at":null,"merged_at":null,"merge_commit_sha":"5157fdb2866616f6dfd4b156ea103e40b95dbf20","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/mozilla-b2g/gaia/pulls/31897/commits","review_comments_url":"https://api.github.com/repos/mozilla-b2g/gaia/pulls/31897/comments","review_comment_url":"https://api.github.com/repos/mozilla-b2g/gaia/pulls/comments{/number}","comments_url":"https://api.github.com/repos/mozilla-b2g/gaia/issues/31897/comments","statuses_url":"https://api.github.com/repos/mozilla-b2g/gaia/statuses/b4bbcf7967392f42c2a7ea4f5753366fab7d8be7","head":{"label":"mcav:tm-perf","ref":"tm-perf","sha":"b4bbcf7967392f42c2a7ea4f5753366fab7d8be7","user":{"login":"mcav","id":42708,"avatar_url":"https://avatars.githubusercontent.com/u/42708?v=3","gravatar_id":"","url":"https://api.github.com/users/mcav","html_url":"https://github.com/mcav","followers_url":"https://api.github.com/users/mcav/followers","following_url":"https://api.github.com/users/mcav/following{/other_user}","gists_url":"https://api.github.com/users/mcav/gists{/gist_id}","starred_url":"https://api.github.com/users/mcav/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mcav/subscriptions","organizations_url":"https://api.github.com/users/mcav/orgs","repos_url":"https://api.github.com/users/mcav/repos","events_url":"https://api.github.com/users/mcav/events{/privacy}","received_events_url":"https://api.github.com/users/mcav/received_events","type":"User","site_admin":false},"repo":{"id":12065405,"name":"gaia","full_name":"mcav/gaia","owner":{"login":"mcav","id":42708,"avatar_url":"https://avatars.githubusercontent.com/u/42708?v=3","gravatar_id":"","url":"https://api.github.com/users/mcav","html_url":"https://github.com/mcav","followers_url":"https://api.github.com/users/mcav/followers","following_url":"https://api.github.com/users/mcav/following{/other_user}","gists_url":"https://api.github.com/users/mcav/gists{/gist_id}","starred_url":"https://api.github.com/users/mcav/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mcav/subscriptions","organizations_url":"https://api.github.com/users/mcav/orgs","repos_url":"https://api.github.com/users/mcav/repos","events_url":"https://api.github.com/users/mcav/events{/privacy}","received_events_url":"https://api.github.com/users/mcav/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/mcav/gaia","description":"Gaia is a HTML5-based Phone UI for the Boot 2 Gecko Project. NOTE: For details of what branches are used for what releases, see https://wiki.mozilla.org/Release_Management/B2G_Landing","fork":true,"url":"https://api.github.com/repos/mcav/gaia","forks_url":"https://api.github.com/repos/mcav/gaia/forks","keys_url":"https://api.github.com/repos/mcav/gaia/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mcav/gaia/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mcav/gaia/teams","hooks_url":"https://api.github.com/repos/mcav/gaia/hooks","issue_events_url":"https://api.github.com/repos/mcav/gaia/issues/events{/number}","events_url":"https://api.github.com/repos/mcav/gaia/events","assignees_url":"https://api.github.com/repos/mcav/gaia/assignees{/user}","branches_url":"https://api.github.com/repos/mcav/gaia/branches{/branch}","tags_url":"https://api.github.com/repos/mcav/gaia/tags","blobs_url":"https://api.github.com/repos/mcav/gaia/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mcav/gaia/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mcav/gaia/git/refs{/sha}","trees_url":"https://api.github.com/repos/mcav/gaia/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mcav/gaia/statuses/{sha}","languages_url":"https://api.github.com/repos/mcav/gaia/languages","stargazers_url":"https://api.github.com/repos/mcav/gaia/stargazers","contributors_url":"https://api.github.com/repos/mcav/gaia/contributors","subscribers_url":"https://api.github.com/repos/mcav/gaia/subscribers","subscription_url":"https://api.github.com/repos/mcav/gaia/subscription","commits_url":"https://api.github.com/repos/mcav/gaia/commits{/sha}","git_commits_url":"https://api.github.com/repos/mcav/gaia/git/commits{/sha}","comments_url":"https://api.github.com/repos/mcav/gaia/comments{/number}","issue_comment_url":"https://api.github.com/repos/mcav/gaia/issues/comments{/number}","contents_url":"https://api.github.com/repos/mcav/gaia/contents/{+path}","compare_url":"https://api.github.com/repos/mcav/gaia/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mcav/gaia/merges","archive_url":"https://api.github.com/repos/mcav/gaia/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mcav/gaia/downloads","issues_url":"https://api.github.com/repos/mcav/gaia/issues{/number}","pulls_url":"https://api.github.com/repos/mcav/gaia/pulls{/number}","milestones_url":"https://api.github.com/repos/mcav/gaia/milestones{/number}","notifications_url":"https://api.github.com/repos/mcav/gaia/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mcav/gaia/labels{/name}","releases_url":"https://api.github.com/repos/mcav/gaia/releases{/id}","created_at":"2013-08-12T20:19:57Z","updated_at":"2015-05-01T23:44:47Z","pushed_at":"2015-09-24T01:51:26Z","git_url":"git://github.com/mcav/gaia.git","ssh_url":"git@github.com:mcav/gaia.git","clone_url":"https://github.com/mcav/gaia.git","svn_url":"https://github.com/mcav/gaia","homepage":"https://wiki.mozilla.org/B2G","size":942429,"stargazers_count":0,"watchers_count":0,"language":"JavaScript","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"mozilla-b2g:master","ref":"master","sha":"68361828ae88dffd04b250121b5f2472a63f4bf0","user":{"login":"mozilla-b2g","id":1261292,"avatar_url":"https://avatars.githubusercontent.com/u/1261292?v=3","gravatar_id":"","url":"https://api.github.com/users/mozilla-b2g","html_url":"https://github.com/mozilla-b2g","followers_url":"https://api.github.com/users/mozilla-b2g/followers","following_url":"https://api.github.com/users/mozilla-b2g/following{/other_user}","gists_url":"https://api.github.com/users/mozilla-b2g/gists{/gist_id}","starred_url":"https://api.github.com/users/mozilla-b2g/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mozilla-b2g/subscriptions","organizations_url":"https://api.github.com/users/mozilla-b2g/orgs","repos_url":"https://api.github.com/users/mozilla-b2g/repos","events_url":"https://api.github.com/users/mozilla-b2g/events{/privacy}","received_events_url":"https://api.github.com/users/mozilla-b2g/received_events","type":"Organization","site_admin":false},"repo":{"id":2317369,"name":"gaia","full_name":"mozilla-b2g/gaia","owner":{"login":"mozilla-b2g","id":1261292,"avatar_url":"https://avatars.githubusercontent.com/u/1261292?v=3","gravatar_id":"","url":"https://api.github.com/users/mozilla-b2g","html_url":"https://github.com/mozilla-b2g","followers_url":"https://api.github.com/users/mozilla-b2g/followers","following_url":"https://api.github.com/users/mozilla-b2g/following{/other_user}","gists_url":"https://api.github.com/users/mozilla-b2g/gists{/gist_id}","starred_url":"https://api.github.com/users/mozilla-b2g/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mozilla-b2g/subscriptions","organizations_url":"https://api.github.com/users/mozilla-b2g/orgs","repos_url":"https://api.github.com/users/mozilla-b2g/repos","events_url":"https://api.github.com/users/mozilla-b2g/events{/privacy}","received_events_url":"https://api.github.com/users/mozilla-b2g/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/mozilla-b2g/gaia","description":"Gaia is a HTML5-based Phone UI for the Boot 2 Gecko Project. NOTE: For details of what branches are used for what releases, see","fork":false,"url":"https://api.github.com/repos/mozilla-b2g/gaia","forks_url":"https://api.github.com/repos/mozilla-b2g/gaia/forks","keys_url":"https://api.github.com/repos/mozilla-b2g/gaia/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mozilla-b2g/gaia/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mozilla-b2g/gaia/teams","hooks_url":"https://api.github.com/repos/mozilla-b2g/gaia/hooks","issue_events_url":"https://api.github.com/repos/mozilla-b2g/gaia/issues/events{/number}","events_url":"https://api.github.com/repos/mozilla-b2g/gaia/events","assignees_url":"https://api.github.com/repos/mozilla-b2g/gaia/assignees{/user}","branches_url":"https://api.github.com/repos/mozilla-b2g/gaia/branches{/branch}","tags_url":"https://api.github.com/repos/mozilla-b2g/gaia/tags","blobs_url":"https://api.github.com/repos/mozilla-b2g/gaia/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mozilla-b2g/gaia/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mozilla-b2g/gaia/git/refs{/sha}","trees_url":"https://api.github.com/repos/mozilla-b2g/gaia/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mozilla-b2g/gaia/statuses/{sha}","languages_url":"https://api.github.com/repos/mozilla-b2g/gaia/languages","stargazers_url":"https://api.github.com/repos/mozilla-b2g/gaia/stargazers","contributors_url":"https://api.github.com/repos/mozilla-b2g/gaia/contributors","subscribers_url":"https://api.github.com/repos/mozilla-b2g/gaia/subscribers","subscription_url":"https://api.github.com/repos/mozilla-b2g/gaia/subscription","commits_url":"https://api.github.com/repos/mozilla-b2g/gaia/commits{/sha}","git_commits_url":"https://api.github.com/repos/mozilla-b2g/gaia/git/commits{/sha}","comments_url":"https://api.github.com/repos/mozilla-b2g/gaia/comments{/number}","issue_comment_url":"https://api.github.com/repos/mozilla-b2g/gaia/issues/comments{/number}","contents_url":"https://api.github.com/repos/mozilla-b2g/gaia/contents/{+path}","compare_url":"https://api.github.com/repos/mozilla-b2g/gaia/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mozilla-b2g/gaia/merges","archive_url":"https://api.github.com/repos/mozilla-b2g/gaia/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mozilla-b2g/gaia/downloads","issues_url":"https://api.github.com/repos/mozilla-b2g/gaia/issues{/number}","pulls_url":"https://api.github.com/repos/mozilla-b2g/gaia/pulls{/number}","milestones_url":"https://api.github.com/repos/mozilla-b2g/gaia/milestones{/number}","notifications_url":"https://api.github.com/repos/mozilla-b2g/gaia/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mozilla-b2g/gaia/labels{/name}","releases_url":"https://api.github.com/repos/mozilla-b2g/gaia/releases{/id}","created_at":"2011-09-03T01:38:46Z","updated_at":"2015-09-24T09:41:23Z","pushed_at":"2015-09-24T13:44:21Z","git_url":"git://github.com/mozilla-b2g/gaia.git","ssh_url":"git@github.com:mozilla-b2g/gaia.git","clone_url":"https://github.com/mozilla-b2g/gaia.git","svn_url":"https://github.com/mozilla-b2g/gaia","homepage":"https://wiki.mozilla.org/B2G","size":1638194,"stargazers_count":1983,"watchers_count":1983,"language":"JavaScript","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":2444,"mirror_url":null,"open_issues_count":1229,"forks":2444,"open_issues":1229,"watchers":1983,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/mozilla-b2g/gaia/pulls/31897"},"html":{"href":"https://github.com/mozilla-b2g/gaia/pull/31897"},"issue":{"href":"https://api.github.com/repos/mozilla-b2g/gaia/issues/31897"},"comments":{"href":"https://api.github.com/repos/mozilla-b2g/gaia/issues/31897/comments"},"review_comments":{"href":"https://api.github.com/repos/mozilla-b2g/gaia/pulls/31897/comments"},"review_comment":{"href":"https://api.github.com/repos/mozilla-b2g/gaia/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/mozilla-b2g/gaia/pulls/31897/commits"},"statuses":{"href":"https://api.github.com/repos/mozilla-b2g/gaia/statuses/b4bbcf7967392f42c2a7ea4f5753366fab7d8be7"}}}}