unpoly-rails 0.55.1 → 0.56.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -2
  3. data/dist/unpoly-bootstrap3.js +6 -4
  4. data/dist/unpoly-bootstrap3.min.js +1 -1
  5. data/dist/unpoly.js +1323 -805
  6. data/dist/unpoly.min.js +4 -3
  7. data/lib/assets/javascripts/unpoly-bootstrap3/{navigation-ext.coffee → feedback-ext.coffee} +2 -0
  8. data/lib/assets/javascripts/unpoly/browser.coffee.erb +7 -7
  9. data/lib/assets/javascripts/unpoly/bus.coffee.erb +5 -6
  10. data/lib/assets/javascripts/unpoly/classes/css_transition.coffee +127 -0
  11. data/lib/assets/javascripts/unpoly/classes/extract_plan.coffee +1 -1
  12. data/lib/assets/javascripts/unpoly/classes/motion_tracker.coffee +62 -32
  13. data/lib/assets/javascripts/unpoly/classes/url_set.coffee +27 -0
  14. data/lib/assets/javascripts/unpoly/dom.coffee.erb +78 -99
  15. data/lib/assets/javascripts/unpoly/feedback.coffee +147 -96
  16. data/lib/assets/javascripts/unpoly/form.coffee.erb +26 -2
  17. data/lib/assets/javascripts/unpoly/history.coffee +2 -1
  18. data/lib/assets/javascripts/unpoly/layout.coffee.erb +68 -12
  19. data/lib/assets/javascripts/unpoly/link.coffee.erb +10 -4
  20. data/lib/assets/javascripts/unpoly/modal.coffee.erb +11 -9
  21. data/lib/assets/javascripts/unpoly/{motion.coffee → motion.coffee.erb} +184 -322
  22. data/lib/assets/javascripts/unpoly/popup.coffee.erb +13 -12
  23. data/lib/assets/javascripts/unpoly/radio.coffee +1 -1
  24. data/lib/assets/javascripts/unpoly/syntax.coffee +8 -17
  25. data/lib/assets/javascripts/unpoly/tooltip.coffee +11 -11
  26. data/lib/assets/javascripts/unpoly/util.coffee +332 -145
  27. data/lib/unpoly/rails/version.rb +1 -1
  28. data/package.json +1 -1
  29. data/spec_app/Gemfile.lock +1 -1
  30. data/spec_app/app/assets/javascripts/integration_test.coffee +1 -0
  31. data/spec_app/app/assets/stylesheets/integration_test.sass +1 -0
  32. data/spec_app/app/assets/stylesheets/jasmine_specs.sass +4 -0
  33. data/spec_app/app/views/motion_test/transitions.erb +13 -0
  34. data/spec_app/app/views/pages/start.erb +1 -0
  35. data/spec_app/spec/javascripts/helpers/to_be_attached.coffee +5 -0
  36. data/spec_app/spec/javascripts/helpers/to_be_detached.coffee +5 -0
  37. data/spec_app/spec/javascripts/helpers/to_contain.js.coffee +1 -1
  38. data/spec_app/spec/javascripts/helpers/to_have_opacity.coffee +11 -0
  39. data/spec_app/spec/javascripts/helpers/to_have_own_property.js.coffee +5 -0
  40. data/spec_app/spec/javascripts/up/dom_spec.js.coffee +217 -102
  41. data/spec_app/spec/javascripts/up/feedback_spec.js.coffee +162 -44
  42. data/spec_app/spec/javascripts/up/layout_spec.js.coffee +97 -10
  43. data/spec_app/spec/javascripts/up/link_spec.js.coffee +3 -3
  44. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +22 -20
  45. data/spec_app/spec/javascripts/up/motion_spec.js.coffee +344 -228
  46. data/spec_app/spec/javascripts/up/popup_spec.js.coffee +1 -1
  47. data/spec_app/spec/javascripts/up/syntax_spec.js.coffee +1 -1
  48. data/spec_app/spec/javascripts/up/tooltip_spec.js.coffee +1 -1
  49. data/spec_app/spec/javascripts/up/util_spec.js.coffee +194 -0
  50. metadata +11 -4
@@ -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.55.1'
7
+ VERSION = '0.56.0'
8
8
  end
