Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

committing full unit test suite

  • Loading branch information...
commit 5929fded772fe0fefb6588d2b4c3462871daff15 1 parent 2b39e7a
Michael Monteleone authored
116  Rakefile
... ...
@@ -0,0 +1,116 @@
  1
+require 'rubygems'
  2
+require 'net/http'
  3
+require 'rake/clean'
  4
+require 'packr'
  5
+require 'zip/zip'
  6
+require 'find'
  7
+require 'fileutils'
  8
+include FileUtils
  9
+  
  10
+task :default => :test
  11
+
  12
+# list of browsers to auto-bind to JsTestDrive Server
  13
+# non-existent browsers will be ignored
  14
+BROWSERS = [
  15
+  '/Applications/Safari.app/Contents/MacOS/Safari',
  16
+  '/Applications/Firefox.app/Contents/MacOS/firefox',
  17
+  '/Applications/Chromium.app/Contents/MacOS/Chromium',
  18
+  '/Applications/Opera.app/Contents/MacOS/Opera',
  19
+  'C:/Program Files/Mozilla Firefox/firefox.exe',
  20
+  'C:/Program Files/Internet Explorer/iexplore.exe',
  21
+  'C:/Program Files/Safari/Safari.exe',
  22
+  'C:/Program Files/Opera/opera.exe' ]
  23
+
  24
+
  25
+desc "Builds a release"
  26
+task :build => [:clean] do
  27
+  # build dist and lib directories
  28
+  mkdir 'dist'
  29
+  mkdir 'dist/lib'
  30
+  mkdir 'dist/example'
  31
+
  32
+  # copy src
  33
+  cp 'jquery.autotype.js', 'dist/jquery.autotype.js'
  34
+  
  35
+  # copy documentation
  36
+  cp 'README.markdown', 'dist/README.markdown'
  37
+  
  38
+  # copy examples
  39
+  cp 'example/example1.html', 'dist/example/example1.html'
  40
+  cp 'example/example2.html', 'dist/example/example2.html'
  41
+
  42
+  # copy lib
  43
+  cp 'lib/jquery-1.3.2.min.js', 'dist/lib/jquery-1.3.2.min.js'
  44
+  cp 'lib/GPL-LICENSE.txt', 'dist/lib/GPL-LICENSE.txt'
  45
+  cp 'lib/MIT-LICENSE.txt', 'dist/lib/MIT-LICENSE.txt'
  46
+  
  47
+  # minify src
  48
+  source = File.read('dist/jquery.autotype.js')
  49
+  minified = Packr.pack(source, :shrink_vars => true, :base62 => false)
  50
+  header = /\/\*.*?\*\//m.match(source)
  51
+
  52
+  # inject header
  53
+  File.open('dist/jquery.autotype.min.js', 'w') do |combined|
  54
+    combined.puts(header)
  55
+    combined.write(minified)  
  56
+  end
  57
+end
  58
+
  59
+desc "Generates a releasable zip archive"
  60
+task :release => [:build] do
  61
+  root = pwd+'/dist'
  62
+  zip_archive = pwd+'/dist/jquery.autotype.zip'
  63
+
  64
+  Zip::ZipFile.open(zip_archive, Zip::ZipFile::CREATE) do |zip|
  65
+    Find.find(root) do |path|
  66
+      Find.prune if File.basename(path)[0] == ?.
  67
+      dest = /dist\/(\w.*)/.match(path)
  68
+      zip.add(dest[1],path) if dest
  69
+    end 
  70
+  end    
  71
+end
  72
+
  73
+desc "Run the tests in default browser"
  74
+task :test => [:build] do  
  75
+  begin
  76
+    # mac
  77
+    sh("open spec/jquery.autotype.specs.html")
  78
+  rescue
  79
+    # windows
  80
+    sh("start spec/jquery.autotype.specs.html")
  81
+  end
  82
+end
  83
+
  84
+
  85
+desc "Run the tests against JsTestDriver"
  86
+task :testdrive => [:build] do
  87
+  sh("java -jar spec/lib/js-test-driver/JsTestDriver.jar --tests all --captureConsole --reset")
  88
+end
  89
+
  90
+
  91
+desc "Start the JsTestDriver server"
  92
+task :server => [:install_server] do
  93
+  browsers = BROWSERS.find_all{|b| File.exists? b}.join(',')
  94
+  sh("java -jar spec/lib/js-test-driver/JsTestDriver.jar --port 9876 --browser \"#{browsers}\"")
  95
+end
  96
+
  97
+
  98
+desc "Download Google JsTestDriver"
  99
+task :install_server do
  100
+  if !File.exist?('spec/lib/js-test-driver/JsTestDriver.jar') then
  101
+    puts 'Downloading JsTestDriver from Google (http://js-test-driver.googlecode.com/files/JsTestDriver-1.0b.jar) ...'
  102
+    Net::HTTP.start("js-test-driver.googlecode.com") do |http|
  103
+      resp = http.get("/files/JsTestDriver-1.0b.jar")
  104
+      open("spec/lib/js-test-driver/JsTestDriver.jar", "wb") do |file|
  105
+        file.write(resp.body)
  106
+      end
  107
+    end
  108
+    puts 'JsTestDriver Downloaded'
  109
+  end
  110
+end
  111
+
  112
+
  113
+# clean deletes built copies
  114
+CLEAN.include('dist/')
  115
+# clobber cleans and uninstalls JsTestDriver server
  116
+CLOBBER.include('spec/lib/js-test-driver/*.jar')  
9  jsTestDriver.conf
... ...
@@ -0,0 +1,9 @@
  1
+server: http://localhost:9876
  2
+ 
  3
+load:
  4
+  - spec/lib/js-test-driver/qunit/equiv.js
  5
+  - spec/lib/js-test-driver/qunit/QUnitAdapter.js
  6
