fix unit test

fix unit test

--- test/unit/coffee/LearnedWordsManagerTests.coffee
+++ test/unit/coffee/LearnedWordsManagerTests.coffee
@@ -87,11 +87,11 @@

 	describe "deleteUsersLearnedWords", ->
 		beforeEach ->
-			@db.spellingPreferences.delete = sinon.stub().callsArgWith(1)
+			@db.spellingPreferences.remove = sinon.stub().callsArgWith(1)

 		it "should get the word list for the given user", (done)->
 			@LearnedWordsManager.deleteUsersLearnedWords @token, =>
-				@db.spellingPreferences.delete.calledWith(token: @token).should.equal true
+				@db.spellingPreferences.remove.calledWith(token: @token).should.equal true
 				done()


Inline valid extensions to check for

Inline valid extensions to check for

--- src/module-cache.coffee
+++ src/module-cache.coffee
@@ -2,11 +2,6 @@
 path = require 'path'
 fs = require 'fs-plus'
 semver = require 'semver'
-
-# Make sure CoffeeScript is required when this file is required directly
-# by apm
-unless require.extensions['.coffee']
-  require('coffee-script').register()

 nativeModules = process.binding('natives')

@@ -58,7 +53,7 @@
   onDirectory = (childPath) ->
     path.basename(childPath) isnt 'node_modules'

-  extensions = Object.keys(require.extensions)
+  extensions = ['.js', '.coffee', '.json', '.node']
   paths = {}
   onFile = (childPath) ->
     if path.extname(childPath) in extensions


improved x-editable handling

improved x-editable handling

--- admin/experimentAdmin.coffee
+++ admin/experimentAdmin.coffee
@@ -82,17 +82,16 @@

 Template.tsAdminBatchEditDesc.rendered = ->
   container = @$('div.editable')
-  settings =
-    # When opening the popover, get the value from text
-    value: -> $.trim container.text()
-    # Don't set innerText ourselves, let Meteor update to preserve reactivity
-    display: ->
+  grabValue = -> $.trim container.text() # Always get reactively updated value
+  container.editable
+    value: grabValue
+    display: -> # Never set text; have Meteor update to preserve reactivity
     success: (response, newValue) =>
       Batches.update @data._id,
         $set: { desc: newValue }
-      # Reconstruct the editable so it shows the correct form value next time
-      container.editable('destroy').editable(settings)
-  container.editable(settings)
+      # Thinks it knows the value, but it actually doesn't - grab a fresh value each time
+      Meteor.defer -> container.data('editableContainer').formOptions.value = grabValue
+      return # The value of this function matters
   return

 Template.tsAdminBatchEditTreatments.events =


CoreFunctions: Refactor to share asImmediateFunction

--- src/main/runtime/ReactiveRunner.coffee
+++ src/main/runtime/ReactiveRunner.coffee
@@ -17,22 +17,13 @@
   isRxObservable = (func) -> typeof func.subscribe == 'function'
   returnsStream = (func) -> func.kind == ReactiveRunner.STREAM or func.kind == ReactiveRunner.TRANSFORM_STREAM or func.returnKind == ReactiveRunner.STREAM_RETURN or func.returnKind == ReactiveRunner.SEQUENCE_RETURN
   returnsAggregate = (func) -> func.returnKind == ReactiveRunner.AGGREGATE_RETURN
+  withKind = (func, kind) -> func.kind = kind; func

-  asImmediateFunction = (func) -> (s) ->
+  asImmediateFunction = (func) -> (s, f) ->
     results = []
     seq = Rx.Observable.from s, null, null, Rx.Scheduler.immediate
-    func(seq).subscribe (x) -> results.push x
+    func(seq, f).subscribe (x) -> results.push x
     if returnsAggregate(func) then _.last results else results
-
-  asImmediateTransformFunction = (func) ->
-    immFn = (s, f) ->
-      results = []
-      seq = Rx.Observable.from s, null, null, Rx.Scheduler.immediate
-      func(seq, f).subscribe (x) -> results.push x
-      if returnsAggregate(func) then _.last results else results
-
-    immFn.kind = ReactiveRunner.TRANSFORM
-    immFn

   constructor: (@providedFunctions = {}, @userFunctions = {}) ->
     @valueChanges = new Rx.Subject()
@@ -54,9 +45,8 @@
   addProvidedStreams: (functionMap) -> @addProvidedStream n, s for n, s of functionMap

   addProvidedTransformFunction: (name, fn) ->
-    @_addProvidedFunction name, asImmediateTransformFunction fn
-    fn.kind = ReactiveRunner.TRANSFORM_STREAM
-    @_addProvidedFunction name + 'Over', fn
+    @_addProvidedFunction name, withKind(asImmediateFunction(fn), ReactiveRunner.TRANSFORM)
+    @_addProvidedFunction name + 'Over', withKind(fn, ReactiveRunner.TRANSFORM_STREAM)

   addProvidedTransformFunctions: (functionMap) -> @addProvidedTransformFunction n, f for n, f of functionMap


converting some natives to async

converting some natives to async

we're still failing some in-browser tests

--- src/natives/main.coffee
+++ src/natives/main.coffee
@@ -116,12 +116,6 @@
           rs.meta_stack().push {}  # dummy
           resume_cb()),
         except_cb
-
-stat_fd = (fd) ->
-  try
-    return fs.fstatSync fd
-  catch e
-    return null

 stat_file = (fname, cb) ->
   fs.stat fname, (err, stat) ->
@@ -729,8 +723,9 @@
               fs.open fname.jvm2js_str(), 'a', (err, fd) ->
                 fd_obj = _this.get_field rs, 'Ljava/io/FileOutputStream;fd'
                 fd_obj.set_field rs, 'Ljava/io/FileDescriptor;fd', fd
-                _this.$pos = (stat_fd fd).size
-                resume_cb()
+                fs.fstat fd, (err, stats) ->
+                  _this.$pos = stats.size
+                  resume_cb()
         o 'writeBytes([BIIZ)V', write_to_file  # OpenJDK version
         o 'writeBytes([BII)V', write_to_file   # Apple-java version
         o 'close0()V', (rs, _this) ->
@@ -750,18 +745,22 @@
             fd = fd_obj.get_field rs, 'Ljava/io/FileDescriptor;fd'
             rs.java_throw rs.get_bs_class('Ljava/io/IOException;'), "Bad file descriptor" if fd is -1
             return 0 if fd is 0 # no buffering for stdin
-            stats = fs.fstatSync fd
-            stats.size - _this.$pos
+            rs.async_op (cb) ->
+              fs.fstat fd, (err, stats) ->
+                cb(stats.size - _this.$pos)
         o 'read()I', (rs, _this) ->
             fd_obj = _this.get_field rs, 'Ljava/io/FileInputStream;fd'
             fd = fd_obj.get_field rs, 'Ljava/io/FileDescriptor;fd'
             rs.java_throw rs.get_bs_class('Ljava/io/IOException;'), "Bad file descriptor" if fd is -1
             unless fd is 0
               # this is a real file that we've already opened
-              buf = new Buffer((fs.fstatSync fd).size)
-              bytes_read = fs.readSync(fd, buf, 0, 1, _this.$pos)
-              _this.$pos++
-              return if bytes_read == 0 then -1 else buf.readUInt8(0)
+              rs.async_op (cb) ->
+                fs.fstat fd, (err, stats) ->
+                  buf = new Buffer(stats.size)
+                  fs.read fd, buf, 0, 1, _this.$pos, (err, bytes_read) ->
+                    _this.$pos++
+                    cb(if bytes_read == 0 then -1 else buf.readUInt8(0))
+              return
             # reading from System.in, do it async
             rs.async_op (cb) ->
               rs.async_input 1, (byte) ->
@@ -774,16 +773,15 @@
               # this is a real file that we've already opened
               pos = _this.$pos
               buf = new Buffer n_bytes
-              # if at end of file, return -1.
-              filesize = fs.fstatSync(fd).size
-              if filesize > 0 and pos >= filesize-1
-                return -1
-              bytes_read = fs.readSync(fd, buf, 0, n_bytes, pos)
-              # not clear why, but sometimes node doesn't move the file pointer,
-              # so we do it here ourselves
-              _this.$pos += bytes_read
-              byte_arr.array[offset+i] = buf.readUInt8(i) for i in [0...bytes_read] by 1
-              return if bytes_read == 0 and n_bytes isnt 0 then -1 else bytes_read
+              rs.async_op (cb) ->
+                fs.read fd, buf, 0, n_bytes, pos, (err, bytes_read) ->
+                  return cb(-1) if err?  # XXX: should check this
+                  # not clear why, but sometimes node doesn't move the
+                  # file pointer, so we do it here ourselves.
+                  _this.$pos += bytes_read
+                  byte_arr.array[offset+i] = buf.readUInt8(i) for i in [0...bytes_read] by 1
+                  cb(if bytes_read == 0 and n_bytes isnt 0 then -1 else bytes_read)
+              return
             # reading from System.in, do it async
             rs.async_op (cb) ->
               rs.async_input n_bytes, (bytes) ->
@@ -821,10 +819,13 @@
             fd = fd_obj.get_field rs, 'Ljava/io/FileDescriptor;fd'
             rs.java_throw rs.get_bs_class('Ljava/io/IOException;'), "Bad file descriptor" if fd is -1
             unless fd is 0
-              bytes_left = fs.fstatSync(fd).size - _this.$pos
-              to_skip = Math.min(n_bytes.toNumber(), bytes_left)
-              _this.$pos += to_skip
-              return gLong.fromNumber(to_skip)
+              rs.async_op (cb) ->
+                fs.fstat fd, (err, stats) ->
+                  bytes_left = stats.size - _this.$pos
+                  to_skip = Math.min(n_bytes.toNumber(), bytes_left)
+                  _this.$pos += to_skip
+                  cb gLong.fromNumber(to_skip), null
+              return
             # reading from System.in, do it async
             rs.async_op (cb) ->
               rs.async_input n_bytes.toNumber(), (bytes) ->
@@ -867,19 +868,20 @@
         o 'length()J', (rs, _this) ->
             fd_obj = _this.get_field rs, 'Ljava/io/RandomAccessFile;fd'
             fd = fd_obj.get_field rs, 'Ljava/io/FileDescriptor;fd'
-            gLong.fromNumber (stat_fd fd).size
+            rs.async_op (cb) ->
+              fs.fstat fd, (err, stats) ->
+                cb gLong.fromNumber(stats.size), null
         o 'seek(J)V', (rs, _this, pos) -> _this.$pos = pos.toNumber()
         o 'readBytes([BII)I', (rs, _this, byte_arr, offset, len) ->
             fd_obj = _this.get_field rs, 'Ljava/io/RandomAccessFile;fd'
             fd = fd_obj.get_field rs, 'Ljava/io/FileDescriptor;fd'
-            # if at end of file, return -1.
-            if _this.$pos >= fs.fstatSync(fd).size-1
-              return -1
             buf = new Buffer len
-            bytes_read = fs.readSync(fd, buf, 0, len, _this.$pos)
-            byte_arr.array[offset+i] = buf.readUInt8(i) for i in [0...bytes_read] by 1
-            _this.$pos += bytes_read
-            return if bytes_read == 0 and len isnt 0 then -1 else bytes_read
+            rs.async_op (cb) ->
+              fs.read fd, buf, 0,len, _this.$pos, (err, bytes_read) ->
+                return cb(-1) if err?  # XXX: should check this
+                byte_arr.array[offset+i] = buf.readUInt8(i) for i in [0...bytes_read] by 1
+                _this.$pos += bytes_read
+                cb(if bytes_read == 0 and len isnt 0 then -1 else bytes_read)
         o 'writeBytes([BII)V', (rs, _this, byte_arr, offset, len) ->
             fd_obj = _this.get_field rs, 'Ljava/io/RandomAccessFile;fd'
             fd = fd_obj.get_field rs, 'Ljava/io/FileDescriptor;fd'