9
9
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unpoly",
3
- "version": "0.55.1",
3
+ "version": "0.56.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.54.1)
4
+ unpoly-rails (0.55.1)
5
5
  rails (>= 3)
6
6
 
7
7
  GEM
@@ -1,6 +1,7 @@
1
1
  #= require jquery
2
2
  #= require jquery_ujs
3
3
  #= require es6-promise.auto
4
+ #= require helpers/knife
4
5
  #= require unpoly
5
6
 
6
7
  up.compiler '.compiler', ($element) ->
@@ -31,6 +31,7 @@ a
31
31
 
32
32
  .fixed-top-bar
33
33
  position: fixed
34
+ z-index: 9999999
34
35
  top: 0
35
36
  left: 0
36
37
  right: 0
@@ -14,3 +14,7 @@
14
14
 
15
15
  &:empty
16
16
  display: none
17
+
18
+ // For specs that need to query computed styles
19
+ .red-background
20
+ background-color: #f00
@@ -0,0 +1,13 @@
1
+ <div class="fixed-top-bar">
2
+ <% for animation in %w(cross-fade move-up move-right move-down move-left) do %>
3
+ <span class="button" onclick="up.replace('.morphed-object', 'transitions', { transition: '<%= animation %>', duration: 2000, cache: false })">
4
+ <%= animation %>
5
+ </span>
6
+ <% end %>
7
+ </div>
8
+
9
+ <div class="example">
10
+ <div class="morphed-object" style="padding: 30px; background-color: #<%= SecureRandom.hex(3) %>; color: white; width: 300px; height: 200px; margin-left: 200px; margin-top: 200px">
11
+ <%= ('a'..'z').to_a.shuffle[0, 20].join %>
12
+ </div>
13
+ </div>
@@ -63,6 +63,7 @@
63
63
  <li><%= link_to 'Hash links (without Unpoly)', '/hash_test/vanilla' %></li>
64
64
  <li><%= link_to 'Revealing long pages', '/reveal_test/long1' %></li>
65
65
  <li><%= link_to 'Animations', '/motion_test/animations' %></li>
66
+ <li><%= link_to 'Transitions', '/motion_test/transitions' %></li>
66
67
  </ul>
67
68
 
68
69
  </div>
@@ -0,0 +1,5 @@
1
+ beforeEach ->
2
+ jasmine.addMatchers
3
+ toBeAttached: (util, customEqualityTesters) ->
4
+ compare: (actual) ->
5
+ pass: !up.util.isDetached(actual)
@@ -0,0 +1,5 @@
1
+ beforeEach ->
2
+ jasmine.addMatchers
3
+ toBeDetached: (util, customEqualityTesters) ->
4
+ compare: (actual) ->
5
+ pass: up.util.isDetached(actual)
@@ -2,4 +2,4 @@ beforeEach ->
2
2
  jasmine.addMatchers
3
3
  toContain: (util, customEqualityTesters) ->
4
4
  compare: (object, expectedElement) ->
5
- pass: up.util.contains(object, expectedElement)
5
+ pass: up.util.isGiven(object) && up.util.contains(object, expectedElement)
@@ -0,0 +1,11 @@
1
+ beforeEach ->
2
+ jasmine.addMatchers
3
+ toHaveOpacity: (util, customEqualityTesters) ->
4
+ compare: (element, expectedOpacity, tolerance = 0.0) ->
5
+ element = up.util.element(element)
6
+ actualOpacity = up.util.opacity(element)
7
+ result = {}
8
+ result.pass = Math.abs(expectedOpacity - actualOpacity) <= tolerance
9
+ unless result.pass
10
+ result.message = up.browser.sprintf("Expected %o to have opacity %o, but it was %o", element, expectedOpacity, actualOpacity)
11
+ return result
@@ -0,0 +1,5 @@
1
+ beforeEach ->
2
+ jasmine.addMatchers
3
+ toHaveOwnProperty: (util, customEqualityTesters) ->
4
+ compare: (object, expectedProperty) ->
5
+ pass: object.hasOwnProperty(expectedProperty)
@@ -48,39 +48,7 @@ describe 'up.dom', ->
48
48
  expect(resolution).toHaveBeenCalled()
49
49
  expect($('.middle')).toHaveText('new-middle')
50
50
 