+  - spec/lib/pavlov/*.js
  7
+  - spec/lib/delorean/*.js
  8
+  - dist/*.js
  9
+  - spec/*.js
28  spec/jquery.autotype.specs.html
... ...
@@ -0,0 +1,28 @@
  1
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
  2
+<html>
  3
+<head>
  4
+<script type="text/javascript" src="../lib/jquery-1.3.2.min.js"></script>
  5
+<script type="text/javascript" src="lib/qunit/qunit.js"></script>
  6
+<link rel="stylesheet" href="lib/qunit/qunit.css" type="text/css" media="screen" />
  7
+<script type="text/javascript" src="lib/pavlov/pavlov.js"></script>
  8
+<script type="text/javascript" src="lib/delorean/delorean.js"></script>
  9
+
  10
+<script type="text/javascript" src="../jquery.autotype.js"></script>
  11
+<script type="text/javascript" src="jquery.autotype.specs.js"></script>
  12
+<style type="text/css">
  13
+#testbed {
  14
+    display: none;
  15
+    }
  16
+</style>
  17
+</head>
  18
+<body>    
  19
+	<h1 id="qunit-header"></h1>
  20
+	<h2 id="qunit-banner"></h2>
  21
+	<div id="qunit-testrunner-toolbar"></div>
  22
+	<h2 id="qunit-userAgent"></h2>
  23
+	<ol id="qunit-tests"></ol>
  24
+	<div id="testbed">
  25
+	    <form method="post" action="/"></form>
  26
+	</div>	
  27
+</body>
  28
+</html>
376  spec/jquery.autotype.specs.js
... ...
@@ -0,0 +1,376 @@
  1
+QUnit.specify("jQuery.autotype", function() {
  2
+    
  3
+    // stub global with interval setting and clearing provided by 
  4
+    // DeLorean time-mocking library instead of browser window.
  5
+    var fakeGlobal = {
  6
+        setInterval: DeLorean.setInterval,
  7
+        clearInterval: DeLorean.clearInterval
  8
+    };
  9
+
  10
+    // shortcut for building up and breaking down stub forms
  11
+    var FormBuilder = {
  12
+        clear: function(){
  13
+            $('div#testbed form').empty();
  14
+        },
  15
+        addTextInput: function(name, value){
  16
+            var input = $('<input class="test" type="text" id="' + name + '" name="' + name + '" value="' + value + '" />');
  17
+            $('div#testbed form').append(input);
  18
+            return input;
  19
+        },
  20
+        addTextArea: function(name, value){
  21
+            var input = $('<textarea class="test" name="' + name + '" id="' + name + '">' + value + '</textarea>');
  22
+            $('div#testbed form').append(input);
  23
+            return input;
  24
+        }
  25
+    };
  26
+
  27
+    describe('jQuery.fn.autotype', function(){
  28
+        var log = [];
  29
+        before(function(){
  30
+            FormBuilder.addTextArea('text1','');
  31
+            $('textarea').keydown(function(e){ logEvent(e, this); })
  32
+                .keypress(function(e){ logEvent(e, this); })
  33
+                .keyup(function(e){ logEvent(e, this); });
  34
+        });
  35
+        after(function(){
  36
+            FormBuilder.clear();
  37
+            log = [];
  38
+        });
  39
+        var logEvent = function(e, field) {
  40
+            log.push({
  41
+                type: e.type,
  42
+                value: $(field).val(),
  43
+                event: e
  44
+            });
  45
+        };
  46
+            
  47
+        it('should throw an exception if not passed a value', function(){
  48
+            assert(function(){
  49
+                $('textarea').autotype();                
  50
+            }).throwsException('Value is required by jQuery.autotype plugin');          
  51
+            assert(function(){
  52
+                $('textarea').autotype(null);
  53
+            }).throwsException('Value is required by jQuery.autotype plugin');          
  54
+        });  
  55
+        it('should return original selection', function(){
  56
+            var selection = $('textarea');
  57
+            var result = selection.autotype('ab');
  58
+            assert(result.length).equals(selection.length);
  59
+            assert(result.get(0)).isSameAs(selection.get(0));            
  60
+        });
  61
+        describe('defaults', function(){
  62
+            it("should have a default delay of 0", function(){
  63
+                assert($.fn.autotype.defaults.delay).equals(0);                
  64
+            });
  65
+            it('should default to enUs keyBoard', function(){
  66
+                assert($.fn.autotype.defaults.keyBoard).equals('enUs');                                
  67
+            });
  68
+            it('should contain an enUs keycode set', function(){
  69
+                assert($.fn.autotype.defaults.keyCodes.enUs).isDefined();                
  70
+            });
  71
+        });
  72
+        describe('with plain text entry', function(){
  73
+            it('should trigger keydown,press,and up for each character', function(){
  74
+                $('textarea').autotype('ab');
  75
+                assert(log.length).equals(6);
  76
+                assert(log[0].type).equals('keydown');
  77
+                assert(log[1].type).equals('keypress');
  78
+                assert(log[2].type).equals('keyup');
  79
+                assert(log[3].type).equals('keydown');
  80
+                assert(log[4].type).equals('keypress');
  81
+                assert(log[5].type).equals('keyup');
  82
+            });
  83
+            it('should change value of input between keypress and keyup event', function(){
  84
+                $('textarea').autotype('ab');
  85
+                assert(log.length).equals(6);
  86
+
  87
+                assert(log[0].value).equals('');
  88
+                assert(log[0].type).equals('keydown');
  89
+
  90
+                assert(log[1].value).equals('');
  91
+                assert(log[1].type).equals('keypress');
  92
+
  93
+                assert(log[2].value).equals('a');
  94
+                assert(log[2].type).equals('keyup');
  95
+
  96
+                assert(log[3].value).equals('a');
  97
+                assert(log[3].type).equals('keydown');
  98
+
  99
+                assert(log[4].value).equals('a');
  100
+                assert(log[4].type).equals('keypress');
  101
+
  102
+                assert(log[5].value).equals('ab');
  103
+                assert(log[5].type).equals('keyup');                
  104
+            });
  105
+            it('should include keycode only on down and up event', function(){
  106
+                $('textarea').autotype('a');
  107
+                assert(log[0].event.keyCode).equals(65);
  108
+                assert(log[1].event.keyCode).equals(0);
  109
+                assert(log[2].event.keyCode).equals(65);
  110
+            });
  111
+            it('should include charcode only on press event', function(){
  112
+                $('textarea').autotype('a');
  113
+                assert(log[0].event.charCode).equals(0);
  114
+                assert(log[1].event.charCode).equals(97);
  115
+                assert(log[2].event.charCode).equals(0);
  116
+            });
  117
+            describe('when keydown is canceled', function(){
  118
+                it('should still raise down,press,and up', function(){
  119
+                    // set up keydown to cancel propagation
  120
+                    $('textarea').keydown(function(){
  121
+                        return false;
  122
+                    })
  123
+                    $('textarea').autotype('a');
  124
+                    assert(log.length).equals(3);
  125
+                    assert(log[0].type).equals('keydown');
  126
+                    assert(log[1].type).equals('keypress');
  127
+                    assert(log[2].type).equals('keyup');
  128
+                });
  129
+                it('should not change value', function(){
  130
+                    // set up keydown to cancel propagation
  131
+                    $('textarea').keydown(function(){
  132
+                        return false;
  133
+                    })
  134
+                    $('textarea').autotype('a');
  135
+                    assert($('textarea').val()).equals('');
  136
+                });
  137
+            });
  138
+            describe('when keypress is cancelled', function(){
  139
+                it('should still raise down,press,and up', function(){
  140
+                    // set up keypress to cancel propagation
  141
+                    $('textarea').keypress(function(){
  142
+                        return false;
  143
+                    })
  144
+                    $('textarea').autotype('a');
  145
+                    assert(log.length).equals(3);
  146
+                    assert(log[0].type).equals('keydown');
  147
+                    assert(log[1].type).equals('keypress');
  148
+                    assert(log[2].type).equals('keyup');                    
  149
+                });
  150
+                it('should not change value', function(){
  151
+                    // set up keypress to cancel propagation
  152
+                    $('textarea').keypress(function(){
  153
+                        return false;
  154
+                    })
  155
+                    $('textarea').autotype('a');
  156
+                    assert($('textarea').val()).equals('');                    
  157
+                });
  158
+            });
  159
+            describe('when keyup is cancelled', function(){
  160
+                it('should still raise down, press, and up', function(){
  161
+                    // set up keyup to cancel propagation
  162
+                    $('textarea').keyup(function(){
  163
+                        return false;
  164
+                    })
  165
+                    $('textarea').autotype('a');
  166
+                    assert(log.length).equals(3);
  167
+                    assert(log[0].type).equals('keydown');
  168
+                    assert(log[1].type).equals('keypress');
  169
+                    assert(log[2].type).equals('keyup');                                        
  170
+                });
  171
+                it('should change value', function(){
  172
+                    // set up keyup to cancel propagation
  173
+                    $('textarea').keyup(function(){
  174
+                        return false;
  175
+                    })
  176
+                    $('textarea').autotype('a');
  177
+                    assert($('textarea').val()).equals('a');                    
  178
+                });
  179
+            });
  180
+        });
  181
+        describe('with explicit control keys', function(){
  182
+            given(['pgup',33],['pgdn',34],['home',36],['end',35],['left',37],['right',39],['down',40],['up',38]).
  183
+                it('should pass correct keycodes for non-modifier control keys', function(keyName, expectedKeyCode) {
  184
+                    $('textarea').autotype('{{' + keyName + '}}');
  185
+                    assert(log.length).equals(3);
  186
+                    assert(log[0].event.keyCode).equals(expectedKeyCode);
  187
+                    assert(log[2].event.keyCode).equals(expectedKeyCode);
  188
+                });
  189
+            describe('when contorl key is an enter', function(){
  190
+                it('should add a newline to value', function(){
  191
+                    $('textarea').autotype('line1{{enter}}line2');
  192
+                    assert($('textarea').val()).equals("line1\nline2");
  193
+                });              
  194
+            });
  195
+            describe('when control key is a back', function(){
  196
+                it('should remove last character from value', function(){
  197
+                    $('textarea').autotype('hello  {{back}}there.{{back}}');
  198
+                    assert($('textarea').val()).equals("hello there");
  199
+                });              
  200
+            });
  201
+            describe('when control keys are modifiers', function(){
  202
+                given(['ctrl',17],['alt',18],['meta',224],['shift',16]).
  203
+                    it('should pass correct keycodes for modifier control keys upon their opening and include modifiers on event', function(keyName, expectedKeyCode) {
  204
+                        $('textarea').autotype('{{' + keyName + '}}');
  205
+                        assert(log.length).equals(1);
  206
+                        assert(log[0].event.keyCode).equals(expectedKeyCode);
  207
+                        // make sure ctrlKey, altKey, metaKey, and shiftKey are set to true on event
  208
+                        assert(log[0].event[keyName + 'Key']).isTrue();
  209
+                    });
  210
+                given(['ctrl',17],['alt',18],['meta',224]).
  211
+                    it('should block change of value on subsequent character keys after non-shift modifier control keys, while still raising events and including modifiers in event', function(keyName, expectedKeyCode){
  212
+                        $('textarea').autotype('ab{{' + keyName + '}}cd');
  213
+                        assert(log.length).equals(13);
  214
+                        assert(log[6].event.keyCode).equals(expectedKeyCode);
  215
+                        assert($('textarea').val()).equals('ab');
  216
+                        
  217
+                        // make sure ctrlKey, altKey, metaKey, and shiftKey are set to true
  218
+                        // on event and following keys' events
  219
+                        assert(log[6].event[keyName + 'Key']).isTrue();
  220
+                        assert(log[7].event[keyName + 'Key']).isTrue();
  221
+                        
  222
+                        // ensure normal events raised on following keys, even though the value won't change
  223
+                        assert(log[7].type).equals('keydown');
  224
+                        assert(log[8].type).equals('keypress');
  225
+                        assert(log[9].type).equals('keyup');                        
  226
+                        assert(log[10].event[keyName + 'Key']).isTrue();
  227
+                        assert(log[10].type).equals('keydown');
  228
+                        assert(log[11].type).equals('keypress');
  229
+                        assert(log[12].type).equals('keyup');
  230
+                    });
  231
+                given(['shift',16]).
  232
+                    it('should not block change of value on subsequent character keys after shift modifier key', function(keyName, expectedKeyCode){
  233
+                        $('textarea').autotype('ab{{' + keyName + '}}cd');
  234
+                        assert(log.length).equals(13);
  235
+                        assert(log[6].event.keyCode).equals(expectedKeyCode);
  236
+                        assert($('textarea').val()).equals('abCD');
  237
+                    });
  238
+                given(['shift',16]).
  239
+                    it('should change case of following characters until un-shifted', function(keyName, expectedKeyCode){
  240
+                        $('textarea').autotype('ab{{' + keyName + '}}cd{{/' + keyName + '}}ef');
  241
+                        assert(log.length).equals(20);
  242
+                        
  243
+                        // keydown of shift before 'cd'
  244
+                        assert(log[6].event.keyCode).equals(expectedKeyCode);
  245
+                        assert(log[6].type).equals('keydown');
  246
+                        
  247
+                        // keyup of shift after 'cd'
  248
+                        assert(log[13].event.keyCode).equals(expectedKeyCode);
  249
+                        assert(log[13].type).equals('keyup');
  250
+                        
  251
+                        assert($('textarea').val()).equals('abCDef');
  252
+                    });
  253
+                given(['ctrl',17],['alt',18],['meta',224],['shift',16]).
  254
+                    it('should undo modifiers on key event upon release of modifier control keys', function(keyName, expectedKeyCode){
  255
+                        $('textarea').autotype('ab{{'+keyName+'}}cd{{/'+keyName+'}}ef');
  256
+                        assert(log.length).equals(20);
  257
+                        
  258
+                        // keyup right before the keydown of modifier, should say the modifier is false                   
  259
+                        assert(log[5].event[keyName+'Key']).isFalse();
  260
+                        
  261
+                        // now make sure all subsequent events include the modifier
  262
+                        assert(log[6].event[keyName+'Key']).isTrue();  // keydown of modifier
  263
+                        assert(log[7].event[keyName+'Key']).isTrue();  // c
  264
+                        assert(log[8].event[keyName+'Key']).isTrue();  // c
  265
+                        assert(log[9].event[keyName+'Key']).isTrue();  // c
  266
+                        assert(log[10].event[keyName+'Key']).isTrue(); // d
  267
+                        assert(log[11].event[keyName+'Key']).isTrue(); // d
  268
+                        assert(log[12].event[keyName+'Key']).isTrue(); // d
  269
+                        
  270
+                        // keyup of modifier
  271
+                        assert(log[13].event[keyName+'Key']).isFalse();
  272
+                        assert(log[14].event[keyName+'Key']).isFalse();
  273
+                    });
  274
+                given(['ctrl',17],['alt',18],['meta',224],['shift',16]).
  275
+                    it('should raise only keydown and keyup events on modifier keys', function(keyName, expectedKeyCode){
  276
+                        $('textarea').autotype('{{'+keyName+'}}{{/'+keyName+'}}');
  277
+                        assert(log.length).equals(2);
  278
+                        assert(log[0].type).equals('keydown');
  279
+                        assert(log[0].event.keyCode).equals(expectedKeyCode);
  280
+                        assert(log[1].type).equals('keyup');
  281
+                        assert(log[1].event.keyCode).equals(expectedKeyCode);
  282
+                    });                    
  283
+                given(['ctrl',17],['alt',18],['meta',224],['shift',16]).
  284
+                    it('should only raise keydown on modifier key opening', function(keyName, expectedKeyCode){
  285
+                        $('textarea').autotype('a{{'+keyName+'}}');
  286
+                        assert(log.length).equals(4);
  287
+                        assert(log[3].type).equals('keydown');
  288
+                        assert(log[3].event.keyCode).equals(expectedKeyCode);
  289
+                    });
  290
+                given(['ctrl',17],['alt',18],['meta',224],['shift',16]).
  291
+                    it('should only raise keyup on modifier key closing', function(keyName, expectedKeyCode){
  292
+                        $('textarea').autotype('a{{/'+keyName+'}}');
  293
+                        assert(log.length).equals(4);
  294
+                        assert(log[3].type).equals('keyup');
  295
+                        assert(log[3].event.keyCode).equals(expectedKeyCode);
  296
+                    });
  297
+            });
  298
+        });
  299
+        describe('with implicit control keys', function(){
  300
+            it('should register shift key downs and ups around implicit changings of case of input text', function(){
  301
+                $('textarea').autotype('HeLLo');
  302
+                assert(log.length).equals(19);
  303
+                assert($('textarea').val()).equals('HeLLo');
  304
+                assert(log[0].type).equals('keydown'); // shift down
  305
+                assert(log[1].type).equals('keydown');// h down
  306
+                assert(log[2].type).equals('keypress');  // h press
  307
+                assert(log[3].type).equals('keyup');  // h up
  308
+                assert(log[4].type).equals('keyup');  // shift up
  309
+                assert(log[5].type).equals('keydown');  // e down
  310
+                assert(log[8].type).equals('keydown');  // shift down
  311
+                assert(log[9].type).equals('keydown');  // l down
  312
+                assert(log[10].type).equals('keypress');  // l press
  313
+                assert(log[11].type).equals('keyup');  // l up
  314
+                assert(log[12].type).equals('keydown');  // l down
  315
+                assert(log[13].type).equals('keypress');  // l press
  316
+                assert(log[14].type).equals('keyup');  // l up
  317
+                assert(log[15].type).equals('keyup');  // shift down                
  318
+            });
  319
+        });
  320
+        describe('event model', function(){
  321
+            it("should raise 'autotyped' event at end of non-delayed entry", function(){
  322
+                var autotypedRaised = false;
  323
+                $('textarea').bind('autotyped', function(){
  324
+                    autotypedRaised = true;
  325
+                })
  326
+                $('textarea').autotype('abcd');
  327
+                assert(autotypedRaised).isTrue();
  328
+            });
  329
+            it("should raise 'autotyped' event at end of delayed entry", function(){
  330
+                var autotypedRaised = false;
  331
+                $('textarea').bind('autotyped', function(){
  332
+                    autotypedRaised = true;
  333
+                })
  334
+                $('textarea').autotype('abcd', {delay: 20, global: fakeGlobal});
  335
+
  336
+                // at 75 ms, shouldn't have quite finished the last character
  337
+                DeLorean.advance(75);
  338
+                assert(autotypedRaised).isFalse();
  339
+
  340
+                // at 81, should have finished
  341
+                DeLorean.advance(6);
  342
+                assert(autotypedRaised).isTrue();                
  343
+            });
  344
+        });
  345
+        describe('delays', function(){
  346
+            it('should type content immediately, synchronously when delay is 0', function(){
  347
+                $('textarea').autotype('abcd', {delay: 0, global: fakeGlobal});
  348
+                assert($('textarea').val()).equals('abcd');
  349
+            });
  350
+            it('should iteratively type each character when delay is 20', function(){
  351
+                var autotypedRaised = false;
  352
+                $('textarea').bind('autotyped', function(){
  353
+                    autotypedRaised = true;
  354
+                })
  355
+                $('textarea').autotype('abcd', {delay: 10, global: fakeGlobal});
  356
+                assert($('textarea').val()).equals('');
  357
+                DeLorean.advance(1);
  358
+                
  359
+                DeLorean.advance(10);
  360
+                assert($('textarea').val()).equals('a');
  361
+                
  362
+                DeLorean.advance(10);
  363
+                assert($('textarea').val()).equals('ab');
  364
+                
  365
+                DeLorean.advance(10);
  366
+                assert($('textarea').val()).equals('abc');
  367
+                
  368
+                assert(autotypedRaised).isFalse();
  369
+                
  370
+                DeLorean.advance(10);
  371
+                assert($('textarea').val()).equals('abcd');
  372
+                assert(autotypedRaised).isTrue();
  373
+            });
  374
+        });
  375
+    });
  376
+});
333  spec/lib/delorean/delorean.js
... ...
@@ -0,0 +1,333 @@
  1
+/**
  2
+ * DeLorean - Flux capacitor for accurately faking time-bound 
  3
+ * JavaScript unit testing, including timeouts, intervals, and dates
  4
+ *
  5
+ * version 0.1.2
  6
+ * 
  7
+ * http://michaelmonteleone.net/projects/delorean
  8
+ * http://github.com/mmonteleone/delorean
  9
+ *
  10
+ * Copyright (c) 2009 Michael Monteleone
  11
+ * Licensed under terms of the MIT License (README.markdown)
  12
+ */
  13