@@ -1317,10 +1319,11 @@
           # Reports this file's size
           o 'size0(Ljava/io/FileDescriptor;)J', (rs, _this, fd_obj) ->
             fd = fd_obj.get_field rs, 'Ljava/io/FileDescriptor;fd'
-            try
-              return gLong.fromNumber(fs.fstatSync(fd).size)
-            catch e
-              rs.java_throw rs.get_bs_class('Ljava/io/IOException;'), 'Bad file descriptor.'
+            rs.async_op (cb) ->
+              fs.fstat fd, (err, stats) ->
+                if err?
+                  rs.java_throw rs.get_bs_class('Ljava/io/IOException;'), 'Bad file descriptor.'
+                cb gLong.fromNumber(stats.size)
           o 'position0(Ljava/io/FileDescriptor;J)J', (rs, _this, fd, offset) ->
               parent = _this.get_field rs, 'Lsun/nio/ch/FileChannelImpl;parent'
               gLong.fromNumber(
@@ -1336,14 +1339,15 @@
             # read upto len bytes and store into mmap'd buffer at address
             block_addr = rs.block_addr(address)
             buf = new Buffer len
-            bytes_read = fs.readSync(fd, buf, 0, len)
-            if DataView?
-              for i in [0...bytes_read] by 1
-                rs.mem_blocks[block_addr].setInt8(i, buf.readInt8(i))
-            else
-              for i in [0...bytes_read] by 1
-                rs.mem_blocks[block_addr+i] = buf.readInt8(i)
-            return bytes_read
+            rs.async_op (cb) ->
+              fs.read fd, buf, 0, len, (err, bytes_read) ->
+                if DataView?
+                  for i in [0...bytes_read] by 1
+                    rs.mem_blocks[block_addr].setInt8(i, buf.readInt8(i))
+                else
+                  for i in [0...bytes_read] by 1
+                    rs.mem_blocks[block_addr+i] = buf.readInt8(i)
+                cb bytes_read
           o 'preClose0(Ljava/io/FileDescriptor;)V', (rs, fd_obj) ->
             # NOP, I think the actual fs.close is called later. If not, NBD.
         ]


Deprecated n-style: fix for tests

Deprecated n-style: fix for tests

--- tests/integration/src/styles/style.server.coffee
+++ tests/integration/src/styles/style.server.coffee
@@ -323,35 +323,35 @@
             ulNode.props.set 'n-each', [5, 6, 7]
             test()

-    describe "'n-style:' attributes", ->
+    describe "'style:' attributes", ->
         it 'are set on a style item', ->
             doc = render
-                html: '<b n-style="renderer:Item" n-style:x="50" />'
+                html: '<b n-style="renderer:Item" style:x="50" />'

             {node, item} = doc.styles[0]

             assert.instanceOf item, Renderer.Item
-            assert.is item.x, node.props['n-style:x']
+            assert.is item.x, node.props['style:x']

         it 'on change are set on a style item', ->
             doc = render
-                html: '<b n-style="renderer:Item" n-style:x="50" />'
+                html: '<b n-style="renderer:Item" style:x="50" />'

             {node, item} = doc.styles[0]

             node.props.set 'style:x', 70
-            assert.is item.x, node.props['n-style:x']
+            assert.is item.x, node.props['style:x']

         it "'null' value is not set for non-object properties", ->
             doc = render
-                html: '<b n-style="renderer:Item" n-style:x="null" />'
+                html: '<b n-style="renderer:Item" style:x="null" />'

             {node, item} = doc.styles[0]

             assert.is item.x, 0

-            node.props.set 'n-style:x', 4
-            node.props.set 'n-style:x', null
+            node.props.set 'style:x', 4
+            node.props.set 'style:x', null
             assert.is item.x, 4

     describe "'class' attribute", ->


Move turbolinks up to make client_side_validations working after ajax request

--- app/assets/javascripts/application.js.coffee
+++ app/assets/javascripts/application.js.coffee
@@ -3,12 +3,12 @@
 #= require jquery_ujs
 #= require foundation/foundation
 #= require foundation
+#= require turbolinks
+#= require nprogress
+#= require nprogress-turbolinks
 #= require rails.validations
 #= require rails.validations.simple_form
 #= require rails.validations.simple_form.fix
-#= require turbolinks
-#= require nprogress
-#= require nprogress-turbolinks
 #= require jquery.autosize
 #= require globals/_functions
 #= require tools/mapbox


Rename view to bufferFindAndReplaceView

Rename view to bufferFindAndReplaceView

--- spec/buffer/buffer-find-and-replace-view-spec.coffee
+++ spec/buffer/buffer-find-and-replace-view-spec.coffee
@@ -2,7 +2,7 @@
 RootView = require 'root-view'

 describe 'BufferFindAndReplaceView', ->
-  [editor, view] = []
+  [editor, bufferFindAndReplaceView] = []

   beforeEach ->
     window.rootView = new RootView()
@@ -11,7 +11,7 @@
     rootView.attachToDom()
     editor = rootView.getActiveView()
     pack = atom.activatePackage("find-and-replace")
-    view = pack.mainModule.bufferFindAndReplaceView
+    bufferFindAndReplaceView = pack.mainModule.bufferFindAndReplaceView

   describe "when find-and-replace:display-find is triggered", ->
     it "attaches BufferFindAndReplaceView to the root view", ->
@@ -28,38 +28,38 @@
     beforeEach ->
       editor.setCursorBufferPosition([2,0])
       editor.trigger 'find-and-replace:display-find'
-      view.findEditor.setText 'items'
+      bufferFindAndReplaceView.findEditor.setText 'items'
       $(document.activeElement).trigger 'core:confirm'

     it "selects the first match following the cursor", ->
-      expect(view.resultCounter.text()).toEqual('2 of 6')
+      expect(bufferFindAndReplaceView.resultCounter.text()).toEqual('2 of 6')
       expect(editor.getSelectedBufferRange()).toEqual [[2, 8], [2, 13]]

     it "selects the next match when the next match button is pressed", ->
       $('.find-and-replace .icon-next').click()
-      expect(view.resultCounter.text()).toEqual('3 of 6')
+      expect(bufferFindAndReplaceView.resultCounter.text()).toEqual('3 of 6')
       expect(editor.getSelectedBufferRange()).toEqual [[2, 34], [2, 39]]

     it "selects the next match when the 'find-and-replace:focus-next' event is triggered", ->
       editor.trigger('find-and-replace:find-next')
-      expect(view.resultCounter.text()).toEqual('3 of 6')
+      expect(bufferFindAndReplaceView.resultCounter.text()).toEqual('3 of 6')
       expect(editor.getSelectedBufferRange()).toEqual [[2, 34], [2, 39]]

     it "selects the previous match when the previous match button is pressed", ->
       $('.find-and-replace .icon-previous').click()
-      expect(view.resultCounter.text()).toEqual('1 of 6')
+      expect(bufferFindAndReplaceView.resultCounter.text()).toEqual('1 of 6')
       expect(editor.getSelectedBufferRange()).toEqual [[1, 27], [1, 22]]

     it "selects the previous match when the 'find-and-replace:focus-previous' event is triggered", ->
       editor.trigger('find-and-replace:find-previous')
-      expect(view.resultCounter.text()).toEqual('1 of 6')
+      expect(bufferFindAndReplaceView.resultCounter.text()).toEqual('1 of 6')
       expect(editor.getSelectedBufferRange()).toEqual [[1, 27], [1, 22]]

     it "replaces results counter with number of results found when user moves cursor outside a marker", ->
       editor.moveCursorDown()
-      expect(view.resultCounter.text()).toBe '6 found'
+      expect(bufferFindAndReplaceView.resultCounter.text()).toBe '6 found'
       editor.moveCursorUp()
-      expect(view.resultCounter.text()).toBe '2 of 6'
+      expect(bufferFindAndReplaceView.resultCounter.text()).toBe '2 of 6'

     describe "when the active editor changes", ->
       it "detaches the view when there are no more active editors", ->
@@ -70,28 +70,28 @@
     beforeEach ->
       editor.setCursorBufferPosition([2,0])
       editor.trigger 'find-and-replace:display-replace'
-      view.findEditor.setText('items')
-      view.replaceEditor.setText('cats')
+      bufferFindAndReplaceView.findEditor.setText('items')
+      bufferFindAndReplaceView.replaceEditor.setText('cats')

     describe "replace next", ->
       describe "when core:confirm is triggered", ->
         it "replaces the match after the cursor and selects the next match", ->
-          view.replaceEditor.trigger 'core:confirm'
-          expect(view.resultCounter.text()).toEqual('2 of 5')
+          bufferFindAndReplaceView.replaceEditor.trigger 'core:confirm'
+          expect(bufferFindAndReplaceView.resultCounter.text()).toEqual('2 of 5')
           expect(editor.lineForBufferRow(2)).toBe "    if (cats.length <= 1) return items;"
           expect(editor.getSelectedBufferRange()).toEqual [[2, 33], [2, 38]]

       describe "when the replace next button is pressed", ->
         it "replaces the match after the cursor and selects the next match", ->
           $('.find-and-replace .btn-next').click()
-          expect(view.resultCounter.text()).toEqual('2 of 5')
+          expect(bufferFindAndReplaceView.resultCounter.text()).toEqual('2 of 5')
           expect(editor.lineForBufferRow(2)).toBe "    if (cats.length <= 1) return items;"
           expect(editor.getSelectedBufferRange()).toEqual [[2, 33], [2, 38]]

       describe "when the 'find-and-replace:replace-next' event is triggered", ->
         it "replaces the match after the cursor and selects the next match", ->
           editor.trigger 'find-and-replace:replace-next'
-          expect(view.resultCounter.text()).toEqual('2 of 5')
+          expect(bufferFindAndReplaceView.resultCounter.text()).toEqual('2 of 5')
           expect(editor.lineForBufferRow(2)).toBe "    if (cats.length <= 1) return items;"
           expect(editor.getSelectedBufferRange()).toEqual [[2, 33], [2, 38]]

@@ -99,77 +99,77 @@
       describe "when the replace all button is pressed", ->
         it "replaces the match after the cursor and selects the next match", ->
           $('.find-and-replace .btn-all').click()
-          expect(view.resultCounter.text()).toEqual('0 found')
+          expect(bufferFindAndReplaceView.resultCounter.text()).toEqual('0 found')
           expect(editor.getText()).not.toMatch /items/
           expect(editor.getSelectedBufferRange()).toEqual [[2, 0], [2, 0]]

       describe "when the 'find-and-replace:replace-next' event is triggered", ->
         it "replaces the match after the cursor and selects the next match", ->
           editor.trigger 'find-and-replace:replace-all'
-          expect(view.resultCounter.text()).toEqual('0 found')
+          expect(bufferFindAndReplaceView.resultCounter.text()).toEqual('0 found')
           expect(editor.getText()).not.toMatch /items/
           expect(editor.getSelectedBufferRange()).toEqual [[2, 0], [2, 0]]

   describe "history", ->
     beforeEach ->
       editor.trigger 'find-and-replace:display-find'
-      view.searchModel.setPattern('one')
-      view.searchModel.setPattern('two')
-      view.searchModel.setPattern('three')
+      bufferFindAndReplaceView.searchModel.setPattern('one')
+      bufferFindAndReplaceView.searchModel.setPattern('two')
+      bufferFindAndReplaceView.searchModel.setPattern('three')

