Commit | Line | Data |
---|---|---|
6a1aa64f DV |
1 | /** |
2 | * SimpleTest, a partial Test.Simple/Test.More API compatible test library. | |
3 | * | |
4 | * Why? | |
5 | * | |
6 | * Test.Simple doesn't work on IE < 6. | |
7 | * TODO: | |
8 | * * Support the Test.Simple API used by MochiKit, to be able to test MochiKit | |
9 | * itself against IE 5.5 | |
10 | * | |
11 | **/ | |
12 | ||
13 | if (typeof(SimpleTest) == "undefined") { | |
14 | var SimpleTest = {}; | |
15 | } | |
16 | ||
17 | // Check to see if the TestRunner is present and has logging | |
18 | if (typeof(parent) != "undefined" && parent.TestRunner) { | |
19 | SimpleTest._logEnabled = parent.TestRunner.logEnabled; | |
20 | } | |
21 | ||
22 | SimpleTest._tests = []; | |
23 | SimpleTest._stopOnLoad = true; | |
24 | ||
25 | /** | |
26 | * Something like assert. | |
27 | **/ | |
28 | SimpleTest.ok = function (condition, name, diag) { | |
29 | var test = {'result': !!condition, 'name': name, 'diag': diag || ""}; | |
30 | if (SimpleTest._logEnabled) { | |
31 | var msg = test.result ? "PASS" : "FAIL"; | |
32 | msg += " | " + test.name; | |
33 | if (test.result) { | |
34 | parent.TestRunner.logger.log(msg); | |
35 | } else { | |
36 | msg += " | " + test.diag; | |
37 | parent.TestRunner.logger.error(msg); | |
38 | } | |
39 | } | |
40 | SimpleTest._tests.push(test); | |
41 | }; | |
42 | ||
43 | /** | |
44 | * Roughly equivalent to ok(a==b, name) | |
45 | **/ | |
46 | SimpleTest.is = function (a, b, name) { | |
47 | var repr = MochiKit.Base.repr; | |
48 | SimpleTest.ok(a == b, name, "got " + repr(a) + ", expected " + repr(b)); | |
49 | }; | |
50 | ||
51 | ||
52 | /** | |
53 | * Makes a test report, returns it as a DIV element. | |
54 | **/ | |
55 | SimpleTest.report = function () { | |
56 | var DIV = MochiKit.DOM.DIV; | |
57 | var passed = 0; | |
58 | var failed = 0; | |
59 | var results = MochiKit.Base.map( | |
60 | function (test) { | |
61 | var cls, msg; | |
62 | if (test.result) { | |
63 | passed++; | |
64 | cls = "test_ok"; | |
65 | msg = "ok - " + test.name; | |
66 | } else { | |
67 | failed++; | |
68 | cls = "test_not_ok"; | |
69 | msg = "not ok - " + test.name + " " + test.diag; | |
70 | } | |
71 | return DIV({"class": cls}, msg); | |
72 | }, | |
73 | SimpleTest._tests | |
74 | ); | |
75 | var summary_class = ((failed == 0) ? 'all_pass' : 'some_fail'); | |
76 | return DIV({'class': 'tests_report'}, | |
77 | DIV({'class': 'tests_summary ' + summary_class}, | |
78 | DIV({'class': 'tests_passed'}, "Passed: " + passed), | |
79 | DIV({'class': 'tests_failed'}, "Failed: " + failed)), | |
80 | results | |
81 | ); | |
82 | }; | |
83 | ||
84 | /** | |
85 | * Toggle element visibility | |
86 | **/ | |
87 | SimpleTest.toggle = function(el) { | |
88 | if (MochiKit.Style.getStyle(el, 'display') == 'block') { | |
89 | el.style.display = 'none'; | |
90 | } else { | |
91 | el.style.display = 'block'; | |
92 | } | |
93 | }; | |
94 | ||
95 | /** | |
96 | * Toggle visibility for divs with a specific class. | |
97 | **/ | |
98 | SimpleTest.toggleByClass = function (cls) { | |
99 | var elems = getElementsByTagAndClassName('div', cls); | |
100 | MochiKit.Base.map(SimpleTest.toggle, elems); | |
101 | }; | |
102 | ||
103 | /** | |
104 | * Shows the report in the browser | |
105 | **/ | |
106 | ||
107 | SimpleTest.showReport = function() { | |
108 | var togglePassed = A({'href': '#'}, "Toggle passed tests"); | |
109 | var toggleFailed = A({'href': '#'}, "Toggle failed tests"); | |
110 | togglePassed.onclick = partial(SimpleTest.toggleByClass, 'test_ok'); | |
111 | toggleFailed.onclick = partial(SimpleTest.toggleByClass, 'test_not_ok'); | |
112 | var body = document.getElementsByTagName("body")[0]; | |
113 | var firstChild = body.childNodes[0]; | |
114 | var addNode; | |
115 | if (firstChild) { | |
116 | addNode = function (el) { | |
117 | body.insertBefore(el, firstChild); | |
118 | }; | |
119 | } else { | |
120 | addNode = function (el) { | |
121 | body.appendChild(el) | |
122 | }; | |
123 | } | |
124 | addNode(togglePassed); | |
125 | addNode(SPAN(null, " ")); | |
126 | addNode(toggleFailed); | |
127 | addNode(SimpleTest.report()); | |
128 | }; | |
129 | ||
130 | /** | |
131 | * Tells SimpleTest to don't finish the test when the document is loaded, | |
132 | * useful for asynchronous tests. | |
133 | * | |
134 | * When SimpleTest.waitForExplicitFinish is called, | |
135 | * explicit SimpleTest.finish() is required. | |
136 | **/ | |
137 | SimpleTest.waitForExplicitFinish = function () { | |
138 | SimpleTest._stopOnLoad = false; | |
139 | }; | |
140 | ||
141 | /** | |
142 | * Talks to the TestRunner if being ran on a iframe and the parent has a | |
143 | * TestRunner object. | |
144 | **/ | |
145 | SimpleTest.talkToRunner = function () { | |
146 | if (typeof(parent) != "undefined" && parent.TestRunner) { | |
147 | parent.TestRunner.testFinished(document); | |
148 | } | |
149 | }; | |
150 | ||
151 | /** | |
152 | * Finishes the tests. This is automatically called, except when | |
153 | * SimpleTest.waitForExplicitFinish() has been invoked. | |
154 | **/ | |
155 | SimpleTest.finish = function () { | |
156 | SimpleTest.showReport(); | |
157 | SimpleTest.talkToRunner(); | |
158 | }; | |
159 | ||
160 | ||
161 | addLoadEvent(function() { | |
162 | if (SimpleTest._stopOnLoad) { | |
163 | SimpleTest.finish(); | |
164 | } | |
165 | }); | |
166 | ||
167 | // --------------- Test.Builder/Test.More isDeeply() ----------------- | |
168 | ||
169 | ||
170 | SimpleTest.DNE = {dne: 'Does not exist'}; | |
171 | SimpleTest.LF = "\r\n"; | |
172 | SimpleTest._isRef = function (object) { | |
173 | var type = typeof(object); | |
174 | return type == 'object' || type == 'function'; | |
175 | }; | |
176 | ||
177 | ||
178 | SimpleTest._deepCheck = function (e1, e2, stack, seen) { | |
179 | var ok = false; | |
180 | // Either they're both references or both not. | |
181 | var sameRef = !(!SimpleTest._isRef(e1) ^ !SimpleTest._isRef(e2)); | |
182 | if (e1 == null && e2 == null) { | |
183 | ok = true; | |
184 | } else if (e1 != null ^ e2 != null) { | |
185 | ok = false; | |
186 | } else if (e1 == SimpleTest.DNE ^ e2 == SimpleTest.DNE) { | |
187 | ok = false; | |
188 | } else if (sameRef && e1 == e2) { | |
189 | // Handles primitives and any variables that reference the same | |
190 | // object, including functions. | |
191 | ok = true; | |
192 | } else if (SimpleTest.isa(e1, 'Array') && SimpleTest.isa(e2, 'Array')) { | |
193 | ok = SimpleTest._eqArray(e1, e2, stack, seen); | |
194 | } else if (typeof e1 == "object" && typeof e2 == "object") { | |
195 | ok = SimpleTest._eqAssoc(e1, e2, stack, seen); | |
196 | } else { | |
197 | // If we get here, they're not the same (function references must | |
198 | // always simply rererence the same function). | |
199 | stack.push({ vals: [e1, e2] }); | |
200 | ok = false; | |
201 | } | |
202 | return ok; | |
203 | }; | |
204 | ||
205 | SimpleTest._eqArray = function (a1, a2, stack, seen) { | |
206 | // Return if they're the same object. | |
207 | if (a1 == a2) return true; | |
208 | ||
209 | // JavaScript objects have no unique identifiers, so we have to store | |
210 | // references to them all in an array, and then compare the references | |
211 | // directly. It's slow, but probably won't be much of an issue in | |
212 | // practice. Start by making a local copy of the array to as to avoid | |
213 | // confusing a reference seen more than once (such as [a, a]) for a | |
214 | // circular reference. | |
215 | for (var j = 0; j < seen.length; j++) { | |
216 | if (seen[j][0] == a1) { | |
217 | return seen[j][1] == a2; | |
218 | } | |
219 | } | |
220 | ||
221 | // If we get here, we haven't seen a1 before, so store it with reference | |
222 | // to a2. | |
223 | seen.push([ a1, a2 ]); | |
224 | ||
225 | var ok = true; | |
226 | // Only examines enumerable attributes. Only works for numeric arrays! | |
227 | // Associative arrays return 0. So call _eqAssoc() for them, instead. | |
228 | var max = a1.length > a2.length ? a1.length : a2.length; | |
229 | if (max == 0) return SimpleTest._eqAssoc(a1, a2, stack, seen); | |
230 | for (var i = 0; i < max; i++) { | |
231 | var e1 = i > a1.length - 1 ? SimpleTest.DNE : a1[i]; | |
232 | var e2 = i > a2.length - 1 ? SimpleTest.DNE : a2[i]; | |
233 | stack.push({ type: 'Array', idx: i, vals: [e1, e2] }); | |
234 | if (ok = SimpleTest._deepCheck(e1, e2, stack, seen)) { | |
235 | stack.pop(); | |
236 | } else { | |
237 | break; | |
238 | } | |
239 | } | |
240 | return ok; | |
241 | }; | |
242 | ||
243 | SimpleTest._eqAssoc = function (o1, o2, stack, seen) { | |
244 | // Return if they're the same object. | |
245 | if (o1 == o2) return true; | |
246 | ||
247 | // JavaScript objects have no unique identifiers, so we have to store | |
248 | // references to them all in an array, and then compare the references | |
249 | // directly. It's slow, but probably won't be much of an issue in | |
250 | // practice. Start by making a local copy of the array to as to avoid | |
251 | // confusing a reference seen more than once (such as [a, a]) for a | |
252 | // circular reference. | |
253 | seen = seen.slice(0); | |
254 | for (var j = 0; j < seen.length; j++) { | |
255 | if (seen[j][0] == o1) { | |
256 | return seen[j][1] == o2; | |
257 | } | |
258 | } | |
259 | ||
260 | // If we get here, we haven't seen o1 before, so store it with reference | |
261 | // to o2. | |
262 | seen.push([ o1, o2 ]); | |
263 | ||
264 | // They should be of the same class. | |
265 | ||
266 | var ok = true; | |
267 | // Only examines enumerable attributes. | |
268 | var o1Size = 0; for (var i in o1) o1Size++; | |
269 | var o2Size = 0; for (var i in o2) o2Size++; | |
270 | var bigger = o1Size > o2Size ? o1 : o2; | |
271 | for (var i in bigger) { | |
272 | var e1 = o1[i] == undefined ? SimpleTest.DNE : o1[i]; | |
273 | var e2 = o2[i] == undefined ? SimpleTest.DNE : o2[i]; | |
274 | stack.push({ type: 'Object', idx: i, vals: [e1, e2] }); | |
275 | if (ok = SimpleTest._deepCheck(e1, e2, stack, seen)) { | |
276 | stack.pop(); | |
277 | } else { | |
278 | break; | |
279 | } | |
280 | } | |
281 | return ok; | |
282 | }; | |
283 | ||
284 | SimpleTest._formatStack = function (stack) { | |
285 | var variable = '$Foo'; | |
286 | for (var i = 0; i < stack.length; i++) { | |
287 | var entry = stack[i]; | |
288 | var type = entry['type']; | |
289 | var idx = entry['idx']; | |
290 | if (idx != null) { | |
291 | if (/^\d+$/.test(idx)) { | |
292 | // Numeric array index. | |
293 | variable += '[' + idx + ']'; | |
294 | } else { | |
295 | // Associative array index. | |
296 | idx = idx.replace("'", "\\'"); | |
297 | variable += "['" + idx + "']"; | |
298 | } | |
299 | } | |
300 | } | |
301 | ||
302 | var vals = stack[stack.length-1]['vals'].slice(0, 2); | |
303 | var vars = [ | |
304 | variable.replace('$Foo', 'got'), | |
305 | variable.replace('$Foo', 'expected') | |
306 | ]; | |
307 | ||
308 | var out = "Structures begin differing at:" + SimpleTest.LF; | |
309 | for (var i = 0; i < vals.length; i++) { | |
310 | var val = vals[i]; | |
311 | if (val == null) { | |
312 | val = 'undefined'; | |
313 | } else { | |
314 | val == SimpleTest.DNE ? "Does not exist" : "'" + val + "'"; | |
315 | } | |
316 | } | |
317 | ||
318 | out += vars[0] + ' = ' + vals[0] + SimpleTest.LF; | |
319 | out += vars[1] + ' = ' + vals[1] + SimpleTest.LF; | |
320 | ||
321 | return ' ' + out; | |
322 | }; | |
323 | ||
324 | ||
325 | SimpleTest.isDeeply = function (it, as, name) { | |
326 | var ok; | |
327 | // ^ is the XOR operator. | |
328 | if (SimpleTest._isRef(it) ^ SimpleTest._isRef(as)) { | |
329 | // One's a reference, one isn't. | |
330 | ok = false; | |
331 | } else if (!SimpleTest._isRef(it) && !SimpleTest._isRef(as)) { | |
332 | // Neither is an object. | |
333 | ok = SimpleTest.is(it, as, name); | |
334 | } else { | |
335 | // We have two objects. Do a deep comparison. | |
336 | var stack = [], seen = []; | |
337 | if ( SimpleTest._deepCheck(it, as, stack, seen)) { | |
338 | ok = SimpleTest.ok(true, name); | |
339 | } else { | |
340 | ok = SimpleTest.ok(false, name, SimpleTest._formatStack(stack)); | |
341 | } | |
342 | } | |
343 | return ok; | |
344 | }; | |
345 | ||
346 | SimpleTest.typeOf = function (object) { | |
347 | var c = Object.prototype.toString.apply(object); | |
348 | var name = c.substring(8, c.length - 1); | |
349 | if (name != 'Object') return name; | |
350 | // It may be a non-core class. Try to extract the class name from | |
351 | // the constructor function. This may not work in all implementations. | |
352 | if (/function ([^(\s]+)/.test(Function.toString.call(object.constructor))) { | |
353 | return RegExp.$1; | |
354 | } | |
355 | // No idea. :-( | |
356 | return name; | |
357 | }; | |
358 | ||
359 | SimpleTest.isa = function (object, clas) { | |
360 | return SimpleTest.typeOf(object) == clas; | |
361 | }; | |
362 | ||
363 | // Global symbols: | |
364 | var ok = SimpleTest.ok; | |
365 | var is = SimpleTest.is; | |
366 | var isDeeply = SimpleTest.isDeeply; |