├── ruby_libs
└── init.rb
├── tests
├── test_helpers.js
├── notifications.js
├── test.css
├── fluid.html
├── bubbles.html
└── cookies.html
├── src
├── drivers
│ ├── bubbles.js
│ └── fluid.js
├── cookies.js
└── ssbx.js
├── Rakefile
├── README.textile
├── ssbx.js
└── lib
├── unittest.js
└── prototype.js
/ruby_libs/init.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'find'
3 | require 'ftools'
4 |
--------------------------------------------------------------------------------
/tests/test_helpers.js:
--------------------------------------------------------------------------------
1 | function include(file) {
2 | var script = document.createElement("script");
3 | script.setAttribute('src', file);
4 | $$('head').first().insert(script);
5 | }
--------------------------------------------------------------------------------
/src/drivers/bubbles.js:
--------------------------------------------------------------------------------
1 | SSBXBase.Drivers.Bubbles = Class.create({
2 | initialize: function() {
3 | if (SSBXBase.Debug) {
4 | SSB.console.init('info');
5 | }
6 | },
7 |
8 | /* Options: message, title, unique_id */
9 | notify: function(options) {
10 | /* concat these until there's something more intelligent I can do with Bubbles */
11 | var text = options['title'] + ' ' + options['message'];
12 | return SSB.simpleNotify(text);
13 | },
14 |
15 | log: function(message) {
16 | SSB.console.debug(message);
17 | }
18 | });
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'ruby_libs/init'
2 |
3 | namespace :ssbx do
4 | desc "Compiles all of the source files into ssbx.js"
5 | task :compile do
6 | source = ''
7 | Find.find('src/') do |path|
8 | if path.match /\.js/
9 | file_name = path.split('/').last
10 | source << "/* START #{file_name} */\n"
11 | source << File.read(path)
12 | source << "\n/* END #{file_name} */\n"
13 | end
14 | end
15 | File.open('ssbx.js', 'w') { |f| f.write(source) }
16 | end
17 |
18 | desc "Runs the tests"
19 | task :test do
20 | end
21 | end
--------------------------------------------------------------------------------
/src/drivers/fluid.js:
--------------------------------------------------------------------------------
1 | SSBXBase.Drivers.Fluid = Class.create({
2 | initialize: function() {
3 | },
4 |
5 | /* Options: message, title, unique_id */
6 | notify: function(options) {
7 | window.fluid.showGrowlNotification({
8 | title: options['title'],
9 | description: options['message'],
10 | priority: 1,
11 | sticky: false,
12 | identifier: options['unique_id']
13 | });
14 | },
15 |
16 | setDockBadge: function(count) {
17 | window.fluid.dockBadge = count;
18 | },
19 |
20 | log: function(message) {
21 | window.console.log(message)
22 | }
23 | });
--------------------------------------------------------------------------------
/tests/notifications.js:
--------------------------------------------------------------------------------
1 | var notificationTests = {
2 | setup: function() {
3 | },
4 |
5 | teardown: function() {
6 | SSBXBase.Internal.Cookies.destroy(SSBXBase.CookieName)
7 | },
8 |
9 | testNotify: function() {
10 | SSBX.notify({ message: 'This is a message', title: 'Hello' })
11 | },
12 |
13 | testNotifyOnce: function() { with(this) {
14 | SSBX.notifyOnce({ unique_id: 1, message: 'This should be displayed once', title: 'Hello Once' })
15 | wait(1000, function() {
16 | SSBX.notifyOnce({ unique_id: 1, message: 'This should be displayed once', title: 'Hello Once' })
17 | })
18 | }}
19 | }
20 |
--------------------------------------------------------------------------------
/src/cookies.js:
--------------------------------------------------------------------------------
1 | SSBXBase.Internal.Cookies = {
2 | save: function(name, value, days, path) {
3 | var expires = '';
4 | path = typeof path == 'undefined' ? '/' : path;
5 |
6 | if (days) {
7 | var date = new Date();
8 | date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
9 | expires = "; expires=" + date.toGMTString();
10 | }
11 |
12 | if (name && value) {
13 | document.cookie = name + '=' + escape(value) + expires + '; path=' + path;
14 | }
15 | },
16 |
17 | find: function(name) {
18 | var matches = document.cookie.match(name + '=([^;]*)');
19 |
20 | if (matches && matches.length == 2) {
21 | return unescape(matches[1]);
22 | }
23 | },
24 |
25 | destroy: function(name) {
26 | this.save(name, ' ', -1);
27 | }
28 | };
--------------------------------------------------------------------------------
/tests/test.css:
--------------------------------------------------------------------------------
1 | body { background-color: white; font-family: "Helvetica Neue", Helvetica, sans-serif; padding: 0 4em 2em 4em; margin: 0; font-size: 14px; }
2 |
3 | h1 { font-family: "Helvetica Neue", Helvetica, sans-serif; font-weight: bold; color: #411; }
4 | h2 { color: #292; font-family: "Trebuchet MS", Helvetica, sans-serif; font-size: 2em; margin: 0; padding: 5px 0; letter-spacing: -2px; }
5 | h3 { color: #141; margin: 10px 0 4px 0; padding: 0; font-size: normal}
6 | p { margin: 10px 0 0 0; padding: 0; }
7 | h1 img { margin: 0 0 0 0; padding: 0 0 0 20px; clear: none;}
8 | a { color: #333; text-decoration: none; background-color: #ffc; }
9 | a:hover { background-color: #333; color: white; text-decoration: none; }
10 |
11 | h2.page_title { color: #ff3092; text-align: left;}
12 |
13 | #Busybox { background-color: #000; color: #fff; }
14 | #HelpContainer { background-color: #000; color: #fff; }
15 |
16 | #Logo { float: right; }
--------------------------------------------------------------------------------
/tests/fluid.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 | SSBX Fluid Tests
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | SSBX Fluid Tests
20 |
21 | Tests for notifications.
22 |
23 |
24 |
25 |
26 |
27 |
28 |
41 |
42 |
--------------------------------------------------------------------------------
/tests/bubbles.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 | SSBX Bubbles Tests
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | SSBX Bubbles Tests
20 |
21 | Tests for notifications.
22 |
23 |
24 |
25 |
26 |
27 |
28 |
41 |
42 |
--------------------------------------------------------------------------------
/README.textile:
--------------------------------------------------------------------------------
1 | h2. Introduction
2 |
3 | SSBX provides a stupidly simple API for supporting Single Site Browsers (SSB) in your website.
4 |
5 | The current version is a basic implementation to test out the feasibility of the concept, and explore how to make a consistent unified API across platforms.
6 |
7 | h3. Examples
8 |
9 |
10 | if (SSBX.isAvailable()) {
11 | SSBX.log('hello');
12 | SSBX.notify({ message: 'This is a message', title: 'Title' });
13 | SSBX.notifyOnce({ message: 'This is a message', title: 'Title', unique_id: 5});
14 | SSBX.setDockBadge(5);
15 | SSBX.log(SSBX.availableDriver);
16 | }
17 |
18 |
19 | You can also only run when there's a compatible SSB available like this:
20 |
21 |
22 | SSBX.run(function() {
23 | SSBX.log('hello');
24 | }
25 |
26 |
27 | h2. Requirements
28 |
29 | SSBX requires "Prototype":http://prototypejs.org.
30 |
31 | h3. Supported SSBs
32 |
33 | * Windows: "Bubbles":http://bubbleshq.com/api
34 | * Mac OS: "Fluid":http://fluidapp.com/
35 |
36 | h3. Notification API
37 |
38 | Show a notification (requires Growl in Mac OS). Title is optional.
39 |
40 | SSBX.notify({ message: 'This is a message', title: 'Title' })
41 |
42 | Show a notification and log it so it doesn't get displayed again. Logs to a cookie:
43 |
44 | SSBX.notifyOnce({ unique_id: 1, message: 'This is a message', title: 'Title' })
45 |
46 | SSBX expects a unique integer ID for messages to be handled this way.
47 |
48 | h3. Dock badge icons
49 |
50 | Only available in Mac OS (until someone offers a Windows equivalent):
51 |
52 | SSBX.setDockBadge(3)
53 |
54 | h3. Console logging
55 |
56 | To make it easier to debug your JavaScript from within an SSB you can use unified console logging:
57 |
58 | SSBX.log('Message')
59 |
60 | h3. Capabilities
61 |
62 | * Test if the SSB is available: SSBX.isAvailable()
63 | * Check which SSB is active: SSBX.availableDriver
64 |
65 | h3. Drag and drop
66 |
67 | h2. Running tests
68 |
69 | Run tests inside the appropriate SSB. There's tests for Bubbles and Fluid.
70 |
71 | h2. Library style
72 |
73 | I've added semicolons to line endings to make compressing/obscuring the library easier (if you do that sort of thing).
--------------------------------------------------------------------------------
/tests/cookies.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 | SSBX Cookies Test
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | SSBX Cookies Test
18 |
19 | Tests for SSBXBase.Internal.Cookies
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
85 |
86 |
--------------------------------------------------------------------------------
/src/ssbx.js:
--------------------------------------------------------------------------------
1 | var SSBXBase = {
2 | Version: '0.1',
3 | Debug: true,
4 | CookieName: 'ssbx_cookies',
5 |
6 | Supported: {
7 | Fluid: function() { return !!window.fluid },
8 | Bubbles: function() { return (typeof(SSB) != 'undefined') }
9 | },
10 |
11 | API: {},
12 | Internal: {},
13 | Drivers: {}
14 | };
15 |
16 | SSBXBase.Internal = {
17 | Cookies: {},
18 |
19 | findDriver: function() {
20 | var ssb = $H(SSBXBase.Supported).find(function(ssb) {
21 | return ssb[1]()
22 | })
23 |
24 | if (ssb) {
25 | return ssb[0]
26 | }
27 | },
28 |
29 | logNotification: function(unique_id) {
30 | // Log message to cookie
31 | var cookie = this.Cookies.find(SSBXBase.CookieName);
32 |
33 | if (Object.isUndefined(cookie)) {
34 | cookie = unique_id;
35 | } else {
36 | cookie = cookie + ',' + unique_id;
37 | }
38 |
39 | this.Cookies.save(SSBXBase.CookieName, cookie, 365);
40 | },
41 |
42 | displayedNotification: function(unique_id) {
43 | var cookie = this.Cookies.find(SSBXBase.CookieName)
44 | unique_id = parseInt(unique_id)
45 |
46 | if (Object.isUndefined(cookie)) {
47 | return false
48 | }
49 |
50 | var unique_ids = $A(cookie.split(','))
51 |
52 | return unique_ids.find(function(logged_id) {
53 | if (parseInt(logged_id) == unique_id) {
54 | return true
55 | }
56 | })
57 | },
58 |
59 | // Logging uses console.log by default
60 | log: function(message) {
61 | console.log(message)
62 | }
63 | };
64 |
65 | SSBXBase.API = Class.create({
66 | initialize: function() {
67 | this.internal = SSBXBase.Internal;
68 | var delegate = this.internal.findDriver();
69 |
70 | if (delegate) {
71 | this.delegate = eval('new SSBXBase.Drivers.' + delegate);
72 |
73 | // Intended for the public interface
74 | this.availableDriver = delegate;
75 | }
76 | },
77 |
78 | isAvailable: function() {
79 | return !!this.delegate;
80 | },
81 |
82 | run: function(callback) {
83 | if (this.isAvailable()) {
84 | callback()
85 | }
86 | },
87 |
88 | // Only impleemnt the notify method
89 | notifyOnce: function(options) {
90 | // Display if it hasn't been logged
91 | if (!this.internal.displayedNotification(options['unique_id'])) {
92 | // Log the message
93 | this.internal.logNotification(options['unique_id']);
94 |
95 | return this.delegate.notify(options);
96 | }
97 | },
98 |
99 | // Implement these methods to add drivers in SSBXBase.Drivers.YourDriver
100 | notify: function(options) {
101 | return this.delegate.notify(options);
102 | },
103 |
104 | setDockBadge: function(count) {
105 | return this.delegate.setDockBadge ? this.delegate.setDockBadge(count) : false
106 | },
107 |
108 | // Logging only gets run when Debug is set to true
109 | log: function(message) {
110 | if (SSBXBase.Debug) {
111 | return this.delegate.log ? this.delegate.log(message) : SSBXBase.Internal.log(message)
112 | }
113 | }
114 | });
115 |
116 | // Create the public SSBX API object
117 | document.observe('dom:loaded', function() {
118 | SSBX = new SSBXBase.API;
119 | });
120 |
121 |
--------------------------------------------------------------------------------
/ssbx.js:
--------------------------------------------------------------------------------
1 | /* START ssbx.js */
2 | var SSBXBase = {
3 | Version: '0.1',
4 | Debug: true,
5 | CookieName: 'ssbx_cookies',
6 |
7 | Supported: {
8 | Fluid: function() { return !!window.fluid },
9 | Bubbles: function() { return (typeof(SSB) != 'undefined') }
10 | },
11 |
12 | API: {},
13 | Internal: {},
14 | Drivers: {}
15 | };
16 |
17 | SSBXBase.Internal = {
18 | Cookies: {},
19 |
20 | findDriver: function() {
21 | var ssb = $H(SSBXBase.Supported).find(function(ssb) {
22 | return ssb[1]()
23 | })
24 |
25 | if (ssb) {
26 | return ssb[0]
27 | }
28 | },
29 |
30 | logNotification: function(unique_id) {
31 | // Log message to cookie
32 | var cookie = this.Cookies.find(SSBXBase.CookieName);
33 |
34 | if (Object.isUndefined(cookie)) {
35 | cookie = unique_id;
36 | } else {
37 | cookie = cookie + ',' + unique_id;
38 | }
39 |
40 | this.Cookies.save(SSBXBase.CookieName, cookie, 365);
41 | },
42 |
43 | displayedNotification: function(unique_id) {
44 | var cookie = this.Cookies.find(SSBXBase.CookieName)
45 | unique_id = parseInt(unique_id)
46 |
47 | if (Object.isUndefined(cookie)) {
48 | return false
49 | }
50 |
51 | var unique_ids = $A(cookie.split(','))
52 |
53 | return unique_ids.find(function(logged_id) {
54 | if (parseInt(logged_id) == unique_id) {
55 | return true
56 | }
57 | })
58 | },
59 |
60 | // Logging uses console.log by default
61 | log: function(message) {
62 | console.log(message)
63 | }
64 | };
65 |
66 | SSBXBase.API = Class.create({
67 | initialize: function() {
68 | this.internal = SSBXBase.Internal;
69 | var delegate = this.internal.findDriver();
70 |
71 | if (delegate) {
72 | this.delegate = eval('new SSBXBase.Drivers.' + delegate);
73 |
74 | // Intended for the public interface
75 | this.availableDriver = delegate;
76 | }
77 | },
78 |
79 | isAvailable: function() {
80 | return !!this.delegate;
81 | },
82 |
83 | run: function(callback) {
84 | if (this.isAvailable()) {
85 | callback()
86 | }
87 | },
88 |
89 | // Only impleemnt the notify method
90 | notifyOnce: function(options) {
91 | // Display if it hasn't been logged
92 | if (!this.internal.displayedNotification(options['unique_id'])) {
93 | // Log the message
94 | this.internal.logNotification(options['unique_id']);
95 |
96 | return this.delegate.notify(options);
97 | }
98 | },
99 |
100 | // Implement these methods to add drivers in SSBXBase.Drivers.YourDriver
101 | notify: function(options) {
102 | return this.delegate.notify(options);
103 | },
104 |
105 | setDockBadge: function(count) {
106 | return this.delegate.setDockBadge ? this.delegate.setDockBadge(count) : false
107 | },
108 |
109 | // Logging only gets run when Debug is set to true
110 | log: function(message) {
111 | if (SSBXBase.Debug) {
112 | return this.delegate.log ? this.delegate.log(message) : SSBXBase.Internal.log(message)
113 | }
114 | }
115 | });
116 |
117 | // Create the public SSBX API object
118 | document.observe('dom:loaded', function() {
119 | SSBX = new SSBXBase.API;
120 | });
121 |
122 |
123 | /* END ssbx.js */
124 | /* START fluid.js */
125 | SSBXBase.Drivers.Fluid = Class.create({
126 | initialize: function() {
127 | },
128 |
129 | /* Options: message, title, unique_id */
130 | notify: function(options) {
131 | window.fluid.showGrowlNotification({
132 | title: options['title'],
133 | description: options['message'],
134 | priority: 1,
135 | sticky: false,
136 | identifier: options['unique_id']
137 | });
138 | },
139 |
140 | setDockBadge: function(count) {
141 | window.fluid.dockBadge = count;
142 | },
143 |
144 | log: function(message) {
145 | window.console.log(message)
146 | }
147 | });
148 | /* END fluid.js */
149 | /* START bubbles.js */
150 | SSBXBase.Drivers.Bubbles = Class.create({
151 | initialize: function() {
152 | if (SSBXBase.Debug) {
153 | SSB.console.init('info');
154 | }
155 | },
156 |
157 | /* Options: message, title, unique_id */
158 | notify: function(options) {
159 | /* concat these until there's something more intelligent I can do with Bubbles */
160 | var text = options['title'] + ' ' + options['message'];
161 | return SSB.simpleNotify(text);
162 | },
163 |
164 | log: function(message) {
165 | SSB.console.debug(message);
166 | }
167 | });
168 | /* END bubbles.js */
169 | /* START cookies.js */
170 | SSBXBase.Internal.Cookies = {
171 | save: function(name, value, days, path) {
172 | var expires = '';
173 | path = typeof path == 'undefined' ? '/' : path;
174 |
175 | if (days) {
176 | var date = new Date();
177 | date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
178 | expires = "; expires=" + date.toGMTString();
179 | }
180 |
181 | if (name && value) {
182 | document.cookie = name + '=' + escape(value) + expires + '; path=' + path;
183 | }
184 | },
185 |
186 | find: function(name) {
187 | var matches = document.cookie.match(name + '=([^;]*)');
188 |
189 | if (matches && matches.length == 2) {
190 | return unescape(matches[1]);
191 | }
192 | },
193 |
194 | destroy: function(name) {
195 | this.save(name, ' ', -1);
196 | }
197 | };
198 | /* END cookies.js */
199 |
--------------------------------------------------------------------------------
/lib/unittest.js:
--------------------------------------------------------------------------------
1 | // script.aculo.us unittest.js v1.8.0, Tue Nov 06 15:01:40 +0300 2007
2 |
3 | // Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
4 | // (c) 2005-2007 Jon Tirsen (http://www.tirsen.com)
5 | // (c) 2005-2007 Michael Schuerig (http://www.schuerig.de/michael/)
6 | //
7 | // script.aculo.us is freely distributable under the terms of an MIT-style license.
8 | // For details, see the script.aculo.us web site: http://script.aculo.us/
9 |
10 | // experimental, Firefox-only
11 | Event.simulateMouse = function(element, eventName) {
12 | var options = Object.extend({
13 | pointerX: 0,
14 | pointerY: 0,
15 | buttons: 0,
16 | ctrlKey: false,
17 | altKey: false,
18 | shiftKey: false,
19 | metaKey: false
20 | }, arguments[2] || {});
21 | var oEvent = document.createEvent("MouseEvents");
22 | oEvent.initMouseEvent(eventName, true, true, document.defaultView,
23 | options.buttons, options.pointerX, options.pointerY, options.pointerX, options.pointerY,
24 | options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, 0, $(element));
25 |
26 | if(this.mark) Element.remove(this.mark);
27 | this.mark = document.createElement('div');
28 | this.mark.appendChild(document.createTextNode(" "));
29 | document.body.appendChild(this.mark);
30 | this.mark.style.position = 'absolute';
31 | this.mark.style.top = options.pointerY + "px";
32 | this.mark.style.left = options.pointerX + "px";
33 | this.mark.style.width = "5px";
34 | this.mark.style.height = "5px;";
35 | this.mark.style.borderTop = "1px solid red;"
36 | this.mark.style.borderLeft = "1px solid red;"
37 |
38 | if(this.step)
39 | alert('['+new Date().getTime().toString()+'] '+eventName+'/'+Test.Unit.inspect(options));
40 |
41 | $(element).dispatchEvent(oEvent);
42 | };
43 |
44 | // Note: Due to a fix in Firefox 1.0.5/6 that probably fixed "too much", this doesn't work in 1.0.6 or DP2.
45 | // You need to downgrade to 1.0.4 for now to get this working
46 | // See https://bugzilla.mozilla.org/show_bug.cgi?id=289940 for the fix that fixed too much
47 | Event.simulateKey = function(element, eventName) {
48 | var options = Object.extend({
49 | ctrlKey: false,
50 | altKey: false,
51 | shiftKey: false,
52 | metaKey: false,
53 | keyCode: 0,
54 | charCode: 0
55 | }, arguments[2] || {});
56 |
57 | var oEvent = document.createEvent("KeyEvents");
58 | oEvent.initKeyEvent(eventName, true, true, window,
59 | options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
60 | options.keyCode, options.charCode );
61 | $(element).dispatchEvent(oEvent);
62 | };
63 |
64 | Event.simulateKeys = function(element, command) {
65 | for(var i=0; i' +
116 | '' +
117 | 'Status Test Message ' +
118 | ' ' +
119 | '
';
120 | this.logsummary = $('logsummary')
121 | this.loglines = $('loglines');
122 | },
123 | _toHTML: function(txt) {
124 | return txt.escapeHTML().replace(/\n/g," ");
125 | },
126 | addLinksToResults: function(){
127 | $$("tr.failed .nameCell").each( function(td){ // todo: limit to children of this.log
128 | td.title = "Run only this test"
129 | Event.observe(td, 'click', function(){ window.location.search = "?tests=" + td.innerHTML;});
130 | });
131 | $$("tr.passed .nameCell").each( function(td){ // todo: limit to children of this.log
132 | td.title = "Run all tests"
133 | Event.observe(td, 'click', function(){ window.location.search = "";});
134 | });
135 | }
136 | }
137 |
138 | Test.Unit.Runner = Class.create();
139 | Test.Unit.Runner.prototype = {
140 | initialize: function(testcases) {
141 | this.options = Object.extend({
142 | testLog: 'testlog'
143 | }, arguments[1] || {});
144 | this.options.resultsURL = this.parseResultsURLQueryParameter();
145 | this.options.tests = this.parseTestsQueryParameter();
146 | if (this.options.testLog) {
147 | this.options.testLog = $(this.options.testLog) || null;
148 | }
149 | if(this.options.tests) {
150 | this.tests = [];
151 | for(var i = 0; i < this.options.tests.length; i++) {
152 | if(/^test/.test(this.options.tests[i])) {
153 | this.tests.push(new Test.Unit.Testcase(this.options.tests[i], testcases[this.options.tests[i]], testcases["setup"], testcases["teardown"]));
154 | }
155 | }
156 | } else {
157 | if (this.options.test) {
158 | this.tests = [new Test.Unit.Testcase(this.options.test, testcases[this.options.test], testcases["setup"], testcases["teardown"])];
159 | } else {
160 | this.tests = [];
161 | for(var testcase in testcases) {
162 | if(/^test/.test(testcase)) {
163 | this.tests.push(
164 | new Test.Unit.Testcase(
165 | this.options.context ? ' -> ' + this.options.titles[testcase] : testcase,
166 | testcases[testcase], testcases["setup"], testcases["teardown"]
167 | ));
168 | }
169 | }
170 | }
171 | }
172 | this.currentTest = 0;
173 | this.logger = new Test.Unit.Logger(this.options.testLog);
174 | setTimeout(this.runTests.bind(this), 1000);
175 | },
176 | parseResultsURLQueryParameter: function() {
177 | return window.location.search.parseQuery()["resultsURL"];
178 | },
179 | parseTestsQueryParameter: function(){
180 | if (window.location.search.parseQuery()["tests"]){
181 | return window.location.search.parseQuery()["tests"].split(',');
182 | };
183 | },
184 | // Returns:
185 | // "ERROR" if there was an error,
186 | // "FAILURE" if there was a failure, or
187 | // "SUCCESS" if there was neither
188 | getResult: function() {
189 | var hasFailure = false;
190 | for(var i=0;i 0) {
192 | return "ERROR";
193 | }
194 | if (this.tests[i].failures > 0) {
195 | hasFailure = true;
196 | }
197 | }
198 | if (hasFailure) {
199 | return "FAILURE";
200 | } else {
201 | return "SUCCESS";
202 | }
203 | },
204 | postResults: function() {
205 | if (this.options.resultsURL) {
206 | new Ajax.Request(this.options.resultsURL,
207 | { method: 'get', parameters: 'result=' + this.getResult(), asynchronous: false });
208 | }
209 | },
210 | runTests: function() {
211 | var test = this.tests[this.currentTest];
212 | if (!test) {
213 | // finished!
214 | this.postResults();
215 | this.logger.summary(this.summary());
216 | return;
217 | }
218 | if(!test.isWaiting) {
219 | this.logger.start(test.name);
220 | }
221 | test.run();
222 | if(test.isWaiting) {
223 | this.logger.message("Waiting for " + test.timeToWait + "ms");
224 | setTimeout(this.runTests.bind(this), test.timeToWait || 1000);
225 | } else {
226 | this.logger.finish(test.status(), test.summary());
227 | this.currentTest++;
228 | // tail recursive, hopefully the browser will skip the stackframe
229 | this.runTests();
230 | }
231 | },
232 | summary: function() {
233 | var assertions = 0;
234 | var failures = 0;
235 | var errors = 0;
236 | var messages = [];
237 | for(var i=0;i 0) return 'failed';
282 | if (this.errors > 0) return 'error';
283 | return 'passed';
284 | },
285 | assert: function(expression) {
286 | var message = arguments[1] || 'assert: got "' + Test.Unit.inspect(expression) + '"';
287 | try { expression ? this.pass() :
288 | this.fail(message); }
289 | catch(e) { this.error(e); }
290 | },
291 | assertEqual: function(expected, actual) {
292 | var message = arguments[2] || "assertEqual";
293 | try { (expected == actual) ? this.pass() :
294 | this.fail(message + ': expected "' + Test.Unit.inspect(expected) +
295 | '", actual "' + Test.Unit.inspect(actual) + '"'); }
296 | catch(e) { this.error(e); }
297 | },
298 | assertInspect: function(expected, actual) {
299 | var message = arguments[2] || "assertInspect";
300 | try { (expected == actual.inspect()) ? this.pass() :
301 | this.fail(message + ': expected "' + Test.Unit.inspect(expected) +
302 | '", actual "' + Test.Unit.inspect(actual) + '"'); }
303 | catch(e) { this.error(e); }
304 | },
305 | assertEnumEqual: function(expected, actual) {
306 | var message = arguments[2] || "assertEnumEqual";
307 | try { $A(expected).length == $A(actual).length &&
308 | expected.zip(actual).all(function(pair) { return pair[0] == pair[1] }) ?
309 | this.pass() : this.fail(message + ': expected ' + Test.Unit.inspect(expected) +
310 | ', actual ' + Test.Unit.inspect(actual)); }
311 | catch(e) { this.error(e); }
312 | },
313 | assertNotEqual: function(expected, actual) {
314 | var message = arguments[2] || "assertNotEqual";
315 | try { (expected != actual) ? this.pass() :
316 | this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); }
317 | catch(e) { this.error(e); }
318 | },
319 | assertIdentical: function(expected, actual) {
320 | var message = arguments[2] || "assertIdentical";
321 | try { (expected === actual) ? this.pass() :
322 | this.fail(message + ': expected "' + Test.Unit.inspect(expected) +
323 | '", actual "' + Test.Unit.inspect(actual) + '"'); }
324 | catch(e) { this.error(e); }
325 | },
326 | assertNotIdentical: function(expected, actual) {
327 | var message = arguments[2] || "assertNotIdentical";
328 | try { !(expected === actual) ? this.pass() :
329 | this.fail(message + ': expected "' + Test.Unit.inspect(expected) +
330 | '", actual "' + Test.Unit.inspect(actual) + '"'); }
331 | catch(e) { this.error(e); }
332 | },
333 | assertNull: function(obj) {
334 | var message = arguments[1] || 'assertNull'
335 | try { (obj==null) ? this.pass() :
336 | this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); }
337 | catch(e) { this.error(e); }
338 | },
339 | assertMatch: function(expected, actual) {
340 | var message = arguments[2] || 'assertMatch';
341 | var regex = new RegExp(expected);
342 | try { (regex.exec(actual)) ? this.pass() :
343 | this.fail(message + ' : regex: "' + Test.Unit.inspect(expected) + ' did not match: ' + Test.Unit.inspect(actual) + '"'); }
344 | catch(e) { this.error(e); }
345 | },
346 | assertHidden: function(element) {
347 | var message = arguments[1] || 'assertHidden';
348 | this.assertEqual("none", element.style.display, message);
349 | },
350 | assertNotNull: function(object) {
351 | var message = arguments[1] || 'assertNotNull';
352 | this.assert(object != null, message);
353 | },
354 | assertType: function(expected, actual) {
355 | var message = arguments[2] || 'assertType';
356 | try {
357 | (actual.constructor == expected) ? this.pass() :
358 | this.fail(message + ': expected "' + Test.Unit.inspect(expected) +
359 | '", actual "' + (actual.constructor) + '"'); }
360 | catch(e) { this.error(e); }
361 | },
362 | assertNotOfType: function(expected, actual) {
363 | var message = arguments[2] || 'assertNotOfType';
364 | try {
365 | (actual.constructor != expected) ? this.pass() :
366 | this.fail(message + ': expected "' + Test.Unit.inspect(expected) +
367 | '", actual "' + (actual.constructor) + '"'); }
368 | catch(e) { this.error(e); }
369 | },
370 | assertInstanceOf: function(expected, actual) {
371 | var message = arguments[2] || 'assertInstanceOf';
372 | try {
373 | (actual instanceof expected) ? this.pass() :
374 | this.fail(message + ": object was not an instance of the expected type"); }
375 | catch(e) { this.error(e); }
376 | },
377 | assertNotInstanceOf: function(expected, actual) {
378 | var message = arguments[2] || 'assertNotInstanceOf';
379 | try {
380 | !(actual instanceof expected) ? this.pass() :
381 | this.fail(message + ": object was an instance of the not expected type"); }
382 | catch(e) { this.error(e); }
383 | },
384 | assertRespondsTo: function(method, obj) {
385 | var message = arguments[2] || 'assertRespondsTo';
386 | try {
387 | (obj[method] && typeof obj[method] == 'function') ? this.pass() :
388 | this.fail(message + ": object doesn't respond to [" + method + "]"); }
389 | catch(e) { this.error(e); }
390 | },
391 | assertReturnsTrue: function(method, obj) {
392 | var message = arguments[2] || 'assertReturnsTrue';
393 | try {
394 | var m = obj[method];
395 | if(!m) m = obj['is'+method.charAt(0).toUpperCase()+method.slice(1)];
396 | m() ? this.pass() :
397 | this.fail(message + ": method returned false"); }
398 | catch(e) { this.error(e); }
399 | },
400 | assertReturnsFalse: function(method, obj) {
401 | var message = arguments[2] || 'assertReturnsFalse';
402 | try {
403 | var m = obj[method];
404 | if(!m) m = obj['is'+method.charAt(0).toUpperCase()+method.slice(1)];
405 | !m() ? this.pass() :
406 | this.fail(message + ": method returned true"); }
407 | catch(e) { this.error(e); }
408 | },
409 | assertRaise: function(exceptionName, method) {
410 | var message = arguments[2] || 'assertRaise';
411 | try {
412 | method();
413 | this.fail(message + ": exception expected but none was raised"); }
414 | catch(e) {
415 | ((exceptionName == null) || (e.name==exceptionName)) ? this.pass() : this.error(e);
416 | }
417 | },
418 | assertElementsMatch: function() {
419 | var expressions = $A(arguments), elements = $A(expressions.shift());
420 | if (elements.length != expressions.length) {
421 | this.fail('assertElementsMatch: size mismatch: ' + elements.length + ' elements, ' + expressions.length + ' expressions');
422 | return false;
423 | }
424 | elements.zip(expressions).all(function(pair, index) {
425 | var element = $(pair.first()), expression = pair.last();
426 | if (element.match(expression)) return true;
427 | this.fail('assertElementsMatch: (in index ' + index + ') expected ' + expression.inspect() + ' but got ' + element.inspect());
428 | }.bind(this)) && this.pass();
429 | },
430 | assertElementMatches: function(element, expression) {
431 | this.assertElementsMatch([element], expression);
432 | },
433 | benchmark: function(operation, iterations) {
434 | var startAt = new Date();
435 | (iterations || 1).times(operation);
436 | var timeTaken = ((new Date())-startAt);
437 | this.info((arguments[2] || 'Operation') + ' finished ' +
438 | iterations + ' iterations in ' + (timeTaken/1000)+'s' );
439 | return timeTaken;
440 | },
441 | _isVisible: function(element) {
442 | element = $(element);
443 | if(!element.parentNode) return true;
444 | this.assertNotNull(element);
445 | if(element.style && Element.getStyle(element, 'display') == 'none')
446 | return false;
447 |
448 | return this._isVisible(element.parentNode);
449 | },
450 | assertNotVisible: function(element) {
451 | this.assert(!this._isVisible(element), Test.Unit.inspect(element) + " was not hidden and didn't have a hidden parent either. " + ("" || arguments[1]));
452 | },
453 | assertVisible: function(element) {
454 | this.assert(this._isVisible(element), Test.Unit.inspect(element) + " was not visible. " + ("" || arguments[1]));
455 | },
456 | benchmark: function(operation, iterations) {
457 | var startAt = new Date();
458 | (iterations || 1).times(operation);
459 | var timeTaken = ((new Date())-startAt);
460 | this.info((arguments[2] || 'Operation') + ' finished ' +
461 | iterations + ' iterations in ' + (timeTaken/1000)+'s' );
462 | return timeTaken;
463 | }
464 | }
465 |
466 | Test.Unit.Testcase = Class.create();
467 | Object.extend(Object.extend(Test.Unit.Testcase.prototype, Test.Unit.Assertions.prototype), {
468 | initialize: function(name, test, setup, teardown) {
469 | Test.Unit.Assertions.prototype.initialize.bind(this)();
470 | this.name = name;
471 |
472 | if(typeof test == 'string') {
473 | test = test.gsub(/(\.should[^\(]+\()/,'#{0}this,');
474 | test = test.gsub(/(\.should[^\(]+)\(this,\)/,'#{1}(this)');
475 | this.test = function() {
476 | eval('with(this){'+test+'}');
477 | }
478 | } else {
479 | this.test = test || function() {};
480 | }
481 |
482 | this.setup = setup || function() {};
483 | this.teardown = teardown || function() {};
484 | this.isWaiting = false;
485 | this.timeToWait = 1000;
486 | },
487 | wait: function(time, nextPart) {
488 | this.isWaiting = true;
489 | this.test = nextPart;
490 | this.timeToWait = time;
491 | },
492 | run: function() {
493 | try {
494 | try {
495 | if (!this.isWaiting) this.setup.bind(this)();
496 | this.isWaiting = false;
497 | this.test.bind(this)();
498 | } finally {
499 | if(!this.isWaiting) {
500 | this.teardown.bind(this)();
501 | }
502 | }
503 | }
504 | catch(e) { this.error(e); }
505 | }
506 | });
507 |
508 | // *EXPERIMENTAL* BDD-style testing to please non-technical folk
509 | // This draws many ideas from RSpec http://rspec.rubyforge.org/
510 |
511 | Test.setupBDDExtensionMethods = function(){
512 | var METHODMAP = {
513 | shouldEqual: 'assertEqual',
514 | shouldNotEqual: 'assertNotEqual',
515 | shouldEqualEnum: 'assertEnumEqual',
516 | shouldBeA: 'assertType',
517 | shouldNotBeA: 'assertNotOfType',
518 | shouldBeAn: 'assertType',
519 | shouldNotBeAn: 'assertNotOfType',
520 | shouldBeNull: 'assertNull',
521 | shouldNotBeNull: 'assertNotNull',
522 |
523 | shouldBe: 'assertReturnsTrue',
524 | shouldNotBe: 'assertReturnsFalse',
525 | shouldRespondTo: 'assertRespondsTo'
526 | };
527 | var makeAssertion = function(assertion, args, object) {
528 | this[assertion].apply(this,(args || []).concat([object]));
529 | }
530 |
531 | Test.BDDMethods = {};
532 | $H(METHODMAP).each(function(pair) {
533 | Test.BDDMethods[pair.key] = function() {
534 | var args = $A(arguments);
535 | var scope = args.shift();
536 | makeAssertion.apply(scope, [pair.value, args, this]); };
537 | });
538 |
539 | [Array.prototype, String.prototype, Number.prototype, Boolean.prototype].each(
540 | function(p){ Object.extend(p, Test.BDDMethods) }
541 | );
542 | }
543 |
544 | Test.context = function(name, spec, log){
545 | Test.setupBDDExtensionMethods();
546 |
547 | var compiledSpec = {};
548 | var titles = {};
549 | for(specName in spec) {
550 | switch(specName){
551 | case "setup":
552 | case "teardown":
553 | compiledSpec[specName] = spec[specName];
554 | break;
555 | default:
556 | var testName = 'test'+specName.gsub(/\s+/,'-').camelize();
557 | var body = spec[specName].toString().split('\n').slice(1);
558 | if(/^\{/.test(body[0])) body = body.slice(1);
559 | body.pop();
560 | body = body.map(function(statement){
561 | return statement.strip()
562 | });
563 | compiledSpec[testName] = body.join('\n');
564 | titles[testName] = specName;
565 | }
566 | }
567 | new Test.Unit.Runner(compiledSpec, { titles: titles, testLog: log || 'testlog', context: name });
568 | };
--------------------------------------------------------------------------------
/lib/prototype.js:
--------------------------------------------------------------------------------
1 | /* Prototype JavaScript framework, version 1.6.0.1
2 | * (c) 2005-2007 Sam Stephenson
3 | *
4 | * Prototype is freely distributable under the terms of an MIT-style license.
5 | * For details, see the Prototype web site: http://www.prototypejs.org/
6 | *
7 | *--------------------------------------------------------------------------*/
8 |
9 | var Prototype = {
10 | Version: '1.6.0.1',
11 |
12 | Browser: {
13 | IE: !!(window.attachEvent && !window.opera),
14 | Opera: !!window.opera,
15 | WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
16 | Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1,
17 | MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
18 | },
19 |
20 | BrowserFeatures: {
21 | XPath: !!document.evaluate,
22 | ElementExtensions: !!window.HTMLElement,
23 | SpecificElementExtensions:
24 | document.createElement('div').__proto__ &&
25 | document.createElement('div').__proto__ !==
26 | document.createElement('form').__proto__
27 | },
28 |
29 | ScriptFragment: '