-      expect(view.searchModel.history.length).toEqual 3
-      expect(view.searchModel.historyIndex).toEqual 2
+      expect(bufferFindAndReplaceView.searchModel.history.length).toEqual 3
+      expect(bufferFindAndReplaceView.searchModel.historyIndex).toEqual 2

     it "can navigate the entire history stack", ->
-      expect(view.findEditor.getText()).toEqual 'three'
+      expect(bufferFindAndReplaceView.findEditor.getText()).toEqual 'three'

-      view.findEditor.trigger 'find-and-replace:search-previous-in-history'
-      expect(view.findEditor.getText()).toEqual 'two'
+      bufferFindAndReplaceView.findEditor.trigger 'find-and-replace:search-previous-in-history'
+      expect(bufferFindAndReplaceView.findEditor.getText()).toEqual 'two'

-      view.findEditor.trigger 'find-and-replace:search-previous-in-history'
-      expect(view.findEditor.getText()).toEqual 'one'
+      bufferFindAndReplaceView.findEditor.trigger 'find-and-replace:search-previous-in-history'
+      expect(bufferFindAndReplaceView.findEditor.getText()).toEqual 'one'

-      view.findEditor.trigger 'find-and-replace:search-previous-in-history'
-      expect(view.findEditor.getText()).toEqual 'one'
+      bufferFindAndReplaceView.findEditor.trigger 'find-and-replace:search-previous-in-history'
+      expect(bufferFindAndReplaceView.findEditor.getText()).toEqual 'one'

-      view.findEditor.trigger 'find-and-replace:search-next-in-history'
-      expect(view.findEditor.getText()).toEqual 'two'
+      bufferFindAndReplaceView.findEditor.trigger 'find-and-replace:search-next-in-history'
+      expect(bufferFindAndReplaceView.findEditor.getText()).toEqual 'two'

-      view.findEditor.trigger 'find-and-replace:search-next-in-history'
-      expect(view.findEditor.getText()).toEqual 'three'
+      bufferFindAndReplaceView.findEditor.trigger 'find-and-replace:search-next-in-history'
+      expect(bufferFindAndReplaceView.findEditor.getText()).toEqual 'three'

-      view.findEditor.trigger 'find-and-replace:search-next-in-history'
-      expect(view.findEditor.getText()).toEqual ''
+      bufferFindAndReplaceView.findEditor.trigger 'find-and-replace:search-next-in-history'
+      expect(bufferFindAndReplaceView.findEditor.getText()).toEqual ''

-      view.findEditor.trigger 'find-and-replace:search-next-in-history'
-      expect(view.findEditor.getText()).toEqual ''
+      bufferFindAndReplaceView.findEditor.trigger 'find-and-replace:search-next-in-history'
+      expect(bufferFindAndReplaceView.findEditor.getText()).toEqual ''

     it "maintains current unsearched text in the history", ->
       text = 'something I want to search for but havent yet'
-      view.findEditor.setText(text)
+      bufferFindAndReplaceView.findEditor.setText(text)

-      view.findEditor.trigger 'find-and-replace:search-previous-in-history'
-      expect(view.findEditor.getText()).toEqual 'two'
+      bufferFindAndReplaceView.findEditor.trigger 'find-and-replace:search-previous-in-history'
+      expect(bufferFindAndReplaceView.findEditor.getText()).toEqual 'two'

-      view.findEditor.trigger 'find-and-replace:search-next-in-history'
-      expect(view.findEditor.getText()).toEqual 'three'
+      bufferFindAndReplaceView.findEditor.trigger 'find-and-replace:search-next-in-history'
+      expect(bufferFindAndReplaceView.findEditor.getText()).toEqual 'three'

-      view.findEditor.trigger 'find-and-replace:search-next-in-history'
-      expect(view.findEditor.getText()).toEqual text
+      bufferFindAndReplaceView.findEditor.trigger 'find-and-replace:search-next-in-history'
+      expect(bufferFindAndReplaceView.findEditor.getText()).toEqual text

-      view.findEditor.trigger 'find-and-replace:search-next-in-history'
-      expect(view.findEditor.getText()).toEqual text
+      bufferFindAndReplaceView.findEditor.trigger 'find-and-replace:search-next-in-history'
+      expect(bufferFindAndReplaceView.findEditor.getText()).toEqual text

-      view.findEditor.trigger 'find-and-replace:search-previous-in-history'
-      expect(view.findEditor.getText()).toEqual 'three'
+      bufferFindAndReplaceView.findEditor.trigger 'find-and-replace:search-previous-in-history'
+      expect(bufferFindAndReplaceView.findEditor.getText()).toEqual 'three'

     it "adds confirmed patterns to the history", ->
-      view.findEditor.setText("cool stuff")
-      view.findEditor.trigger 'core:confirm'
+      bufferFindAndReplaceView.findEditor.setText("cool stuff")
+      bufferFindAndReplaceView.findEditor.trigger 'core:confirm'

-      view.findEditor.setText("cooler stuff")
-      view.findEditor.trigger 'find-and-replace:search-previous-in-history'
-      expect(view.findEditor.getText()).toEqual 'cool stuff'
+      bufferFindAndReplaceView.findEditor.setText("cooler stuff")
+      bufferFindAndReplaceView.findEditor.trigger 'find-and-replace:search-previous-in-history'
+      expect(bufferFindAndReplaceView.findEditor.getText()).toEqual 'cool stuff'

-      view.findEditor.trigger 'find-and-replace:search-previous-in-history'
-      expect(view.findEditor.getText()).toEqual 'three'
+      bufferFindAndReplaceView.findEditor.trigger 'find-and-replace:search-previous-in-history'
+      expect(bufferFindAndReplaceView.findEditor.getText()).toEqual 'three'


update login text

update login text

--- app/assets/javascripts/components/login.cjsx
+++ app/assets/javascripts/components/login.cjsx
@@ -53,7 +53,7 @@

-      { label || "Log In" }
+      { "Log In" }

         { links }


Move target selection specs to up.change

--- spec_app/spec/javascripts/up/fragment_spec.js.coffee
+++ spec_app/spec/javascripts/up/fragment_spec.js.coffee
@@ -287,7 +287,695 @@

         it 'derives a selector from an element given as first argument'

