uki 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (235) hide show
  1. data/.gitignore +7 -0
  2. data/.gitmodules +3 -0
  3. data/LICENSE +20 -0
  4. data/Rakefile +23 -0
  5. data/Readme.rdoc +9 -0
  6. data/VERSION +1 -0
  7. data/bin/uki +102 -0
  8. data/frameworks/jspec/lib/images/bg.png +0 -0
  9. data/frameworks/jspec/lib/images/hr.png +0 -0
  10. data/frameworks/jspec/lib/images/loading.gif +0 -0
  11. data/frameworks/jspec/lib/images/sprites.bg.png +0 -0
  12. data/frameworks/jspec/lib/images/sprites.png +0 -0
  13. data/frameworks/jspec/lib/images/vr.png +0 -0
  14. data/frameworks/jspec/lib/jspec.css +149 -0
  15. data/frameworks/jspec/lib/jspec.growl.js +115 -0
  16. data/frameworks/jspec/lib/jspec.jquery.js +72 -0
  17. data/frameworks/jspec/lib/jspec.js +1756 -0
  18. data/frameworks/jspec/lib/jspec.shell.js +39 -0
  19. data/frameworks/jspec/lib/jspec.timers.js +90 -0
  20. data/frameworks/jspec/lib/jspec.xhr.js +195 -0
  21. data/frameworks/uki/README.rdoc +179 -0
  22. data/frameworks/uki/compiler.jar +0 -0
  23. data/frameworks/uki/run.rb +2 -0
  24. data/frameworks/uki/spec/commands/example_command.rb +19 -0
  25. data/frameworks/uki/spec/dom.html +39 -0
  26. data/frameworks/uki/spec/support/images/bg.png +0 -0
  27. data/frameworks/uki/spec/support/images/hr.png +0 -0
  28. data/frameworks/uki/spec/support/images/loading.gif +0 -0
  29. data/frameworks/uki/spec/support/images/sprites.bg.png +0 -0
  30. data/frameworks/uki/spec/support/images/sprites.png +0 -0
  31. data/frameworks/uki/spec/support/images/vr.png +0 -0
  32. data/frameworks/uki/spec/support/jspec.css +149 -0
  33. data/frameworks/uki/spec/support/jspec.js +1773 -0
  34. data/frameworks/uki/spec/support/jspec.xhr.js +193 -0
  35. data/frameworks/uki/spec/support/spec.helper.js +1 -0
  36. data/frameworks/uki/spec/unit/background.spec.js +29 -0
  37. data/frameworks/uki/spec/unit/builder.spec.js +51 -0
  38. data/frameworks/uki/spec/unit/data/model.spec.js +29 -0
  39. data/frameworks/uki/spec/unit/dom/dnd.spec.js +71 -0
  40. data/frameworks/uki/spec/unit/dom/event.spec.js +78 -0
  41. data/frameworks/uki/spec/unit/dom.spec.js +28 -0
  42. data/frameworks/uki/spec/unit/geometry.spec.js +79 -0
  43. data/frameworks/uki/spec/unit/selector.spec.js +140 -0
  44. data/frameworks/uki/spec/unit/theme/template.spec.js +31 -0
  45. data/frameworks/uki/spec/unit/utils.spec.js +176 -0
  46. data/frameworks/uki/spec/unit/view/base.spec.js +86 -0
  47. data/frameworks/uki/spec/unit/view/container.spec.js +73 -0
  48. data/frameworks/uki/spec/unit/view.spec.js +13 -0
  49. data/frameworks/uki/src/airport.js +1 -0
  50. data/frameworks/uki/src/uki-core/attachment.js +175 -0
  51. data/frameworks/uki/src/uki-core/background/css.js +37 -0
  52. data/frameworks/uki/src/uki-core/background/cssBox.js +73 -0
  53. data/frameworks/uki/src/uki-core/background/multi.js +20 -0
  54. data/frameworks/uki/src/uki-core/background/null.js +10 -0
  55. data/frameworks/uki/src/uki-core/background/rows.js +77 -0
  56. data/frameworks/uki/src/uki-core/background/sliced9.js +206 -0
  57. data/frameworks/uki/src/uki-core/background.js +35 -0
  58. data/frameworks/uki/src/uki-core/builder.js +68 -0
  59. data/frameworks/uki/src/uki-core/collection.js +278 -0
  60. data/frameworks/uki/src/uki-core/const.js +17 -0
  61. data/frameworks/uki/src/uki-core/dom/dnd.js +93 -0
  62. data/frameworks/uki/src/uki-core/dom/event.js +194 -0
  63. data/frameworks/uki/src/uki-core/dom/nativeLayout.js +18 -0
  64. data/frameworks/uki/src/uki-core/dom/offset.js +130 -0
  65. data/frameworks/uki/src/uki-core/dom/w3cdnd.js +333 -0
  66. data/frameworks/uki/src/uki-core/dom.js +109 -0
  67. data/frameworks/uki/src/uki-core/geometry.js +658 -0
  68. data/frameworks/uki/src/uki-core/image.js +90 -0
  69. data/frameworks/uki/src/uki-core/selector.js +201 -0
  70. data/frameworks/uki/src/uki-core/theme/base.js +39 -0
  71. data/frameworks/uki/src/uki-core/theme/template.js +26 -0
  72. data/frameworks/uki/src/uki-core/theme.js +45 -0
  73. data/frameworks/uki/src/uki-core/uki.js +45 -0
  74. data/frameworks/uki/src/uki-core/utils.js +399 -0
  75. data/frameworks/uki/src/uki-core/view/base.js +480 -0
  76. data/frameworks/uki/src/uki-core/view/container.js +155 -0
  77. data/frameworks/uki/src/uki-core/view/focusable.js +93 -0
  78. data/frameworks/uki/src/uki-core/view/observable.js +66 -0
  79. data/frameworks/uki/src/uki-core/view/styleable.js +70 -0
  80. data/frameworks/uki/src/uki-core/view/utils.js +66 -0
  81. data/frameworks/uki/src/uki-core/view.js +21 -0
  82. data/frameworks/uki/src/uki-core.js +36 -0
  83. data/frameworks/uki/src/uki-data/data.js +1 -0
  84. data/frameworks/uki/src/uki-data/model.js +28 -0
  85. data/frameworks/uki/src/uki-data/observable.js +34 -0
  86. data/frameworks/uki/src/uki-data.js +1 -0
  87. data/frameworks/uki/src/uki-more/more/utils.js +20 -0
  88. data/frameworks/uki/src/uki-more/more/view/listContainer.js +4 -0
  89. data/frameworks/uki/src/uki-more/more/view/multiselectList.js +196 -0
  90. data/frameworks/uki/src/uki-more/more/view/radioButton.js +27 -0
  91. data/frameworks/uki/src/uki-more/more/view/splitTable.js +79 -0
  92. data/frameworks/uki/src/uki-more/more/view/toggleButton.js +24 -0
  93. data/frameworks/uki/src/uki-more/more/view/treeList/render.js +53 -0
  94. data/frameworks/uki/src/uki-more/more/view/treeList.js +110 -0
  95. data/frameworks/uki/src/uki-more/more/view.js +2 -0
  96. data/frameworks/uki/src/uki-more/more.js +1 -0
  97. data/frameworks/uki/src/uki-more.js +4 -0
  98. data/frameworks/uki/src/uki-theamless.js +15 -0
  99. data/frameworks/uki/src/uki-theme/airport/i/button/down-c.gif +0 -0
  100. data/frameworks/uki/src/uki-theme/airport/i/button/down-c.png +0 -0
  101. data/frameworks/uki/src/uki-theme/airport/i/button/down-h.gif +0 -0
  102. data/frameworks/uki/src/uki-theme/airport/i/button/down-h.png +0 -0
  103. data/frameworks/uki/src/uki-theme/airport/i/button/down-m.png +0 -0
  104. data/frameworks/uki/src/uki-theme/airport/i/button/down-v.png +0 -0
  105. data/frameworks/uki/src/uki-theme/airport/i/button/focusRing-c.png +0 -0
  106. data/frameworks/uki/src/uki-theme/airport/i/button/focusRing-h.png +0 -0
  107. data/frameworks/uki/src/uki-theme/airport/i/button/focusRing-m.png +0 -0
  108. data/frameworks/uki/src/uki-theme/airport/i/button/focusRing-v.png +0 -0
  109. data/frameworks/uki/src/uki-theme/airport/i/button/focusRing.png +0 -0
  110. data/frameworks/uki/src/uki-theme/airport/i/button/hover-c.gif +0 -0
  111. data/frameworks/uki/src/uki-theme/airport/i/button/hover-c.png +0 -0
  112. data/frameworks/uki/src/uki-theme/airport/i/button/hover-h.gif +0 -0
  113. data/frameworks/uki/src/uki-theme/airport/i/button/hover-h.png +0 -0
  114. data/frameworks/uki/src/uki-theme/airport/i/button/hover-m.png +0 -0
  115. data/frameworks/uki/src/uki-theme/airport/i/button/hover-v.png +0 -0
  116. data/frameworks/uki/src/uki-theme/airport/i/button/hover.png +0 -0
  117. data/frameworks/uki/src/uki-theme/airport/i/button/normal-c.gif +0 -0
  118. data/frameworks/uki/src/uki-theme/airport/i/button/normal-c.png +0 -0
  119. data/frameworks/uki/src/uki-theme/airport/i/button/normal-h.gif +0 -0
  120. data/frameworks/uki/src/uki-theme/airport/i/button/normal-h.png +0 -0
  121. data/frameworks/uki/src/uki-theme/airport/i/button/normal-m.png +0 -0
  122. data/frameworks/uki/src/uki-theme/airport/i/button/normal-v.png +0 -0
  123. data/frameworks/uki/src/uki-theme/airport/i/button/normal.png +0 -0
  124. data/frameworks/uki/src/uki-theme/airport/i/checkbox/checkbox.png +0 -0
  125. data/frameworks/uki/src/uki-theme/airport/i/checkbox/focus.png +0 -0
  126. data/frameworks/uki/src/uki-theme/airport/i/checkbox/normal.gif +0 -0
  127. data/frameworks/uki/src/uki-theme/airport/i/checkbox/normal.png +0 -0
  128. data/frameworks/uki/src/uki-theme/airport/i/panel/dark-h.gif +0 -0
  129. data/frameworks/uki/src/uki-theme/airport/i/panel/dark-h.png +0 -0
  130. data/frameworks/uki/src/uki-theme/airport/i/panel/dark-m.png +0 -0
  131. data/frameworks/uki/src/uki-theme/airport/i/panel/dark.png +0 -0
  132. data/frameworks/uki/src/uki-theme/airport/i/popup/normal.png +0 -0
  133. data/frameworks/uki/src/uki-theme/airport/i/radio/focus.png +0 -0
  134. data/frameworks/uki/src/uki-theme/airport/i/radio/normal.gif +0 -0
  135. data/frameworks/uki/src/uki-theme/airport/i/radio/normal.png +0 -0
  136. data/frameworks/uki/src/uki-theme/airport/i/radio/radio.png +0 -0
  137. data/frameworks/uki/src/uki-theme/airport/i/shadow/large-c.png +0 -0
  138. data/frameworks/uki/src/uki-theme/airport/i/shadow/large-h.png +0 -0
  139. data/frameworks/uki/src/uki-theme/airport/i/shadow/large-m.png +0 -0
  140. data/frameworks/uki/src/uki-theme/airport/i/shadow/large-v.png +0 -0
  141. data/frameworks/uki/src/uki-theme/airport/i/shadow/large.png +0 -0
  142. data/frameworks/uki/src/uki-theme/airport/i/slider/bar-m.gif +0 -0
  143. data/frameworks/uki/src/uki-theme/airport/i/slider/bar-m.png +0 -0
  144. data/frameworks/uki/src/uki-theme/airport/i/slider/bar-v.gif +0 -0
  145. data/frameworks/uki/src/uki-theme/airport/i/slider/bar-v.png +0 -0
  146. data/frameworks/uki/src/uki-theme/airport/i/slider/bar.png +0 -0
  147. data/frameworks/uki/src/uki-theme/airport/i/slider/focus.png +0 -0
  148. data/frameworks/uki/src/uki-theme/airport/i/slider/handle.gif +0 -0
  149. data/frameworks/uki/src/uki-theme/airport/i/splitPane/horizontal.gif +0 -0
  150. data/frameworks/uki/src/uki-theme/airport/i/splitPane/horizontal.png +0 -0
  151. data/frameworks/uki/src/uki-theme/airport/i/splitPane/vertical.gif +0 -0
  152. data/frameworks/uki/src/uki-theme/airport/i/x.gif +0 -0
  153. data/frameworks/uki/src/uki-theme/airport.js +322 -0
  154. data/frameworks/uki/src/uki-theme/aristo/i/button/down-c.gif +0 -0
  155. data/frameworks/uki/src/uki-theme/aristo/i/button/down-c.png +0 -0
  156. data/frameworks/uki/src/uki-theme/aristo/i/button/down-h.png +0 -0
  157. data/frameworks/uki/src/uki-theme/aristo/i/button/down-m.png +0 -0
  158. data/frameworks/uki/src/uki-theme/aristo/i/button/down-v.png +0 -0
  159. data/frameworks/uki/src/uki-theme/aristo/i/button/down.png +0 -0
  160. data/frameworks/uki/src/uki-theme/aristo/i/button/focusRing-c.png +0 -0
  161. data/frameworks/uki/src/uki-theme/aristo/i/button/focusRing-h.png +0 -0
  162. data/frameworks/uki/src/uki-theme/aristo/i/button/focusRing-m.png +0 -0
  163. data/frameworks/uki/src/uki-theme/aristo/i/button/focusRing-v.png +0 -0
  164. data/frameworks/uki/src/uki-theme/aristo/i/button/focusRing.png +0 -0
  165. data/frameworks/uki/src/uki-theme/aristo/i/button/normal-c.gif +0 -0
  166. data/frameworks/uki/src/uki-theme/aristo/i/button/normal-c.png +0 -0
  167. data/frameworks/uki/src/uki-theme/aristo/i/button/normal-h.png +0 -0
  168. data/frameworks/uki/src/uki-theme/aristo/i/button/normal-m.png +0 -0
  169. data/frameworks/uki/src/uki-theme/aristo/i/button/normal-v.png +0 -0
  170. data/frameworks/uki/src/uki-theme/aristo/i/button/normal.png +0 -0
  171. data/frameworks/uki/src/uki-theme/aristo/i/checkbox/focus.png +0 -0
  172. data/frameworks/uki/src/uki-theme/aristo/i/checkbox/normal.gif +0 -0
  173. data/frameworks/uki/src/uki-theme/aristo/i/checkbox/normal.png +0 -0
  174. data/frameworks/uki/src/uki-theme/aristo/i/panel/normal-h.png +0 -0
  175. data/frameworks/uki/src/uki-theme/aristo/i/panel/normal-m.png +0 -0
  176. data/frameworks/uki/src/uki-theme/aristo/i/panel/normal.png +0 -0
  177. data/frameworks/uki/src/uki-theme/aristo/i/popup/normal.png +0 -0
  178. data/frameworks/uki/src/uki-theme/aristo/i/radio/focus.png +0 -0
  179. data/frameworks/uki/src/uki-theme/aristo/i/radio/normal.gif +0 -0
  180. data/frameworks/uki/src/uki-theme/aristo/i/radio/normal.png +0 -0
  181. data/frameworks/uki/src/uki-theme/aristo/i/shadow/large-c.png +0 -0
  182. data/frameworks/uki/src/uki-theme/aristo/i/shadow/large-h.png +0 -0
  183. data/frameworks/uki/src/uki-theme/aristo/i/shadow/large-m.png +0 -0
  184. data/frameworks/uki/src/uki-theme/aristo/i/shadow/large-v.png +0 -0
  185. data/frameworks/uki/src/uki-theme/aristo/i/shadow/large.png +0 -0
  186. data/frameworks/uki/src/uki-theme/aristo/i/slider/bar-m.gif +0 -0
  187. data/frameworks/uki/src/uki-theme/aristo/i/slider/bar-m.png +0 -0
  188. data/frameworks/uki/src/uki-theme/aristo/i/slider/bar-v.gif +0 -0
  189. data/frameworks/uki/src/uki-theme/aristo/i/slider/bar-v.png +0 -0
  190. data/frameworks/uki/src/uki-theme/aristo/i/slider/bar.png +0 -0
  191. data/frameworks/uki/src/uki-theme/aristo/i/slider/handle.gif +0 -0
  192. data/frameworks/uki/src/uki-theme/aristo/i/slider/handle.png +0 -0
  193. data/frameworks/uki/src/uki-theme/aristo/i/splitPane/horizontal.gif +0 -0
  194. data/frameworks/uki/src/uki-theme/aristo/i/splitPane/horizontal.png +0 -0
  195. data/frameworks/uki/src/uki-theme/aristo/i/splitPane/vertical.gif +0 -0
  196. data/frameworks/uki/src/uki-theme/aristo/i/x.gif +0 -0
  197. data/frameworks/uki/src/uki-theme/aristo.js +217 -0
  198. data/frameworks/uki/src/uki-view/view/box.js +1 -0
  199. data/frameworks/uki/src/uki-view/view/button.js +126 -0
  200. data/frameworks/uki/src/uki-view/view/checkbox.js +36 -0
  201. data/frameworks/uki/src/uki-view/view/flow.js +48 -0
  202. data/frameworks/uki/src/uki-view/view/image.js +9 -0
  203. data/frameworks/uki/src/uki-view/view/label.js +123 -0
  204. data/frameworks/uki/src/uki-view/view/list/render.js +23 -0
  205. data/frameworks/uki/src/uki-view/view/list.js +442 -0
  206. data/frameworks/uki/src/uki-view/view/popup.js +113 -0
  207. data/frameworks/uki/src/uki-view/view/radio.js +57 -0
  208. data/frameworks/uki/src/uki-view/view/scrollPane.js +139 -0
  209. data/frameworks/uki/src/uki-view/view/slider.js +154 -0
  210. data/frameworks/uki/src/uki-view/view/splitPane.js +213 -0
  211. data/frameworks/uki/src/uki-view/view/table/column.js +96 -0
  212. data/frameworks/uki/src/uki-view/view/table/header.js +53 -0
  213. data/frameworks/uki/src/uki-view/view/table/render.js +25 -0
  214. data/frameworks/uki/src/uki-view/view/table.js +71 -0
  215. data/frameworks/uki/src/uki-view/view/textField.js +145 -0
  216. data/frameworks/uki/src/uki-view/view/toolbar.js +93 -0
  217. data/frameworks/uki/src/uki-view.js +15 -0
  218. data/frameworks/uki/src/uki.js +2 -0
  219. data/frameworks/uki/thin.yaml +11 -0
  220. data/frameworks/uki/uki.rb +38 -0
  221. data/frameworks/uki/uki.ru +2 -0
  222. data/lib/uki/include_js.rb +50 -0
  223. data/lib/uki/project.rb +207 -0
  224. data/lib/uki/routes.rb +20 -0
  225. data/lib/uki/server.rb +42 -0
  226. data/lib/uki.rb +9 -0
  227. data/templates/index.html.erb +10 -0
  228. data/templates/model.js.erb +5 -0
  229. data/templates/myapp.js.erb +44 -0
  230. data/templates/package.js.erb +3 -0
  231. data/templates/spec.html.erb +23 -0
  232. data/templates/spec.js.erb +6 -0
  233. data/templates/view.js.erb +10 -0
  234. data/uki.gemspec +281 -0
  235. metadata +317 -0
