mercury-rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (153) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +152 -0
  3. data/VERSION +1 -0
  4. data/app/assets/images/mercury/button.png +0 -0
  5. data/app/assets/images/mercury/clippy.png +0 -0
  6. data/app/assets/images/mercury/default-snippet.png +0 -0
  7. data/app/assets/images/mercury/loading-dark.gif +0 -0
  8. data/app/assets/images/mercury/loading-light.gif +0 -0
  9. data/app/assets/images/mercury/search-icon.png +0 -0
  10. data/app/assets/images/mercury/toolbar/editable/buttons.png +0 -0
  11. data/app/assets/images/mercury/toolbar/markupable/buttons.png +0 -0
  12. data/app/assets/images/mercury/toolbar/primary/_expander.png +0 -0
  13. data/app/assets/images/mercury/toolbar/primary/_pressed.png +0 -0
  14. data/app/assets/images/mercury/toolbar/primary/historypanel.png +0 -0
  15. data/app/assets/images/mercury/toolbar/primary/insertcharacter.png +0 -0
  16. data/app/assets/images/mercury/toolbar/primary/insertlink.png +0 -0
  17. data/app/assets/images/mercury/toolbar/primary/insertmedia.png +0 -0
  18. data/app/assets/images/mercury/toolbar/primary/inserttable.png +0 -0
  19. data/app/assets/images/mercury/toolbar/primary/inspectorpanel.png +0 -0
  20. data/app/assets/images/mercury/toolbar/primary/notespanel.png +0 -0
  21. data/app/assets/images/mercury/toolbar/primary/objectspanel.png +0 -0
  22. data/app/assets/images/mercury/toolbar/primary/preview.png +0 -0
  23. data/app/assets/images/mercury/toolbar/primary/redo.png +0 -0
  24. data/app/assets/images/mercury/toolbar/primary/save.png +0 -0
  25. data/app/assets/images/mercury/toolbar/primary/todospanel.png +0 -0
  26. data/app/assets/images/mercury/toolbar/primary/undo.png +0 -0
  27. data/app/assets/images/mercury/toolbar/snippetable/buttons.png +0 -0
  28. data/app/assets/javascripts/mercury.js +30 -0
  29. data/app/assets/javascripts/mercury/dialog.js.coffee +75 -0
  30. data/app/assets/javascripts/mercury/dialogs/backcolor.js.coffee +6 -0
  31. data/app/assets/javascripts/mercury/dialogs/forecolor.js.coffee +6 -0
  32. data/app/assets/javascripts/mercury/dialogs/formatblock.js.coffee +4 -0
  33. data/app/assets/javascripts/mercury/dialogs/objectspanel.js.coffee +10 -0
  34. data/app/assets/javascripts/mercury/dialogs/style.js.coffee +4 -0
  35. data/app/assets/javascripts/mercury/history_buffer.js.coffee +30 -0
  36. data/app/assets/javascripts/mercury/mercury.js.coffee +293 -0
  37. data/app/assets/javascripts/mercury/modal.js.coffee +177 -0
  38. data/app/assets/javascripts/mercury/modals/htmleditor.js.coffee +10 -0
  39. data/app/assets/javascripts/mercury/modals/insertcharacter.js.coffee +4 -0
  40. data/app/assets/javascripts/mercury/modals/insertlink.js.coffee +92 -0
  41. data/app/assets/javascripts/mercury/modals/insertmedia.js.coffee +72 -0
  42. data/app/assets/javascripts/mercury/modals/insertsnippet.js.coffee +11 -0
  43. data/app/assets/javascripts/mercury/modals/inserttable.js.coffee +56 -0
  44. data/app/assets/javascripts/mercury/native_extensions.js.coffee +47 -0
  45. data/app/assets/javascripts/mercury/page_editor.js.coffee +139 -0
  46. data/app/assets/javascripts/mercury/palette.js.coffee +29 -0
  47. data/app/assets/javascripts/mercury/panel.js.coffee +97 -0
  48. data/app/assets/javascripts/mercury/region.js.coffee +103 -0
  49. data/app/assets/javascripts/mercury/regions/editable.js.coffee +546 -0
  50. data/app/assets/javascripts/mercury/regions/markupable.js.coffee +380 -0
  51. data/app/assets/javascripts/mercury/regions/snippetable.js.coffee +127 -0
  52. data/app/assets/javascripts/mercury/select.js.coffee +40 -0
  53. data/app/assets/javascripts/mercury/snippet.js.coffee +92 -0
  54. data/app/assets/javascripts/mercury/snippet_toolbar.js.coffee +69 -0
  55. data/app/assets/javascripts/mercury/statusbar.js.coffee +25 -0
  56. data/app/assets/javascripts/mercury/table_editor.js.coffee +266 -0
  57. data/app/assets/javascripts/mercury/toolbar.button.js.coffee +152 -0
  58. data/app/assets/javascripts/mercury/toolbar.button_group.js.coffee +42 -0
  59. data/app/assets/javascripts/mercury/toolbar.expander.js.coffee +56 -0
  60. data/app/assets/javascripts/mercury/toolbar.js.coffee +72 -0
  61. data/app/assets/javascripts/mercury/tooltip.js.coffee +67 -0
  62. data/app/assets/javascripts/mercury/uploader.js.coffee +213 -0
  63. data/app/assets/javascripts/mercury/websocket.js.coffee +34 -0
  64. data/app/assets/stylesheets/mercury.css +31 -0
  65. data/app/assets/stylesheets/mercury/dialog.scss +178 -0
  66. data/app/assets/stylesheets/mercury/mercury.scss +119 -0
  67. data/app/assets/stylesheets/mercury/modal.scss +192 -0
  68. data/app/assets/stylesheets/mercury/statusbar.scss +23 -0
  69. data/app/assets/stylesheets/mercury/toolbar.scss +417 -0
  70. data/app/assets/stylesheets/mercury/tooltip.scss +26 -0
  71. data/app/assets/stylesheets/mercury/uploader.scss +109 -0
  72. data/app/controllers/images_controller.rb +19 -0
  73. data/app/controllers/mercury_controller.rb +20 -0
  74. data/app/models/image.rb +14 -0
  75. data/app/views/layouts/mercury.html.haml +12 -0
  76. data/app/views/mercury/modals/character.html.haml +252 -0
  77. data/app/views/mercury/modals/htmleditor.html.haml +8 -0
  78. data/app/views/mercury/modals/link.html.haml +31 -0
  79. data/app/views/mercury/modals/media.html.haml +33 -0
  80. data/app/views/mercury/modals/sanitizer.html.haml +4 -0
  81. data/app/views/mercury/modals/table.html.haml +49 -0
  82. data/app/views/mercury/palettes/backcolor.html.haml +79 -0
  83. data/app/views/mercury/palettes/forecolor.html.haml +79 -0
  84. data/app/views/mercury/panels/history.html.haml +0 -0
  85. data/app/views/mercury/panels/notes.html.haml +0 -0
  86. data/app/views/mercury/panels/snippets.html.haml +10 -0
  87. data/app/views/mercury/selects/formatblock.html.haml +10 -0
  88. data/app/views/mercury/selects/style.html.haml +4 -0
  89. data/app/views/mercury/snippets/example.html.haml +2 -0
  90. data/app/views/mercury/snippets/example_options.html.haml +16 -0
  91. data/config/engine.rb +6 -0
  92. data/config/routes.rb +15 -0
  93. data/db/migrate/20110526035601_create_images.rb +11 -0
  94. data/features/editing/basic.feature +11 -0
  95. data/features/step_definitions/debug_steps.rb +14 -0
  96. data/features/step_definitions/web_steps.rb +211 -0
  97. data/features/support/env.rb +46 -0
  98. data/features/support/paths.rb +35 -0
  99. data/features/support/selectors.rb +42 -0
  100. data/lib/mercury-rails.rb +4 -0
  101. data/log/.gitkeep +0 -0
  102. data/mercury-rails.gemspec +230 -0
  103. data/spec/javascripts/mercury/dialog_spec.js.coffee +258 -0
  104. data/spec/javascripts/mercury/history_buffer_spec.js.coffee +79 -0
  105. data/spec/javascripts/mercury/mercury_spec.js.coffee +52 -0
  106. data/spec/javascripts/mercury/native_extensions_spec.js.coffee +66 -0
  107. data/spec/javascripts/mercury/page_editor_spec.js.coffee +435 -0
  108. data/spec/javascripts/mercury/palette_spec.js.coffee +51 -0
  109. data/spec/javascripts/mercury/panel_spec.js.coffee +147 -0
  110. data/spec/javascripts/mercury/region_spec.js.coffee +261 -0
  111. data/spec/javascripts/mercury/regions/_editable_.js.coffee +0 -0
  112. data/spec/javascripts/mercury/regions/_markupable_.js.coffee +0 -0
  113. data/spec/javascripts/mercury/regions/snippetable_spec.js.coffee +368 -0
  114. data/spec/javascripts/mercury/select_spec.js.coffee +51 -0
  115. data/spec/javascripts/mercury/snippet_spec.js.coffee +246 -0
  116. data/spec/javascripts/mercury/snippet_toolbar_spec.js.coffee +186 -0
  117. data/spec/javascripts/mercury/statusbar_spec.js.coffee +78 -0
  118. data/spec/javascripts/mercury/table_editor_spec.js.coffee +192 -0
  119. data/spec/javascripts/mercury/toolbar.button_group_spec.js.coffee +92 -0
  120. data/spec/javascripts/mercury/toolbar.button_spec.js.coffee +341 -0
  121. data/spec/javascripts/mercury/toolbar.expander_spec.js.coffee +120 -0
  122. data/spec/javascripts/mercury/toolbar_spec.js.coffee +152 -0
  123. data/spec/javascripts/mercury/tooltip_spec.js.coffee +188 -0
  124. data/spec/javascripts/mercury/uploader_spec.js.coffee +512 -0
  125. data/spec/javascripts/responses/blank.html +1 -0
  126. data/spec/javascripts/spec_helper.js +513 -0
  127. data/spec/javascripts/templates/mercury/dialog.html +2 -0
  128. data/spec/javascripts/templates/mercury/page_editor.html +24 -0
  129. data/spec/javascripts/templates/mercury/palette.html +16 -0
  130. data/spec/javascripts/templates/mercury/panel.html +16 -0
  131. data/spec/javascripts/templates/mercury/region.html +2 -0
  132. data/spec/javascripts/templates/mercury/regions/snippetable.html +4 -0
  133. data/spec/javascripts/templates/mercury/select.html +16 -0
  134. data/spec/javascripts/templates/mercury/snippet.html +1 -0
  135. data/spec/javascripts/templates/mercury/snippet_toolbar.html +16 -0
  136. data/spec/javascripts/templates/mercury/statusbar.html +7 -0
  137. data/spec/javascripts/templates/mercury/table_editor.html +65 -0
  138. data/spec/javascripts/templates/mercury/toolbar.button.html +64 -0
  139. data/spec/javascripts/templates/mercury/toolbar.button_group.html +9 -0
  140. data/spec/javascripts/templates/mercury/toolbar.expander.html +18 -0
  141. data/spec/javascripts/templates/mercury/toolbar.html +10 -0
  142. data/spec/javascripts/templates/mercury/tooltip.html +12 -0
  143. data/spec/javascripts/templates/mercury/uploader.html +11 -0
  144. data/vendor/assets/javascripts/jquery-1.6.js +8865 -0
  145. data/vendor/assets/javascripts/jquery-ui-1.8.13.custom.min.js +249 -0
  146. data/vendor/assets/javascripts/jquery-ui-1.8.13.sortable.custom.js +1078 -0
  147. data/vendor/assets/javascripts/jquery.easing.js +173 -0
  148. data/vendor/assets/javascripts/jquery.json2.js +178 -0
  149. data/vendor/assets/javascripts/jquery.serialize_object.js +16 -0
  150. data/vendor/assets/javascripts/jquery.ujs.js +289 -0
  151. data/vendor/assets/javascripts/liquidmetal.js +88 -0
  152. data/vendor/assets/javascripts/showdown.js +1362 -0
  153. metadata +364 -0