-        it 'uses a default target for the layer that is being updated if nothing else is specified'
+        it 'uses a default target for the layer that is being updated if no other option suggests a target'
+
+        it "ignores an element that matches the selector but also matches .up-destroying", (done) ->
+          html = '
text
'
+          $fixture('.foo-bar.up-destroying')
+          promise = up.change('.foo-bar', document: html)
+
+          u.task ->
+            promiseState(promise).then (result) =>
+              expect(result.state).toEqual('rejected')
+              expect(result.value).toMatch(/Could not match target/i)
+              done()
+
+        it "ignores an element that matches the selector but also has a parent matching .up-destroying", (done) ->
+          html = '
text
'
+          $parent = $fixture('.up-destroying')
+          $child = $fixture('.foo-bar').appendTo($parent)
+          promise = up.change('.foo-bar', document: html)
+
+          u.task ->
+            promiseState(promise).then (result) =>
+              expect(result.state).toEqual('rejected')
+              expect(result.value).toMatch(/Could not match target/i)
+              done()
+
+        it 'only replaces the first element matching the selector', asyncSpec (next) ->
+          html = '
text
'
+          $fixture('.foo-bar')
+          $fixture('.foo-bar')
+          up.change('.foo-bar', document: html)
+
+          next =>
+            $elements = $('.foo-bar')
+            expect($($elements.get(0)).text()).toEqual('text')
+            expect($($elements.get(1)).text()).toEqual('')
+
+        it 'replaces the body if asked to replace the "html" selector'
+
+        it 'prepends instead of replacing when the target has a :before pseudo-selector', asyncSpec (next) ->
+          target = fixture('.target')
+          e.affix(target, '.child', text: 'old')
+          up.change('.target:before', document: """
+

+
new

+
"
+            """
+          )
+
+          next =>
+            children = target.querySelectorAll('.child')
+            expect(children.length).toBe(2)
+            expect(children[0]).toHaveText('new')
+            expect(children[1]).toHaveText('old')
+
+        it 'appends instead of replacing when the target has a :after pseudo-selector', asyncSpec (next) ->
+          target = fixture('.target')
+          e.affix(target, '.child', text: 'old')
+          up.change('.target:after', document: """
+

+
new

+

+            """
+          )
+
+          next =>
+            children = target.querySelectorAll('.child')
+            expect(children.length).toBe(2)
+            expect(children[0]).toHaveText('old')
+            expect(children[1]).toHaveText('new')
+
+        it "lets the developer choose between replacing/prepending/appending for each selector", asyncSpec (next) ->
+          fixture('.before', text: 'old-before')
+          fixture('.middle', text: 'old-middle')
+          fixture('.after', text: 'old-after')
+          up.change '.before:before, .middle, .after:after', document: """
+
new-before

+
new-middle

+
new-after

+            """
+          next =>
+            expect($('.before')).toHaveText('new-beforeold-before')
+            expect($('.middle')).toHaveText('new-middle')
+            expect($('.after')).toHaveText('old-afternew-after')
+
+        it 'understands non-standard CSS selector extensions such as :has(...)', asyncSpec (next) ->
+          $first = $fixture('.boxx#first')
+          $firstChild = $('old first').appendTo($first)
+          $second = $fixture('.boxx#second')
+          $secondChild = $('old second').appendTo($second)
+
+          promise = up.replace('.boxx:has(.first-child)', '/path')
+
+          next =>
+            @respondWith """
+

+                new first
+

+              """
+            next.await(promise)
+
+          next =>
+            expect($('#first span')).toHaveText('new first')
+            expect($('#second span')).toHaveText('old second')
+
+        it 'replaces multiple selectors separated with a comma', asyncSpec (next) ->
+          fixture('.before', text: 'old-before')
+          fixture('.middle', text: 'old-middle')
+          fixture('.after', text: 'old-after')
+
+          up.change '.middle, .after', document: """
+
new-before

+
new-middle

+
new-after

+            """
+
+          next =>
+            expect($('.before')).toHaveText('old-before')
+            expect($('.middle')).toHaveText('new-middle')
+            expect($('.after')).toHaveText('new-after')
+
+        describe 'merging of nested selectors', ->
+
+          it 'replaces a single fragment if a selector contains a subsequent selector in the current page', asyncSpec (next) ->
+            $outer = $fixture('.outer').text('old outer text')
+            $inner = $outer.affix('.inner').text('old inner text')
+
+            replacePromise = up.change('.outer, .inner', url: '/path')
+
+            next =>
+              expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer')
+
+              @respondWith """
+

+                  new outer text
+

+                    new inner text
+

+

+                """
+
+            next =>
+              expect($('.outer')).toBeAttached()
+              expect($('.outer').text()).toContain('new outer text')
+              expect($('.inner')).toBeAttached()
+              expect($('.inner').text()).toContain('new inner text')
+
+            next.await =>
+              promise = promiseState(replacePromise)
+              promise.then (result) => expect(result.state).toEqual('fulfilled')
+
+          it 'does not merge selectors if a selector contains a subsequent selector, but prepends instead of replacing', asyncSpec (next) ->
+            $outer = $fixture('.outer').text('old outer text')
+            $inner = $outer.affix('.inner').text('old inner text')
+
+            replacePromise = up.change('.outer:before, .inner', url: '/path')
+
+            next =>
+              expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer:before, .inner')
+
+              @respondWith """
+

+                  new outer text
+

+                    new inner text
+

+

+                """
+
+            next =>
+              expect($('.outer')).toBeAttached()
+              expect($('.outer').text()).toContain('new outer text')
+              expect($('.outer').text()).toContain('old outer text')
+              expect($('.inner')).toBeAttached()
+              expect($('.inner').text()).toContain('new inner text')
+
+            next.await =>
+              promise = promiseState(replacePromise)
+              promise.then (result) => expect(result.state).toEqual('fulfilled')
+
+          it 'does not merge selectors if a selector contains a subsequent selector, but appends instead of replacing', asyncSpec (next) ->
+            $outer = $fixture('.outer').text('old outer text')
+            $inner = $outer.affix('.inner').text('old inner text')
+
+            replacePromise = up.change('.outer:after, .inner', url: '/path')
+
+            next =>
+              expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer:after, .inner')
+
+              @respondWith """
+

+                  new outer text
+

+                    new inner text
+

+

+                """
+
+            next =>
+              expect($('.outer')).toBeAttached()
+              expect($('.outer').text()).toContain('old outer text')
+              expect($('.outer').text()).toContain('new outer text')
+              expect($('.inner')).toBeAttached()
+              expect($('.inner').text()).toContain('new inner text')
+
+            next.await =>
+              promise = promiseState(replacePromise)
+              promise.then (result) => expect(result.state).toEqual('fulfilled')
+
+          it 'does not lose selector pseudo-classes when merging selectors (bugfix)', asyncSpec (next) ->
+            $outer = $fixture('.outer').text('old outer text')
+            $inner = $outer.affix('.inner').text('old inner text')
+
+            replacePromise = up.change('.outer:after, .inner', url: '/path')
+
+            next =>
+              expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer:after, .inner')
+
+          it 'replaces a single fragment if a selector contains a previous selector in the current page', asyncSpec (next) ->
+            $outer = $fixture('.outer').text('old outer text')
+            $inner = $outer.affix('.inner').text('old inner text')
+
+            replacePromise = up.change('.outer, .inner', url: '/path')
+
+            next =>
+              expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer')
+
+              @respondWith """
+

+                  new outer text
+

+                    new inner text
+

+

+                """
+
+            next =>
+              expect($('.outer')).toBeAttached()
+              expect($('.outer').text()).toContain('new outer text')
+              expect($('.inner')).toBeAttached()
+              expect($('.inner').text()).toContain('new inner text')
+
+            next.await =>
+              promise = promiseState(replacePromise)
+              promise.then (result) => expect(result.state).toEqual('fulfilled')
+
+          it 'does not lose a { reveal: true } option if the first selector was merged into a subsequent selector', asyncSpec (next) ->
+            revealStub = up.viewport.knife.mock('reveal').and.returnValue(Promise.resolve())
+
+            $outer = $fixture('.outer').text('old outer text')
+            $inner = $outer.affix('.inner').text('old inner text')
+
+            up.change('.inner, .outer', url: '/path', reveal: true)
+
+            next =>
+              expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer')
+
+              @respondWith """
+

+                  new outer text
+

+                    new inner text
+

+

+                """
+
+            next =>
+              expect($('.outer')).toBeAttached()
+              expect($('.outer').text()).toContain('new outer text')
+              expect($('.inner')).toBeAttached()
+              expect($('.inner').text()).toContain('new inner text')
+
+              expect(revealStub).toHaveBeenCalled()
+
+          it 'does not lose a { reveal: string } option if the first selector was merged into a subsequent selector', asyncSpec (next) ->
+            revealStub = up.viewport.knife.mock('reveal').and.returnValue(Promise.resolve())
+
+            $outer = $fixture('.outer').text('old outer text')
+            $inner = $outer.affix('.inner').text('old inner text')
+
+            up.change('.inner, .outer', url: '/path', reveal: '.revealee')
+
+            next =>
+              expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer')
+
+              @respondWith """
+

+                  new outer text
+

+                    new inner text
+

+                      revealee text
+

+

+

+                """
+
+            next =>
+              expect($('.outer')).toBeAttached()
+              expect($('.outer').text()).toContain('new outer text')
+              expect($('.inner')).toBeAttached()
+              expect($('.inner').text()).toContain('new inner text')
+
+              expect(revealStub).toHaveBeenCalled()
+              revealArg = revealStub.calls.mostRecent().args[0]
+              expect(revealArg).toMatchSelector('.revealee')
+
+          it 'replaces a single fragment if the nesting differs in current page and response', asyncSpec (next) ->
+            $outer = $fixture('.outer').text('old outer text')
+            $inner = $outer.affix('.inner').text('old inner text')
+
+            replacePromise = up.change('.outer, .inner', url: '/path')
+
+            next =>
+              @respondWith """
+

+                  new inner text
+

+                    new outer text
+

+

+                """
+
+            next =>
+              expect($('.outer')).toBeAttached()
+              expect($('.outer').text()).toContain('new outer text')
+              expect($('.inner')).not.toBeAttached()
+
+            next.await =>
+              promise = promiseState(replacePromise)
+              promise.then (result) => expect(result.state).toEqual('fulfilled')
+
+          it 'does not crash if two selectors that are siblings in the current page are nested in the response', asyncSpec (next) ->
+            $outer = $fixture('.one').text('old one text')
+            $inner = $fixture('.two').text('old two text')
+
+            replacePromise = up.change('.one, .two', url: '/path')
+
+            next =>
+              @respondWith """
+

+                  new one text
+

+                    new two text
+

+

+                """
+
+            next =>
+              expect($('.one')).toBeAttached()
+              expect($('.one').text()).toContain('new one text')
+              expect($('.two')).toBeAttached()
+              expect($('.two').text()).toContain('new two text')
+
+            next.await =>
+              promise = promiseState(replacePromise)
+              promise.then (result) => expect(result.state).toEqual('fulfilled')
+
+          it 'does not crash if selectors that siblings in the current page are inversely nested in the response', asyncSpec (next) ->
+            $outer = $fixture('.one').text('old one text')
+            $inner = $fixture('.two').text('old two text')
+
+            replacePromise = up.change('.one, .two', url: '/path')
+
+            next =>
+              @respondWith """
+

+                  new two text
+

+                    new one text
+

+

+                """
+
+            next =>
+              expect($('.one')).toBeAttached()
+              expect($('.one').text()).toContain('new one text')
+              expect($('.two')).toBeAttached()
+              expect($('.two').text()).toContain('new two text')
+
+            next.await =>
+              promise = promiseState(replacePromise)
+              promise.then (result) => expect(result.state).toEqual('fulfilled')
+
+          it 'updates the first selector if the same element is targeted twice in a single replacement', asyncSpec (next) ->
+            $one = $fixture('.one.alias').text('old one text')
+
+            replacePromise = up.change('.one, .alias', url: '/path')
+
+            next =>
+              expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.one')
+
+              @respondWith """
+

+                  new one text
+

+                """
+
+            next =>
+              expect($('.one')).toBeAttached()
+              expect($('.one').text()).toContain('new one text')
+
+            next.await =>
+              promise = promiseState(replacePromise)
+              promise.then (result) => expect(result.state).toEqual('fulfilled')
+
+          it 'updates the first selector if the same element is prepended or appended twice in a single replacement', asyncSpec (next) ->
+            $one = $fixture('.one').text('old one text')
+
+            replacePromise = up.change('.one:before, .one:after', url: '/path')
+
+            next =>
+              expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.one:before')
+
+              @respondWith """
+

+                  new one text
+

+                """
+
+            next =>
+              expect($('.one')).toBeAttached()
+              expect($('.one').text()).toMatchText('new one text old one text')
+
+            next.await =>
+              promise = promiseState(replacePromise)
+              promise.then (result) => expect(result.state).toEqual('fulfilled')
+
+          it "updates the first selector if the same element is prepended, replaced and appended in a single replacement", asyncSpec (next) ->
+            $elem = $fixture('.elem.alias1.alias2').text("old text")
+
+            replacePromise = up.change('.elem:before, .alias1, .alias2:after', url: '/path')
+
+            next =>
+              expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.elem:before')
+
+              @respondWith """
+

+                  new text
+

+                """
+
+            next =>
+              expect('.elem').toBeAttached()
+              expect($('.elem').text()).toMatchText('new text old text')
+
+            next.await =>
+              promise = promiseState(replacePromise)
+              promise.then (result) => expect(result.state).toEqual('fulfilled')
+
+        describe 'when selectors are missing on the page before the request was made', ->
+
+          beforeEach ->
+            # In helpers/protect_jasmine_runner wie have configured .default-fallback
+            # as a default target for all layers.
+            up.layer.config.all.targets = []
+
+          it 'tries selectors from options.fallback before making a request', asyncSpec (next) ->
+            $fixture('.box').text('old box')
+            up.change('.unknown', url: '/path', fallback: '.box')
+
+            next => @respondWith '
new box
'
+            next => expect('.box').toHaveText('new box')
+
+          it 'rejects the promise if all alternatives are exhausted', (done) ->
+            promise = up.change('.unknown', url: '/path', fallback: '.more-unknown')
+
+            u.task ->
+              promiseState(promise).then (result) ->
+                expect(result.state).toEqual('rejected')
+                expect(result.value).toBeError(/Could not find target in current page/i)
+                done()
+
+          it 'considers a union selector to be missing if one of its selector-atoms are missing', asyncSpec (next) ->
+            $fixture('.target').text('old target')
+            $fixture('.fallback').text('old fallback')
+            up.change('.target, .unknown', url: '/path', fallback: '.fallback')
+
+            next =>
+              @respondWith """
+
new target

+
new fallback

+              """
+
+            next =>
+              expect('.target').toHaveText('old target')
+              expect('.fallback').toHaveText('new fallback')
+
+          it "tries the layer's default target if options.fallback is missing", asyncSpec (next) ->
+            up.layer.config.all.targets = ['.existing']
+            $fixture('.existing').text('old existing')
+            up.change('.unknown', url: '/path')
+            next => @respondWith '
new existing
'
+            next => expect('.existing').toHaveText('new existing')
+
+          it "does not try the layer's default targets and rejects the promise wieht { fallback: false }", (done) ->
+            up.layer.config.all.targets = ['.existing']
+            $fixture('.existing').text('old existing')
+            up.change('.unknown', url: '/path', fallback: false).catch (e) ->
+              expect(e).toBeError(/Could not find target in current page/i)
+              done()
+
+        describe 'when selectors are missing on the page after the request was made', ->
+
+          beforeEach ->
+            up.layer.config.all.targets = []
+
+          it 'tries the selector in options.fallback before swapping elements', asyncSpec (next) ->
+            $target = $fixture('.target').text('old target')
+            $fallback = $fixture('.fallback').text('old fallback')
+            up.change('.target', url: '/path', fallback: '.fallback')
+            $target.remove()
+
+            next =>
+              @respondWith """
+
new target

+
new fallback

+              """
+
+            next =>
+              expect('.fallback').toHaveText('new fallback')
+
+          it 'rejects the promise if all alternatives are exhausted', (done) ->
+            $target = $fixture('.target').text('old target')
+            $fallback = $fixture('.fallback').text('old fallback')
+            promise = up.change('.target', url: '/path', fallback: '.fallback')
+
+            u.task =>
+              $target.remove()
+              $fallback.remove()
+
+              @respondWith """
+
new target

+
new fallback

+              """
+
+              u.task =>
+                promiseState(promise).then (result) ->
+                  expect(result.state).toEqual('rejected')
+                  expect(result.value).toBeError(/Could not match target in current page and response/i)
+                  done()
+
+          it 'considers a union selector to be missing if one of its selector-atoms are missing', asyncSpec (next) ->
+            $target = $fixture('.target').text('old target')
+            $target2 = $fixture('.target2').text('old target2')
+            $fallback = $fixture('.fallback').text('old fallback')
+            up.change('.target, .target2', url: '/path', fallback: '.fallback')
+            $target2.remove()
+
+            next =>
+              @respondWith """
+
new target

+
new target2

+
new fallback

+              """
+            next =>
+              expect('.target').toHaveText('old target')
+              expect('.fallback').toHaveText('new fallback')
+
+          it "tries the layer's default targets if options.fallback is missing", asyncSpec (next) ->
+            up.layer.config.all.targets = ['.fallback']
+            $target = $fixture('.target').text('old target')
+            $fallback = $fixture('.fallback').text('old fallback')
+            up.change('.target', url: '/path')
+            $target.remove()
+
+            next =>
+              @respondWith """
+
new target

+
new fallback

+              """
+
+            next =>
+              expect('.fallback').toHaveText('new fallback')
+
+          it "does not try the layer's default targets and rejects the promise if options.fallback is false", (done) ->
+            up.layer.config.all.fallbacks = ['.fallback']
+            $target = $fixture('.target').text('old target')
+            $fallback = $fixture('.fallback').text('old fallback')
+            promise = up.change('.target', url: '/path', fallback: false)
+
+            u.task =>
+              $target.remove()
+
+              @respondWith """
+
new target

+
new fallback

+              """
+
+              promise.catch (e) ->
+                expect(e).toBeError(/Could not match target/i)
+                done()
+
+        describe 'when selectors are missing in the response', ->
+
+          beforeEach ->
+            up.layer.config.all.targets = []
+
+          it "tries the selector in options.fallback before swapping elements", asyncSpec (next) ->
+            $target = $fixture('.target').text('old target')
+            $fallback = $fixture('.fallback').text('old fallback')
+            up.change('.target', url: '/path', fallback: '.fallback')
+
+            next =>
+              @respondWith """
+
new fallback

+              """
+
+            next =>
+              expect('.target').toHaveText('old target')
+              expect('.fallback').toHaveText('new fallback')
+
+          describe 'if all alternatives are exhausted', ->
+
+            it 'rejects the promise', (done) ->
+              $target = $fixture('.target').text('old target')
+              $fallback = $fixture('.fallback').text('old fallback')
+              promise = up.change('.target', url: '/path', fallback: '.fallback')
+
+              u.task =>
+                @respondWith '
new unexpected
'
+
+              promise.catch (e) ->
+                expect(e).toBeError(/Could not match target/i)
+                done()
+
+            it 'shows a link to open the unexpected response', (done) ->
+              $target = $fixture('.target').text('old target')
+              $fallback = $fixture('.fallback').text('old fallback')
+              promise = up.change('.target', url: '/path', fallback: '.fallback')
+              loadPage = spyOn(up.browser, 'loadPage')
+
+              u.task =>
+                @respondWith '
new unexpected
'
+
+              promise.catch (e) ->
+                $toast = $('.up-toast')
+                expect($toast).toBeAttached()
+                $inspectLink = $toast.find(".up-toast-action:contains('Open response')")
+                expect($inspectLink).toBeAttached()
+                expect(loadPage).not.toHaveBeenCalled()
+
+                Trigger.clickSequence($inspectLink)
+
+                u.task =>
+                  expect(loadPage).toHaveBeenCalledWith('/path', {})
+                  done()
+
+          it 'considers a union selector to be missing if one of its selector-atoms are missing', asyncSpec (next) ->
+            $target = $fixture('.target').text('old target')
+            $target2 = $fixture('.target2').text('old target2')
+            $fallback = $fixture('.fallback').text('old fallback')
+            up.change('.target, .target2', url: '/path', fallback: '.fallback')
+
+            next =>
+              @respondWith """
+
new target

+
new fallback

+              """
+
+            next =>
+              expect('.target').toHaveText('old target')
+              expect('.target2').toHaveText('old target2')
+              expect('.fallback').toHaveText('new fallback')
+
+          it "tries the layer's default targets if options.fallback is missing", asyncSpec (next) ->
+            up.layer.config.all.targets = ['.fallback']
+            $target = $fixture('.target').text('old target')
+            $fallback = $fixture('.fallback').text('old fallback')
+            up.change('.target', url: '/path')
+
+            next =>
+              @respondWith '
new fallback
'
+
+            next =>
+              expect('.target').toHaveText('old target')
+              expect('.fallback').toHaveText('new fallback')
+
+          it "does not try the layer's default targets and rejects the promise with { fallback: false }", (done) ->
+            up.layer.config.all.targets = ['.fallback']
+            $target = $fixture('.target').text('old target')
+            $fallback = $fixture('.fallback').text('old fallback')
+            promise = up.change('.target', url: '/path', fallback: false)
+
+            u.task =>
+              @respondWith '
new fallback
'
+
+            promise.catch (e) ->
+              expect(e).toBeError(/Could not match target/i)
+              done()

       describe 'choice of layer', ->