@@ -0,0 +1,1773 @@
1
+
2
+ // JSpec - Core - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
3
+
4
+ ;(function(){
5
+
6
+ JSpec = {
7
+ version : '3.1.3',
8
+ assert : true,
9
+ cache : {},
10
+ suites : [],
11
+ modules : [],
12
+ allSuites : [],
13
+ matchers : {},
14
+ stubbed : [],
15
+ options : {},
16
+ request : 'XMLHttpRequest' in this ? XMLHttpRequest : null,
17
+ stats : { specs: 0, assertions: 0, failures: 0, passes: 0, specsFinished: 0, suitesFinished: 0 },
18
+
19
+ /**
20
+ * Default context in which bodies are evaluated.
21
+ *
22
+ * Replace context simply by setting JSpec.context
23
+ * to your own like below:
24
+ *
25
+ * JSpec.context = { foo : 'bar' }
26
+ *
27
+ * Contexts can be changed within any body, this can be useful
28
+ * in order to provide specific helper methods to specific suites.
29
+ *
30
+ * To reset (usually in after hook) simply set to null like below:
31
+ *
32
+ * JSpec.context = null
33
+ *
34
+ */
35
+
36
+ defaultContext : {
37
+
38
+ /**
39
+ * Return an object used for proxy assertions.
40
+ * This object is used to indicate that an object
41
+ * should be an instance of _object_, not the constructor
42
+ * itself.
43
+ *
44
+ * @param {function} constructor
45
+ * @return {hash}
46
+ * @api public
47
+ */
48
+
49
+ an_instance_of : function(constructor) {
50
+ return { an_instance_of : constructor }
51
+ },
52
+
53
+ /**
54
+ * Load fixture at _path_.
55
+ *
56
+ * Fixtures are resolved as:
57
+ *
58
+ * - <path>
59
+ * - <path>.html
60
+ *
61
+ * @param {string} path
62
+ * @return {string}
63
+ * @api public
64
+ */
65
+
66
+ fixture : function(path) {
67
+ if (JSpec.cache[path]) return JSpec.cache[path]
68
+ return JSpec.cache[path] =
69
+ JSpec.tryLoading(JSpec.options.fixturePath + '/' + path) ||
70
+ JSpec.tryLoading(JSpec.options.fixturePath + '/' + path + '.html')
71
+ }
72
+ },
73
+
74
+ // --- Objects
75
+
76
+ reporters : {
77
+
78
+ /**
79
+ * Report to server.
80
+ *
81
+ * Options:
82
+ * - uri specific uri to report to.
83
+ * - verbose weither or not to output messages
84
+ * - failuresOnly output failure messages only
85
+ *
86
+ * @api public
87
+ */
88
+
89
+ Server : function(results, options) {
90
+ var uri = options.uri || 'http://' + window.location.host + '/results'
91
+ JSpec.post(uri, {
92
+ stats: JSpec.stats,
93
+ options: options,
94
+ results: map(results.allSuites, function(suite) {
95
+ if (suite.hasSpecs())
96
+ return {
97
+ description: suite.description,
98
+ specs: map(suite.specs, function(spec) {
99
+ return {
100
+ description: spec.description,
101
+ message: !spec.passed() ? spec.failure().message : null,
102
+ status: spec.requiresImplementation() ? 'pending' :
103
+ spec.passed() ? 'pass' :
104
+ 'fail',
105
+ assertions: map(spec.assertions, function(assertion){
106
+ return {
107
+ passed: assertion.passed
108
+ }
109
+ })
110
+ }
111
+ })
112
+ }
113
+ })
114
+ })
115
+ if ('close' in main) main.close()
116
+ },
117
+
118
+ /**
119
+ * Default reporter, outputting to the DOM.
120
+ *
121
+ * Options:
122
+ * - reportToId id of element to output reports to, defaults to 'jspec'
123
+ * - failuresOnly displays only suites with failing specs
124
+ *
125
+ * @api public
126
+ */
127
+
128
+ DOM : function(results, options) {
129
+ var id = option('reportToId') || 'jspec'
130
+ var report = document.getElementById(id)
131
+ var failuresOnly = option('failuresOnly')
132
+ var classes = results.stats.failures ? 'has-failures' : ''
133
+ if (!report) throw 'JSpec requires the element #' + id + ' to output its reports'
134
+
135
+ function bodyContents(body) {
136
+ return JSpec.
137
+ escape(JSpec.contentsOf(body)).
138
+ replace(/^ */gm, function(a){ return (new Array(Math.round(a.length / 3))).join(' ') }).
139
+ replace(/\r\n|\r|\n/gm, '<br/>')
140
+ }
141
+
142
+ report.innerHTML = '<div id="jspec-report" class="' + classes + '"><div class="heading"> \
143
+ <span class="passes">Passes: <em>' + results.stats.passes + '</em></span> \
144
+ <span class="failures">Failures: <em>' + results.stats.failures + '</em></span> \
145
+ <span class="passes">Duration: <em>' + results.duration + '</em> ms</span> \
146
+ </div><table class="suites">' + map(results.allSuites, function(suite) {
147
+ var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
148
+ if (displaySuite && suite.hasSpecs())
149
+ return '<tr class="description"><td colspan="2">' + escape(suite.description) + '</td></tr>' +
150
+ map(suite.specs, function(i, spec) {
151
+ return '<tr class="' + (i % 2 ? 'odd' : 'even') + '">' +
152
+ (spec.requiresImplementation() ?
153
+ '<td class="requires-implementation" colspan="2">' + escape(spec.description) + '</td>' :
154
+ (spec.passed() && !failuresOnly) ?
155
+ '<td class="pass">' + escape(spec.description)+ '</td><td>' + spec.assertionsGraph() + '</td>' :
156
+ !spec.passed() ?
157
+ '<td class="fail">' + escape(spec.description) +
158
+ map(spec.failures(), function(a){ return '<em>' + escape(a.message) + '</em>' }).join('') +
159
+ '</td><td>' + spec.assertionsGraph() + '</td>' :
160
+ '') +
161
+ '<tr class="body"><td colspan="2"><pre>' + bodyContents(spec.body) + '</pre></td></tr>'
162
+ }).join('') + '</tr>'
163
+ }).join('') + '</table></div>'
164
+ },
165
+
166
+ /**
167
+ * Terminal reporter.
168
+ *
169
+ * @api public
170
+ */
171
+
172
+ Terminal : function(results, options) {
173
+ failuresOnly = option('failuresOnly')
174
+ print(color("\n Passes: ", 'bold') + color(results.stats.passes, 'green') +
175
+ color(" Failures: ", 'bold') + color(results.stats.failures, 'red') +
176
+ color(" Duration: ", 'bold') + color(results.duration, 'green') + " ms \n")
177
+
178
+ function indent(string) {
179
+ return string.replace(/^(.)/gm, ' $1')
180
+ }
181
+
182
+ each(results.allSuites, function(suite) {
183
+ var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
184
+ if (displaySuite && suite.hasSpecs()) {
185
+ print(color(' ' + suite.description, 'bold'))
186
+ each(suite.specs, function(spec){
187
+ var assertionsGraph = inject(spec.assertions, '', function(graph, assertion){
188
+ return graph + color('.', assertion.passed ? 'green' : 'red')
189
+ })
190
+ if (spec.requiresImplementation())
191
+ print(color(' ' + spec.description, 'blue') + assertionsGraph)
192
+ else if (spec.passed() && !failuresOnly)
193
+ print(color(' ' + spec.description, 'green') + assertionsGraph)
194
+ else if (!spec.passed())
195
+ print(color(' ' + spec.description, 'red') + assertionsGraph +
196
+ "\n" + indent(map(spec.failures(), function(a){ return a.message }).join("\n")) + "\n")
197
+ })
198
+ print("")
199
+ }
200
+ })
201
+
202
+ quit(results.stats.failures)
203
+ },
204
+
205
+ /**
206
+ * Console reporter.
207
+ *
208
+ * @api public
209
+ */
210
+
211
+ Console : function(results, options) {
212
+ console.log('')
213
+ console.log('Passes: ' + results.stats.passes + ' Failures: ' + results.stats.failures)
214
+ each(results.allSuites, function(suite) {
215
+ if (suite.ran) {
216
+ console.group(suite.description)
217
+ each(suite.specs, function(spec){
218
+ var assertionCount = spec.assertions.length + ':'
219
+ if (spec.requiresImplementation())
220
+ console.warn(spec.description)
221
+ else if (spec.passed())
222
+ console.log(assertionCount + ' ' + spec.description)
223
+ else
224
+ console.error(assertionCount + ' ' + spec.description + ', ' + spec.failure().message)
225
+ })
226
+ console.groupEnd()
227
+ }
228
+ })
229
+ }
230
+ },
231
+
232
+ Assertion : function(matcher, actual, expected, negate) {
233
+ extend(this, {
234
+ message: '',
235
+ passed: false,
236
+ actual: actual,
237
+ negate: negate,
238
+ matcher: matcher,
239
+ expected: expected,
240
+
241
+ // Report assertion results
242
+
243
+ report : function() {
244
+ if (JSpec.assert)
245
+ this.passed ? JSpec.stats.passes++ : JSpec.stats.failures++
246
+ return this
247
+ },
248
+
249
+ // Run the assertion
250
+
251
+ run : function() {
252
+ // TODO: remove unshifting
253
+ expected.unshift(actual)
254
+ this.result = matcher.match.apply(this, expected)
255
+ this.passed = negate ? !this.result : this.result
256
+ if (!this.passed) this.message = matcher.message.call(this, actual, expected, negate, matcher.name)
257
+ return this
258
+ }
259
+ })
260
+ },
261
+
262
+ ProxyAssertion : function(object, method, times, negate) {
263
+ var self = this
264
+ var old = object[method]
265
+
266
+ // Proxy
267
+
268
+ object[method] = function(){
269
+ args = toArray(arguments)
270
+ result = old.apply(object, args)
271
+ self.calls.push({ args : args, result : result })
272
+ return result
273
+ }
274
+
275
+ // Times
276
+
277
+ this.times = {
278
+ once : 1,
279
+ twice : 2
280
+ }[times] || times || 1
281
+
282
+ extend(this, {
283
+ calls: [],
284
+ message: '',
285
+ defer: true,
286
+ passed: false,
287
+ negate: negate,
288
+ object: object,
289
+ method: method,
290
+
291
+ // Proxy return value
292
+
293
+ and_return : function(result) {
294
+ this.expectedResult = result
295
+ return this
296
+ },
297
+
298
+ // Proxy arguments passed
299
+
300
+ with_args : function() {
301
+ this.expectedArgs = toArray(arguments)
302
+ return this
303
+ },
304
+
305
+ // Check if any calls have failing results
306
+
307
+ anyResultsFail : function() {
308
+ return any(this.calls, function(call){
309
+ return self.expectedResult.an_instance_of ?
310
+ call.result.constructor != self.expectedResult.an_instance_of:
311
+ !equal(self.expectedResult, call.result)
312
+ })
313
+ },
314
+
315
+ // Check if any calls have passing results
316
+
317
+ anyResultsPass : function() {
318
+ return any(this.calls, function(call){
319
+ return self.expectedResult.an_instance_of ?
320
+ call.result.constructor == self.expectedResult.an_instance_of:
321
+ equal(self.expectedResult, call.result)
322
+ })
323
+ },
324
+
325
+ // Return the passing result
326
+
327
+ passingResult : function() {
328
+ return this.anyResultsPass().result
329
+ },
330
+
331
+ // Return the failing result
332
+
333
+ failingResult : function() {
334
+ return this.anyResultsFail().result
335
+ },
336
+
337
+ // Check if any arguments fail
338
+
339
+ anyArgsFail : function() {
340
+ return any(this.calls, function(call){
341
+ return any(self.expectedArgs, function(i, arg){
342
+ if (arg == null) return call.args[i] == null
343
+ return arg.an_instance_of ?
344
+ call.args[i].constructor != arg.an_instance_of:
345
+ !equal(arg, call.args[i])
346
+
347
+ })
348
+ })
349
+ },
350
+
351
+ // Check if any arguments pass
352
+
353
+ anyArgsPass : function() {
354
+ return any(this.calls, function(call){
355
+ return any(self.expectedArgs, function(i, arg){
356
+ return arg.an_instance_of ?
357
+ call.args[i].constructor == arg.an_instance_of:
358
+ equal(arg, call.args[i])
359
+
360
+ })
361
+ })
362
+ },
363
+
364
+ // Return the passing args
365
+
366
+ passingArgs : function() {
367
+ return this.anyArgsPass().args
368
+ },
369
+
370
+ // Return the failing args
371
+
372
+ failingArgs : function() {
373
+ return this.anyArgsFail().args
374
+ },
375
+
376
+ // Report assertion results
377
+
378
+ report : function() {
379
+ if (JSpec.assert)
380
+ this.passed ? ++JSpec.stats.passes : ++JSpec.stats.failures
381
+ return this
382
+ },
383
+
384
+ // Run the assertion
385
+
386
+ run : function() {
387
+ var methodString = 'expected ' + object.toString() + '.' + method + '()' + (negate ? ' not' : '' )
388
+
389
+ function times(n) {
390
+ return n > 2 ? n + ' times' : { 1: 'once', 2: 'twice' }[n]
391
+ }
392
+
393
+ if (this.expectedResult != null && (negate ? this.anyResultsPass() : this.anyResultsFail()))
394
+ this.message = methodString + ' to return ' + puts(this.expectedResult) +
395
+ ' but ' + (negate ? 'it did' : 'got ' + puts(this.failingResult()))
396
+
397
+ if (this.expectedArgs && (negate ? !this.expectedResult && this.anyArgsPass() : this.anyArgsFail()))
398
+ this.message = methodString + ' to be called with ' + puts.apply(this, this.expectedArgs) +
399
+ ' but was' + (negate ? '' : ' called with ' + puts.apply(this, this.failingArgs()))
400
+
401
+ if (negate ? !this.expectedResult && !this.expectedArgs && this.calls.length >= this.times : this.calls.length != this.times)
402
+ this.message = methodString + ' to be called ' + times(this.times) +
403
+ ', but ' + (this.calls.length == 0 ? ' was not called' : ' was called ' + times(this.calls.length))
404
+
405
+ if (!this.message.length)
406
+ this.passed = true
407
+
408
+ return this
409
+ }
410
+ })
411
+ },
412
+
413
+ /**
414
+ * Specification Suite block object.
415
+ *
416
+ * @param {string} description
417
+ * @param {function} body
418
+ * @api private
419
+ */
420
+
421
+ Suite : function(description, body) {
422
+ var self = this
423
+ extend(this, {
424
+ body: body,
425
+ description: description,
426
+ suites: [],
427
+ specs: [],
428
+ ran: false,
429
+ hooks: { 'before' : [], 'after' : [], 'before_each' : [], 'after_each' : [] },
430
+
431
+ // Add a spec to the suite
432
+
433
+ addSpec : function(description, body) {
434
+ var spec = new JSpec.Spec(description, body)
435
+ this.specs.push(spec)
436
+ JSpec.stats.specs++ // TODO: abstract
437
+ spec.suite = this
438
+ },
439
+
440
+ // Add a hook to the suite
441
+
442
+ addHook : function(hook, body) {
443
+ this.hooks[hook].push(body)
444
+ },
445
+
446
+ // Add a nested suite
447
+
448
+ addSuite : function(description, body) {
449
+ var suite = new JSpec.Suite(description, body)
450
+ JSpec.allSuites.push(suite)
451
+ suite.name = suite.description
452
+ suite.description = this.description + ' ' + suite.description
453
+ this.suites.push(suite)
454
+ suite.suite = this
455
+ },
456
+
457
+ // Invoke a hook in context to this suite
458
+
459
+ hook : function(hook) {
460
+ if (this.suite) this.suite.hook(hook)
461
+ each(this.hooks[hook], function(body) {
462
+ JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + self.description + "': ")
463
+ })
464
+ },
465
+
466
+ // Check if nested suites are present
467
+
468
+ hasSuites : function() {
469
+ return this.suites.length
470
+ },
471
+
472
+ // Check if this suite has specs
473
+
474
+ hasSpecs : function() {
475
+ return this.specs.length
476
+ },
477
+
478
+ // Check if the entire suite passed
479
+
480
+ passed : function() {
481
+ return !any(this.specs, function(spec){
482
+ return !spec.passed()
483
+ })
484
+ }
485
+ })
486
+ },
487
+
488
+ /**
489
+ * Specification block object.
490
+ *
491
+ * @param {string} description
492
+ * @param {function} body
493
+ * @api private
494
+ */
495
+
496
+ Spec : function(description, body) {
497
+ extend(this, {
498
+ body: body,
499
+ description: description,
500
+ assertions: [],
501
+
502
+ // Add passing assertion
503
+
504
+ pass : function(message) {
505
+ this.assertions.push({ passed: true, message: message })
506
+ if (JSpec.assert) ++JSpec.stats.passes
507
+ },
508
+
509
+ // Add failing assertion
510
+
511
+ fail : function(message) {
512
+ this.assertions.push({ passed: false, message: message })
513
+ if (JSpec.assert) ++JSpec.stats.failures
514
+ },
515
+
516
+ // Run deferred assertions
517
+
518
+ runDeferredAssertions : function() {
519
+ each(this.assertions, function(assertion){
520
+ if (assertion.defer) assertion.run().report(), hook('afterAssertion', assertion)
521
+ })
522
+ },
523
+
524
+ // Find first failing assertion
525
+
526
+ failure : function() {
527
+ return find(this.assertions, function(assertion){
528
+ return !assertion.passed
529
+ })
530
+ },
531
+
532
+ // Find all failing assertions
533
+
534
+ failures : function() {
535
+ return select(this.assertions, function(assertion){
536
+ return !assertion.passed
537
+ })
538
+ },
539
+
540
+ // Weither or not the spec passed
541
+
542
+ passed : function() {
543
+ return !this.failure()
544
+ },
545
+
546
+ // Weither or not the spec requires implementation (no assertions)
547
+
548
+ requiresImplementation : function() {
549
+ return this.assertions.length == 0
550
+ },
551
+
552
+ // Sprite based assertions graph
553
+
554
+ assertionsGraph : function() {
555
+ return map(this.assertions, function(assertion){
556
+ return '<span class="assertion ' + (assertion.passed ? 'passed' : 'failed') + '"></span>'
557
+ }).join('')
558
+ }
559
+ })
560
+ },
561
+
562
+ Module : function(methods) {
563
+ extend(this, methods)
564
+ },
565
+
566
+ JSON : {
567
+
568
+ /**
569
+ * Generic sequences.
570
+ */
571
+
572
+ meta : {
573
+ '\b' : '\\b',
574
+ '\t' : '\\t',
575
+ '\n' : '\\n',
576
+ '\f' : '\\f',
577
+ '\r' : '\\r',
578
+ '"' : '\\"',
579
+ '\\' : '\\\\'
580
+ },
581
+
582
+ /**
583
+ * Escapable sequences.
584
+ */
585
+
586
+ escapable : /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
587
+
588
+ /**
589
+ * JSON encode _object_.
590
+ *
591
+ * @param {mixed} object
592
+ * @return {string}
593
+ * @api private
594
+ */
595
+
596
+ encode : function(object) {
597
+ var self = this
598
+ if (object == undefined || object == null) return 'null'
599
+ if (object === true) return 'true'
600
+ if (object === false) return 'false'
601
+ switch (typeof object) {
602
+ case 'number': return object
603
+ case 'string': return this.escapable.test(object) ?
604
+ '"' + object.replace(this.escapable, function (a) {
605
+ return typeof self.meta[a] === 'string' ? self.meta[a] :
606
+ '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4)
607
+ }) + '"' :
608
+ '"' + object + '"'
609
+ case 'object':
610
+ if (object.constructor == Array)
611
+ return '[' + map(object, function(val){
612
+ return self.encode(val)
613
+ }).join(', ') + ']'
614
+ else if (object)
615
+ return '{' + map(object, function(key, val){
616
+ return self.encode(key) + ':' + self.encode(val)
617
+ }).join(', ') + '}'
618
+ }
619
+ return 'null'
620
+ }
621
+ },
622
+
623
+ // --- DSLs
624
+
625
+ DSLs : {
626
+ snake : {
627
+ expect : function(actual){
628
+ return JSpec.expect(actual)
629
+ },
630
+
631
+ describe : function(description, body) {
632
+ return JSpec.currentSuite.addSuite(description, body)
633
+ },
634
+
635
+ it : function(description, body) {
636
+ return JSpec.currentSuite.addSpec(description, body)
637
+ },
638
+
639
+ before : function(body) {
640
+ return JSpec.currentSuite.addHook('before', body)
641
+ },
642
+
643
+ after : function(body) {
644
+ return JSpec.currentSuite.addHook('after', body)
645
+ },
646
+
647
+ before_each : function(body) {
648
+ return JSpec.currentSuite.addHook('before_each', body)
649
+ },
650
+
651
+ after_each : function(body) {
652
+ return JSpec.currentSuite.addHook('after_each', body)
653
+ },
654
+
655
+ should_behave_like : function(description) {
656
+ return JSpec.shareBehaviorsOf(description)
657
+ }
658
+ }
659
+ },
660
+
661
+ // --- Methods
662
+
663
+ /**
664
+ * Check if _value_ is 'stop'. For use as a
665
+ * utility callback function.
666
+ *
667
+ * @param {mixed} value
668
+ * @return {bool}
669
+ * @api public
670
+ */
671
+
672
+ haveStopped : function(value) {
673
+ return value === 'stop'
674
+ },
675
+
676
+ /**
677
+ * Include _object_ which may be a hash or Module instance.
678
+ *
679
+ * @param {hash, Module} object
680
+ * @return {JSpec}
681
+ * @api public
682
+ */
683
+
684
+ include : function(object) {
685
+ var module = object.constructor == JSpec.Module ? object : new JSpec.Module(object)
686
+ this.modules.push(module)
687
+ if ('init' in module) module.init()
688
+ if ('utilities' in module) extend(this.defaultContext, module.utilities)
689
+ if ('matchers' in module) this.addMatchers(module.matchers)
690
+ if ('reporters' in module) extend(this.reporters, module.reporters)
691
+ if ('DSLs' in module)
692
+ each(module.DSLs, function(name, methods){
693
+ JSpec.DSLs[name] = JSpec.DSLs[name] || {}
694
+ extend(JSpec.DSLs[name], methods)
695
+ })
696
+ return this
697
+ },
698
+
699
+ /**
700
+ * Add a module hook _name_, which is immediately
701
+ * called per module with the _args_ given. An array of
702
+ * hook return values is returned.
703
+ *
704
+ * @param {name} string
705
+ * @param {...} args
706
+ * @return {array}
707
+ * @api private
708
+ */
709
+
710
+ hook : function(name, args) {
711
+ args = toArray(arguments, 1)
712
+ return inject(JSpec.modules, [], function(results, module){
713
+ if (typeof module[name] == 'function')
714
+ results.push(JSpec.evalHook(module, name, args))
715
+ })
716
+ },
717
+
718
+ /**
719
+ * Eval _module_ hook _name_ with _args_. Evaluates in context
720
+ * to the module itself, JSpec, and JSpec.context.
721
+ *
722
+ * @param {Module} module
723
+ * @param {string} name
724
+ * @param {array} args
725
+ * @return {mixed}
726
+ * @api private
727
+ */
728
+
729
+ evalHook : function(module, name, args) {
730
+ hook('evaluatingHookBody', module, name)
731
+ try { return module[name].apply(module, args) }
732
+ catch(e) { error('Error in hook ' + module.name + '.' + name + ': ', e) }
733
+ },
734
+
735
+ /**
736
+ * Same as hook() however accepts only one _arg_ which is
737
+ * considered immutable. This function passes the arg
738
+ * to the first module, then passes the return value of the last
739
+ * module called, to the following module.
740
+ *
741
+ * @param {string} name
742
+ * @param {mixed} arg
743
+ * @return {mixed}
744
+ * @api private
745
+ */
746
+
747
+ hookImmutable : function(name, arg) {
748
+ return inject(JSpec.modules, arg, function(result, module){
749
+ if (typeof module[name] == 'function')
750
+ return JSpec.evalHook(module, name, [result])
751
+ })
752
+ },
753
+
754
+ /**
755
+ * Find a suite by its description or name.
756
+ *
757
+ * @param {string} description
758
+ * @return {Suite}
759
+ * @api private
760
+ */
761
+
762
+ findSuite : function(description) {
763
+ return find(this.allSuites, function(suite){
764
+ return suite.name == description || suite.description == description
765
+ })
766
+ },
767
+
768
+ /**
769
+ * Share behaviors (specs) of the given suite with
770
+ * the current suite.
771
+ *
772
+ * @param {string} description
773
+ * @api public
774
+ */
775
+
776
+ shareBehaviorsOf : function(description) {
777
+ if (suite = this.findSuite(description)) this.copySpecs(suite, this.currentSuite)
778
+ else throw 'failed to share behaviors. ' + puts(description) + ' is not a valid Suite name'
779
+ },
780
+
781
+ /**
782
+ * Copy specs from one suite to another.
783
+ *
784
+ * @param {Suite} fromSuite
785
+ * @param {Suite} toSuite
786
+ * @api public
787
+ */
788
+
789
+ copySpecs : function(fromSuite, toSuite) {
790
+ each(fromSuite.specs, function(spec){
791
+ spec.assertions = []
792
+ toSuite.specs.push(spec)
793
+ })
794
+ },
795
+
796
+ /**
797
+ * Convert arguments to an array.
798
+ *
799
+ * @param {object} arguments
800
+ * @param {int} offset
801
+ * @return {array}
802
+ * @api public
803
+ */
804
+
805
+ toArray : function(arguments, offset) {
806
+ return Array.prototype.slice.call(arguments, offset || 0)
807
+ },
808
+
809
+ /**
810
+ * Return ANSI-escaped colored string.
811
+ *
812
+ * @param {string} string
813
+ * @param {string} color
814
+ * @return {string}
815
+ * @api public
816
+ */
817
+
818
+ color : function(string, color) {
819
+ return "\u001B[" + {
820
+ bold : 1,
821
+ black : 30,
822
+ red : 31,
823
+ green : 32,
824
+ yellow : 33,
825
+ blue : 34,
826
+ magenta : 35,
827
+ cyan : 36,
828
+ white : 37
829
+ }[color] + 'm' + string + "\u001B[0m"
830
+ },
831
+
832
+ /**
833
+ * Default matcher message callback.
834
+ *
835
+ * @api private
836
+ */
837
+
838
+ defaultMatcherMessage : function(actual, expected, negate, name) {
839
+ return 'expected ' + puts(actual) + ' to ' +
840
+ (negate ? 'not ' : '') +
841
+ name.replace(/_/g, ' ') +
842
+ ' ' + (expected.length > 1 ?
843
+ puts.apply(this, expected.slice(1)) :
844
+ '')
845
+ },
846
+
847
+ /**
848
+ * Normalize a matcher message.
849
+ *
850
+ * When no messge callback is present the defaultMatcherMessage
851
+ * will be assigned, will suffice for most matchers.
852
+ *
853
+ * @param {hash} matcher
854
+ * @return {hash}
855
+ * @api public
856
+ */
857
+
858
+ normalizeMatcherMessage : function(matcher) {
859
+ if (typeof matcher.message != 'function')
860
+ matcher.message = this.defaultMatcherMessage
861
+ return matcher
862
+ },
863
+
864
+ /**
865
+ * Normalize a matcher body
866
+ *
867
+ * This process allows the following conversions until
868
+ * the matcher is in its final normalized hash state.
869
+ *
870
+ * - '==' becomes 'actual == expected'
871
+ * - 'actual == expected' becomes 'return actual == expected'
872
+ * - function(actual, expected) { return actual == expected } becomes
873
+ * { match : function(actual, expected) { return actual == expected }}
874
+ *
875
+ * @param {mixed} body
876
+ * @return {hash}
877
+ * @api public
878
+ */
879
+
880
+ normalizeMatcherBody : function(body) {
881
+ switch (body.constructor) {
882
+ case String:
883
+ if (captures = body.match(/^alias (\w+)/)) return JSpec.matchers[last(captures)]
884
+ if (body.length < 4) body = 'actual ' + body + ' expected'
885
+ return { match: function(actual, expected) { return eval(body) }}
886
+
887
+ case Function:
888
+ return { match: body }
889
+
890
+ default:
891
+ return body
892
+ }
893
+ },
894
+
895
+ /**
896
+ * Get option value. This method first checks if
897
+ * the option key has been set via the query string,
898
+ * otherwise returning the options hash value.
899
+ *
900
+ * @param {string} key
901
+ * @return {mixed}
902
+ * @api public
903
+ */
904
+
905
+ option : function(key) {
906
+ return (value = query(key)) !== null ? value :
907
+ JSpec.options[key] || null
908
+ },
909
+
910
+ /**
911
+ * Check if object _a_, is equal to object _b_.
912
+ *
913
+ * @param {object} a
914
+ * @param {object} b
915
+ * @return {bool}
916
+ * @api private
917
+ */
918
+
919
+ equal: function(a, b) {
920
+ if (typeof a != typeof b) return
921
+ if (a === b) return true
922
+ if (a instanceof RegExp)
923
+ return a.toString() === b.toString()
924
+ if (a instanceof Date)
925
+ return Number(a) === Number(b)
926
+ if (typeof a != 'object') return
927
+ if (a.length !== undefined)
928
+ if (a.length !== b.length) return
929
+ else
930
+ for (var i = 0, len = a.length; i < len; ++i)
931
+ if (!equal(a[i], b[i]))
932
+ return
933
+ for (var key in a)
934
+ if (!equal(a[key], b[key]))
935
+ return
936
+ return true
937
+ },
938
+
939
+ /**
940
+ * Return last element of an array.
941
+ *
942
+ * @param {array} array
943
+ * @return {object}
944
+ * @api public
945
+ */
946
+
947
+ last : function(array) {
948
+ return array[array.length - 1]
949
+ },
950
+
951
+ /**
952
+ * Convert object(s) to a print-friend string.
953
+ *
954
+ * @param {...} object
955
+ * @return {string}
956
+ * @api public
957
+ */
958
+
959
+ puts : function(object) {
960
+ if (arguments.length > 1)
961
+ return map(toArray(arguments), function(arg){
962
+ return puts(arg)
963
+ }).join(', ')
964
+ if (object === undefined) return 'undefined'
965
+ if (object === null) return 'null'
966
+ if (object === true) return 'true'
967
+ if (object === false) return 'false'
968
+ if (object.an_instance_of) return 'an instance of ' + object.an_instance_of.name
969
+ if (object.jquery && object.selector.length > 0) return 'selector ' + puts(object.selector)
970
+ if (object.jquery) return object.get(0).outerHTML
971
+ if (object.nodeName) return object.outerHTML
972
+ switch (object.constructor) {
973
+ case Function: return object.name || object
974
+ case String:
975
+ return '"' + object
976
+ .replace(/"/g, '\\"')
977
+ .replace(/\n/g, '\\n')
978
+ .replace(/\t/g, '\\t')
979
+ + '"'
980
+ case Array:
981
+ return inject(object, '[', function(b, v){
982
+ return b + ', ' + puts(v)
983
+ }).replace('[,', '[') + ' ]'
984
+ case Object:
985
+ object.__hit__ = true
986
+ return inject(object, '{', function(b, k, v) {
987
+ if (k == '__hit__') return b
988
+ return b + ', ' + k + ': ' + (v && v.__hit__ ? '<circular reference>' : puts(v))
989
+ }).replace('{,', '{') + ' }'
990
+ default:
991
+ return object.toString()
992
+ }
993
+ },
994
+
995
+ /**
996
+ * Escape HTML.
997
+ *
998
+ * @param {string} html
999
+ * @return {string}
1000
+ * @api public
1001
+ */
1002
+
1003
+ escape : function(html) {
1004
+ return html.toString()
1005
+ .replace(/&/gmi, '&amp;')
1006
+ .replace(/"/gmi, '&quot;')
1007
+ .replace(/>/gmi, '&gt;')
1008
+ .replace(/</gmi, '&lt;')
1009
+ },
1010
+
1011
+ /**
1012
+ * Perform an assertion without reporting.
1013
+ *
1014
+ * This method is primarily used for internal
1015
+ * matchers in order retain DRYness. May be invoked
1016
+ * like below:
1017
+ *
1018
+ * does('foo', 'eql', 'foo')
1019
+ * does([1,2], 'include', 1, 2)
1020
+ *
1021
+ * External hooks are not run for internal assertions
1022
+ * performed by does().
1023
+ *
1024
+ * @param {mixed} actual
1025
+ * @param {string} matcher
1026
+ * @param {...} expected
1027
+ * @return {mixed}
1028
+ * @api private
1029
+ */
1030
+
1031
+ does : function(actual, matcher, expected) {
1032
+ var assertion = new JSpec.Assertion(JSpec.matchers[matcher], actual, toArray(arguments, 2))
1033
+ return assertion.run().result
1034
+ },
1035
+
1036
+ /**
1037
+ * Perform an assertion.
1038
+ *
1039
+ * expect(true).to('be', true)
1040
+ * expect('foo').not_to('include', 'bar')
1041
+ * expect([1, [2]]).to('include', 1, [2])
1042
+ *
1043
+ * @param {mixed} actual
1044
+ * @return {hash}
1045
+ * @api public
1046
+ */
1047
+
1048
+ expect : function(actual) {
1049
+ function assert(matcher, args, negate) {
1050
+ var expected = toArray(args, 1)
1051
+ matcher.negate = negate
1052
+ assertion = new JSpec.Assertion(matcher, actual, expected, negate)
1053
+ hook('beforeAssertion', assertion)
1054
+ if (matcher.defer) assertion.run()
1055
+ else JSpec.currentSpec.assertions.push(assertion.run().report()), hook('afterAssertion', assertion)
1056
+ return assertion.result
1057
+ }
1058
+
1059
+ function to(matcher) {
1060
+ return assert(matcher, arguments, false)
1061
+ }
1062
+
1063
+ function not_to(matcher) {
1064
+ return assert(matcher, arguments, true)
1065
+ }
1066
+
1067
+ return {
1068
+ to : to,
1069
+ should : to,
1070
+ not_to: not_to,
1071
+ should_not : not_to
1072
+ }
1073
+ },
1074
+
1075
+ /**
1076
+ * Strim whitespace or chars.
1077
+ *
1078
+ * @param {string} string
1079
+ * @param {string} chars
1080
+ * @return {string}
1081
+ * @api public
1082
+ */
1083
+
1084
+ strip : function(string, chars) {
1085
+ return string.
1086
+ replace(new RegExp('[' + (chars || '\\s') + ']*$'), '').
1087
+ replace(new RegExp('^[' + (chars || '\\s') + ']*'), '')
1088
+ },
1089
+
1090
+ /**
1091
+ * Call an iterator callback with arguments a, or b
1092
+ * depending on the arity of the callback.
1093
+ *
1094
+ * @param {function} callback
1095
+ * @param {mixed} a
1096
+ * @param {mixed} b
1097
+ * @return {mixed}
1098
+ * @api private
1099
+ */
1100
+
1101
+ callIterator : function(callback, a, b) {
1102
+ return callback.length == 1 ? callback(b) : callback(a, b)
1103
+ },
1104
+
1105
+ /**
1106
+ * Extend an object with another.
1107
+ *
1108
+ * @param {object} object
1109
+ * @param {object} other
1110
+ * @api public
1111
+ */
1112
+
1113
+ extend : function(object, other) {
1114
+ each(other, function(property, value){
1115
+ object[property] = value
1116
+ })
1117
+ },
1118
+
1119
+ /**
1120
+ * Iterate an object, invoking the given callback.
1121
+ *
1122
+ * @param {hash, array} object
1123
+ * @param {function} callback
1124
+ * @return {JSpec}
1125
+ * @api public
1126
+ */
1127
+
1128
+ each : function(object, callback) {
1129
+ if (object.constructor == Array)
1130
+ for (var i = 0, len = object.length; i < len; ++i)
1131
+ callIterator(callback, i, object[i])
1132
+ else
1133
+ for (var key in object)
1134
+ if (object.hasOwnProperty(key))
1135
+ callIterator(callback, key, object[key])
1136
+ },
1137
+
1138
+ /**
1139
+ * Iterate with memo.
1140
+ *
1141
+ * @param {hash, array} object
1142
+ * @param {object} memo
1143
+ * @param {function} callback
1144
+ * @return {object}
1145
+ * @api public
1146
+ */
1147
+
1148
+ inject : function(object, memo, callback) {
1149
+ each(object, function(key, value){
1150
+ memo = (callback.length == 2 ?
1151
+ callback(memo, value):
1152
+ callback(memo, key, value)) ||
1153
+ memo
1154
+ })
1155
+ return memo
1156
+ },
1157
+
1158
+ /**
1159
+ * Destub _object_'s _method_. When no _method_ is passed
1160
+ * all stubbed methods are destubbed. When no arguments
1161
+ * are passed every object found in JSpec.stubbed will be
1162
+ * destubbed.
1163
+ *
1164
+ * @param {mixed} object
1165
+ * @param {string} method
1166
+ * @api public
1167
+ */
1168
+
1169
+ destub : function(object, method) {
1170
+ if (method) {
1171
+ if (object['__prototype__' + method])
1172
+ delete object[method]
1173
+ else
1174
+ object[method] = object['__original__' + method]
1175
+ delete object['__prototype__' + method]
1176
+ delete object['__original____' + method]
1177
+ }
1178
+ else if (object) {
1179
+ for (var key in object)
1180
+ if (captures = key.match(/^(?:__prototype__|__original__)(.*)/))
1181
+ destub(object, captures[1])
1182
+ }
1183
+ else
1184
+ while (JSpec.stubbed.length)
1185
+ destub(JSpec.stubbed.shift())
1186
+ },
1187
+
1188
+ /**
1189
+ * Stub _object_'s _method_.
1190
+ *
1191
+ * stub(foo, 'toString').and_return('bar')
1192
+ *
1193
+ * @param {mixed} object
1194
+ * @param {string} method
1195
+ * @return {hash}
1196
+ * @api public
1197
+ */
1198
+
1199
+ stub : function(object, method) {
1200
+ hook('stubbing', object, method)
1201
+ JSpec.stubbed.push(object)
1202
+ var type = object.hasOwnProperty(method) ? '__original__' : '__prototype__'
1203
+ object[type + method] = object[method]
1204
+ object[method] = function(){}
1205
+ return {
1206
+ and_return : function(value) {
1207
+ if (typeof value == 'function') object[method] = value
1208
+ else object[method] = function(){ return value }
1209
+ }
1210
+ }
1211
+ },
1212
+
1213
+ /**
1214
+ * Map callback return values.
1215
+ *
1216
+ * @param {hash, array} object
1217
+ * @param {function} callback
1218
+ * @return {array}
1219
+ * @api public
1220
+ */
1221
+
1222
+ map : function(object, callback) {
1223
+ return inject(object, [], function(memo, key, value){
1224
+ memo.push(callIterator(callback, key, value))
1225
+ })
1226
+ },
1227
+
1228
+ /**
1229
+ * Returns the first matching expression or null.
1230
+ *
1231
+ * @param {hash, array} object
1232
+ * @param {function} callback
1233
+ * @return {mixed}
1234
+ * @api public
1235
+ */
1236
+
1237
+ any : function(object, callback) {
1238
+ return inject(object, null, function(state, key, value){
1239
+ if (state == undefined)
1240
+ return callIterator(callback, key, value) ? value : state
1241
+ })
1242
+ },
1243
+
1244
+ /**
1245
+ * Returns an array of values collected when the callback
1246
+ * given evaluates to true.
1247
+ *
1248
+ * @param {hash, array} object
1249
+ * @return {function} callback
1250
+ * @return {array}
1251
+ * @api public
1252
+ */
1253
+
1254
+ select : function(object, callback) {
1255
+ return inject(object, [], function(selected, key, value){
1256
+ if (callIterator(callback, key, value))
1257
+ selected.push(value)
1258
+ })
1259
+ },
1260
+
1261
+ /**
1262
+ * Define matchers.
1263
+ *
1264
+ * @param {hash} matchers
1265
+ * @api public
1266
+ */
1267
+
1268
+ addMatchers : function(matchers) {
1269
+ each(matchers, function(name, body){
1270
+ JSpec.addMatcher(name, body)
1271
+ })
1272
+ },
1273
+
1274
+ /**
1275
+ * Define a matcher.
1276
+ *
1277
+ * @param {string} name
1278
+ * @param {hash, function, string} body
1279
+ * @api public
1280
+ */
1281
+
1282
+ addMatcher : function(name, body) {
1283
+ hook('addingMatcher', name, body)
1284
+ if (name.indexOf(' ') != -1) {
1285
+ var matchers = name.split(/\s+/)
1286
+ var prefix = matchers.shift()
1287
+ each(matchers, function(name) {
1288
+ JSpec.addMatcher(prefix + '_' + name, body(name))
1289
+ })
1290
+ }
1291
+ this.matchers[name] = this.normalizeMatcherMessage(this.normalizeMatcherBody(body))
1292
+ this.matchers[name].name = name
1293
+ },
1294
+
1295
+ /**
1296
+ * Add a root suite to JSpec.
1297
+ *
1298
+ * @param {string} description
1299
+ * @param {body} function
1300
+ * @api public
1301
+ */
1302
+
1303
+ describe : function(description, body) {
1304
+ var suite = new JSpec.Suite(description, body)
1305
+ hook('addingSuite', suite)
1306
+ this.allSuites.push(suite)
1307
+ this.suites.push(suite)
1308
+ },
1309
+
1310
+ /**
1311
+ * Return the contents of a function body.
1312
+ *
1313
+ * @param {function} body
1314
+ * @return {string}
1315
+ * @api public
1316
+ */
1317
+
1318
+ contentsOf : function(body) {
1319
+ return body.toString().match(/^[^\{]*{((.*\n*)*)}/m)[1]
1320
+ },
1321
+
1322
+ /**
1323
+ * Evaluate a JSpec capture body.
1324
+ *
1325
+ * @param {function} body
1326
+ * @param {string} errorMessage (optional)
1327
+ * @return {Type}
1328
+ * @api private
1329
+ */
1330
+
1331
+ evalBody : function(body, errorMessage) {
1332
+ var dsl = this.DSL || this.DSLs.snake
1333
+ var matchers = this.matchers
1334
+ var context = this.context || this.defaultContext
1335
+ var contents = this.contentsOf(body)
1336
+ hook('evaluatingBody', dsl, matchers, context, contents)
1337
+ try { with (dsl){ with (context) { with (matchers) { eval(contents) }}} }
1338
+ catch(e) { error(errorMessage, e) }
1339
+ },
1340
+
1341
+ /**
1342
+ * Pre-process a string of JSpec.
1343
+ *
1344
+ * @param {string} input
1345
+ * @return {string}
1346
+ * @api private
1347
+ */
1348
+
1349
+ preprocess : function(input) {
1350
+ if (typeof input != 'string') return
1351
+ input = hookImmutable('preprocessing', input)
1352
+ return input.
1353
+ replace(/\t/g, ' ').
1354
+ replace(/\r\n|\n|\r/g, '\n').
1355
+ split('__END__')[0].
1356
+ replace(/([\w\.]+)\.(stub|destub)\((.*?)\)$/gm, '$2($1, $3)').
1357
+ replace(/describe\s+(.*?)$/gm, 'describe($1, function(){').
1358
+ replace(/^\s+it\s+(.*?)$/gm, ' it($1, function(){').
1359
+ replace(/^ *(before_each|after_each|before|after)(?= |\n|$)/gm, 'JSpec.currentSuite.addHook("$1", function(){').
1360
+ replace(/^\s*end(?=\s|$)/gm, '});').
1361
+ replace(/-\{/g, 'function(){').
1362
+ replace(/(\d+)\.\.(\d+)/g, function(_, a, b){ return range(a, b) }).
1363
+ replace(/\.should([_\.]not)?[_\.](\w+)(?: |;|$)(.*)$/gm, '.should$1_$2($3)').
1364
+ replace(/([\/\s]*)(.+?)\.(should(?:[_\.]not)?)[_\.](\w+)\((.*)\)\s*;?$/gm, '$1 expect($2).$3($4, $5)').
1365
+ replace(/, \)/g, ')').
1366
+ replace(/should\.not/g, 'should_not')
1367
+ },
1368
+
1369
+ /**
1370
+ * Create a range string which can be evaluated to a native array.
1371
+ *
1372
+ * @param {int} start
1373
+ * @param {int} end
1374
+ * @return {string}
1375
+ * @api public
1376
+ */
1377
+
1378
+ range : function(start, end) {
1379
+ var current = parseInt(start), end = parseInt(end), values = [current]
1380
+ if (end > current) while (++current <= end) values.push(current)
1381
+ else while (--current >= end) values.push(current)
1382
+ return '[' + values + ']'
1383
+ },
1384
+
1385
+ /**
1386
+ * Report on the results.
1387
+ *
1388
+ * @api public
1389
+ */
1390
+
1391
+ report : function() {
1392
+ this.duration = Number(new Date) - this.start
1393
+ hook('reporting', JSpec.options)
1394
+ new (JSpec.options.reporter || JSpec.reporters.DOM)(JSpec, JSpec.options)
1395
+ },
1396
+
1397
+ /**
1398
+ * Run the spec suites. Options are merged
1399
+ * with JSpec options when present.
1400
+ *
1401
+ * @param {hash} options
1402
+ * @return {JSpec}
1403
+ * @api public
1404
+ */
1405
+
1406
+ run : function(options) {
1407
+ if (any(hook('running'), haveStopped)) return this
1408
+ if (options) extend(this.options, options)
1409
+ this.start = Number(new Date)
1410
+ each(this.suites, function(suite) { JSpec.runSuite(suite) })
1411
+ return this
1412
+ },
1413
+
1414
+ /**
1415
+ * Run a suite.
1416
+ *
1417
+ * @param {Suite} suite
1418
+ * @api public
1419
+ */
1420
+
1421
+ runSuite : function(suite) {
1422
+ this.currentSuite = suite
1423
+ this.evalBody(suite.body)
1424
+ suite.ran = true
1425
+ hook('beforeSuite', suite), suite.hook('before')
1426
+ each(suite.specs, function(spec) {
1427
+ hook('beforeSpec', spec)
1428
+ suite.hook('before_each')
1429
+ JSpec.runSpec(spec)
1430
+ hook('afterSpec', spec)
1431
+ suite.hook('after_each')
1432
+ })
1433
+ if (suite.hasSuites()) {
1434
+ each(suite.suites, function(suite) {
1435
+ JSpec.runSuite(suite)
1436
+ })
1437
+ }
1438
+ hook('afterSuite', suite), suite.hook('after')
1439
+ this.stats.suitesFinished++
1440
+ },
1441
+
1442
+ /**
1443
+ * Report a failure for the current spec.
1444
+ *
1445
+ * @param {string} message
1446
+ * @api public
1447
+ */
1448
+
1449
+ fail : function(message) {
1450
+ JSpec.currentSpec.fail(message)
1451
+ },
1452
+
1453
+ /**
1454
+ * Report a passing assertion for the current spec.
1455
+ *
1456
+ * @param {string} message
1457
+ * @api public
1458
+ */
1459
+
1460
+ pass : function(message) {
1461
+ JSpec.currentSpec.pass(message)
1462
+ },
1463
+
1464
+ /**
1465
+ * Run a spec.
1466
+ *
1467
+ * @param {Spec} spec
1468
+ * @api public
1469
+ */
1470
+
1471
+ runSpec : function(spec) {
1472
+ this.currentSpec = spec
1473
+ try { this.evalBody(spec.body) }
1474
+ catch (e) { fail(e) }
1475
+ spec.runDeferredAssertions()
1476
+ destub()
1477
+ this.stats.specsFinished++
1478
+ this.stats.assertions += spec.assertions.length
1479
+ },
1480
+
1481
+ /**
1482
+ * Require a dependency, with optional message.
1483
+ *
1484
+ * @param {string} dependency
1485
+ * @param {string} message (optional)
1486
+ * @return {JSpec}
1487
+ * @api public
1488
+ */
1489
+
1490
+ requires : function(dependency, message) {
1491
+ hook('requiring', dependency, message)
1492
+ try { eval(dependency) }
1493
+ catch (e) { throw 'JSpec depends on ' + dependency + ' ' + message }
1494
+ return this
1495
+ },
1496
+
1497
+ /**
1498
+ * Query against the current query strings keys
1499
+ * or the queryString specified.
1500
+ *
1501
+ * @param {string} key
1502
+ * @param {string} queryString
1503
+ * @return {string, null}
1504
+ * @api private
1505
+ */
1506
+
1507
+ query : function(key, queryString) {
1508
+ var queryString = (queryString || (main.location ? main.location.search : null) || '').substring(1)
1509
+ return inject(queryString.split('&'), null, function(value, pair){
1510
+ parts = pair.split('=')
1511
+ return parts[0] == key ? parts[1].replace(/%20|\+/gmi, ' ') : value
1512
+ })
1513
+ },
1514
+
1515
+ /**
1516
+ * Throw a JSpec related error.
1517
+ *
1518
+ * @param {string} message
1519
+ * @param {Exception} e
1520
+ * @api public
1521
+ */
1522
+
1523
+ error : function(message, e) {
1524
+ throw (message ? message : '') + e.toString() +
1525
+ (e.line ? ' near line ' + e.line : '')
1526
+ },
1527
+
1528
+ /**
1529
+ * Ad-hoc POST request for JSpec server usage.
1530
+ *
1531
+ * @param {string} uri
1532
+ * @param {string} data
1533
+ * @api private
1534
+ */
1535
+
1536
+ post : function(uri, data) {
1537
+ if (any(hook('posting', uri, data), haveStopped)) return
1538
+ var request = this.xhr()
1539
+ request.open('POST', uri, false)
1540
+ request.setRequestHeader('Content-Type', 'application/json')
1541
+ request.send(JSpec.JSON.encode(data))
1542
+ },
1543
+
1544
+ /**
1545
+ * Instantiate an XMLHttpRequest.
1546
+ *
1547
+ * Here we utilize IE's lame ActiveXObjects first which
1548
+ * allow IE access serve files via the file: protocol, otherwise
1549
+ * we then default to XMLHttpRequest.
1550
+ *
1551
+ * @return {XMLHttpRequest, ActiveXObject}
1552
+ * @api private
1553
+ */
1554
+
1555
+ xhr : function() {
1556
+ return this.ieXhr() || new JSpec.request
1557
+ },
1558
+
1559
+ /**
1560
+ * Return Microsoft piece of crap ActiveXObject.
1561
+ *
1562
+ * @return {ActiveXObject}
1563
+ * @api public
1564
+ */
1565
+
1566
+ ieXhr : function() {
1567
+ function object(str) {
1568
+ try { return new ActiveXObject(str) } catch(e) {}
1569
+ }
1570
+ return object('Msxml2.XMLHTTP.6.0') ||
1571
+ object('Msxml2.XMLHTTP.3.0') ||
1572
+ object('Msxml2.XMLHTTP') ||
1573
+ object('Microsoft.XMLHTTP')
1574
+ },
1575
+
1576
+ /**
1577
+ * Check for HTTP request support.
1578
+ *
1579
+ * @return {bool}
1580
+ * @api private
1581
+ */
1582
+
1583
+ hasXhr : function() {
1584
+ return JSpec.request || 'ActiveXObject' in main
1585
+ },
1586
+
1587
+ /**
1588
+ * Try loading _file_ returning the contents
1589
+ * string or null. Chain to locate / read a file.
1590
+ *
1591
+ * @param {string} file
1592
+ * @return {string}
1593
+ * @api public
1594
+ */
1595
+
1596
+ tryLoading : function(file) {
1597
+ try { return JSpec.load(file) } catch (e) {}
1598
+ },
1599
+
1600
+ /**
1601
+ * Load a _file_'s contents.
1602
+ *
1603
+ * @param {string} file
1604
+ * @param {function} callback
1605
+ * @return {string}
1606
+ * @api public
1607
+ */
1608
+
1609
+ load : function(file, callback) {
1610
+ if (any(hook('loading', file), haveStopped)) return
1611
+ if ('readFile' in main)
1612
+ return readFile(file)
1613
+ else if (this.hasXhr()) {
1614
+ var request = this.xhr()
1615
+ request.open('GET', file, false)
1616
+ request.send(null)
1617
+ if (request.readyState == 4 &&
1618
+ (request.status == 0 ||
1619
+ request.status.toString().charAt(0) == 2))
1620
+ return request.responseText
1621
+ }
1622
+ else
1623
+ error("failed to load `" + file + "'")
1624
+ },
1625
+
1626
+ /**
1627
+ * Load, pre-process, and evaluate a file.
1628
+ *
1629
+ * @param {string} file
1630
+ * @param {JSpec}
1631
+ * @api public
1632
+ */
1633
+
1634
+ exec : function(file) {
1635
+ if (any(hook('executing', file), haveStopped)) return this
1636
+ eval('with (JSpec){' + this.preprocess(this.load(file)) + '}')
1637
+ return this
1638
+ }
1639
+ }
1640
+
1641
+ // --- Utility functions
1642
+
1643
+ var main = this
1644
+ var find = JSpec.any
1645
+ var utils = 'haveStopped stub hookImmutable hook destub map any last pass fail range each option inject select \
1646
+ error escape extend puts query strip color does addMatchers callIterator toArray equal'.split(/\s+/)
1647
+ while (utils.length) eval('var ' + utils[0] + ' = JSpec.' + utils.shift())
1648
+ if (!main.setTimeout) main.setTimeout = function(callback){ callback() }
1649
+
1650
+ // --- Matchers
1651
+
1652
+ addMatchers({
1653
+ equal : "===",
1654
+ eql : "equal(actual, expected)",
1655
+ be : "alias equal",
1656
+ be_greater_than : ">",
1657
+ be_less_than : "<",
1658
+ be_at_least : ">=",
1659
+ be_at_most : "<=",
1660
+ be_a : "actual.constructor == expected",
1661
+ be_an : "alias be_a",
1662
+ be_an_instance_of : "actual instanceof expected",
1663
+ be_null : "actual == null",
1664
+ be_true : "actual == true",
1665
+ be_false : "actual == false",
1666
+ be_undefined : "typeof actual == 'undefined'",
1667
+ be_type : "typeof actual == expected",
1668
+ match : "typeof actual == 'string' ? actual.match(expected) : false",
1669
+ respond_to : "typeof actual[expected] == 'function'",
1670
+ have_length : "actual.length == expected",
1671
+ be_within : "actual >= expected[0] && actual <= last(expected)",
1672
+ have_length_within : "actual.length >= expected[0] && actual.length <= last(expected)",
1673
+
1674
+ receive : { defer : true, match : function(actual, method, times) {
1675
+ proxy = new JSpec.ProxyAssertion(actual, method, times, this.negate)
1676
+ JSpec.currentSpec.assertions.push(proxy)
1677
+ return proxy
1678
+ }},
1679
+
1680
+ be_empty : function(actual) {
1681
+ if (actual.constructor == Object && actual.length == undefined)
1682
+ for (var key in actual)
1683
+ return false;
1684
+ return !actual.length
1685
+ },
1686
+
1687
+ include : function(actual) {
1688
+ for (state = true, i = 1; i < arguments.length; i++) {
1689
+ arg = arguments[i]
1690
+ switch (actual.constructor) {
1691
+ case String:
1692
+ case Number:
1693
+ case RegExp:
1694
+ case Function:
1695
+ state = actual.toString().indexOf(arg) !== -1
1696
+ break
1697
+
1698
+ case Object:
1699
+ state = arg in actual
1700
+ break
1701
+
1702
+ case Array:
1703
+ state = any(actual, function(value){ return equal(value, arg) })
1704
+ break
1705
+ }
1706
+ if (!state) return false
1707
+ }
1708
+ return true
1709
+ },
1710
+
1711
+ throw_error : { match : function(actual, expected, message) {
1712
+ try { actual() }
1713
+ catch (e) {
1714
+ this.e = e
1715
+ var assert = function(arg) {
1716
+ switch (arg.constructor) {
1717
+ case RegExp : return arg.test(e.message || e.toString())
1718
+ case String : return arg == (e.message || e.toString())
1719
+ case Function : return e instanceof arg || e.name == arg.name
1720
+ }
1721
+ }
1722
+ return message ? assert(expected) && assert(message) :
1723
+ expected ? assert(expected) :
1724
+ true
1725
+ }
1726
+ }, message : function(actual, expected, negate) {
1727
+ // TODO: refactor when actual is not in expected [0]
1728
+ var message_for = function(i) {
1729
+ if (expected[i] == undefined) return 'exception'
1730
+ switch (expected[i].constructor) {
1731
+ case RegExp : return 'exception matching ' + puts(expected[i])
1732
+ case String : return 'exception of ' + puts(expected[i])
1733
+ case Function : return expected[i].name || 'Error'
1734
+ }
1735
+ }
1736
+ exception = message_for(1) + (expected[2] ? ' and ' + message_for(2) : '')
1737
+ return 'expected ' + exception + (negate ? ' not ' : '' ) +
1738
+ ' to be thrown, but ' + (this.e ? 'got ' + puts(this.e) : 'nothing was')
1739
+ }},
1740
+
1741
+ have : function(actual, length, property) {
1742
+ return actual[property].length == length
1743
+ },
1744
+
1745
+ have_at_least : function(actual, length, property) {
1746
+ return actual[property].length >= length
1747
+ },
1748
+
1749
+ have_at_most :function(actual, length, property) {
1750
+ return actual[property].length <= length
1751
+ },
1752
+
1753
+ have_within : function(actual, range, property) {
1754
+ length = actual[property].length
1755
+ return length >= range.shift() && length <= range.pop()
1756
+ },
1757
+
1758
+ have_prop : function(actual, property, value) {
1759
+ return actual[property] == null ||
1760
+ actual[property] instanceof Function ? false:
1761
+ value == null ? true:
1762
+ does(actual[property], 'eql', value)
1763
+ },
1764
+
1765
+ have_property : function(actual, property, value) {
1766
+ return actual[property] == null ||
1767
+ actual[property] instanceof Function ? false:
1768
+ value == null ? true:
1769
+ value === actual[property]
1770
+ }
1771
+ })
1772
+
1773
+ })()