PageRenderTime 100ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/v2/src/jsspec2.js

http://jsspec.googlecode.com/
JavaScript | 686 lines | 446 code | 116 blank | 124 comment | 72 complexity | f064e7d8803b842d5c9ff407ba577963 MD5 | raw file
Possible License(s): LGPL-2.1
  1. /**
  2. * @namespace Single namespace that contains all classes and functions of jsspec
  3. */
  4. var jsspec = {};
  5. /**
  6. * @class Base class to emulate classical class-based OOP
  7. */
  8. jsspec.Class = function() {};
  9. jsspec.Class._initializing = false;
  10. jsspec.Class._fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
  11. /**
  12. * @param {object} base Base class to be extended
  13. * @return {jsspec.Class} Extended class instance
  14. */
  15. jsspec.Class.extend = function(base) {
  16. // Inspired by http://ejohn.org/blog/simple-javascript-inheritance/
  17. var _super = this.prototype;
  18. jsspec.Class._initialiing = true;
  19. var prototype = new this();
  20. jsspec.Class._initialiing = false;
  21. for(var name in base) {
  22. prototype[name] = typeof base[name] == 'function' &&
  23. typeof _super[name] == 'function' && jsspec.Class._fnTest.test(base[name]) ?
  24. (function(name, fn){
  25. return function() {
  26. var tmp = this._super;
  27. this._super = _super[name];
  28. var ret = fn.apply(this, arguments);
  29. this._super = tmp;
  30. return ret;
  31. };
  32. })(name, base[name]) :
  33. base[name];
  34. }
  35. function Class() {
  36. if ( !jsspec.Class._initializing && this.init ) this.init.apply(this, arguments);
  37. }
  38. Class.prototype = prototype;
  39. Class.constructor = Class;
  40. Class.extend = arguments.callee;
  41. return Class;
  42. };
  43. /**
  44. * @class Encapsulates differences of various host environments
  45. * @extends jsspec.Class
  46. */
  47. jsspec.HostEnvironment = jsspec.Class.extend(/** @lends jsspec.HostEnvironment.prototype */{
  48. /**
  49. * Prints single line message to console
  50. *
  51. * @param {string} message A message to print
  52. */
  53. log: function(message) {throw 'Not implemented';},
  54. /**
  55. * @return {string} Short description of current host environment
  56. */
  57. getDescription: function() {return 'Unknown environment'}
  58. });
  59. /**
  60. * Static factory
  61. *
  62. * @returns {jsspec.HostEnvironment} Platform specific instance
  63. */
  64. jsspec.HostEnvironment.getInstance = function() {
  65. if(jsspec.root.navigator) {
  66. return new jsspec.BrowserHostEnvironment();
  67. } else if(jsspec.root.load) {
  68. return new jsspec.RhinoHostEnvironment();
  69. } else if(jsspec.root.WScript) {
  70. return new jsspec.WScriptHostEnvironment();
  71. }
  72. }
  73. /**
  74. * @class Browser host environment
  75. * @extends jsspec.HostEnvironment
  76. */
  77. jsspec.BrowserHostEnvironment = jsspec.HostEnvironment.extend(/** @lends jsspec.BrowserHostEnvironment.prototype */{
  78. log: function(message) {
  79. jsspec.root.document.title = message;
  80. var escaped = (message + '\n').replace(/</img, '&lt;').replace(/\n/img, '<br />');
  81. jsspec.root.document.write(escaped);
  82. },
  83. getDescription: function() {
  84. return jsspec.root.navigator.userAgent;
  85. },
  86. _findBasePath: function() {
  87. var scripts = document.getElementsByTagName("script");
  88. for(var i = 0; i < scripts.length; i++) {
  89. var script = scripts[i];
  90. if(script.src && script.src.match(/jsspec2\.js/i)) {
  91. return script.src.match(/(.*\/)jsspec2\.js.*/i)[1];
  92. }
  93. }
  94. return './';
  95. }
  96. });
  97. /**
  98. * @class Rhino host environment
  99. * @extends jsspec.HostEnvironment
  100. */
  101. jsspec.RhinoHostEnvironment = jsspec.HostEnvironment.extend(/** @lends jsspec.RhinoHostEnvironment.prototype */{
  102. log: function(message) {
  103. jsspec.root.print(message);
  104. },
  105. getDescription: function() {
  106. return 'Rhino (Java ' + jsspec.root.environment['java.version'] + ')';
  107. },
  108. _findBasePath: function() {
  109. return jsspec.root.environment['user.dir'] + jsspec.root.environment['file.separator'];
  110. }
  111. });
  112. /**
  113. * @class Windows Script host environment
  114. * @extends jsspec.HostEnvironment
  115. */
  116. jsspec.WScriptHostEnvironment = jsspec.HostEnvironment.extend(/** @lends jsspec.WScriptHostEnvironment.prototype */{
  117. log: function(message) {
  118. jsspec.root.WScript.StdOut.WriteLine(message);
  119. },
  120. getDescription: function() {
  121. return 'Windows Script Host ' + WScript.Version;
  122. },
  123. _readFile: function(path) {
  124. var fso = new jsspec.root.ActiveXObject('Scripting.FileSystemObject');
  125. var file;
  126. try {
  127. file = fso.OpenTextFile(path);
  128. return file.ReadAll();
  129. } finally {
  130. try {if(file) file.Close();} catch(ignored) {}
  131. }
  132. },
  133. _findBasePath: function() {
  134. return '.';
  135. }
  136. });
  137. /**
  138. * @class Collection of assertion APIs
  139. */
  140. jsspec.Assertion = {
  141. /**
  142. * Makes an example fail unconditionally
  143. *
  144. * @param {string} [description] Optional description
  145. */
  146. fail: function(description) {
  147. throw new jsspec.ExpectationFailure(description || 'Failed');
  148. },
  149. /**
  150. * Performs equality test
  151. *
  152. * @param {object} expected Expected value
  153. * @param {object} actual Actual value
  154. * @param {string} [description] Optional description
  155. */
  156. assertEquals: function(expected, actual, description) {
  157. var matcher = jsspec.Matcher.getInstance(expected, actual);
  158. if(!matcher.matches()) throw new jsspec.ExpectationFailure((description || 'Expectation failure') + '. Expected [' + expected + '] but [' + actual + ']');
  159. },
  160. /**
  161. * Performs type test
  162. *
  163. * @param {string} expected Expected type
  164. * @param {object} actual Actual object
  165. * @param {string} [description] Optional description
  166. */
  167. assertType: function(expected, actual, description) {
  168. var type = jsspec.util.getType(actual);
  169. if(expected !== type) throw new jsspec.ExpectationFailure((description || 'Type expectation failure') + '. Expected [' + expected + '] but [' + type + ']');
  170. },
  171. /**
  172. * Checks if given value is true
  173. *
  174. * @param {boolean} actual Actual object
  175. * @param {string} [description] Optional description
  176. */
  177. assertTrue: function(actual, description) {
  178. var expected = true;
  179. if(expected !== actual) throw new jsspec.ExpectationFailure((description || 'Expectation failure') + '. Expected [' + expected + '] but [' + actual + ']');
  180. },
  181. /**
  182. * Checks if given value is false
  183. *
  184. * @param {boolean} actual Actual object
  185. * @param {string} [description] Optional description
  186. */
  187. assertFalse: function(actual, description) {
  188. var expected = false;
  189. if(expected !== actual) throw new jsspec.ExpectationFailure((description || 'Expectation failure') + '. Expected [' + expected + '] but [' + actual + ']');
  190. }
  191. };
  192. /**
  193. * @class Exception class to represent expectation failure (instead of error)
  194. * @extends jsspec.Class
  195. */
  196. jsspec.ExpectationFailure = jsspec.Class.extend(/** @lends jsspec.ExpectationFailure.prototype */{
  197. /**
  198. * @constructs
  199. * @param {string} message An failure message
  200. */
  201. init: function(message) {
  202. this._message = message;
  203. },
  204. toString: function() {
  205. return this._message;
  206. }
  207. });
  208. /**
  209. * @class Performs equality check for given objects
  210. * @extends jsspec.Class
  211. */
  212. jsspec.Matcher = jsspec.Class.extend(/** @lends jsspec.Matcher.prototype */{
  213. /**
  214. * @constructs
  215. * @param {object} expected An expected object
  216. * @param {object} actual An actual object
  217. */
  218. init: function(expected, actual) {
  219. this._expected = expected;
  220. this._actual = actual;
  221. },
  222. /**
  223. * @returns {object} An expected object
  224. */
  225. getExpected: function() {return this._expected;},
  226. /**
  227. * @returns {object} An actual object
  228. */
  229. getActual: function() {return this._actual;},
  230. /**
  231. * @param {boolean} True if matches
  232. */
  233. matches: function() {return this.getExpected() === this.getActual();}
  234. });
  235. /**
  236. * Returns appropriate jsspec.Matcher instance for given parameters' type
  237. *
  238. * @param {object} expected An expected object
  239. * @param {object} actual An actual object
  240. * @returns {jsspec.Matcher} An instance of jsspec.Matcher
  241. */
  242. jsspec.Matcher.getInstance = function(expected, actual) {
  243. if(expected === null || expected === undefined) return new jsspec.Matcher(expected, actual);
  244. var type = jsspec.util.getType(expected);
  245. var clazz = null;
  246. if('array' === type) {
  247. clazz = jsspec.ArrayMatcher;
  248. } else if('date' === type) {
  249. clazz = jsspec.DateMatcher;
  250. } else if('regexp' === type) {
  251. clazz = jsspec.RegexpMatcher;
  252. } else if('object' === type) {
  253. clazz = jsspec.ObjectMatcher;
  254. } else { // if string, boolean, number, function and anything else
  255. clazz = jsspec.Matcher;
  256. }
  257. return new clazz(expected, actual);
  258. };
  259. /**
  260. * @class Performs equality check for two arrays
  261. * @extends jsspec.Matcher
  262. */
  263. jsspec.ArrayMatcher = jsspec.Matcher.extend(/** @lends jsspec.ArrayMatcher.prototype */{
  264. matches: function() {
  265. if(!this.getActual()) return false;
  266. if(this.getExpected().length !== this.getActual().length) return false;
  267. for(var i = 0; i < this.getExpected().length; i++) {
  268. var expected = this.getExpected()[i];
  269. var actual = this.getActual()[i];
  270. if(!jsspec.Matcher.getInstance(expected, actual).matches()) return false;
  271. }
  272. return true;
  273. }
  274. });
  275. /**
  276. * @class Performs equality check for two date instances
  277. * @extends jsspec.Matcher
  278. */
  279. jsspec.DateMatcher = jsspec.Matcher.extend(/** @lends jsspec.DateMatcher.prototype */{
  280. matches: function() {
  281. if(!this.getActual()) return false;
  282. return this.getExpected().getTime() === this.getActual().getTime();
  283. }
  284. });
  285. /**
  286. * @class Performs equality check for two regular expressions
  287. * @extends jsspec.Matcher
  288. */
  289. jsspec.RegexpMatcher = jsspec.Matcher.extend(/** @lends jsspec.RegexpMatcher.prototype */{
  290. matches: function() {
  291. if(!this.getActual()) return false;
  292. return this.getExpected().source === this.getActual().source;
  293. }
  294. });
  295. /**
  296. * @class Performs equality check for two objects
  297. * @extends jsspec.Matcher
  298. */
  299. jsspec.ObjectMatcher = jsspec.Matcher.extend(/** @lends jsspec.ObjectMatcher.prototype */{
  300. matches: function() {
  301. if(!this.getActual()) return false;
  302. for(var key in this.getExpected()) {
  303. var expected = this.getExpected()[key];
  304. var actual = this.getActual()[key];
  305. if(!jsspec.Matcher.getInstance(expected, actual).matches()) return false;
  306. }
  307. for(var key in this.getActual()) {
  308. var expected = this.getActual()[key];
  309. var actual = this.getExpected()[key];
  310. if(!jsspec.Matcher.getInstance(expected, actual).matches()) return false;
  311. }
  312. return true;
  313. }
  314. });
  315. jsspec.Example = jsspec.Class.extend({
  316. init: function(name, func) {
  317. this._name = name;
  318. this._func = func;
  319. this._result = null;
  320. },
  321. getName: function() {return this._name;},
  322. getFunction: function() {return this._func;},
  323. getResult: function() {return this._result;},
  324. run: function(reporter, context) {
  325. reporter.onExampleStart(this);
  326. var exception = null;
  327. try {
  328. this.getFunction().apply(context);
  329. } catch(e) {
  330. jsspec.host.log("{{{");
  331. for(var key in e) {
  332. jsspec.host.log(" - " + key + ": " + e[key]);
  333. }
  334. jsspec.host.log("}}}");
  335. jsspec.host.log(" ");
  336. exception = e;
  337. }
  338. this._result = new jsspec.Result(this, exception);
  339. reporter.onExampleEnd(this);
  340. }
  341. });
  342. jsspec.ExampleSet = jsspec.Class.extend({
  343. init: function(name, examples) {
  344. this._name = name;
  345. this._examples = examples || [];
  346. this._setup = jsspec._EMPTY_FUNCTION;
  347. this._teardown = jsspec._EMPTY_FUNCTION;
  348. },
  349. getName: function() {return this._name;},
  350. getSetup: function() {return this._setup;},
  351. getTeardown: function() {return this._teardown;},
  352. addExample: function(example) {
  353. this._examples.push(example);
  354. },
  355. addExamples: function(examples) {
  356. for(var i = 0; i < examples.length; i++) {
  357. this.addExample(examples[i]);
  358. }
  359. },
  360. setSetup: function(func) {
  361. this._setup = func;
  362. },
  363. setTeardown: function(func) {
  364. this._teardown = func;
  365. },
  366. getLength: function() {
  367. return this._examples.length;
  368. },
  369. getExampleAt: function(index) {
  370. return this._examples[index];
  371. },
  372. run: function(reporter) {
  373. reporter.onExampleSetStart(this);
  374. for(var i = 0; i < this.getLength(); i++) {
  375. var context = {};
  376. this.getSetup().apply(context);
  377. this.getExampleAt(i).run(reporter, context);
  378. this.getTeardown().apply(context);
  379. }
  380. reporter.onExampleSetEnd(this);
  381. }
  382. });
  383. jsspec.Result = jsspec.Class.extend({
  384. init: function(example, exception) {
  385. this._example = example;
  386. this._exception = exception;
  387. },
  388. getExample: function() {return this._example;},
  389. getException: function() {return this._exception;},
  390. success: function() {
  391. return !this.getException();
  392. },
  393. failure: function() {
  394. return !this.success() && (this.getException() instanceof jsspec.ExpectationFailure);
  395. },
  396. error: function() {
  397. return !this.success() && !(this.getException() instanceof jsspec.ExpectationFailure);
  398. }
  399. });
  400. jsspec.Reporter = jsspec.Class.extend({
  401. init: function(host) {
  402. this._host = host;
  403. },
  404. onStart: function() {throw 'Not implemented';},
  405. onEnd: function() {throw 'Not implemented';},
  406. onExampleSetStart: function(exset) {throw 'Not implemented';},
  407. onExampleSetEnd: function(exset) {throw 'Not implemented';},
  408. onExampleStart: function(example) {throw 'Not implemented';},
  409. onExampleEnd: function(example) {throw 'Not implemented';}
  410. });
  411. jsspec.Reporter.getInstance = function() {
  412. if(jsspec.host instanceof jsspec.BrowserHostEnvironment) {
  413. // return new jsspec.HtmlReporter(jsspec.host);
  414. return new jsspec.ConsoleReporter(jsspec.host);
  415. } else {
  416. return new jsspec.ConsoleReporter(jsspec.host);
  417. }
  418. };
  419. jsspec.DummyReporter = jsspec.Reporter.extend({
  420. init: function() {
  421. this.log = [];
  422. },
  423. onStart: function() {
  424. this.log.push({op: 'onStart'});
  425. },
  426. onEnd: function() {
  427. this.log.push({op: 'onEnd'});
  428. },
  429. onExampleSetStart: function(exset) {
  430. this.log.push({op: 'onExampleSetStart', exset:exset.getName()});
  431. },
  432. onExampleSetEnd: function(exset) {
  433. this.log.push({op: 'onExampleSetEnd', exset:exset.getName()});
  434. },
  435. onExampleStart: function(example) {
  436. this.log.push({op: 'onExampleStart', example:example.getName()});
  437. },
  438. onExampleEnd: function(example) {
  439. this.log.push({op: 'onExampleEnd', example:example.getName()});
  440. }
  441. });
  442. jsspec.HtmlReporter = jsspec.Reporter.extend({
  443. init: function(host) {
  444. this._super(host);
  445. this._total = 0;
  446. this._failures = 0;
  447. this._errors = 0;
  448. document.write('<h1>JSSpec</h1>');
  449. },
  450. onStart: function() {
  451. },
  452. onEnd: function() {
  453. },
  454. onExampleSetStart: function(exset) {
  455. },
  456. onExampleSetEnd: function(exset) {
  457. },
  458. onExampleStart: function(example) {
  459. },
  460. onExampleEnd: function(example) {
  461. this._total++;
  462. var result = example.getResult();
  463. if(result.success()) return;
  464. this._host.log('- ' + result.getException());
  465. if(result.failure()) {
  466. this._failures++;
  467. } else {
  468. this._errors++;
  469. }
  470. }
  471. });
  472. jsspec.ConsoleReporter = jsspec.Reporter.extend({
  473. init: function(host) {
  474. this._super(host);
  475. this._total = 0;
  476. this._failures = 0;
  477. this._errors = 0;
  478. },
  479. onStart: function() {
  480. this._host.log('JSSpec2 on ' + this._host.getDescription());
  481. this._host.log('');
  482. },
  483. onEnd: function() {
  484. this._host.log('----');
  485. this._host.log('Total: ' + this._total + ', Failures: ' + this._failures + ', Errors: ' + this._errors + '');
  486. },
  487. onExampleSetStart: function(exset) {
  488. this._host.log('[' + exset.getName() + ']');
  489. },
  490. onExampleSetEnd: function(exset) {
  491. this._host.log('');
  492. },
  493. onExampleStart: function(example) {
  494. this._host.log(example.getName());
  495. },
  496. onExampleEnd: function(example) {
  497. this._total++;
  498. var result = example.getResult();
  499. if(result.success()) return;
  500. this._host.log('- ' + result.getException());
  501. if(result.failure()) {
  502. this._failures++;
  503. } else {
  504. this._errors++;
  505. }
  506. }
  507. });
  508. jsspec.util = {
  509. getType: function(o) {
  510. var ctor = o.constructor;
  511. if(ctor == Array) {
  512. return 'array';
  513. } else if(ctor == Date) {
  514. return 'date';
  515. } else if(ctor == RegExp) {
  516. return 'regexp';
  517. } else {
  518. return typeof o;
  519. }
  520. }
  521. }
  522. jsspec.dsl = {
  523. TDD: new (jsspec.Class.extend({
  524. init: function() {
  525. this._current = null;
  526. this._exsets = [];
  527. },
  528. suite: function(name) {
  529. this._current = new jsspec.ExampleSet(name);
  530. this._exsets.push(this._current);
  531. return this;
  532. },
  533. test: function(name, func) {
  534. if(!this._current) {
  535. this.suite('Default example set');
  536. }
  537. this._current.addExample(new jsspec.Example(name, func));
  538. return this;
  539. },
  540. setup: function(func) {
  541. if(!this._current) {
  542. this.suite('Default example set');
  543. }
  544. this._current.setSetup(func);
  545. },
  546. teardown: function(func) {
  547. if(!this._current) {
  548. this.suite('Default example set');
  549. }
  550. this._current.setTeardown(func);
  551. },
  552. run: function() {
  553. this._reporter = jsspec.Reporter.getInstance();
  554. this._reporter.onStart();
  555. for(var i = 0; i < this._exsets.length; i++) {
  556. this._exsets[i].run(this._reporter);
  557. }
  558. this._reporter.onEnd();
  559. return this;
  560. },
  561. fail: jsspec.Assertion.fail,
  562. assertEquals: jsspec.Assertion.assertEquals,
  563. assertType: jsspec.Assertion.assertType,
  564. assertTrue: jsspec.Assertion.assertTrue,
  565. assertFalse: jsspec.Assertion.assertFalse
  566. }))
  567. };
  568. jsspec.root = this;
  569. jsspec._EMPTY_FUNCTION = function() {}
  570. jsspec.host = jsspec.HostEnvironment.getInstance();