Have main_loop.update_fps receive an object with averages and peaks of each stage.

--- engine/main_loop.coffee
+++ engine/main_loop.coffee
@@ -158,7 +158,25 @@
         @render_durations[@_fdi] = time5 - time4
         @_fdi = (@_fdi+1) % @last_frame_durations.length
         if @_fdi == 0 and @update_fps
-            @update_fps(@)
+            @update_fps {
+                max_fps: 1000/Math.min.apply(null, @last_frame_durations),
+                min_fps: 1000/Math.max.apply(null, @last_frame_durations),
+                average_fps: 1000/average(@last_frame_durations),
+                max_logic_duration: 1000/Math.max.apply(null, @logic_durations),
+                average_logic_duration: average @logic_durations,
+                max_physics_durations: 1000/Math.max.apply(null, @physics_durations),
+                average_physics_durations: average @physics_durations,
+                max_animation_durations: 1000/Math.max.apply(null, @animation_durations),
+                average_animation_durations: average @animation_durations,
+                max_render_durations: 1000/Math.max.apply(null, @render_durations),
+                average_render_durations: average @render_durations,
+            }

+average = (list) ->
+    r = 0
+    for v in list
+        r += v
+    return r/list.length
+
 module.exports = {MainLoop}


Code refactoring

Code refactoring

--- client/finder/lib/index.coffee
+++ client/finder/lib/index.coffee
@@ -63,13 +63,12 @@
       hoverDetect : no
       delegate    : controller

-    onDrag = (args...) =>
+    onDrag = (event) =>

       { internalDragging }  = controller.treeController
-      mouseEvent            = args.filter (arg) -> arg instanceof KDView is no

-      if mouseEvent.length
-        { items }  = mouseEvent[0].originalEvent.dataTransfer
+      if event
+        { items }  = event.originalEvent.dataTransfer

         if items?[0].kind is 'string' and not internalDragging
           return finderView.overlay?.show()
@@ -84,12 +83,12 @@
       uploader.reset()

     {treeController} = controller
-    treeController.on 'dragEnter', onDrag
-    treeController.on 'dragOver' , onDrag
+    treeController.on 'dragEnter', (nodeView, event) -> onDrag event
+    treeController.on 'dragOver', (nodeView, event) ->  onDrag event

     finderView = controller.getView()
     finderView.on 'dragenter', onDrag
-    finderView.on 'dragover' , onDrag
+    finderView.on 'dragover', onDrag
     finderView.on 'drop', -> finderView.overlay?.hide()

     uploader


fix tab-stops

fix tab-stops

--- snippets/core.nsL.cson
+++ snippets/core.nsL.cson
@@ -1028,7 +1028,7 @@
     'body': 'UninstallSubCaption(${1});$2'
   'UninstallText()':
     'prefix': 'UninstallText()'
-    'body': 'UninstallText("${1:text}"${2:, "${3:subtext}"});$2'
+    'body': 'UninstallText("${1:text}"${2:, "${3:subtext}"});$4'
   'UnRegDLL()':
     'prefix': 'UnRegDLL()'
     'body': 'UnRegDLL(${1});$2'


add rain bomb

add rain bomb

--- scripts/example.coffee
+++ scripts/example.coffee
@@ -12,6 +12,11 @@

    robot.hear /make it rain/i, (msg) ->
      msg.send "https://s3.amazonaws.com/uploads.hipchat.com/12562/184609/xdhwG85hEGYKVeR/rainingkev-2.gif"
+
+   robot.hear /rain bomb( (\d+))?/i, (msg) ->
+     count = msg.match[2] || 5
+     for i in [0..count] by 1
+        msg.send "https://s3.amazonaws.com/uploads.hipchat.com/12562/184609/xdhwG85hEGYKVeR/rainingkev-2.gif"
   #
   # robot.respond /open the (.*) doors/i, (msg) ->
   #   doorType = msg.match[1]


add rain bomb

add rain bomb

--- scripts/example.coffee
+++ scripts/example.coffee
@@ -15,7 +15,7 @@

    robot.hear /rain bomb( (\d+))?/i, (msg) ->
      count = msg.match[2] || 5
-     for i in [0..count] by 1
+     for i in [1..count] by 1
         msg.send "https://s3.amazonaws.com/uploads.hipchat.com/12562/184609/xdhwG85hEGYKVeR/rainingkev-2.gif"
   #
   # robot.respond /open the (.*) doors/i, (msg) ->


Fixed upgrade test

Fixed upgrade test

--- test/integration/upgrade.coffee
+++ test/integration/upgrade.coffee
@@ -73,6 +73,10 @@
         before (done) ->
           wd40.click '.modal .btn', done

+        # TODO: Not sure why we need this timeout.
+        before (done) ->
+          setTimeout done, 5000
+
         it 'it closes the modal window', (done) ->
           wd40.waitForInvisibleByCss '.modal', done

@@ -93,6 +97,10 @@
       context 'when I click the only button on the modal', ->
         before (done) ->
           wd40.click '.modal .btn', done
+
+        # TODO: Not sure why we need this timeout.
+        before (done) ->
+          setTimeout done, 5000

         it 'it closes the modal window', (done) ->
           wd40.waitForInvisibleByCss '.modal', done
@@ -100,4 +108,4 @@
         it 'it shows I am on the medium plan', (done) ->
           wd40.elementByCss '.account-medium .currentPlan', (err, span) ->
             should.exist span
-            done()+            done()


Remove edge text

Remove edge text

--- app/assets/javascripts/directives.coffee
+++ app/assets/javascripts/directives.coffee
@@ -124,7 +124,7 @@
           attrs: '.marker-target': d: 'M 4 0 L 0 2 L 4 4 z'
           labels: [ {
             position: 0.5
-            attrs: text: text: 'on'
+            attrs: text: text: ''
           } ]
           connector: name: 'smooth')
         #}
@@ -194,8 +194,8 @@
         joint.layout.DirectedGraph.layout graph,
           setLinkVertices: false
           rankDir: 'lr'
-          nodeSep: 100
-          rankSep: 100
+          nodeSep: 80
+          rankSep: 80
         return

       V(paper.viewport).translate 20, 20


--- src/ng-bundle-collection.coffee
+++ src/ng-bundle-collection.coffee
@@ -837,6 +837,8 @@
 	</pre>
 	###
 	fetch: (params = {}) =>
+		# preventing params mixing if the same params object is passed to different collections
+		params = angular.copy params
 		# extending params with default preconfigured params
 		_.extend params, @config.params


Allow overlay to be opened multiple times

