web/privatedoc/js/unittest.js
changeset 7 7a5d38af0e65
equal deleted inserted replaced
6:06c4f682b7b2 7:7a5d38af0e65
       
     1 // script.aculo.us unittest.js v1.6.4, Wed Sep 06 11:30:58 CEST 2006
       
     2 
       
     3 // Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
       
     4 //           (c) 2005 Jon Tirsen (http://www.tirsen.com)
       
     5 //           (c) 2005 Michael Schuerig (http://www.schuerig.de/michael/)
       
     6 //
       
     7 // Permission is hereby granted, free of charge, to any person obtaining
       
     8 // a copy of this software and associated documentation files (the
       
     9 // "Software"), to deal in the Software without restriction, including
       
    10 // without limitation the rights to use, copy, modify, merge, publish,
       
    11 // distribute, sublicense, and/or sell copies of the Software, and to
       
    12 // permit persons to whom the Software is furnished to do so, subject to
       
    13 // the following conditions:
       
    14 // 
       
    15 // The above copyright notice and this permission notice shall be
       
    16 // included in all copies or substantial portions of the Software.
       
    17 // 
       
    18 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
       
    19 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
       
    20 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
       
    21 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
       
    22 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       
    23 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
       
    24 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       
    25 
       
    26 
       
    27 // experimental, Firefox-only
       
    28 Event.simulateMouse = function(element, eventName) {
       
    29   var options = Object.extend({
       
    30     pointerX: 0,
       
    31     pointerY: 0,
       
    32     buttons: 0
       
    33   }, arguments[2] || {});
       
    34   var oEvent = document.createEvent("MouseEvents");
       
    35   oEvent.initMouseEvent(eventName, true, true, document.defaultView, 
       
    36     options.buttons, options.pointerX, options.pointerY, options.pointerX, options.pointerY, 
       
    37     false, false, false, false, 0, $(element));
       
    38   
       
    39   if(this.mark) Element.remove(this.mark);
       
    40   this.mark = document.createElement('div');
       
    41   this.mark.appendChild(document.createTextNode(" "));
       
    42   document.body.appendChild(this.mark);
       
    43   this.mark.style.position = 'absolute';
       
    44   this.mark.style.top = options.pointerY + "px";
       
    45   this.mark.style.left = options.pointerX + "px";
       
    46   this.mark.style.width = "5px";
       
    47   this.mark.style.height = "5px;";
       
    48   this.mark.style.borderTop = "1px solid red;"
       
    49   this.mark.style.borderLeft = "1px solid red;"
       
    50   
       
    51   if(this.step)
       
    52     alert('['+new Date().getTime().toString()+'] '+eventName+'/'+Test.Unit.inspect(options));
       
    53   
       
    54   $(element).dispatchEvent(oEvent);
       
    55 };
       
    56 
       
    57 // Note: Due to a fix in Firefox 1.0.5/6 that probably fixed "too much", this doesn't work in 1.0.6 or DP2.
       
    58 // You need to downgrade to 1.0.4 for now to get this working
       
    59 // See https://bugzilla.mozilla.org/show_bug.cgi?id=289940 for the fix that fixed too much
       
    60 Event.simulateKey = function(element, eventName) {
       
    61   var options = Object.extend({
       
    62     ctrlKey: false,
       
    63     altKey: false,
       
    64     shiftKey: false,
       
    65     metaKey: false,
       
    66     keyCode: 0,
       
    67     charCode: 0
       
    68   }, arguments[2] || {});
       
    69 
       
    70   var oEvent = document.createEvent("KeyEvents");
       
    71   oEvent.initKeyEvent(eventName, true, true, window, 
       
    72     options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
       
    73     options.keyCode, options.charCode );
       
    74   $(element).dispatchEvent(oEvent);
       
    75 };
       
    76 
       
    77 Event.simulateKeys = function(element, command) {
       
    78   for(var i=0; i<command.length; i++) {
       
    79     Event.simulateKey(element,'keypress',{charCode:command.charCodeAt(i)});
       
    80   }
       
    81 };
       
    82 
       
    83 var Test = {}
       
    84 Test.Unit = {};
       
    85 
       
    86 // security exception workaround
       
    87 Test.Unit.inspect = Object.inspect;
       
    88 
       
    89 Test.Unit.Logger = Class.create();
       
    90 Test.Unit.Logger.prototype = {
       
    91   initialize: function(log) {
       
    92     this.log = $(log);
       
    93     if (this.log) {
       
    94       this._createLogTable();
       
    95     }
       
    96   },
       
    97   start: function(testName) {
       
    98     if (!this.log) return;
       
    99     this.testName = testName;
       
   100     this.lastLogLine = document.createElement('tr');
       
   101     this.statusCell = document.createElement('td');
       
   102     this.nameCell = document.createElement('td');
       
   103     this.nameCell.appendChild(document.createTextNode(testName));
       
   104     this.messageCell = document.createElement('td');
       
   105     this.lastLogLine.appendChild(this.statusCell);
       
   106     this.lastLogLine.appendChild(this.nameCell);
       
   107     this.lastLogLine.appendChild(this.messageCell);
       
   108     this.loglines.appendChild(this.lastLogLine);
       
   109   },
       
   110   finish: function(status, summary) {
       
   111     if (!this.log) return;
       
   112     this.lastLogLine.className = status;
       
   113     this.statusCell.innerHTML = status;
       
   114     this.messageCell.innerHTML = this._toHTML(summary);
       
   115   },
       
   116   message: function(message) {
       
   117     if (!this.log) return;
       
   118     this.messageCell.innerHTML = this._toHTML(message);
       
   119   },
       
   120   summary: function(summary) {
       
   121     if (!this.log) return;
       
   122     this.logsummary.innerHTML = this._toHTML(summary);
       
   123   },
       
   124   _createLogTable: function() {
       
   125     this.log.innerHTML =
       
   126     '<div id="logsummary"></div>' +
       
   127     '<table id="logtable">' +
       
   128     '<thead><tr><th>Status</th><th>Test</th><th>Message</th></tr></thead>' +
       
   129     '<tbody id="loglines"></tbody>' +
       
   130     '</table>';
       
   131     this.logsummary = $('logsummary')
       
   132     this.loglines = $('loglines');
       
   133   },
       
   134   _toHTML: function(txt) {
       
   135     return txt.escapeHTML().replace(/\n/g,"<br/>");
       
   136   }
       
   137 }
       
   138 
       
   139 Test.Unit.Runner = Class.create();
       
   140 Test.Unit.Runner.prototype = {
       
   141   initialize: function(testcases) {
       
   142     this.options = Object.extend({
       
   143       testLog: 'testlog'
       
   144     }, arguments[1] || {});
       
   145     this.options.resultsURL = this.parseResultsURLQueryParameter();
       
   146     if (this.options.testLog) {
       
   147       this.options.testLog = $(this.options.testLog) || null;
       
   148     }
       
   149     if(this.options.tests) {
       
   150       this.tests = [];
       
   151       for(var i = 0; i < this.options.tests.length; i++) {
       
   152         if(/^test/.test(this.options.tests[i])) {
       
   153           this.tests.push(new Test.Unit.Testcase(this.options.tests[i], testcases[this.options.tests[i]], testcases["setup"], testcases["teardown"]));
       
   154         }
       
   155       }
       
   156     } else {
       
   157       if (this.options.test) {
       
   158         this.tests = [new Test.Unit.Testcase(this.options.test, testcases[this.options.test], testcases["setup"], testcases["teardown"])];
       
   159       } else {
       
   160         this.tests = [];
       
   161         for(var testcase in testcases) {
       
   162           if(/^test/.test(testcase)) {
       
   163             this.tests.push(
       
   164                new Test.Unit.Testcase(
       
   165                  this.options.context ? ' -> ' + this.options.titles[testcase] : testcase, 
       
   166                  testcases[testcase], testcases["setup"], testcases["teardown"]
       
   167                ));
       
   168           }
       
   169         }
       
   170       }
       
   171     }
       
   172     this.currentTest = 0;
       
   173     this.logger = new Test.Unit.Logger(this.options.testLog);
       
   174     setTimeout(this.runTests.bind(this), 1000);
       
   175   },
       
   176   parseResultsURLQueryParameter: function() {
       
   177     return window.location.search.parseQuery()["resultsURL"];
       
   178   },
       
   179   // Returns:
       
   180   //  "ERROR" if there was an error,
       
   181   //  "FAILURE" if there was a failure, or
       
   182   //  "SUCCESS" if there was neither
       
   183   getResult: function() {
       
   184     var hasFailure = false;
       
   185     for(var i=0;i<this.tests.length;i++) {
       
   186       if (this.tests[i].errors > 0) {
       
   187         return "ERROR";
       
   188       }
       
   189       if (this.tests[i].failures > 0) {
       
   190         hasFailure = true;
       
   191       }
       
   192     }
       
   193     if (hasFailure) {
       
   194       return "FAILURE";
       
   195     } else {
       
   196       return "SUCCESS";
       
   197     }
       
   198   },
       
   199   postResults: function() {
       
   200     if (this.options.resultsURL) {
       
   201       new Ajax.Request(this.options.resultsURL, 
       
   202         { method: 'get', parameters: 'result=' + this.getResult(), asynchronous: false });
       
   203     }
       
   204   },
       
   205   runTests: function() {
       
   206     var test = this.tests[this.currentTest];
       
   207     if (!test) {
       
   208       // finished!
       
   209       this.postResults();
       
   210       this.logger.summary(this.summary());
       
   211       return;
       
   212     }
       
   213     if(!test.isWaiting) {
       
   214       this.logger.start(test.name);
       
   215     }
       
   216     test.run();
       
   217     if(test.isWaiting) {
       
   218       this.logger.message("Waiting for " + test.timeToWait + "ms");
       
   219       setTimeout(this.runTests.bind(this), test.timeToWait || 1000);
       
   220     } else {
       
   221       this.logger.finish(test.status(), test.summary());
       
   222       this.currentTest++;
       
   223       // tail recursive, hopefully the browser will skip the stackframe
       
   224       this.runTests();
       
   225     }
       
   226   },
       
   227   summary: function() {
       
   228     var assertions = 0;
       
   229     var failures = 0;
       
   230     var errors = 0;
       
   231     var messages = [];
       
   232     for(var i=0;i<this.tests.length;i++) {
       
   233       assertions +=   this.tests[i].assertions;
       
   234       failures   +=   this.tests[i].failures;
       
   235       errors     +=   this.tests[i].errors;
       
   236     }
       
   237     return (
       
   238       (this.options.context ? this.options.context + ': ': '') + 
       
   239       this.tests.length + " tests, " + 
       
   240       assertions + " assertions, " + 
       
   241       failures   + " failures, " +
       
   242       errors     + " errors");
       
   243   }
       
   244 }
       
   245 
       
   246 Test.Unit.Assertions = Class.create();
       
   247 Test.Unit.Assertions.prototype = {
       
   248   initialize: function() {
       
   249     this.assertions = 0;
       
   250     this.failures   = 0;
       
   251     this.errors     = 0;
       
   252     this.messages   = [];
       
   253   },
       
   254   summary: function() {
       
   255     return (
       
   256       this.assertions + " assertions, " + 
       
   257       this.failures   + " failures, " +
       
   258       this.errors     + " errors" + "\n" +
       
   259       this.messages.join("\n"));
       
   260   },
       
   261   pass: function() {
       
   262     this.assertions++;
       
   263   },
       
   264   fail: function(message) {
       
   265     this.failures++;
       
   266     this.messages.push("Failure: " + message);
       
   267   },
       
   268   info: function(message) {
       
   269     this.messages.push("Info: " + message);
       
   270   },
       
   271   error: function(error) {
       
   272     this.errors++;
       
   273     this.messages.push(error.name + ": "+ error.message + "(" + Test.Unit.inspect(error) +")");
       
   274   },
       
   275   status: function() {
       
   276     if (this.failures > 0) return 'failed';
       
   277     if (this.errors > 0) return 'error';
       
   278     return 'passed';
       
   279   },
       
   280   assert: function(expression) {
       
   281     var message = arguments[1] || 'assert: got "' + Test.Unit.inspect(expression) + '"';
       
   282     try { expression ? this.pass() : 
       
   283       this.fail(message); }
       
   284     catch(e) { this.error(e); }
       
   285   },
       
   286   assertEqual: function(expected, actual) {
       
   287     var message = arguments[2] || "assertEqual";
       
   288     try { (expected == actual) ? this.pass() :
       
   289       this.fail(message + ': expected "' + Test.Unit.inspect(expected) + 
       
   290         '", actual "' + Test.Unit.inspect(actual) + '"'); }
       
   291     catch(e) { this.error(e); }
       
   292   },
       
   293   assertEnumEqual: function(expected, actual) {
       
   294     var message = arguments[2] || "assertEnumEqual";
       
   295     try { $A(expected).length == $A(actual).length && 
       
   296       expected.zip(actual).all(function(pair) { return pair[0] == pair[1] }) ?
       
   297         this.pass() : this.fail(message + ': expected ' + Test.Unit.inspect(expected) + 
       
   298           ', actual ' + Test.Unit.inspect(actual)); }
       
   299     catch(e) { this.error(e); }
       
   300   },
       
   301   assertNotEqual: function(expected, actual) {
       
   302     var message = arguments[2] || "assertNotEqual";
       
   303     try { (expected != actual) ? this.pass() : 
       
   304       this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); }
       
   305     catch(e) { this.error(e); }
       
   306   },
       
   307   assertIdentical: function(expected, actual) { 
       
   308     var message = arguments[2] || "assertIdentical"; 
       
   309     try { (expected === actual) ? this.pass() : 
       
   310       this.fail(message + ': expected "' + Test.Unit.inspect(expected) +  
       
   311         '", actual "' + Test.Unit.inspect(actual) + '"'); } 
       
   312     catch(e) { this.error(e); } 
       
   313   },
       
   314   assertNotIdentical: function(expected, actual) { 
       
   315     var message = arguments[2] || "assertNotIdentical"; 
       
   316     try { !(expected === actual) ? this.pass() : 
       
   317       this.fail(message + ': expected "' + Test.Unit.inspect(expected) +  
       
   318         '", actual "' + Test.Unit.inspect(actual) + '"'); } 
       
   319     catch(e) { this.error(e); } 
       
   320   },
       
   321   assertNull: function(obj) {
       
   322     var message = arguments[1] || 'assertNull'
       
   323     try { (obj==null) ? this.pass() : 
       
   324       this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); }
       
   325     catch(e) { this.error(e); }
       
   326   },
       
   327   assertMatch: function(expected, actual) {
       
   328     var message = arguments[2] || 'assertMatch';
       
   329     var regex = new RegExp(expected);
       
   330     try { (regex.exec(actual)) ? this.pass() :
       
   331       this.fail(message + ' : regex: "' +  Test.Unit.inspect(expected) + ' did not match: ' + Test.Unit.inspect(actual) + '"'); }
       
   332     catch(e) { this.error(e); }
       
   333   },
       
   334   assertHidden: function(element) {
       
   335     var message = arguments[1] || 'assertHidden';
       
   336     this.assertEqual("none", element.style.display, message);
       
   337   },
       
   338   assertNotNull: function(object) {
       
   339     var message = arguments[1] || 'assertNotNull';
       
   340     this.assert(object != null, message);
       
   341   },
       
   342   assertType: function(expected, actual) {
       
   343     var message = arguments[2] || 'assertType';
       
   344     try { 
       
   345       (actual.constructor == expected) ? this.pass() : 
       
   346       this.fail(message + ': expected "' + Test.Unit.inspect(expected) +  
       
   347         '", actual "' + (actual.constructor) + '"'); }
       
   348     catch(e) { this.error(e); }
       
   349   },
       
   350   assertNotOfType: function(expected, actual) {
       
   351     var message = arguments[2] || 'assertNotOfType';
       
   352     try { 
       
   353       (actual.constructor != expected) ? this.pass() : 
       
   354       this.fail(message + ': expected "' + Test.Unit.inspect(expected) +  
       
   355         '", actual "' + (actual.constructor) + '"'); }
       
   356     catch(e) { this.error(e); }
       
   357   },
       
   358   assertInstanceOf: function(expected, actual) {
       
   359     var message = arguments[2] || 'assertInstanceOf';
       
   360     try { 
       
   361       (actual instanceof expected) ? this.pass() : 
       
   362       this.fail(message + ": object was not an instance of the expected type"); }
       
   363     catch(e) { this.error(e); } 
       
   364   },
       
   365   assertNotInstanceOf: function(expected, actual) {
       
   366     var message = arguments[2] || 'assertNotInstanceOf';
       
   367     try { 
       
   368       !(actual instanceof expected) ? this.pass() : 
       
   369       this.fail(message + ": object was an instance of the not expected type"); }
       
   370     catch(e) { this.error(e); } 
       
   371   },
       
   372   assertRespondsTo: function(method, obj) {
       
   373     var message = arguments[2] || 'assertRespondsTo';
       
   374     try {
       
   375       (obj[method] && typeof obj[method] == 'function') ? this.pass() : 
       
   376       this.fail(message + ": object doesn't respond to [" + method + "]"); }
       
   377     catch(e) { this.error(e); }
       
   378   },
       
   379   assertReturnsTrue: function(method, obj) {
       
   380     var message = arguments[2] || 'assertReturnsTrue';
       
   381     try {
       
   382       var m = obj[method];
       
   383       if(!m) m = obj['is'+method.charAt(0).toUpperCase()+method.slice(1)];
       
   384       m() ? this.pass() : 
       
   385       this.fail(message + ": method returned false"); }
       
   386     catch(e) { this.error(e); }
       
   387   },
       
   388   assertReturnsFalse: function(method, obj) {
       
   389     var message = arguments[2] || 'assertReturnsFalse';
       
   390     try {
       
   391       var m = obj[method];
       
   392       if(!m) m = obj['is'+method.charAt(0).toUpperCase()+method.slice(1)];
       
   393       !m() ? this.pass() : 
       
   394       this.fail(message + ": method returned true"); }
       
   395     catch(e) { this.error(e); }
       
   396   },
       
   397   assertRaise: function(exceptionName, method) {
       
   398     var message = arguments[2] || 'assertRaise';
       
   399     try { 
       
   400       method();
       
   401       this.fail(message + ": exception expected but none was raised"); }
       
   402     catch(e) {
       
   403       (e.name==exceptionName) ? this.pass() : this.error(e); 
       
   404     }
       
   405   },
       
   406   assertElementsMatch: function() {
       
   407     var expressions = $A(arguments), elements = $A(expressions.shift());
       
   408     if (elements.length != expressions.length) {
       
   409       this.fail('assertElementsMatch: size mismatch: ' + elements.length + ' elements, ' + expressions.length + ' expressions');
       
   410       return false;
       
   411     }
       
   412     elements.zip(expressions).all(function(pair, index) {
       
   413       var element = $(pair.first()), expression = pair.last();
       
   414       if (element.match(expression)) return true;
       
   415       this.fail('assertElementsMatch: (in index ' + index + ') expected ' + expression.inspect() + ' but got ' + element.inspect());
       
   416     }.bind(this)) && this.pass();
       
   417   },
       
   418   assertElementMatches: function(element, expression) {
       
   419     this.assertElementsMatch([element], expression);
       
   420   },
       
   421   benchmark: function(operation, iterations) {
       
   422     var startAt = new Date();
       
   423     (iterations || 1).times(operation);
       
   424     var timeTaken = ((new Date())-startAt);
       
   425     this.info((arguments[2] || 'Operation') + ' finished ' + 
       
   426        iterations + ' iterations in ' + (timeTaken/1000)+'s' );
       
   427     return timeTaken;
       
   428   },
       
   429   _isVisible: function(element) {
       
   430     element = $(element);
       
   431     if(!element.parentNode) return true;
       
   432     this.assertNotNull(element);
       
   433     if(element.style && Element.getStyle(element, 'display') == 'none')
       
   434       return false;
       
   435     
       
   436     return this._isVisible(element.parentNode);
       
   437   },
       
   438   assertNotVisible: function(element) {
       
   439     this.assert(!this._isVisible(element), Test.Unit.inspect(element) + " was not hidden and didn't have a hidden parent either. " + ("" || arguments[1]));
       
   440   },
       
   441   assertVisible: function(element) {
       
   442     this.assert(this._isVisible(element), Test.Unit.inspect(element) + " was not visible. " + ("" || arguments[1]));
       
   443   },
       
   444   benchmark: function(operation, iterations) {
       
   445     var startAt = new Date();
       
   446     (iterations || 1).times(operation);
       
   447     var timeTaken = ((new Date())-startAt);
       
   448     this.info((arguments[2] || 'Operation') + ' finished ' + 
       
   449        iterations + ' iterations in ' + (timeTaken/1000)+'s' );
       
   450     return timeTaken;
       
   451   }
       
   452 }
       
   453 
       
   454 Test.Unit.Testcase = Class.create();
       
   455 Object.extend(Object.extend(Test.Unit.Testcase.prototype, Test.Unit.Assertions.prototype), {
       
   456   initialize: function(name, test, setup, teardown) {
       
   457     Test.Unit.Assertions.prototype.initialize.bind(this)();
       
   458     this.name           = name;
       
   459     
       
   460     if(typeof test == 'string') {
       
   461       test = test.gsub(/(\.should[^\(]+\()/,'#{0}this,');
       
   462       test = test.gsub(/(\.should[^\(]+)\(this,\)/,'#{1}(this)');
       
   463       this.test = function() {
       
   464         eval('with(this){'+test+'}');
       
   465       }
       
   466     } else {
       
   467       this.test = test || function() {};
       
   468     }
       
   469     
       
   470     this.setup          = setup || function() {};
       
   471     this.teardown       = teardown || function() {};
       
   472     this.isWaiting      = false;
       
   473     this.timeToWait     = 1000;
       
   474   },
       
   475   wait: function(time, nextPart) {
       
   476     this.isWaiting = true;
       
   477     this.test = nextPart;
       
   478     this.timeToWait = time;
       
   479   },
       
   480   run: function() {
       
   481     try {
       
   482       try {
       
   483         if (!this.isWaiting) this.setup.bind(this)();
       
   484         this.isWaiting = false;
       
   485         this.test.bind(this)();
       
   486       } finally {
       
   487         if(!this.isWaiting) {
       
   488           this.teardown.bind(this)();
       
   489         }
       
   490       }
       
   491     }
       
   492     catch(e) { this.error(e); }
       
   493   }
       
   494 });
       
   495 
       
   496 // *EXPERIMENTAL* BDD-style testing to please non-technical folk
       
   497 // This draws many ideas from RSpec http://rspec.rubyforge.org/
       
   498 
       
   499 Test.setupBDDExtensionMethods = function(){
       
   500   var METHODMAP = {
       
   501     shouldEqual:     'assertEqual',
       
   502     shouldNotEqual:  'assertNotEqual',
       
   503     shouldEqualEnum: 'assertEnumEqual',
       
   504     shouldBeA:       'assertType',
       
   505     shouldNotBeA:    'assertNotOfType',
       
   506     shouldBeAn:      'assertType',
       
   507     shouldNotBeAn:   'assertNotOfType',
       
   508     shouldBeNull:    'assertNull',
       
   509     shouldNotBeNull: 'assertNotNull',
       
   510     
       
   511     shouldBe:        'assertReturnsTrue',
       
   512     shouldNotBe:     'assertReturnsFalse',
       
   513     shouldRespondTo: 'assertRespondsTo'
       
   514   };
       
   515   Test.BDDMethods = {};
       
   516   for(m in METHODMAP) {
       
   517     Test.BDDMethods[m] = eval(
       
   518       'function(){'+
       
   519       'var args = $A(arguments);'+
       
   520       'var scope = args.shift();'+
       
   521       'scope.'+METHODMAP[m]+'.apply(scope,(args || []).concat([this])); }');
       
   522   }
       
   523   [Array.prototype, String.prototype, Number.prototype].each(
       
   524     function(p){ Object.extend(p, Test.BDDMethods) }
       
   525   );
       
   526 }
       
   527 
       
   528 Test.context = function(name, spec, log){
       
   529   Test.setupBDDExtensionMethods();
       
   530   
       
   531   var compiledSpec = {};
       
   532   var titles = {};
       
   533   for(specName in spec) {
       
   534     switch(specName){
       
   535       case "setup":
       
   536       case "teardown":
       
   537         compiledSpec[specName] = spec[specName];
       
   538         break;
       
   539       default:
       
   540         var testName = 'test'+specName.gsub(/\s+/,'-').camelize();
       
   541         var body = spec[specName].toString().split('\n').slice(1);
       
   542         if(/^\{/.test(body[0])) body = body.slice(1);
       
   543         body.pop();
       
   544         body = body.map(function(statement){ 
       
   545           return statement.strip()
       
   546         });
       
   547         compiledSpec[testName] = body.join('\n');
       
   548         titles[testName] = specName;
       
   549     }
       
   550   }
       
   551   new Test.Unit.Runner(compiledSpec, { titles: titles, testLog: log || 'testlog', context: name });
       
   552 };