51
- describe 'cleaning up', ->
52
-
53
- it 'calls destructors on the replaced element', asyncSpec (next) ->
54
- destructor = jasmine.createSpy('destructor')
55
- up.compiler '.container', -> destructor
56
- $container = affix('.container')
57
- up.hello($container)
58
- up.replace('.container', '/path')
59
-
60
- next =>
61
- @respondWith '<div class="container">new text</div>'
62
-
63
- next =>
64
- expect('.container').toHaveText('new text')
65
- expect(destructor).toHaveBeenCalled()
66
-
67
- it 'calls destructors when the replaced element is a singleton element like <body> (bugfix)', asyncSpec (next) ->
68
- # shouldSwapElementsDirectly() is true for body, but can't have the example replace the Jasmine test runner UI
69
- up.dom.knife.mock('shouldSwapElementsDirectly').and.callFake ($element) -> $element.is('.container')
70
- destructor = jasmine.createSpy('destructor')
71
- up.compiler '.container', -> destructor
72
- $container = affix('.container')
73
- up.hello($container)
74
- up.replace('.container', '/path')
75
-
76
- next =>
77
- @respondWith '<div class="container">new text</div>'
78
-
79
- next =>
80
- expect('.container').toHaveText('new text')
81
- expect(destructor).toHaveBeenCalled()
82
-
83
- describe 'transitions', ->
51
+ describe 'with { transition } option', ->
84
52
 
85
53
  it 'returns a promise that will be fulfilled once the server response was received and the swap transition has completed', asyncSpec (next) ->
86
54
  resolution = jasmine.createSpy()
@@ -99,21 +67,6 @@ describe 'up.dom', ->
99
67
  next.after 80, =>
100
68
  expect(resolution).toHaveBeenCalled()
101
69
 
102
- it 'ignores a { transition } option when replacing the body element', asyncSpec (next) ->
103
- up.dom.knife.mock('swapElementsDirectly') # can't have the example replace the Jasmine test runner UI
104
- up.dom.knife.mock('destroy') # if we don't swap the body, up.dom will destroy it
105
- replaceCallback = jasmine.createSpy()
106
- promise = up.replace('body', '/path', transition: 'cross-fade', duration: 50)
107
- promise.then(replaceCallback)
108
- expect(replaceCallback).not.toHaveBeenCalled()
109
-
110
- next =>
111
- @responseText = '<body>new text</body>'
112
- @respond()
113
-
114
- next =>
115
- expect(replaceCallback).toHaveBeenCalled()
116
-
117
70
  describe 'with { data } option', ->
118
71
 
119
72
  it "uses the given params as a non-GET request's payload", asyncSpec (next) ->
@@ -1438,15 +1391,6 @@ describe 'up.dom', ->
1438
1391
  expect(result.value).toMatch(/Could not find selector/i)
1439
1392
  done()
1440
1393
 
1441
- it "ignores an element that matches the selector but also matches .up-ghost", (done) ->
1442
- html = '<div class="foo-bar">text</div>'
1443
- affix('.foo-bar.up-ghost')
1444
- promise = up.extract('.foo-bar', html)
1445
- promiseState(promise).then (result) =>
1446
- expect(result.state).toEqual('rejected')
1447
- expect(result.value).toMatch(/Could not find selector/i)
1448
- done()
1449
-
1450
1394
  it "ignores an element that matches the selector but also has a parent matching .up-destroying", (done) ->
1451
1395
  html = '<div class="foo-bar">text</div>'
1452
1396
  $parent = affix('.up-destroying')
@@ -1457,16 +1401,6 @@ describe 'up.dom', ->
1457
1401
  expect(result.value).toMatch(/Could not find selector/i)
1458
1402
  done()
1459
1403
 
1460
- it "ignores an element that matches the selector but also has a parent matching .up-ghost", (done) ->
1461
- html = '<div class="foo-bar">text</div>'
1462
- $parent = affix('.up-ghost')
1463
- $child = affix('.foo-bar').appendTo($parent)
1464
- promise = up.extract('.foo-bar', html)
1465
- promiseState(promise).then (result) =>
1466
- expect(result.state).toEqual('rejected')
1467
- expect(result.value).toMatch(/Could not find selector/i)
1468
- done()
1469
-
1470
1404
  it 'only replaces the first element matching the selector', asyncSpec (next) ->
