1 /** @fileOverview
  2       A mocking framework for screw-unit.  Allows you to temporarily mock out
  3       functions with objects you are not testing.
  4 
  5       @author <a href="mailto:topper@toppingdesign.com">Topper Bowers</a>
  6 */
  7 
  8 /** Test helper main namespace
  9     @namespace
 10 */
 11 var TH = (function () {
 12     /** @namespace */
 13     var publicObj = {};
 14     
 15     /** used to insert a dom mock for the test being run
 16         assumes a <div> with an id of "dom_test" and directory
 17         of dom_mocks at the same level as suite.html called "dom_mocks"
 18         
 19         @param {String} mock the name of the mock to put into the div (you can skip the html)
 20         @param {Object} opts takes an optional insert or a specific element (instead of #dom_test)
 21         
 22         @throws mock must be specified if no mock is specified
 23         
 24         @example
 25           TH.insertDomMock("some_mock"); // will insert dom_mocks/some_mock.html into <div id='#dom_test'><div>
 26           
 27         @name TH.insertDomMock
 28         @function
 29     */
 30     publicObj.insertDomMock = function(mock, opts) {
 31         if (!mock) {
 32             throw new Error("mock must be specified");
 33         }
 34         opts = opts || {};
 35         
 36         var element = opts.element || jQuery("#dom_test");
 37         
 38         element = jQuery(element);
 39         if (!/\.html$/.test(mock)) {
 40             mock += ".html";
 41         }
 42         
 43         var url = "dom_mocks/" + mock;
 44 
 45         var handleResponse = function (html) {
 46             if (opts.insert) {
 47                 element.append(html);
 48             } else {
 49                 element.html(html);
 50             }
 51         };
 52         
 53         // use jQuery here so we can mock out Ajax.Request for the tests
 54         var ajx = jQuery.ajax({
 55             url: url,
 56             async: false,
 57             type: 'GET',
 58             success: handleResponse
 59         });
 60     };
 61 
 62     /** simulate a browser click on an element passed to the function
 63         @param {DomElement} el The element to receive the click
 64         
 65         @name TH.click
 66         @function
 67     */
 68     publicObj.click = function(el) {
 69         if(jQuery.browser.msie) {
 70           el.click();
 71         } else {
 72           var evt = document.createEvent("MouseEvents");
 73           evt.initEvent("click", true, true);
 74           el.dispatchEvent(evt);
 75         }
 76     };
 77     
 78     /** pause the operation of a page for X miliseconds
 79         @param {Number} millis the number of miliseconds to pause
 80         @name TH.pause
 81         @function
 82     */
 83     publicObj.pause = function (millis) {
 84         var date = new Date();
 85         var curDate = null;
 86 
 87         do { curDate = new Date(); }
 88         while(curDate-date < millis);
 89     };
 90 
 91     return publicObj;    
 92 })();
 93 
 94 /** Mock out objects using a fairly simple interface.  Also adds some counting functions.
 95     @example
 96       var someObj = {
 97           foo: function () { return 'bar' }
 98       };
 99       someObj.foo() == 'bar';
100       TH.Mock.Obj("someObj", {
101           foo: function () { return 'somethingElse' }
102       });
103       someObj.foo() == 'somethingElse'; // BUT!  Only for this test the next test will have a normal someObj;
104       
105     @example
106       var someObj = {
107           foo: function () { return 'bar' }
108       };
109       someObj.foo() == 'bar';
110       TH.Mock.Obj("someObj");
111       someObj.countCallsOf("foo");
112       someObj.foo() == true;
113       someObj.numberOfCallsTo("foo") == 1;
114       // using that you can then do cool expectations:  expect(someObj.numberOfCallsTo("foo")).to(equal, 1);
115 
116     @namespace
117 */
118 TH.Mock = (function () {
119     /** @namespace */
120     var publicObj = {};
121     /** the mocked out objects
122         @name TH.Mock.mockedObjects
123     */
124     publicObj.mockedObjects = {};
125     
126     /** taken from prototype - bind a function to a certain object
127         @private
128     */
129     var bind = function(func, obj) {
130         if (obj === undefined) return func;
131         var __method = func, object = obj;
132         return function() {
133           return __method.apply(object, jQuery.makeArray(arguments));
134         }
135     };
136     
137     /** taken from prototype - copy one object to another
138         @private
139     */
140     var extendObject = function (destination, source) {
141         for (var property in source) {
142             destination[property] = source[property];
143         }
144         return destination;
145     };
146     
147     /** this is used as a constructor to make a new mocked object and cache it
148         @private
149         @constructs
150     */
151     var MockedObject = function (props) {
152         extendObject(this, props);
153         this.countCallsCache = {};
154     };
155     
156     /** Adds the countCallsOf and numberOfCallsTo methods to any object that
157         is getting mocked
158         @private
159     */
160     MockedObject.prototype = {
161         countCallsOf: function (propString) {
162             this.countCallsCache[propString] = {};
163             var prop = this.countCallsCache[propString];
164             prop.count = 0;
165             this[propString] = bind(function () {
166                 this.countCallsCache[propString].count++;
167             }, this);         
168         },
169         numberOfCallsTo: function(propString) {
170             return this.countCallsCache[propString].count;
171         }
172         
173     };
174     
175     /** main mocking interface
176         
177         @param {String} mockString the string representation of the object you are trying to mock
178         @param {Object} newObj (optional) functions you want to mock on the other object
179     
180         @throws if the mockString does not eval into an object
181     
182         @see TH.Mock
183         @name TH.Mock.obj
184         @function
185     */
186     publicObj.obj = function (mockString, newObj) {
187         var oldObj = eval(mockString);
188         var obj;
189         if (!(typeof oldObj == "object")) {
190             throw new Error("TH.Mock.obj called on a string that doesn't evaluate into an object");
191         }
192         obj = new MockedObject(oldObj);
193         extendObject(obj, newObj);
194         
195         publicObj.mockedObjects[mockString] = {};
196         publicObj.mockedObjects[mockString].newObj = obj;
197         publicObj.mockedObjects[mockString].oldObj = oldObj;
198         
199         eval(mockString + " = TH.Mock.mockedObjects['" + mockString + "'].newObj");
200         
201         return obj;
202     };
203     
204     /** Used in a before() to reset all the objects that have been mocked to their original splendor
205         @name TH.Mock.reset
206         @function
207     */
208     publicObj.reset = function () {
209         var m;
210         var obj;
211         for (mockString in publicObj.mockedObjects) {
212             if (publicObj.mockedObjects.hasOwnProperty(mockString)) {
213                 eval(mockString + " = TH.Mock.mockedObjects['" + mockString + "'].oldObj");
214             }
215         }
216         publicObj.mockedObjects = {};
217     };
218     
219     /** this will let you call TH.Mock.numberOfCallsTo("name", "prop") - for convenience
220         @name TH.Mock.numberOfCallsTo
221         @function
222     */
223     publicObj.numberOfCallsTo = function (mockString, propString) {
224         var obj = eval(mockString);
225         return obj.numberOfCallsTo(propString);
226     };
227 
228     // for dev
229     publicObj.dirMocks = function () {
230         console.dir(mockCache);
231     };
232     
233     publicObj.dirCountCalls = function () {
234         console.dir(countCallsCache);
235     };
236     
237     return publicObj;
238 })();
239 
240 /** This mocks out Prototype's ajax calls so that you don't need a server in your tests
241     @example
242       TH.Ajax.mock("/a_url", "someText", 200);
243       var ajx = new Ajax.Request("/a_url", {
244           onComplete: function (resp) { response = resp }
245       });
246       expect(response.responseText).to(equal, "someText");
247       
248     @namespace
249 */
250 TH.Ajax = (function () {
251     /** @namespace */
252     var publicObj = {};
253     
254     var mockAjaxHash = {};
255     
256     /** Lets you count the number of requests on a certain URL
257         @example
258           TH.Ajax.mock("/a_url", "someText", 200);
259            var ajx = new Ajax.Request("/a_url", {
260                onComplete: function (resp) { response = resp }
261            });
262            expect(TH.Ajax.requestCont["/a_url"]).to(equal, 1);
263     
264         @name TH.Ajax.requestCount
265     */
266     publicObj.requestCount = {};
267     
268     /** Reset the request count - used in a before()
269         @name TH.Ajax.reset
270         @function
271     */
272     publicObj.reset = function () {
273         publicObj.requestCount = {};
274     };
275     
276     /** this is the main mocking interface
277         @param {String} urlToMock the url that you want to respond with your response
278         @param {String} response the text you want the server to send back. Text will try to be
279           evaled into JSON so that responseJSON can be set.
280         @param {Number} status (optional) the response code you want the server to send
281         
282         @example
283           TH.Ajax.mock("/a_url", "someText", 200);
284           var ajx = new Ajax.Request("/a_url", {
285               onComplete: function (resp) { response = resp }
286           });
287           expect(response.responseText).to(equal, "someText");
288         
289         @name TH.Ajax.mock
290         @function
291     */
292     publicObj.mock = function (urlToMock, response, status) {
293         status = status || 200;
294         mockAjaxHash[urlToMock] = { response: response, status: status };
295         
296         Ajax = {};
297         Ajax.Request = function(url, opts) {
298             if (!mockAjaxHash.hasOwnProperty(url)) {
299                 throw new Error("ajax request called with: " + url + " but no mock was found");
300             }
301             
302             if (!publicObj.requestCount[url]) {
303                 publicObj.requestCount[url] = 1;
304             } else {
305                 publicObj.requestCount[url]++;
306             }
307             
308             if(opts.onComplete || opts.onSuccess || opts.onFailure) {
309                 response = {};
310                 response.responseText = mockAjaxHash[url].response;
311                 response.status = mockAjaxHash[url].status;
312                 try {
313                     response.responseJSON = response.responseText.evalJSON();
314                 } catch (e) {
315                     response.responseJSON = null;
316                 }
317                 
318                 if ((response.status == 200) && opts.onSuccess) {
319                     opts.onSuccess(response);
320                 } else {                    
321                     if (opts.onFailure) {
322                         opts.onFailure(response);
323                     }
324                 }
325                 if (opts.onComplete) {
326                     opts.onComplete(response);
327                 }
328             }
329         };
330     };
331     
332     return publicObj;
333 })();
334