@@ -1277,7 +1965,6 @@

       describe 'destruction of old element', ->

-        # up.extract
         it 'emits an up:fragment:destroyed event on the former parent element after the element has been removed from the DOM', (done) ->
           $parent = $fixture('.parent')
           $element = $parent.affix('.element.v1').text('v1')
@@ -1882,709 +2569,21 @@
             next =>
               expect(constructorSpy.calls.count()).toBe(1)

-
-
-    ##############################################################################
-    ##############################################################################
-    ##############################################################################
-    ##############################################################################
-    ##############################################################################
-
     describe 'up.replace()', ->

-      describeCapability 'canPushState', ->
-
-        describe 'selector processing', ->
-
-          it 'replaces multiple selectors separated with a comma', asyncSpec (next) ->
-            promise = up.replace('.middle, .after', '/path')
-            next =>
-              @respond()
-              next.await(promise)
-            next =>
-              expect($('.before')).toHaveText('old-before')
-              expect($('.middle')).toHaveText('new-middle')
-              expect($('.after')).toHaveText('new-after')
-
-          describe 'nested selector merging', ->
-
-            it 'replaces a single fragment if a selector contains a subsequent selector in the current page', asyncSpec (next) ->
-              $outer = $fixture('.outer').text('old outer text')
-              $inner = $outer.affix('.inner').text('old inner text')
-
-              replacePromise = up.replace('.outer, .inner', '/path')
-
-              next =>
-                expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer')
-
-                @respondWith """
-

-                    new outer text
-

-                      new inner text
-

-

-                  """
-
-              next =>
-                expect($('.outer')).toBeAttached()
-                expect($('.outer').text()).toContain('new outer text')
-                expect($('.inner')).toBeAttached()
-                expect($('.inner').text()).toContain('new inner text')
-
-              next.await =>
-                promise = promiseState(replacePromise)
-                promise.then (result) => expect(result.state).toEqual('fulfilled')
-
-            it 'does not merge selectors if a selector contains a subsequent selector, but prepends instead of replacing', asyncSpec (next) ->
-              $outer = $fixture('.outer').text('old outer text')
-              $inner = $outer.affix('.inner').text('old inner text')
-
-              replacePromise = up.replace('.outer:before, .inner', '/path')
-
-              next =>
-                expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer:before, .inner')
-
-                @respondWith """
-

-                    new outer text
-

-                      new inner text
-

-

-                  """
-
-              next =>
-                expect($('.outer')).toBeAttached()
-                expect($('.outer').text()).toContain('new outer text')
-                expect($('.outer').text()).toContain('old outer text')
-                expect($('.inner')).toBeAttached()
-                expect($('.inner').text()).toContain('new inner text')
-
-              next.await =>
-                promise = promiseState(replacePromise)
-                promise.then (result) => expect(result.state).toEqual('fulfilled')
-
-            it 'does not merge selectors if a selector contains a subsequent selector, but appends instead of replacing', asyncSpec (next) ->
-              $outer = $fixture('.outer').text('old outer text')
-              $inner = $outer.affix('.inner').text('old inner text')
-
-              replacePromise = up.replace('.outer:after, .inner', '/path')
-
-              next =>
-                expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer:after, .inner')
-
-                @respondWith """
-

-                    new outer text
-

-                      new inner text
-

-

-                  """
-
-              next =>
-                expect($('.outer')).toBeAttached()
-                expect($('.outer').text()).toContain('old outer text')
-                expect($('.outer').text()).toContain('new outer text')
-                expect($('.inner')).toBeAttached()
-                expect($('.inner').text()).toContain('new inner text')
-
-              next.await =>
-                promise = promiseState(replacePromise)
-                promise.then (result) => expect(result.state).toEqual('fulfilled')
-
-            it 'does not lose selector pseudo-classes when merging selectors (bugfix)', asyncSpec (next) ->
-              $outer = $fixture('.outer').text('old outer text')
-              $inner = $outer.affix('.inner').text('old inner text')
-
-              replacePromise = up.replace('.outer:after, .inner', '/path')
-
-              next =>
-                expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer:after, .inner')
-
-            it 'replaces a single fragment if a selector contains a previous selector in the current page', asyncSpec (next) ->
-              $outer = $fixture('.outer').text('old outer text')
-              $inner = $outer.affix('.inner').text('old inner text')
-
-              replacePromise = up.replace('.outer, .inner', '/path')
-
-              next =>
-                expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer')
-
-                @respondWith """
-

-                    new outer text
-

-                      new inner text
-

-

-                  """
-
-              next =>
-                expect($('.outer')).toBeAttached()
-                expect($('.outer').text()).toContain('new outer text')
-                expect($('.inner')).toBeAttached()
-                expect($('.inner').text()).toContain('new inner text')
-
-              next.await =>
-                promise = promiseState(replacePromise)
-                promise.then (result) => expect(result.state).toEqual('fulfilled')
-
-            it 'does not lose a { reveal: true } option if the first selector was merged into a subsequent selector', asyncSpec (next) ->
-              revealStub = up.viewport.knife.mock('reveal').and.returnValue(Promise.resolve())
-
-              $outer = $fixture('.outer').text('old outer text')
-              $inner = $outer.affix('.inner').text('old inner text')
-
-              up.replace('.inner, .outer', '/path', reveal: true)
-
-              next =>
-                expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer')
-
-                @respondWith """
-

-                    new outer text
-

-                      new inner text
-

-

-                  """
-
-              next =>
-                expect($('.outer')).toBeAttached()
-                expect($('.outer').text()).toContain('new outer text')
-                expect($('.inner')).toBeAttached()
-                expect($('.inner').text()).toContain('new inner text')
-
-                expect(revealStub).toHaveBeenCalled()
-
-
-            it 'does not lose a { reveal: string } option if the first selector was merged into a subsequent selector', asyncSpec (next) ->
-              revealStub = up.viewport.knife.mock('reveal').and.returnValue(Promise.resolve())
-
-              $outer = $fixture('.outer').text('old outer text')
-              $inner = $outer.affix('.inner').text('old inner text')
-
-              up.replace('.inner, .outer', '/path', reveal: '.revealee')
-
-              next =>
-                expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.outer')
-
-                @respondWith """
-

-                    new outer text
-

-                      new inner text
-

-                        revealee text
-

-

-

-                  """
-
-              next =>
-                expect($('.outer')).toBeAttached()
-                expect($('.outer').text()).toContain('new outer text')
-                expect($('.inner')).toBeAttached()
-                expect($('.inner').text()).toContain('new inner text')
-
-                expect(revealStub).toHaveBeenCalled()
-                revealArg = revealStub.calls.mostRecent().args[0]
-                expect(revealArg).toMatchSelector('.revealee')
-
-
-            it 'replaces a single fragment if the nesting differs in current page and response', asyncSpec (next) ->
-              $outer = $fixture('.outer').text('old outer text')
-              $inner = $outer.affix('.inner').text('old inner text')
-
-              replacePromise = up.replace('.outer, .inner', '/path')
-
-              next =>
-                @respondWith """
-

-                    new inner text
-

-                      new outer text
-

-

-                  """
-
-              next =>
-                expect($('.outer')).toBeAttached()
-                expect($('.outer').text()).toContain('new outer text')
-                expect($('.inner')).not.toBeAttached()
-
-              next.await =>
-                promise = promiseState(replacePromise)
-                promise.then (result) => expect(result.state).toEqual('fulfilled')
-
-            it 'does not crash if two selectors that are siblings in the current page are nested in the response', asyncSpec (next) ->
-              $outer = $fixture('.one').text('old one text')
-              $inner = $fixture('.two').text('old two text')
-
-              replacePromise = up.replace('.one, .two', '/path')
-
-              next =>
-                @respondWith """
-

-                    new one text
-

-                      new two text
-

-

-                  """
-
-              next =>
-                expect($('.one')).toBeAttached()
-                expect($('.one').text()).toContain('new one text')
-                expect($('.two')).toBeAttached()
-                expect($('.two').text()).toContain('new two text')
-
-              next.await =>
-                promise = promiseState(replacePromise)
-                promise.then (result) => expect(result.state).toEqual('fulfilled')
-
-            it 'does not crash if selectors that siblings in the current page are inversely nested in the response', asyncSpec (next) ->
-              $outer = $fixture('.one').text('old one text')
-              $inner = $fixture('.two').text('old two text')
-
-              replacePromise = up.replace('.one, .two', '/path')
-
-              next =>
-                @respondWith """
-

-                    new two text
-

-                      new one text
-

-

-                  """
-
-              next =>
-                expect($('.one')).toBeAttached()
-                expect($('.one').text()).toContain('new one text')
-                expect($('.two')).toBeAttached()
-                expect($('.two').text()).toContain('new two text')
-
-              next.await =>
-                promise = promiseState(replacePromise)
-                promise.then (result) => expect(result.state).toEqual('fulfilled')
-
-            it 'updates the first selector if the same element is targeted twice in a single replacement', asyncSpec (next) ->
-              $one = $fixture('.one.alias').text('old one text')
-
-              replacePromise = up.replace('.one, .alias', '/path')
-
-              next =>
-                expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.one')
-
-                @respondWith """
-

-                    new one text
-

-                  """
-
-              next =>
-                expect($('.one')).toBeAttached()
-                expect($('.one').text()).toContain('new one text')
-
-              next.await =>
-                promise = promiseState(replacePromise)
-                promise.then (result) => expect(result.state).toEqual('fulfilled')
-
-            it 'updates the first selector if the same element is prepended or appended twice in a single replacement', asyncSpec (next) ->
-              $one = $fixture('.one').text('old one text')
-
-              replacePromise = up.replace('.one:before, .one:after', '/path')
-
-              next =>
-                expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.one:before')
-
-                @respondWith """
-

-                    new one text
-

-                  """
-
-              next =>
-                expect($('.one')).toBeAttached()
-                expect($('.one').text()).toMatchText('new one text old one text')
-
-              next.await =>
-                promise = promiseState(replacePromise)
-                promise.then (result) => expect(result.state).toEqual('fulfilled')
-
-            it "updates the first selector if the same element is prepended, replaced and appended in a single replacement", asyncSpec (next) ->
-              $elem = $fixture('.elem.alias1.alias2').text("old text")
-
-              replacePromise = up.replace('.elem:before, .alias1, .alias2:after', '/path')
-
-              next =>
-                expect(@lastRequest().requestHeaders['X-Up-Target']).toEqual('.elem:before')
-
-                @respondWith """
-

-                    new text
-

-                  """
-
-              next =>
-                expect('.elem').toBeAttached()
-                expect($('.elem').text()).toMatchText('new text old text')
-
-              next.await =>
-                promise = promiseState(replacePromise)
-                promise.then (result) => expect(result.state).toEqual('fulfilled')
-
-          it 'replaces the body if asked to replace the "html" selector'
-
-          it 'prepends instead of replacing when the target has a :before pseudo-selector', asyncSpec (next) ->
-            promise = up.replace('.middle:before', '/path')
-            next =>
-              @respond()
-              next.await(promise)
-            next =>
-              expect($('.before')).toHaveText('old-before')
-              expect($('.middle')).toHaveText('new-middleold-middle')
-              expect($('.after')).toHaveText('old-after')
-
-          it 'appends instead of replacing when the target has a :after pseudo-selector', asyncSpec (next) ->
-            promise = up.replace('.middle:after', '/path')
-            next =>
-              @respond()
-              next.await(promise)
-            next =>
-              expect($('.before')).toHaveText('old-before')
-              expect($('.middle')).toHaveText('old-middlenew-middle')
-              expect($('.after')).toHaveText('old-after')
-
-          it "lets the developer choose between replacing/prepending/appending for each selector", asyncSpec (next) ->
-            promise = up.replace('.before:before, .middle, .after:after', '/path')
-            next =>
-              @respond()
-              next.await(promise)
-            next =>
-              expect($('.before')).toHaveText('new-beforeold-before')
-              expect($('.middle')).toHaveText('new-middle')
-              expect($('.after')).toHaveText('old-afternew-after')
-
-          it 'understands non-standard CSS selector extensions such as :has(...)', asyncSpec (next) ->
-            $first = $fixture('.boxx#first')
-            $firstChild = $('old first').appendTo($first)
-            $second = $fixture('.boxx#second')
-            $secondChild = $('old second').appendTo($second)
-
-            promise = up.replace('.boxx:has(.first-child)', '/path')
-
-            next =>
-              @respondWith """
-

-                  new first
-

-                """
-              next.await(promise)
-
-            next =>
-              expect($('#first span')).toHaveText('new first')
-              expect($('#second span')).toHaveText('old second')
-
-          describe 'when selectors are missing on the page before the request was made', ->
-
-            beforeEach ->
-              up.layer.config.all.targets = []
-
-            it 'tries selectors from options.fallback before making a request', asyncSpec (next) ->
-              $fixture('.box').text('old box')
-              up.replace('.unknown', '/path', fallback: '.box')
-
-              next => @respondWith '
new box
'
-              next => expect('.box').toHaveText('new box')
-
-            it 'rejects the promise if all alternatives are exhausted', (done) ->
-              promise = up.replace('.unknown', '/path', fallback: '.more-unknown')
-
-              u.task ->
-                promiseState(promise).then (result) ->
-                  expect(result.state).toEqual('rejected')
-                  expect(result.value).toBeError(/Could not find target in current page/i)
-                  done()
-
-            it 'considers a union selector to be missing if one of its selector-atoms are missing', asyncSpec (next) ->
-              $fixture('.target').text('old target')
-              $fixture('.fallback').text('old fallback')
-              up.replace('.target, .unknown', '/path', fallback: '.fallback')
-
-              next =>
-                @respondWith """
-
new target

-
new fallback

-                """
-
-              next =>
-                expect('.target').toHaveText('old target')
-                expect('.fallback').toHaveText('new fallback')
-
-            it "tries the layer's default target if options.fallback is missing", asyncSpec (next) ->
-              up.layer.config.all.targets = ['.existing']
-              $fixture('.existing').text('old existing')
-              up.replace('.unknown', '/path')
-              next => @respondWith '
new existing
'
-              next => expect('.existing').toHaveText('new existing')
-
-            it "does not try the layer's default targets and rejects the promise if options.fallback is false", (done) ->
-              up.layer.config.all.targets = ['.existing']
-              $fixture('.existing').text('old existing')
-              up.replace('.unknown', '/path', fallback: false).catch (e) ->
-                expect(e).toBeError(/Could not find target in current page/i)
-                done()
-
-          describe 'when selectors are missing on the page after the request was made', ->
-
-            beforeEach ->
-              up.layer.config.all.targets = []
-
-            it 'tries the selector in options.fallback before swapping elements', asyncSpec (next) ->
-              $target = $fixture('.target').text('old target')
-              $fallback = $fixture('.fallback').text('old fallback')
-              up.replace('.target', '/path', fallback: '.fallback')
-              $target.remove()
-
-              next =>
-                @respondWith """
-
new target

-
new fallback

-                """
-
-              next =>
-                expect('.fallback').toHaveText('new fallback')
-
-            it 'rejects the promise if all alternatives are exhausted', (done) ->
-              $target = $fixture('.target').text('old target')
-              $fallback = $fixture('.fallback').text('old fallback')
-              promise = up.replace('.target', '/path', fallback: '.fallback')
-
-              u.task =>
-                $target.remove()
-                $fallback.remove()
-
-                @respondWith """
-
new target

-
new fallback

-                """
-
-                u.task =>
-                  promiseState(promise).then (result) ->
-                    expect(result.state).toEqual('rejected')
-                    expect(result.value).toBeError(/Could not match target in current page and response/i)
-                    done()
-
-            it 'considers a union selector to be missing if one of its selector-atoms are missing', asyncSpec (next) ->
-              $target = $fixture('.target').text('old target')
-              $target2 = $fixture('.target2').text('old target2')
-              $fallback = $fixture('.fallback').text('old fallback')
-              up.replace('.target, .target2', '/path', fallback: '.fallback')
-              $target2.remove()
-
-              next =>
-                @respondWith """
-
new target

-
new target2

-
new fallback

-                """
-              next =>
-                expect('.target').toHaveText('old target')
-                expect('.fallback').toHaveText('new fallback')
-
-            it "tries the layer's default targets if options.fallback is missing", asyncSpec (next) ->
-              up.layer.config.all.targets = ['.fallback']
-              $target = $fixture('.target').text('old target')
-              $fallback = $fixture('.fallback').text('old fallback')
-              up.replace('.target', '/path')
-              $target.remove()
-
-              next =>
-                @respondWith """
-
new target

-
new fallback

-                """
-
-              next =>
-                expect('.fallback').toHaveText('new fallback')
-
-            it "tries the layer's default targets  and rejects the promise if options.fallback is false", (done) ->
-              up.layer.config.all.fallbacks = ['.fallback']
-              $target = $fixture('.target').text('old target')
-              $fallback = $fixture('.fallback').text('old fallback')
-              promise = up.replace('.target', '/path', fallback: false)
-
-              u.task =>
-                $target.remove()
-
-                @respondWith """
-
new target

-
new fallback

-                """
-
-                promise.catch (e) ->
-                  expect(e).toBeError(/Could not match target/i)
-                  done()
-
-          describe 'when selectors are missing in the response', ->
-
-            beforeEach ->
-              up.layer.config.all.targets = []
-
-            it "tries the selector in options.fallback before swapping elements", asyncSpec (next) ->
-              $target = $fixture('.target').text('old target')
-              $fallback = $fixture('.fallback').text('old fallback')
-              up.replace('.target', '/path', fallback: '.fallback')
-
-              next =>
-                @respondWith """
-
new fallback

-                """
-
-              next =>
-                expect('.target').toHaveText('old target')
-                expect('.fallback').toHaveText('new fallback')
-
-            describe 'if all alternatives are exhausted', ->
-
-              it 'rejects the promise', (done) ->
-                $target = $fixture('.target').text('old target')
-                $fallback = $fixture('.fallback').text('old fallback')
-                promise = up.replace('.target', '/path', fallback: '.fallback')
-
-                u.task =>
-                  @respondWith '
new unexpected
'
-
-                promise.catch (e) ->
-                  expect(e).toBeError(/Could not match target/i)
-                  done()
-
-              it 'shows a link to open the unexpected response', (done) ->
-                $target = $fixture('.target').text('old target')
-                $fallback = $fixture('.fallback').text('old fallback')
-                promise = up.replace('.target', '/path', fallback: '.fallback')
-                loadPage = spyOn(up.browser, 'loadPage')
-
-                u.task =>
-                  @respondWith '
new unexpected
'
-
-                promise.catch (e) ->
-                  $toast = $('.up-toast')
-                  expect($toast).toBeAttached()
-                  $inspectLink = $toast.find(".up-toast-action:contains('Open response')")
-                  expect($inspectLink).toBeAttached()
-                  expect(loadPage).not.toHaveBeenCalled()
-
-                  Trigger.clickSequence($inspectLink)
-
-                  u.task =>
-                    expect(loadPage).toHaveBeenCalledWith('/path', {})
-                    done()
-
-            it 'considers a union selector to be missing if one of its selector-atoms are missing', asyncSpec (next) ->
-              $target = $fixture('.target').text('old target')
-              $target2 = $fixture('.target2').text('old target2')
-              $fallback = $fixture('.fallback').text('old fallback')
-              up.replace('.target, .target2', '/path', fallback: '.fallback')
-
-              next =>
-                @respondWith """
-
new target

-
new fallback

-                """
-
-              next =>
-                expect('.target').toHaveText('old target')
-                expect('.target2').toHaveText('old target2')
-                expect('.fallback').toHaveText('new fallback')
-
-            it "tries the layer's default targets if options.fallback is missing", asyncSpec (next) ->
-              up.layer.config.all.targets = ['.fallback']
-              $target = $fixture('.target').text('old target')
-              $fallback = $fixture('.fallback').text('old fallback')
-              up.replace('.target', '/path')
-
-              next =>
-                @respondWith '
new fallback
'
-
-              next =>
-                expect('.target').toHaveText('old target')
-                expect('.fallback').toHaveText('new fallback')
-
-            it "does not try the layer's default targets and rejects the promise if options.fallback is false", (done) ->
-              up.layer.config.all.targets = ['.fallback']
-              $target = $fixture('.target').text('old target')
-              $fallback = $fixture('.fallback').text('old fallback')
-              promise = up.replace('.target', '/path', fallback: false)
-
-              u.task =>
-                @respondWith '
new fallback
'
-
-              promise.catch (e) ->
-                expect(e).toBeError(/Could not match target/i)
-                done()
-
-
-
-
-
-
-
+      it 'delegates to up.change(target, { url })', ->
+        changeSpy = up.fragment.knife.mock('makeChange')
+        up.replace('target', 'url')
+        expect(changeSpy).toHaveBeenCalledWith('target', { url: 'url' })

     describe 'up.extract()', ->

