unpoly-rails 0.52.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of unpoly-rails might be problematic. Click here for more details.

@@ -5,7 +5,7 @@ class up.ExtractPlan
5
5
  constructor: (selector, options) ->
6
6
  @origin = options.origin
7
7
  @selector = up.dom.resolveSelector(selector, options.origin)
8
- @transition = options.transition || options.animation || 'none'
8
+ @transition = options.transition
9
9
  @response = options.response
10
10
  @oldLayer = options.layer
11
11
  @steps = @parseSteps()
@@ -41,16 +41,11 @@ class up.ExtractPlan
41
41
  ]
42
42
  ###
43
43
  parseSteps: =>
44
- if u.isString(@transition)
45
- transitions = @transition.split(comma)
46
- else
47
- transitions = [@transition]
48
-
49
44
  comma = /\ *,\ */
50
45
 
51
46
  disjunction = @selector.split(comma)
52
47
 
53
- u.map disjunction, (literal, i) ->
48
+ u.map disjunction, (literal, i) =>
54
49
  literalParts = literal.match(/^(.+?)(?:\:(before|after))?$/)
55
50
  literalParts or up.fail('Could not parse selector literal "%s"', literal)
56
51
  selector = literalParts[1]
@@ -60,8 +55,7 @@ class up.ExtractPlan
60
55
  selector = 'body'
61
56
 
62
57
  pseudoClass = literalParts[2]
63
- transition = transitions[i] || u.last(transitions)
64
58
 
65
59
  selector: selector
66
60
  pseudoClass: pseudoClass