1471
1405
  html = '<div class="foo-bar">text</div>'
1472
1406
  affix('.foo-bar')
@@ -1474,86 +1408,209 @@ describe 'up.dom', ->
1474
1408
  up.extract('.foo-bar', html)
1475
1409
 
1476
1410
  next =>
1477
- elements = $('.foo-bar')
1478
- expect($(elements.get(0)).text()).toEqual('text')
1479
- expect($(elements.get(1)).text()).toEqual('')
1411
+ $elements = $('.foo-bar')
1412
+ expect($($elements.get(0)).text()).toEqual('text')
1413
+ expect($($elements.get(1)).text()).toEqual('')
1414
+
1415
+ it 'focuses an [autofocus] element in the new fragment', asyncSpec (next) ->
1416
+ affix('.foo-bar')
1417
+ up.extract '.foo-bar', """
1418
+ <form class='foo-bar'>
1419
+ <input class="autofocused-input" autofocus>
1420
+ </form>
1421
+ """
1422
+
1423
+ next =>
1424
+ input = $('.autofocused-input').get(0)
1425
+ expect(input).toBeGiven()
1426
+ expect(document.activeElement).toBe(input)
1427
+
1428
+ describe 'cleaning up', ->
1429
+
1430
+ it 'calls destructors on the old element', asyncSpec (next) ->
1431
+ destructor = jasmine.createSpy('destructor')
1432
+ up.compiler '.container', ($element) ->
1433
+ -> destructor($element.text())
1434
+ $container = affix('.container').text('old text')
1435
+ up.hello($container)
1436
+ up.extract('.container', '<div class="container">new text</div>')
1437
+
1438
+ next =>
1439
+ expect('.container').toHaveText('new text')
1440
+ expect(destructor).toHaveBeenCalledWith('old text')
1441
+
1442
+ it 'calls destructors on the old element after a { transition }', (done) ->
1443
+ destructor = jasmine.createSpy('destructor')
1444
+ up.compiler '.container', ($element) ->
1445
+ -> destructor($element.text())
1446
+ $container = affix('.container').text('old text')
1447
+ up.hello($container)
1448
+
1449
+ up.extract('.container', '<div class="container">new text</div>', transition: 'cross-fade', duration: 100)
1450
+
1451
+ u.setTimer 50, =>
1452
+ expect(destructor).not.toHaveBeenCalled()
1453
+
1454
+ u.setTimer 220, =>
1455
+ expect('.container').toHaveText('new text')
1456
+ expect(destructor).toHaveBeenCalledWith('old text')
1457
+ done()
1458
+
1459
+ it 'calls destructors when the replaced element is a singleton element like <body> (bugfix)', asyncSpec (next) ->
1460
+ # shouldSwapElementsDirectly() is true for body, but can't have the example replace the Jasmine test runner UI
1461
+ up.motion.knife.mock('isSingletonElement').and.callFake ($element) -> $element.is('.container')
1462
+ destructor = jasmine.createSpy('destructor')
1463
+ up.compiler '.container', -> destructor
1464
+ $container = affix('.container')
1465
+ up.hello($container)
1466
+ up.extract('.container', '<div class="container">new text</div>')
1467
+
1468
+ next =>
1469
+ expect('.container').toHaveText('new text')
1470
+ expect(destructor).toHaveBeenCalled()
1471
+
1472
+ it 'calls destructors while the element is still attached to the DOM, so destructors see ancestry and events bubble up', asyncSpec (next) ->
1473
+ spy = jasmine.createSpy('parent spy')
1474
+ up.compiler '.element', ($element) ->
1475
+ return -> spy($element.text(), $element.parent())
1476
+
1477
+ $parent = affix('.parent')
1478
+ $element = $parent.affix('.element').text('old text')
1479
+ up.hello($element)
1480
+
1481
+ up.extract '.element', '<div class="element">new text</div>'
1482
+
1483
+ next =>
1484
+ expect(spy).toHaveBeenCalledWith('old text', $parent)
1485
+
1486
+ it 'calls destructors while the element is still attached to the DOM when also using a { transition }', (done) ->
1487
+ spy = jasmine.createSpy('parent spy')
1488
+ up.compiler '.element', ($element) ->
1489
+ return ->
1490
+ # We must seek .parent in our ancestry, because our direct parent() is an .up-bounds container
1491
+ spy($element.text(), $element.closest('.parent'))
1492
+
1493
+ $parent = affix('.parent')
1494
+ $element = $parent.affix('.element').text('old text')
1495
+ up.hello($element)
1496
+
1497
+ extractDone = up.extract('.element', '<div class="element">new text</div>', transition: 'cross-fade', duration: 30)
1498
+
1499
+ extractDone.then ->
1500
+ expect(spy).toHaveBeenCalledWith('old text', $parent)
1501
+ done()
1480
1502
 