-      it "rejects if the selector can't be found on the current page", (done) ->
-        html = '
text
'
-        promise = up.extract('.foo-bar', html)
-
-        u.task ->
-          promiseState(promise).then (result) =>
-            expect(result.state).toEqual('rejected')
-            expect(result.value).toMatch(/Could not match target in current page/i)
-            done()
-
-      it "ignores an element that matches the selector but also matches .up-destroying", (done) ->
-        html = '
text
'
-        $fixture('.foo-bar.up-destroying')
-        promise = up.extract('.foo-bar', html)
-
-        u.task ->
-          promiseState(promise).then (result) =>
-            expect(result.state).toEqual('rejected')
-            expect(result.value).toMatch(/Could not match target/i)
-            done()
-
-      it "ignores an element that matches the selector but also has a parent matching .up-destroying", (done) ->
-        html = '
text
'
-        $parent = $fixture('.up-destroying')
-        $child = $fixture('.foo-bar').appendTo($parent)
-        promise = up.extract('.foo-bar', html)
-
-        u.task ->
-          promiseState(promise).then (result) =>
-            expect(result.state).toEqual('rejected')
-            expect(result.value).toMatch(/Could not match target/i)
-            done()
-
-      it 'only replaces the first element matching the selector', asyncSpec (next) ->
-        html = '
text
'
-        $fixture('.foo-bar')
-        $fixture('.foo-bar')
-        up.extract('.foo-bar', html)
-
-        next =>
-          $elements = $('.foo-bar')
-          expect($($elements.get(0)).text()).toEqual('text')
-          expect($($elements.get(1)).text()).toEqual('')
-
-
-
-
-    describe 'up.destroy', ->
+      it 'delegates to up.change(target, { document })', ->
+        changeSpy = up.fragment.knife.mock('makeChange')
+        up.extract('target', 'document')
+        expect(changeSpy).toHaveBeenCalledWith('target', { document: 'document' })
+
+    describe 'up.destroy()', ->

       it 'removes the element with the given selector', (done) ->
         $fixture('.element')