--- app/assets/javascripts/overlay.js.coffee
+++ app/assets/javascripts/overlay.js.coffee
@@ -4,6 +4,7 @@
     @options = options
     self = this
     @overlay = $("

")
+    $('body').append(@overlay)

     $(@selector).click (event) ->
       event.preventDefault()
@@ -15,10 +16,9 @@

   open: (target) ->
-    overlayContent = $(target).find('.js-overlay').remove()
+    overlayContent = $(target).find('.js-overlay').clone()

     @overlay.append(overlayContent)
-    $('body').append(@overlay)

     maxWidth = $(window).width() - 20
     maxHeight = $(window).height() - 20
@@ -54,11 +54,12 @@
     @overlay.css('top', top)
     @overlay.width(width)
     @overlay.height(height)
+    @overlay.show()

     $('body').addClass('scroll-lock')

     overlayContent.show()

   close: ->
-    @overlay.hide()
+    @overlay.empty().hide()
     $('body').removeClass('scroll-lock')


account: html decode ssh key title

account: html decode ssh key title

--- client/Account/views/sshkeys.coffee
+++ client/Account/views/sshkeys.coffee
@@ -114,8 +114,8 @@

     {title, key} = @getData()

-    form.titleInput.setValue  title if title
-    form.keyTextarea.setValue key   if key
+    form.titleInput.setValue Encoder.htmlDecode title  if title
+    form.keyTextarea.setValue key  if key

     @info = info = new KDCustomHTMLView
       tagName  : "span"


Kloud:info: try to ping machine klient if possible, then fallback to kloud.info

--- client/Main/kite/kites/kloud.coffee
+++ client/Main/kite/kites/kloud.coffee
@@ -28,10 +28,23 @@
   # will be queued up and resolved by the pending request

   info: ({ machineId, currentState }) ->
+
     if @needsRequest[machineId] in [undefined, yes]
+
       @needsRequest[machineId] = no
+
+      @askInfoFromKlient machineId, (klientInfo)=>
+
+        if klientInfo?
+
           @requestingInfo[machineId].forEach ({ resolve }) ->
+            resolve klientInfo
+
           @requestingInfo[machineId] = null
+          @needsRequest[machineId]   = yes
+
+        else
+
           @askInfoFromKloud machineId, currentState

     new Promise (resolve, reject) =>


Kloud: add metrics for individual kloud calls too, count timeout errors

--- client/Main/kite/kites/kloud.coffee
+++ client/Main/kite/kites/kloud.coffee
@@ -95,6 +95,8 @@

       .catch ->

+        KiteLogger.logFailed 'klient', 'kite.ping'
+
         callback null

@@ -115,10 +117,14 @@

       .catch (err) =>

-        if err.name is "TimeoutError" and not @_reconnectedOnce
-          warn "First time timeout, reconnecting to kloud..."
-          kontrol.kites.kloud.singleton?.reconnect?()
-          @_reconnectedOnce = yes
+        if err.name is "TimeoutError"
+
+          unless @_reconnectedOnce
+            warn "First time timeout, reconnecting to kloud..."
+            kontrol.kites.kloud.singleton?.reconnect?()
+            @_reconnectedOnce = yes
+
+          KiteLogger.logFailed 'kloud', 'info'

         warn "[kloud:info] failed, sending current state back:", { currentState, err }
         @resolveRequestingInfos machineId, State: currentState


Update messages to reflect latest Sinon formatting.

Fixes broken tests!

--- test/messages.coffee
+++ test/messages.coffee
@@ -42,9 +42,9 @@
             spyB.displayName = "spyB"

             expect(-> spyA.should.have.been.calledBefore(spyB)).to
-                .throw("expected spyA to have been called before spyB")
+                .throw("expected spyA to have been called before function spyB() {}")
             expect(-> spyB.should.have.been.calledAfter(spyA)).to
-                .throw("expected spyB to have been called after spyA")
+                .throw("expected spyB to have been called after function spyA() {}")

         it "should be correct for the negated cases", ->
             spyA = sinon.spy()
@@ -57,9 +57,9 @@
             spyB()

             expect(-> spyA.should.not.have.been.calledBefore(spyB)).to
-                .throw("expected spyA to not have been called before spyB")
+                .throw("expected spyA to not have been called before function spyB() {}")
             expect(-> spyB.should.not.have.been.calledAfter(spyA)).to
-                .throw("expected spyB to not have been called after spyA")
+                .throw("expected spyB to not have been called after function spyA() {}")

     describe "about call context", ->
         it "should be correct for the basic case", ->
@@ -69,7 +69,8 @@

             spy.call(badContext)

-            expected = "expected spy to have been called with {} as this, but it was called with " + spy.printf("%t")
+            expected = "expected spy to have been called with {  } as this, but it was called with " +
+                spy.printf("%t") + " instead"
             expect(-> spy.should.have.been.calledOn(context)).to.throw(expected)
             expect(-> spy.getCall(0).should.have.been.calledOn(context)).to.throw(expected)

@@ -79,7 +80,7 @@

             spy.call(context)

-            expected = "expected spy to not have been called with {} as this"
+            expected = "expected spy to not have been called with {  } as this"
             expect(-> spy.should.not.have.been.calledOn(context)).to.throw(expected)
             expect(-> spy.getCall(0).should.not.have.been.calledOn(context)).to.throw(expected)

@@ -90,8 +91,8 @@

             spy.call(badContext)

-            expected = "expected spy to always have been called with {} as this, but it was called with " +
-                spy.printf("%t")
+            expected = "expected spy to always have been called with {  } as this, but it was called with " +
+                spy.printf("%t") + " instead"
             expect(-> spy.should.always.have.been.calledOn(context)).to.throw(expected)

     describe "about call arguments", ->
@@ -180,9 +181,9 @@
                 .throw("expected spy to have thrown TypeError")

             expect(-> throwingSpy.should.have.thrown({ message: "x" })).to
-                .throw("expected spy to have thrown { message: 'x' }")
+                .throw('expected spy to have thrown { message: "x" }')
             expect(-> throwingSpy.getCall(0).should.have.thrown({ message: "x" })).to
-                .throw("expected spy to have thrown { message: 'x' }")
+                .throw('expected spy to have thrown { message: "x" }')

         it "should be correct for the negated cases", ->
             error = new Error("boo!")
@@ -197,9 +198,9 @@
             expect(-> spy.getCall(0).should.not.have.thrown("Error")).to.throw("expected spy to not have thrown Error")

             expect(-> spy.should.not.have.thrown(error)).to
-                .throw("expected spy to not have thrown [Error: boo!]")
+                .throw("expected spy to not have thrown Error: boo!")
             expect(-> spy.getCall(0).should.not.have.thrown(error)).to
-                .throw("expected spy to not have thrown [Error: boo!]")
+                .throw("expected spy to not have thrown Error: boo!")

         it "should be correct for the always cases", ->
             spy = sinon.spy()
@@ -215,7 +216,7 @@
                 .throw("expected spy to always have thrown TypeError")

             expect(-> throwingSpy.should.have.always.thrown({ message: "x" })).to
-                .throw("expected spy to always have thrown { message: 'x' }")
+                .throw('expected spy to always have thrown { message: "x" }')

     describe "when used on a non-spy/non-call", ->
         notSpy = ->


Complete documentation for Notification creation

Notifications support several options that were yet to be documented:
`buttons`, `description`, and `stack`. Add descriptions for each option
and its defaults (where applicable, e.g. .buttons.className).

`stack` is technically available on all notification types, but it
doesn't make sense to use it on types other than 'error' and 'fatal'.
Therefore it's documented only in those two cases.

The usage and expected display, like preformatted vs. Markdown, taken
from [atom/notifications/lib/notification-element.coffee][1].

[1]: https://github.com/atom/notifications/blob/v0.64.1/lib/notification-element.coffee

--- src/notification-manager.coffee
+++ src/notification-manager.coffee
@@ -33,8 +33,18 @@
   #
   # * `message` A {String} message
   # * `options` (optional) An options {Object} with the following keys:
-  #    * `detail` (optional) A {String} with additional details about the
-  #      notification.
+  #    * `buttons` (optional) An {Array} of {Object} where each {Object} has the
+  #      following options:
+  #      * `className` (optional) {String} additional class name add to the
+  #        button. It will already receive `btn btn-success`.
+  #      * `onClick` (optional) {Function} callback to call when the button is
+  #        clicked. The context will be set to the {NotificationElement}
+  #        instance.
+  #      * `text` {String} inner text for the button
+  #    * `description` (optional) A {String} that will be rendered as Markdown
+  #      to describe the notification.
+  #    * `detail` (optional) A preformatted {String} that will be rendered as
+  #      plain text with details about the notification.
   #    * `dismissable` (optional) A {Boolean} indicating whether this
   #      notification can be dismissed by the user. Defaults to `false`.
   #    * `icon` (optional) A {String} name of an icon from Octicons to display
@@ -46,8 +56,18 @@
   #
   # * `message` A {String} message
   # * `options` (optional) An options {Object} with the following keys:
-  #    * `detail` (optional) A {String} with additional details about the
-  #      notification.
+  #    * `buttons` (optional) An {Array} of {Object} where each {Object} has the
+  #      following options:
+  #      * `className` (optional) {String} additional class name add to the
+  #        button. It will already receive `btn btn-info`.
+  #      * `onClick` (optional) {Function} callback to call when the button is
+  #        clicked. The context will be set to the {NotificationElement}
+  #        instance.
+  #      * `text` {String} inner text for the button
+  #    * `description` (optional) A {String} that will be rendered as Markdown
+  #      to describe the notification.
+  #    * `detail` (optional) A preformatted {String} that will be rendered as
+  #      plain text with details about the notification.
   #    * `dismissable` (optional) A {Boolean} indicating whether this
   #      notification can be dismissed by the user. Defaults to `false`.
   #    * `icon` (optional) A {String} name of an icon from Octicons to display
@@ -59,8 +79,18 @@
   #
   # * `message` A {String} message
   # * `options` (optional) An options {Object} with the following keys:
-  #    * `detail` (optional) A {String} with additional details about the
-  #      notification.
+  #    * `buttons` (optional) An {Array} of {Object} where each {Object} has the
+  #      following options:
+  #      * `className` (optional) {String} additional class name add to the
+  #        button. It will already receive `btn btn-warning`.
+  #      * `onClick` (optional) {Function} callback to call when the button is
+  #        clicked. The context will be set to the {NotificationElement}
+  #        instance.
+  #      * `text` {String} inner text for the button
+  #    * `description` (optional) A {String} that will be rendered as Markdown
+  #      to describe the notification.
+  #    * `detail` (optional) A preformatted {String} that will be rendered as
+  #      plain text with details about the notification.
   #    * `dismissable` (optional) A {Boolean} indicating whether this
   #      notification can be dismissed by the user. Defaults to `false`.
   #    * `icon` (optional) A {String} name of an icon from Octicons to display
@@ -72,12 +102,24 @@
   #
   # * `message` A {String} message
   # * `options` (optional) An options {Object} with the following keys:
-  #    * `detail` (optional) A {String} with additional details about the
-  #      notification.
+  #    * `buttons` (optional) An {Array} of {Object} where each {Object} has the
+  #      following options:
+  #      * `className` (optional) {String} additional class name add to the
+  #        button. It will already receive `btn btn-error`.
+  #      * `onClick` (optional) {Function} callback to call when the button is
+  #        clicked. The context will be set to the {NotificationElement}
+  #        instance.
+  #      * `text` {String} inner text for the button
+  #    * `description` (optional) A {String} that will be rendered as Markdown
+  #      to describe the notification.
+  #    * `detail` (optional) A preformatted {String} that will be rendered as
+  #      plain text with details about the notification.
   #    * `dismissable` (optional) A {Boolean} indicating whether this
   #      notification can be dismissed by the user. Defaults to `false`.
   #    * `icon` (optional) A {String} name of an icon from Octicons to display
   #      in the notification header. Defaults to `'flame'`.
+  #    * `stack` (optional) A preformatted {String} with stack trace information
+  #      describing the location of the error.
   addError: (message, options) ->
     @addNotification(new Notification('error', message, options))

@@ -85,12 +127,24 @@
   #
   # * `message` A {String} message
   # * `options` (optional) An options {Object} with the following keys:
-  #    * `detail` (optional) A {String} with additional details about the
-  #      notification.
+  #    * `buttons` (optional) An {Array} of {Object} where each {Object} has the
+  #      following options:
+  #      * `className` (optional) {String} additional class name add to the
+  #        button. It will already receive `btn btn-error`.
+  #      * `onClick` (optional) {Function} callback to call when the button is
+  #        clicked. The context will be set to the {NotificationElement}
+  #        instance.
+  #      * `text` {String} inner text for the button
+  #    * `description` (optional) A {String} that will be rendered as Markdown
+  #      to describe the notification.
+  #    * `detail` (optional) A preformatted {String} that will be rendered as
+  #      plain text with details about the notification.
   #    * `dismissable` (optional) A {Boolean} indicating whether this
   #      notification can be dismissed by the user. Defaults to `false`.
   #    * `icon` (optional) A {String} name of an icon from Octicons to display
   #      in the notification header. Defaults to `'bug'`.
+  #    * `stack` (optional) A preformatted {String} with stack trace information
+  #      describing the location of the error.
   addFatalError: (message, options) ->
     @addNotification(new Notification('fatal', message, options))


repo uses /me not /api/me

repo uses /me not /api/me

--- scripts/session.coffee
+++ scripts/session.coffee
@@ -7,7 +7,7 @@
   _authenticated = false

   return new (Backbone.Model.extend
-    url: '/api/me'
+    url: '/me'

     initialize: () ->
       @load()
@@ -15,7 +15,7 @@
     load: () ->
       @fetch
         success: (model, response, options) =>
-          if response.id
+          if response.user_id
             # Logged in

             @set('user', response)


update router to redirect poets and lovers (#6029)

--- app/router.cjsx
+++ app/router.cjsx
@@ -168,6 +168,7 @@
     <MonorepoRoute path="/projects/kmc35/peoples-contest-digital-archive" />
     <MonorepoRoute path="/projects/rachaelsking/corresponding-with-quakers" />
     <MonorepoRoute path="/projects/mariaedgeworthletters/maria-edgeworth-letters" />
+    <MonorepoRoute path="/projects/pmlogan/poets-and-lovers" />

     <Route path="/projects/mschwamb/planet-four/authors" component={() => <ExternalRedirect newUrl='https://authors.planetfour.org/' />} />


Teambuilder: Fix Pokemon#toJSON

Teambuilder: Fix Pokemon#toJSON

--- client/app/js/models/pokemon.coffee
+++ client/app/js/models/pokemon.coffee
@@ -130,8 +130,9 @@
     (nature[0].toUpperCase() + nature.substr(1)  for nature of natures)

   toJSON: ->
-    attributes = _.clone(model.attributes)
+    attributes = _.clone(@attributes)
     delete attributes.hiddenPowerType
+    attributes

 # A hash that keys a nature with the stats that it boosts.
 # Neutral natures are ignored.


Correct displayName of Task component

Correct displayName of Task component

--- src/components/task/index.cjsx
+++ src/components/task/index.cjsx
@@ -23,7 +23,7 @@
   propTypes:
     id: React.PropTypes.string

-  displayName: 'ReadingTask'
+  displayName: 'Task'

   mixins: [StepFooterMixin, CrumbMixin]


Fix display of number of events to import, refs #488

--- client/app/views/import_view.coffee
+++ client/app/views/import_view.coffee
@@ -83,6 +83,10 @@
     # Show the event preview list. It doesn't display all events at the same
     # time because Firefox cannot handle it and freezes.
     showEventsPreview: (events) ->
+        # The ics file may contain events with duplicate ID that won't be added
+        # to the collection but will be imported, so we can't rely on eventList
+        # length for the total number of events to import
+        @eventsCount = events.length
         # Break event to import in smaller lists
         @eventLists = helpers.getLists events, 100

@@ -176,13 +180,12 @@

     # Set import counter to 0.
     initCounter: ->
-        total = @eventList.collection.length
         @counter = 0

         # Set the progress widget
         $('.import-progress').html """

#{t 'imported events'}:
-            0/#{total}

+            0/#{@eventsCount}

         """

     # Update counter current value.


clean up locale

clean up locale

--- src/restaurant.coffee
+++ src/restaurant.coffee
@@ -1,20 +1,11 @@
 module.exports = [
     {
         "locale": "en_US"
-    },
-    {
-        "locale": "zh_CN"
-    },
+    }
     {
         "locale": "zh_HK"
-    },
-    {
-        "locale": "zh_TW"
-    },
+    }
     {
         "locale": "th_TH"
-    },
-    {
-        "locale": "ja_JP"
     }
 ]


corrected new playlist name tracks transfert bug (the right one)

--- coffee/_Playlists.class.coffee
+++ coffee/_Playlists.class.coffee
@@ -44,7 +44,7 @@
                     i++
                 __playlists = playlists
                 success? playlists
-
+
     @getTracksForPlaylist = (playlist, success) ->
         tracks = []
         db.transaction (tx) ->
@@ -58,3 +58,10 @@
     @rename: (name, new_name) ->
         db.transaction (tx) ->
             tx.executeSql 'UPDATE playlists SET name = ? WHERE name = ?', [new_name, name]
+        Playlists.getTracksForPlaylist(name, ((tracks) ->
+            i = 0
+            while i < tracks.length
+                Playlists.addTrack(tracks[i].artist, tracks[i].title, tracks[i].cover_url_medium, tracks[i].cover_url_large, new_name)
+                i++
+        ))
+        Playlists.delete(name)


use debug

use debug

--- coffee/services/project.coffee
+++ coffee/services/project.coffee
@@ -249,7 +249,7 @@
           @set local
           callback local
           Log.groupCollapsed "Project.load()"
-          Log.table local
+          Log.debug local
           Log.groupEnd "Project.load()"
         else
           @_load Chrome.storage.sync, (sync) =>


moved click action to separate function (therefore useable by contextmenu plugin)

--- coffeescripts/functions.coffee
+++ coffeescripts/functions.coffee
@@ -162,7 +162,11 @@
     cur_a = $('#tb_label_popup li.label' + i + ' a')
     # TODO check if click event is defined instead of unbinding?
     cur_a.unbind 'click'
-    cur_a.click ->
+    cur_a.click rcm_tb_label_onclick
+    i++
+  return
+
+rcm_tb_label_onclick = ->
       toggle_label = $(this).parent().data('labelname')
       toggle_label_no = toggle_label
       selection = rcm_tb_label_get_selection()
@@ -232,8 +236,6 @@
         rcm_tb_label_unflag_msgs unflag_uids, toggle_label_no
         i++
       return
-    i++
-  return

 rcmail_ctxm_label = (command, el, pos) ->
   # my code works only on selected rows, contextmenu also on unselected


redraw correct colors

redraw correct colors

--- views/gorillas.coffee
+++ views/gorillas.coffee
@@ -36,19 +36,20 @@
       row++

   create_window:(x, y) ->
-    @randomize_window_color()
-    @context.fillStyle = @color
+    color = @randomize_window_color()
+    @context.fillStyle = color
     @context.fillRect x, y, 8, 16

   randomize_color:() ->
     colors = [ '#C0C0C0', '#800000', '#00FFFF' ]
     random = Math.floor(Math.random()*colors.length)
-    @color = colors[random]
+    @color ||= colors[random]

   randomize_window_color:() ->
     colors = [ '#808080', '#FFFF00' ]
     random = Math.floor(Math.random()*5)
-    @color = if random > 0 then colors[1] else colors[random]
+    color = if random > 0 then colors[1] else colors[random]
+    color

 class Sun
   constructor:(context) ->


remove double title in conversations

remove double title in conversations

--- client/app/components/conversation.coffee
+++ client/app/components/conversation.coffee
@@ -81,7 +81,7 @@
             #        a href: '#', 'Responsive Cozy Emails'
             #        span className: 'close', 'x'

-            h3 null,
+            h3 className: 'message-title',
                 @props.message.get 'subject'
                 a
                     href: closeUrl,
@@ -97,8 +97,6 @@
                         href: collapseUrl,
                         className: 'close-conversation pull-right',
                             i className:'fa fa-compress'
-            h3 className: 'message-title',
-                @props.message.get 'subject'

             ul className: 'thread list-unstyled',
                 for message, key in @props.conversation


Code cleanup

Code cleanup

--- client/app/components/message-list.coffee
+++ client/app/components/message-list.coffee
@@ -5,7 +5,8 @@
 DomUtils       = require '../utils/dom_utils'
 MessageUtils   = require '../utils/message_utils'
 SocketUtils    = require '../utils/socketio_utils'
-{MessageFlags, MessageFilter, FlagsConstants} = require '../constants/app_constants'
+{MessageFlags, MessageFilter, FlagsConstants} =
+    require '../constants/app_constants'

 AccountActionCreator      = require '../actions/account_action_creator'
 ContactActionCreator      = require '../actions/contact_action_creator'
@@ -28,7 +29,8 @@
     mixins: [RouterMixin]

     shouldComponentUpdate: (nextProps, nextState) ->
-        should = not(_.isEqual(nextState, @state)) or not (_.isEqual(nextProps, @props))
+        should = not(_.isEqual(nextState, @state)) or
+            not (_.isEqual(nextProps, @props))
         return should

     getInitialState: ->
@@ -73,7 +75,10 @@
             fullWidth: true

         advanced   = @props.settings.get('advanced')
-        nbSelected = if Object.keys(@state.selected).length > 0 then null else true
+        if Object.keys(@state.selected).length > 0
+            nbSelected = null
+        else
+            nbSelected = true

         showList = =>
             params = _.clone(MessageStore.getParams())
@@ -82,7 +87,10 @@
             LayoutActionCreator.showMessageList parameters: params

         toggleFilterFlag = =>
-            filter = if @state.filterFlag then MessageFilter.ALL else MessageFilter.FLAGGED
+            if @state.filterFlag
+                filter = MessageFilter.ALL
+            else
+                filter = MessageFilter.FLAGGED
             LayoutActionCreator.filterMessages filter
             showList()
             @setState
@@ -91,7 +99,10 @@
                 filterAttach: false

         toggleFilterUnseen = =>
-            filter = if @state.filterUnseen then MessageFilter.ALL else MessageFilter.UNSEEN
+            if @state.filterUnseen
+                filter = MessageFilter.ALL
+            else
+                filter = MessageFilter.UNSEEN
             LayoutActionCreator.filterMessages filter
             showList()
             @setState
@@ -100,7 +111,10 @@
                 filterAttach: false

         toggleFilterAttach = =>
-            filter = if @state.filterAttach then MessageFilter.ALL else MessageFilter.ATTACH
+            if @state.filterAttach
+                filter = MessageFilter.ALL
+            else
+                filter = MessageFilter.ATTACH
             LayoutActionCreator.filterMessages filter
             showList()
             @setState
@@ -272,14 +286,22 @@
                             else
                                 delete selected[id]
                             if Object.keys(selected).length > 0
-                                @setState edited: true, selected: selected
+                                newState =
+                                    edited: true
+                                    selected: selected
                             else
-                                @setState allSelected: false, edited: false, selected: {}
-
-                    # If message list is filtered, we can't only rely on message count
-                    # So we assume that if query.pageAfter is null, there's no more
-                    # messages to display
-                    if @props.messages.count() < parseInt(@props.counterMessage, 10) and
+                                newState =
+                                    allSelected: false
+                                    edited: false
+                                    selected: {}
+                            @setState newState
+
+                    # If message list is filtered, we can't only rely on
+                    # message count
+                    # So we assume that if query.pageAfter is null, there's
+                    # no more messages to display
+                    nbMessages = parseInt(@props.counterMessage, 10)
+                    if @props.messages.count() < nbMessages and
                        @props.query.pageAfter?
                         p className: 'text-center list-footer',
                             if @props.fetching
@@ -335,7 +357,8 @@
             window.confirm confirmMessage
                 MessageUtils.delete selected, conv, =>
                     if selected.length > 0 and @props.messages.count() > 0
-                        MessageActionCreator.setCurrent @props.messages.first().get('id'), true
+                        firstMessageID = @props.messages.first().get('id')
+                        MessageActionCreator.setCurrent firstMessageID, true

     onConversationMove: (args) ->
@@ -351,7 +374,8 @@
             newbox = args.target.dataset.value
             MessageUtils.move selected, conv, @props.mailboxID, newbox, =>
                 if selected.length > 0 and @props.messages.count() > 0
-                    MessageActionCreator.setCurrent @props.messages.first().get('id'), true
+                    firstMessageID = @props.messages.first().get('id')
+                    MessageActionCreator.setCurrent firstMessageID, true

     onMark: (args) ->
         selected = Object.keys @state.selected
@@ -366,11 +390,13 @@
                     when FlagsConstants.SEEN
                         flags.push MessageFlags.SEEN
                     when FlagsConstants.UNSEEN
-                        flags = flags.filter (e) -> return e isnt FlagsConstants.SEEN
+                        flags = flags.filter (e) ->
+                            return e isnt FlagsConstants.SEEN
                     when FlagsConstants.FLAGGED
                         flags.push MessageFlags.FLAGGED
                     when FlagsConstants.NOFLAG
-                        flags = flags.filter (e) -> return e isnt FlagsConstants.FLAGGED
+                        flags = flags.filter (e) ->
+                            return e isnt FlagsConstants.FLAGGED
                 MessageActionCreator.updateFlag message, flags, (error) ->
                     if error?
                         alertError "#{t("message action mark ko")} #{error}"
@@ -513,8 +539,8 @@
         @_onMount()

     _onMount: ->
-        # If selected message has changed, scroll the list to put current message
-        # into view
+        # If selected message has changed, scroll the list to put
+        # current message into view
         if @state.messageID isnt @props.messageID
             active = document.querySelector("[data-message-id='#{@props.messageID}']")
             if active? and not DomUtils.isVisible(active)
@@ -532,7 +558,8 @@
         updatedProps = Object.keys(nextProps).filter (prop) =>
             return typeof nextProps[prop] isnt 'function' and
                 not (_.isEqual(nextProps[prop], @props[prop]))
-        shouldUpdate = not _.isEqual(nextState, @state) or updatedProps.length > 0
+        shouldUpdate = not _.isEqual(nextState, @state) or
+            updatedProps.length > 0

         return shouldUpdate

@@ -591,7 +618,6 @@
         ,
             tag
                 href: url,
-                'data-href': url,
                 className: 'wrapper',
                 'data-message-id': message.get('id'),
                 onClick: @onMessageClick,
@@ -652,7 +678,7 @@
                 event.preventDefault()
                 MessageActionCreator.setCurrent node.dataset.messageId, true
                 if @props.settings.get('displayPreview')
-                    href = '#' + node.dataset.href.split('#')[1]
+                    href = '#' + node.getAttribute('href').split('#')[1]
                     @redirect href

     onMessageDblClick: (event) ->


MachineShareManager: Fix missing callback variable.

--- client/app/lib/machinesharemanager.coffee
+++ client/app/lib/machinesharemanager.coffee
@@ -62,17 +62,21 @@
     @set machineUId, {type, workspaceId}

-  registerChannelEvent: (channelId) ->
+  registerChannelEvent: (channelId, callback = kd.noop) ->

     { socialapi, groupsController } = kd.singletons

     socialapi.channel.byId { id: channelId }, (err, channel) ->
-      return callback err  if err
+      if err
+        kd.warn err
+        return callback err

       group = groupsController.getCurrentGroup()
       socialapi.registerAndOpenChannel group, channel, (err, pubnubChannel) ->

-        return callback err  if err
+        if err
+          kd.warn err
+          return callback err

         pubnubChannel?.channel?.once 'RemovedFromChannel', ->
           dataProvider.fetch ->


Client: Activity: Updated subscriptionData to post channel original name

--- client/app/lib/socialapicontroller.coffee
+++ client/app/lib/socialapicontroller.coffee
@@ -286,6 +286,7 @@
     item.payload             = data.payload

     if not isKoding() and item.typeConstant in ['privatemessage', 'bot']
+      item._originalName = item.name
       item.name = data.purpose
       item.purpose = item.payload?.description or ''
     else
@@ -425,12 +426,12 @@
     socialapi.cacheItem socialApiChannel
     socialapi.openedChannels[channelName] = {} # placeholder to avoid duplicate registration

-    {name, typeConstant, token, id} = socialApiChannel
+    {name, typeConstant, token, id, _originalName} = socialApiChannel

     subscriptionData =
       group      : group.slug
       channelType: typeConstant
-      channelName: name
+      channelName: _originalName or name
       channelId  : id
       token      : token

@@ -456,8 +457,8 @@
       socialApiChannels.forEach (socialApiChannel) ->
         registerAndOpenChannel group, socialApiChannel, kd.noop

-  generateChannelName = ({name, typeConstant, groupName}) ->
-    return "socialapi.#{groupName}-#{typeConstant}-#{name}"
+  generateChannelName = ({name, typeConstant, groupName, _originalName}) ->
+    return "socialapi.#{groupName}-#{typeConstant}-#{_originalName or name}"

   addToScreenMap = (options) ->


ReceivedFiles: update error message

ReceivedFiles: update error message

--- components/ReceiveFiles.coffee
+++ components/ReceiveFiles.coffee
@@ -16,7 +16,7 @@
       if request.files
         @sendFiles(request)
       else
-        @sendError("No file number #{@num}")
+        @sendError("No files attached")
     @inPorts.in.on 'disconnect', () =>
       @outPorts.out.disconnect() if @outPorts.out.isConnected()


Fix failing tests

Fix failing tests

--- test/test_pce.coffee
+++ test/test_pce.coffee
@@ -35,7 +35,7 @@

     onComplete = (result) ->
       result.retval.should.equal "success"
-      result.operation.should.equal operation
+      JSON.stringify(result.operation).should.equal messageJsonResult
       finish()

     @pce.forward_operation(operation).then(onComplete).done()
@@ -55,7 +55,7 @@

     onComplete1 = (result) ->
       result.retval.should.equal "success"
-      result.operation.should.equal operation1
+      JSON.stringify(result.operation).should.equal messageJsonResult1
       true

     @pce.forward_operation(operation1).then(onComplete1).then =>


Fix CommandService specs

Fix CommandService specs

--- spec/command_service_spec.coffee
+++ spec/command_service_spec.coffee
@@ -1,4 +1,4 @@
-describe.skip 'CommandService', ->
+describe 'CommandService', ->

   sinon    = require 'sinon'
   expect   = require 'expect.js'
@@ -12,77 +12,87 @@

   sandbox = null
   aggregateStubId = 1
-  myAggregateStub = null
+  exampleAggregateStub = null
   aggregateRepositoryStub = null
+  domainEventService = null
   beforeEach ->
     sandbox = sinon.sandbox.create()

-    # build Aggregate Stub
-    myAggregateStub = sinon.createStubInstance AggregateRoot
-    myAggregateStub.myAggregateFunction = sandbox.stub()
-    myAggregateStub.id = aggregateStubId
+    # example aggregate
+    class ExampleAggregate extends AggregateRoot
+
+    # build ExampleAggregateStub
+    exampleAggregateStub = sinon.createStubInstance ExampleAggregate
+    exampleAggregateStub.doSomething = sandbox.stub()
+    exampleAggregateStub.id = aggregateStubId

     # stub the repository
     aggregateRepositoryStub = sinon.createStubInstance AggregateRepository
-    aggregateRepositoryStub.findById.withArgs(aggregateStubId).returns(myAggregateStub)
+
+    # getClass returns a Stub which returns the ExampleAggregateStub on instantiation
+    aggregateRepositoryStub.getClass.returns sandbox.stub().returns exampleAggregateStub

     # stub the DomainEventService
-    sandbox.stub DomainEventService
+    domainEventService = sinon.createStubInstance DomainEventService
+    domainEventService.saveAndTrigger.yields null

   afterEach ->
     sandbox.restore()

   describe '#createAggregate', ->

-    aggregateId = null
-    readAggregate = null
     commandService = null
     beforeEach ->
+      # findById should find nothing
+      aggregateRepositoryStub.findById.yields null, null
+
       # instantiate the CommandService with the ReadAggregateRepository stub
-      commandService = new CommandService aggregateRepositoryStub
+      commandService = new CommandService domainEventService, aggregateRepositoryStub

-      # create an Aggregate Class stub which returns the myAggregateStub on instantiation
-      AggregateStub = sandbox.stub().returns myAggregateStub
+    it 'should call the create method of the given aggregate', (done) ->
+      commandService.createAggregate 'ExampleAggregate', ->
+        expect(exampleAggregateStub.create.calledOnce).to.be.ok()
+        done()

-      # call the create method on the CommandService with the Aggregate Class stub
-      aggregateId = commandService.createAggregate AggregateStub
-
-    it 'should call the create method of the given aggregate', ->
-      expect(myAggregateStub.create.calledOnce).to.be.ok()
-
-    it 'should return the aggregateId', ->
-      expect(aggregateId).to.be aggregateStubId
+    it 'should return the aggregateId', (done) ->
+      commandService.createAggregate 'ExampleAggregate', (err, aggregateId) ->
+        expect(aggregateId).to.be aggregateStubId
+        done()

   describe '#commandAggregate', ->

     commandService = null
     beforeEach ->
+      # findById should find something
+      aggregateRepositoryStub.findById.yields null, exampleAggregateStub
+
       # instantiate the command service
-      commandService = new CommandService aggregateRepositoryStub
+      commandService = new CommandService domainEventService, aggregateRepositoryStub

-    it 'should call the command on the aggregate', ->
-      commandService.commandAggregate 1, 'myAggregateFunction'
-      expect(myAggregateStub.myAggregateFunction.calledOnce).to.be.ok()
+    it 'should call the command on the aggregate', (done) ->
+      commandService.commandAggregate 'ExampleAggregate', 1, 'doSomething', (err, aggregateId) ->
+        expect(exampleAggregateStub.doSomething.calledOnce).to.be.ok()
+        done()

-    it 'should store the aggregate into a local cache using its ID', ->
-      commandService.commandAggregate 1, 'myAggregateFunction'
-      expect(commandService.aggregateCache[1]).to.be.a AggregateRoot
+    it 'should call the generateDomainEvent method of the given aggregate', (done) ->
+      commandService.commandAggregate 'ExampleAggregate', 1, 'doSomething', (err, aggregateId) ->
+        expect(exampleAggregateStub.generateDomainEvent.calledWith 'doSomething').to.be.ok()
+        done()

-    it 'should call the generateDomainEvent method of the given aggregate', ->
-      commandService.commandAggregate 1, 'myAggregateFunction'
-      expect(myAggregateStub.generateDomainEvent.calledWith 'myAggregateFunction').to.be.ok()
+    it 'should call saveAndTrigger on DomainEventService with the generated DomainEvents', (done) ->
+      events = {}
+      exampleAggregateStub.getDomainEvents.returns events

-    it 'should pass the accumulated domainEvents from the Aggregate to the DomainEventService', ->
-      events = {}
-      myAggregateStub.getDomainEvents.returns events
+      commandService.commandAggregate 'ExampleAggregate', 1, 'doSomething', (err, aggregateId) ->
+        expect(domainEventService.saveAndTrigger.withArgs(events).calledOnce).to.be.ok()
+        done()

-      commandService.commandAggregate 1, 'myAggregateFunction'
-      expect(DomainEventService.handle.withArgs(events).calledOnce).to.be.ok()
+    it 'should call the clearChanges method of the given aggregate', (done) ->
+      commandService.commandAggregate 'ExampleAggregate', 1, 'doSomething', (err, aggregateId) ->
+        expect(exampleAggregateStub.clearChanges.calledOnce).to.be.ok()
+        done()

-    it 'should call the clearChanges method of the given aggregate', ->
-      commandService.commandAggregate 1, 'myAggregateFunction'
-      expect(myAggregateStub.clearChanges.calledOnce).to.be.ok()
-
-    it 'should return the aggregateId', ->
-      aggregateId = commandService.commandAggregate 1, 'myAggregateFunction'
-      expect(aggregateId).to.be 1+    it 'should return the aggregateId', (done) ->
+      commandService.commandAggregate 'ExampleAggregate', 1, 'doSomething', (err, aggregateId) ->
+        expect(aggregateId).to.be 1
+        done()


Unfocus spec

Unfocus spec

--- spec/package-manager-spec.coffee
+++ spec/package-manager-spec.coffee
@@ -1026,7 +1026,7 @@
         expect(atom.packages.enablePackage("this-doesnt-exist")).toBeNull()
         expect(console.warn.callCount).toBe 1

-      fit "does not disable an already disabled package", ->
+      it "does not disable an already disabled package", ->
         packageName = 'package-with-main'
         atom.config.pushAtKeyPath('core.disabledPackages', packageName)
         atom.packages.observeDisabledPackages()


Added tests for 'slideUpDownSlices', 'foldSlices' and 'fadeSlices' (#6).

--- spec/rambling.slider.spec.coffee
+++ spec/rambling.slider.spec.coffee
@@ -501,6 +501,68 @@
         timeout_callback()
         expect($.fn.animate).toHaveBeenCalledWith {height: "#{rambling_slider.height()}px", opacity: '1'}, $.fn.ramblingSlider.defaults.speed, '', jasmine.any(Function)

+    describe 'and calling the slide up down slices helper function', ->
+      sort_callback = null
+      timeout_callback = null
+
+      beforeEach ->
+        timeout_spy.andCallFake (callback) -> timeout_callback = callback
+        sort_callback = jasmine.createSpy()
+        sort_callback.andCallFake -> @
+
+        result = helper.slideUpDownSlices sort_callback
+
+      it 'should sort the slices', ->
+        expect(sort_callback).toHaveBeenCalled()
+
+      it 'should set the height to the slider height and the opacity to 1', ->
+        timeout_callback()
+        expect($.fn.animate).toHaveBeenCalledWith {height: "#{rambling_slider.height()}px", opacity: '1'}, $.fn.ramblingSlider.defaults.speed, '', jasmine.any(Function)
+
+    describe 'and calling the slide fold slices helper function', ->
+      sort_callback = null
+      timeout_callback = null
+
+      beforeEach ->
+        timeout_spy.andCallFake (callback) -> timeout_callback = callback
+        sort_callback = jasmine.createSpy()
+        sort_callback.andCallFake -> @
+
+        result = helper.foldSlices sort_callback
+
+      it 'should sort the slices', ->
+        expect(sort_callback).toHaveBeenCalled()
+
+      it 'should align the slices to the bottom', ->
+        timeout_callback()
+        expect($.fn.css).toHaveBeenCalledWith top: '0px', height: '100%', width: '0px'
+
+      it 'should set the height to the slider width and the opacity to 1', ->
+        timeout_callback()
+        expect($.fn.animate).toHaveBeenCalledWith {width: "#{rambling_slider.width()}px", opacity: '1'}, $.fn.ramblingSlider.defaults.speed, '', jasmine.any(Function)
+
+    describe 'and calling the slide fade slices helper function', ->
+      sort_callback = null
+      timeout_callback = null
+
+      beforeEach ->
+        timeout_spy.andCallFake (callback) -> timeout_callback = callback
+        sort_callback = jasmine.createSpy()
+        sort_callback.andCallFake -> @
+
+        result = helper.fadeSlices sort_callback
+
+      it 'should sort the slices', ->
+        expect(sort_callback).toHaveBeenCalled()
+
+      it 'should align the slices to the bottom', ->
+        timeout_callback()
+        expect($.fn.css).toHaveBeenCalledWith height: "#{rambling_slider.height()}px"
+
+      it 'should set the opacity to 1', ->
+        timeout_callback()
+        expect($.fn.animate).toHaveBeenCalledWith {opacity: '1'}, $.fn.ramblingSlider.defaults.speed, '', jasmine.any(Function)
+
   # Methods
   describe 'when getting the effect', ->
     it 'should return the default one', ->


Add test for shallow association case

Add test for shallow association case

--- spec/storage-manager-spec.coffee
+++ spec/storage-manager-spec.coffee
@@ -458,64 +458,123 @@
       describe "fetching multiple levels of associations", ->
         callCount = success = checkStructure = null

-        beforeEach ->
-          projectOneTimeEntryTask = buildTask()
-          projectOneTimeEntry = buildTimeEntry(title: "without task"); projectOneTimeEntryWithTask = buildTimeEntry(id: projectOneTimeEntry.id, task_id: projectOneTimeEntryTask.id, title: "with task")
-          projectOne = buildProject(); projectOneWithTimeEntries = buildProject(id: projectOne.id, time_entry_ids: [projectOneTimeEntry.id])
-          projectTwo = buildProject(); projectTwoWithTimeEntries = buildProject(id: projectTwo.id, time_entry_ids: [])
-          taskOneAssignee = buildUser()
-          taskTwoAssignee = buildUser()
-          taskOneSubAssignee = buildUser()
-          taskOneSub = buildTask(project_id: projectOne.id, parent_id: 10); taskOneSubWithAssignees = buildTask(id: taskOneSub.id, assignee_ids: [taskOneSubAssignee.id], parent_id: 10)
-          taskTwoSub = buildTask(project_id: projectTwo.id, parent_id: 11); taskTwoSubWithAssignees = buildTask(id: taskTwoSub.id, assignee_ids: [taskTwoAssignee.id], parent_id: 11)
-          taskOne = buildTask(id: 10, project_id: projectOne.id, assignee_ids: [taskOneAssignee.id], sub_task_ids: [taskOneSub.id])
-          taskTwo = buildTask(id: 11, project_id: projectTwo.id, assignee_ids: [taskTwoAssignee.id], sub_task_ids: [taskTwoSub.id])
-          respondWith server, "/api/tasks?include=assignees%2Cproject%2Csub_tasks&parents_only=true&per_page=20&page=1", data: { results: resultsArray("tasks", [taskOne, taskTwo]), tasks: resultsObject([taskOne, taskTwo, taskOneSub, taskTwoSub]), users: resultsObject([taskOneAssignee, taskTwoAssignee]), projects: resultsObject([projectOne, projectTwo]) }
-          respondWith server, "/api/tasks?include=assignees&only=#{taskOneSub.id}%2C#{taskTwoSub.id}&apply_default_filters=false", data: { results: resultsArray("tasks", [taskOneSub, taskTwoSub]), tasks: resultsObject([taskOneSubWithAssignees, taskTwoSubWithAssignees]), users: resultsObject([taskOneSubAssignee, taskTwoAssignee]) }
-          respondWith server, "/api/projects?include=time_entries&only=#{projectOne.id}%2C#{projectTwo.id}&apply_default_filters=false", data: { results: resultsArray("projects", [projectOne, projectTwo]), projects: resultsObject([projectOneWithTimeEntries, projectTwoWithTimeEntries]), time_entries: resultsObject([projectOneTimeEntry]) }
-          respondWith server, "/api/time_entries?include=task&only=#{projectOneTimeEntry.id}&apply_default_filters=false", data: { results: resultsArray("time_entries", [projectOneTimeEntry]), time_entries: resultsObject([projectOneTimeEntryWithTask]), tasks: resultsObject([projectOneTimeEntryTask]) }
-
-          callCount = 0
-          checkStructure = (collection) ->
-            expect(collection.pluck("id").sort()).toEqual [taskOne.id, taskTwo.id]
-            expect(collection.get(taskOne.id).get("project").id).toEqual projectOne.id
-            expect(collection.get(taskOne.id).get("assignees").pluck("id")).toEqual [taskOneAssignee.id]
-            expect(collection.get(taskTwo.id).get("assignees").pluck("id")).toEqual [taskTwoAssignee.id]
-            expect(collection.get(taskOne.id).get("sub_tasks").pluck("id")).toEqual [taskOneSub.id]
-            expect(collection.get(taskTwo.id).get("sub_tasks").pluck("id")).toEqual [taskTwoSub.id]
-            expect(collection.get(taskOne.id).get("sub_tasks").get(taskOneSub.id).get("assignees").pluck("id")).toEqual [taskOneSubAssignee.id]
-            expect(collection.get(taskTwo.id).get("sub_tasks").get(taskTwoSub.id).get("assignees").pluck("id")).toEqual [taskTwoAssignee.id]
-            expect(collection.get(taskOne.id).get("project").get("time_entries").pluck("id")).toEqual [projectOneTimeEntry.id]
-            expect(collection.get(taskOne.id).get("project").get("time_entries").models[0].get("task").id).toEqual projectOneTimeEntryTask.id
-            callCount += 1
-
-          success = jasmine.createSpy().and.callFake checkStructure
-
-        context 'deeply nested json structure', ->
-          it "separately requests each layer of associations", ->
-            collection = manager.loadCollection "tasks",
-              filters: { parents_only: "true" },
-              success: success,
-              include: [
-                'assignees',
-                { project: ["time_entries": "task"] },
-                { sub_tasks: ["assignees"] }
-              ]
-
-            collection.bind "loaded", checkStructure
-            collection.bind "reset", checkStructure
-
-            expect(success).not.toHaveBeenCalled()
-
-            server.respond() until server.queue.length == 0
-
-            expect(success).toHaveBeenCalledWith(collection)
-            expect(callCount).toEqual 3
-
-        context 'using a backbone collection', ->
-          it "separately requests each layer of associations", ->
+        context 'deeply nested associations', ->
+          beforeEach ->
+            projectOneTimeEntryTask = buildTask()
+            projectOneTimeEntry = buildTimeEntry(title: "without task"); projectOneTimeEntryWithTask = buildTimeEntry(id: projectOneTimeEntry.id, task_id: projectOneTimeEntryTask.id, title: "with task")
+            projectOne = buildProject(); projectOneWithTimeEntries = buildProject(id: projectOne.id, time_entry_ids: [projectOneTimeEntry.id])
+            projectTwo = buildProject(); projectTwoWithTimeEntries = buildProject(id: projectTwo.id, time_entry_ids: [])
+            taskOneAssignee = buildUser()
+            taskTwoAssignee = buildUser()
+            taskOneSubAssignee = buildUser()
+            taskOneSub = buildTask(project_id: projectOne.id, parent_id: 10); taskOneSubWithAssignees = buildTask(id: taskOneSub.id, assignee_ids: [taskOneSubAssignee.id], parent_id: 10)
+            taskTwoSub = buildTask(project_id: projectTwo.id, parent_id: 11); taskTwoSubWithAssignees = buildTask(id: taskTwoSub.id, assignee_ids: [taskTwoAssignee.id], parent_id: 11)
+            taskOne = buildTask(id: 10, project_id: projectOne.id, assignee_ids: [taskOneAssignee.id], sub_task_ids: [taskOneSub.id])
+            taskTwo = buildTask(id: 11, project_id: projectTwo.id, assignee_ids: [taskTwoAssignee.id], sub_task_ids: [taskTwoSub.id])
+            respondWith server, "/api/tasks?include=assignees%2Cproject%2Csub_tasks&parents_only=true&per_page=20&page=1", data: { results: resultsArray("tasks", [taskOne, taskTwo]), tasks: resultsObject([taskOne, taskTwo, taskOneSub, taskTwoSub]), users: resultsObject([taskOneAssignee, taskTwoAssignee]), projects: resultsObject([projectOne, projectTwo]) }
+            respondWith server, "/api/tasks?include=assignees&only=#{taskOneSub.id}%2C#{taskTwoSub.id}&apply_default_filters=false", data: { results: resultsArray("tasks", [taskOneSub, taskTwoSub]), tasks: resultsObject([taskOneSubWithAssignees, taskTwoSubWithAssignees]), users: resultsObject([taskOneSubAssignee, taskTwoAssignee]) }
+            respondWith server, "/api/projects?include=time_entries&only=#{projectOne.id}%2C#{projectTwo.id}&apply_default_filters=false", data: { results: resultsArray("projects", [projectOne, projectTwo]), projects: resultsObject([projectOneWithTimeEntries, projectTwoWithTimeEntries]), time_entries: resultsObject([projectOneTimeEntry]) }
+            respondWith server, "/api/time_entries?include=task&only=#{projectOneTimeEntry.id}&apply_default_filters=false", data: { results: resultsArray("time_entries", [projectOneTimeEntry]), time_entries: resultsObject([projectOneTimeEntryWithTask]), tasks: resultsObject([projectOneTimeEntryTask]) }
+
+            callCount = 0
+            checkStructure = (collection) ->
+              expect(collection.pluck("id").sort()).toEqual [taskOne.id, taskTwo.id]
+              expect(collection.get(taskOne.id).get("project").id).toEqual projectOne.id
+              expect(collection.get(taskOne.id).get("assignees").pluck("id")).toEqual [taskOneAssignee.id]
+              expect(collection.get(taskTwo.id).get("assignees").pluck("id")).toEqual [taskTwoAssignee.id]
+              expect(collection.get(taskOne.id).get("sub_tasks").pluck("id")).toEqual [taskOneSub.id]
+              expect(collection.get(taskTwo.id).get("sub_tasks").pluck("id")).toEqual [taskTwoSub.id]
+              expect(collection.get(taskOne.id).get("sub_tasks").get(taskOneSub.id).get("assignees").pluck("id")).toEqual [taskOneSubAssignee.id]
+              expect(collection.get(taskTwo.id).get("sub_tasks").get(taskTwoSub.id).get("assignees").pluck("id")).toEqual [taskTwoAssignee.id]
+              expect(collection.get(taskOne.id).get("project").get("time_entries").pluck("id")).toEqual [projectOneTimeEntry.id]
+              expect(collection.get(taskOne.id).get("project").get("time_entries").models[0].get("task").id).toEqual projectOneTimeEntryTask.id
+              callCount += 1
+
+            success = jasmine.createSpy().and.callFake checkStructure
+
+          context 'with json structure', ->
+            it "separately requests each layer of associations", ->
+              collection = manager.loadCollection "tasks",
+                filters: { parents_only: "true" },
+                success: success,
+                include: [
+                  'assignees',
+                  { project: ["time_entries": "task"] },
+                  { sub_tasks: ["assignees"] }
+                ]
+
+              collection.bind "loaded", checkStructure
+              collection.bind "reset", checkStructure
+
+              expect(success).not.toHaveBeenCalled()
+
+              server.respond() until server.queue.length == 0
+
+              expect(success).toHaveBeenCalledWith(collection)
+              expect(callCount).toEqual 3
+
+          context 'using a backbone collection', ->
+            it "separately requests each layer of associations", ->
+              projectCollection = new Projects null,
+                include: ["time_entries": "task"]
+                test: 10
+
+              collection = manager.loadCollection "tasks",
+                filters: { parents_only: "true" },
+                success: success,
+                include: [
+                  'assignees',
+                  { project: projectCollection },
+                  { sub_tasks: ["assignees"] }
+                ]
+
+              collection.bind "loaded", checkStructure
+              collection.bind "reset", checkStructure
+
+              expect(success).not.toHaveBeenCalled()
+
+              server.respond() until server.queue.length == 0
+              expect(success).toHaveBeenCalled()
+              expect(callCount).toEqual 3
+
+        context 'a shallowly nested json structure', ->
+          beforeEach ->
+            projectOneTimeEntryTask = buildTask()
+            projectOneTimeEntry = buildTimeEntry(title: "without task"); projectOneTimeEntryWithTask = buildTimeEntry(id: projectOneTimeEntry.id, task_id: projectOneTimeEntryTask.id, title: "with task")
+            projectOne = buildProject(); projectOneWithTimeEntries = buildProject(id: projectOne.id, time_entry_ids: [projectOneTimeEntry.id])
+            projectTwo = buildProject(); projectTwoWithTimeEntries = buildProject(id: projectTwo.id, time_entry_ids: [])
+            taskOneAssignee = buildUser()
+            taskTwoAssignee = buildUser()
+            taskOneSubAssignee = buildUser()
+            taskOneSub = buildTask(project_id: projectOne.id, parent_id: 10); taskOneSubWithAssignees = buildTask(id: taskOneSub.id, assignee_ids: [taskOneSubAssignee.id], parent_id: 10)
+            taskTwoSub = buildTask(project_id: projectTwo.id, parent_id: 11); taskTwoSubWithAssignees = buildTask(id: taskTwoSub.id, assignee_ids: [taskTwoAssignee.id], parent_id: 11)
+            taskOne = buildTask(id: 10, project_id: projectOne.id, assignee_ids: [taskOneAssignee.id], sub_task_ids: [taskOneSub.id])
+            taskTwo = buildTask(id: 11, project_id: projectTwo.id, assignee_ids: [taskTwoAssignee.id], sub_task_ids: [taskTwoSub.id])
+            respondWith server, "/api/tasks?include=assignees%2Cproject%2Csub_tasks&parents_only=true&per_page=20&page=1", data: { results: resultsArray("tasks", [taskOne, taskTwo]), tasks: resultsObject([taskOne, taskTwo, taskOneSub, taskTwoSub]), users: resultsObject([taskOneAssignee, taskTwoAssignee]), projects: resultsObject([projectOne, projectTwo]) }
+            respondWith server, "/api/tasks?include=assignees&only=#{taskOneSub.id}%2C#{taskTwoSub.id}&apply_default_filters=false", data: { results: resultsArray("tasks", [taskOneSub, taskTwoSub]), tasks: resultsObject([taskOneSubWithAssignees, taskTwoSubWithAssignees]), users: resultsObject([taskOneSubAssignee, taskTwoAssignee]) }
+            respondWith server, "/api/projects?only=#{projectOne.id}%2C#{projectTwo.id}&apply_default_filters=false&test=10", data: { results: resultsArray("projects", [projectOne, projectTwo]), projects: resultsObject([projectOneWithTimeEntries, projectTwoWithTimeEntries]) }
+
+            callCount = 0
+            checkStructure = (collection) ->
+              expect(collection.pluck("id").sort()).toEqual [taskOne.id, taskTwo.id]
+              expect(collection.get(taskOne.id).get("project").id).toEqual projectOne.id
+              expect(collection.get(taskOne.id).get("assignees").pluck("id")).toEqual [taskOneAssignee.id]
+              expect(collection.get(taskTwo.id).get("assignees").pluck("id")).toEqual [taskTwoAssignee.id]
+              expect(collection.get(taskOne.id).get("sub_tasks").pluck("id")).toEqual [taskOneSub.id]
+              expect(collection.get(taskTwo.id).get("sub_tasks").pluck("id")).toEqual [taskTwoSub.id]
+              expect(collection.get(taskOne.id).get("sub_tasks").get(taskOneSub.id).get("assignees").pluck("id")).toEqual [taskOneSubAssignee.id]
+              expect(collection.get(taskTwo.id).get("sub_tasks").get(taskTwoSub.id).get("assignees").pluck("id")).toEqual [taskTwoAssignee.id]
+
+              callCount += 1
+
+            success = jasmine.createSpy().and.callFake checkStructure
+
+          it "separately requests each layer of associations with filters", ->
             projectCollection = new Projects null,
-              include: ["time_entries": "task"]
+              filters:
+                test: 10
+
+            spyOn(projectCollection.storageManager, 'loadObject').and.callThrough()

             collection = manager.loadCollection "tasks",
               filters: { parents_only: "true" },
@@ -534,6 +593,11 @@
             server.respond() until server.queue.length == 0
             expect(success).toHaveBeenCalled()
             expect(callCount).toEqual 3
+
+            expectedFilters = projectCollection.storageManager.loadObject.calls.all()[1].args[1].filters
+
+            expect(projectCollection.storageManager.loadObject).toHaveBeenCalled()
+            expect(expectedFilters).toEqual({ test: 10 })

       describe "caching", ->
         describe "without ordering", ->


updated form spec to use the new fixtures

--- spec/tests/form/form_spec.coffee
+++ spec/tests/form/form_spec.coffee
@@ -14,7 +14,7 @@
 	singleElementValue = elements[0].value

 	beforeEach ->
-		createTestFormFixture()
+		createAddressFormFixture()

 		shippingForm   = document.getElementById("test-form")
 		addressElement = shippingForm['address']


& should be escaped in plain text

& should be escaped in plain text

--- assets/javascripts/mailcatcher.js.coffee
+++ assets/javascripts/mailcatcher.js.coffee
@@ -277,6 +277,7 @@
         message_iframe = $("#message iframe").contents()
         text = message_iframe.text()
         text = text.replace(/((http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:\/~\+#]*[\w\-\@?^=%&\/~\+#])?)/g, """$1""")
+        text = text.replace(/&/g, "&")
         text = text.replace(/\n/g, "
")
         message_iframe.find("html").html("#{text}")


Remove focus from group spec

Remove focus from group spec

--- angular/test/protractor/group_spec.coffee
+++ angular/test/protractor/group_spec.coffee
@@ -12,7 +12,7 @@
       page.expectElement '.sidebar__list-item--selected'

   describe 'start group from home page', ->
-    fit 'allows starting a group via the start_group route', ->
+    it 'allows starting a group via the start_group route', ->
       staticPage.loadPath 'view_homepage_as_visitor'
       staticPage.click '.header__item--start-button'
       staticPage.fillIn '#group_name', 'My First Group'


fixes

fixes

--- app/assets/javascripts/application.coffee
+++ app/assets/javascripts/application.coffee
@@ -17,7 +17,7 @@
 #= require bootstrap
 #= require wookmark-jquery
 #= require jquery.minicolors
-#= require imagesloaded
+

 #= require_tree .

@@ -53,8 +53,7 @@
     editor = new Editor()
     editor.render()

-  $('.wookmarkable').imagesLoaded ->
-    $('.wookmarkable').wookmark(options)
+  $('.wookmarkable').wookmark(options)

   return

@@ -62,5 +61,3 @@
 # Fire setup event whenever the page loads
 $(document).ready( MakerMap.setup )
 $(document).on('page:load', MakerMap.setup )
-
-$('.wookmarkable').wookmark(options)


Fix datepicker bug!

Fix datepicker bug!

--- app/assets/javascripts/postings.js.coffee
+++ app/assets/javascripts/postings.js.coffee
@@ -82,8 +82,6 @@
                           posting_id: $(this).data("posting-id")
                       success: ->
                         vex.close $vexContent.data().vex.id
-
-                    return
                 )
                 $.extend({}, vex.dialog.buttons.NO,
                   className: "secondary"
@@ -94,29 +92,20 @@
                     $.ajax
                       url: "#{$(this).data("uri")}/#{jobApplicationId}.json"
                       type: "DELETE"
-                      success: ->
+                      success: =>
                         vex.close $vexContent.data().vex.id
-
-                    return
                 )
               ]
-
-              # Give a visual confirmation of what's happened (either way)
               callback: (value) =>
-                if value is "undo"
-                  console.log "Application destroyed!"
-                else
-                  console.log "Application saved!"
+                unless value is "undo"
                   $(this).parents('section').removeClass('action-required')
-                  $(this).replaceWith response.replacementHTML
-                return
+                  $(this).replaceWith(response.replacementHTML)

-              error: (response) ->
-                console.log "Error: ", response
-
-        $("input[type=date]").datepicker {dateFormat: "yy-mm-dd"}
+            $("#job_application_cover_letter").trigger "focus"
+            $("input[type=date]").datepicker {dateFormat: "yy-mm-dd"}

-
+          error: (response) ->
+            console.log "Error: ", response

     # Mark application as followed-up when "follow up" is clicked (from
     # index or detail page)


enhance: show no device found if not available

--- app/components/project-preferences.coffee
+++ app/components/project-preferences.coffee
@@ -30,10 +30,9 @@
         platformType = ENUMS.PLATFORM
         availableVersions = that.set "availableVersions", deviceSelection(deviceType,versions,currentDevice,platformType,platform)
         if Ember.isEmpty availableVersions
-          availableDevices = that.set "availableDevices", ["Loading..."]
-          debugger
+          availableDevices = that.set "availableDevices", [{ "platform_version": "No Device Found" }]
         else
-          that.set "availableDevices", availableVersions
+          availableDevices = that.set "availableDevices", availableVersions
       .catch (error) ->
         that.get("notify").error "failed"
         if Ember.isEmpty error?.errors


Format minutes as ":05" rather than as "5m".

--- bokehjs/src/coffee/common/ticking.coffee
+++ bokehjs/src/coffee/common/ticking.coffee
@@ -580,7 +580,7 @@
         'milliseconds': ['%3Nms', '%S.%3Ns']
         'seconds':      ['%Ss']
         'minsec':       ['%M:%S']
-        'minutes':      ['%Mm']
+        'minutes':      [':%M', '%Mm']
         'hourmin':      ['%H:%M']
         'hours':        ['%Hh', '%H:%M']
         'days':         ['%m/%d', '%a%d']


client: home: convert stacks tab to topnav'ed tabview

--- client/home/lib/stacks/index.coffee
+++ client/home/lib/stacks/index.coffee
@@ -18,18 +18,43 @@

     super options, data

+    @addSubView @topNav  = new kd.TabHandleContainer
+
+    @wrapper.addSubView @tabView = new kd.TabView
+      maxHandleWidth       : 'none'
+      hideHandleCloseIcons : yes
+      detachPanes          : no
+      tabHandleContainer   : @topNav
+
+    @tabView.unsetClass 'kdscrollview'
+
+    @tabView.addPane @stacks      = new kd.TabPaneView { title: 'Stacks', name: 'List' }
+    @tabView.addPane @vms         = new kd.TabPaneView { title: 'Virtual Machines', name: 'VMs' }
+    @tabView.addPane @credentials = new kd.TabPaneView { title: 'Credentials', name: 'Credentials' }
+
+    @tabView.showPane @stacks
+
     kd.singletons.mainController.ready =>
+      @createStacksViews()

-      EnvironmentFlux.actions.loadTeamStackTemplates()
-      EnvironmentFlux.actions.loadPrivateStackTemplates()

-      @wrapper.addSubView new HomeStacksCreate
+  createStacksViews: ->

-      @wrapper.addSubView headerize 'Team Stacks'
-      @wrapper.addSubView sectionize 'Team Stacks', HomeStacksTeamStacks
+    EnvironmentFlux.actions.loadTeamStackTemplates()
+    EnvironmentFlux.actions.loadPrivateStackTemplates()

-      @wrapper.addSubView headerize 'Private Stacks'
-      @wrapper.addSubView sectionize 'Private Stacks', HomeStacksPrivateStacks
+    @stacks.addSubView new HomeStacksCreate

-      @wrapper.addSubView headerize 'Drafts'
-      @wrapper.addSubView sectionize 'Drafts', HomeStacksDrafts
+    @stacks.addSubView headerize 'Team Stacks'
+    @stacks.addSubView sectionize 'Team Stacks', HomeStacksTeamStacks
+
+    @stacks.addSubView headerize 'Private Stacks'
+    @stacks.addSubView sectionize 'Private Stacks', HomeStacksPrivateStacks
+
+    @stacks.addSubView headerize 'Drafts'
+    @stacks.addSubView sectionize 'Drafts', HomeStacksDrafts
+
+
+
+
+


Login: github button added

Login: github button added

--- client/landing/Login/AppView.coffee
+++ client/landing/Login/AppView.coffee
@@ -46,6 +46,11 @@
       tagName     : "a"
       partial     : "Create Account"
       click       : registerHandler
+
+    @github       = new KDButtonView
+      title       : 'Sign in with GitHub'
+      style       : 'solid'
+      callback    : -> KD.singletons.oauthController.openPopup "github"

     # @loginOptions = new LoginOptions
     #   cssClass : "login-options-holder log"
@@ -164,6 +169,7 @@

{{> @goToRecoverLink}}

{{> @goToRegisterLink}}•{{> @backToLoginLink}}

+        {{> @github}}


:bug:Fix missing i18n for es-419

:bug:Fix missing i18n for es-419

--- app/locale/es-419.coffee
+++ app/locale/es-419.coffee
@@ -1047,7 +1047,6 @@
     maka_title: "Defensor del Consumidor"
     robin_title: "Diseñadora de UX & Investigadora" # {change}
     nolan_title: "Administrador de territorio" # {change}
-    lisa_title: "Gerente de Desarrollo de Negocios"
     david_title: "Director de Marketing"
     titles_csm: "Gerente de éxito del cliente"
     titles_territory_manager: "Administrador de territorio"


collaboration: don't kill autosave interval

--- client/ide/collaborationcontroller.coffee
+++ client/ide/collaborationcontroller.coffee
@@ -723,7 +723,6 @@

   cleanupCollaboration: ->

-    KD.utils.killRepeat @autoSaveInterval
     KD.utils.killRepeat @pingInterval
     @rtm?.dispose()
     @rtm = null
@@ -882,4 +881,3 @@
         # remove the leaving participant's info from the collaborative doc
         @removeParticipant KD.nick()

-


Added generator for custom free variable types, e.g., '_cache', '_cache2', and so on.

--- scope.coffee
+++ scope.coffee
@@ -54,12 +54,19 @@

   # If we need to store an intermediate result, find an available name for a
   # compiler-generated variable. `_a`, `_b`, and so on...
-  freeVariable: ->
-    while @check @tempVars.general
-      ordinal = 1 + parseInt @tempVars.general.substr(1), 36
-      @tempVars.general = '_' + ordinal.toString(36).replace(/\d/g, 'a')
-    @variables[@tempVars.general] = 'var'
-    @tempVars.general
+  freeVariable: (type) ->
+    if type
+      next = (prev) ->
+        '_' + type + ((prev and Number(prev.match(/\d+$/) or 1) + 1) or '')
+    else
+      type = 'general'
+      next = (prev) ->
+        ordinal = 1 + parseInt prev.substr(1), 36
+        '_' + ordinal.toString(36).replace /\d/g, 'a'
+    while @check @tempVars[type] or= next()
+      @tempVars[type] = next @tempVars[type]
+    @variables[@tempVars[type]] = 'var'
+    @tempVars[type]

   # Ensure that an assignment is made at the top of this scope
   # (or at the top-level scope, if requested).


ChatSettingsPane: Emit 'SessionStarted' event.

--- client/ide/views/chat/settingspane.coffee
+++ client/ide/views/chat/settingspane.coffee
@@ -97,6 +97,7 @@
       return @startSession.enable()  if err

       @toggleButtons 'started'
+      @emit 'SessionStarted'

   leaveSession: ->


Only set fk id if model isn't new

Only set fk id if model isn't new

--- client/lanes/models/AssociationMap.coffee
+++ client/lanes/models/AssociationMap.coffee
@@ -71,7 +71,7 @@
                 association[fn_name]( attributes )
                 # if we're replaceing the model's contents with another, copy the dirty status as well
                 if association.isModel
-                    model.set(this.pk(name), association.id)
+                    model.set(this.pk(name), association.id) unless association.isNew
                     association.isDirty = value.isDirty if value.isModel

     pk: (name)->


NSE: Events: keep in structure

NSE: Events: keep in structure

--- client/new-stack-editor/lib/events.coffee
+++ client/new-stack-editor/lib/events.coffee
@@ -1,19 +1,38 @@
-module.exports = {
-  'CredentialChangesRevertRequested'
-  'CredentialChangesSaveRequested'
-  'WarnAboutMissingCredentials'
-  'CredentialSelectionChanged'
-  'CredentialFilterChanged'
-  'SelectedProviderChanged'
-  'CredentialListUpdated'
-  'StackWizardCancelled'
-  'InitializeRequested'
-  'TemplateDataChanged'
-  'ToggleCredentials'
-  'ProviderSelected'
-  'LazyLoadFinished'
-  'LazyLoadStarted'
-  'ShowCredentials'
-  'GotFocus'
-  'Log'
-}
+jspath = require 'jspath'
+
+module.exports = do ->
+
+  events = [
+    'CredentialChangesRevertRequested'
+    'CredentialChangesSaveRequested'
+    'WarnAboutMissingCredentials'
+    'CredentialSelectionChanged'
+    'CredentialFilterChanged'
+    'SelectedProviderChanged'
+    'CredentialListUpdated'
+    'StackWizardCancelled'
+    'InitializeRequested'
+    'TemplateDataChanged'
+    'ToggleCredentials'
+    'ProviderSelected'
+    'LazyLoadFinished'
+    'LazyLoadStarted'
+    'ShowCredentials'
+    'GotFocus'
+    'Log'
+    'MenuAction'
+    'Menu.Test'
+    'Menu.Initialize'
+    'Menu.MakeTeamDefault'
+    'Menu.Rename'
+    'Menu.Clone'
+    'Menu.Credentials'
+    'Menu.Logs'
+    'Menu.Delete'
+  ]
+
+  obj = {}
+  events.forEach (event) ->
+    jspath.setAt obj, event, event
+
+  return obj


NavigationMachineItem: Update machine label on UI when machine domain updated.

--- client/Main/navigation/navigationmachineitem.coffee
+++ client/Main/navigation/navigationmachineitem.coffee
@@ -54,6 +54,14 @@
         @setClass status.toLowerCase()

+    @machine.on 'MachineDomainUpdated', @bound 'updateLabel'
+
+
+  updateLabel: ->
+
+    @label.updatePartial @machine.getName()
+
+
   pistachio:->

     return """


sanity check for invalid tson in test

sanity check for invalid tson in test

--- test/test.coffee
+++ test/test.coffee
@@ -1,4 +1,5 @@
 test = require 'tape'
+TSON = require 'tson'

 # mod = require '../src'

@@ -6,6 +7,8 @@
 	t.equals require('../src/config').port, 3000, 'Loaded from cwd'
 	t.end()

+tson = TSON.load __dirname + '/../data/infolis.tson'
+console.log tson
 test "Test config loading", testConfigLoading

 # ALT: src/index.coffee


Compare function should return number (not boolean)

--- test/test.coffee
+++ test/test.coffee
@@ -117,7 +117,7 @@

     it 'sort \'special\' keys before the other keys (+ \'compareFunction\')', (done) ->
       data = fs.readFileSync @fileCustomProperty, {encoding: 'utf8'}
-      compareFunction = (a, b) -> a < b
+      compareFunction = (a, b) -> if a < b then 1 else if a > b then -1 else 0
       options = filename: 'pkg.json', sortOptions:{ compareFunction}

       Finepack data, options, (err, output, messages) ->


Fix the function signatures in tests for them all to pass

--- test/test.coffee
+++ test/test.coffee
@@ -285,14 +285,14 @@
       opts = {poll}

       it 'returns a result it already applies to', (done) -> @create {x:5}, =>
-        @collection.query @client, {'x':5}, opts, (err, emitter) =>
+        @collection.query {'x':5}, opts, (err, emitter) =>
           expected = [docName:@docName, data:{x:5}, type:otTypes.json0.uri, v:1, c:@cName]
           assert.deepEqual emitter.data, expected
           emitter.destroy()
           done()

       it 'gets an empty result set when you query something with no results', (done) ->
-        @collection.query @client, {'xyz':123}, opts, (err, emitter) ->
+        @collection.query {'xyz':123}, opts, (err, emitter) ->
           assert.deepEqual emitter.data, []
           emitter.on 'diff', -> throw new Error 'should not have added results'

@@ -301,7 +301,7 @@
             done()

       it 'adds an element when it matches', (done) ->
-        @collection.query @client, {'x':5}, opts, (err, emitter) =>
+        @collection.query {'x':5}, opts, (err, emitter) =>
           emitter.on 'diff', (diff) =>
             assert.deepEqual diff, [
               index: 0
@@ -316,7 +316,7 @@

       #console.log util.inspect diff, colors:yes, depth:null
       it 'remove an element that no longer matches', (done) -> @create {x:5}, =>
-        @collection.query @client, {'x':5}, opts, (err, emitter) =>
+        @collection.query {'x':5}, opts, (err, emitter) =>
           emitter.on 'diff', (diff) =>
             assert.deepEqual diff, [type:'remove', index:0, howMany:1]

@@ -332,7 +332,7 @@
           @collection.submit @docName, v:1, op:[{p:['x'], od:5, oi:6}], (err, v) =>

       it 'removes deleted elements', (done) -> @create {x:5}, =>
-        @collection.query @client, {'x':5}, opts, (err, emitter) =>
+        @collection.query {'x':5}, opts, (err, emitter) =>
           assert.strictEqual emitter.data.length, 1

           emitter.on 'diff', (diff) =>
@@ -346,7 +346,7 @@
             throw new Error err if err

       it 'does not emit receive events to a destroyed query', (done) ->
-        @collection.query @client, {'x':5}, opts, (err, emitter) =>
+        @collection.query {'x':5}, opts, (err, emitter) =>
           emitter.on 'diff', -> throw new Error 'add called after destroy'

           emitter.destroy()
@@ -363,19 +363,19 @@
         @create2 '_p1', {x:5, i:1}, => @create2 '_p2', {x:5, i:2}, => @create2 '_p3', {x:5, i:3}, => callback()

       it 'respects limit queries', (done) ->
-        @collection.query @client, {$query:{'x':5}, $orderby:{'i':1}, $limit:1}, {poll:true}, (err, emitter) ->
+        @collection.query {$query:{'x':5}, $orderby:{'i':1}, $limit:1}, {poll:true}, (err, emitter) ->
           assert.strictEqual emitter.data.length, 1
           assert.strictEqual emitter.data[0].docName, '_p1'
           done()

       it 'respects skips', (done) ->
-        @collection.query @client, {$query:{'x':5}, $orderby:{'i':1}, $limit:1, $skip:1}, {poll:true}, (err, emitter) ->
+        @collection.query {$query:{'x':5}, $orderby:{'i':1}, $limit:1, $skip:1}, {poll:true}, (err, emitter) ->
           assert.strictEqual emitter.data.length, 1
           assert.strictEqual emitter.data[0].docName, '_p2'
           done()

       it 'will insert an element in the set', (done) ->
-        @collection.query @client, {$query:{'x':5}, $orderby:{'i':1}}, {poll:true}, (err, emitter) =>
+        @collection.query {$query:{'x':5}, $orderby:{'i':1}}, {poll:true}, (err, emitter) =>
           assert.equal emitter.data.length, 3

           emitter.on 'diff', (diff) =>
@@ -391,7 +391,7 @@
           @create2 '_p4', {x:5, i:1.5}

       it 'will remove an element from the set', (done) ->
-        @collection.query @client, {$query:{'x':5}, $orderby:{'i':1}}, {poll:true}, (err, emitter) =>
+        @collection.query {$query:{'x':5}, $orderby:{'i':1}}, {poll:true}, (err, emitter) =>
           emitter.once 'diff', (diff) ->
             assert.deepEqual diff, [type:'remove', index:0, howMany:1]
             emitter.once 'diff', (diff) ->
@@ -434,7 +434,7 @@
           [@cName, @cName2]

         called = 0
-        @testWrapper.query = (cName, query, callback) ->
+        @testWrapper.query = (livedb, cName, query, callback) ->
           # This should get called three times:
           # When client.query is called initially and when the c1 and c2 documents are created
           called++
@@ -470,7 +470,7 @@
         @testWrapper.query = (client, cName, query, callback) ->
           callback null, {results:[], extra:{x:x++}}

-        @collection.query @client, {x:5}, {backend:'test'}, (err, stream) =>
+        @collection.query {x:5}, {backend:'test'}, (err, stream) =>
           assert.deepEqual stream.extra, {x:1}

           stream.on 'extra', (extra) ->


Clean up

Clean up

--- test/test.coffee
+++ test/test.coffee
@@ -26,15 +26,8 @@
         .waitForExist '.crowdstart-line-item'
         .waitForVisible '.crowdstart-line-item:nth-child(2) .select2'

-
         # Select 2 for 'Such T-shirt
-        # TODO Use clicks instead
         .selectByValue('.crowdstart-invoice > div:nth-child(2) select', '2')
-        # .click '.crowdstart-line-item:nth-child(2) .select2'
-
-        # .waitForVisible 'ul.select2-results__options > li:nth-child(3)'
-        # .moveToObject 'ul.select2-results__options > li:nth-child(3)'
-        # .click 'ul.select2-results__options > li:nth-child(3)'

         .getText 'div.crowdstart-invoice > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > span', (err, res) ->
           unitPrice = parsePrice res
@@ -42,7 +35,6 @@
         .getText 'div.crowdstart-invoice > div:nth-child(2) > div:nth-child(2) > div.crowdstart-col-1-3-bl.crowdstart-text-right.crowdstart-money', (err, res) ->
           lineItemPrice = parsePrice res
           assert.strictEqual lineItemPrice, unitPrice * 2
-          console.log lineItemPrice
         .end done

   describe 'Completing the form', ->


Minor stuff

Minor stuff

--- src/scripts/airline_tycoon.cjsx
+++ src/scripts/airline_tycoon.cjsx
@@ -7,19 +7,14 @@
 player1 =
   name: 'Blue'
   color: blue
-  planes: [
-  ]

 player2 =
+  name: 'Pink'
   color: pink
-  name: 'Pink'
-  planes: [
-  ]

 nobody =
+  name: 'Nobody'
   color: grey
-  name: 'Nobody'
-  planes: []

 planes = [
   {name: 'Plane1', flights: 0, location: 'DUB', owner: player1},
@@ -62,6 +57,7 @@

 Airport = React.createClass
   render: ->
+    # There are here cause it was quick and I'm lazy, but parts probably be better suited in CSS.
     style =
       backgroundColor: @props.owner.color
       border: '1px solid #000'
@@ -78,6 +74,7 @@

 Route = React.createClass
   render: ->
+    # There are here cause it was quick and I'm lazy, but parts probably be better suited in CSS.
     style =
       backgroundColor: '#ccc'
       border: '1px solid #000'


Strip trailing slash from URL

Strip trailing slash from URL

Looks like beeradvocate.com is adding trailing slashes which break the image URL.

--- src/scripts/beeradvocate.coffee
+++ src/scripts/beeradvocate.coffee
@@ -21,7 +21,7 @@
         msg.http("http://beeradvocate.com/beer/")
             .get() (err, res, body) ->
                 if (res.statusCode == 200)
-                    reg = /
(.+?)<\/a><\/h6>/i
+                    reg = /
(.+?)<\/a><\/h6>/i
                     results = body.match(reg)
                     if (results != null && results.length > 3)
                         msg.send results[3]


Tweaks to grocery list

Tweaks to grocery list

--- src/scripts/grocery-list.coffee
+++ src/scripts/grocery-list.coffee
@@ -11,7 +11,7 @@

   groceryList =
     get: ->
-      unless robot.brain.data.grocerylist
+      unless robot.brain.data.grocerylist?
         robot.brain.data.groceryList = []
       robot.brain.data.groceryList

@@ -28,7 +28,7 @@
     list = groceryList.get().join("\n") || "No items in your grocery list."
     msg.send list

-  robot.respond /remind me to buy (.*)/i, (msg) ->
+  robot.respond /remind me to (buy|get) (.*)/i, (msg) ->
     item = msg.match[1].trim()
     groceryList.add item
     msg.send "ok, added #{item} to your grocery list."


Disable save button while the session is not dirty

--- assets/js/directives/toolbar.coffee
+++ assets/js/directives/toolbar.coffee
@@ -16,7 +16,7 @@
   template: """

-        <button ng-click="session.save()" title="Save your work as a new Plunk" class="btn btn-primary"> Save</button>
+        <button ng-disabled="!session.isDirty()" ng-click="session.save()" title="Save your work as a new Plunk" class="btn btn-primary"> Save</button>

         <button ng-click="session.fork()" title="Save your changes as a fork of this Plunk" class="btn"> Fork</button>


xit -> it

xit -> it

--- spec/lib/facade.coffee
+++ spec/lib/facade.coffee
@@ -25,7 +25,7 @@
             f = Facade.createInstance()
             expect(f.hasClass('hobby')).to.be.false

-        xit 'returns true if a class with the given name is found', ->
+        it 'returns true if a class with the given name is found', ->
             f = Facade.createInstance()
             f.addClass('hobby', ->)
             expect(f.hasClass('hobby')).to.be.true


recfrac2: pull out click as a function

recfrac2: pull out click as a function

--- recfrac2/rf.coffee
+++ recfrac2/rf.coffee
@@ -119,7 +119,9 @@
     hilit[0].remove()
     hilit[1].remove()

-  cells.on("click", (d) ->
+  cells.on("click", (d) -> click(d))
+
+  click = (d) ->
     svg.append("text")
         .text(->
           if d.row is d.col
@@ -162,8 +164,6 @@
               xscale(d.col)-xscale.rangeBand()/2
           )
           .attr("y", yscale(d.row)+yscale.rangeBand())
-  )
-

   # add black border
   svg.append("rect")


Longer timeout so travis can load the html

--- spec/suite.spec.coffee
+++ spec/suite.spec.coffee
@@ -20,6 +20,7 @@
 describe '#text', ->

   it 'verifies that an element has exact text', (done) ->
+    @timeout 5000 # I know. :(
     expect('h1').dom.to.have.text "The following text is an excerpt from Finnegan's Wake by James Joyce", done

   it 'verifies that an element does not have exact text', (done) ->


Updated javascript errors to be a bit terser

--- attitude/display/assets/math.coffee
+++ attitude/display/assets/math.coffee
@@ -9,7 +9,7 @@
       newArray[j].push array[i][j]
   newArray

- identity = [[1,0,0],[0,1,0],[0,0,1]]
+identity = [[1,0,0],[0,1,0],[0,0,1]]

 norm = (d)->
   # L2 norm (hypotenuse)
@@ -28,6 +28,14 @@
   angles = (i*step for i in [0...n])
   ([Math.cos(a),Math.sin(a)] for a in angles)

+cart2sph = (opts)->
+  opts.degrees ?= true
+  c = if opts.degrees then 180/Math.PI else 1
+  (d)->
+    r = norm(d)
+    [x,y,z] = d
+    # Converts xyz to lat lon
+    [c*Math.atan2(y,x),c*Math.asin z/r]

 planeErrors = (axesCovariance, axes, opts={})->
   # Get a single level of planar errors (or the
@@ -43,8 +51,6 @@

   s = axesCovariance.map Math.sqrt
   axes = transpose(axes)
-
-  c = if degrees then 180/Math.PI else 1

   scales =
     upper: 1
@@ -66,7 +72,6 @@
          s[2]*c1]

     d = (sdot(e,i) for i in axes)
-    r = norm(d)

     if opts.traditionalLayout
       [y,z,x] = d
@@ -77,11 +82,15 @@
     if not upperHemisphere
       z *= -1

+    c = if degrees then 180/Math.PI else 1
+    d = [x,y,z]
+    r = norm(d)
     return [
       c*Math.atan2(y,x),
       c*Math.asin z/r]

-  return ell.map(stepFunc)
+  return ell
+    .map stepFunc

 normalErrors = (axesCovariance, axes, opts={})->
   # Get a single level of planar errors (or the
@@ -101,8 +110,6 @@
   s = axesCovariance.map Math.sqrt
   axes = transpose(axes)

-  c = if degrees then 180/Math.PI else 1
-
   c1 = 1
   if upperHemisphere
     c1 *= -1
@@ -120,7 +127,6 @@
     e.push norm(f)

     d = (sdot(e,i) for i in axes)
-    r = norm(d)

     if opts.traditionalLayout
       [y,z,x] = d
@@ -131,11 +137,15 @@
     if not upperHemisphere
       z *= -1

+    c = if degrees then 180/Math.PI else 1
+    d = [x,y,z]
+    r = norm(d)
     return [
       c*Math.atan2(y,x),
       c*Math.asin z/r]

-  return ell.map(stepFunc)
+  return ell
+    .map stepFunc

 combinedErrors = (sv, ax, opts={})->
   func = (type)->


fix git view

fix git view

--- asterisk.coffee
+++ asterisk.coffee
@@ -152,9 +152,14 @@
             marks: marks

 gitdiff = (s, filename) ->
-    print "running git diff", filename
-    command = "git diff --no-color -U0 #{filename}"
+    print "running git diff",
+    dir = filename.match(/(.*\/)[^\/]/)[1]
+
+    command = "cd '#{dir}'; git diff --no-color -U0 #{filename}"
     exec command, (error, stdout, stderr) ->
+        console.log "command", command
+        console.log "stdout", stdout
+        console.log "stderr", stderr
         marks = []

         make_mark = (regex) ->


In case of an error the project will now stop, which it shouldn't, this solves that issue

--- src/extra/project/index.coffee
+++ src/extra/project/index.coffee
@@ -57,9 +57,10 @@

 		@i++

+		@cb() if (@options.type is 1 and @i is 4) or (@options.type is 2 and @i is 2)
+
 		return log.debug 'LDE - Project', 'Unable to create folder', e if e

-		@cb() if (@options.type is 1 and @i is 4) or (@options.type is 2 and @i is 2)


skip nested attributes if there aren't any

--- src/extras/batman.rails.coffee
+++ src/extras/batman.rails.coffee
@@ -119,6 +119,9 @@

   @::before 'create', 'update', (env, next) ->
+    nestedAttributeKeys = @model._encodesNestedAttributesForKeys
+    return next() unless nestedAttributeKeys.length
+
     # if not serializing as form, the data has already been stringified
     if @serializeAsForm
       data = env.options.data
@@ -130,7 +133,7 @@
     else
       recordJSON = data

-    for key in @model._encodesNestedAttributesForKeys
+    for key in nestedAttributeKeys
       if recordJSON[key]?
         attrs = recordJSON["#{key}_attributes"] = recordJSON[key]
         delete recordJSON[key]


Now Post is a proper class

Now Post is a proper class

global Post is not overridden here
so it is absolutely has nothing to do
with global Post.

Will gradually move all Post functionality from
previous implementation here.

--- lib/assets/javascripts/moebooru/post.js.coffee
+++ lib/assets/javascripts/moebooru/post.js.coffee
@@ -1,24 +1,29 @@
 # Start Post transition from prototypejs.
+#

-Post.highres = ($) ->
-    if Post.inLargerVersion
+class Post
+    constructor: ->
+        @inLargerVersion = false
+        @image = jQuery('#image')
+        @width = @image.attr('large_width')
+        @height = @image.attr('large_height')
+        @largeSource = jQuery('#highres-show').attr('href')
+
+    highres: () ->
+        return false if @inLargerVersion
+        @image.detach()
+        @image.attr('src', '')
+        @image.attr('width', @width)
+        @image.attr('height', @height)
+        @image.attr('src', @largeSource)
+        @image.insertAfter('#note-container')
+        @inLargerVersion = true
         return false
-    Post.inLargerVersion = true
-    img = $("#image")
-    large_image = $("#highres-show").attr("href")
-    w = img.attr("large_width")
-    h = img.attr("large_height")
-    img = img.detach()
-    img.attr("src", "")
-    img.attr("width", w)
-    img.attr("height", h)
-    img.attr("src", large_image)
-    img.insertAfter('#note-container')
-    return false
+

 # Post.highres patch for #image
-# Still have to pass $ around manually.
 # XXX: Centralized .ready (?)
 jQuery ($) ->
-    $("#highres-show").on "click", () ->
-        Post.highres($)
+    post = new Post()
+    $('#highres-show').on 'click', ->
+        post.highres()


Add codo comments

Add codo comments

--- lib/client_connector/socketio_connector.coffee
+++ lib/client_connector/socketio_connector.coffee
@@ -28,15 +28,18 @@
 validator = require "../validator"
 codes = require "../codes"

+#
+# Class that defines a socket-IO Connector (to connect a hAPI to a hEngine)
+#
 class SocketIO_Connector
-  ###
-  Runs a SocketIO Connector with the given arguments.
-  @param args - {
-  logLevel : DEBUG, INFO, WARN or ERROR
-  port : int
-  commandOptions : {} Command Controller Options
-  }
-  ###
+
+  # @property {boolean} Connector's status
+  started: undefined
+
+  #
+  # Connector's constructor. Create the websocket which be used by the socketIO adapter.
+  # @param properties {object} Properties of the socket-IO connector
+  #
   constructor: (properties) ->
     if properties.owner
     then @owner = properties.owner
@@ -59,7 +62,7 @@

     io.set "log level", logLevels[@owner.log_properties.logLevel]

-    channel = io.on("connection", (socket) =>
+    io.on("connection", (socket) =>
       id = socket.id
       clients[id] =
         id: id
@@ -73,10 +76,11 @@

     )

-  ###
-  @param client - Reference to the client
-  @param data - Expected {urn, password}
-  ###
+  #
+  # Method which connect the websocket
+  # @param client {object} Reference to the client
+  # @param data {object} Connexion data (urn, password)
+  #
   connect: (client, data) ->
     unless client
       @owner.log "warn", "A client sent an invalid ID with data", data
@@ -131,13 +135,13 @@

       cb authResult.actor, codes.errors.NO_ERROR

-  ###
-  Disconnects the current session and socket.
-  @param client - Reference to the client to close
-  ###
+  #
+  # Method which disconnect the websocket
+  # @param client {object} Reference to the client to close
+  #
   disconnect: (client) ->
     if client and client.hClient
-      log.debug "Disconnecting Client " + client.publisher
+      @owner.log "debug", "Disconnecting Client " + client.publisher
       client.socket.disconnect()  if client.socket
       delete clients[client.id]
     else if client
@@ -146,10 +150,15 @@

     @owner.send @owner.h_buildSignal(client.child, "stop", {}) if client.child

-
+  #
+  # Method called when the connector is started
+  #
   start: ->
     @started = true

+  #
+  # Method called when the connector is stopping
+  #
   stop: ->
     @started = false


Applied helpers to the server.

Applied helpers to the server.

--- src/app.coffee
+++ src/app.coffee
@@ -1,9 +1,10 @@
 configuration = require './configuration'
+helpers = require './helpers'
 express = require 'express'

 server = express.createServer()

 configuration.apply server
+helpers.apply server

 server.listen server.set 'listening-port'
-


Don't show size in thumbnail mode.

Don't show size in thumbnail mode.

--- src/FileManager/FileDirective.coffee
+++ src/FileManager/FileDirective.coffee
@@ -20,7 +20,7 @@

{{ file.metadata.name |ellipsis:20 }}

                   <input type="text" ng-model="newName" ng-show="isEditMode()" ng-blur="changeName(true)" ng-keypress="changeName($event)" select="isEditMode()"/>

-
{{ file.metadata.size |size }}

+
{{ file.metadata.size |size }}

               """


Camelize error attributes

Camelize error attributes

--- src/interfaces/json_api.coffee
+++ src/interfaces/json_api.coffee
@@ -462,7 +462,7 @@
       else
         _.each error['source']['pointer'].split('/data'), (i) ->
           if(m = i.match(/\/(attributes|relationships|)\/(\w+)/))?
-            attribute.push m[2]
+            attribute.push s.camelize(m[2])

       resource.errors().add(attribute.join('.'), s.camelize(error['code']), error['detail'])
     resource


call previous twice

call previous twice

--- spotify-control.coffee
+++ spotify-control.coffee
@@ -64,6 +64,8 @@
       if err
         msg.send "Error: #{err}"
       else
+        #call twice, once to go to start, then again to go to last track
+        iface.Previous()
         iface.Previous()
         msg.send ":previous:"


CLI tests now uses child_process.fork

CLI tests now uses child_process.fork

--- tests/cli/cli.coffee
+++ tests/cli/cli.coffee
@@ -12,12 +12,21 @@

 NEFT_BIN_PATH = pathUtils.join(fs.realpathSync('./'), 'bin/neft.js')

+forkSilent = (path, args, options, callback) ->
+    options = utils.mergeAll {}, options, silent: true
+    child = cp.fork path, args, options
+    stdout = ''
+    child.stdout.on 'data', (data) -> stdout += data
+    stderr = ''
+    child.stderr.on 'data', (data) -> stderr += data
+    child.on 'exit', (code) ->
+        callback code, stdout, stderr
+    child
+
 createApp = (callback) ->
     path = pathUtils.join os.tmpdir(), 'neft-project'
     fs.removeSync path
-
-    cpCmd = "#{NEFT_BIN_PATH} create #{path}"
-    cp.exec cpCmd, callback
+    forkSilent NEFT_BIN_PATH, ['create', path], null, callback
     path

 describe 'CLI', ->
@@ -31,14 +40,14 @@
         describe 'node', ->
             it 'creates bundle', (done) ->
                 path = createApp (err, _, stderr) ->
-                    cp.exec "cd #{path} && #{NEFT_BIN_PATH} build node", (err, _, stderr) ->
+                    forkSilent NEFT_BIN_PATH, ['build', 'node'], cwd: path, (err, _, stderr) ->
                         assert.ok fs.existsSync pathUtils.join(path, 'build/app-node-develop.js')
                         done err or stderr

         describe 'browser', ->
             it 'creates bundle', (done) ->
                 path = createApp (err, _, stderr) ->
-                    cp.exec "cd #{path} && #{NEFT_BIN_PATH} build browser", (err, _, stderr) ->
+                    forkSilent NEFT_BIN_PATH, ['build', 'browser'], cwd: path, (err, _, stderr) ->
                         assert.ok fs.existsSync pathUtils.join(path, 'build/app-browser-develop.js')
                         done err or stderr

@@ -46,10 +55,10 @@
         describe 'node', ->
             it 'starts node server', (done) ->
                 path = createApp (err, _, stderr) ->
-                    child = cp.exec "cd #{path} && #{NEFT_BIN_PATH} run node", (err, _, stderr) ->
+                    child = forkSilent NEFT_BIN_PATH, ['run', 'node'], cwd: path, (err, _, stderr) ->
                         done err or stderr

                     child.stdout.on 'data', (msg) ->
-                        if utils.has(msg, 'Start as')
+                        if utils.has(String(msg), 'Start as')
                             child.kill()
                             setTimeout done