1481
1503
  describe 'with { transition } option', ->
1482
1504
 
1483
1505
  it 'morphs between the old and new element', asyncSpec (next) ->
1484
- affix('.element').text('version 1')
1485
- up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
1506
+ affix('.element.v1').text('version 1')
1507
+ up.extract('.element', '<div class="element v2">version 2</div>', transition: 'cross-fade', duration: 100, easing: 'linear')
1508
+
1509
+ $old = undefined
1510
+ $new = undefined
1486
1511
 
1487
1512
  next =>
1488
- @$ghost1 = $('.element.up-ghost:contains("version 1")')
1489
- expect(@$ghost1).toHaveLength(1)
1490
- expect(u.opacity(@$ghost1)).toBeAround(1.0, 0.1)
1513
+ $old = $('.element.v1')
1514
+ $new = $('.element.v2')
1491
1515
 
1492
- @$ghost2 = $('.element.up-ghost:contains("version 2")')
1493
- expect(@$ghost2).toHaveLength(1)
1494
- expect(u.opacity(@$ghost2)).toBeAround(0.0, 0.1)
1516
+ expect($old).toHaveLength(1)
1517
+ expect(u.opacity($old)).toBeAround(1.0, 0.2)
1495
1518
 
1496
- next.after 190, =>
1497
- expect(u.opacity(@$ghost1)).toBeAround(0.0, 0.3)
1498
- expect(u.opacity(@$ghost2)).toBeAround(1.0, 0.3)
1519
+ expect($new).toHaveLength(1)
1520
+ expect(u.opacity($new)).toBeAround(0.0, 0.2)
1499
1521
 
1500
- it 'marks the old fragment and its ghost as .up-destroying during the transition', asyncSpec (next) ->
1522
+ next.after 50, =>
1523
+ expect(u.opacity($old)).toBeAround(0.5, 0.2)
1524
+ expect(u.opacity($new)).toBeAround(0.5, 0.2)
1525
+
1526
+ next.after 60, =>
1527
+ expect(u.opacity($new)).toBeAround(1.0, 0.1)
1528
+ expect($old).toBeDetached()
1529
+
1530
+
1531
+ it 'ignores a { transition } option when replacing a singleton element like <body>', asyncSpec (next) ->
1532
+ # shouldSwapElementsDirectly() is true for body, but can't have the example replace the Jasmine test runner UI
1533
+ up.motion.knife.mock('isSingletonElement').and.callFake ($element) -> $element.is('.container')
1534
+
1535
+ affix('.container').text('old text')
1536
+
1537
+ extractDone = jasmine.createSpy()
1538
+ promise = up.extract('.container', '<div class="container">new text</div>', transition: 'cross-fade', duration: 200)
1539
+ promise.then(extractDone)
1540
+
1541
+ next =>
1542
+ # See that we've already immediately swapped the element and ignored the duration of 200ms
1543
+ expect(extractDone).toHaveBeenCalled()
1544
+ expect($('.container').length).toEqual(1)
1545
+ expect(u.opacity($('.container'))).toEqual(1.0)
1546
+
1547
+
1548
+ it 'marks the old fragment as .up-destroying during the transition', asyncSpec (next) ->
1501
1549
  affix('.element').text('version 1')
1502
1550
  up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
1503
1551
 
1504
1552
  next =>
1505
- $version1 = $('.element:not(.up-ghost):contains("version 1")')
1506
- $version1Ghost = $('.element.up-ghost:contains("version 1")')
1553
+ $version1 = $('.element:contains("version 1")')
1507
1554
  expect($version1).toHaveLength(1)
1508
- expect($version1Ghost).toHaveLength(1)
1509
1555
  expect($version1).toHaveClass('up-destroying')