@@ -0,0 +1,152 @@
1
+ require '/assets/mercury/mercury.js'
2
+
3
+ describe "Mercury.Toolbar", ->
4
+
5
+ template 'mercury/toolbar.html'
6
+
7
+ beforeEach ->
8
+ ajaxSpy = spyOn($, 'ajax').andCallFake (url, options) =>
9
+ options.success('data') if options.success
10
+
11
+ afterEach ->
12
+ @toolbar = null
13
+ delete(@toolbar)
14
+
15
+ describe "constructor", ->
16
+
17
+ beforeEach ->
18
+ @buildSpy = spyOn(Mercury.Toolbar.prototype, 'build').andCallFake(=>)
19
+ @bindEventsSpy = spyOn(Mercury.Toolbar.prototype, 'bindEvents').andCallFake(=>)
20
+ @toolbar = new Mercury.Toolbar({appendTo: '#test', foo: true})
21
+
22
+ it "accepts options as an argument", ->
23
+ expect(@toolbar.options).toEqual({appendTo: '#test', foo: true})
24
+
25
+ it "calls build", ->
26
+ expect(@buildSpy.callCount).toEqual(1)
27
+
28
+ it "calls bindEvents", ->
29
+ expect(@bindEventsSpy.callCount).toEqual(1)
30
+
31
+
32
+ describe "#height", ->
33
+
34
+ beforeEach ->
35
+ spyOn(Mercury.Toolbar.prototype, 'buildButton').andCallFake(=> $('<div>'))
36
+ spyOn(Mercury.Toolbar.prototype, 'bindEvents').andCallFake(=>)
37
+ @toolbar = new Mercury.Toolbar({appendTo: '#test'})
38
+
39
+ it "knows it's own height", ->
40
+ expect(@toolbar.height()).toEqual(200) # styled with css in the template
41
+
42
+
43
+ describe "#build", ->
44
+
45
+ beforeEach ->
46
+ @buildButtonSpy = spyOn(Mercury.Toolbar.prototype, 'buildButton').andCallFake(=> $('<div>'))
47
+ @toolbar = new Mercury.Toolbar({appendTo: '#toolbar_container'})
48
+
49
+ it "builds an element", ->
50
+ expect($('.mercury-toolbar-container').length).toEqual(1)
51
+
52
+ it "can append to any element", ->
53
+ expect($('#toolbar_container .mercury-toolbar-container').length).toEqual(1)
54
+
55
+ it "builds out toolbar elements from the configuration", ->
56
+ expect($('.mercury-primary-toolbar').length).toEqual(1)
57
+ expect($('.mercury-editable-toolbar').length).toEqual(1)
58
+ expect($('.mercury-editable-toolbar').data('regions')).toEqual('editable,markupable')
59
+
60
+ it "builds buttons etc.", ->
61
+ expect(@buildButtonSpy.callCount).toBeGreaterThan(10)
62
+
63
+ it "sets it's width back to 100%", ->
64
+ expect(@toolbar.element.get(0).style.width).toEqual('100%')
65
+
66
+ it "disables all but the primary toolbar", ->
67
+ expect($('.mercury-editable-toolbar').hasClass('disabled')).toEqual(true)
68
+
69
+ it "adds an expander when white-space: nowrap (meaning the toolbar shouldn't wrap)", ->
70
+ expect($('.mercury-toolbar-button-container').length).toBeGreaterThan(1)
71
+ expect($('.mercury-toolbar-expander').length).toEqual(1)
72
+
73
+
74
+ describe "#buildButton", ->
75
+
76
+ beforeEach ->
77
+ @toolbar = new Mercury.Toolbar({appendTo: '#test'})
78
+
79
+ it "throws an exception when invalid options are passed", ->
80
+ expect(=> @toolbar.buildButton('foo', false)).toThrow('Unknown button structure -- please provide an array, object, or string for foo.')
81
+
82
+ it "returns false if the name is _custom, or _regions", ->
83
+ expect(@toolbar.buildButton('_custom', 'foo')).toEqual(false)
84
+ expect(@toolbar.buildButton('_regions', ['regiontype', 'another_regiontype'])).toEqual(false)
85
+
86
+ it "builds buttons", ->
87
+ html = $('<div>').html(@toolbar.buildButton('foobutton', ['title', 'summary', {}])).html()
88
+ expect(html).toContain('title="summary"')
89
+ expect(html).toContain('class="mercury-button mercury-foobutton-button"')
90
+ expect(html).toContain('<em>title</em>')
91
+
92
+ it "builds button groups", ->
93
+ html = $('<div>').html(@toolbar.buildButton('foogroup', {foobutton: ['title', 'summary', {}]})).html()
94
+ expect(html).toContain('class="mercury-button-group mercury-foogroup-group"')
95
+ expect(html).toContain('title="summary"')
96
+ expect(html).toContain('class="mercury-button mercury-foobutton-button"')
97
+ expect(html).toContain('<em>title</em>')
98
+
99
+ it "builds separators", ->
100
+ html = $('<div>').html(@toolbar.buildButton('foosep1', ' ')).html()
101
+ expect(html).toEqual('<hr class="mercury-separator">')
102
+
103
+ html = $('<div>').html(@toolbar.buildButton('foosep1', '-')).html()
104
+ expect(html).toEqual('<hr class="mercury-line-separator">')
105
+
106
+ it "builds buttons from configuration", ->
107
+ expect($('.mercury-primary-toolbar .mercury-save-button').length).toEqual(1)
108
+ expect($('.mercury-primary-toolbar .mercury-preview-button').length).toEqual(1)
109
+
110
+ it "builds button groups from configuration", ->
111
+ expect($('.mercury-editable-toolbar .mercury-decoration-group').length).toEqual(1)
112
+ expect($('.mercury-editable-toolbar .mercury-script-group').length).toEqual(1)
113
+
114
+ it "builds separators from configuration", ->
115
+ expect($('.mercury-separator').length).toBeGreaterThan(1);
116
+ expect($('.mercury-line-separator').length).toBeGreaterThan(1);
117
+
118
+
119
+ describe "observed events", ->
120
+
121
+ beforeEach ->
122
+ @toolbar = new Mercury.Toolbar({appendTo: '#test'})
123
+
124
+ describe "custom event: region:focused", ->
125
+
126
+ it "enables toolbars based on the region type", ->
127
+ $('.mercury-editable-toolbar').addClass('disabled')
128
+ Mercury.trigger('region:focused', {region: {type: 'editable'}})
129
+ expect($('.mercury.editable-toolbar').hasClass('disabled')).toEqual(false)
130
+
131
+ $('.mercury-editable-toolbar').addClass('disabled')
132
+ Mercury.trigger('region:focused', {region: {type: 'markupable'}})
133
+ expect($('.mercury.editable-toolbar').hasClass('disabled')).toEqual(false)
134
+
135
+
136
+ describe "custom event: region:blurred", ->
137
+
138
+ it "disables toolbars for the region type", ->
139
+ $('.mercury-editable-toolbar').removeClass('disabled')
140
+ Mercury.trigger('region:blurred', {region: {type: 'editable'}})
141
+ expect($('.mercury-editable-toolbar').hasClass('disabled')).toEqual(true)
142
+
143
+
144
+ describe "click", ->
145
+
146
+ it "triggers hide:dialogs", ->
147
+ spy = spyOn(Mercury, 'trigger')
148
+
149
+ jasmine.simulate.click(@toolbar.element.get(0))
150
+
151
+ expect(spy.callCount).toEqual(1)
152
+ expect(spy.argsForCall[0]).toEqual(['hide:dialogs'])
@@ -0,0 +1,188 @@
1
+ require '/assets/mercury/mercury.js'
2
+
3
+ describe "Mercury.tooltip", ->
4
+
5
+ template 'mercury/tooltip.html'
6
+
7
+ beforeEach ->
8
+ @forElement = $('#for_element')
9
+ $.fx.off = true
10
+
11
+ afterEach ->
12
+ Mercury.tooltip.visible = false
13
+ Mercury.tooltip.initialized = false
14
+
15
+ describe "singleton method", ->
16
+
17
+ beforeEach ->
18
+ @showSpy = spyOn(Mercury.tooltip, 'show').andCallFake(=>)
19
+
20
+ it "calls show", ->
21
+ Mercury.tooltip()
22
+ expect(@showSpy.callCount).toEqual(1)
23
+
24
+ it "returns the function object", ->
25
+ ret = Mercury.tooltip()
26
+ expect(ret).toEqual(Mercury.tooltip)
27
+
28
+
29
+ describe "#show", ->
30
+
31
+ beforeEach ->
32
+ @initializeSpy = spyOn(Mercury.tooltip, 'initialize').andCallFake(=>)
33
+ @updateSpy = spyOn(Mercury.tooltip, 'update').andCallFake(=>)
34
+ @appearSpy = spyOn(Mercury.tooltip, 'appear').andCallFake(=>)
35
+
36
+ it "gets the document from the element passed in", ->
37
+ Mercury.tooltip.show(@forElement, 'content')
38
+ expect(Mercury.tooltip.document.get(0)).toEqual($(document).get(0))
39
+
40
+ it "calls initialize", ->
41
+ Mercury.tooltip.show(@forElement, 'content')
42
+ expect(@initializeSpy.callCount).toEqual(1)
43
+
44
+ describe "if visible", ->
45
+
46
+ beforeEach ->
47
+ Mercury.tooltip.visible = true
48
+
49
+ it "calls update", ->
50
+ Mercury.tooltip.show(@forElement, 'content')
51
+ expect(@updateSpy.callCount).toEqual(1)
52
+
53
+ describe "if not visible", ->
54
+
55
+ beforeEach ->
56
+ Mercury.tooltip.visible = false
57
+
58
+ it "calls appear", ->
59
+ Mercury.tooltip.show(@forElement, 'content')
60
+ expect(@appearSpy.callCount).toEqual(1)
61
+
62
+
63
+ describe "#initialize", ->
64
+
65
+ it "calls build", ->
66
+ spy = spyOn(Mercury.tooltip, 'build').andCallFake(=>)
67
+ spyOn(Mercury.tooltip, 'bindEvents').andCallFake(=>)
68
+ Mercury.tooltip.initialize()
69
+ expect(spy.callCount).toEqual(1)
70
+
71
+ it "calls bindEvents", ->
72
+ spy = spyOn(Mercury.tooltip, 'bindEvents').andCallFake(=>)
73
+ Mercury.tooltip.initialize()
74
+ expect(spy.callCount).toEqual(1)
75
+
76
+ it "sets initialized to true", ->
77
+ Mercury.tooltip.initialize()
78
+ expect(Mercury.tooltip.initialized).toEqual(true)
79
+
80
+ it "does nothing if already initialized", ->
81
+ spy = spyOn(Mercury.tooltip, 'bindEvents').andCallFake(=>)
82
+ Mercury.tooltip.initialized = true
83
+ Mercury.tooltip.initialize()
84
+ expect(spy.callCount).toEqual(0)
85
+
86
+
87
+ describe "#build", ->
88
+
89
+ it "builds an element", ->
90
+ Mercury.tooltip.build()
91
+ html = $('<div>').html(Mercury.tooltip.element).html()
92
+ expect(html).toContain('class="mercury-tooltip"')
93
+
94
+ it "can append to any element", ->
95
+ Mercury.tooltip.options = {appendTo: '#tooltip_container'}
96
+ Mercury.tooltip.build()
97
+ expect($('#tooltip_container').html()).toContain('class="mercury-tooltip"')
98
+
99
+
100
+ describe "observed events", ->
101
+
102
+ describe "custom event: resize", ->
103
+
104
+ it "call position if visible", ->
105
+ Mercury.tooltip.visible = true
106
+ spy = spyOn(Mercury.tooltip, 'position').andCallFake(=>)
107
+ Mercury.trigger('resize')
108
+ expect(spy.callCount).toEqual(1)
109
+
110
+ Mercury.tooltip.visible = false
111
+ Mercury.trigger('resize')
112
+ expect(spy.callCount).toEqual(1)
113
+
114
+ describe "document scrolling", ->
115
+
116
+ it "calls position if visible", ->
117
+ # untestable
118
+
119
+ describe "element mousedown", ->
120
+
121
+ it "stops the event", ->
122
+ # untestable
123
+
124
+
125
+ describe "#appear", ->
126
+
127
+ beforeEach ->
128
+ Mercury.tooltip.build()
129
+ @updateSpy = spyOn(Mercury.tooltip, 'update').andCallFake(=>)
130
+
131
+ it "calls update", ->
132
+ Mercury.tooltip.appear()
133
+ expect(@updateSpy.callCount).toEqual(1)
134
+
135
+ it "shows the element", ->
136
+ Mercury.tooltip.appear()
137
+ expect(Mercury.tooltip.element.css('display')).toEqual('block')
138
+ expect(Mercury.tooltip.element.css('opacity')).toEqual('1')
139
+
140
+ it "sets visible to true", ->
141
+ Mercury.tooltip.visible = false
142
+ Mercury.tooltip.appear()
143
+ expect(Mercury.tooltip.visible).toEqual(true)
144
+
145
+
146
+ describe "#update", ->
147
+
148
+ beforeEach ->
149
+ Mercury.tooltip.build()
150
+ @positionSpy = spyOn(Mercury.tooltip, 'position').andCallFake(=>)
151
+
152
+ it "sets the html", ->
153
+ Mercury.tooltip.content = 'foo'
154
+ Mercury.tooltip.update()
155
+ expect(Mercury.tooltip.element.html()).toEqual('foo')
156
+
157
+ it "calls position", ->
158
+ Mercury.tooltip.update()
159
+ expect(@positionSpy.callCount).toEqual(1)
160
+
161
+
162
+ describe "#position", ->
163
+
164
+ beforeEach ->
165
+ Mercury.tooltip.build()
166
+ Mercury.displayRect = {top: 0, left: 0, width: 200, height: 200}
167
+
168
+ it "positions based on the element we're showing for", ->
169
+ Mercury.tooltip.forElement = @forElement
170
+ Mercury.tooltip.position()
171
+ expect(Mercury.tooltip.element.offset()).toEqual({top: 20 + @forElement.outerHeight(), left: 42})
172
+
173
+
174
+ describe "#hide", ->
175
+
176
+ beforeEach ->
177
+ Mercury.tooltip.build()
178
+ Mercury.tooltip.initialized = true
179
+
180
+ it "hides the element", ->
181
+ Mercury.tooltip.element.css({display: 'block'})
182
+ Mercury.tooltip.hide()
183
+ expect(Mercury.tooltip.element.css('display')).toEqual('none')
184
+
185
+ it "sets visible to false", ->
186
+ Mercury.tooltip.visible = true
187
+ Mercury.tooltip.hide()
188
+ expect(Mercury.tooltip.visible).toEqual(false)
@@ -0,0 +1,512 @@
1
+ require '/assets/mercury/mercury.js'
2
+
3
+ describe "Mercury.uploader", ->
4
+
5
+ template 'mercury/uploader.html'
6
+
7
+ beforeEach ->
8
+ $.fx.off = true
9
+ @mockFile = {
10
+ size: 1024
11
+ fileName: 'image.png'
12
+ type: 'image/png'
13
+ }
14
+
15
+ afterEach ->
16
+ Mercury.uploader.initialized = false
17
+
18
+ describe "singleton method", ->
19
+
20
+ beforeEach ->
21
+ @showSpy = spyOn(Mercury.uploader, 'show').andCallFake(=>)
22
+
23
+ it "calls show", ->
24
+ Mercury.uploader(@mockFile)
25
+ expect(@showSpy.callCount).toEqual(1)
26
+
27
+ it "returns the function object", ->
28
+ ret = Mercury.uploader(@mockFile)
29
+ expect(ret).toEqual(Mercury.uploader)
30
+
31
+
32
+ describe "#show", ->
33
+
34
+ beforeEach ->
35
+ @initializeSpy = spyOn(Mercury.uploader, 'initialize').andCallFake(=>)
36
+ @appearSpy = spyOn(Mercury.uploader, 'appear').andCallFake(=>)
37
+
38
+ it "expects a file object", ->
39
+ Mercury.uploader.show(@mockFile)
40
+ expect(Mercury.uploader.file.name).toEqual(@mockFile.fileName)
41
+
42
+ it "accepts options", ->
43
+ Mercury.uploader.show(@mockFile, {foo: 'bar'})
44
+ expect(Mercury.uploader.options).toEqual({foo: 'bar'})
45
+
46
+ it "creates a file instance from the file", ->
47
+ Mercury.uploader.show(@mockFile)
48
+ expect(Mercury.uploader.file.name).toEqual(@mockFile.fileName)
49
+ expect(Mercury.uploader.file.fullSize).toEqual(1024)
50
+
51
+ it "alerts and stops if the file has errors (too large or unsupported mimetype)", ->
52
+ @mockFile.size = 123123123123
53
+ spy = spyOn(window, 'alert').andCallFake(=>)
54
+ Mercury.uploader.show(@mockFile)
55
+ expect(spy.callCount).toEqual(1)
56
+ expect(spy.argsForCall[0]).toEqual(['Error: Too large'])
57
+
58
+ it "doesn't do anything unless xhr uploading is supported in the browser", ->
59
+ spyOn(Mercury.uploader, 'supported').andCallFake(=> false)
60
+ Mercury.uploader.show(@mockFile)
61
+ expect(@initializeSpy.callCount).toEqual(0)
62
+
63
+ it "triggers an event to focus the window", ->
64
+ spy = spyOn(Mercury, 'trigger').andCallFake(=>)
65
+ Mercury.uploader.show(@mockFile)
66
+ expect(spy.callCount).toEqual(1)
67
+ expect(spy.argsForCall[0]).toEqual(['focus:window'])
68
+
69
+ it "calls initialize", ->
70
+ Mercury.uploader.show(@mockFile)
71
+ expect(@initializeSpy.callCount).toEqual(1)
72
+
73
+ it "calls appear", ->
74
+ Mercury.uploader.show(@mockFile)
75
+ expect(@appearSpy.callCount).toEqual(1)
76
+
77
+
78
+ describe "#initialize", ->
79
+
80
+ beforeEach ->
81
+ @buildSpy = spyOn(Mercury.uploader, 'build').andCallFake(=>)
82
+ @bindEventsSpy = spyOn(Mercury.uploader, 'bindEvents').andCallFake(=>)
83
+
84
+ it "calls build", ->
85
+ Mercury.uploader.initialize()
86
+ expect(@buildSpy.callCount).toEqual(1)
87
+
88
+ it "calls bindEvents", ->
89
+ Mercury.uploader.initialize()
90
+ expect(@bindEventsSpy.callCount).toEqual(1)
91
+
92
+ it "only initializes once", ->
93
+ Mercury.uploader.initialize()
94
+ expect(@buildSpy.callCount).toEqual(1)
95
+ Mercury.uploader.initialize()
96
+ expect(@buildSpy.callCount).toEqual(1)
97
+
98
+
99
+ describe "#supported", ->
100
+
101
+ it "prototypes sendAsBinary onto XMLHttpRequest if it's not already there", ->
102
+ XMLHttpRequest.prototype.sendAsBinary = null
103
+ Mercury.uploader.supported()
104
+ expect(XMLHttpRequest.prototype.sendAsBinary).toBeDefined()
105
+
106
+ it "returns true if everything needed is supported", ->
107
+ ret = Mercury.uploader.supported()
108
+ expect(ret).toEqual(true)
109
+
110
+ it "returns false if everything isn't supported", ->
111
+ window.Uint8Array = null
112
+ ret = Mercury.uploader.supported()
113
+ expect(ret).toEqual(true)
114
+
115
+
116
+ describe "#build", ->
117
+
118
+ beforeEach ->
119
+ Mercury.uploader.options = {appendTo: '#test'}
120
+
121
+ it "builds an element structure", ->
122
+ Mercury.uploader.build()
123
+ html = $('<div>').html(Mercury.uploader.element).html()
124
+ expect(html).toContain('class="mercury-uploader"')
125
+ expect(html).toContain('class="mercury-uploader-preview"')
126
+ expect(html).toContain('<b><img></b>')
127
+ expect(html).toContain('class="mercury-uploader-details"')
128
+ expect(html).toContain('<span>Processing...</span>')
129
+ expect(html).toContain('class="mercury-uploader-indicator"')
130
+ expect(html).toContain('<div><b>0%</b></div>')
131
+
132
+ it "builds an overlay", ->
133
+ Mercury.uploader.build()
134
+ html = $('<div>').html(Mercury.uploader.overlay).html()
135
+ expect(html).toContain('class="mercury-uploader-overlay"')
136
+
137
+ it "appends to any element", ->
138
+ Mercury.uploader.options = {appendTo: '#uploader_container'}
139
+ Mercury.uploader.build()
140
+ expect($('#uploader_container .mercury-uploader').length).toEqual(1)
141
+
142
+
143
+ describe "observed events", ->
144
+
145
+ describe "custom event: resize", ->
146
+
147
+ it "calls position", ->
148
+ spy = spyOn(Mercury.uploader, 'position').andCallFake(=>)
149
+ Mercury.uploader.bindEvents()
150
+ Mercury.trigger('resize')
151
+ expect(spy.callCount).toEqual(1)
152
+
153
+
154
+ describe "#appear", ->
155
+
156
+ beforeEach ->
157
+ Mercury.uploader.options = {appendTo: '#test'}
158
+ Mercury.uploader.build()
159
+ @fillDisplaySpy = spyOn(Mercury.uploader, 'fillDisplay').andCallFake(=>)
160
+ @positionSpy = spyOn(Mercury.uploader, 'position').andCallFake(=>)
161
+ @loadImageSpy = spyOn(Mercury.uploader, 'loadImage').andCallFake(=>)
162
+
163
+ it "calls fillDisplay", ->
164
+ Mercury.uploader.appear()
165
+ expect(@fillDisplaySpy.callCount).toEqual(1)
166
+
167
+ it "calls position", ->
168
+ Mercury.uploader.appear()
169
+ expect(@positionSpy.callCount).toEqual(1)
170
+
171
+ it "displays the overlay, and the element", ->
172
+ Mercury.uploader.appear()
173
+ expect($('#test .mercury-uploader').css('display')).toEqual('block')
174
+ expect($('#test .mercury-uploader-overlay').css('display')).toEqual('block')
175
+
176
+ it "sets visible to true", ->
177
+ Mercury.uploader.appear()
178
+ expect(Mercury.uploader.visible).toEqual(true)
179
+
180
+ it "calls loadImage", ->
181
+ Mercury.uploader.appear()
182
+ expect(@loadImageSpy.callCount).toEqual(1)
183
+
184
+
185
+ describe "#position", ->
186
+
187
+ beforeEach ->
188
+ Mercury.uploader.options = {appendTo: '#test'}
189
+ Mercury.uploader.build()
190
+ @fillDisplaySpy = spyOn(Mercury.uploader, 'fillDisplay').andCallFake(=>)
191
+ @positionSpy = spyOn(Mercury.uploader, 'position').andCallFake(=>)
192
+
193
+ it "centers the element in the viewport", ->
194
+ # todo: this isn't really being tested
195
+ Mercury.uploader.element.css({display: 'block'})
196
+ Mercury.uploader.position()
197
+ @expect($('#test .mercury-uploader').offset()).toEqual({top: 0, left: 0})
198
+
199
+
200
+ describe "#fillDisplay", ->
201
+
202
+ beforeEach ->
203
+ Mercury.uploader.options = {appendTo: '#test'}
204
+ Mercury.uploader.file = {name: 'image.png', size: 1024, type: 'image/png'}
205
+ Mercury.uploader.build()
206
+
207
+ it "puts the file details into the element", ->
208
+ Mercury.uploader.fillDisplay()
209
+ expect($('#test .mercury-uploader-details').html()).toEqual('Name: image.png<br>Size: undefined<br>Type: image/png')
210
+
211
+
212
+ describe "#loadImage", ->
213
+
214
+ beforeEach ->
215
+ Mercury.uploader.options = {appendTo: '#test'}
216
+ Mercury.uploader.file = new Mercury.uploader.File(@mockFile)
217
+ Mercury.uploader.build()
218
+ spyOn(FileReader.prototype, 'readAsBinaryString').andCallFake(=>)
219
+ @readAsDataURLSpy = spyOn(Mercury.uploader.File.prototype, 'readAsDataURL').andCallFake((callback) => callback('data-url'))
220
+
221
+ it "calls file.readAsDataURL", ->
222
+ Mercury.uploader.loadImage()
223
+ expect(@readAsDataURLSpy.callCount).toEqual(1)
224
+
225
+ it "sets the preview image src to the file contents", ->
226
+ Mercury.uploader.loadImage()
227
+ expect($('#test .mercury-uploader-preview img').attr('src')).toEqual('data-url')
228
+
229
+ it "calls upload", ->
230
+ spy = spyOn(Mercury.uploader, 'upload').andCallFake(=>)
231
+ Mercury.uploader.loadImage()
232
+ expect(spy.callCount).toEqual(1)
233
+
234
+
235
+ describe "#upload", ->
236
+
237
+ it "", ->
238
+
239
+
240
+ describe "#updateStatus", ->
241
+
242
+ beforeEach ->
243
+ Mercury.uploader.options = {appendTo: '#test'}
244
+ Mercury.uploader.build()
245
+
246
+ it "updated the message in the progress display", ->
247
+ Mercury.uploader.updateStatus('status message')
248
+ expect($('#test .mercury-uploader-progress span').html()).toEqual('status message')
249
+
250
+ it "updates the progress indicator width", ->
251
+ Mercury.uploader.updateStatus('message', 512)
252
+ expect($('#test .mercury-uploader-indicator div').css('width')).toEqual('50%')
253
+
254
+ it "updates the progress indicator value", ->
255
+ Mercury.uploader.updateStatus('message', 512)
256
+ expect($('#test .mercury-uploader-indicator b').html()).toEqual('50%')
257
+
258
+
259
+ describe "#hide", ->
260
+
261
+ beforeEach ->
262
+ @setTimeoutSpy = spyOn(window, 'setTimeout')
263
+ Mercury.uploader.options = {appendTo: '#test'}
264
+ Mercury.uploader.build()
265
+
266
+ it "accepts a delay", ->
267
+ @setTimeoutSpy.andCallFake(=>)
268
+ Mercury.uploader.hide(1)
269
+ expect(@setTimeoutSpy.callCount).toEqual(1)
270
+ expect(@setTimeoutSpy.argsForCall[0][1]).toEqual(1000)
271
+
272
+ it "hides the overlay and element", ->
273
+ @setTimeoutSpy.andCallFake((callback) => callback())
274
+ Mercury.uploader.hide()
275
+ expect($('#test .mercury-uploader').css('opacity')).toEqual('0')
276
+ expect($('#test .mercury-uploader-overlay').css('opacity')).toEqual('0')
277
+
278
+ it "calls reset", ->
279
+ @setTimeoutSpy.andCallFake((callback) => callback())
280
+ spy = spyOn(Mercury.uploader, 'reset').andCallFake(=>)
281
+ Mercury.uploader.hide()
282
+ expect(spy.callCount).toEqual(1)
283
+
284
+ it "sets visible to false", ->
285
+ @setTimeoutSpy.andCallFake((callback) => callback())
286
+ Mercury.uploader.hide()
287
+ expect(Mercury.uploader.visible).toEqual(false)
288
+
289
+ it "focuses the frame", ->
290
+ @setTimeoutSpy.andCallFake((callback) => callback())
291
+ spy = spyOn(Mercury, 'trigger').andCallFake(=>)
292
+ Mercury.uploader.hide()
293
+ expect(spy.callCount).toEqual(1)
294
+ expect(spy.argsForCall[0]).toEqual(['focus:frame'])
295
+
296
+
297
+ describe "#reset", ->
298
+
299
+ beforeEach ->
300
+ Mercury.uploader.options = {appendTo: '#test'}
301
+ Mercury.uploader.build()
302
+
303
+ it "removes the preview image", ->
304
+ $('#test .mercury-uploader-indicator div').html('foo')
305
+ Mercury.uploader.reset()
306
+ expect($('#test .mercury-uploader-preview b').html()).toEqual('')
307
+
308
+ it "resets the progress back to 0", ->
309
+ $('#test .mercury-uploader-indicator div').css({width: '50%'})
310
+ $('#test .mercury-uploader-indicator b').html('50%')
311
+ Mercury.uploader.reset()
312
+ expect($('#test .mercury-uploader-indicator div').css('width')).toEqual('0px')
313
+ expect($('#test .mercury-uploader-indicator b').html()).toEqual('0%')
314
+
315
+ it "sets the status back to 'Processing...' for next time", ->
316
+ spy = spyOn(Mercury.uploader, 'updateStatus').andCallFake(=>)
317
+ Mercury.uploader.reset()
318
+ expect(spy.callCount).toEqual(1)
319
+
320
+
321
+ describe "uploaderEvents", ->
322
+
323
+ beforeEach ->
324
+ Mercury.uploader.file = @mockFile
325
+ @updateStatusSpy = spyOn(Mercury.uploader, 'updateStatus').andCallFake(=>)
326
+ @hideSpy = spyOn(Mercury.uploader, 'hide').andCallFake(=>)
327
+ @events = Mercury.uploader.uploaderEvents
328
+
329
+ describe ".onloadstart", ->
330
+
331
+ it "updates the status to 'Uploading...'", ->
332
+ @events.onloadstart.call(Mercury.uploader)
333
+ expect(@updateStatusSpy.callCount).toEqual(1)
334
+ expect(@updateStatusSpy.argsForCall[0]).toEqual(['Uploading...'])
335
+
336
+ describe ".onprogress", ->
337
+
338
+ it "updates the status to 'Uploading...' and passes the amount sent so far", ->
339
+ @events.onprogress.call(Mercury.uploader, {loaded: 512})
340
+ expect(@updateStatusSpy.callCount).toEqual(1)
341
+ expect(@updateStatusSpy.argsForCall[0]).toEqual(['Uploading...', 512])
342
+
343
+ describe ".onabort", ->
344
+
345
+ it "updates the status to 'Aborted'", ->
346
+ @events.onabort.call(Mercury.uploader)
347
+ expect(@updateStatusSpy.callCount).toEqual(1)
348
+ expect(@updateStatusSpy.argsForCall[0]).toEqual(['Aborted'])
349
+
350
+ it "calls hide", ->
351
+ @events.onabort.call(Mercury.uploader)
352
+ expect(@hideSpy.callCount).toEqual(1)
353
+
354
+ describe ".onload", ->
355
+
356
+ it "updates the status to 'Successfully uploaded' and passes the total file size", ->
357
+ @events.onload.call(Mercury.uploader)
358
+ expect(@updateStatusSpy.callCount).toEqual(1)
359
+ expect(@updateStatusSpy.argsForCall[0]).toEqual(['Successfully uploaded', 1024])
360
+
361
+ it "calls hide", ->
362
+ @events.onload.call(Mercury.uploader)
363
+ expect(@hideSpy.callCount).toEqual(1)
364
+
365
+ describe ".onerror", ->
366
+
367
+ it "updates the status to 'Error: Unable to upload the file'", ->
368
+ @events.onerror.call(Mercury.uploader)
369
+ expect(@updateStatusSpy.callCount).toEqual(1)
370
+ expect(@updateStatusSpy.argsForCall[0]).toEqual(['Error: Unable to upload the file'])
371
+
372
+ it "calls hide", ->
373
+ @events.onerror.call(Mercury.uploader)
374
+ expect(@hideSpy.callCount).toEqual(1)
375
+
376
+
377
+
378
+ describe "Mercury.uploader.File", ->
379
+
380
+ beforeEach ->
381
+ @mockFile = {
382
+ size: 1024
383
+ fileName: 'image.png'
384
+ type: 'image/png'
385
+ }
386
+
387
+ afterEach ->
388
+ @file = null
389
+ delete(@file)
390
+
391
+ describe "constructor", ->
392
+
393
+ it "expects a file", ->
394
+ @file = new Mercury.uploader.File(@mockFile)
395
+ expect(@file.file).toEqual(@mockFile)
396
+
397
+ it "reads attributes of the file and sets variables", ->
398
+ @file = new Mercury.uploader.File(@mockFile)
399
+ expect(@file.size).toEqual(1024)
400
+ expect(@file.fullSize).toEqual(1024)
401
+ expect(@file.readableSize).toEqual('1.00 kb')
402
+ expect(@file.name).toEqual('image.png')
403
+ expect(@file.type).toEqual('image/png')
404
+
405
+ it "adds errors if there's any", ->
406
+ Mercury.config.uploading.maxFileSize = 100
407
+ Mercury.config.uploading.allowedMimeTypes = ['image/foo']
408
+ @file = new Mercury.uploader.File(@mockFile)
409
+ expect(@file.errors).toEqual('Too large / Unsupported format')
410
+
411
+
412
+ describe "#readAsDataURL", ->
413
+
414
+ it "it calls readAsDataURL on a FileReader object", ->
415
+ spy = spyOn(window.FileReader.prototype, 'readAsDataURL').andCallFake(=>)
416
+ @file = new Mercury.uploader.File(@mockFile)
417
+ @file.readAsDataURL()
418
+ expect(spy.callCount).toEqual(1)
419
+
420
+ it "calls a callback if one was provided", ->
421
+ spyOn(FileReader.prototype, 'readAsDataURL').andCallFake(=>)
422
+ FileReader.prototype.result = 'result'
423
+ callCount = 0
424
+ callback = (r) => callCount += 1
425
+
426
+ @file = new Mercury.uploader.File(@mockFile)
427
+ onload = @file.readAsDataURL(callback)
428
+ onload()
429
+ expect(callCount).toEqual(1)
430
+
431
+
432
+ describe "#readAsBinaryString", ->
433
+
434
+ it "it calls readAsBinaryString on a FileReader object", ->
435
+ spy = spyOn(window.FileReader.prototype, 'readAsBinaryString').andCallFake(=>)
436
+ @file = new Mercury.uploader.File(@mockFile)
437
+ @file.readAsBinaryString()
438
+ expect(spy.callCount).toEqual(1)
439
+
440
+ it "calls a callback if one was provided", ->
441
+ spyOn(FileReader.prototype, 'readAsBinaryString').andCallFake(=>)
442
+ FileReader.prototype.result = 'result'
443
+ callCount = 0
444
+ callback = (r) => callCount += 1
445
+
446
+ @file = new Mercury.uploader.File(@mockFile)
447
+ onload = @file.readAsBinaryString(callback)
448
+ onload()
449
+ expect(callCount).toEqual(1)
450
+
451
+
452
+ describe "#updateSize", ->
453
+
454
+ it "updates the size based on a delta", ->
455
+ @file = new Mercury.uploader.File(@mockFile)
456
+ @file.updateSize(20)
457
+ expect(@file.fullSize).toEqual(1044)
458
+
459
+
460
+
461
+ describe "Mercury.uploader.MultiPartPost", ->
462
+
463
+ beforeEach ->
464
+ @mockFile = {
465
+ size: 1024
466
+ name: 'image.png'
467
+ type: 'image/png'
468
+ }
469
+
470
+ afterEach ->
471
+ @multiPartPost = null
472
+ delete(@multiPartPost)
473
+
474
+ describe "constructor", ->
475
+
476
+ it "expects an inputName, file, and file contents", ->
477
+ @multiPartPost = new Mercury.uploader.MultiPartPost('foo[bar]', @mockFile, 'file contents')
478
+ expect(@multiPartPost.inputName).toEqual('foo[bar]')
479
+ expect(@multiPartPost.file).toEqual(@mockFile)
480
+ expect(@multiPartPost.contents).toEqual('file contents')
481
+
482
+ it "accepts a formInputs object", ->
483
+ @multiPartPost = new Mercury.uploader.MultiPartPost('foo[bar]', @mockFile, 'file contents', {foo: 'bar'})
484
+ expect(@multiPartPost.formInputs).toEqual({foo: 'bar'})
485
+
486
+ it "defines a boundary string", ->
487
+ @multiPartPost = new Mercury.uploader.MultiPartPost('foo[bar]', @mockFile, 'file contents')
488
+ expect(@multiPartPost.boundary).toEqual('Boundaryx20072377098235644401115438165x')
489
+
490
+ it "calls buildBody", ->
491
+ spy = spyOn(Mercury.uploader.MultiPartPost.prototype, 'buildBody').andCallFake(=>)
492
+ @multiPartPost = new Mercury.uploader.MultiPartPost('foo[bar]', @mockFile, 'file contents')
493
+ expect(spy.callCount).toEqual(1)
494
+
495
+ it "sets a delta based on the body size and file size", ->
496
+ @multiPartPost = new Mercury.uploader.MultiPartPost('foo[bar]', @mockFile, 'file contents')
497
+ expect(@multiPartPost.delta).toEqual(-790)
498
+
499
+
500
+ describe "#buildBody", ->
501
+
502
+ it "creates a multipart post body with the file information", ->
503
+ @multiPartPost = new Mercury.uploader.MultiPartPost('foo[bar]', @mockFile, 'file contents')
504
+ expect(@multiPartPost.body).toContain('--Boundaryx20072377098235644401115438165x')
505
+ expect(@multiPartPost.body).toContain('Content-Disposition: form-data; name="foo[bar]"; filename="image.png"')
506
+ expect(@multiPartPost.body).toContain('Content-Type: image/png')
507
+ expect(@multiPartPost.body).toContain('Content-Transfer-Encoding: binary')
508
+ expect(@multiPartPost.body).toContain('file contents')
509
+
510
+ it "includes form inputs if passed in", ->
511
+ @multiPartPost = new Mercury.uploader.MultiPartPost('foo[bar]', @mockFile, 'file contents', {foo: 'bar'})
512
+ expect(@multiPartPost.body).toContain('Content-Disposition: form-data; name="foo"\r\n\r\nbar')