+ (function() {
  14
+
  15
+    var global = this;          // capture reference to global scope     
  16
+    var version = '0.1.2';
  17
+    var globalizedApi = false;  // whether or not api has been injected into global scope    
  18
+    var callbacks = {};         // collection of scheduled functions    
  19
+    var advancedMs = 0;         // accumulation of total requested ms advancements
  20
+    var elapsedMs = 0;          // accumulation of current time as of each callback    
  21
+    var funcCount = 0;          // number of scheduled functions
  22
+    var currentlyAdvancing = false;     // whether or not an advance is in motion
  23
+    var executionInterrupted = false;   // whether or not last advance was interrupted
  24
+
  25
+    /**
  26
+     * Captures references to original values of timing functions
  27
+     */
  28
+    var originalClock = {
  29
+        setTimeout: global.setTimeout,
  30
+        setInterval: global.setInterval,
  31
+        clearTimeout: global.clearTimeout,
  32
+        clearInterval: global.clearInterval,
  33
+        Date: global.Date
  34
+    };
  35
+
  36
+    /**
  37
+     * Extension of standard Date using "parasitic inheritance"
  38
+     * http://www.crockford.com/javascript/inheritance.html
  39
+     * Intercepts requests to create Date instances of current time
  40
+     * and offsets them by the faked time advancement
  41
+     * @param {Number} year year
  42
+     * @param {Number} month month
  43
+     * @param {Number} day day of month
  44
+     * @param {Number} hour hour of day
  45
+     * @param {Number} minute minute
  46
+     * @param {Number} second second
  47
+     * @param {Number} millisecond millisecond
  48
+     * @returns date
  49
+     */
  50
+    var ShiftedDate = function(year, month, day, hour, minute, second, millisecond) {
  51
+        var shiftedDate;
  52
+        if (arguments.length === 0) {
  53
+            shiftedDate = new originalClock.Date();
  54
+            shiftedDate.setMilliseconds(shiftedDate.getMilliseconds() + effectiveOffset());
  55
+        } else if (arguments.length == 1) {
  56
+            shiftedDate = new originalClock.Date(arguments[0]);
  57
+        } else {
  58
+            shiftedDate = new originalClock.Date(
  59
+            year || null, month || null, day || null, hour || null,
  60
+            minute || null, second || null, millisecond || null);
  61
+        }
  62
+        return shiftedDate;
  63
+    };
  64
+
  65
+    /**
  66
+     * Basic extension helper for copying properties of one object to another
  67
+     * @param {Object} dest object to receive properties
  68
+     * @param {Object} src object containing properties to copy
  69
+     */
  70
+    var extend = function(dest, src) {
  71
+        for (var prop in src) {
  72
+            dest[prop] = src[prop];
  73
+        }
  74
+    };
  75
+
  76
+    /**
  77
+     * Resets fake time advancement back to 0,
  78
+     * removing all scheduled functions
  79
+     */
  80
+    var reset = function() {
  81
+        callbacks = {};
  82
+        funcCount = 0;
  83
+        advancedMs = 0;
  84
+        currentlyAdvancing = false;
  85
+        executionInterrupted = false;
  86
+        elapsedMs = 0;
  87
+    };
  88
+
  89
+    /**
  90
+     * Helper function to return whether a variable is truly numeric
  91
+     * @param {Object} value value to test
  92
+     * @returns boolean of whether value was numeric
  93
+     */
  94
+    var isNumeric = function(value) {
  95
+        return value !== null && !isNaN(value);
  96
+    };
  97
+    
  98
+    /**
  99
+     * Helper function to return the effective current offset of time
  100
+     * from the perspective of executing callbacks
  101
+     * @returns milliseconds as Number
  102
+     */
  103
+    var effectiveOffset = function() {
  104
+        return currentlyAdvancing ? elapsedMs: advancedMs;
  105
+    };    
  106
+    
  107
+    /**
  108
+     * Advances fake time by an arbitrary quantity of milliseconds,
  109
+     * executing all scheduled callbacks that would have occurred within
  110
+     * advanced range in proper native order and context
  111
+     * @param {Number} ms quantity of milliseconds to advance fake clock
  112
+     */
  113
+    var advance = function(ms) {
  114
+        // advance can optionally accept no parameters 
  115
+        // for just returning accumulated advanced offset
  116
+        if(!!ms) {            
  117
+            if (!isNumeric(ms) || ms < 0) {
  118
+                throw ("'ms' argument must be a positive number");
  119
+            }
  120
+            // scheduled callbacks to be executed within range
  121
+            var schedule = [];         
  122
+            // build an object to hold time range of this advancement
  123
+            var range = {
  124
+                start: advancedMs,
  125
+                end: advancedMs += ms                
  126
+            };
  127
+            
  128
+            // register an instance of a callback to occur
  129
+            // at a particular point in this advance's schedule
  130
+            var register = function(fn, at) {
  131
+                schedule.push({
  132
+                    fn: fn,
  133
+                    at: at                    
  134
+                });
  135
+            };
  136
+
  137
+            // loop through the scheduleing and execution of callback
  138
+            // functions since callbacks could possibly schedule more
  139
+            // callbacks of their own (which would interrupt execution)
  140
+            do {
  141
+                executionInterrupted = false;
  142
+
  143
+                // collect applicable functions to run
  144
+                for (var id in callbacks) {
  145
+                    var fn = callbacks[id];
  146
+                    
  147
+                    // schedule all non-repeating timeouts that fall within advvanced range
  148
+                    if (!fn.repeats && fn.firstRunAt <= range.end) {
  149
+                        register(fn, fn.firstRunAt);
  150
+                    // schedule repeating inervals that would fall during the range
  151
+                    } else {
  152
+                        // schedule instances of first runs of intervals
  153
+                        if (fn.lastRunAt === null &&
  154
+                            fn.firstRunAt > range.start &&
  155
+                            (fn.lastRunAt || fn.firstRunAt) <= range.end) {
  156
+                                fn.lastRunAt = fn.firstRunAt;
  157
+                                register(fn, fn.lastRunAt);
  158
+                        }
  159
+                        // add as many instances of interval callbacks as would occur within range
  160
+                        while ((fn.lastRunAt || fn.firstRunAt) + fn.ms <= range.end) {
  161
+                            fn.lastRunAt += fn.ms;
  162
+                            register(fn, fn.lastRunAt);
  163
+                        }
  164
+                    }
  165
+                }
  166
+
  167
+                // sort all the scheduled callback instances to 
  168
+                // execute in correct browser order
  169
+                schedule.sort(function(a, b) {
  170
+                    // ORDER BY
  171
+                    //   [execution point] ASC, 
  172
+                    //   [interval length] DESC, 
  173
+                    //   [order of addition] ASC
  174
+                    var order = a.at - b.at;
  175
+                    if (order === 0) {
  176
+                        order = b.fn.ms - a.fn.ms;
  177
+                        if (order === 0) {
  178
+                            order = a.fn.id - b.fn.id;
  179
+                        }
  180
+                    }
  181
+                    return order;
  182
+                });        
  183
+
  184
+                // run scheduled callback instances
  185
+                var ran = [];
  186
+                for (var i = 0; i < schedule.length; ++i) {
  187
+                    var fn = schedule[i].fn;                    
  188
+                    // only run callbacks that are still in master schedule, since a 
  189
+                    // callback could have been cleared by a subsequent run of anther callback
  190
+                    if ( !! callbacks[fn.id]) {
  191
+                        elapsedMs = schedule[i].at;
  192
+                        // run fn surrounded by a state of
  193
+                        // currently advancing
  194
+                        currentlyAdvancing = true;
  195
+                        try {
  196
+                            // run callback function on global context
  197
+                            fn.fn.apply(global);
  198
+                        } finally {
  199
+                            currentlyAdvancing = false;
  200
+                            
  201
+                            // record this fn instance as having occurred, and thus trashable
  202
+                            ran.push(i);
  203
+
  204
+                            // completely trash non-repeating instance 
  205
+                            // from ever being scheduled again 
  206
+                            if (!fn.repeats) {
  207
+                                removeCallback(fn.id);
  208
+                            }
  209
+                            
  210
+                            // execution could have been interrupted if 
  211
+                            // a callback had performed some scheduling of its own
  212
+                            if (executionInterrupted) {
  213
+                                break;
  214
+                            }
  215
+                        }                        
  216
+                    }
  217
+                }
  218
+                // remove all run callback instances from schedule
  219
+                for (var i = ran.length - 1; i >= 0; i--) {
  220
+                    schedule.splice(ran[i], 1);
  221
+                }
  222
+            }
  223
+            while (executionInterrupted);            
  224
+        }
  225
+        return effectiveOffset();
  226
+    };
  227
+
  228
+    /**
  229
+     * Adds a callback to the master schedule
  230
+     * @param {Function} fn callback function
  231
+     * @param {Number} ms millisecond at which to schedule callback
  232
+     * @returns unique Number id of scheduled callback
  233
+     */
  234
+    var addCallback = function(fn, ms, repeats) {        
  235
+        // if scheduled fn was old-school string of code
  236
+        // (yes, js officially allows for this)
  237
+        if (typeof(fn) == 'string') {
  238
+            fn = new Function(fn);
  239
+        }
  240
+        var at = effectiveOffset();
  241
+        var id = funcCount++;
  242
+        callbacks[id] = {
  243
+            id: id,
  244
+            fn: fn,
  245
+            ms: ms,
  246
+            addedAt: at,
  247
+            firstRunAt: (at + ms),
  248
+            lastRunAt: null,
  249
+            repeats: repeats
  250
+        };
  251
+        
  252
+        // stop any currently advancing range of fns
  253
+        // so that newly scheduled callback can be 
  254
+        // rolled into advance's schedule (if necessary)
  255
+        if (currentlyAdvancing) {
  256
+            executionInterrupted = true;
  257
+        }
  258
+        
  259
+        return id;
  260
+    };
  261
+
  262
+    /**
  263
+     * Removes a callback from the master schedule
  264
+     * @param {Number} id callback identifier
  265
+     */
  266
+    var removeCallback = function(id) {
  267
+        delete callbacks[id];
  268
+    };
  269
+
  270
+    /**
  271
+     * Gets (and optinally sets) value of whether
  272
+     * the native timing functions 
  273
+     * (setInterval, clearInterval, setTimeout, clearTimeout, Date) 
  274
+     * should be overwritten by DeLorean's fakes
  275
+     * @param {Boolean} shouldOverrideGlobal optional value, when passed, adds or removes the api from global scope
  276
+     * @returns {Boolean} true if native API is overwritten, false if not
  277
+     */
  278
+    var globalApi = function(shouldOverrideGlobal) {
  279
+        if (typeof(shouldOverrideGlobal) !== 'undefined') {
  280
+            globalizedApi = shouldOverrideGlobal;
  281
+            extend(global, globalizedApi ? api: originalClock);
  282
+        }
  283
+        return globalizedApi;
  284
+    };
  285
+
  286
+    /**
  287
+     * Faked timing API
  288
+     * These are kept in their own object to allow for easy 
  289
+     * extending and unextending of them from the global scope
  290
+     */
  291
+    var api = {
  292
+        setTimeout: function(fn, ms) {
  293
+            // handle exceptional parameters
  294
+            if (arguments.length === 0) {
  295
+                throw ("Function setTimeout requires at least 1 parameter");
  296
+            } else if (arguments.length === 1 && isNumeric(arguments[0])) {
  297
+                throw ("useless setTimeout call (missing quotes around argument?)");
  298
+            } else if (arguments.length === 1) {
  299
+                return addCallback(fn, 0, false);
  300
+            }
  301
+            // schedule func
  302
+            return addCallback(fn, ms, false);
  303
+        },
  304
+        setInterval: function(fn, ms) {
  305
+            // handle exceptional parameters
  306
+            if (arguments.length === 0) {
  307
+                throw ("Function setInterval requires at least 1 parameter");
  308
+            } else if (arguments.length === 1 && isNumeric(arguments[0])) {
  309
+                throw ("useless setTimeout call (missing quotes around argument?)");
  310
+            } else if (arguments.length === 1) {
  311
+                return addCallback(fn, 0, false);
  312
+            }
  313
+            // schedule func
  314
+            return addCallback(fn, ms, true);
  315
+        },
  316
+        clearTimeout: removeCallback,
  317
+        clearInterval: removeCallback,
  318
+        Date: ShiftedDate
  319
+    };
  320
+
  321
+    // expose a public api containing DeLorean utility methods
  322
+    global.DeLorean = {
  323
+        reset: reset,
  324
+        advance: advance,
  325
+        globalApi: globalApi,
  326
+        version: version
  327
+    };
  328
+    // extend public API with the timing methods
  329
+    extend(global.DeLorean, api);
  330
+
  331
+    // set the initial state
  332
+    reset();    
  333
+})();
10  spec/lib/js-test-driver/qunit/History.txt
... ...
@@ -0,0 +1,10 @@
  1
