├── .gitignore ├── riotjs.html ├── test.js ├── README.textile └── riot.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /riotjs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
10 | Riot.run(function() {
11 | context('basic riot functionality', function() {
12 | given('some simple equality tests', function() {
13 | asserts('a simple truth test should return true', true).isTrue();
14 | asserts('isNull is null', null).isNull();
15 | });
16 |
17 | given('another context', function() {
18 | asserts('equals should compare strings as expected', 'test string').equals('test string');
19 | });
20 |
21 | given('a context concerned with functions', function() {
22 | asserts('asserts() should allow functions to be compared', function() {
23 | return 'test string';
24 | }).equals('test string');
25 | });
26 | });
27 |
28 | given('yet another context', function() {
29 | asserts('equals should compare strings as expected', 'test string').equals('test string');
30 | });
31 | });
32 |
33 |
34 | There are a few aliases to make tests (and output) read more naturally:
35 |
36 | * context: given -- Given will add "Given..." to the title in the test output
37 | * asserts: should
38 |
39 | h3. Assertions
40 |
41 | * equals - for example, asserts('description').equals('value')
42 | * matches - matches a regex
43 | * typeOf - aliased to isTypeOf, kindOf
44 | * isNull
45 | * isTrue
46 | * raises
47 |
48 | h3. Riot.run
49 |
50 | Riot.run(function() { // your tests }); just adds your tests to window.onload. If there's already an onload handler it won't add itself. If there's no window it will just run the tests.
51 |
52 | Riot.load() can be used to load required scripts in a browser or interpreter. Riot.require() only loads files once by keeping a record of files that have already been loaded.
53 |
54 | It can also be called with no parameters to run tests defined with Riot.context. This can be used to create a set of files with tests inside a Riot.context for each file
55 |
56 | h3. Packaging
57 |
58 | Packaged as a RubyGem usable via "XPCOMCore-RubyGems":http://github.com/conflagrationjs/xpcomcore-rubygem -- "riotjs-xpcc":http://github.com/conflagrationjs/riotjs-xpcc.
59 |
60 | h3. Writing a test for a browser AND interpreter
61 |
62 | My current pattern looks like this:
63 |
64 |
65 | var Riot;
66 | if (typeof load !== 'undefined') {
67 | load('riot.js');
68 | } else if (typeof require !== 'undefined') {
69 | Riot = require('./riot').Riot;
70 | }
71 |
72 | Riot.require('../turing.core.js');
73 | Riot.require('../turing.oo.js');
74 | Riot.require('fixtures/example_classes.js');
75 |
76 | Riot.context('turing.oo.js', function() {
77 | // Tests go here
78 | });
79 |
80 | Riot.run();
81 |
82 |
83 | 84 | 85 | 86 | 87 |102 | 103 | Note: 104 | 105 | *Riot 88 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 |
load() is no-op'd in browsers if it doesn't exist by Riot
106 | * Riot.run() will only run once, so you can include multiple Riot test files
107 | * Specifying Riot.run() at the end of each test file lets you run that file with js Riot.run() in the console
108 |
109 | h3. Contributors
110 |
111 | * ac94 (Aron Carroll)
112 | * bgerrissen (Ben Gerrissen)
113 |
114 |
--------------------------------------------------------------------------------
/riot.js:
--------------------------------------------------------------------------------
1 | /*jslint white: false plusplus: false onevar: false browser: true evil: true*/
2 | /*riotGlobal window: true*/
3 | (function(riotGlobal) {
4 | var Riot = {
5 | results: [],
6 | contexts: [],
7 |
8 | run: function(tests) {
9 | switch (Riot.detectEnvironment()) {
10 | case 'xpcomcore':
11 | Riot.formatter = new Riot.Formatters.XPComCore();
12 | Riot.runAndReport(tests);
13 | Sys.exit(Riot.exitCode);
14 | break;
15 |
16 | case 'rhino':
17 | Riot.formatter = new Riot.Formatters.Text();
18 | Riot.runAndReport(tests);
19 | java.lang.System.exit(Riot.exitCode);
20 | break;
21 |
22 | case 'node':
23 | Riot.formatter = new Riot.Formatters.Text();
24 | Riot.runAndReport(tests);
25 | // TODO: exit with exit code from riot
26 | break;
27 |
28 | case 'non-browser-interpreter':
29 | Riot.formatter = new Riot.Formatters.Text();
30 | Riot.runAndReport(tests);
31 | if (typeof quit !== 'undefined') {
32 | quit(Riot.exitCode);
33 | }
34 | break;
35 |
36 | case 'browser':
37 | Riot.formatter = new Riot.Formatters.HTML();
38 | if (typeof window.onload === 'undefined' || window.onload == null) {
39 | Riot.browserAutoLoad(tests);
40 | }
41 | break;
42 | }
43 | },
44 |
45 | browserAutoLoad: function(tests) {
46 | var timer;
47 | function fireContentLoadedEvent() {
48 | if (document.loaded) return;
49 | if (timer) window.clearTimeout(timer);
50 | document.loaded = true;
51 |
52 | if (Riot.requiredFiles.length > 0) {
53 | Riot.loadBrowserScripts(Riot.requiredFiles, tests);
54 | } else {
55 | Riot.runAndReport(tests);
56 | }
57 | }
58 |
59 | function checkReadyState() {
60 | if (document.readyState === 'complete') {
61 | document.detachEvent('readystatechange', checkReadyState);
62 | fireContentLoadedEvent();
63 | }
64 | }
65 |
66 | function pollDoScroll() {
67 | try { document.documentElement.doScroll('left'); }
68 | catch(e) {
69 | timer = setTimeout(pollDoScroll, 10);
70 | return;
71 | }
72 | fireContentLoadedEvent();
73 | }
74 |
75 | if (document.addEventListener) {
76 | document.addEventListener('DOMContentLoaded', fireContentLoadedEvent, false);
77 | } else {
78 | document.attachEvent('readystatechange', checkReadyState);
79 | if (window == top)
80 | timer = setTimeout(pollDoScroll, 10);
81 | }
82 |
83 | window.onload = fireContentLoadedEvent;
84 | },
85 |
86 | loadBrowserScripts: function(files, tests) {
87 | var i, file;
88 |
89 | function loadBrowserScript(src, callback) {
90 | var script = document.createElement('script'),
91 | head = document.getElementsByTagName('head')[0],
92 | readyState;
93 | script.setAttribute('type', 'text/javascript');
94 | script.setAttribute('src', src);
95 | script.onload = script.onreadystatechange = function() {
96 | if (!(readyState = script.readyState) || /loaded|complete/.test(readyState)) {
97 | script.onload = script.onreadystatechange = null;
98 | head.removeChild(script);
99 | if (callback) {
100 | setTimeout(callback, 1);
101 | }
102 | }
103 | };
104 |
105 | head.insertBefore(script, head.firstChild);
106 | }
107 |
108 | if (files.length > 1) {
109 | file = files[0];
110 | loadBrowserScript(file, function() { Riot.loadBrowserScripts(files.slice(1), tests); });
111 | } else {
112 | file = files[0];
113 | loadBrowserScript(file, function() { Riot.runAndReport(tests); });
114 | }
115 | },
116 |
117 | load: function() {
118 | switch (Riot.detectEnvironment()) {
119 | case 'xpcomcore':
120 | case 'rhino':
121 | case 'non-browser-interpreter':
122 | load(arguments[0]);
123 | break;
124 | case 'node':
125 | // Evaluate the required code in the global context, like load() would
126 | global.eval.call(global, Riot.node.fs.readFileSync(arguments[0]).toString());
127 | break;
128 | case 'browser':
129 | var script = document.createElement('script'),
130 | head = document.getElementsByTagName('head');
131 | script.setAttribute('type', 'text/javascript');
132 | script.setAttribute('src', arguments[0]);
133 | head[0].insertBefore(script, head.firstChild);
134 | break;
135 | }
136 | },
137 |
138 | requiredFiles: [],
139 |
140 | indexOf: function(array, value) {
141 | for (var i = 0; i < array.length; i++) {
142 | if (array[i] === value) {
143 | return i;
144 | }
145 | }
146 | return -1;
147 | },
148 |
149 | require: function() {
150 | if (this.indexOf(this.requiredFiles, arguments[0]) == -1) {
151 | this.requiredFiles.push(arguments[0]);
152 | if (Riot.detectEnvironment() !== 'browser') {
153 | this.load(arguments[0]);
154 | }
155 | }
156 | },
157 |
158 | detectEnvironment: function() {
159 | if (typeof this.env !== 'undefined') {
160 | return this.env;
161 | }
162 |
163 | this.env = (function() {
164 | if (typeof XPCOMCore !== 'undefined') {
165 | Riot.puts = print;
166 | return 'xpcomcore';
167 | } else if (typeof window === 'undefined' && typeof java !== 'undefined') {
168 | Riot.puts = print;
169 | return 'rhino';
170 | } else if (typeof exports !== 'undefined') {
171 | // TODO: Node should be checked more thoroughly
172 | Riot.node = {
173 | fs: require('fs'),
174 | sys: require('sys')
175 | }
176 |
177 | Riot.puts = Riot.node.sys.puts;
178 |
179 | return 'node';
180 | } else if (typeof window === 'undefined') {
181 | Riot.puts = print;
182 | return 'non-browser-interpreter';
183 | } else {
184 | return 'browser';
185 | }
186 | })();
187 |
188 | return this.env;
189 | },
190 |
191 | runAndReport: function(tests) {
192 | this.running = true;
193 | var benchmark = Riot.Benchmark.run(1, function() { Riot.runAllContexts(tests); });
194 | Riot.formatter.separator();
195 | Riot.summariseAllResults();
196 | Riot.formatter.line(benchmark);
197 | this.running = false;
198 | },
199 |
200 | runAllContexts: function(tests) {
201 | if (typeof tests !== 'undefined') {
202 | this.withDSL(tests)();
203 | }
204 |
205 | for (var i = 0; i < this.contexts.length; i++) {
206 | this.contexts[i].run();
207 | }
208 | },
209 |
210 | functionBody: function(fn) {
211 | return '(' + fn.toString().replace(/\s+$/, '') + ')()';
212 | },
213 |
214 | withDSL: function(fn, context) {
215 | var body = this.functionBody(fn),
216 | f = new Function('context', 'given', 'asserts', 'should', 'setup', 'teardown', body),
217 | args = [
218 | Riot.context,
219 | Riot.given,
220 | function() { return context.asserts.apply(context, arguments); },
221 | function() { return context.should.apply(context, arguments); },
222 | function() { return context.setup.apply(context, arguments); },
223 | function() { return context.teardown.apply(context, arguments); }
224 | ];
225 |
226 | return function() { f.apply(Riot, args); };
227 | },
228 |
229 | context: function(title, callback) {
230 | var context = new Riot.Context(title, callback);
231 |
232 | if (this.running) {
233 | context.run();
234 | } else {
235 | Riot.contexts.push(context);
236 | }
237 |
238 | return context;
239 | },
240 |
241 | given: function(title, callback) {
242 | title = 'Given ' + title;
243 | return Riot.context(title, callback);
244 | },
245 |
246 | summariseAllResults: function() { return this.summarise(this.results); },
247 |
248 | summarise: function(results) {
249 | var failures = 0;
250 | for (var i = 0; i < results.length; i++) {
251 | if (!results[i].pass) { failures++; }
252 | }
253 | this.formatter.line(results.length + ' assertions: ' + failures + ' failures');
254 | this.exitCode = failures > 0 ? 1 : 0;
255 | },
256 |
257 | addResult: function(context, assertion, pass) {
258 | var result = {
259 | assertion: assertion,
260 | pass: pass,
261 | context: context
262 | };
263 | this.results.push(result);
264 | }
265 | };
266 |
267 | Riot.Benchmark = {
268 | results: [],
269 |
270 | addResult: function(start, end) {
271 | this.results.push(end - start);
272 | },
273 |
274 | displayResults: function() {
275 | var total = 0,
276 | seconds = 0,
277 | i = 0;
278 | for (i = 0; i < this.results.length; i++) {
279 | total += this.results[i];
280 | }
281 | seconds = total / 1000;
282 | return 'Elapsed time: ' + total + 'ms (' + seconds + ' seconds)';
283 | },
284 |
285 | run: function(times, callback) {
286 | this.results = [];
287 | for (var i = 0; i < times; i++) {
288 | var start = new Date(),
289 | end = null;
290 | callback();
291 | end = new Date();
292 | this.addResult(start, end);
293 | }
294 | return this.displayResults();
295 | }
296 | };
297 |
298 | Riot.Formatters = {
299 | HTML: function() {
300 | function display(html) {
301 | var results = document.getElementById('test-results');
302 | results.innerHTML += html;
303 | }
304 |
305 | this.line = function(text) {
306 | display('' + text + '
'); 307 | }; 308 | 309 | this.pass = function(message) { 310 | display('' + message + '
'); 311 | }; 312 | 313 | this.fail = function(message) { 314 | display('' + message + '
'); 315 | }; 316 | 317 | this.error = function(message, exception) { 318 | this.fail(message); 319 | display('Exception: ' + exception + '
'); 320 | }; 321 | 322 | this.context = function(name) { 323 | display('