@@ -2726,7 +2725,6 @@
         next =>
           expect($('.element')).toHaveText('new text')

-
       describeFallback 'canPushState', ->

         it 'makes a page load from the closest known source URL', asyncSpec (next) ->


fixed gulp watch for new file structure.

--- Gulpfile.coffee
+++ Gulpfile.coffee
@@ -60,4 +60,4 @@

 gulp.task "watch", ["build"], ->
   gulp.watch path.wrapper + "*.js", ["build"]
-  gulp.watch path.public + "app.coffee", ["build"]
+  gulp.watch path.public + "**/*.coffee", ["build"]


Set msvs_version in Gulpfile

Set msvs_version in Gulpfile

--- Gulpfile.coffee
+++ Gulpfile.coffee
@@ -15,7 +15,7 @@
     globals:
       should: require 'should'
 gulp.task 'gyp', shell.task [
-  'node-pre-gyp configure build'
+  'node-pre-gyp --msvs_version=2012 configure build'
 ]
 gulp.task 'install', shell.task [
   'node-pre-gyp install --fallback-to-build'


Read sentry dsn from env

Read sentry dsn from env

--- services/clsi/config/settings.defaults.coffee
+++ services/clsi/config/settings.defaults.coffee
@@ -39,6 +39,8 @@
 	parallelSqlQueryLimit:1
 	filestoreDomainOveride: process.env["FILESTORE_DOMAIN_OVERRIDE"]
 	texliveImageNameOveride: process.env["TEX_LIVE_IMAGE_NAME_OVERRIDE"]
+	sentry:
+		dsn: process.env['SENTRY_DSN']

 if process.env["DOCKER_RUNNER"]


Fix #3858: app-meta service doesn't overwrite the title of all the svg icons

--- app/modules/services/app-meta.service.coffee
+++ app/modules/services/app-meta.service.coffee
@@ -33,7 +33,7 @@
         return if not key

         if key == "title"
-            meta = $("title")
+            meta = $("head title")

             if meta.length == 0
                 meta = $("<title></title>")
@@ -41,7 +41,7 @@

             meta.text(value or "")
         else if key.indexOf("og:") == 0
-            meta = $("meta[property='#{key}']")
+            meta = $("head meta[property='#{key}']")

             if meta.length == 0
                 meta = $("<meta property='#{key}'/>")
@@ -49,7 +49,7 @@

             meta.attr("content", value or "")
         else
-            meta = $("meta[name='#{key}']")
+            meta = $("head meta[name='#{key}']")

             if meta.length == 0
                 meta = $("<meta name='#{key}'/>")
@@ -91,7 +91,7 @@
         )

     removeMobileViewport: () ->
-        $("meta[name=\"viewport\"]").remove()
+        $("head meta[name=\"viewport\"]").remove()

     setfn: (fn) ->
         @._listener() if @.listener


use correct mission number (not always 1)

--- src/scripts/controllers/missionDetailController.coffee
+++ src/scripts/controllers/missionDetailController.coffee
@@ -1,9 +1,10 @@
 class Controller
-  constructor: (@$log, @missionService) ->
+  constructor: (@$log, @$routeParams, @missionService) ->
     setMissions = =>
-      @missionService.getMission(1).then (results) =>
+      console.log("Getting mission " + $routeParams.id)
+      @missionService.getMission($routeParams.id).then (results) =>
         @mission = results

     setMissions()

-angular.module('app').controller 'missionDetailController', ['$log', 'missionService', Controller]
+angular.module('app').controller 'missionDetailController', ['$log', '$routeParams', 'missionService', Controller]


:lipstick:

:lipstick:

--- src/packages/editor-stats/index.coffee
+++ src/packages/editor-stats/index.coffee
@@ -4,9 +4,7 @@
 module.exports =
 class EditorStats extends DeferredAtomPackage
   loadEvents: ['editor-stats:toggle']
-
   instanceClass: 'editor-stats/src/editor-stats-view'
-
   stats: new Stats

   activate: (rootView) ->


Don't show duplicate label on student level playtimes when one student has played the level

--- app/views/teachers/TeacherStudentView.coffee
+++ app/views/teachers/TeacherStudentView.coffee
@@ -245,7 +245,7 @@
       chart.append('text')
         .attr('x', ((d) -> xRange(d.levelIndex) + (xRange.rangeBand())/2))
         .attr('y', ((d) -> yRange(d.classAvg) - 3 ))
-        .text((d)-> if d.classAvg isnt 0 then d.classAvg)
+        .text((d)-> if d.classAvg isnt 0 and d.classAvg isnt d.studentTime then d.classAvg)
         .attr('class', 'label')
       # draw student playtime bars
       chart.append('rect')


Add comment

Add comment

--- services/web/public/coffee/ide.coffee
+++ services/web/public/coffee/ide.coffee
@@ -128,6 +128,8 @@
 		ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)

 		# Set up labels
+		# TODO: figure out angular init order and move this
+		# code somewhere more appropriate
 		ide.socket.on 'broadcastDocLabels', (data) ->
 			labels.onBroadcastDocLabels(data)
 		$scope.$on 'entity:deleted', labels.onEntityDeleted


Remove duplicate api from URL

Remove duplicate api from URL

accounts_api setting already includes "api" path