+1.0.1
  2
+-----
  3
+
  4
+Fixed ok() assertion to behave the same as jquery (it now succeeds with anything other than 0, false, or null)
  5
+
  6
+
  7
+1.0
  8
+---
  9
+
  10
+First release
77  spec/lib/js-test-driver/qunit/QUnitAdapter.js
... ...
@@ -0,0 +1,77 @@
  1
+/*
  2
+QUnitAdapter
  3
+Version: 1.0.1
  4
+
  5
+Run qunit tests using JS Test Driver
  6
+
  7
+This provides almost the same api as qunit.
  8
+
  9
+Tests must run sychronously, which means no use of stop and start methods.
  10
+You can use jsUnit Clock object to deal with timeouts and intervals:
  11
+http://googletesting.blogspot.com/2007/03/javascript-simulating-time-in-jsunit.html
  12
+
  13
+The qunit #main DOM element is not included. If you need to do any DOM manipulation
  14
+you need to set it up and tear it down in each test.
  15
+
  16
+*/
  17
+(function() {
  18
+   
  19
+    window.module = function(name, lifecycle) {
  20
+        QUnitTestCase = TestCase(name);
  21
+		QUnitTestCase.tearDown = function() {};
  22
+
  23
+        if (lifecycle) {
  24
+            QUnitTestCase.prototype.setUp = lifecycle.setup;
  25
+            QUnitTestCase.tearDown = lifecycle.teardown;
  26
+        }
  27
+    };
  28
+    
  29
+    window.test = function(name, test) {
  30
+		var tearDown = QUnitTestCase.tearDown;
  31
+        QUnitTestCase.prototype['test ' + name] = function() {
  32
+			try {
  33
+				test();
  34
+			} catch(ex) {
  35
+				throw(ex);
  36
+			} finally {
  37
+				tearDown();
  38
+			}
  39
+		};
  40
+    };
  41
+    
  42
+    window.expect = function(count) {
  43
+        expectAsserts(count);
  44
+    };
  45
+    
  46
+    window.ok = function(actual, msg) {
  47
+        assertTrue(msg ? msg : '', !!actual);
  48
+    };
  49
+    
  50
+    window.equals = function(a, b, msg) {
  51
+        assertEquals(msg ? msg : '', b, a);
  52
+    };
  53
+    
  54
+    window.start = window.stop = function() {
  55
+        fail('start and stop methods are not available when using JS Test Driver.\n' +
  56
+            'Use jsUnit Clock object to deal with timeouts and intervals:\n' + 
  57
+            'http://googletesting.blogspot.com/2007/03/javascript-simulating-time-in-jsunit.html.');
  58
+    };
  59
+    
  60
+    window.same = function(a, b, msg) {
  61
+        assertTrue(msg ? msg : '', window.equiv(b, a));
  62
+    };
  63
+    
  64
+    window.reset = function() {
  65
+    	fail('reset method is not available when using JS Test Driver');
  66
+    };
  67
+
  68
+    window.isLocal = function() {
  69
+    	return false;
  70
+    };
  71
+    
  72
+    window.QUnit = {
  73
+    	equiv: window.equiv,
  74
+    	ok: window.ok
  75
+    };
  76
+
  77
+})();
74  spec/lib/js-test-driver/qunit/README
... ...
@@ -0,0 +1,74 @@
  1
