js_spec 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/core/JSSpec.js ADDED
@@ -0,0 +1,1506 @@
1
+ /**
2
+ * JSSpec
3
+ *
4
+ * Copyright 2007 Alan Kang
5
+ * - mailto:jania902@gmail.com
6
+ * - http://jania.pe.kr
7
+ *
8
+ * http://jania.pe.kr/aw/moin.cgi/JSSpec
9
+ *
10
+ * Dependencies:
11
+ * - diff_match_patch.js ( http://code.google.com/p/google-diff-match-patch )
12
+ *
13
+ * This library is free software; you can redistribute it and/or
14
+ * modify it under the terms of the GNU Lesser General Public
15
+ * License as published by the Free Software Foundation; either
16
+ * version 2.1 of the License, or (at your option) any later version.
17
+ *
18
+ * This library is distributed in the hope that it will be useful,
19
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21
+ * Lesser General Public License for more details.
22
+ *
23
+ * You should have received a copy of the GNU Lesser General Public
24
+ * License along with this library; if not, write to the Free Software
25
+ * Foundation, Inc, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
26
+ */
27
+
28
+ /**
29
+ * Namespace
30
+ */
31
+
32
+ var JSSpec = {
33
+ specs: [],
34
+
35
+ EMPTY_FUNCTION: function() {},
36
+
37
+ Browser: {
38
+ Trident: navigator.appName == "Microsoft Internet Explorer",
39
+ Webkit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
40
+ Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1,
41
+ Presto: navigator.appName == "Opera"
42
+ }
43
+ };
44
+
45
+
46
+
47
+ /**
48
+ * Executor
49
+ */
50
+ JSSpec.Executor = function(target, onSuccess, onException) {
51
+ this.target = target;
52
+ this.onSuccess = typeof onSuccess == 'function' ? onSuccess : JSSpec.EMPTY_FUNCTION;
53
+ this.onException = typeof onException == 'function' ? onException : JSSpec.EMPTY_FUNCTION;
54
+
55
+ if(JSSpec.Browser.Trident) {
56
+ // Exception handler for Trident. It helps to collect exact line number where exception occured.
57
+ window.onerror = function(message, fileName, lineNumber) {
58
+ var self = window._curExecutor;
59
+ var ex = {message:message, fileName:fileName, lineNumber:lineNumber};
60
+
61
+ if(JSSpec._secondPass) {
62
+ ex = self.mergeExceptions(JSSpec._assertionFailure, ex);
63
+ delete JSSpec._secondPass;
64
+ delete JSSpec._assertionFailure;
65
+
66
+ ex.type = "failure";
67
+ self.onException(self, ex);
68
+ } else if(JSSpec._assertionFailure) {
69
+ JSSpec._secondPass = true;
70
+ self.run();
71
+ } else {
72
+ self.onException(self, ex);
73
+ }
74
+
75
+ return true;
76
+ };
77
+ }
78
+ };
79
+ JSSpec.Executor.prototype.mergeExceptions = function(assertionFailure, normalException) {
80
+ var merged = {
81
+ message:assertionFailure.message,
82
+ fileName:normalException.fileName,
83
+ lineNumber:normalException.lineNumber
84
+ };
85
+
86
+ return merged;
87
+ };
88
+
89
+ JSSpec.Executor.prototype.run = function() {
90
+ var self = this;
91
+ var target = this.target;
92
+ var onSuccess = this.onSuccess;
93
+ var onException = this.onException;
94
+
95
+ window.setTimeout(
96
+ function() {
97
+ var result;
98
+ if(JSSpec.Browser.Trident) {
99
+ window._curExecutor = self;
100
+
101
+ result = self.target();
102
+ self.onSuccess(self, result);
103
+ } else {
104
+ try {
105
+ result = self.target();
106
+ self.onSuccess(self, result);
107
+ } catch(ex) {
108
+ if(JSSpec.Browser.Webkit) ex = {message:ex.message, fileName:ex.sourceURL, lineNumber:ex.line};
109
+
110
+ if(JSSpec._secondPass) {
111
+ ex = self.mergeExceptions(JSSpec._assertionFailure, ex);
112
+ delete JSSpec._secondPass;
113
+ delete JSSpec._assertionFailure;
114
+
115
+ ex.type = "failure";
116
+ self.onException(self, ex);
117
+ } else if(JSSpec._assertionFailure) {
118
+ JSSpec._secondPass = true;
119
+ self.run();
120
+ } else {
121
+ self.onException(self, ex);
122
+ }
123
+ }
124
+ }
125
+ },
126
+ 0
127
+ );
128
+ };
129
+
130
+
131
+
132
+ /**
133
+ * CompositeExecutor composites one or more executors and execute them sequencially.
134
+ */
135
+ JSSpec.CompositeExecutor = function(onSuccess, onException, continueOnException) {
136
+ this.queue = [];
137
+ this.onSuccess = typeof onSuccess == 'function' ? onSuccess : JSSpec.EMPTY_FUNCTION;
138
+ this.onException = typeof onException == 'function' ? onException : JSSpec.EMPTY_FUNCTION;
139
+ this.continueOnException = !!continueOnException;
140
+ };
141
+
142
+ JSSpec.CompositeExecutor.prototype.addFunction = function(func) {
143
+ this.addExecutor(new JSSpec.Executor(func));
144
+ };
145
+
146
+ JSSpec.CompositeExecutor.prototype.addExecutor = function(executor) {
147
+ var last = this.queue.length == 0 ? null : this.queue[this.queue.length - 1];
148
+ if(last) {
149
+ last.next = executor;
150
+ }
151
+
152
+ executor.parent = this;
153
+ executor.onSuccessBackup = executor.onSuccess;
154
+ executor.onSuccess = function(result) {
155
+ this.onSuccessBackup(result);
156
+ if(this.next) {
157
+ this.next.run();
158
+ } else {
159
+ this.parent.onSuccess();
160
+ }
161
+ };
162
+ executor.onExceptionBackup = executor.onException;
163
+ executor.onException = function(executor, ex) {
164
+ this.onExceptionBackup(executor, ex);
165
+
166
+ if(this.parent.continueOnException) {
167
+ if(this.next) {
168
+ this.next.run();
169
+ } else {
170
+ this.parent.onSuccess();
171
+ }
172
+ } else {
173
+ this.parent.onException(executor, ex);
174
+ }
175
+ };
176
+
177
+ this.queue.push(executor);
178
+ };
179
+
180
+ JSSpec.CompositeExecutor.prototype.run = function() {
181
+ if(this.queue.length > 0) {
182
+ this.queue[0].run();
183
+ }
184
+ };
185
+
186
+ /**
187
+ * Spec is a set of Examples in a specific context
188
+ */
189
+ JSSpec.Spec = function(context, entries) {
190
+ this.id = JSSpec.Spec.id++;
191
+ this.context = context;
192
+ this.url = location.href;
193
+
194
+ this.filterEntriesByEmbeddedExpressions(entries);
195
+ this.extractOutSpecialEntries(entries);
196
+ this.examples = this.makeExamplesFromEntries(entries);
197
+ this.examplesMap = this.makeMapFromExamples(this.examples);
198
+ };
199
+
200
+ JSSpec.Spec.id = 0;
201
+ JSSpec.Spec.prototype.getExamples = function() {
202
+ return this.examples;
203
+ };
204
+
205
+ JSSpec.Spec.prototype.hasException = function() {
206
+ return this.getTotalFailures() > 0 || this.getTotalErrors() > 0;
207
+ };
208
+
209
+ JSSpec.Spec.prototype.getTotalFailures = function() {
210
+ var examples = this.examples;
211
+ var failures = 0;
212
+ for(var i = 0; i < examples.length; i++) {
213
+ if(examples[i].isFailure()) failures++;
214
+ }
215
+ return failures;
216
+ };
217
+
218
+ JSSpec.Spec.prototype.getTotalErrors = function() {
219
+ var examples = this.examples;
220
+ var errors = 0;
221
+ for(var i = 0; i < examples.length; i++) {
222
+ if(examples[i].isError()) errors++;
223
+ }
224
+ return errors;
225
+ };
226
+
227
+ JSSpec.Spec.prototype.filterEntriesByEmbeddedExpressions = function(entries) {
228
+ var isTrue;
229
+ for(name in entries) {
230
+ var m = name.match(/\[\[(.+)\]\]/);
231
+ if(m && m[1]) {
232
+ eval("isTrue = (" + m[1] + ")");
233
+ if(!isTrue) delete entries[name];
234
+ }
235
+ }
236
+ };
237
+
238
+ JSSpec.Spec.prototype.extractOutSpecialEntries = function(entries) {
239
+ this.beforeEach = JSSpec.EMPTY_FUNCTION;
240
+ this.beforeAll = JSSpec.EMPTY_FUNCTION;
241
+ this.afterEach = JSSpec.EMPTY_FUNCTION;
242
+ this.afterAll = JSSpec.EMPTY_FUNCTION;
243
+
244
+ for(name in entries) {
245
+ if(name == 'before' || name == 'before each' || name == 'before_each') {
246
+ this.beforeEach = entries[name];
247
+ } else if(name == 'before all' || name == 'before_all') {
248
+ this.beforeAll = entries[name];
249
+ } else if(name == 'after' || name == 'after each' || name == 'after_each') {
250
+ this.afterEach = entries[name];
251
+ } else if(name == 'after all' || name == 'after_all') {
252
+ this.afterAll = entries[name];
253
+ }
254
+ }
255
+
256
+ delete entries['before'];
257
+ delete entries['before each'];
258
+ delete entries['before_each'];
259
+ delete entries['before all'];
260
+ delete entries['before_all'];
261
+ delete entries['after'];
262
+ delete entries['after each'];
263
+ delete entries['after_each'];
264
+ delete entries['after all'];
265
+ delete entries['after_all'];
266
+ };
267
+
268
+ JSSpec.Spec.prototype.makeExamplesFromEntries = function(entries) {
269
+ var examples = [];
270
+ for(name in entries) {
271
+ examples.push(new JSSpec.Example(name, entries[name], this.beforeEach, this.afterEach));
272
+ }
273
+ return examples;
274
+ };
275
+
276
+ JSSpec.Spec.prototype.makeMapFromExamples = function(examples) {
277
+ var map = {};
278
+ for(var i = 0; i < examples.length; i++) {
279
+ var example = examples[i];
280
+ map[example.id] = examples[i];
281
+ }
282
+ return map;
283
+ };
284
+
285
+ JSSpec.Spec.prototype.getExampleById = function(id) {
286
+ return this.examplesMap[id];
287
+ };
288
+
289
+ JSSpec.Spec.prototype.getExecutor = function() {
290
+ var self = this;
291
+ var onException = function(executor, ex) {
292
+ self.exception = ex;
293
+ };
294
+
295
+ var composite = new JSSpec.CompositeExecutor();
296
+ composite.addFunction(function() {JSSpec.log.onSpecStart(self);});
297
+ composite.addExecutor(new JSSpec.Executor(this.beforeAll, null, function(exec, ex) {
298
+ self.exception = ex;
299
+ JSSpec.log.onSpecEnd(self);
300
+ }));
301
+
302
+ var exampleAndAfter = new JSSpec.CompositeExecutor(null,null,true);
303
+ for(var i = 0; i < this.examples.length; i++) {
304
+ exampleAndAfter.addExecutor(this.examples[i].getExecutor());
305
+ }
306
+ exampleAndAfter.addExecutor(new JSSpec.Executor(this.afterAll, null, onException));
307
+ exampleAndAfter.addFunction(function() {JSSpec.log.onSpecEnd(self);});
308
+ composite.addExecutor(exampleAndAfter);
309
+
310
+ return composite;
311
+ };
312
+
313
+ /**
314
+ * Example
315
+ */
316
+ JSSpec.Example = function(name, target, before, after) {
317
+ this.id = JSSpec.Example.id++;
318
+ this.name = name;
319
+ this.target = target;
320
+ this.before = before;
321
+ this.after = after;
322
+ };
323
+
324
+ JSSpec.Example.id = 0;
325
+ JSSpec.Example.prototype.isFailure = function() {
326
+ return this.exception && this.exception.type == "failure";
327
+ };
328
+
329
+ JSSpec.Example.prototype.isError = function() {
330
+ return this.exception && !this.exception.type;
331
+ };
332
+
333
+ JSSpec.Example.prototype.getExecutor = function() {
334
+ var self = this;
335
+ var onException = function(executor, ex) {
336
+ self.exception = ex;
337
+ };
338
+
339
+ var composite = new JSSpec.CompositeExecutor();
340
+ composite.addFunction(function() {JSSpec.log.onExampleStart(self);});
341
+ composite.addExecutor(new JSSpec.Executor(this.before, null, function(exec, ex) {
342
+ self.exception = ex;
343
+ JSSpec.log.onExampleEnd(self);
344
+ }));
345
+
346
+ var targetAndAfter = new JSSpec.CompositeExecutor(null,null,true);
347
+
348
+ targetAndAfter.addExecutor(new JSSpec.Executor(this.target, null, onException));
349
+ targetAndAfter.addExecutor(new JSSpec.Executor(this.after, null, onException));
350
+ targetAndAfter.addFunction(function() {JSSpec.log.onExampleEnd(self);});
351
+
352
+ composite.addExecutor(targetAndAfter);
353
+
354
+ return composite;
355
+ };
356
+
357
+ /**
358
+ * Runner
359
+ */
360
+ JSSpec.Runner = function(specs, logger) {
361
+ JSSpec.log = logger;
362
+
363
+ this.totalExamples = 0;
364
+ this.specs = [];
365
+ this.specsMap = {};
366
+ this.addAllSpecs(specs);
367
+ };
368
+
369
+ JSSpec.Runner.prototype.addAllSpecs = function(specs) {
370
+ for(var i = 0; i < specs.length; i++) {
371
+ this.addSpec(specs[i]);
372
+ }
373
+ };
374
+
375
+ JSSpec.Runner.prototype.addSpec = function(spec) {
376
+ this.specs.push(spec);
377
+ this.specsMap[spec.id] = spec;
378
+ this.totalExamples += spec.getExamples().length;
379
+ };
380
+
381
+ JSSpec.Runner.prototype.getSpecById = function(id) {
382
+ return this.specsMap[id];
383
+ };
384
+
385
+ JSSpec.Runner.prototype.getSpecByContext = function(context) {
386
+ for(var i = 0; i < this.specs.length; i++) {
387
+ if(this.specs[i].context == context) return this.specs[i];
388
+ }
389
+ return null;
390
+ };
391
+
392
+ JSSpec.Runner.prototype.getSpecs = function() {
393
+ return this.specs;
394
+ };
395
+
396
+ JSSpec.Runner.prototype.hasException = function() {
397
+ return this.getTotalFailures() > 0 || this.getTotalErrors() > 0;
398
+ };
399
+
400
+ JSSpec.Runner.prototype.getTotalFailures = function() {
401
+ var specs = this.specs;
402
+ var failures = 0;
403
+ for(var i = 0; i < specs.length; i++) {
404
+ failures += specs[i].getTotalFailures();
405
+ }
406
+ return failures;
407
+ };
408
+
409
+ JSSpec.Runner.prototype.getTotalErrors = function() {
410
+ var specs = this.specs;
411
+ var errors = 0;
412
+ for(var i = 0; i < specs.length; i++) {
413
+ errors += specs[i].getTotalErrors();
414
+ }
415
+ return errors;
416
+ };
417
+
418
+
419
+ JSSpec.Runner.prototype.run = function() {
420
+ JSSpec.log.onRunnerStart();
421
+ var executor = new JSSpec.CompositeExecutor(function() {JSSpec.log.onRunnerEnd()},null,true);
422
+ for(var i = 0; i < this.specs.length; i++) {
423
+ executor.addExecutor(this.specs[i].getExecutor());
424
+ }
425
+ executor.run();
426
+ };
427
+
428
+
429
+ JSSpec.Runner.prototype.rerun = function(context) {
430
+ JSSpec.runner = new JSSpec.Runner([this.getSpecByContext(context)], JSSpec.log);
431
+ JSSpec.runner.run();
432
+ };
433
+
434
+ /**
435
+ * Logger
436
+ */
437
+ JSSpec.Logger = function() {
438
+ this.finishedExamples = 0;
439
+ this.startedAt = null;
440
+ };
441
+
442
+ JSSpec.Logger.prototype.onRunnerStart = function() {
443
+ this._title = document.title;
444
+
445
+ this.startedAt = new Date();
446
+ var container = document.getElementById('jsspec_container');
447
+ if(container) {
448
+ container.innerHTML = "";
449
+ } else {
450
+ container = document.createElement("DIV");
451
+ container.id = "jsspec_container";
452
+ document.body.appendChild(container);
453
+ }
454
+
455
+ var title = document.createElement("DIV");
456
+ title.id = "title";
457
+ title.innerHTML = [
458
+ '<h1>JSSpec runner</h1>',
459
+ '<ul>',
460
+ ' <li><span id="total_examples">' + JSSpec.runner.totalExamples + '</span> examples</li>',
461
+ ' <li><span id="total_failures">0</span> failures</li>',
462
+ ' <li><span id="total_errors">0</span> errors</li>',
463
+ ' <li><span id="progress">0</span>% done</li>',
464
+ ' <li><span id="total_elapsed">0</span> secs</li>',
465
+ '</ul>',
466
+ '<p><a href="http://jania.pe.kr/aw/moin.cgi/JSSpec">JSSpec homepage</a></p>',
467
+ ].join("");
468
+ container.appendChild(title);
469
+
470
+ var list = document.createElement("DIV");
471
+ list.id = "list";
472
+ list.innerHTML = [
473
+ '<h2>List</h2>',
474
+ '<ul class="specs">',
475
+ function() {
476
+ var specs = JSSpec.runner.getSpecs();
477
+ var sb = [];
478
+ for(var i = 0; i < specs.length; i++) {
479
+ var spec = specs[i];
480
+ sb.push('<li id="spec_' + specs[i].id + '_list"><h3><a href="#spec_' + specs[i].id + '">' + JSSpec.util.escapeTags(specs[i].context) + '</a> [<a href="?rerun=' + encodeURIComponent(specs[i].context) + '">rerun</a>]</h3></li>');
481
+ }
482
+ return sb.join("");
483
+ }(),
484
+ '</ul>'
485
+ ].join("");
486
+ container.appendChild(list);
487
+
488
+ var log = document.createElement("DIV");
489
+ log.id = "log";
490
+ log.innerHTML = [
491
+ '<h2>Log</h2>',
492
+ '<ul class="specs">',
493
+ function() {
494
+ var specs = JSSpec.runner.getSpecs();
495
+ var sb = [];
496
+ for(var i = 0; i < specs.length; i++) {
497
+ var spec = specs[i];
498
+ sb.push(' <li id="spec_' + specs[i].id + '">');
499
+ sb.push(' <h3>' + JSSpec.util.escapeTags(specs[i].context) + ' [<a href="?rerun=' + encodeURIComponent(specs[i].context) + '">rerun</a>]</h3>');
500
+ sb.push(' <ul id="spec_' + specs[i].id + '_examples" class="examples">');
501
+ for(var j = 0; j < spec.examples.length; j++) {
502
+ var example = spec.examples[j];
503
+ sb.push(' <li id="example_' + example.id + '">')
504
+ sb.push(' <h4>' + JSSpec.util.escapeTags(example.name) + '</h4>')
505
+ sb.push(' </li>')
506
+ }
507
+ sb.push(' </ul>');
508
+ sb.push(' </li>');
509
+ }
510
+ return sb.join("");
511
+ }(),
512
+ '</ul>'
513
+ ].join("");
514
+
515
+ container.appendChild(log);
516
+
517
+ // add event handler for toggling
518
+ var specs = JSSpec.runner.getSpecs();
519
+ var sb = [];
520
+ for(var i = 0; i < specs.length; i++) {
521
+ var spec = document.getElementById("spec_" + specs[i].id);
522
+ var title = spec.getElementsByTagName("H3")[0];
523
+ title.onclick = function(e) {
524
+ var target = document.getElementById(this.parentNode.id + "_examples");
525
+ target.style.display = target.style.display == "none" ? "block" : "none";
526
+ return true;
527
+ }
528
+ }
529
+ };
530
+
531
+ JSSpec.Logger.prototype.onRunnerEnd = function() {
532
+ if(JSSpec.runner.hasException()) {
533
+ var times = 4;
534
+ var title1 = "*" + this._title;
535
+ var title2 = "*F" + JSSpec.runner.getTotalFailures() + " E" + JSSpec.runner.getTotalErrors() + "* " + this._title;
536
+ } else {
537
+ var times = 2;
538
+ var title1 = this._title;
539
+ var title2 = "Success";
540
+ }
541
+ this.blinkTitle(times,title1,title2);
542
+ };
543
+
544
+ JSSpec.Logger.prototype.blinkTitle = function(times, title1, title2) {
545
+ var times = times * 2;
546
+ var mode = true;
547
+
548
+ var f = function() {
549
+ if(times > 0) {
550
+ document.title = mode ? title1 : title2;
551
+ mode = !mode;
552
+ times--;
553
+ window.setTimeout(f, 500);
554
+ } else {
555
+ document.title = title1;
556
+ }
557
+ };
558
+
559
+ f();
560
+ };
561
+
562
+ JSSpec.Logger.prototype.onSpecStart = function(spec) {
563
+ var spec_list = document.getElementById("spec_" + spec.id + "_list");
564
+ var spec_log = document.getElementById("spec_" + spec.id);
565
+
566
+ spec_list.className = "ongoing";
567
+ spec_log.className = "ongoing";
568
+ };
569
+
570
+ JSSpec.Logger.prototype.onSpecEnd = function(spec) {
571
+ var spec_list = document.getElementById("spec_" + spec.id + "_list");
572
+ var spec_log = document.getElementById("spec_" + spec.id);
573
+ var examples = document.getElementById("spec_" + spec.id + "_examples");
574
+ var className = spec.hasException() ? "exception" : "success";
575
+
576
+ spec_list.className = className;
577
+ spec_log.className = className;
578
+
579
+ if(JSSpec.options.autocollapse && !spec.hasException()) examples.style.display = "none";
580
+
581
+ if(spec.exception) {
582
+ heading.appendChild(document.createTextNode(" - " + spec.exception.message));
583
+ }
584
+ };
585
+
586
+ JSSpec.Logger.prototype.onExampleStart = function(example) {
587
+ var li = document.getElementById("example_" + example.id);
588
+ li.className = "ongoing";
589
+ };
590
+
591
+ JSSpec.Logger.prototype.onExampleEnd = function(example) {
592
+ var li = document.getElementById("example_" + example.id);
593
+ li.className = example.exception ? "exception" : "success";
594
+
595
+ if(example.exception) {
596
+ var div = document.createElement("DIV");
597
+ div.innerHTML = example.exception.message + "<p><br />" + " at " + example.exception.fileName + ", line " + example.exception.lineNumber + "</p>";
598
+ li.appendChild(div);
599
+ }
600
+
601
+ var title = document.getElementById("title");
602
+ var runner = JSSpec.runner;
603
+
604
+ title.className = runner.hasException() ? "exception" : "success";
605
+
606
+ this.finishedExamples++;
607
+ document.getElementById("total_failures").innerHTML = runner.getTotalFailures();
608
+ document.getElementById("total_errors").innerHTML = runner.getTotalErrors();
609
+ var progress = parseInt(this.finishedExamples / runner.totalExamples * 100);
610
+ document.getElementById("progress").innerHTML = progress;
611
+ document.getElementById("total_elapsed").innerHTML = (new Date().getTime() - this.startedAt.getTime()) / 1000;
612
+
613
+ document.title = progress + "%: " + this._title;
614
+ };
615
+
616
+ /**
617
+ * IncludeMatcher
618
+ */
619
+ JSSpec.IncludeMatcher = function(actual, expected, condition) {
620
+ this.actual = actual;
621
+ this.expected = expected;
622
+ this.condition = condition;
623
+ this.match = false;
624
+ this.explaination = this.makeExplain();
625
+ };
626
+
627
+ JSSpec.IncludeMatcher.createInstance = function(actual, expected, condition) {
628
+ return new JSSpec.IncludeMatcher(actual, expected, condition);
629
+ };
630
+
631
+ JSSpec.IncludeMatcher.prototype.matches = function() {
632
+ return this.match;
633
+ };
634
+
635
+ JSSpec.IncludeMatcher.prototype.explain = function() {
636
+ return this.explaination;
637
+ };
638
+
639
+ JSSpec.IncludeMatcher.prototype.makeExplain = function() {
640
+ if(typeof this.actual.length == 'undefined') {
641
+ return this.makeExplainForNotArray();
642
+ } else {
643
+ return this.makeExplainForArray();
644
+ }
645
+ };
646
+
647
+ JSSpec.IncludeMatcher.prototype.makeExplainForNotArray = function() {
648
+ var sb = [];
649
+ sb.push('<p>actual value:</p>');
650
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.actual) + '</p>');
651
+ sb.push('<p>should ' + (this.condition ? '' : 'not') + ' include:</p>');
652
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.expected) + '</p>');
653
+ sb.push('<p>but since it\s not an array, include or not doesn\'t make any sense.</p>');
654
+ return sb.join("");
655
+ }
656
+ ;
657
+ JSSpec.IncludeMatcher.prototype.makeExplainForArray = function() {
658
+ var matches;
659
+ if(this.condition) {
660
+ for(var i = 0; i < this.actual.length; i++) {
661
+ matches = JSSpec.EqualityMatcher.createInstance(this.expected, this.actual[i]).matches();
662
+ if(matches) {
663
+ this.match = true;
664
+ break;
665
+ }
666
+ }
667
+ } else {
668
+ for(var i = 0; i < this.actual.length; i++) {
669
+ matches = JSSpec.EqualityMatcher.createInstance(this.expected, this.actual[i]).matches();
670
+ if(matches) {
671
+ this.match = false;
672
+ break;
673
+ }
674
+ }
675
+ }
676
+
677
+ if(this.match) return "";
678
+
679
+ var sb = [];
680
+ sb.push('<p>actual value:</p>');
681
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.actual, false, this.condition ? null : i) + '</p>');
682
+ sb.push('<p>should ' + (this.condition ? '' : 'not') + ' include:</p>');
683
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.expected) + '</p>');
684
+ return sb.join("");
685
+ };
686
+
687
+ /**
688
+ * PropertyLengthMatcher
689
+ */
690
+ JSSpec.PropertyLengthMatcher = function(num, property, o, condition) {
691
+ this.num = num;
692
+ this.o = o;
693
+ this.property = property;
694
+ if((property == 'characters' || property == 'items') && typeof o.length != 'undefined') {
695
+ this.property = 'length';
696
+ }
697
+
698
+ this.condition = condition;
699
+ this.conditionMet = function(x) {
700
+ if(condition == 'exactly') return x.length == num;
701
+ if(condition == 'at least') return x.length >= num;
702
+ if(condition == 'at most') return x.length <= num;
703
+
704
+ throw "Unknown condition '" + condition + "'";
705
+ };
706
+ this.match = false;
707
+ this.explaination = this.makeExplain();
708
+ };
709
+
710
+ JSSpec.PropertyLengthMatcher.prototype.makeExplain = function() {
711
+ if(this.o._type == 'String' && this.property == 'length') {
712
+ this.match = this.conditionMet(this.o);
713
+ return this.match ? '' : this.makeExplainForString();
714
+ } else if(typeof this.o.length != 'undefined' && this.property == "length") {
715
+ this.match = this.conditionMet(this.o);
716
+ return this.match ? '' : this.makeExplainForArray();
717
+ } else if(typeof this.o[this.property] != 'undefined' && this.o[this.property] != null) {
718
+ this.match = this.conditionMet(this.o[this.property]);
719
+ return this.match ? '' : this.makeExplainForObject();
720
+ } else if(typeof this.o[this.property] == 'undefined' || this.o[this.property] == null) {
721
+ this.match = false;
722
+ return this.makeExplainForNoProperty();
723
+ }
724
+
725
+ this.match = true;
726
+ };
727
+
728
+ JSSpec.PropertyLengthMatcher.prototype.makeExplainForString = function() {
729
+ var sb = [];
730
+
731
+ var exp = this.num == 0 ?
732
+ 'be an <strong>empty string</strong>' :
733
+ 'have <strong>' + this.condition + ' ' + this.num + ' characters</strong>';
734
+
735
+ sb.push('<p>actual value has <strong>' + this.o.length + ' characters</strong>:</p>');
736
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.o) + '</p>');
737
+ sb.push('<p>but it should ' + exp + '.</p>');
738
+
739
+ return sb.join("");
740
+ };
741
+
742
+ JSSpec.PropertyLengthMatcher.prototype.makeExplainForArray = function() {
743
+ var sb = [];
744
+
745
+ var exp = this.num == 0 ?
746
+ 'be an <strong>empty array</strong>' :
747
+ 'have <strong>' + this.condition + ' ' + this.num + ' items</strong>';
748
+
749
+ sb.push('<p>actual value has <strong>' + this.o.length + ' items</strong>:</p>');
750
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.o) + '</p>');
751
+ sb.push('<p>but it should ' + exp + '.</p>');
752
+
753
+ return sb.join("");
754
+ };
755
+
756
+ JSSpec.PropertyLengthMatcher.prototype.makeExplainForObject = function() {
757
+ var sb = [];
758
+
759
+ var exp = this.num == 0 ?
760
+ 'be <strong>empty</strong>' :
761
+ 'have <strong>' + this.condition + ' ' + this.num + ' ' + this.property + '.</strong>';
762
+
763
+ sb.push('<p>actual value has <strong>' + this.o[this.property].length + ' ' + this.property + '</strong>:</p>');
764
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.o, false, this.property) + '</p>');
765
+ sb.push('<p>but it should ' + exp + '.</p>');
766
+
767
+ return sb.join("");
768
+ };
769
+
770
+ JSSpec.PropertyLengthMatcher.prototype.makeExplainForNoProperty = function() {
771
+ var sb = [];
772
+
773
+ sb.push('<p>actual value:</p>');
774
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.o) + '</p>');
775
+ sb.push('<p>should have <strong>' + this.condition + ' ' + this.num + ' ' + this.property + '</strong> but there\'s no such property.</p>');
776
+
777
+ return sb.join("");
778
+ };
779
+
780
+ JSSpec.PropertyLengthMatcher.prototype.matches = function() {
781
+ return this.match;
782
+ };
783
+
784
+ JSSpec.PropertyLengthMatcher.prototype.explain = function() {
785
+ return this.explaination;
786
+ };
787
+
788
+ JSSpec.PropertyLengthMatcher.createInstance = function(num, property, o, condition) {
789
+ return new JSSpec.PropertyLengthMatcher(num, property, o, condition);
790
+ };
791
+
792
+ /**
793
+ * EqualityMatcher
794
+ */
795
+ JSSpec.EqualityMatcher = {};
796
+
797
+ JSSpec.EqualityMatcher.createInstance = function(expected, actual) {
798
+ if(expected == null || actual == null) {
799
+ return new JSSpec.NullEqualityMatcher(expected, actual);
800
+ } else if(expected._type && expected._type == actual._type) {
801
+ if(expected._type == "String") {
802
+ return new JSSpec.StringEqualityMatcher(expected, actual);
803
+ } else if(expected._type == "Date") {
804
+ return new JSSpec.DateEqualityMatcher(expected, actual);
805
+ } else if(expected._type == "Number") {
806
+ return new JSSpec.NumberEqualityMatcher(expected, actual);
807
+ } else if(expected._type == "Array") {
808
+ return new JSSpec.ArrayEqualityMatcher(expected, actual);
809
+ } else if(expected._type == "Boolean") {
810
+ return new JSSpec.BooleanEqualityMatcher(expected, actual);
811
+ }
812
+ }
813
+
814
+ return new JSSpec.ObjectEqualityMatcher(expected, actual);
815
+ };
816
+
817
+ JSSpec.EqualityMatcher.basicExplain = function(expected, actual, expectedDesc, actualDesc) {
818
+ var sb = [];
819
+
820
+ sb.push(actualDesc || '<p>actual value:</p>');
821
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(actual) + '</p>');
822
+ sb.push(expectedDesc || '<p>should be:</p>');
823
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(expected) + '</p>');
824
+
825
+ return sb.join("");
826
+ };
827
+
828
+ JSSpec.EqualityMatcher.diffExplain = function(expected, actual) {
829
+ var sb = [];
830
+
831
+ sb.push('<p>diff:</p>');
832
+ sb.push('<p style="margin-left:2em;">');
833
+
834
+ var dmp = new diff_match_patch();
835
+ var diff = dmp.diff_main(expected, actual);
836
+ dmp.diff_cleanupEfficiency(diff);
837
+
838
+ sb.push(JSSpec.util.inspect(dmp.diff_prettyHtml(diff), true));
839
+
840
+ sb.push('</p>');
841
+
842
+ return sb.join("");
843
+ };
844
+
845
+ /**
846
+ * BooleanEqualityMatcher
847
+ */
848
+ JSSpec.BooleanEqualityMatcher = function(expected, actual) {
849
+ this.expected = expected;
850
+ this.actual = actual;
851
+ };
852
+
853
+ JSSpec.BooleanEqualityMatcher.prototype.explain = function() {
854
+ var sb = [];
855
+
856
+ sb.push('<p>actual value:</p>');
857
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.actual) + '</p>');
858
+ sb.push('<p>should be:</p>');
859
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.expected) + '</p>');
860
+
861
+ return sb.join("");
862
+ };
863
+
864
+ JSSpec.BooleanEqualityMatcher.prototype.matches = function() {
865
+ return this.expected == this.actual;
866
+ };
867
+
868
+ /**
869
+ * NullEqualityMatcher
870
+ */
871
+ JSSpec.NullEqualityMatcher = function(expected, actual) {
872
+ this.expected = expected;
873
+ this.actual = actual;
874
+ };
875
+
876
+ JSSpec.NullEqualityMatcher.prototype.matches = function() {
877
+ return this.expected == this.actual && typeof this.expected == typeof this.actual;
878
+ };
879
+
880
+ JSSpec.NullEqualityMatcher.prototype.explain = function() {
881
+ return JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual);
882
+ };
883
+
884
+ JSSpec.DateEqualityMatcher = function(expected, actual) {
885
+ this.expected = expected;
886
+ this.actual = actual;
887
+ };
888
+
889
+ JSSpec.DateEqualityMatcher.prototype.matches = function() {
890
+ return this.expected.getTime() == this.actual.getTime();
891
+ };
892
+
893
+ JSSpec.DateEqualityMatcher.prototype.explain = function() {
894
+ var sb = [];
895
+
896
+ sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual));
897
+ sb.push(JSSpec.EqualityMatcher.diffExplain(this.expected.toString(), this.actual.toString()));
898
+
899
+ return sb.join("");
900
+ };
901
+
902
+ /**
903
+ * ObjectEqualityMatcher
904
+ */
905
+ JSSpec.ObjectEqualityMatcher = function(expected, actual) {
906
+ this.expected = expected;
907
+ this.actual = actual;
908
+ this.match = this.expected == this.actual;
909
+ this.explaination = this.makeExplain();
910
+ };
911
+
912
+ JSSpec.ObjectEqualityMatcher.prototype.matches = function() {return this.match};
913
+
914
+ JSSpec.ObjectEqualityMatcher.prototype.explain = function() {return this.explaination};
915
+
916
+ JSSpec.ObjectEqualityMatcher.prototype.makeExplain = function() {
917
+ if(this.expected == this.actual) {
918
+ this.match = true;
919
+ return "";
920
+ }
921
+
922
+ if(JSSpec.util.isDomNode(this.expected)) {
923
+ return this.makeExplainForDomNode();
924
+ }
925
+
926
+ var key, expectedHasItem, actualHasItem;
927
+
928
+ for(key in this.expected) {
929
+ expectedHasItem = this.expected[key] != null && typeof this.expected[key] != 'undefined';
930
+ actualHasItem = this.actual[key] != null && typeof this.actual[key] != 'undefined';
931
+ if(expectedHasItem && !actualHasItem) return this.makeExplainForMissingItem(key);
932
+ }
933
+ for(key in this.actual) {
934
+ expectedHasItem = this.expected[key] != null && typeof this.expected[key] != 'undefined';
935
+ actualHasItem = this.actual[key] != null && typeof this.actual[key] != 'undefined';
936
+ if(actualHasItem && !expectedHasItem) return this.makeExplainForUnknownItem(key);
937
+ }
938
+
939
+ for(key in this.expected) {
940
+ var matcher = JSSpec.EqualityMatcher.createInstance(this.expected[key], this.actual[key]);
941
+ if(!matcher.matches()) return this.makeExplainForItemMismatch(key);
942
+ }
943
+
944
+ this.match = true;
945
+ };
946
+
947
+ JSSpec.ObjectEqualityMatcher.prototype.makeExplainForDomNode = function(key) {
948
+ var sb = [];
949
+
950
+ sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual));
951
+
952
+ return sb.join("");
953
+ };
954
+
955
+ JSSpec.ObjectEqualityMatcher.prototype.makeExplainForMissingItem = function(key) {
956
+ var sb = [];
957
+
958
+ sb.push('<p>actual value has no item named <strong>' + JSSpec.util.inspect(key) + '</strong></p>');
959
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.actual, false, key) + '</p>');
960
+ sb.push('<p>but it should have the item whose value is <strong>' + JSSpec.util.inspect(this.expected[key]) + '</strong></p>');
961
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.expected, false, key) + '</p>');
962
+
963
+ return sb.join("");
964
+ };
965
+
966
+ JSSpec.ObjectEqualityMatcher.prototype.makeExplainForUnknownItem = function(key) {
967
+ var sb = [];
968
+
969
+ sb.push('<p>actual value has item named <strong>' + JSSpec.util.inspect(key) + '</strong></p>');
970
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.actual, false, key) + '</p>');
971
+ sb.push('<p>but there should be no such item</p>');
972
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.expected, false, key) + '</p>');
973
+
974
+ return sb.join("");
975
+ };
976
+
977
+ JSSpec.ObjectEqualityMatcher.prototype.makeExplainForItemMismatch = function(key) {
978
+ var sb = [];
979
+
980
+ sb.push('<p>actual value has an item named <strong>' + JSSpec.util.inspect(key) + '</strong> whose value is <strong>' + JSSpec.util.inspect(this.actual[key]) + '</strong></p>');
981
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.actual, false, key) + '</p>');
982
+ sb.push('<p>but it\'s value should be <strong>' + JSSpec.util.inspect(this.expected[key]) + '</strong></p>');
983
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.expected, false, key) + '</p>');
984
+
985
+ return sb.join("");
986
+ };
987
+
988
+
989
+
990
+
991
+ /**
992
+ * ArrayEqualityMatcher
993
+ */
994
+ JSSpec.ArrayEqualityMatcher = function(expected, actual) {
995
+ this.expected = expected;
996
+ this.actual = actual;
997
+ this.match = this.expected == this.actual;
998
+ this.explaination = this.makeExplain();
999
+ };
1000
+
1001
+ JSSpec.ArrayEqualityMatcher.prototype.matches = function() {return this.match};
1002
+
1003
+ JSSpec.ArrayEqualityMatcher.prototype.explain = function() {return this.explaination};
1004
+
1005
+ JSSpec.ArrayEqualityMatcher.prototype.makeExplain = function() {
1006
+ if(this.expected.length != this.actual.length) return this.makeExplainForLengthMismatch();
1007
+
1008
+ for(var i = 0; i < this.expected.length; i++) {
1009
+ var matcher = JSSpec.EqualityMatcher.createInstance(this.expected[i], this.actual[i]);
1010
+ if(!matcher.matches()) return this.makeExplainForItemMismatch(i);
1011
+ }
1012
+
1013
+ this.match = true;
1014
+ };
1015
+
1016
+ JSSpec.ArrayEqualityMatcher.prototype.makeExplainForLengthMismatch = function() {
1017
+ return JSSpec.EqualityMatcher.basicExplain(
1018
+ this.expected,
1019
+ this.actual,
1020
+ '<p>but it should be <strong>' + this.expected.length + '</strong></p>',
1021
+ '<p>actual value has <strong>' + this.actual.length + '</strong> items</p>'
1022
+ );
1023
+ };
1024
+
1025
+ JSSpec.ArrayEqualityMatcher.prototype.makeExplainForItemMismatch = function(index) {
1026
+ var postfix = ["th", "st", "nd", "rd", "th"][Math.min((index + 1) % 10,4)];
1027
+
1028
+ var sb = [];
1029
+
1030
+ sb.push('<p>' + (index + 1) + postfix + ' item (index ' + index + ') of actual value is <strong>' + JSSpec.util.inspect(this.actual[index]) + '</strong>:</p>');
1031
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.actual, false, index) + '</p>');
1032
+ sb.push('<p>but it should be <strong>' + JSSpec.util.inspect(this.expected[index]) + '</strong>:</p>');
1033
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.expected, false, index) + '</p>');
1034
+
1035
+ return sb.join("");
1036
+ };
1037
+
1038
+ /**
1039
+ * NumberEqualityMatcher
1040
+ */
1041
+ JSSpec.NumberEqualityMatcher = function(expected, actual) {
1042
+ this.expected = expected;
1043
+ this.actual = actual;
1044
+ };
1045
+
1046
+ JSSpec.NumberEqualityMatcher.prototype.matches = function() {
1047
+ if(this.expected == this.actual) return true;
1048
+ };
1049
+
1050
+ JSSpec.NumberEqualityMatcher.prototype.explain = function() {
1051
+ return JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual);
1052
+ };
1053
+
1054
+ /**
1055
+ * StringEqualityMatcher
1056
+ */
1057
+ JSSpec.StringEqualityMatcher = function(expected, actual) {
1058
+ this.expected = expected;
1059
+ this.actual = actual;
1060
+ };
1061
+
1062
+ JSSpec.StringEqualityMatcher.prototype.matches = function() {
1063
+ if(this.expected == this.actual) return true;
1064
+ };
1065
+
1066
+ JSSpec.StringEqualityMatcher.prototype.explain = function() {
1067
+ var sb = [];
1068
+
1069
+ sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual));
1070
+ sb.push(JSSpec.EqualityMatcher.diffExplain(this.expected, this.actual));
1071
+ return sb.join("");
1072
+ };
1073
+
1074
+ /**
1075
+ * PatternMatcher
1076
+ */
1077
+ JSSpec.PatternMatcher = function(actual, pattern, condition) {
1078
+ this.actual = actual;
1079
+ this.pattern = pattern;
1080
+ this.condition = condition;
1081
+ this.match = false;
1082
+ this.explaination = this.makeExplain();
1083
+ };
1084
+
1085
+ JSSpec.PatternMatcher.createInstance = function(actual, pattern, condition) {
1086
+ return new JSSpec.PatternMatcher(actual, pattern, condition);
1087
+ };
1088
+
1089
+ JSSpec.PatternMatcher.prototype.makeExplain = function() {
1090
+ var sb;
1091
+ if(this.actual == null || this.actual._type != 'String') {
1092
+ sb = [];
1093
+ sb.push('<p>actual value:</p>');
1094
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.actual) + '</p>');
1095
+ sb.push('<p>should ' + (this.condition ? '' : 'not') + ' match with pattern:</p>');
1096
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.pattern) + '</p>');
1097
+ sb.push('<p>but pattern matching cannot be performed.</p>');
1098
+ return sb.join("");
1099
+ } else {
1100
+ this.match = this.condition == !!this.actual.match(this.pattern);
1101
+ if(this.match) return "";
1102
+
1103
+ sb = [];
1104
+ sb.push('<p>actual value:</p>');
1105
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.actual) + '</p>');
1106
+ sb.push('<p>should ' + (this.condition ? '' : 'not') + ' match with pattern:</p>');
1107
+ sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.pattern) + '</p>');
1108
+ return sb.join("");
1109
+ }
1110
+ };
1111
+
1112
+ JSSpec.PatternMatcher.prototype.matches = function() {
1113
+ return this.match;
1114
+ };
1115
+
1116
+ JSSpec.PatternMatcher.prototype.explain = function() {
1117
+ return this.explaination;
1118
+ };
1119
+
1120
+ /**
1121
+ * Domain Specific Languages
1122
+ */
1123
+ JSSpec.DSL = {};
1124
+
1125
+ JSSpec.DSL.forString = {
1126
+ normalizeHtml: function() {
1127
+ var html = this;
1128
+
1129
+ // Uniformize quotation, turn tag names and attribute names into lower case
1130
+ html = html.replace(/<(\/?)(\w+)([^>]*?)>/img, function(str, closingMark, tagName, attrs) {
1131
+ var sortedAttrs = JSSpec.util.sortHtmlAttrs(JSSpec.util.correctHtmlAttrQuotation(attrs).toLowerCase())
1132
+ return "<" + closingMark + tagName.toLowerCase() + sortedAttrs + ">"
1133
+ });
1134
+
1135
+ // validation self-closing tags
1136
+ html = html.replace(/<(br|hr|img)([^>]*?)>/mg, function(str, tag, attrs) {
1137
+ return "<" + tag + attrs + " />";
1138
+ });
1139
+
1140
+ // append semi-colon at the end of style value
1141
+ html = html.replace(/style="(.*?)"/mg, function(str, styleStr) {
1142
+ styleStr = JSSpec.util.sortStyleEntries(styleStr.strip()); // for Safari
1143
+ if(styleStr.charAt(styleStr.length - 1) != ';') styleStr += ";"
1144
+
1145
+ return 'style="' + styleStr + '"'
1146
+ });
1147
+
1148
+ // sort style entries
1149
+
1150
+ // remove empty style attributes
1151
+ html = html.replace(/ style=";"/mg, "");
1152
+
1153
+ // remove new-lines
1154
+ html = html.replace(/\r/mg, '');
1155
+ html = html.replace(/\n/mg, '');
1156
+
1157
+ return html;
1158
+ }
1159
+ };
1160
+
1161
+
1162
+
1163
+ JSSpec.DSL.describe = function(context, entries) {
1164
+ JSSpec.specs.push(new JSSpec.Spec(context, entries));
1165
+ };
1166
+
1167
+ JSSpec.DSL.value_of = function(target) {
1168
+ if(JSSpec._secondPass) return {};
1169
+
1170
+ var subject = new JSSpec.DSL.Subject(target);
1171
+ return subject;
1172
+ };
1173
+
1174
+ JSSpec.DSL.Subject = function(target) {
1175
+ this.target = target;
1176
+ };
1177
+
1178
+ JSSpec.DSL.Subject.prototype._type = 'Subject';
1179
+
1180
+ JSSpec.DSL.Subject.prototype.should_fail = function(message) {
1181
+ JSSpec._assertionFailure = {message:message};
1182
+ throw JSSpec._assertionFailure;
1183
+ };
1184
+
1185
+ JSSpec.DSL.Subject.prototype.should_be = function(expected) {
1186
+ var matcher = JSSpec.EqualityMatcher.createInstance(expected, this.target);
1187
+ if(!matcher.matches()) {
1188
+ JSSpec._assertionFailure = {message:matcher.explain()};
1189
+ throw JSSpec._assertionFailure;
1190
+ }
1191
+ };
1192
+
1193
+ JSSpec.DSL.Subject.prototype.should_not_be = function(expected) {
1194
+ // TODO JSSpec.EqualityMatcher should support 'condition'
1195
+ var matcher = JSSpec.EqualityMatcher.createInstance(expected, this.target);
1196
+ if(matcher.matches()) {
1197
+ JSSpec._assertionFailure = {message:"'" + this.target + "' should not be '" + expected + "'"};
1198
+ throw JSSpec._assertionFailure;
1199
+ }
1200
+ };
1201
+
1202
+ JSSpec.DSL.Subject.prototype.should_be_empty = function() {
1203
+ this.should_have(0, this.getType() == 'String' ? 'characters' : 'items');
1204
+ };
1205
+
1206
+ JSSpec.DSL.Subject.prototype.should_not_be_empty = function() {
1207
+ this.should_have_at_least(1, this.getType() == 'String' ? 'characters' : 'items');
1208
+ };
1209
+
1210
+ JSSpec.DSL.Subject.prototype.should_be_true = function() {
1211
+ this.should_be(true);
1212
+ };
1213
+
1214
+ JSSpec.DSL.Subject.prototype.should_be_false = function() {
1215
+ this.should_be(false);
1216
+ };
1217
+
1218
+ JSSpec.DSL.Subject.prototype.should_be_null = function() {
1219
+ this.should_be(null);
1220
+ };
1221
+
1222
+ JSSpec.DSL.Subject.prototype.should_be_undefined = function() {
1223
+ this.should_be(undefined);
1224
+ };
1225
+
1226
+ JSSpec.DSL.Subject.prototype.should_not_be_null = function() {
1227
+ this.should_not_be(null);
1228
+ };
1229
+
1230
+ JSSpec.DSL.Subject.prototype.should_not_be_undefined = function() {
1231
+ this.should_not_be(undefined);
1232
+ };
1233
+
1234
+ JSSpec.DSL.Subject.prototype._should_have = function(num, property, condition) {
1235
+ var matcher = JSSpec.PropertyLengthMatcher.createInstance(num, property, this.target, condition);
1236
+ if(!matcher.matches()) {
1237
+ JSSpec._assertionFailure = {message:matcher.explain()};
1238
+ throw JSSpec._assertionFailure;
1239
+ }
1240
+ };
1241
+
1242
+ JSSpec.DSL.Subject.prototype.should_have = function(num, property) {
1243
+ this._should_have(num, property, "exactly");
1244
+ };
1245
+
1246
+ JSSpec.DSL.Subject.prototype.should_have_exactly = function(num, property) {
1247
+ this._should_have(num, property, "exactly");
1248
+ };
1249
+
1250
+ JSSpec.DSL.Subject.prototype.should_have_at_least = function(num, property) {
1251
+ this._should_have(num, property, "at least");
1252
+ };
1253
+
1254
+ JSSpec.DSL.Subject.prototype.should_have_at_most = function(num, property) {
1255
+ this._should_have(num, property, "at most");
1256
+ };
1257
+
1258
+ JSSpec.DSL.Subject.prototype.should_include = function(expected) {
1259
+ var matcher = JSSpec.IncludeMatcher.createInstance(this.target, expected, true);
1260
+ if(!matcher.matches()) {
1261
+ JSSpec._assertionFailure = {message:matcher.explain()};
1262
+ throw JSSpec._assertionFailure;
1263
+ }
1264
+ };
1265
+
1266
+ JSSpec.DSL.Subject.prototype.should_not_include = function(expected) {
1267
+ var matcher = JSSpec.IncludeMatcher.createInstance(this.target, expected, false);
1268
+ if(!matcher.matches()) {
1269
+ JSSpec._assertionFailure = {message:matcher.explain()};
1270
+ throw JSSpec._assertionFailure;
1271
+ }
1272
+ };
1273
+
1274
+ JSSpec.DSL.Subject.prototype.should_match = function(pattern) {
1275
+ var matcher = JSSpec.PatternMatcher.createInstance(this.target, pattern, true);
1276
+ if(!matcher.matches()) {
1277
+ JSSpec._assertionFailure = {message:matcher.explain()};
1278
+ throw JSSpec._assertionFailure;
1279
+ }
1280
+ }
1281
+ JSSpec.DSL.Subject.prototype.should_not_match = function(pattern) {
1282
+ var matcher = JSSpec.PatternMatcher.createInstance(this.target, pattern, false);
1283
+ if(!matcher.matches()) {
1284
+ JSSpec._assertionFailure = {message:matcher.explain()};
1285
+ throw JSSpec._assertionFailure;
1286
+ }
1287
+ };
1288
+
1289
+ JSSpec.DSL.Subject.prototype.getType = function() {
1290
+ if(typeof this.target == 'undefined') {
1291
+ return 'undefined';
1292
+ } else if(this.target == null) {
1293
+ return 'null';
1294
+ } else if(this.target._type) {
1295
+ return this.target._type;
1296
+ } else if(JSSpec.util.isDomNode(this.target)) {
1297
+ return 'DomNode';
1298
+ } else {
1299
+ return 'object';
1300
+ }
1301
+ };
1302
+
1303
+ /**
1304
+ * Utilities
1305
+ */
1306
+ JSSpec.util = {
1307
+ escapeTags: function(string) {
1308
+ return string.replace(/</img, '&lt;').replace(/>/img, '&gt;');
1309
+ },
1310
+ parseOptions: function(defaults) {
1311
+ var options = defaults;
1312
+
1313
+ var url = location.href;
1314
+ var queryIndex = url.indexOf('?');
1315
+ if(queryIndex == -1) return options;
1316
+
1317
+ var query = url.substring(queryIndex + 1);
1318
+ var pairs = query.split('&');
1319
+ for(var i = 0; i < pairs.length; i++) {
1320
+ var tokens = pairs[i].split('=');
1321
+ options[tokens[0]] = tokens[1];
1322
+ }
1323
+
1324
+ return options;
1325
+ },
1326
+ correctHtmlAttrQuotation: function(html) {
1327
+ html = html.replace(/(\w+)=['"]([^'"]+)['"]/mg,function (str, name, value) {return name + '=' + '"' + value + '"';});
1328
+ html = html.replace(/(\w+)=([^ '"]+)/mg,function (str, name, value) {return name + '=' + '"' + value + '"';});
1329
+ html = html.replace(/'/mg, '"');
1330
+
1331
+ return html;
1332
+ },
1333
+ sortHtmlAttrs: function(html) {
1334
+ var attrs = [];
1335
+ html.replace(/((\w+)="[^"]+")/mg, function(str, matched) {
1336
+ attrs.push(matched);
1337
+ });
1338
+ return attrs.length == 0 ? "" : " " + attrs.sort().join(" ");
1339
+ },
1340
+ sortStyleEntries: function(styleText) {
1341
+ var entries = styleText.split(/; /);
1342
+ return entries.sort().join("; ");
1343
+ },
1344
+ escapeHtml: function(str) {
1345
+ if(!this._div) {
1346
+ this._div = document.createElement("DIV");
1347
+ this._text = document.createTextNode('');
1348
+ this._div.appendChild(this._text);
1349
+ }
1350
+ this._text.data = str;
1351
+ return this._div.innerHTML;
1352
+ },
1353
+ isDomNode: function(o) {
1354
+ // TODO: make it more stricter
1355
+ return (typeof o.nodeName == 'string') && (typeof o.nodeType == 'number');
1356
+ },
1357
+ inspectDomPath: function(o) {
1358
+ var sb = [];
1359
+ while(o && o.nodeName != '#document' && o.parent) {
1360
+ var siblings = o.parentNode.childNodes;
1361
+ for(var i = 0; i < siblings.length; i++) {
1362
+ if(siblings[i] == o) {
1363
+ sb.push(o.nodeName + (i == 0 ? '' : '[' + i + ']'));
1364
+ break;
1365
+ }
1366
+ }
1367
+ o = o.parentNode;
1368
+ }
1369
+ return sb.join(" &gt; ");
1370
+ },
1371
+ inspectDomNode: function(o) {
1372
+ if(o.nodeType == 1) {
1373
+ var nodeName = o.nodeName.toLowerCase();
1374
+ var sb = [];
1375
+ sb.push('<span class="dom_value">');
1376
+ sb.push("&lt;");
1377
+ sb.push(nodeName);
1378
+
1379
+ var attrs = o.attributes;
1380
+ for(var i = 0; i < attrs.length; i++) {
1381
+ if(
1382
+ attrs[i].nodeValue &&
1383
+ attrs[i].nodeName != 'contentEditable' &&
1384
+ attrs[i].nodeName != 'style' &&
1385
+ typeof attrs[i].nodeValue != 'function'
1386
+ ) sb.push(' <span class="dom_attr_name">' + attrs[i].nodeName.toLowerCase() + '</span>=<span class="dom_attr_value">"' + attrs[i].nodeValue + '"</span>');
1387
+ }
1388
+ if(o.style && o.style.cssText) {
1389
+ sb.push(' <span class="dom_attr_name">style</span>=<span class="dom_attr_value">"' + o.style.cssText + '"</span>');
1390
+ }
1391
+ sb.push('&gt;');
1392
+ sb.push(JSSpec.util.escapeHtml(o.innerHTML));
1393
+ sb.push('&lt;/' + nodeName + '&gt;');
1394
+ sb.push(' <span class="dom_path">(' + JSSpec.util.inspectDomPath(o) + ')</span>' );
1395
+ sb.push('</span>');
1396
+ return sb.join("");
1397
+ } else if(o.nodeType == 3) {
1398
+ return '<span class="dom_value">#text ' + o.nodeValue + '</span>';
1399
+ } else {
1400
+ return '<span class="dom_value">UnknownDomNode</span>';
1401
+ }
1402
+ },
1403
+ inspect: function(o, dontEscape, emphasisKey) {
1404
+ var sb, inspected;
1405
+
1406
+ if(typeof o == 'undefined') return '<span class="undefined_value">undefined</span>';
1407
+ if(o == null) return '<span class="null_value">null</span>';
1408
+ if(o._type == 'String') return '<span class="string_value">"' + (dontEscape ? o : JSSpec.util.escapeHtml(o)) + '"</span>';
1409
+
1410
+ if(o._type == 'Date') {
1411
+ return '<span class="date_value">"' + o.toString() + '"</span>';
1412
+ }
1413
+
1414
+ if(o._type == 'Number') return '<span class="number_value">' + (dontEscape ? o : JSSpec.util.escapeHtml(o)) + '</span>';
1415
+
1416
+ if(o._type == 'Boolean') return '<span class="boolean_value">' + o + '</span>';
1417
+
1418
+ if(o._type == 'RegExp') return '<span class="regexp_value">' + JSSpec.util.escapeHtml(o.toString()) + '</span>';
1419
+
1420
+ if(JSSpec.util.isDomNode(o)) return JSSpec.util.inspectDomNode(o);
1421
+
1422
+ if(o._type == 'Array' || typeof o.length != 'undefined') {
1423
+ sb = [];
1424
+ for(var i = 0; i < o.length; i++) {
1425
+ inspected = JSSpec.util.inspect(o[i]);
1426
+ sb.push(i == emphasisKey ? ('<strong>' + inspected + '</strong>') : inspected);
1427
+ }
1428
+ return '<span class="array_value">[' + sb.join(', ') + ']</span>';
1429
+ }
1430
+
1431
+ // object
1432
+ sb = [];
1433
+ for(var key in o) {
1434
+ if(key == 'should') continue;
1435
+
1436
+ inspected = JSSpec.util.inspect(key) + ":" + JSSpec.util.inspect(o[key]);
1437
+ sb.push(key == emphasisKey ? ('<strong>' + inspected + '</strong>') : inspected);
1438
+ }
1439
+ return '<span class="object_value">{' + sb.join(', ') + '}</span>';
1440
+ }
1441
+ };
1442
+
1443
+ describe = JSSpec.DSL.describe;
1444
+ behavior_of = JSSpec.DSL.describe;
1445
+ value_of = JSSpec.DSL.value_of;
1446
+ expect = JSSpec.DSL.value_of; // @deprecated
1447
+
1448
+ String.prototype._type = "String";
1449
+ Number.prototype._type = "Number";
1450
+ Date.prototype._type = "Date";
1451
+ Array.prototype._type = "Array";
1452
+ Boolean.prototype._type = "Boolean";
1453
+ RegExp.prototype._type = "RegExp";
1454
+
1455
+ var targets = [Array.prototype, Date.prototype, Number.prototype, String.prototype, Boolean.prototype, RegExp.prototype];
1456
+
1457
+ String.prototype.normalizeHtml = JSSpec.DSL.forString.normalizeHtml;
1458
+ String.prototype.asHtml = String.prototype.normalizeHtml; //@deprecated
1459
+
1460
+
1461
+
1462
+ /**
1463
+ * Main
1464
+ */
1465
+ JSSpec.defaultOptions = {
1466
+ autorun: 1,
1467
+ specIdBeginsWith: 0,
1468
+ exampleIdBeginsWith: 0,
1469
+ autocollapse: 1
1470
+ };
1471
+ JSSpec.options = JSSpec.util.parseOptions(JSSpec.defaultOptions);
1472
+
1473
+ JSSpec.Spec.id = JSSpec.options.specIdBeginsWith;
1474
+ JSSpec.Example.id = JSSpec.options.exampleIdBeginsWith;
1475
+
1476
+
1477
+
1478
+ window.onload = function() {
1479
+ if(JSSpec.specs.length > 0) {
1480
+ if(!JSSpec.options.inSuite) {
1481
+ JSSpec.runner = new JSSpec.Runner(JSSpec.specs, new JSSpec.Logger());
1482
+ if(JSSpec.options.rerun) {
1483
+ JSSpec.runner.rerun(decodeURIComponent(JSSpec.options.rerun));
1484
+ } else {
1485
+ JSSpec.runner.run();
1486
+ }
1487
+ } else {
1488
+ // in suite, send all specs to parent
1489
+ var parentWindow = window.frames.parent.window;
1490
+ for(var i = 0; i < JSSpec.specs.length; i++) {
1491
+ parentWindow.JSSpec.specs.push(JSSpec.specs[i]);
1492
+ }
1493
+ }
1494
+ } else {
1495
+ var links = document.getElementById('list').getElementsByTagName('A');
1496
+ var frameContainer = document.createElement('DIV');
1497
+ frameContainer.style.display = 'none';
1498
+ document.body.appendChild(frameContainer);
1499
+
1500
+ for(var i = 0; i < links.length; i++) {
1501
+ var frame = document.createElement('IFRAME');
1502
+ frame.src = links[i].href + '?inSuite=0&specIdBeginsWith=' + (i * 10000) + '&exampleIdBeginsWith=' + (i * 10000);
1503
+ frameContainer.appendChild(frame);
1504
+ }
1505
+ }
1506
+ }