67
- transition: transition
61
+ transition: @transition
@@ -28,13 +28,15 @@ up.dom = (($) ->
28
28
  It is recommend to always keep `'body'` as the last selector in the last in the case
29
29
  your server or load balancer renders an error message that does not contain your
30
30
  application layout.
31
- @param {string} [options.fallbackTransition='none']
32
- The transition to use when using a fallback target.
31
+ @param {string} [options.fallbackTransition=null]
32
+ The transition to use when using a [fallback target](/#options.fallbacks).
33
+
34
+ By default this is not set and the original replacement's transition is used.
33
35
  @stable
34
36
  ###
35
37
  config = u.config
36
38
  fallbacks: ['body']
37
- fallbackTransition: 'none'
39
+ fallbackTransition: null
38
40
 
39
41
  reset = ->
40
42
  config.reset()
@@ -187,8 +189,11 @@ up.dom = (($) ->
187
189
  If set to `false`, the history will remain unchanged.
188
190
  @param {boolean|string} [options.source=true]
189
191
  @param {boolean|string} [options.reveal=false]
190
- Whether to [reveal](/up.reveal) the element being updated, by
191
- scrolling its containing viewport.
192
+ Whether to [reveal](/up.reveal) the new fragment.
193
+
194
+ You can also pass a CSS selector for the element to reveal.
195
+ @param {boolean|string} [options.failReveal=false]
196
+ Whether to [reveal](/up.reveal) the new fragment when the server responds with an error.
192
197
 
193
198
  You can also pass a CSS selector for the element to reveal.
194
199
  @param {boolean} [options.restoreScroll=false]
@@ -239,8 +244,12 @@ up.dom = (($) ->
239
244
  failureOptions = u.merge options,
240
245
  humanizedTarget: 'failure target'
241
246
  provideTarget: undefined # don't provide a target if we're targeting the failTarget
247
+ restoreScroll: false
248
+ hungry: false
249
+
242
250
  u.renameKey(failureOptions, 'failTransition', 'transition')
243
251
  u.renameKey(failureOptions, 'failLayer', 'layer')
252
+ u.renameKey(failureOptions, 'failReveal', 'reveal')
244
253
 
245
254
  try
246
255
  improvedTarget = bestPreflightSelector(selectorOrElement, successOptions)
@@ -102,8 +102,14 @@ up.form = (($) ->
102
102
  The delay before the transition starts. See [`up.morph()`](/up.morph).
103
103
  @param {string} [options.easing]
104
104
  The timing function that controls the transition's acceleration. [`up.morph()`](/up.morph).
105
- @param {Element|jQuery|string} [options.reveal=true]
106
- Whether to reveal the target element within its viewport.
105
+ @param {Element|string} [options.reveal=true]
106
+ Whether to reveal the target fragment after it was replaced.
107
+
108
+ You can also pass a CSS selector for the element to reveal.
109
+ @param {boolean|string} [options.failReveal=true]
110
+ Whether to [reveal](/up.reveal) the target fragment when the server responds with an error.
111
+
112
+ You can also pass a CSS selector for the element to reveal.
107
113
  @param {boolean} [options.restoreScroll]
108
114
  If set to `true`, this will attempt to [`restore scroll positions`](/up.restoreScroll)
109
115
  previously seen on the destination URL.
@@ -135,13 +141,14 @@ up.form = (($) ->
135
141
  target = u.option(options.target, $form.attr('up-target'), 'body')
136
142
  url = u.option(options.url, $form.attr('action'), up.browser.url())
137
143
  options.failTarget = u.option(options.failTarget, $form.attr('up-fail-target')) || u.selectorForElement($form)
144
+ options.reveal = u.option(options.reveal, u.castedAttr($form, 'up-reveal'), true)
145
+ options.failReveal = u.option(options.failReveal, u.castedAttr($form, 'up-fail-reveal'), true)
138
146
  options.fallback = u.option(options.fallback, $form.attr('up-fallback'))
139
147
  options.history = u.option(options.history, u.castedAttr($form, 'up-history'), true)
140
148
  options.transition = u.option(options.transition, u.castedAttr($form, 'up-transition'), 'none')
141
149
  options.failTransition = u.option(options.failTransition, u.castedAttr($form, 'up-fail-transition'), 'none')
142
150
  options.method = u.option(options.method, $form.attr('up-method'), $form.attr('data-method'), $form.attr('method'), 'post').toUpperCase()
143
151
  options.headers = u.option(options.headers, {})
144
- options.reveal = u.option(options.reveal, u.castedAttr($form, 'up-reveal'), true)
145
152
  options.cache = u.option(options.cache, u.castedAttr($form, 'up-cache'))
146
153
  options.restoreScroll = u.option(options.restoreScroll, u.castedAttr($form, 'up-restore-scroll'))
147
154
  options.origin = u.option(options.origin, $form)
@@ -520,7 +527,7 @@ up.form = (($) ->
520
527
  If omitted, Unpoly will replace the `<form>` tag itself, assuming that the
521
528
  server has echoed the form with validation errors.
522
529
  @param [up-fallback]
523
- The selector to replace if the server responds with a non-200 status code.
530
+ The selector to replace if the server responds with an error.
524
531
  @param {string} [up-transition]
525
532
  The animation to use when the form is replaced after a successful submission.
526
533
  @param {string} [up-fail-transition]
@@ -549,7 +556,18 @@ up.form = (($) ->
549
556
  The name of the layer that ought to be updated if the server sends a
550
557
  non-200 status code.
551
558
  @param {string} [up-reveal='true']
552
- Whether to reveal the target element within its viewport before updating.
559
+ Whether to reveal the target element after it was replaced.
560
+
561
+ You can also pass a CSS selector for the element to reveal.
562
+ @param {string} [up-fail-reveal='true']
563
+ Whether to reveal the target element when the server responds with an error.
564
+
565
+ You can also pass a CSS selector for the element to reveal. You may use this, for example,
566
+ to reveal the first validation error message:
567
+
568
+ <form up-target=".content" up-fail-reveal=".error">
569
+ ...
570
+ </form>
553
571
  @param {string} [up-restore-scroll='false']
554
572
  Whether to restore previously known scroll position of all viewports
555
573
  within the target selector.
@@ -237,7 +237,7 @@ up.layout = (($) ->
237
237
  @stable
238
238
  ###
239
239
  reveal = (elementOrSelector, options) ->
240
- $element = $(elementOrSelector)
240
+ $element = $(elementOrSelector).first() # we can only reveal one element
241
241
  up.puts 'Revealing fragment %o', $element.get(0)
242
242
  options = u.options(options)
243
243
 
@@ -473,7 +473,13 @@ up.layout = (($) ->
473
473
  selector = revealSelector(options.reveal)
474
474
  $element = up.first(selector) || $element
475
475
  revealOptions.top = true
476
- return reveal($element, revealOptions)
476
+
477
+ # If selectorOrElement was a CSS selector, don't blow up by calling reveal()
478
+ # with an empty jQuery collection. This might happen if a failed form submission
479
+ # reveals the first validation error message, but the error is shown in an
480
+ # unexpected element.
481
+ if $element.length
482
+ return reveal($element, revealOptions)
477
483
 
478
484
  # If we didn't need to scroll above, just return a resolved promise
479
485
  # to fulfill this function's signature.
@@ -124,7 +124,14 @@ up.link = (($) ->
124
124
  The selector to replace.
125
125
  Defaults to the `[up-target]`, `[up-modal]` or `[up-popup]` attribute on `link`.
126
126
  If no target is given, the `<body>` element will be replaced.
127
+ @param {boolean|string} [options.reveal=true]
128
+ Whether to [reveal](/up.reveal) the target fragment after it was replaced.
127
129
 
130
+ You can also pass a CSS selector for the element to reveal.
131
+ @param {boolean|string} [options.failReveal=true]
132
+ Whether to [reveal](/up.reveal) the target fragment when the server responds with an error.
133
+
134
+ You can also pass a CSS selector for the element to reveal.
128
135
  @return {Promise}
129
136
  A promise that will be fulfilled when the link destination
130
137
  has been loaded and rendered.
@@ -163,6 +170,7 @@ up.link = (($) ->
163
170
  options.failTransition = u.option(options.failTransition, u.castedAttr($link, 'up-fail-transition'), 'none')
164
171
  options.history = u.option(options.history, u.castedAttr($link, 'up-history'))
165
172
  options.reveal = u.option(options.reveal, u.castedAttr($link, 'up-reveal'), true)
173
+ options.failReveal = u.option(options.failReveal, u.castedAttr($link, 'up-fail-reveal'), true)
166
174
  options.cache = u.option(options.cache, u.castedAttr($link, 'up-cache'))
167
175
  options.restoreScroll = u.option(options.restoreScroll, u.castedAttr($link, 'up-restore-scroll'))
168
176
  options.method = followMethod($link, options)
@@ -347,10 +355,10 @@ up.link = (($) ->
347
355
  @param {string} [up-transition='none']
348
356
  The [transition](/up.motion) to use for morphing between the old and new elements.
349
357
  @param [up-fail-target='body']
350
- The selector to replace if the server responds with a non-200 status code.
358
+ The selector to replace if the server responds with an error.
351
359
  @param {string} [up-fail-transition='none']
352
360
  The [transition](/up.motion) to use for morphing between the old and new elements
353
- when the server responds with a non-200 status code.
361
+ when the server responds with an error.
354
362
  @param {string} [up-fallback]
355
363
  The selector to update when the original target was not found in the page.
356
364
  @param {string} [up-href]
@@ -360,7 +368,13 @@ up.link = (($) ->
360
368
  A message that will be displayed in a cancelable confirmation dialog
361
369
  before the link is followed.
362
370
  @param {string} [up-reveal='true']
363
- Whether to reveal the target element within its viewport before updating.
371
+ Whether to reveal the target element after it was replaced.
372
+
373
+ You can also pass a CSS selector for the element to reveal.
374
+ @param {string} [up-fail-reveal='true']
375
+ Whether to reveal the target element when the server responds with an error.
376
+
377
+ You can also pass a CSS selector for the element to reveal.
364
378
  @param {string} [up-restore-scroll='false']
365
379
  Whether to restore previously known scroll position of all viewports
366
380
  within the target selector.
@@ -416,14 +430,14 @@ up.link = (($) ->
416
430
  @param {string} [up-method='get']
417
431
  The HTTP method to use for the request.
418
432
  @param [up-fail-target='body']
419
- The selector to replace if the server responds with a non-200 status code.
433
+ The selector to replace if the server responds with an error.
420
434
  @param {string} [up-fallback]
421
435
  The selector to update when the original target was not found in the page.
422
436
  @param {string} [up-transition='none']
423
437
  The [transition](/up.motion) to use for morphing between the old and new elements.
424
438
  @param {string} [up-fail-transition='none']
425
439
  The [transition](/up.motion) to use for morphing between the old and new elements
426
- when the server responds with a non-200 status code.
440
+ when the server responds with an error.
427
441
  @param [up-href]
428
442
  The destination URL to follow.
429
443
  If omitted, the the link's `href` attribute will be used.
@@ -11,16 +11,22 @@ That said, there is an **optional** protocol your server can use to
11
11
  exchange additional information when Unpoly is [updating fragments](/up.link).
12
12
 
13
13
  While the protocol can help you optimize performance and handle some
14
- edge cases, implementing it is entirely optional. For instance,
14
+ edge cases, implementing it is **entirely optional**. For instance,
15
15
  `unpoly.com` itself is a static site that uses Unpoly on the frontend
16
16
  and doesn't even have a server component.
17
17
 
18
- If you have [installed Unpoly as a Rails gem](/install/rails), the protocol
19
- is already implemented and you will get some
20
- [Ruby bindings](https://github.com/unpoly/unpoly/blob/master/README_RAILS.md)
21
- in your controllers and views. If your server-side app uses another language
22
- or framework, you should be able to implement the protocol in a very short time.
18
+ ## Existing implementations
23
19
 
20
+ You should be able to implement the protocol in a very short time.
21
+ There are existing implementations for various web frameworks:
22
+
23
+ - [Ruby on Rails](/install/rails)
24
+ - [Roda](https://github.com/adam12/roda-unpoly)
25
+ - [Rack](https://github.com/adam12/rack-unpoly) (Sinatra, Padrino, Hanami, Cuba, ...)
26
+ - [Phoenix](https://elixirforum.com/t/unpoly-a-framework-like-turbolinks/3614/15) (Elixir)
27
+
28
+
29
+ ## Protocol details
24
30
 
25
31
  \#\#\# Redirect detection for IE11
26
32
 
@@ -0,0 +1,63 @@
1
+ ###*
2
+ Passive updates
3
+ ===============
4
+
5
+ This work-in-progress package will contain functionality to
6
+ passively receive updates from the server.
7
+
8
+ @class up.radio
9
+ ###
10
+ up.radio = (($) ->
11
+
12
+ u = up.util
13
+
14
+ ###*
15
+ Configures defaults for passive updates.
16
+
17
+ @property up.radio.config
18
+ @param {Array<string>} [options.hungry]
19
+ An array of CSS selectors that is replaced whenever a matching element is found in a response.
20
+ These elements are replaced even when they were not targeted directly.
21
+
22
+ By default this contains the [`[up-hungry]`](/up-hungry) attribute as well as
23
+ `<meta name="csrf-param">` and `<meta name="csrf-token">` tags.
24
+ @param {string} [options.hungryTransition=null]
25
+ The transition to use when a [hungry element](/up-hungry) is replacing itself
26
+ while another target is replaced.
27
+
28
+ By default this is not set and the original replacement's transition is used.
29
+ @stable
30
+ ###
31
+ config = u.config
32
+ hungry: ['[up-hungry]', 'meta[name="csrf-param"]', 'meta[name="csrf-token"]']
33
+ hungryTransition: null
34
+
35
+ reset = ->
36
+ config.reset()
37
+
38
+ ###*
39
+ @function up.radio.hungrySelector
40
+ @internal
41
+ ###
42
+ hungrySelector = ->
43
+ u.multiSelector(config.hungry)
44
+
45
+ ###*
46
+ Elements with this attribute are [updated](/up.replace) whenever there is a
47
+ matching element found in a successful response. The element is replaced even
48
+ when it isn't [targeted](/a-up-target) directly.
49
+
50
+ Use cases for this are unread message counters or notification flashes.
51
+ Such elements often live in the layout, outside of the content area that is
52
+ being replaced.
53
+
54
+ @selector [up-hungry]
55
+ @stable
56
+ ###
57
+
58
+ up.on 'up:framework:reset', reset
59
+
60
+ config: config
61
+ hungrySelector: hungrySelector
62
+
63
+ )(jQuery)
@@ -187,20 +187,31 @@ up.util = (($) ->
187
187
  $element = $(element)
188
188
  selector = undefined
189
189
 
190
+ tagName = $element.prop('tagName').toLowerCase()
191
+
190
192
  if upId = presence($element.attr("up-id"))
191
- selector = "[up-id='#{upId}']"
193
+ selector = attributeSelector('up-id', upId)
192
194
  else if id = presence($element.attr("id"))
193
- selector = "##{id}"
195
+ if id.match(/^[a-z0-9\-_]+$/i)
196
+ selector = "##{id}"
197
+ else
198
+ selector = attributeSelector('id', id)
194
199
  else if name = presence($element.attr("name"))
195
- selector = "[name='#{name}']"
200
+ selector = tagName + attributeSelector('name', name)
196
201
  else if classes = presence(nonUpClasses($element))
197
202
  selector = ''
198
203
  for klass in classes
199
204
  selector += ".#{klass}"
205
+ else if ariaLabel = presence($element.attr("aria-label"))
206
+ selector = attributeSelector('aria-label', ariaLabel)
200
207
  else
201
- selector = $element.prop('tagName').toLowerCase()
208
+ selector = tagName
202
209
  selector
203
210
 
211
+ attributeSelector = (attribute, value) ->
212
+ value = value.replace(/"/g, '\\"')
213
+ "[#{attribute}=\"#{value}\"]"
214
+
204
215
  nonUpClasses = ($element) ->
205
216
  classString = $element.attr('class') || ''
206
217
  classes = classString.split(' ')
@@ -18,6 +18,7 @@
18
18
  #= require ./unpoly/modal
19
19
  #= require ./unpoly/tooltip
20
20
  #= require ./unpoly/feedback
21
+ #= require ./unpoly/radio
21
22
  #= require ./unpoly/rails
22
23
 
23
24
  up.boot()
@@ -4,6 +4,6 @@ module Unpoly
4
4
  # The current version of the unpoly-rails gem.
5
5
  # This version number is also used for releases of the Unpoly
6
6
  # frontend code.
7
- VERSION = '0.52.0'
7
+ VERSION = '0.53.0'
8
8
  end
9
9
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unpoly",
3
- "version": "0.52.0",
3
+ "version": "0.53.0",
4
4
  "description": "Unobtrusive JavaScript framework",
5
5
  "main": "dist/unpoly.js",
6
6
  "files": [
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- unpoly-rails (0.51.1)
4
+ unpoly-rails (0.52.0)
5
5
  rails (>= 3)
6
6
 
7
7
  GEM
@@ -0,0 +1,5 @@
1
+ class HashTestController < ApplicationController
2
+
3
+ layout 'integration_test'
4
+
5
+ end
@@ -0,0 +1,13 @@
1
+ <h1>Hash links (without Unpoly)</h1>
2
+
3
+ <div class="example" id="one" style="height: 1000px">
4
+ <h2>This is #one</h2>
5
+
6
+ <a href="#two">Scroll down to #two</a>
7
+ </div>
8
+
9
+ <div class="example" id="two" style="height: 1000px">
10
+ <h2>This is #two</h2>
11
+
12
+ <a href="#one">Scroll up to #one</a>
13
+ </div>
@@ -60,6 +60,7 @@
60
60
  <li><%= link_to 'Error', '/error_test/trigger' %></li>
61
61
  <li><%= link_to 'Booting with non-GET method', '/method_test/page1' %></li>
62
62
  <li><%= link_to 'Fragment update', '/replace_test/page1' %></li>
63
+ <li><%= link_to 'Hash links (without Unpoly)', '/hash_test/vanilla' %></li>
63
64
  </ul>
64
65
 
65
66
  </div>
@@ -11,6 +11,7 @@ Rails.application.routes.draw do
11
11
  get 'error_test/:action', controller: 'error_test'
12
12
  post 'error_test/:action', controller: 'error_test'
13
13
  get 'replace_test/:action', controller: 'replace_test'
14
+ get 'hash_test/:action', controller: 'hash_test'
14
15
 
15
16
  namespace :form_test do
16
17
  resource :basic, only: [:new, :create]
@@ -133,7 +133,7 @@ describe 'up.dom', ->
133
133
  up.replace('.middle', '/path', method: 'put')
134
134
  next => expect(@lastRequest()).toHaveRequestMethod('PUT')
135
135
 
136
- describe 'when the server responds with a non-200 status code', ->
136
+ describe 'when the server responds with an error', ->
137
137
 
138
138
  it 'replaces the first fallback instead of the given selector', asyncSpec (next) ->
139
139
  up.dom.config.fallbacks = ['.fallback']
@@ -392,6 +392,116 @@ describe 'up.form', ->
392
392
  next => @respondWith('<div class="response">new-text</div>')
393
393
  next =>expect(up.browser.url()).toEqual(@hrefBeforeExample)
394
394
 
395
+ describe 'revealing', ->
396
+
397
+ it 'reaveals the target fragment if the submission succeeds', asyncSpec (next) ->
398
+ $form = affix('form[action="/action"][up-target=".target"]')
399
+ $target = affix('.target')
400
+
401
+ revealStub = up.layout.knife.mock('reveal')
402
+
403
+ up.submit($form)
404
+
405
+ next =>
406
+ @respondWith('<div class="target">new text</div>')
407
+
408
+ next =>
409
+ expect(revealStub).toHaveBeenCalled()
410
+ expect(revealStub.calls.mostRecent().args[0]).toBeMatchedBy('.target')
411
+
412
+ it 'reveals the form if the submission fails', asyncSpec (next) ->
413
+ $form = affix('form#foo-form[action="/action"][up-target=".target"]')
414
+ $target = affix('.target')
415
+
416
+ revealStub = up.layout.knife.mock('reveal')
417
+
418
+ up.submit($form)
419
+
420
+ next =>
421
+ @respondWith
422
+ status: 500,
423
+ responseText: """
424
+ <form id="foo-form">
425
+ Errors here
426
+ </form>
427
+ """
428
+
429
+ next =>
430
+ expect(revealStub).toHaveBeenCalled()
431
+ expect(revealStub.calls.mostRecent().args[0]).toBeMatchedBy('#foo-form')
432
+
433
+
434
+ describe 'with { reveal } option', ->
435
+
436
+ it 'allows to reveal a different selector', asyncSpec (next) ->
437
+ $form = affix('form[action="/action"][up-target=".target"]')
438
+ $target = affix('.target')
439
+ $other = affix('.other')
440
+
441
+ revealStub = up.layout.knife.mock('reveal')
442
+
443
+ up.submit($form, reveal: '.other')
444
+
445
+ next =>
446
+ @respondWith """
447
+ <div class="target">
448
+ new text
449
+ </div>
450
+ <div class="other">
451
+ new other
452
+ </div>
453
+ """
454
+
455
+ next =>
456
+ expect(revealStub).toHaveBeenCalled()
457
+ expect(revealStub.calls.mostRecent().args[0]).toBeMatchedBy('.other')
458
+
459
+ it 'still reveals the form for a failed submission', asyncSpec (next) ->
460
+ $form = affix('form#foo-form[action="/action"][up-target=".target"]')
461
+ $target = affix('.target')
462
+ $other = affix('.other')
463
+
464
+ revealStub = up.layout.knife.mock('reveal')
465
+
466
+ up.submit($form, reveal: '.other')
467
+
468
+ next =>
469
+ @respondWith
470
+ status: 500,
471
+ responseText: """
472
+ <form id="foo-form">
473
+ Errors here
474
+ </form>
475
+ """
476
+
477
+ next =>
478
+ expect(revealStub).toHaveBeenCalled()
479
+ expect(revealStub.calls.mostRecent().args[0]).toBeMatchedBy('#foo-form')
480
+
481
+ describe 'with { failReveal } option', ->
482
+
483
+ it 'reveals the given selector for a failed submission', asyncSpec (next) ->
484
+ $form = affix('form#foo-form[action="/action"][up-target=".target"]')
485
+ $target = affix('.target')
486
+ $other = affix('.other')
487
+
488
+ revealStub = up.layout.knife.mock('reveal')
489
+
490
+ up.submit($form, reveal: '.other', failReveal: '.error')
491
+
492
+ next =>
493
+ @respondWith
494
+ status: 500,
495
+ responseText: """
496
+ <form id="foo-form">
497
+ <div class="error">Errors here</div>
498
+ </form>
499
+ """
500
+
501
+ next =>
502
+ expect(revealStub).toHaveBeenCalled()
503
+ expect(revealStub.calls.mostRecent().args[0]).toBeMatchedBy('.error')
504
+
395
505
  describe 'in a form with file inputs', ->
396
506
 
397
507
  beforeEach ->
@@ -599,7 +709,7 @@ describe 'up.form', ->
599
709
  next =>
600
710
  request = @lastRequest()
601
711
  expect(request.requestHeaders['X-Up-Validate']).toEqual('user')
602
- expect(request.requestHeaders['X-Up-Target']).toEqual(".field-group:has([name='user'])")
712
+ expect(request.requestHeaders['X-Up-Target']).toEqual('.field-group:has(input[name="user"])')
603
713
 
604
714
  @respondWith """
605
715
  <div class="field-group has-error">