+QUnit to JS Test Driver adapter
  2
+===============================
  3
+
  4
+Version: 1.0.1
  5
+Author: Karl O'Keeffe (karl@monket.net, http://monket.net)
  6
+Blog post introduction: http://monket.net/blog/2009/06/new-qunit-to-js-test-driver-adapter/
  7
+http://code.google.com/p/js-test-driver/source/browse/#svn/trunk/JsTestDriver/contrib/qunit
  8
+
  9
+Introduction
  10
+------------
  11
+
  12
+Qunit Adapter provides a small wrapper around your QUnit tests that allows them to be run using JS Test Driver.
  13
+
  14
+It works by converting each qunit test and assertion into corresponding JS Test Driver test methods and assertions. Each qunit module maps to a JS Test Driver TestCase, each qunit test maps to a test method on that TestCase. And each qunit ok, equals, or same assertion maps to a JS Test Driver assertion. Qunit lifecycles (setup and teardown) also map to JS Test Driver setUp and tearDown.
  15
+
  16
+This ensures you still get assertion level error reporting when running your qunit tests with JS Test Driver.
  17
+
  18
+Essentially this adapter allows you to write native JS Test Driver tests, but using the less verbose qunit syntax.
  19
+
  20
+
  21
+Installing the QUnit Adapter
  22