1510
- expect($version1Ghost).toHaveClass('up-destroying')
1511
1556
 
1512
- $version2 = $('.element:not(.up-ghost):contains("version 2")')
1513
- $version2Ghost = $('.element.up-ghost:contains("version 2")')
1557
+ $version2 = $('.element:contains("version 2")')
1514
1558
  expect($version2).toHaveLength(1)
1515
- expect($version2Ghost).toHaveLength(1)
1516
1559
  expect($version2).not.toHaveClass('up-destroying')
1517
- expect($version2Ghost).not.toHaveClass('up-destroying')
1518
1560
 
1519
1561
  it 'cancels an existing transition by instantly jumping to the last frame', asyncSpec (next) ->
1520
- affix('.element').text('version 1')
1521
- up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
1562
+ affix('.element.v1').text('version 1')
1563
+
1564
+ up.extract('.element', '<div class="element v2">version 2</div>', transition: 'cross-fade', duration: 200)
1522
1565
 
1523
1566
  next =>
1524
- $ghost1 = $('.element.up-ghost:contains("version 1")')
1567
+ $ghost1 = $('.element:contains("version 1")')
1525
1568
  expect($ghost1).toHaveLength(1)
1526
1569
  expect($ghost1.css('opacity')).toBeAround(1.0, 0.1)
1527
1570
 
1528
- $ghost2 = $('.element.up-ghost:contains("version 2")')
1571
+ $ghost2 = $('.element:contains("version 2")')
1529
1572
  expect($ghost2).toHaveLength(1)
1530
1573
  expect($ghost2.css('opacity')).toBeAround(0.0, 0.1)
1531
1574
 
1532
1575
  next =>
1533
- up.extract('.element', '<div class="element">version 3</div>', transition: 'cross-fade', duration: 200)
1576
+ up.extract('.element', '<div class="element v3">version 3</div>', transition: 'cross-fade', duration: 200)
1534
1577
 
1535
1578
  next =>
1536
- $ghost1 = $('.element.up-ghost:contains("version 1")')
1579
+ $ghost1 = $('.element:contains("version 1")')
1537
1580
  expect($ghost1).toHaveLength(0)
1538
1581
 
1539
- $ghost2 = $('.element.up-ghost:contains("version 2")')
1582
+ $ghost2 = $('.element:contains("version 2")')
1540
1583
  expect($ghost2).toHaveLength(1)
1541
1584
  expect($ghost2.css('opacity')).toBeAround(1.0, 0.1)
1542
1585
 
1543
- $ghost3 = $('.element.up-ghost:contains("version 3")')
1586
+ $ghost3 = $('.element:contains("version 3")')
1544
1587
  expect($ghost3).toHaveLength(1)
1545
1588
  expect($ghost3.css('opacity')).toBeAround(0.0, 0.1)
1546
1589
 
1547
1590
  it 'delays the resolution of the returned promise until the transition is over', (done) ->
1548
1591
  affix('.element').text('version 1')
1549
1592
  resolution = jasmine.createSpy()
1550
- promise = up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 30)
1593
+ promise = up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 60)
1551
1594
  promise.then(resolution)
1552
1595
  expect(resolution).not.toHaveBeenCalled()
1553
- u.setTimer 80, ->
1596
+
1597
+ u.setTimer 40, ->
1598
+ expect(resolution).not.toHaveBeenCalled()
1599
+
1600
+ u.setTimer 200, ->
1554
1601
  expect(resolution).toHaveBeenCalled()
1555
1602
  done()
1556
1603
 
1604
+ it 'attaches the new element to the DOM before compilers are called, so they can see their parents and trigger bubbling events', asyncSpec (next)->
1605
+ $parent = affix('.parent')
1606
+ $element = $parent.affix('.element').text('old text')
1607
+ spy = jasmine.createSpy('parent spy')
1608
+ up.compiler '.element', ($element) -> spy($element.text(), $element.parent())
1609
+ up.extract '.element', '<div class="element">new text</div>', transition: 'cross-fade', duration: 50
1610
+
1611
+ next =>
1612
+ expect(spy).toHaveBeenCalledWith('new text', $parent)
1613
+
1557
1614
  describe 'when animation is disabled', ->
1558
1615
 
1559
1616
  beforeEach ->