--- shared/src/model/notifications.coffee
+++ shared/src/model/notifications.coffee
@@ -23,7 +23,7 @@

   startPolling: (@windowImpl = window) ->
     @_startPolling(
-      'accounts', URLs.construct('accounts_api', 'api', 'user')
+      'accounts', URLs.construct('accounts_api', 'user')
     ) if URLs.get('accounts_api')

     @_startPolling(


Ignore .sw* files in editor

Ignore .sw* files in editor

--- dotfiles/.atom/config.cson
+++ dotfiles/.atom/config.cson
@@ -22,6 +22,7 @@
       "._*"
       "Thumbs.db"
       "node_modules"
+      ".sw*"
     ]
     telemetryConsent: "limited"
     themes: [


tp #1070 tp #1071, minor surveyView refactoring

--- jsapp/xlform_model_view/view.surveyApp.coffee
+++ jsapp/xlform_model_view/view.surveyApp.coffee
@@ -69,6 +69,7 @@
       "click .js-toggle-row-settings": "toggleRowSettings"
       "click .js-toggle-row-multioptions": "toggleRowMultioptions"
       "click .js-expand-row-selector": "expandRowSelector"
+      "click .js-expand-multioptions--all": "expandMultioptions"
       "click .rowselector_toggle-library": "toggleLibrary"
       "mouseenter .card__buttons__button": "buttonHoverIn"
       "mouseleave .card__buttons__button": "buttonHoverOut"
@@ -274,69 +275,75 @@
           throw new Error('View for row was not found: ' + rowId)
         new $viewRowSelector.RowSelector(el: $spacer.get(0), ngScope: @ngScope, spawnedFromView: view, surveyView: @, reversible:true).expand()

-    render: ()->
-      @$el.removeClass("content--centered").removeClass("content")
+    _render_html: ->
       @$el.html $viewTemplates.$$render('surveyApp', @)
+      @formEditorEl = @$(".-form-editor")
+      @settingsBox = @$("#additional-options")
+
+    _render_attachEvents: ->
       @survey.settings.on 'validated:invalid', (model, validations) ->
         for key, value of validations
             break
-
-      @formEditorEl = @$(".-form-editor")
-      # @$(".survey-editor__null-top-row .survey__row__spacer .btn--addrow").click (evt)=>
-      #   if !@emptySurveyXlfRowSelector
-      #     @emptySurveyXlfRowSelector = new $viewRowSelector.RowSelector(el: @$el.find(".survey__row__spacer").get(0), survey: @survey, ngScope: @ngScope)
-      #   @emptySurveyXlfRowSelector.expand()
-
-      @null_top_row_view_selector = new $viewRowSelector.RowSelector(el: @$el.find(".survey__row__spacer").get(0), survey: @survey, ngScope: @ngScope, surveyView: @, reversible:true)

       if @features.displayTitle
         $viewUtils.makeEditable @, @survey.settings, '.form-title', property:'form_title', options: validate: (value) ->
           if value.length > 255
             return "Length cannot exceed 255 characters, is " + value.length + " characters."
           return
-      else
+
+      if @features.surveySettings
+        $viewUtils.makeEditable @, @survey.settings, '.form-id', property:'form_id', transformFunction: $modelUtils.sluggify
+
+
+    _render_addSubViews: ->
+      for detail in @survey.surveyDetails.models
+        @settingsBox.append((new $surveyDetailView.SurveyDetailView(model: detail)).render().el)
+
+      # in which cases is the null_top_row_view_selector viewed
+      @null_top_row_view_selector = new $viewRowSelector.RowSelector(el: @$el.find(".survey__row__spacer").get(0), survey: @survey, ngScope: @ngScope, surveyView: @, reversible:true)
+
+    _render_hideConditionallyDisplayedContent: ->
+      if not @features.displayTitle
         @$(".survey-header__inner").hide()
-
-      # see this page for info on what should be in a form_id
-      # http://opendatakit.org/help/form-design/guidelines/
-      $viewUtils.makeEditable @, @survey.settings, '.form-id', property:'form_id', transformFunction: $modelUtils.sluggify
-      # @.survey.on 'change:form_id', _.bind viewUtils.handleChange('form_id', XLF.sluggify), @
-
-      addOpts = @$("#additional-options")
-      for detail in @survey.surveyDetails.models
-        addOpts.append((new $surveyDetailView.SurveyDetailView(model: detail)).render().el)
-
-      @reset()

       if not @features.surveySettings
         @$(".survey-header__options-toggle").hide()

-      if @features.multipleQuestions
-        @activateSortable()
-      else
+      if !@features.multipleQuestions
         @$el.addClass('survey-editor--singlequestion')
-
+        @$el.find(".survey-editor__null-top-row").find(".survey-editor__message").addClass("hidden")
         if @survey.rows.length is 0
-          new $viewRowSelector.RowSelector(el: @$el.find(".survey__row__spacer").get(0), survey: @survey, ngScope: @ngScope, surveyView: @, reversible: false).expand()
-
-      if not @features.copyToLibrary
-        # TODO: what happened to this element?
-        @$(".row-extras__add-to-question-library").hide()
-
-
-      if @expand_all_multioptions is null
-        $expand_multioptions = @$(".js-expand-multioptions--all")
-        $expand_multioptions.click () =>
-          if @expand_all_multioptions
-            @expand_all_multioptions = false
-            $(".card.card--selectquestion").removeClass("card--expandedchoices")
-            $expand_multioptions.html($expand_multioptions.html().replace("Collapse", "Expand"));
-          else
-            @expand_all_multioptions = true
-            $(".card.card--selectquestion").addClass("card--expandedchoices")
-            $expand_multioptions.html($expand_multioptions.html().replace("Expand", "Collapse"));
-
+          @null_top_row_view_selector.expand()
+
+    render: ()->
+      @$el.addClass("survey-editor--loading")
+      @$el.removeClass("content--centered").removeClass("content")
+
+      try
+        @_render_html()
+        @_render_attachEvents()
+        @_render_addSubViews()
+        @_render_hideConditionallyDisplayedContent()
+
+        @_reset()
+
+      catch error
+        @$el.addClass("survey-editor--error")
+        throw error
+
+      @$el.removeClass("survey-editor--loading")
       @
+
+    expandMultioptions: ->
+      $expand_multioptions = @$(".js-expand-multioptions--all")
+      if @expand_all_multioptions
+        @expand_all_multioptions = false
+        @$(".card.card--selectquestion").removeClass("card--expandedchoices")
+        $expand_multioptions.html($expand_multioptions.html().replace("Collapse", "Show"));
+      else
+        @expand_all_multioptions = true
+        @$(".card.card--selectquestion").addClass("card--expandedchoices")
+        $expand_multioptions.html($expand_multioptions.html().replace("Show", "Collapse"));

     getItemPosition: (item) ->
       i = 0
@@ -476,16 +483,16 @@
       @survey.forEachRow(fn, includeErrors: true, includeGroups: true, flat: true)

       null_top_row = @formEditorEl.find(".survey-editor__null-top-row, .survey-editor__message").removeClass("expanded")
-      if isEmpty
+
+      if isEmpty and @features.multipleQuestions
         null_top_row.removeClass("hidden")
-      else
+      else if @features.multipleQuestions
         null_top_row.addClass("hidden")

       if @features.multipleQuestions
         @activateSortable()

-      # $viewUtils.reorderElemsByData(".xlf-row-view", @$el, "row-index")
-      return
+      ``

     clickRemoveRow: (evt)->
       evt.preventDefault()


Style fixes for partner_sku_reviews

Style fixes for partner_sku_reviews

Use only div and span html tags

--- src/plugins/partner_sku_reviews.coffee
+++ src/plugins/partner_sku_reviews.coffee
@@ -62,7 +62,7 @@
 ratingBreakdown = (breakdown) ->
   list_elements = breakdown.map ({star, percentage, count}) ->
     """
-

+

           #{starText(star)}

@@ -70,13 +70,13 @@

#{count}

-

+

     """

   """
-

+

       #{list_elements.join('')}
-

+

   """

 ## Components
@@ -167,19 +167,19 @@
       # possible styles are: "good", "so-so" and "bad"
       # which translate into classes: "sa-good", "sa-so-so" and "sa-bad"
       """
-

+

           #{label}

-

+

       """

     """

-

+

           #{aggregation_elements.join('')}
-

+

     """

@@ -198,11 +198,11 @@
       features = sentiments[sentiment]
       return '' unless features && features.length

-      feature_elements = features.map (feature) -> "
#{feature}
"
+      feature_elements = features.map (feature) -> "
#{feature}
"
       """
-

+

           #{feature_elements.join('')}
-

+

       """

     """
@@ -220,7 +220,7 @@
       else
         ''
       """
-

+

@@ -234,17 +234,17 @@

-
#{review.review}

+
#{review.review}

             #{expand_button}
             #{reviewSentiments(review.sentiments)}

-

+

       """

     """
-

+

         #{review_elements.join('')}
-

+

     """

   constructor: (el, assigns) ->
@@ -260,7 +260,7 @@
       body = """

@@translations.partner_sku_reviews.share_your_experience

-
@@translations.partner_sku_reviews.write_review_for #{@title} @@translations.partner_sku_reviews.and_help

+
@@translations.partner_sku_reviews.write_review_for #{@title} @@translations.partner_sku_reviews.and_help

           @@translations.partner_sku_reviews.review_this

@@ -268,7 +268,7 @@
     else
       body = """

-
#{@title}

+
#{@title}

               #{starRating(@reviewscore, @reviews_count)}
@@ -511,6 +511,11 @@
       font-family: Verdana, Arial, sans-serif !important;
     }

+    ##{flavor}-product-reviews-inline div, ##{flavor}-product-reviews-inline span,
+    ##{flavor}-product-reviews-extended div, ##{flavor}-product-reviews-extended span {
+      all: unset;
+    }
+
     ##{flavor}-product-reviews-inline .sa-star-rating-wrapper,
     ##{flavor}-product-reviews-extended .sa-star-rating-wrapper {
       display: inline-block;
@@ -814,14 +819,12 @@
       align-items: center;
       -ms-flex-align: center;
       padding: 15px;
-      margin: 0;
       border-bottom: 1px solid #e3e3e3;
     }

     ##{flavor}-product-reviews-extended .sa-extended-reviews-title {
       display: inline-block;
       font-size: 17px;
-      margin: 0;
     }

     ##{flavor}-product-reviews-extended .sa-extended-reviews-title::first-letter {
@@ -831,7 +834,6 @@
     ##{flavor}-product-reviews-extended .sa-brand-logo {
       height: 22px;
       width: 80px;
-      margin: 0;
     }

     ##{flavor}-product-reviews-extended .sa-brand-powered-by {
@@ -954,7 +956,7 @@
     ##{flavor}-product-reviews-extended .sa-rating-breakdown-class {
       display: list-item;
       list-style-type: none;
-      margin: 0;
+      margin: 0 0 4px 0;
     }

     ##{flavor}-product-reviews-extended .sa-num-stars {
@@ -1162,7 +1164,6 @@

     ##{flavor}-product-reviews-extended .sa-review-text {
       display: block;
-      max-width: 60em;
       margin: 20px 0 10px;
       word-wrap: break-all;
       word-wrap: break-word;
@@ -1300,7 +1301,7 @@
     /* EXTENDED - NO CONTENT */

     ##{flavor}-product-reviews-extended .sa-review-prompt {
-      padding: 30px;
+      padding: 20px 15px;
       display: block;
     }


Remove extra semicolons in coffee

Remove extra semicolons in coffee

--- resources/assets/lib/register-components.coffee
+++ resources/assets/lib/register-components.coffee
@@ -5,7 +5,7 @@
 import { CommentsManager } from 'comments-manager'
 import { CountdownTimer } from 'countdown-timer'
 import { FriendButton } from 'friend-button'
-import { LandingNews } from 'landing-news';
+import { LandingNews } from 'landing-news'
 import NotificationWidget from 'notification-widget/main'
 import NotificationWidgetWorker from 'notification-widget/worker'
 import { SpotlightSelectOptions } from 'spotlight-select-options'
@@ -13,7 +13,7 @@
 import { UserCardStore } from 'user-card-store'
 import { startListening, UserCardTooltip } from 'user-card-tooltip'
 import { UserCards } from 'user-cards'
-import { WikiSearch } from 'wiki-search';
+import { WikiSearch } from 'wiki-search'

 window.reactTurbolinks ?= new ReactTurbolinks()


organize style initialization

organize style initialization

--- lib/TermView.coffee
+++ lib/TermView.coffee
@@ -61,12 +61,10 @@
     term.on "data", (data)=> @input data
     term.open this.get(0)

-    # remove background color in favor of the atom background
-    term.element.style.background = null
-
     @input "#{runCommand}#{os.EOL}" if runCommand
     term.focus()

+    @applyStyle()
     @attachEvents()
     @resizeToPane()

@@ -91,6 +89,10 @@

   getIconName: ->
     "terminal"
+
+  applyStyle: ->
+    # remove background color in favor of the atom background
+    @term.element.style.background = null

   attachEvents: ->
     @resizeToPane = @resizeToPane.bind this


Collaboration: Call callback after fetching social channel.

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -1163,6 +1163,8 @@

       @socialChannel = channel

+      callback @socialChannel
+

   startCollaborationSession: (callback) ->