+----------------------------
  23
+
  24
+Copy both the equiv.js and QUnitAdapter.js files to your project test directory (for example tests/qunit/).
  25
+
  26
+
  27
+Configuring JS Test Driver
  28
+--------------------------
  29
+
  30
+To run your qunit tests in JS Test Driver you need to configure it to load the adapter before your qunit tests.
  31
+
  32
+Update your jsTestDriver.conf to load the files:
  33
+
  34
+	server: http://localhost:9876
  35
+ 
  36
+	load:
  37
+	  # Add these lines to load the equiv function and adapter in order, before the tests
  38
+	  # (assuming they are saved to tests/qunit/)
  39
+	  - tests/qunit/equiv.js
  40
+	  - tests/qunit/QUnitAdapter.js
  41
+ 
  42
+	  # This is where we load the qunit tests
  43
+	  - tests/js/*.js
  44
+ 
  45
+	  # And this loads the source files we are testing
  46
+	  - src/js/*.js
  47
+
  48
+
  49
+Running JS Test Driver with qunit tests
  50
+---------------------------------------
  51
+
  52
+Now we can run JS Test Driver and watch as it runs all our qunit tests!
  53
+
  54
+The tests will run as individual JS Test Driver tests, with the format Module Name.Test Name.
  55
+
  56
+Example output:
  57
+
  58
+	[PASSED] Module 1.test Test 1
  59
+	[PASSED] Module 1.test Test 2
  60
+	[PASSED] Module 2.test Test 1
  61
+	Total 3 tests (Passed: 3; Fails: 0; Errors: 0) (1.00 ms)
  62
+	  Safari 530.18: Run 3 tests (Passed: 3; Fails: 0; Errors 0) (1.00 ms)
  63
+
  64
+
  65
+Limitations
  66
+-----------
  67
+
  68
+There are a few limitations on which qunit tests will successfully be converted.
  69
+
  70
+The tests must run synchronously (which means no use of the qunit stop and start methods).
  71
+
  72
+If you need to test timeouts, intervals, or other asynchronous sections of code, consider using the jsUnit Clock object to deal with timeouts and intervals.
  73
+
  74
+QUnit DOM support is not included. Consider avoiding interacting directly with the browser within your unit tests. But if you do need to, you’ll need to create and remove the DOM objects yourself with each test, or the setup and teardown methods.
185  spec/lib/js-test-driver/qunit/equiv.js
... ...
@@ -0,0 +1,185 @@
  1
+
  2
+// Tests for equality any JavaScript type and structure without unexpected results.
  3
+// Discussions and reference: http://philrathe.com/articles/equiv
  4
+// Test suites: http://philrathe.com/tests/equiv
  5
+// Author: Philippe RathŽ <prathe@gmail.com>
  6
+window.equiv = function () {
  7
+
  8
+    var innerEquiv; // the real equiv function
  9
+    var callers = []; // stack to decide between skip/abort functions
  10
+
  11
+    // Determine what is o.
  12
+    function hoozit(o) {
  13
+        if (typeof o === "string") {
  14
+            return "string";
  15
+
  16
+        } else if (typeof o === "boolean") {
  17
+            return "boolean";
  18
+
  19
+        } else if (typeof o === "number") {
  20
+
  21
+            if (isNaN(o)) {
  22
+                return "nan";
  23
+            } else {
  24
+                return "number";
  25
+            }
  26
+
  27
+        } else if (typeof o === "undefined") {
  28
+            return "undefined";
  29
+
  30
+        // consider: typeof null === object
  31
+        } else if (o === null) {
  32
+            return "null";
  33
+
  34
+        // consider: typeof [] === object
  35
+        } else if (o instanceof Array) {
  36
+            return "array";
  37
+        
  38
+        // consider: typeof new Date() === object
  39
+        } else if (o instanceof Date) {
  40
+            return "date";
  41
+
  42
+        // consider: /./ instanceof Object;
  43
+        //           /./ instanceof RegExp;
  44
+        //          typeof /./ === "function"; // => false in IE and Opera,
  45
+        //                                          true in FF and Safari
  46
+        } else if (o instanceof RegExp) {
  47
+            return "regexp";
  48
+
  49
+        } else if (typeof o === "object") {
  50
+            return "object";
  51
+
  52
+        } else if (o instanceof Function) {
  53
+            return "function";
  54
+        }
  55
+    }
  56
+
  57
+    // Call the o related callback with the given arguments.
  58
+    function bindCallbacks(o, callbacks, args) {
  59
+        var prop = hoozit(o);
  60
+        if (prop) {
  61
+            if (hoozit(callbacks[prop]) === "function") {
  62
+                return callbacks[prop].apply(callbacks, args);
  63
+            } else {
  64
+                return callbacks[prop]; // or undefined
  65
+            }
  66
+        }
  67
+    }
  68
+
  69
+    var callbacks = function () {
  70
+
  71
+        // for string, boolean, number and null
  72
+        function useStrictEquality(b, a) {
  73
+            return a === b;
  74
+        }
  75
+
  76
+        return {
  77
+            "string": useStrictEquality,
  78
+            "boolean": useStrictEquality,
  79
+            "number": useStrictEquality,
  80
+            "null": useStrictEquality,
  81
+            "undefined": useStrictEquality,
  82
+
  83
+            "nan": function (b) {
  84
+                return isNaN(b);
  85
+            },
  86
+
  87
+            "date": function (b, a) {
  88
+                return hoozit(b) === "date" && a.valueOf() === b.valueOf();
  89
+            },
  90
+