@@ -1567,7 +1624,7 @@ describe 'up.dom', ->
1567
1624
  expect($('.up-ghost')).toHaveLength(0)
1568
1625
 
1569
1626
  it "replaces the elements directly, since first inserting and then removing would shift scroll positions", asyncSpec (next) ->
1570
- swapDirectlySpy = up.dom.knife.mock('swapElementsDirectly')
1627
+ swapDirectlySpy = up.motion.knife.mock('swapElementsDirectly')
1571
1628
  affix('.element').text('version 1')
1572
1629
  up.extract('.element', '<div class="element">version 2</div>', transition: false)
1573
1630
 
@@ -1896,7 +1953,7 @@ describe 'up.dom', ->
1896
1953
  expect(insertedListener).toHaveBeenCalledWith(jasmine.anything(), $('.container'), jasmine.anything())
1897
1954
  expect(keptListener).toHaveBeenCalledWith(jasmine.anything(), $('.container .keeper'), jasmine.anything())
1898
1955
 
1899
- it 'emits an up:fragment:kept event on a kept element with a newData property corresponding to the up-data attribute value of the discarded element', (next) ->
1956
+ it 'emits an up:fragment:kept event on a kept element with a newData property corresponding to the up-data attribute value of the discarded element', asyncSpec (next) ->
1900
1957
  keptListener = jasmine.createSpy()
1901
1958
  up.on 'up:fragment:kept', (event) -> keptListener(event.$element, event.newData)
1902
1959
  $container = affix('.container')
@@ -2009,6 +2066,64 @@ describe 'up.dom', ->
2009
2066
  expect(document.title).toEqual('Title from options')
2010
2067
  done()
2011
2068
 
2069
+ it 'runs an animation before removal with { animate } option', asyncSpec (next) ->
2070
+ $element = affix('.element')
2071
+ up.destroy($element, animation: 'fade-out', duration: 150, easing: 'linear')
2072
+
2073
+ next ->
2074
+ expect($element).toHaveOpacity(1.0, 0.2)
2075
+
2076
+ next.after 75, ->
2077
+ expect($element).toHaveOpacity(0.5, 0.2)
2078
+
2079
+ next.after (75 + 20), ->
2080
+ expect($element).toBeDetached()
2081
+
2082
+ it 'marks the element as .up-destroying while it is animating', asyncSpec (next) ->
2083
+ $element = affix('.element')
2084
+ up.destroy($element, animation: 'fade-out', duration: 80, easing: 'linear')
2085
+
2086
+ next ->
2087
+ expect($element).toHaveClass('up-destroying')
2088
+
2089
+ it 'emits an up:fragment:destroy event while the element is still in the DOM', asyncSpec (next) ->
2090
+ $element = affix('.element')
2091
+ expect($element).toBeAttached()
2092
+
2093
+ listener = jasmine.createSpy('event listener')
2094
+ $element.on('up:fragment:destroy', listener)
2095
+
2096
+ destroyDone = up.destroy($element, animation: 'fade-out', duration: 30)
2097
+
2098
+ next ->
2099
+ expect(listener).toHaveBeenCalledWith(jasmine.objectContaining($element: $element))
2100
+ expect($element).toBeAttached()
2101
+
2102
+ next.await(destroyDone)
2103
+
2104
+ next ->
2105
+ expect($element).toBeDetached()
2106
+
2107
+ it 'emits an up:fragment:destroyed event on the former parent element after the element has been removed from the DOM', asyncSpec (next) ->
2108
+ $parent = affix('.parent')
2109
+ $element = $parent.affix('.element')
2110
+ expect($element).toBeAttached()
2111
+
2112
+ listener = jasmine.createSpy('event listener')
2113
+
2114
+ $parent.on('up:fragment:destroyed', listener)
2115
+
2116
+ destroyDone = up.destroy($element, animation: 'fade-out', duration: 30)
2117
+
2118
+ next ->
2119
+ expect(listener).not.toHaveBeenCalled()
2120
+ expect($element).toBeAttached()
2121
+
2122
+ next.await(destroyDone)
2123
+
2124
+ next ->
2125
+ expect(listener).toHaveBeenCalledWith(jasmine.objectContaining($element: $element, $parent: $parent))
2126
+ expect($element).toBeDetached()
2012
2127
 
2013
2128
  describe 'up.reload', ->
2014
2129