Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | /* |
michael@0 | 2 | Distributed under both the W3C Test Suite License [1] and the W3C |
michael@0 | 3 | 3-clause BSD License [2]. To contribute to a W3C Test Suite, see the |
michael@0 | 4 | policies and contribution forms [3]. |
michael@0 | 5 | |
michael@0 | 6 | [1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license |
michael@0 | 7 | [2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license |
michael@0 | 8 | [3] http://www.w3.org/2004/10/27-testcases |
michael@0 | 9 | */ |
michael@0 | 10 | |
michael@0 | 11 | /* |
michael@0 | 12 | * == Introduction == |
michael@0 | 13 | * |
michael@0 | 14 | * This file provides a framework for writing testcases. It is intended to |
michael@0 | 15 | * provide a convenient API for making common assertions, and to work both |
michael@0 | 16 | * for testing synchronous and asynchronous DOM features in a way that |
michael@0 | 17 | * promotes clear, robust, tests. |
michael@0 | 18 | * |
michael@0 | 19 | * == Basic Usage == |
michael@0 | 20 | * |
michael@0 | 21 | * To use this file, import the script and the testharnessreport script into |
michael@0 | 22 | * the test document: |
michael@0 | 23 | * <script src="/resources/testharness.js"></script> |
michael@0 | 24 | * <script src="/resources/testharnessreport.js"></script> |
michael@0 | 25 | * |
michael@0 | 26 | * Within each file one may define one or more tests. Each test is atomic |
michael@0 | 27 | * in the sense that a single test has a single result (pass/fail/timeout). |
michael@0 | 28 | * Within each test one may have a number of asserts. The test fails at the |
michael@0 | 29 | * first failing assert, and the remainder of the test is (typically) not run. |
michael@0 | 30 | * |
michael@0 | 31 | * If the file containing the tests is a HTML file with an element of id "log" |
michael@0 | 32 | * this will be populated with a table containing the test results after all |
michael@0 | 33 | * the tests have run. |
michael@0 | 34 | * |
michael@0 | 35 | * NOTE: By default tests must be created before the load event fires. For ways |
michael@0 | 36 | * to create tests after the load event, see "Determining when all tests |
michael@0 | 37 | * are complete", below |
michael@0 | 38 | * |
michael@0 | 39 | * == Synchronous Tests == |
michael@0 | 40 | * |
michael@0 | 41 | * To create a synchronous test use the test() function: |
michael@0 | 42 | * |
michael@0 | 43 | * test(test_function, name, properties) |
michael@0 | 44 | * |
michael@0 | 45 | * test_function is a function that contains the code to test. For example a |
michael@0 | 46 | * trivial passing test would be: |
michael@0 | 47 | * |
michael@0 | 48 | * test(function() {assert_true(true)}, "assert_true with true") |
michael@0 | 49 | * |
michael@0 | 50 | * The function passed in is run in the test() call. |
michael@0 | 51 | * |
michael@0 | 52 | * properties is an object that overrides default test properties. The |
michael@0 | 53 | * recognised properties are: |
michael@0 | 54 | * timeout - the test timeout in ms |
michael@0 | 55 | * |
michael@0 | 56 | * e.g. |
michael@0 | 57 | * test(test_function, "Sample test", {timeout:1000}) |
michael@0 | 58 | * |
michael@0 | 59 | * would run test_function with a timeout of 1s. |
michael@0 | 60 | * |
michael@0 | 61 | * Additionally, test-specific metadata can be passed in the properties. These |
michael@0 | 62 | * are used when the individual test has different metadata from that stored |
michael@0 | 63 | * in the <head>. |
michael@0 | 64 | * The recognized metadata properties are: |
michael@0 | 65 | * |
michael@0 | 66 | * help - The url of the part of the specification being tested |
michael@0 | 67 | * |
michael@0 | 68 | * assert - A human readable description of what the test is attempting |
michael@0 | 69 | * to prove |
michael@0 | 70 | * |
michael@0 | 71 | * author - Name and contact information for the author of the test in the |
michael@0 | 72 | * format: "Name <email_addr>" or "Name http://contact/url" |
michael@0 | 73 | * |
michael@0 | 74 | * == Asynchronous Tests == |
michael@0 | 75 | * |
michael@0 | 76 | * Testing asynchronous features is somewhat more complex since the result of |
michael@0 | 77 | * a test may depend on one or more events or other callbacks. The API provided |
michael@0 | 78 | * for testing these features is indended to be rather low-level but hopefully |
michael@0 | 79 | * applicable to many situations. |
michael@0 | 80 | * |
michael@0 | 81 | * To create a test, one starts by getting a Test object using async_test: |
michael@0 | 82 | * |
michael@0 | 83 | * async_test(name, properties) |
michael@0 | 84 | * |
michael@0 | 85 | * e.g. |
michael@0 | 86 | * var t = async_test("Simple async test") |
michael@0 | 87 | * |
michael@0 | 88 | * Assertions can be added to the test by calling the step method of the test |
michael@0 | 89 | * object with a function containing the test assertions: |
michael@0 | 90 | * |
michael@0 | 91 | * t.step(function() {assert_true(true)}); |
michael@0 | 92 | * |
michael@0 | 93 | * When all the steps are complete, the done() method must be called: |
michael@0 | 94 | * |
michael@0 | 95 | * t.done(); |
michael@0 | 96 | * |
michael@0 | 97 | * As a convenience, async_test can also takes a function as first argument. |
michael@0 | 98 | * This function is called with the test object as both its `this` object and |
michael@0 | 99 | * first argument. The above example can be rewritten as: |
michael@0 | 100 | * |
michael@0 | 101 | * async_test(function(t) { |
michael@0 | 102 | * object.some_event = function() { |
michael@0 | 103 | * t.step(function (){assert_true(true); t.done();}); |
michael@0 | 104 | * }; |
michael@0 | 105 | * }, "Simple async test"); |
michael@0 | 106 | * |
michael@0 | 107 | * which avoids cluttering the global scope with references to async |
michael@0 | 108 | * tests instances. |
michael@0 | 109 | * |
michael@0 | 110 | * The properties argument is identical to that for test(). |
michael@0 | 111 | * |
michael@0 | 112 | * In many cases it is convenient to run a step in response to an event or a |
michael@0 | 113 | * callback. A convenient method of doing this is through the step_func method |
michael@0 | 114 | * which returns a function that, when called runs a test step. For example |
michael@0 | 115 | * |
michael@0 | 116 | * object.some_event = t.step_func(function(e) {assert_true(e.a)}); |
michael@0 | 117 | * |
michael@0 | 118 | * == Making assertions == |
michael@0 | 119 | * |
michael@0 | 120 | * Functions for making assertions start assert_ |
michael@0 | 121 | * The best way to get a list is to look in this file for functions names |
michael@0 | 122 | * matching that pattern. The general signature is |
michael@0 | 123 | * |
michael@0 | 124 | * assert_something(actual, expected, description) |
michael@0 | 125 | * |
michael@0 | 126 | * although not all assertions precisely match this pattern e.g. assert_true |
michael@0 | 127 | * only takes actual and description as arguments. |
michael@0 | 128 | * |
michael@0 | 129 | * The description parameter is used to present more useful error messages when |
michael@0 | 130 | * a test fails |
michael@0 | 131 | * |
michael@0 | 132 | * NOTE: All asserts must be located in a test() or a step of an async_test(). |
michael@0 | 133 | * asserts outside these places won't be detected correctly by the harness |
michael@0 | 134 | * and may cause a file to stop testing. |
michael@0 | 135 | * |
michael@0 | 136 | * == Harness Timeout == |
michael@0 | 137 | * |
michael@0 | 138 | * The overall harness admits two timeout values "normal" (the |
michael@0 | 139 | * default) and "long", used for tests which have an unusually long |
michael@0 | 140 | * runtime. After the timeout is reached, the harness will stop |
michael@0 | 141 | * waiting for further async tests to complete. By default the |
michael@0 | 142 | * timeouts are set to 10s and 60s, respectively, but may be changed |
michael@0 | 143 | * when the test is run on hardware with different performance |
michael@0 | 144 | * characteristics to a common desktop computer. In order to opt-in |
michael@0 | 145 | * to the longer test timeout, the test must specify a meta element: |
michael@0 | 146 | * <meta name="timeout" content="long"> |
michael@0 | 147 | * |
michael@0 | 148 | * == Setup == |
michael@0 | 149 | * |
michael@0 | 150 | * Sometimes tests require non-trivial setup that may fail. For this purpose |
michael@0 | 151 | * there is a setup() function, that may be called with one or two arguments. |
michael@0 | 152 | * The two argument version is: |
michael@0 | 153 | * |
michael@0 | 154 | * setup(func, properties) |
michael@0 | 155 | * |
michael@0 | 156 | * The one argument versions may omit either argument. |
michael@0 | 157 | * func is a function to be run synchronously. setup() becomes a no-op once |
michael@0 | 158 | * any tests have returned results. Properties are global properties of the test |
michael@0 | 159 | * harness. Currently recognised properties are: |
michael@0 | 160 | * |
michael@0 | 161 | * |
michael@0 | 162 | * explicit_done - Wait for an explicit call to done() before declaring all |
michael@0 | 163 | * tests complete (see below) |
michael@0 | 164 | * |
michael@0 | 165 | * output_document - The document to which results should be logged. By default |
michael@0 | 166 | * this is the current document but could be an ancestor |
michael@0 | 167 | * document in some cases e.g. a SVG test loaded in an HTML |
michael@0 | 168 | * wrapper |
michael@0 | 169 | * |
michael@0 | 170 | * explicit_timeout - disable file timeout; only stop waiting for results |
michael@0 | 171 | * when the timeout() function is called (typically for |
michael@0 | 172 | * use when integrating with some existing test framework |
michael@0 | 173 | * that has its own timeout mechanism). |
michael@0 | 174 | * |
michael@0 | 175 | * allow_uncaught_exception - don't treat an uncaught exception as an error; |
michael@0 | 176 | * needed when e.g. testing the window.onerror |
michael@0 | 177 | * handler. |
michael@0 | 178 | * |
michael@0 | 179 | * timeout_multiplier - Multiplier to apply to per-test timeouts. |
michael@0 | 180 | * |
michael@0 | 181 | * == Determining when all tests are complete == |
michael@0 | 182 | * |
michael@0 | 183 | * By default the test harness will assume there are no more results to come |
michael@0 | 184 | * when: |
michael@0 | 185 | * 1) There are no Test objects that have been created but not completed |
michael@0 | 186 | * 2) The load event on the document has fired |
michael@0 | 187 | * |
michael@0 | 188 | * This behaviour can be overridden by setting the explicit_done property to |
michael@0 | 189 | * true in a call to setup(). If explicit_done is true, the test harness will |
michael@0 | 190 | * not assume it is done until the global done() function is called. Once done() |
michael@0 | 191 | * is called, the two conditions above apply like normal. |
michael@0 | 192 | * |
michael@0 | 193 | * == Generating tests == |
michael@0 | 194 | * |
michael@0 | 195 | * NOTE: this functionality may be removed |
michael@0 | 196 | * |
michael@0 | 197 | * There are scenarios in which is is desirable to create a large number of |
michael@0 | 198 | * (synchronous) tests that are internally similar but vary in the parameters |
michael@0 | 199 | * used. To make this easier, the generate_tests function allows a single |
michael@0 | 200 | * function to be called with each set of parameters in a list: |
michael@0 | 201 | * |
michael@0 | 202 | * generate_tests(test_function, parameter_lists, properties) |
michael@0 | 203 | * |
michael@0 | 204 | * For example: |
michael@0 | 205 | * |
michael@0 | 206 | * generate_tests(assert_equals, [ |
michael@0 | 207 | * ["Sum one and one", 1+1, 2], |
michael@0 | 208 | * ["Sum one and zero", 1+0, 1] |
michael@0 | 209 | * ]) |
michael@0 | 210 | * |
michael@0 | 211 | * Is equivalent to: |
michael@0 | 212 | * |
michael@0 | 213 | * test(function() {assert_equals(1+1, 2)}, "Sum one and one") |
michael@0 | 214 | * test(function() {assert_equals(1+0, 1)}, "Sum one and zero") |
michael@0 | 215 | * |
michael@0 | 216 | * Note that the first item in each parameter list corresponds to the name of |
michael@0 | 217 | * the test. |
michael@0 | 218 | * |
michael@0 | 219 | * The properties argument is identical to that for test(). This may be a |
michael@0 | 220 | * single object (used for all generated tests) or an array. |
michael@0 | 221 | * |
michael@0 | 222 | * == Callback API == |
michael@0 | 223 | * |
michael@0 | 224 | * The framework provides callbacks corresponding to 3 events: |
michael@0 | 225 | * |
michael@0 | 226 | * start - happens when the first Test is created |
michael@0 | 227 | * result - happens when a test result is recieved |
michael@0 | 228 | * complete - happens when all results are recieved |
michael@0 | 229 | * |
michael@0 | 230 | * The page defining the tests may add callbacks for these events by calling |
michael@0 | 231 | * the following methods: |
michael@0 | 232 | * |
michael@0 | 233 | * add_start_callback(callback) - callback called with no arguments |
michael@0 | 234 | * add_result_callback(callback) - callback called with a test argument |
michael@0 | 235 | * add_completion_callback(callback) - callback called with an array of tests |
michael@0 | 236 | * and an status object |
michael@0 | 237 | * |
michael@0 | 238 | * tests have the following properties: |
michael@0 | 239 | * status: A status code. This can be compared to the PASS, FAIL, TIMEOUT and |
michael@0 | 240 | * NOTRUN properties on the test object |
michael@0 | 241 | * message: A message indicating the reason for failure. In the future this |
michael@0 | 242 | * will always be a string |
michael@0 | 243 | * |
michael@0 | 244 | * The status object gives the overall status of the harness. It has the |
michael@0 | 245 | * following properties: |
michael@0 | 246 | * status: Can be compared to the OK, ERROR and TIMEOUT properties |
michael@0 | 247 | * message: An error message set when the status is ERROR |
michael@0 | 248 | * |
michael@0 | 249 | * == External API == |
michael@0 | 250 | * |
michael@0 | 251 | * In order to collect the results of multiple pages containing tests, the test |
michael@0 | 252 | * harness will, when loaded in a nested browsing context, attempt to call |
michael@0 | 253 | * certain functions in each ancestor and opener browsing context: |
michael@0 | 254 | * |
michael@0 | 255 | * start - start_callback |
michael@0 | 256 | * result - result_callback |
michael@0 | 257 | * complete - completion_callback |
michael@0 | 258 | * |
michael@0 | 259 | * These are given the same arguments as the corresponding internal callbacks |
michael@0 | 260 | * described above. |
michael@0 | 261 | * |
michael@0 | 262 | * == External API through cross-document messaging == |
michael@0 | 263 | * |
michael@0 | 264 | * Where supported, the test harness will also send messages using |
michael@0 | 265 | * cross-document messaging to each ancestor and opener browsing context. Since |
michael@0 | 266 | * it uses the wildcard keyword (*), cross-origin communication is enabled and |
michael@0 | 267 | * script on different origins can collect the results. |
michael@0 | 268 | * |
michael@0 | 269 | * This API follows similar conventions as those described above only slightly |
michael@0 | 270 | * modified to accommodate message event API. Each message is sent by the harness |
michael@0 | 271 | * is passed a single vanilla object, available as the `data` property of the |
michael@0 | 272 | * event object. These objects are structures as follows: |
michael@0 | 273 | * |
michael@0 | 274 | * start - { type: "start" } |
michael@0 | 275 | * result - { type: "result", test: Test } |
michael@0 | 276 | * complete - { type: "complete", tests: [Test, ...], status: TestsStatus } |
michael@0 | 277 | * |
michael@0 | 278 | * == List of assertions == |
michael@0 | 279 | * |
michael@0 | 280 | * assert_true(actual, description) |
michael@0 | 281 | * asserts that /actual/ is strictly true |
michael@0 | 282 | * |
michael@0 | 283 | * assert_false(actual, description) |
michael@0 | 284 | * asserts that /actual/ is strictly false |
michael@0 | 285 | * |
michael@0 | 286 | * assert_equals(actual, expected, description) |
michael@0 | 287 | * asserts that /actual/ is the same value as /expected/ |
michael@0 | 288 | * |
michael@0 | 289 | * assert_not_equals(actual, expected, description) |
michael@0 | 290 | * asserts that /actual/ is a different value to /expected/. Yes, this means |
michael@0 | 291 | * that "expected" is a misnomer |
michael@0 | 292 | * |
michael@0 | 293 | * assert_in_array(actual, expected, description) |
michael@0 | 294 | * asserts that /expected/ is an Array, and /actual/ is equal to one of the |
michael@0 | 295 | * members -- expected.indexOf(actual) != -1 |
michael@0 | 296 | * |
michael@0 | 297 | * assert_array_equals(actual, expected, description) |
michael@0 | 298 | * asserts that /actual/ and /expected/ have the same length and the value of |
michael@0 | 299 | * each indexed property in /actual/ is the strictly equal to the corresponding |
michael@0 | 300 | * property value in /expected/ |
michael@0 | 301 | * |
michael@0 | 302 | * assert_approx_equals(actual, expected, epsilon, description) |
michael@0 | 303 | * asserts that /actual/ is a number within +/- /epsilon/ of /expected/ |
michael@0 | 304 | * |
michael@0 | 305 | * assert_less_than(actual, expected, description) |
michael@0 | 306 | * asserts that /actual/ is a number less than /expected/ |
michael@0 | 307 | * |
michael@0 | 308 | * assert_greater_than(actual, expected, description) |
michael@0 | 309 | * asserts that /actual/ is a number greater than /expected/ |
michael@0 | 310 | * |
michael@0 | 311 | * assert_less_than_equal(actual, expected, description) |
michael@0 | 312 | * asserts that /actual/ is a number less than or equal to /expected/ |
michael@0 | 313 | * |
michael@0 | 314 | * assert_greater_than_equal(actual, expected, description) |
michael@0 | 315 | * asserts that /actual/ is a number greater than or equal to /expected/ |
michael@0 | 316 | * |
michael@0 | 317 | * assert_regexp_match(actual, expected, description) |
michael@0 | 318 | * asserts that /actual/ matches the regexp /expected/ |
michael@0 | 319 | * |
michael@0 | 320 | * assert_class_string(object, class_name, description) |
michael@0 | 321 | * asserts that the class string of /object/ as returned in |
michael@0 | 322 | * Object.prototype.toString is equal to /class_name/. |
michael@0 | 323 | * |
michael@0 | 324 | * assert_own_property(object, property_name, description) |
michael@0 | 325 | * assert that object has own property property_name |
michael@0 | 326 | * |
michael@0 | 327 | * assert_inherits(object, property_name, description) |
michael@0 | 328 | * assert that object does not have an own property named property_name |
michael@0 | 329 | * but that property_name is present in the prototype chain for object |
michael@0 | 330 | * |
michael@0 | 331 | * assert_idl_attribute(object, attribute_name, description) |
michael@0 | 332 | * assert that an object that is an instance of some interface has the |
michael@0 | 333 | * attribute attribute_name following the conditions specified by WebIDL |
michael@0 | 334 | * |
michael@0 | 335 | * assert_readonly(object, property_name, description) |
michael@0 | 336 | * assert that property property_name on object is readonly |
michael@0 | 337 | * |
michael@0 | 338 | * assert_throws(code, func, description) |
michael@0 | 339 | * code - the expected exception: |
michael@0 | 340 | * o string: the thrown exception must be a DOMException with the given |
michael@0 | 341 | * name, e.g., "TimeoutError" (for compatibility with existing |
michael@0 | 342 | * tests, a constant is also supported, e.g., "TIMEOUT_ERR") |
michael@0 | 343 | * o object: the thrown exception must have a property called "name" that |
michael@0 | 344 | * matches code.name |
michael@0 | 345 | * o null: allow any exception (in general, one of the options above |
michael@0 | 346 | * should be used) |
michael@0 | 347 | * func - a function that should throw |
michael@0 | 348 | * |
michael@0 | 349 | * assert_unreached(description) |
michael@0 | 350 | * asserts if called. Used to ensure that some codepath is *not* taken e.g. |
michael@0 | 351 | * an event does not fire. |
michael@0 | 352 | * |
michael@0 | 353 | * assert_any(assert_func, actual, expected_array, extra_arg_1, ... extra_arg_N) |
michael@0 | 354 | * asserts that one assert_func(actual, expected_array_N, extra_arg1, ..., extra_arg_N) |
michael@0 | 355 | * is true for some expected_array_N in expected_array. This only works for assert_func |
michael@0 | 356 | * with signature assert_func(actual, expected, args_1, ..., args_N). Note that tests |
michael@0 | 357 | * with multiple allowed pass conditions are bad practice unless the spec specifically |
michael@0 | 358 | * allows multiple behaviours. Test authors should not use this method simply to hide |
michael@0 | 359 | * UA bugs. |
michael@0 | 360 | * |
michael@0 | 361 | * assert_exists(object, property_name, description) |
michael@0 | 362 | * *** deprecated *** |
michael@0 | 363 | * asserts that object has an own property property_name |
michael@0 | 364 | * |
michael@0 | 365 | * assert_not_exists(object, property_name, description) |
michael@0 | 366 | * *** deprecated *** |
michael@0 | 367 | * assert that object does not have own property property_name |
michael@0 | 368 | */ |
michael@0 | 369 | |
michael@0 | 370 | (function () |
michael@0 | 371 | { |
michael@0 | 372 | var debug = false; |
michael@0 | 373 | // default timeout is 10 seconds, test can override if needed |
michael@0 | 374 | var settings = { |
michael@0 | 375 | output:true, |
michael@0 | 376 | harness_timeout:{"normal":10000, |
michael@0 | 377 | "long":60000}, |
michael@0 | 378 | test_timeout:null |
michael@0 | 379 | }; |
michael@0 | 380 | |
michael@0 | 381 | var xhtml_ns = "http://www.w3.org/1999/xhtml"; |
michael@0 | 382 | |
michael@0 | 383 | // script_prefix is used by Output.prototype.show_results() to figure out |
michael@0 | 384 | // where to get testharness.css from. It's enclosed in an extra closure to |
michael@0 | 385 | // not pollute the library's namespace with variables like "src". |
michael@0 | 386 | var script_prefix = null; |
michael@0 | 387 | (function () |
michael@0 | 388 | { |
michael@0 | 389 | var scripts = document.getElementsByTagName("script"); |
michael@0 | 390 | for (var i = 0; i < scripts.length; i++) |
michael@0 | 391 | { |
michael@0 | 392 | if (scripts[i].src) |
michael@0 | 393 | { |
michael@0 | 394 | var src = scripts[i].src; |
michael@0 | 395 | } |
michael@0 | 396 | else if (scripts[i].href) |
michael@0 | 397 | { |
michael@0 | 398 | //SVG case |
michael@0 | 399 | var src = scripts[i].href.baseVal; |
michael@0 | 400 | } |
michael@0 | 401 | if (src && src.slice(src.length - "testharness.js".length) === "testharness.js") |
michael@0 | 402 | { |
michael@0 | 403 | script_prefix = src.slice(0, src.length - "testharness.js".length); |
michael@0 | 404 | break; |
michael@0 | 405 | } |
michael@0 | 406 | } |
michael@0 | 407 | })(); |
michael@0 | 408 | |
michael@0 | 409 | /* |
michael@0 | 410 | * API functions |
michael@0 | 411 | */ |
michael@0 | 412 | |
michael@0 | 413 | var name_counter = 0; |
michael@0 | 414 | function next_default_name() |
michael@0 | 415 | { |
michael@0 | 416 | //Don't use document.title to work around an Opera bug in XHTML documents |
michael@0 | 417 | var title = document.getElementsByTagName("title")[0]; |
michael@0 | 418 | var prefix = (title && title.firstChild && title.firstChild.data) || "Untitled"; |
michael@0 | 419 | var suffix = name_counter > 0 ? " " + name_counter : ""; |
michael@0 | 420 | name_counter++; |
michael@0 | 421 | return prefix + suffix; |
michael@0 | 422 | } |
michael@0 | 423 | |
michael@0 | 424 | function test(func, name, properties) |
michael@0 | 425 | { |
michael@0 | 426 | var test_name = name ? name : next_default_name(); |
michael@0 | 427 | properties = properties ? properties : {}; |
michael@0 | 428 | var test_obj = new Test(test_name, properties); |
michael@0 | 429 | test_obj.step(func); |
michael@0 | 430 | if (test_obj.phase === test_obj.phases.STARTED) { |
michael@0 | 431 | test_obj.done(); |
michael@0 | 432 | } |
michael@0 | 433 | } |
michael@0 | 434 | |
michael@0 | 435 | function async_test(func, name, properties) |
michael@0 | 436 | { |
michael@0 | 437 | if (typeof func !== "function") { |
michael@0 | 438 | properties = name; |
michael@0 | 439 | name = func; |
michael@0 | 440 | func = null; |
michael@0 | 441 | } |
michael@0 | 442 | var test_name = name ? name : next_default_name(); |
michael@0 | 443 | properties = properties ? properties : {}; |
michael@0 | 444 | var test_obj = new Test(test_name, properties); |
michael@0 | 445 | if (func) { |
michael@0 | 446 | test_obj.step(func, test_obj, test_obj); |
michael@0 | 447 | } |
michael@0 | 448 | return test_obj; |
michael@0 | 449 | } |
michael@0 | 450 | |
michael@0 | 451 | function setup(func_or_properties, maybe_properties) |
michael@0 | 452 | { |
michael@0 | 453 | var func = null; |
michael@0 | 454 | var properties = {}; |
michael@0 | 455 | if (arguments.length === 2) { |
michael@0 | 456 | func = func_or_properties; |
michael@0 | 457 | properties = maybe_properties; |
michael@0 | 458 | } else if (func_or_properties instanceof Function){ |
michael@0 | 459 | func = func_or_properties; |
michael@0 | 460 | } else { |
michael@0 | 461 | properties = func_or_properties; |
michael@0 | 462 | } |
michael@0 | 463 | tests.setup(func, properties); |
michael@0 | 464 | output.setup(properties); |
michael@0 | 465 | } |
michael@0 | 466 | |
michael@0 | 467 | function done() { |
michael@0 | 468 | tests.end_wait(); |
michael@0 | 469 | } |
michael@0 | 470 | |
michael@0 | 471 | function generate_tests(func, args, properties) { |
michael@0 | 472 | forEach(args, function(x, i) |
michael@0 | 473 | { |
michael@0 | 474 | var name = x[0]; |
michael@0 | 475 | test(function() |
michael@0 | 476 | { |
michael@0 | 477 | func.apply(this, x.slice(1)); |
michael@0 | 478 | }, |
michael@0 | 479 | name, |
michael@0 | 480 | Array.isArray(properties) ? properties[i] : properties); |
michael@0 | 481 | }); |
michael@0 | 482 | } |
michael@0 | 483 | |
michael@0 | 484 | function on_event(object, event, callback) |
michael@0 | 485 | { |
michael@0 | 486 | object.addEventListener(event, callback, false); |
michael@0 | 487 | } |
michael@0 | 488 | |
michael@0 | 489 | expose(test, 'test'); |
michael@0 | 490 | expose(async_test, 'async_test'); |
michael@0 | 491 | expose(generate_tests, 'generate_tests'); |
michael@0 | 492 | expose(setup, 'setup'); |
michael@0 | 493 | expose(done, 'done'); |
michael@0 | 494 | expose(on_event, 'on_event'); |
michael@0 | 495 | |
michael@0 | 496 | /* |
michael@0 | 497 | * Return a string truncated to the given length, with ... added at the end |
michael@0 | 498 | * if it was longer. |
michael@0 | 499 | */ |
michael@0 | 500 | function truncate(s, len) |
michael@0 | 501 | { |
michael@0 | 502 | if (s.length > len) { |
michael@0 | 503 | return s.substring(0, len - 3) + "..."; |
michael@0 | 504 | } |
michael@0 | 505 | return s; |
michael@0 | 506 | } |
michael@0 | 507 | |
michael@0 | 508 | /* |
michael@0 | 509 | * Return true if object is probably a Node object. |
michael@0 | 510 | */ |
michael@0 | 511 | function is_node(object) |
michael@0 | 512 | { |
michael@0 | 513 | // I use duck-typing instead of instanceof, because |
michael@0 | 514 | // instanceof doesn't work if the node is from another window (like an |
michael@0 | 515 | // iframe's contentWindow): |
michael@0 | 516 | // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295 |
michael@0 | 517 | if ("nodeType" in object |
michael@0 | 518 | && "nodeName" in object |
michael@0 | 519 | && "nodeValue" in object |
michael@0 | 520 | && "childNodes" in object) |
michael@0 | 521 | { |
michael@0 | 522 | try |
michael@0 | 523 | { |
michael@0 | 524 | object.nodeType; |
michael@0 | 525 | } |
michael@0 | 526 | catch (e) |
michael@0 | 527 | { |
michael@0 | 528 | // The object is probably Node.prototype or another prototype |
michael@0 | 529 | // object that inherits from it, and not a Node instance. |
michael@0 | 530 | return false; |
michael@0 | 531 | } |
michael@0 | 532 | return true; |
michael@0 | 533 | } |
michael@0 | 534 | return false; |
michael@0 | 535 | } |
michael@0 | 536 | |
michael@0 | 537 | /* |
michael@0 | 538 | * Convert a value to a nice, human-readable string |
michael@0 | 539 | */ |
michael@0 | 540 | function format_value(val, seen) |
michael@0 | 541 | { |
michael@0 | 542 | if (!seen) { |
michael@0 | 543 | seen = []; |
michael@0 | 544 | } |
michael@0 | 545 | if (typeof val === "object" && val !== null) |
michael@0 | 546 | { |
michael@0 | 547 | if (seen.indexOf(val) >= 0) |
michael@0 | 548 | { |
michael@0 | 549 | return "[...]"; |
michael@0 | 550 | } |
michael@0 | 551 | seen.push(val); |
michael@0 | 552 | } |
michael@0 | 553 | if (Array.isArray(val)) |
michael@0 | 554 | { |
michael@0 | 555 | return "[" + val.map(function(x) {return format_value(x, seen)}).join(", ") + "]"; |
michael@0 | 556 | } |
michael@0 | 557 | |
michael@0 | 558 | switch (typeof val) |
michael@0 | 559 | { |
michael@0 | 560 | case "string": |
michael@0 | 561 | val = val.replace("\\", "\\\\"); |
michael@0 | 562 | for (var i = 0; i < 32; i++) |
michael@0 | 563 | { |
michael@0 | 564 | var replace = "\\"; |
michael@0 | 565 | switch (i) { |
michael@0 | 566 | case 0: replace += "0"; break; |
michael@0 | 567 | case 1: replace += "x01"; break; |
michael@0 | 568 | case 2: replace += "x02"; break; |
michael@0 | 569 | case 3: replace += "x03"; break; |
michael@0 | 570 | case 4: replace += "x04"; break; |
michael@0 | 571 | case 5: replace += "x05"; break; |
michael@0 | 572 | case 6: replace += "x06"; break; |
michael@0 | 573 | case 7: replace += "x07"; break; |
michael@0 | 574 | case 8: replace += "b"; break; |
michael@0 | 575 | case 9: replace += "t"; break; |
michael@0 | 576 | case 10: replace += "n"; break; |
michael@0 | 577 | case 11: replace += "v"; break; |
michael@0 | 578 | case 12: replace += "f"; break; |
michael@0 | 579 | case 13: replace += "r"; break; |
michael@0 | 580 | case 14: replace += "x0e"; break; |
michael@0 | 581 | case 15: replace += "x0f"; break; |
michael@0 | 582 | case 16: replace += "x10"; break; |
michael@0 | 583 | case 17: replace += "x11"; break; |
michael@0 | 584 | case 18: replace += "x12"; break; |
michael@0 | 585 | case 19: replace += "x13"; break; |
michael@0 | 586 | case 20: replace += "x14"; break; |
michael@0 | 587 | case 21: replace += "x15"; break; |
michael@0 | 588 | case 22: replace += "x16"; break; |
michael@0 | 589 | case 23: replace += "x17"; break; |
michael@0 | 590 | case 24: replace += "x18"; break; |
michael@0 | 591 | case 25: replace += "x19"; break; |
michael@0 | 592 | case 26: replace += "x1a"; break; |
michael@0 | 593 | case 27: replace += "x1b"; break; |
michael@0 | 594 | case 28: replace += "x1c"; break; |
michael@0 | 595 | case 29: replace += "x1d"; break; |
michael@0 | 596 | case 30: replace += "x1e"; break; |
michael@0 | 597 | case 31: replace += "x1f"; break; |
michael@0 | 598 | } |
michael@0 | 599 | val = val.replace(RegExp(String.fromCharCode(i), "g"), replace); |
michael@0 | 600 | } |
michael@0 | 601 | return '"' + val.replace(/"/g, '\\"') + '"'; |
michael@0 | 602 | case "boolean": |
michael@0 | 603 | case "undefined": |
michael@0 | 604 | return String(val); |
michael@0 | 605 | case "number": |
michael@0 | 606 | // In JavaScript, -0 === 0 and String(-0) == "0", so we have to |
michael@0 | 607 | // special-case. |
michael@0 | 608 | if (val === -0 && 1/val === -Infinity) |
michael@0 | 609 | { |
michael@0 | 610 | return "-0"; |
michael@0 | 611 | } |
michael@0 | 612 | return String(val); |
michael@0 | 613 | case "object": |
michael@0 | 614 | if (val === null) |
michael@0 | 615 | { |
michael@0 | 616 | return "null"; |
michael@0 | 617 | } |
michael@0 | 618 | |
michael@0 | 619 | // Special-case Node objects, since those come up a lot in my tests. I |
michael@0 | 620 | // ignore namespaces. |
michael@0 | 621 | if (is_node(val)) |
michael@0 | 622 | { |
michael@0 | 623 | switch (val.nodeType) |
michael@0 | 624 | { |
michael@0 | 625 | case Node.ELEMENT_NODE: |
michael@0 | 626 | var ret = "<" + val.tagName.toLowerCase(); |
michael@0 | 627 | for (var i = 0; i < val.attributes.length; i++) |
michael@0 | 628 | { |
michael@0 | 629 | ret += " " + val.attributes[i].name + '="' + val.attributes[i].value + '"'; |
michael@0 | 630 | } |
michael@0 | 631 | ret += ">" + val.innerHTML + "</" + val.tagName.toLowerCase() + ">"; |
michael@0 | 632 | return "Element node " + truncate(ret, 60); |
michael@0 | 633 | case Node.TEXT_NODE: |
michael@0 | 634 | return 'Text node "' + truncate(val.data, 60) + '"'; |
michael@0 | 635 | case Node.PROCESSING_INSTRUCTION_NODE: |
michael@0 | 636 | return "ProcessingInstruction node with target " + format_value(truncate(val.target, 60)) + " and data " + format_value(truncate(val.data, 60)); |
michael@0 | 637 | case Node.COMMENT_NODE: |
michael@0 | 638 | return "Comment node <!--" + truncate(val.data, 60) + "-->"; |
michael@0 | 639 | case Node.DOCUMENT_NODE: |
michael@0 | 640 | return "Document node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children"); |
michael@0 | 641 | case Node.DOCUMENT_TYPE_NODE: |
michael@0 | 642 | return "DocumentType node"; |
michael@0 | 643 | case Node.DOCUMENT_FRAGMENT_NODE: |
michael@0 | 644 | return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children"); |
michael@0 | 645 | default: |
michael@0 | 646 | return "Node object of unknown type"; |
michael@0 | 647 | } |
michael@0 | 648 | } |
michael@0 | 649 | |
michael@0 | 650 | // Fall through to default |
michael@0 | 651 | default: |
michael@0 | 652 | return typeof val + ' "' + truncate(String(val), 60) + '"'; |
michael@0 | 653 | } |
michael@0 | 654 | } |
michael@0 | 655 | expose(format_value, "format_value"); |
michael@0 | 656 | |
michael@0 | 657 | /* |
michael@0 | 658 | * Assertions |
michael@0 | 659 | */ |
michael@0 | 660 | |
michael@0 | 661 | function assert_true(actual, description) |
michael@0 | 662 | { |
michael@0 | 663 | assert(actual === true, "assert_true", description, |
michael@0 | 664 | "expected true got ${actual}", {actual:actual}); |
michael@0 | 665 | }; |
michael@0 | 666 | expose(assert_true, "assert_true"); |
michael@0 | 667 | |
michael@0 | 668 | function assert_false(actual, description) |
michael@0 | 669 | { |
michael@0 | 670 | assert(actual === false, "assert_false", description, |
michael@0 | 671 | "expected false got ${actual}", {actual:actual}); |
michael@0 | 672 | }; |
michael@0 | 673 | expose(assert_false, "assert_false"); |
michael@0 | 674 | |
michael@0 | 675 | function same_value(x, y) { |
michael@0 | 676 | if (y !== y) |
michael@0 | 677 | { |
michael@0 | 678 | //NaN case |
michael@0 | 679 | return x !== x; |
michael@0 | 680 | } |
michael@0 | 681 | else if (x === 0 && y === 0) { |
michael@0 | 682 | //Distinguish +0 and -0 |
michael@0 | 683 | return 1/x === 1/y; |
michael@0 | 684 | } |
michael@0 | 685 | else |
michael@0 | 686 | { |
michael@0 | 687 | //typical case |
michael@0 | 688 | return x === y; |
michael@0 | 689 | } |
michael@0 | 690 | } |
michael@0 | 691 | |
michael@0 | 692 | function assert_equals(actual, expected, description) |
michael@0 | 693 | { |
michael@0 | 694 | /* |
michael@0 | 695 | * Test if two primitives are equal or two objects |
michael@0 | 696 | * are the same object |
michael@0 | 697 | */ |
michael@0 | 698 | if (typeof actual != typeof expected) |
michael@0 | 699 | { |
michael@0 | 700 | assert(false, "assert_equals", description, |
michael@0 | 701 | "expected (" + typeof expected + ") ${expected} but got (" + typeof actual + ") ${actual}", |
michael@0 | 702 | {expected:expected, actual:actual}); |
michael@0 | 703 | return; |
michael@0 | 704 | } |
michael@0 | 705 | assert(same_value(actual, expected), "assert_equals", description, |
michael@0 | 706 | "expected ${expected} but got ${actual}", |
michael@0 | 707 | {expected:expected, actual:actual}); |
michael@0 | 708 | }; |
michael@0 | 709 | expose(assert_equals, "assert_equals"); |
michael@0 | 710 | |
michael@0 | 711 | function assert_not_equals(actual, expected, description) |
michael@0 | 712 | { |
michael@0 | 713 | /* |
michael@0 | 714 | * Test if two primitives are unequal or two objects |
michael@0 | 715 | * are different objects |
michael@0 | 716 | */ |
michael@0 | 717 | assert(!same_value(actual, expected), "assert_not_equals", description, |
michael@0 | 718 | "got disallowed value ${actual}", |
michael@0 | 719 | {actual:actual}); |
michael@0 | 720 | }; |
michael@0 | 721 | expose(assert_not_equals, "assert_not_equals"); |
michael@0 | 722 | |
michael@0 | 723 | function assert_in_array(actual, expected, description) |
michael@0 | 724 | { |
michael@0 | 725 | assert(expected.indexOf(actual) != -1, "assert_in_array", description, |
michael@0 | 726 | "value ${actual} not in array ${expected}", |
michael@0 | 727 | {actual:actual, expected:expected}); |
michael@0 | 728 | } |
michael@0 | 729 | expose(assert_in_array, "assert_in_array"); |
michael@0 | 730 | |
michael@0 | 731 | function assert_object_equals(actual, expected, description) |
michael@0 | 732 | { |
michael@0 | 733 | //This needs to be improved a great deal |
michael@0 | 734 | function check_equal(actual, expected, stack) |
michael@0 | 735 | { |
michael@0 | 736 | stack.push(actual); |
michael@0 | 737 | |
michael@0 | 738 | var p; |
michael@0 | 739 | for (p in actual) |
michael@0 | 740 | { |
michael@0 | 741 | assert(expected.hasOwnProperty(p), "assert_object_equals", description, |
michael@0 | 742 | "unexpected property ${p}", {p:p}); |
michael@0 | 743 | |
michael@0 | 744 | if (typeof actual[p] === "object" && actual[p] !== null) |
michael@0 | 745 | { |
michael@0 | 746 | if (stack.indexOf(actual[p]) === -1) |
michael@0 | 747 | { |
michael@0 | 748 | check_equal(actual[p], expected[p], stack); |
michael@0 | 749 | } |
michael@0 | 750 | } |
michael@0 | 751 | else |
michael@0 | 752 | { |
michael@0 | 753 | assert(same_value(actual[p], expected[p]), "assert_object_equals", description, |
michael@0 | 754 | "property ${p} expected ${expected} got ${actual}", |
michael@0 | 755 | {p:p, expected:expected, actual:actual}); |
michael@0 | 756 | } |
michael@0 | 757 | } |
michael@0 | 758 | for (p in expected) |
michael@0 | 759 | { |
michael@0 | 760 | assert(actual.hasOwnProperty(p), |
michael@0 | 761 | "assert_object_equals", description, |
michael@0 | 762 | "expected property ${p} missing", {p:p}); |
michael@0 | 763 | } |
michael@0 | 764 | stack.pop(); |
michael@0 | 765 | } |
michael@0 | 766 | check_equal(actual, expected, []); |
michael@0 | 767 | }; |
michael@0 | 768 | expose(assert_object_equals, "assert_object_equals"); |
michael@0 | 769 | |
michael@0 | 770 | function assert_array_equals(actual, expected, description) |
michael@0 | 771 | { |
michael@0 | 772 | assert(actual.length === expected.length, |
michael@0 | 773 | "assert_array_equals", description, |
michael@0 | 774 | "lengths differ, expected ${expected} got ${actual}", |
michael@0 | 775 | {expected:expected.length, actual:actual.length}); |
michael@0 | 776 | |
michael@0 | 777 | for (var i=0; i < actual.length; i++) |
michael@0 | 778 | { |
michael@0 | 779 | assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i), |
michael@0 | 780 | "assert_array_equals", description, |
michael@0 | 781 | "property ${i}, property expected to be $expected but was $actual", |
michael@0 | 782 | {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing", |
michael@0 | 783 | actual:actual.hasOwnProperty(i) ? "present" : "missing"}); |
michael@0 | 784 | assert(same_value(expected[i], actual[i]), |
michael@0 | 785 | "assert_array_equals", description, |
michael@0 | 786 | "property ${i}, expected ${expected} but got ${actual}", |
michael@0 | 787 | {i:i, expected:expected[i], actual:actual[i]}); |
michael@0 | 788 | } |
michael@0 | 789 | } |
michael@0 | 790 | expose(assert_array_equals, "assert_array_equals"); |
michael@0 | 791 | |
michael@0 | 792 | function assert_approx_equals(actual, expected, epsilon, description) |
michael@0 | 793 | { |
michael@0 | 794 | /* |
michael@0 | 795 | * Test if two primitive numbers are equal withing +/- epsilon |
michael@0 | 796 | */ |
michael@0 | 797 | assert(typeof actual === "number", |
michael@0 | 798 | "assert_approx_equals", description, |
michael@0 | 799 | "expected a number but got a ${type_actual}", |
michael@0 | 800 | {type_actual:typeof actual}); |
michael@0 | 801 | |
michael@0 | 802 | assert(Math.abs(actual - expected) <= epsilon, |
michael@0 | 803 | "assert_approx_equals", description, |
michael@0 | 804 | "expected ${expected} +/- ${epsilon} but got ${actual}", |
michael@0 | 805 | {expected:expected, actual:actual, epsilon:epsilon}); |
michael@0 | 806 | }; |
michael@0 | 807 | expose(assert_approx_equals, "assert_approx_equals"); |
michael@0 | 808 | |
michael@0 | 809 | function assert_less_than(actual, expected, description) |
michael@0 | 810 | { |
michael@0 | 811 | /* |
michael@0 | 812 | * Test if a primitive number is less than another |
michael@0 | 813 | */ |
michael@0 | 814 | assert(typeof actual === "number", |
michael@0 | 815 | "assert_less_than", description, |
michael@0 | 816 | "expected a number but got a ${type_actual}", |
michael@0 | 817 | {type_actual:typeof actual}); |
michael@0 | 818 | |
michael@0 | 819 | assert(actual < expected, |
michael@0 | 820 | "assert_less_than", description, |
michael@0 | 821 | "expected a number less than ${expected} but got ${actual}", |
michael@0 | 822 | {expected:expected, actual:actual}); |
michael@0 | 823 | }; |
michael@0 | 824 | expose(assert_less_than, "assert_less_than"); |
michael@0 | 825 | |
michael@0 | 826 | function assert_greater_than(actual, expected, description) |
michael@0 | 827 | { |
michael@0 | 828 | /* |
michael@0 | 829 | * Test if a primitive number is greater than another |
michael@0 | 830 | */ |
michael@0 | 831 | assert(typeof actual === "number", |
michael@0 | 832 | "assert_greater_than", description, |
michael@0 | 833 | "expected a number but got a ${type_actual}", |
michael@0 | 834 | {type_actual:typeof actual}); |
michael@0 | 835 | |
michael@0 | 836 | assert(actual > expected, |
michael@0 | 837 | "assert_greater_than", description, |
michael@0 | 838 | "expected a number greater than ${expected} but got ${actual}", |
michael@0 | 839 | {expected:expected, actual:actual}); |
michael@0 | 840 | }; |
michael@0 | 841 | expose(assert_greater_than, "assert_greater_than"); |
michael@0 | 842 | |
michael@0 | 843 | function assert_less_than_equal(actual, expected, description) |
michael@0 | 844 | { |
michael@0 | 845 | /* |
michael@0 | 846 | * Test if a primitive number is less than or equal to another |
michael@0 | 847 | */ |
michael@0 | 848 | assert(typeof actual === "number", |
michael@0 | 849 | "assert_less_than_equal", description, |
michael@0 | 850 | "expected a number but got a ${type_actual}", |
michael@0 | 851 | {type_actual:typeof actual}); |
michael@0 | 852 | |
michael@0 | 853 | assert(actual <= expected, |
michael@0 | 854 | "assert_less_than", description, |
michael@0 | 855 | "expected a number less than or equal to ${expected} but got ${actual}", |
michael@0 | 856 | {expected:expected, actual:actual}); |
michael@0 | 857 | }; |
michael@0 | 858 | expose(assert_less_than_equal, "assert_less_than_equal"); |
michael@0 | 859 | |
michael@0 | 860 | function assert_greater_than_equal(actual, expected, description) |
michael@0 | 861 | { |
michael@0 | 862 | /* |
michael@0 | 863 | * Test if a primitive number is greater than or equal to another |
michael@0 | 864 | */ |
michael@0 | 865 | assert(typeof actual === "number", |
michael@0 | 866 | "assert_greater_than_equal", description, |
michael@0 | 867 | "expected a number but got a ${type_actual}", |
michael@0 | 868 | {type_actual:typeof actual}); |
michael@0 | 869 | |
michael@0 | 870 | assert(actual >= expected, |
michael@0 | 871 | "assert_greater_than_equal", description, |
michael@0 | 872 | "expected a number greater than or equal to ${expected} but got ${actual}", |
michael@0 | 873 | {expected:expected, actual:actual}); |
michael@0 | 874 | }; |
michael@0 | 875 | expose(assert_greater_than_equal, "assert_greater_than_equal"); |
michael@0 | 876 | |
michael@0 | 877 | function assert_regexp_match(actual, expected, description) { |
michael@0 | 878 | /* |
michael@0 | 879 | * Test if a string (actual) matches a regexp (expected) |
michael@0 | 880 | */ |
michael@0 | 881 | assert(expected.test(actual), |
michael@0 | 882 | "assert_regexp_match", description, |
michael@0 | 883 | "expected ${expected} but got ${actual}", |
michael@0 | 884 | {expected:expected, actual:actual}); |
michael@0 | 885 | } |
michael@0 | 886 | expose(assert_regexp_match, "assert_regexp_match"); |
michael@0 | 887 | |
michael@0 | 888 | function assert_class_string(object, class_string, description) { |
michael@0 | 889 | assert_equals({}.toString.call(object), "[object " + class_string + "]", |
michael@0 | 890 | description); |
michael@0 | 891 | } |
michael@0 | 892 | expose(assert_class_string, "assert_class_string"); |
michael@0 | 893 | |
michael@0 | 894 | |
michael@0 | 895 | function _assert_own_property(name) { |
michael@0 | 896 | return function(object, property_name, description) |
michael@0 | 897 | { |
michael@0 | 898 | assert(object.hasOwnProperty(property_name), |
michael@0 | 899 | name, description, |
michael@0 | 900 | "expected property ${p} missing", {p:property_name}); |
michael@0 | 901 | }; |
michael@0 | 902 | } |
michael@0 | 903 | expose(_assert_own_property("assert_exists"), "assert_exists"); |
michael@0 | 904 | expose(_assert_own_property("assert_own_property"), "assert_own_property"); |
michael@0 | 905 | |
michael@0 | 906 | function assert_not_exists(object, property_name, description) |
michael@0 | 907 | { |
michael@0 | 908 | assert(!object.hasOwnProperty(property_name), |
michael@0 | 909 | "assert_not_exists", description, |
michael@0 | 910 | "unexpected property ${p} found", {p:property_name}); |
michael@0 | 911 | }; |
michael@0 | 912 | expose(assert_not_exists, "assert_not_exists"); |
michael@0 | 913 | |
michael@0 | 914 | function _assert_inherits(name) { |
michael@0 | 915 | return function (object, property_name, description) |
michael@0 | 916 | { |
michael@0 | 917 | assert(typeof object === "object", |
michael@0 | 918 | name, description, |
michael@0 | 919 | "provided value is not an object"); |
michael@0 | 920 | |
michael@0 | 921 | assert("hasOwnProperty" in object, |
michael@0 | 922 | name, description, |
michael@0 | 923 | "provided value is an object but has no hasOwnProperty method"); |
michael@0 | 924 | |
michael@0 | 925 | assert(!object.hasOwnProperty(property_name), |
michael@0 | 926 | name, description, |
michael@0 | 927 | "property ${p} found on object expected in prototype chain", |
michael@0 | 928 | {p:property_name}); |
michael@0 | 929 | |
michael@0 | 930 | assert(property_name in object, |
michael@0 | 931 | name, description, |
michael@0 | 932 | "property ${p} not found in prototype chain", |
michael@0 | 933 | {p:property_name}); |
michael@0 | 934 | }; |
michael@0 | 935 | } |
michael@0 | 936 | expose(_assert_inherits("assert_inherits"), "assert_inherits"); |
michael@0 | 937 | expose(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute"); |
michael@0 | 938 | |
michael@0 | 939 | function assert_readonly(object, property_name, description) |
michael@0 | 940 | { |
michael@0 | 941 | var initial_value = object[property_name]; |
michael@0 | 942 | try { |
michael@0 | 943 | //Note that this can have side effects in the case where |
michael@0 | 944 | //the property has PutForwards |
michael@0 | 945 | object[property_name] = initial_value + "a"; //XXX use some other value here? |
michael@0 | 946 | assert(same_value(object[property_name], initial_value), |
michael@0 | 947 | "assert_readonly", description, |
michael@0 | 948 | "changing property ${p} succeeded", |
michael@0 | 949 | {p:property_name}); |
michael@0 | 950 | } |
michael@0 | 951 | finally |
michael@0 | 952 | { |
michael@0 | 953 | object[property_name] = initial_value; |
michael@0 | 954 | } |
michael@0 | 955 | }; |
michael@0 | 956 | expose(assert_readonly, "assert_readonly"); |
michael@0 | 957 | |
michael@0 | 958 | function assert_throws(code, func, description) |
michael@0 | 959 | { |
michael@0 | 960 | try |
michael@0 | 961 | { |
michael@0 | 962 | func.call(this); |
michael@0 | 963 | assert(false, "assert_throws", description, |
michael@0 | 964 | "${func} did not throw", {func:func}); |
michael@0 | 965 | } |
michael@0 | 966 | catch(e) |
michael@0 | 967 | { |
michael@0 | 968 | if (e instanceof AssertionError) { |
michael@0 | 969 | throw(e); |
michael@0 | 970 | } |
michael@0 | 971 | if (code === null) |
michael@0 | 972 | { |
michael@0 | 973 | return; |
michael@0 | 974 | } |
michael@0 | 975 | if (typeof code === "object") |
michael@0 | 976 | { |
michael@0 | 977 | assert(typeof e == "object" && "name" in e && e.name == code.name, |
michael@0 | 978 | "assert_throws", description, |
michael@0 | 979 | "${func} threw ${actual} (${actual_name}) expected ${expected} (${expected_name})", |
michael@0 | 980 | {func:func, actual:e, actual_name:e.name, |
michael@0 | 981 | expected:code, |
michael@0 | 982 | expected_name:code.name}); |
michael@0 | 983 | return; |
michael@0 | 984 | } |
michael@0 | 985 | |
michael@0 | 986 | var code_name_map = { |
michael@0 | 987 | INDEX_SIZE_ERR: 'IndexSizeError', |
michael@0 | 988 | HIERARCHY_REQUEST_ERR: 'HierarchyRequestError', |
michael@0 | 989 | WRONG_DOCUMENT_ERR: 'WrongDocumentError', |
michael@0 | 990 | INVALID_CHARACTER_ERR: 'InvalidCharacterError', |
michael@0 | 991 | NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError', |
michael@0 | 992 | NOT_FOUND_ERR: 'NotFoundError', |
michael@0 | 993 | NOT_SUPPORTED_ERR: 'NotSupportedError', |
michael@0 | 994 | INVALID_STATE_ERR: 'InvalidStateError', |
michael@0 | 995 | SYNTAX_ERR: 'SyntaxError', |
michael@0 | 996 | INVALID_MODIFICATION_ERR: 'InvalidModificationError', |
michael@0 | 997 | NAMESPACE_ERR: 'NamespaceError', |
michael@0 | 998 | INVALID_ACCESS_ERR: 'InvalidAccessError', |
michael@0 | 999 | TYPE_MISMATCH_ERR: 'TypeMismatchError', |
michael@0 | 1000 | SECURITY_ERR: 'SecurityError', |
michael@0 | 1001 | NETWORK_ERR: 'NetworkError', |
michael@0 | 1002 | ABORT_ERR: 'AbortError', |
michael@0 | 1003 | URL_MISMATCH_ERR: 'URLMismatchError', |
michael@0 | 1004 | QUOTA_EXCEEDED_ERR: 'QuotaExceededError', |
michael@0 | 1005 | TIMEOUT_ERR: 'TimeoutError', |
michael@0 | 1006 | INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError', |
michael@0 | 1007 | DATA_CLONE_ERR: 'DataCloneError' |
michael@0 | 1008 | }; |
michael@0 | 1009 | |
michael@0 | 1010 | var name = code in code_name_map ? code_name_map[code] : code; |
michael@0 | 1011 | |
michael@0 | 1012 | var name_code_map = { |
michael@0 | 1013 | IndexSizeError: 1, |
michael@0 | 1014 | HierarchyRequestError: 3, |
michael@0 | 1015 | WrongDocumentError: 4, |
michael@0 | 1016 | InvalidCharacterError: 5, |
michael@0 | 1017 | NoModificationAllowedError: 7, |
michael@0 | 1018 | NotFoundError: 8, |
michael@0 | 1019 | NotSupportedError: 9, |
michael@0 | 1020 | InvalidStateError: 11, |
michael@0 | 1021 | SyntaxError: 12, |
michael@0 | 1022 | InvalidModificationError: 13, |
michael@0 | 1023 | NamespaceError: 14, |
michael@0 | 1024 | InvalidAccessError: 15, |
michael@0 | 1025 | TypeMismatchError: 17, |
michael@0 | 1026 | SecurityError: 18, |
michael@0 | 1027 | NetworkError: 19, |
michael@0 | 1028 | AbortError: 20, |
michael@0 | 1029 | URLMismatchError: 21, |
michael@0 | 1030 | QuotaExceededError: 22, |
michael@0 | 1031 | TimeoutError: 23, |
michael@0 | 1032 | InvalidNodeTypeError: 24, |
michael@0 | 1033 | DataCloneError: 25, |
michael@0 | 1034 | |
michael@0 | 1035 | UnknownError: 0, |
michael@0 | 1036 | ConstraintError: 0, |
michael@0 | 1037 | DataError: 0, |
michael@0 | 1038 | TransactionInactiveError: 0, |
michael@0 | 1039 | ReadOnlyError: 0, |
michael@0 | 1040 | VersionError: 0 |
michael@0 | 1041 | }; |
michael@0 | 1042 | |
michael@0 | 1043 | if (!(name in name_code_map)) |
michael@0 | 1044 | { |
michael@0 | 1045 | throw new AssertionError('Test bug: unrecognized DOMException code "' + code + '" passed to assert_throws()'); |
michael@0 | 1046 | } |
michael@0 | 1047 | |
michael@0 | 1048 | var required_props = { code: name_code_map[name] }; |
michael@0 | 1049 | |
michael@0 | 1050 | if (required_props.code === 0 |
michael@0 | 1051 | || ("name" in e && e.name !== e.name.toUpperCase() && e.name !== "DOMException")) |
michael@0 | 1052 | { |
michael@0 | 1053 | // New style exception: also test the name property. |
michael@0 | 1054 | required_props.name = name; |
michael@0 | 1055 | } |
michael@0 | 1056 | |
michael@0 | 1057 | //We'd like to test that e instanceof the appropriate interface, |
michael@0 | 1058 | //but we can't, because we don't know what window it was created |
michael@0 | 1059 | //in. It might be an instanceof the appropriate interface on some |
michael@0 | 1060 | //unknown other window. TODO: Work around this somehow? |
michael@0 | 1061 | |
michael@0 | 1062 | assert(typeof e == "object", |
michael@0 | 1063 | "assert_throws", description, |
michael@0 | 1064 | "${func} threw ${e} with type ${type}, not an object", |
michael@0 | 1065 | {func:func, e:e, type:typeof e}); |
michael@0 | 1066 | |
michael@0 | 1067 | for (var prop in required_props) |
michael@0 | 1068 | { |
michael@0 | 1069 | assert(typeof e == "object" && prop in e && e[prop] == required_props[prop], |
michael@0 | 1070 | "assert_throws", description, |
michael@0 | 1071 | "${func} threw ${e} that is not a DOMException " + code + ": property ${prop} is equal to ${actual}, expected ${expected}", |
michael@0 | 1072 | {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]}); |
michael@0 | 1073 | } |
michael@0 | 1074 | } |
michael@0 | 1075 | } |
michael@0 | 1076 | expose(assert_throws, "assert_throws"); |
michael@0 | 1077 | |
michael@0 | 1078 | function assert_unreached(description) { |
michael@0 | 1079 | assert(false, "assert_unreached", description, |
michael@0 | 1080 | "Reached unreachable code"); |
michael@0 | 1081 | } |
michael@0 | 1082 | expose(assert_unreached, "assert_unreached"); |
michael@0 | 1083 | |
michael@0 | 1084 | function assert_any(assert_func, actual, expected_array) |
michael@0 | 1085 | { |
michael@0 | 1086 | var args = [].slice.call(arguments, 3) |
michael@0 | 1087 | var errors = [] |
michael@0 | 1088 | var passed = false; |
michael@0 | 1089 | forEach(expected_array, |
michael@0 | 1090 | function(expected) |
michael@0 | 1091 | { |
michael@0 | 1092 | try { |
michael@0 | 1093 | assert_func.apply(this, [actual, expected].concat(args)) |
michael@0 | 1094 | passed = true; |
michael@0 | 1095 | } catch(e) { |
michael@0 | 1096 | errors.push(e.message); |
michael@0 | 1097 | } |
michael@0 | 1098 | }); |
michael@0 | 1099 | if (!passed) { |
michael@0 | 1100 | throw new AssertionError(errors.join("\n\n")); |
michael@0 | 1101 | } |
michael@0 | 1102 | } |
michael@0 | 1103 | expose(assert_any, "assert_any"); |
michael@0 | 1104 | |
michael@0 | 1105 | function Test(name, properties) |
michael@0 | 1106 | { |
michael@0 | 1107 | this.name = name; |
michael@0 | 1108 | |
michael@0 | 1109 | this.phases = { |
michael@0 | 1110 | INITIAL:0, |
michael@0 | 1111 | STARTED:1, |
michael@0 | 1112 | HAS_RESULT:2, |
michael@0 | 1113 | COMPLETE:3 |
michael@0 | 1114 | }; |
michael@0 | 1115 | this.phase = this.phases.INITIAL; |
michael@0 | 1116 | |
michael@0 | 1117 | this.status = this.NOTRUN; |
michael@0 | 1118 | this.timeout_id = null; |
michael@0 | 1119 | |
michael@0 | 1120 | this.properties = properties; |
michael@0 | 1121 | var timeout = properties.timeout ? properties.timeout : settings.test_timeout |
michael@0 | 1122 | if (timeout != null) { |
michael@0 | 1123 | this.timeout_length = timeout * tests.timeout_multiplier; |
michael@0 | 1124 | } else { |
michael@0 | 1125 | this.timeout_length = null; |
michael@0 | 1126 | } |
michael@0 | 1127 | |
michael@0 | 1128 | this.message = null; |
michael@0 | 1129 | |
michael@0 | 1130 | var this_obj = this; |
michael@0 | 1131 | this.steps = []; |
michael@0 | 1132 | |
michael@0 | 1133 | tests.push(this); |
michael@0 | 1134 | } |
michael@0 | 1135 | |
michael@0 | 1136 | Test.statuses = { |
michael@0 | 1137 | PASS:0, |
michael@0 | 1138 | FAIL:1, |
michael@0 | 1139 | TIMEOUT:2, |
michael@0 | 1140 | NOTRUN:3 |
michael@0 | 1141 | }; |
michael@0 | 1142 | |
michael@0 | 1143 | Test.prototype = merge({}, Test.statuses); |
michael@0 | 1144 | |
michael@0 | 1145 | Test.prototype.structured_clone = function() |
michael@0 | 1146 | { |
michael@0 | 1147 | if(!this._structured_clone) |
michael@0 | 1148 | { |
michael@0 | 1149 | var msg = this.message; |
michael@0 | 1150 | msg = msg ? String(msg) : msg; |
michael@0 | 1151 | this._structured_clone = merge({ |
michael@0 | 1152 | name:String(this.name), |
michael@0 | 1153 | status:this.status, |
michael@0 | 1154 | message:msg |
michael@0 | 1155 | }, Test.statuses); |
michael@0 | 1156 | } |
michael@0 | 1157 | return this._structured_clone; |
michael@0 | 1158 | }; |
michael@0 | 1159 | |
michael@0 | 1160 | Test.prototype.step = function(func, this_obj) |
michael@0 | 1161 | { |
michael@0 | 1162 | if (this.phase > this.phases.STARTED) |
michael@0 | 1163 | { |
michael@0 | 1164 | return; |
michael@0 | 1165 | } |
michael@0 | 1166 | this.phase = this.phases.STARTED; |
michael@0 | 1167 | //If we don't get a result before the harness times out that will be a test timout |
michael@0 | 1168 | this.set_status(this.TIMEOUT, "Test timed out"); |
michael@0 | 1169 | |
michael@0 | 1170 | tests.started = true; |
michael@0 | 1171 | |
michael@0 | 1172 | if (this.timeout_id === null) |
michael@0 | 1173 | { |
michael@0 | 1174 | this.set_timeout(); |
michael@0 | 1175 | } |
michael@0 | 1176 | |
michael@0 | 1177 | this.steps.push(func); |
michael@0 | 1178 | |
michael@0 | 1179 | if (arguments.length === 1) |
michael@0 | 1180 | { |
michael@0 | 1181 | this_obj = this; |
michael@0 | 1182 | } |
michael@0 | 1183 | |
michael@0 | 1184 | try |
michael@0 | 1185 | { |
michael@0 | 1186 | return func.apply(this_obj, Array.prototype.slice.call(arguments, 2)); |
michael@0 | 1187 | } |
michael@0 | 1188 | catch(e) |
michael@0 | 1189 | { |
michael@0 | 1190 | if (this.phase >= this.phases.HAS_RESULT) |
michael@0 | 1191 | { |
michael@0 | 1192 | return; |
michael@0 | 1193 | } |
michael@0 | 1194 | var message = (typeof e === "object" && e !== null) ? e.message : e; |
michael@0 | 1195 | if (typeof e.stack != "undefined" && typeof e.message == "string") { |
michael@0 | 1196 | //Try to make it more informative for some exceptions, at least |
michael@0 | 1197 | //in Gecko and WebKit. This results in a stack dump instead of |
michael@0 | 1198 | //just errors like "Cannot read property 'parentNode' of null" |
michael@0 | 1199 | //or "root is null". Makes it a lot longer, of course. |
michael@0 | 1200 | message += "(stack: " + e.stack + ")"; |
michael@0 | 1201 | } |
michael@0 | 1202 | this.set_status(this.FAIL, message); |
michael@0 | 1203 | this.phase = this.phases.HAS_RESULT; |
michael@0 | 1204 | this.done(); |
michael@0 | 1205 | } |
michael@0 | 1206 | }; |
michael@0 | 1207 | |
michael@0 | 1208 | Test.prototype.step_func = function(func, this_obj) |
michael@0 | 1209 | { |
michael@0 | 1210 | var test_this = this; |
michael@0 | 1211 | |
michael@0 | 1212 | if (arguments.length === 1) |
michael@0 | 1213 | { |
michael@0 | 1214 | this_obj = test_this; |
michael@0 | 1215 | } |
michael@0 | 1216 | |
michael@0 | 1217 | return function() |
michael@0 | 1218 | { |
michael@0 | 1219 | test_this.step.apply(test_this, [func, this_obj].concat( |
michael@0 | 1220 | Array.prototype.slice.call(arguments))); |
michael@0 | 1221 | }; |
michael@0 | 1222 | }; |
michael@0 | 1223 | |
michael@0 | 1224 | Test.prototype.step_func_done = function(func, this_obj) |
michael@0 | 1225 | { |
michael@0 | 1226 | var test_this = this; |
michael@0 | 1227 | |
michael@0 | 1228 | if (arguments.length === 1) |
michael@0 | 1229 | { |
michael@0 | 1230 | this_obj = test_this; |
michael@0 | 1231 | } |
michael@0 | 1232 | |
michael@0 | 1233 | return function() |
michael@0 | 1234 | { |
michael@0 | 1235 | test_this.step.apply(test_this, [func, this_obj].concat( |
michael@0 | 1236 | Array.prototype.slice.call(arguments))); |
michael@0 | 1237 | test_this.done(); |
michael@0 | 1238 | }; |
michael@0 | 1239 | } |
michael@0 | 1240 | |
michael@0 | 1241 | Test.prototype.set_timeout = function() |
michael@0 | 1242 | { |
michael@0 | 1243 | if (this.timeout_length !== null) |
michael@0 | 1244 | { |
michael@0 | 1245 | var this_obj = this; |
michael@0 | 1246 | this.timeout_id = setTimeout(function() |
michael@0 | 1247 | { |
michael@0 | 1248 | this_obj.timeout(); |
michael@0 | 1249 | }, this.timeout_length); |
michael@0 | 1250 | } |
michael@0 | 1251 | } |
michael@0 | 1252 | |
michael@0 | 1253 | Test.prototype.set_status = function(status, message) |
michael@0 | 1254 | { |
michael@0 | 1255 | this.status = status; |
michael@0 | 1256 | this.message = message; |
michael@0 | 1257 | } |
michael@0 | 1258 | |
michael@0 | 1259 | Test.prototype.timeout = function() |
michael@0 | 1260 | { |
michael@0 | 1261 | this.timeout_id = null; |
michael@0 | 1262 | this.set_status(this.TIMEOUT, "Test timed out") |
michael@0 | 1263 | this.phase = this.phases.HAS_RESULT; |
michael@0 | 1264 | this.done(); |
michael@0 | 1265 | }; |
michael@0 | 1266 | |
michael@0 | 1267 | Test.prototype.done = function() |
michael@0 | 1268 | { |
michael@0 | 1269 | if (this.phase == this.phases.COMPLETE) { |
michael@0 | 1270 | return; |
michael@0 | 1271 | } else if (this.phase <= this.phases.STARTED) |
michael@0 | 1272 | { |
michael@0 | 1273 | this.set_status(this.PASS, null); |
michael@0 | 1274 | } |
michael@0 | 1275 | |
michael@0 | 1276 | if (this.status == this.NOTRUN) |
michael@0 | 1277 | { |
michael@0 | 1278 | alert(this.phase); |
michael@0 | 1279 | } |
michael@0 | 1280 | |
michael@0 | 1281 | this.phase = this.phases.COMPLETE; |
michael@0 | 1282 | |
michael@0 | 1283 | clearTimeout(this.timeout_id); |
michael@0 | 1284 | tests.result(this); |
michael@0 | 1285 | }; |
michael@0 | 1286 | |
michael@0 | 1287 | |
michael@0 | 1288 | /* |
michael@0 | 1289 | * Harness |
michael@0 | 1290 | */ |
michael@0 | 1291 | |
michael@0 | 1292 | function TestsStatus() |
michael@0 | 1293 | { |
michael@0 | 1294 | this.status = null; |
michael@0 | 1295 | this.message = null; |
michael@0 | 1296 | } |
michael@0 | 1297 | |
michael@0 | 1298 | TestsStatus.statuses = { |
michael@0 | 1299 | OK:0, |
michael@0 | 1300 | ERROR:1, |
michael@0 | 1301 | TIMEOUT:2 |
michael@0 | 1302 | }; |
michael@0 | 1303 | |
michael@0 | 1304 | TestsStatus.prototype = merge({}, TestsStatus.statuses); |
michael@0 | 1305 | |
michael@0 | 1306 | TestsStatus.prototype.structured_clone = function() |
michael@0 | 1307 | { |
michael@0 | 1308 | if(!this._structured_clone) |
michael@0 | 1309 | { |
michael@0 | 1310 | var msg = this.message; |
michael@0 | 1311 | msg = msg ? String(msg) : msg; |
michael@0 | 1312 | this._structured_clone = merge({ |
michael@0 | 1313 | status:this.status, |
michael@0 | 1314 | message:msg |
michael@0 | 1315 | }, TestsStatus.statuses); |
michael@0 | 1316 | } |
michael@0 | 1317 | return this._structured_clone; |
michael@0 | 1318 | }; |
michael@0 | 1319 | |
michael@0 | 1320 | function Tests() |
michael@0 | 1321 | { |
michael@0 | 1322 | this.tests = []; |
michael@0 | 1323 | this.num_pending = 0; |
michael@0 | 1324 | |
michael@0 | 1325 | this.phases = { |
michael@0 | 1326 | INITIAL:0, |
michael@0 | 1327 | SETUP:1, |
michael@0 | 1328 | HAVE_TESTS:2, |
michael@0 | 1329 | HAVE_RESULTS:3, |
michael@0 | 1330 | COMPLETE:4 |
michael@0 | 1331 | }; |
michael@0 | 1332 | this.phase = this.phases.INITIAL; |
michael@0 | 1333 | |
michael@0 | 1334 | this.properties = {}; |
michael@0 | 1335 | |
michael@0 | 1336 | //All tests can't be done until the load event fires |
michael@0 | 1337 | this.all_loaded = false; |
michael@0 | 1338 | this.wait_for_finish = false; |
michael@0 | 1339 | this.processing_callbacks = false; |
michael@0 | 1340 | |
michael@0 | 1341 | this.allow_uncaught_exception = false; |
michael@0 | 1342 | |
michael@0 | 1343 | this.timeout_multiplier = 1; |
michael@0 | 1344 | this.timeout_length = this.get_timeout(); |
michael@0 | 1345 | this.timeout_id = null; |
michael@0 | 1346 | |
michael@0 | 1347 | this.start_callbacks = []; |
michael@0 | 1348 | this.test_done_callbacks = []; |
michael@0 | 1349 | this.all_done_callbacks = []; |
michael@0 | 1350 | |
michael@0 | 1351 | this.status = new TestsStatus(); |
michael@0 | 1352 | |
michael@0 | 1353 | var this_obj = this; |
michael@0 | 1354 | |
michael@0 | 1355 | on_event(window, "load", |
michael@0 | 1356 | function() |
michael@0 | 1357 | { |
michael@0 | 1358 | this_obj.all_loaded = true; |
michael@0 | 1359 | if (this_obj.all_done()) |
michael@0 | 1360 | { |
michael@0 | 1361 | this_obj.complete(); |
michael@0 | 1362 | } |
michael@0 | 1363 | }); |
michael@0 | 1364 | |
michael@0 | 1365 | this.set_timeout(); |
michael@0 | 1366 | } |
michael@0 | 1367 | |
michael@0 | 1368 | Tests.prototype.setup = function(func, properties) |
michael@0 | 1369 | { |
michael@0 | 1370 | if (this.phase >= this.phases.HAVE_RESULTS) |
michael@0 | 1371 | { |
michael@0 | 1372 | return; |
michael@0 | 1373 | } |
michael@0 | 1374 | if (this.phase < this.phases.SETUP) |
michael@0 | 1375 | { |
michael@0 | 1376 | this.phase = this.phases.SETUP; |
michael@0 | 1377 | } |
michael@0 | 1378 | |
michael@0 | 1379 | this.properties = properties; |
michael@0 | 1380 | |
michael@0 | 1381 | for (var p in properties) |
michael@0 | 1382 | { |
michael@0 | 1383 | if (properties.hasOwnProperty(p)) |
michael@0 | 1384 | { |
michael@0 | 1385 | var value = properties[p] |
michael@0 | 1386 | if (p == "allow_uncaught_exception") { |
michael@0 | 1387 | this.allow_uncaught_exception = value; |
michael@0 | 1388 | } |
michael@0 | 1389 | else if (p == "explicit_done" && value) |
michael@0 | 1390 | { |
michael@0 | 1391 | this.wait_for_finish = true; |
michael@0 | 1392 | } |
michael@0 | 1393 | else if (p == "explicit_timeout" && value) { |
michael@0 | 1394 | this.timeout_length = null; |
michael@0 | 1395 | if (this.timeout_id) |
michael@0 | 1396 | { |
michael@0 | 1397 | clearTimeout(this.timeout_id); |
michael@0 | 1398 | } |
michael@0 | 1399 | } |
michael@0 | 1400 | else if (p == "timeout_multiplier") |
michael@0 | 1401 | { |
michael@0 | 1402 | this.timeout_multiplier = value; |
michael@0 | 1403 | } |
michael@0 | 1404 | } |
michael@0 | 1405 | } |
michael@0 | 1406 | |
michael@0 | 1407 | if (func) |
michael@0 | 1408 | { |
michael@0 | 1409 | try |
michael@0 | 1410 | { |
michael@0 | 1411 | func(); |
michael@0 | 1412 | } catch(e) |
michael@0 | 1413 | { |
michael@0 | 1414 | this.status.status = this.status.ERROR; |
michael@0 | 1415 | this.status.message = e; |
michael@0 | 1416 | }; |
michael@0 | 1417 | } |
michael@0 | 1418 | this.set_timeout(); |
michael@0 | 1419 | }; |
michael@0 | 1420 | |
michael@0 | 1421 | Tests.prototype.get_timeout = function() |
michael@0 | 1422 | { |
michael@0 | 1423 | var metas = document.getElementsByTagName("meta"); |
michael@0 | 1424 | for (var i=0; i<metas.length; i++) |
michael@0 | 1425 | { |
michael@0 | 1426 | if (metas[i].name == "timeout") |
michael@0 | 1427 | { |
michael@0 | 1428 | if (metas[i].content == "long") |
michael@0 | 1429 | { |
michael@0 | 1430 | return settings.harness_timeout.long; |
michael@0 | 1431 | } |
michael@0 | 1432 | break; |
michael@0 | 1433 | } |
michael@0 | 1434 | } |
michael@0 | 1435 | return settings.harness_timeout.normal; |
michael@0 | 1436 | } |
michael@0 | 1437 | |
michael@0 | 1438 | Tests.prototype.set_timeout = function() |
michael@0 | 1439 | { |
michael@0 | 1440 | var this_obj = this; |
michael@0 | 1441 | clearTimeout(this.timeout_id); |
michael@0 | 1442 | if (this.timeout_length !== null) |
michael@0 | 1443 | { |
michael@0 | 1444 | this.timeout_id = setTimeout(function() { |
michael@0 | 1445 | this_obj.timeout(); |
michael@0 | 1446 | }, this.timeout_length); |
michael@0 | 1447 | } |
michael@0 | 1448 | }; |
michael@0 | 1449 | |
michael@0 | 1450 | Tests.prototype.timeout = function() { |
michael@0 | 1451 | this.status.status = this.status.TIMEOUT; |
michael@0 | 1452 | this.complete(); |
michael@0 | 1453 | }; |
michael@0 | 1454 | |
michael@0 | 1455 | Tests.prototype.end_wait = function() |
michael@0 | 1456 | { |
michael@0 | 1457 | this.wait_for_finish = false; |
michael@0 | 1458 | if (this.all_done()) { |
michael@0 | 1459 | this.complete(); |
michael@0 | 1460 | } |
michael@0 | 1461 | }; |
michael@0 | 1462 | |
michael@0 | 1463 | Tests.prototype.push = function(test) |
michael@0 | 1464 | { |
michael@0 | 1465 | if (this.phase < this.phases.HAVE_TESTS) { |
michael@0 | 1466 | this.start(); |
michael@0 | 1467 | } |
michael@0 | 1468 | this.num_pending++; |
michael@0 | 1469 | this.tests.push(test); |
michael@0 | 1470 | }; |
michael@0 | 1471 | |
michael@0 | 1472 | Tests.prototype.all_done = function() { |
michael@0 | 1473 | return (this.all_loaded && this.num_pending === 0 && |
michael@0 | 1474 | !this.wait_for_finish && !this.processing_callbacks); |
michael@0 | 1475 | }; |
michael@0 | 1476 | |
michael@0 | 1477 | Tests.prototype.start = function() { |
michael@0 | 1478 | this.phase = this.phases.HAVE_TESTS; |
michael@0 | 1479 | this.notify_start(); |
michael@0 | 1480 | }; |
michael@0 | 1481 | |
michael@0 | 1482 | Tests.prototype.notify_start = function() { |
michael@0 | 1483 | var this_obj = this; |
michael@0 | 1484 | forEach (this.start_callbacks, |
michael@0 | 1485 | function(callback) |
michael@0 | 1486 | { |
michael@0 | 1487 | callback(this_obj.properties); |
michael@0 | 1488 | }); |
michael@0 | 1489 | forEach_windows( |
michael@0 | 1490 | function(w, is_same_origin) |
michael@0 | 1491 | { |
michael@0 | 1492 | if(is_same_origin && w.start_callback) |
michael@0 | 1493 | { |
michael@0 | 1494 | try |
michael@0 | 1495 | { |
michael@0 | 1496 | w.start_callback(this_obj.properties); |
michael@0 | 1497 | } |
michael@0 | 1498 | catch(e) |
michael@0 | 1499 | { |
michael@0 | 1500 | if (debug) |
michael@0 | 1501 | { |
michael@0 | 1502 | throw(e); |
michael@0 | 1503 | } |
michael@0 | 1504 | } |
michael@0 | 1505 | } |
michael@0 | 1506 | if (supports_post_message(w) && w !== self) |
michael@0 | 1507 | { |
michael@0 | 1508 | w.postMessage({ |
michael@0 | 1509 | type: "start", |
michael@0 | 1510 | properties: this_obj.properties |
michael@0 | 1511 | }, "*"); |
michael@0 | 1512 | } |
michael@0 | 1513 | }); |
michael@0 | 1514 | }; |
michael@0 | 1515 | |
michael@0 | 1516 | Tests.prototype.result = function(test) |
michael@0 | 1517 | { |
michael@0 | 1518 | if (this.phase > this.phases.HAVE_RESULTS) |
michael@0 | 1519 | { |
michael@0 | 1520 | return; |
michael@0 | 1521 | } |
michael@0 | 1522 | this.phase = this.phases.HAVE_RESULTS; |
michael@0 | 1523 | this.num_pending--; |
michael@0 | 1524 | this.notify_result(test); |
michael@0 | 1525 | }; |
michael@0 | 1526 | |
michael@0 | 1527 | Tests.prototype.notify_result = function(test) { |
michael@0 | 1528 | var this_obj = this; |
michael@0 | 1529 | this.processing_callbacks = true; |
michael@0 | 1530 | forEach(this.test_done_callbacks, |
michael@0 | 1531 | function(callback) |
michael@0 | 1532 | { |
michael@0 | 1533 | callback(test, this_obj); |
michael@0 | 1534 | }); |
michael@0 | 1535 | |
michael@0 | 1536 | forEach_windows( |
michael@0 | 1537 | function(w, is_same_origin) |
michael@0 | 1538 | { |
michael@0 | 1539 | if(is_same_origin && w.result_callback) |
michael@0 | 1540 | { |
michael@0 | 1541 | try |
michael@0 | 1542 | { |
michael@0 | 1543 | w.result_callback(test); |
michael@0 | 1544 | } |
michael@0 | 1545 | catch(e) |
michael@0 | 1546 | { |
michael@0 | 1547 | if(debug) { |
michael@0 | 1548 | throw e; |
michael@0 | 1549 | } |
michael@0 | 1550 | } |
michael@0 | 1551 | } |
michael@0 | 1552 | if (supports_post_message(w) && w !== self) |
michael@0 | 1553 | { |
michael@0 | 1554 | w.postMessage({ |
michael@0 | 1555 | type: "result", |
michael@0 | 1556 | test: test.structured_clone() |
michael@0 | 1557 | }, "*"); |
michael@0 | 1558 | } |
michael@0 | 1559 | }); |
michael@0 | 1560 | this.processing_callbacks = false; |
michael@0 | 1561 | if (this_obj.all_done()) |
michael@0 | 1562 | { |
michael@0 | 1563 | this_obj.complete(); |
michael@0 | 1564 | } |
michael@0 | 1565 | }; |
michael@0 | 1566 | |
michael@0 | 1567 | Tests.prototype.complete = function() { |
michael@0 | 1568 | if (this.phase === this.phases.COMPLETE) { |
michael@0 | 1569 | return; |
michael@0 | 1570 | } |
michael@0 | 1571 | this.phase = this.phases.COMPLETE; |
michael@0 | 1572 | var this_obj = this; |
michael@0 | 1573 | this.tests.forEach( |
michael@0 | 1574 | function(x) |
michael@0 | 1575 | { |
michael@0 | 1576 | if(x.status === x.NOTRUN) |
michael@0 | 1577 | { |
michael@0 | 1578 | this_obj.notify_result(x); |
michael@0 | 1579 | } |
michael@0 | 1580 | } |
michael@0 | 1581 | ); |
michael@0 | 1582 | this.notify_complete(); |
michael@0 | 1583 | }; |
michael@0 | 1584 | |
michael@0 | 1585 | Tests.prototype.notify_complete = function() |
michael@0 | 1586 | { |
michael@0 | 1587 | clearTimeout(this.timeout_id); |
michael@0 | 1588 | var this_obj = this; |
michael@0 | 1589 | var tests = map(this_obj.tests, |
michael@0 | 1590 | function(test) |
michael@0 | 1591 | { |
michael@0 | 1592 | return test.structured_clone(); |
michael@0 | 1593 | }); |
michael@0 | 1594 | if (this.status.status === null) |
michael@0 | 1595 | { |
michael@0 | 1596 | this.status.status = this.status.OK; |
michael@0 | 1597 | } |
michael@0 | 1598 | |
michael@0 | 1599 | forEach (this.all_done_callbacks, |
michael@0 | 1600 | function(callback) |
michael@0 | 1601 | { |
michael@0 | 1602 | callback(this_obj.tests, this_obj.status); |
michael@0 | 1603 | }); |
michael@0 | 1604 | |
michael@0 | 1605 | forEach_windows( |
michael@0 | 1606 | function(w, is_same_origin) |
michael@0 | 1607 | { |
michael@0 | 1608 | if(is_same_origin && w.completion_callback) |
michael@0 | 1609 | { |
michael@0 | 1610 | try |
michael@0 | 1611 | { |
michael@0 | 1612 | w.completion_callback(this_obj.tests, this_obj.status); |
michael@0 | 1613 | } |
michael@0 | 1614 | catch(e) |
michael@0 | 1615 | { |
michael@0 | 1616 | if (debug) |
michael@0 | 1617 | { |
michael@0 | 1618 | throw e; |
michael@0 | 1619 | } |
michael@0 | 1620 | } |
michael@0 | 1621 | } |
michael@0 | 1622 | if (supports_post_message(w) && w !== self) |
michael@0 | 1623 | { |
michael@0 | 1624 | w.postMessage({ |
michael@0 | 1625 | type: "complete", |
michael@0 | 1626 | tests: tests, |
michael@0 | 1627 | status: this_obj.status.structured_clone() |
michael@0 | 1628 | }, "*"); |
michael@0 | 1629 | } |
michael@0 | 1630 | }); |
michael@0 | 1631 | }; |
michael@0 | 1632 | |
michael@0 | 1633 | var tests = new Tests(); |
michael@0 | 1634 | |
michael@0 | 1635 | window.onerror = function(msg) { |
michael@0 | 1636 | if (!tests.allow_uncaught_exception) |
michael@0 | 1637 | { |
michael@0 | 1638 | tests.status.status = tests.status.ERROR; |
michael@0 | 1639 | tests.status.message = msg; |
michael@0 | 1640 | tests.complete(); |
michael@0 | 1641 | } |
michael@0 | 1642 | } |
michael@0 | 1643 | |
michael@0 | 1644 | function timeout() { |
michael@0 | 1645 | if (tests.timeout_length === null) |
michael@0 | 1646 | { |
michael@0 | 1647 | tests.timeout(); |
michael@0 | 1648 | } |
michael@0 | 1649 | } |
michael@0 | 1650 | expose(timeout, 'timeout'); |
michael@0 | 1651 | |
michael@0 | 1652 | function add_start_callback(callback) { |
michael@0 | 1653 | tests.start_callbacks.push(callback); |
michael@0 | 1654 | } |
michael@0 | 1655 | |
michael@0 | 1656 | function add_result_callback(callback) |
michael@0 | 1657 | { |
michael@0 | 1658 | tests.test_done_callbacks.push(callback); |
michael@0 | 1659 | } |
michael@0 | 1660 | |
michael@0 | 1661 | function add_completion_callback(callback) |
michael@0 | 1662 | { |
michael@0 | 1663 | tests.all_done_callbacks.push(callback); |
michael@0 | 1664 | } |
michael@0 | 1665 | |
michael@0 | 1666 | expose(add_start_callback, 'add_start_callback'); |
michael@0 | 1667 | expose(add_result_callback, 'add_result_callback'); |
michael@0 | 1668 | expose(add_completion_callback, 'add_completion_callback'); |
michael@0 | 1669 | |
michael@0 | 1670 | /* |
michael@0 | 1671 | * Output listener |
michael@0 | 1672 | */ |
michael@0 | 1673 | |
michael@0 | 1674 | function Output() { |
michael@0 | 1675 | this.output_document = document; |
michael@0 | 1676 | this.output_node = null; |
michael@0 | 1677 | this.done_count = 0; |
michael@0 | 1678 | this.enabled = settings.output; |
michael@0 | 1679 | this.phase = this.INITIAL; |
michael@0 | 1680 | } |
michael@0 | 1681 | |
michael@0 | 1682 | Output.prototype.INITIAL = 0; |
michael@0 | 1683 | Output.prototype.STARTED = 1; |
michael@0 | 1684 | Output.prototype.HAVE_RESULTS = 2; |
michael@0 | 1685 | Output.prototype.COMPLETE = 3; |
michael@0 | 1686 | |
michael@0 | 1687 | Output.prototype.setup = function(properties) { |
michael@0 | 1688 | if (this.phase > this.INITIAL) { |
michael@0 | 1689 | return; |
michael@0 | 1690 | } |
michael@0 | 1691 | |
michael@0 | 1692 | //If output is disabled in testharnessreport.js the test shouldn't be |
michael@0 | 1693 | //able to override that |
michael@0 | 1694 | this.enabled = this.enabled && (properties.hasOwnProperty("output") ? |
michael@0 | 1695 | properties.output : settings.output); |
michael@0 | 1696 | }; |
michael@0 | 1697 | |
michael@0 | 1698 | Output.prototype.init = function(properties) |
michael@0 | 1699 | { |
michael@0 | 1700 | if (this.phase >= this.STARTED) { |
michael@0 | 1701 | return; |
michael@0 | 1702 | } |
michael@0 | 1703 | if (properties.output_document) { |
michael@0 | 1704 | this.output_document = properties.output_document; |
michael@0 | 1705 | } else { |
michael@0 | 1706 | this.output_document = document; |
michael@0 | 1707 | } |
michael@0 | 1708 | this.phase = this.STARTED; |
michael@0 | 1709 | }; |
michael@0 | 1710 | |
michael@0 | 1711 | Output.prototype.resolve_log = function() |
michael@0 | 1712 | { |
michael@0 | 1713 | var output_document; |
michael@0 | 1714 | if (typeof this.output_document === "function") |
michael@0 | 1715 | { |
michael@0 | 1716 | output_document = this.output_document.apply(undefined); |
michael@0 | 1717 | } else |
michael@0 | 1718 | { |
michael@0 | 1719 | output_document = this.output_document; |
michael@0 | 1720 | } |
michael@0 | 1721 | if (!output_document) |
michael@0 | 1722 | { |
michael@0 | 1723 | return; |
michael@0 | 1724 | } |
michael@0 | 1725 | var node = output_document.getElementById("log"); |
michael@0 | 1726 | if (node) |
michael@0 | 1727 | { |
michael@0 | 1728 | this.output_document = output_document; |
michael@0 | 1729 | this.output_node = node; |
michael@0 | 1730 | } |
michael@0 | 1731 | }; |
michael@0 | 1732 | |
michael@0 | 1733 | Output.prototype.show_status = function(test) |
michael@0 | 1734 | { |
michael@0 | 1735 | if (this.phase < this.STARTED) |
michael@0 | 1736 | { |
michael@0 | 1737 | this.init(); |
michael@0 | 1738 | } |
michael@0 | 1739 | if (!this.enabled) |
michael@0 | 1740 | { |
michael@0 | 1741 | return; |
michael@0 | 1742 | } |
michael@0 | 1743 | if (this.phase < this.HAVE_RESULTS) |
michael@0 | 1744 | { |
michael@0 | 1745 | this.resolve_log(); |
michael@0 | 1746 | this.phase = this.HAVE_RESULTS; |
michael@0 | 1747 | } |
michael@0 | 1748 | this.done_count++; |
michael@0 | 1749 | if (this.output_node) |
michael@0 | 1750 | { |
michael@0 | 1751 | if (this.done_count < 100 |
michael@0 | 1752 | || (this.done_count < 1000 && this.done_count % 100 == 0) |
michael@0 | 1753 | || this.done_count % 1000 == 0) { |
michael@0 | 1754 | this.output_node.textContent = "Running, " |
michael@0 | 1755 | + this.done_count + " complete, " |
michael@0 | 1756 | + tests.num_pending + " remain"; |
michael@0 | 1757 | } |
michael@0 | 1758 | } |
michael@0 | 1759 | }; |
michael@0 | 1760 | |
michael@0 | 1761 | Output.prototype.show_results = function (tests, harness_status) |
michael@0 | 1762 | { |
michael@0 | 1763 | if (this.phase >= this.COMPLETE) { |
michael@0 | 1764 | return; |
michael@0 | 1765 | } |
michael@0 | 1766 | if (!this.enabled) |
michael@0 | 1767 | { |
michael@0 | 1768 | return; |
michael@0 | 1769 | } |
michael@0 | 1770 | if (!this.output_node) { |
michael@0 | 1771 | this.resolve_log(); |
michael@0 | 1772 | } |
michael@0 | 1773 | this.phase = this.COMPLETE; |
michael@0 | 1774 | |
michael@0 | 1775 | var log = this.output_node; |
michael@0 | 1776 | if (!log) |
michael@0 | 1777 | { |
michael@0 | 1778 | return; |
michael@0 | 1779 | } |
michael@0 | 1780 | var output_document = this.output_document; |
michael@0 | 1781 | |
michael@0 | 1782 | while (log.lastChild) |
michael@0 | 1783 | { |
michael@0 | 1784 | log.removeChild(log.lastChild); |
michael@0 | 1785 | } |
michael@0 | 1786 | |
michael@0 | 1787 | if (script_prefix != null) { |
michael@0 | 1788 | var stylesheet = output_document.createElementNS(xhtml_ns, "link"); |
michael@0 | 1789 | stylesheet.setAttribute("rel", "stylesheet"); |
michael@0 | 1790 | stylesheet.setAttribute("href", script_prefix + "testharness.css"); |
michael@0 | 1791 | var heads = output_document.getElementsByTagName("head"); |
michael@0 | 1792 | if (heads.length) { |
michael@0 | 1793 | heads[0].appendChild(stylesheet); |
michael@0 | 1794 | } |
michael@0 | 1795 | } |
michael@0 | 1796 | |
michael@0 | 1797 | var status_text_harness = {}; |
michael@0 | 1798 | status_text_harness[harness_status.OK] = "OK"; |
michael@0 | 1799 | status_text_harness[harness_status.ERROR] = "Error"; |
michael@0 | 1800 | status_text_harness[harness_status.TIMEOUT] = "Timeout"; |
michael@0 | 1801 | |
michael@0 | 1802 | var status_text = {}; |
michael@0 | 1803 | status_text[Test.prototype.PASS] = "Pass"; |
michael@0 | 1804 | status_text[Test.prototype.FAIL] = "Fail"; |
michael@0 | 1805 | status_text[Test.prototype.TIMEOUT] = "Timeout"; |
michael@0 | 1806 | status_text[Test.prototype.NOTRUN] = "Not Run"; |
michael@0 | 1807 | |
michael@0 | 1808 | var status_number = {}; |
michael@0 | 1809 | forEach(tests, function(test) { |
michael@0 | 1810 | var status = status_text[test.status]; |
michael@0 | 1811 | if (status_number.hasOwnProperty(status)) |
michael@0 | 1812 | { |
michael@0 | 1813 | status_number[status] += 1; |
michael@0 | 1814 | } else { |
michael@0 | 1815 | status_number[status] = 1; |
michael@0 | 1816 | } |
michael@0 | 1817 | }); |
michael@0 | 1818 | |
michael@0 | 1819 | function status_class(status) |
michael@0 | 1820 | { |
michael@0 | 1821 | return status.replace(/\s/g, '').toLowerCase(); |
michael@0 | 1822 | } |
michael@0 | 1823 | |
michael@0 | 1824 | var summary_template = ["section", {"id":"summary"}, |
michael@0 | 1825 | ["h2", {}, "Summary"], |
michael@0 | 1826 | function(vars) |
michael@0 | 1827 | { |
michael@0 | 1828 | if (harness_status.status === harness_status.OK) |
michael@0 | 1829 | { |
michael@0 | 1830 | return null; |
michael@0 | 1831 | } |
michael@0 | 1832 | else |
michael@0 | 1833 | { |
michael@0 | 1834 | var status = status_text_harness[harness_status.status]; |
michael@0 | 1835 | var rv = [["p", {"class":status_class(status)}]]; |
michael@0 | 1836 | |
michael@0 | 1837 | if (harness_status.status === harness_status.ERROR) |
michael@0 | 1838 | { |
michael@0 | 1839 | rv[0].push("Harness encountered an error:"); |
michael@0 | 1840 | rv.push(["pre", {}, harness_status.message]); |
michael@0 | 1841 | } |
michael@0 | 1842 | else if (harness_status.status === harness_status.TIMEOUT) |
michael@0 | 1843 | { |
michael@0 | 1844 | rv[0].push("Harness timed out."); |
michael@0 | 1845 | } |
michael@0 | 1846 | else |
michael@0 | 1847 | { |
michael@0 | 1848 | rv[0].push("Harness got an unexpected status."); |
michael@0 | 1849 | } |
michael@0 | 1850 | |
michael@0 | 1851 | return rv; |
michael@0 | 1852 | } |
michael@0 | 1853 | }, |
michael@0 | 1854 | ["p", {}, "Found ${num_tests} tests"], |
michael@0 | 1855 | function(vars) { |
michael@0 | 1856 | var rv = [["div", {}]]; |
michael@0 | 1857 | var i=0; |
michael@0 | 1858 | while (status_text.hasOwnProperty(i)) { |
michael@0 | 1859 | if (status_number.hasOwnProperty(status_text[i])) { |
michael@0 | 1860 | var status = status_text[i]; |
michael@0 | 1861 | rv[0].push(["div", {"class":status_class(status)}, |
michael@0 | 1862 | ["label", {}, |
michael@0 | 1863 | ["input", {type:"checkbox", checked:"checked"}], |
michael@0 | 1864 | status_number[status] + " " + status]]); |
michael@0 | 1865 | } |
michael@0 | 1866 | i++; |
michael@0 | 1867 | } |
michael@0 | 1868 | return rv; |
michael@0 | 1869 | }]; |
michael@0 | 1870 | |
michael@0 | 1871 | log.appendChild(render(summary_template, {num_tests:tests.length}, output_document)); |
michael@0 | 1872 | |
michael@0 | 1873 | forEach(output_document.querySelectorAll("section#summary label"), |
michael@0 | 1874 | function(element) |
michael@0 | 1875 | { |
michael@0 | 1876 | on_event(element, "click", |
michael@0 | 1877 | function(e) |
michael@0 | 1878 | { |
michael@0 | 1879 | if (output_document.getElementById("results") === null) |
michael@0 | 1880 | { |
michael@0 | 1881 | e.preventDefault(); |
michael@0 | 1882 | return; |
michael@0 | 1883 | } |
michael@0 | 1884 | var result_class = element.parentNode.getAttribute("class"); |
michael@0 | 1885 | var style_element = output_document.querySelector("style#hide-" + result_class); |
michael@0 | 1886 | var input_element = element.querySelector("input"); |
michael@0 | 1887 | if (!style_element && !input_element.checked) { |
michael@0 | 1888 | style_element = output_document.createElementNS(xhtml_ns, "style"); |
michael@0 | 1889 | style_element.id = "hide-" + result_class; |
michael@0 | 1890 | style_element.textContent = "table#results > tbody > tr."+result_class+"{display:none}"; |
michael@0 | 1891 | output_document.body.appendChild(style_element); |
michael@0 | 1892 | } else if (style_element && input_element.checked) { |
michael@0 | 1893 | style_element.parentNode.removeChild(style_element); |
michael@0 | 1894 | } |
michael@0 | 1895 | }); |
michael@0 | 1896 | }); |
michael@0 | 1897 | |
michael@0 | 1898 | // This use of innerHTML plus manual escaping is not recommended in |
michael@0 | 1899 | // general, but is necessary here for performance. Using textContent |
michael@0 | 1900 | // on each individual <td> adds tens of seconds of execution time for |
michael@0 | 1901 | // large test suites (tens of thousands of tests). |
michael@0 | 1902 | function escape_html(s) |
michael@0 | 1903 | { |
michael@0 | 1904 | return s.replace(/\&/g, "&") |
michael@0 | 1905 | .replace(/</g, "<") |
michael@0 | 1906 | .replace(/"/g, """) |
michael@0 | 1907 | .replace(/'/g, "'"); |
michael@0 | 1908 | } |
michael@0 | 1909 | |
michael@0 | 1910 | function has_assertions() |
michael@0 | 1911 | { |
michael@0 | 1912 | for (var i = 0; i < tests.length; i++) { |
michael@0 | 1913 | if (tests[i].properties.hasOwnProperty("assert")) { |
michael@0 | 1914 | return true; |
michael@0 | 1915 | } |
michael@0 | 1916 | } |
michael@0 | 1917 | return false; |
michael@0 | 1918 | } |
michael@0 | 1919 | |
michael@0 | 1920 | function get_assertion(test) |
michael@0 | 1921 | { |
michael@0 | 1922 | if (test.properties.hasOwnProperty("assert")) { |
michael@0 | 1923 | if (Array.isArray(test.properties.assert)) { |
michael@0 | 1924 | return test.properties.assert.join(' '); |
michael@0 | 1925 | } |
michael@0 | 1926 | return test.properties.assert; |
michael@0 | 1927 | } |
michael@0 | 1928 | return ''; |
michael@0 | 1929 | } |
michael@0 | 1930 | |
michael@0 | 1931 | log.appendChild(document.createElementNS(xhtml_ns, "section")); |
michael@0 | 1932 | var assertions = has_assertions(); |
michael@0 | 1933 | var html = "<h2>Details</h2><table id='results' " + (assertions ? "class='assertions'" : "" ) + ">" |
michael@0 | 1934 | + "<thead><tr><th>Result</th><th>Test Name</th>" |
michael@0 | 1935 | + (assertions ? "<th>Assertion</th>" : "") |
michael@0 | 1936 | + "<th>Message</th></tr></thead>" |
michael@0 | 1937 | + "<tbody>"; |
michael@0 | 1938 | for (var i = 0; i < tests.length; i++) { |
michael@0 | 1939 | html += '<tr class="' |
michael@0 | 1940 | + escape_html(status_class(status_text[tests[i].status])) |
michael@0 | 1941 | + '"><td>' |
michael@0 | 1942 | + escape_html(status_text[tests[i].status]) |
michael@0 | 1943 | + "</td><td>" |
michael@0 | 1944 | + escape_html(tests[i].name) |
michael@0 | 1945 | + "</td><td>" |
michael@0 | 1946 | + (assertions ? escape_html(get_assertion(tests[i])) + "</td><td>" : "") |
michael@0 | 1947 | + escape_html(tests[i].message ? tests[i].message : " ") |
michael@0 | 1948 | + "</td></tr>"; |
michael@0 | 1949 | } |
michael@0 | 1950 | html += "</tbody></table>"; |
michael@0 | 1951 | try { |
michael@0 | 1952 | log.lastChild.innerHTML = html; |
michael@0 | 1953 | } catch (e) { |
michael@0 | 1954 | log.appendChild(document.createElementNS(xhtml_ns, "p")) |
michael@0 | 1955 | .textContent = "Setting innerHTML for the log threw an exception."; |
michael@0 | 1956 | log.appendChild(document.createElementNS(xhtml_ns, "pre")) |
michael@0 | 1957 | .textContent = html; |
michael@0 | 1958 | } |
michael@0 | 1959 | }; |
michael@0 | 1960 | |
michael@0 | 1961 | var output = new Output(); |
michael@0 | 1962 | add_start_callback(function (properties) {output.init(properties);}); |
michael@0 | 1963 | add_result_callback(function (test) {output.show_status(tests);}); |
michael@0 | 1964 | add_completion_callback(function (tests, harness_status) {output.show_results(tests, harness_status);}); |
michael@0 | 1965 | |
michael@0 | 1966 | /* |
michael@0 | 1967 | * Template code |
michael@0 | 1968 | * |
michael@0 | 1969 | * A template is just a javascript structure. An element is represented as: |
michael@0 | 1970 | * |
michael@0 | 1971 | * [tag_name, {attr_name:attr_value}, child1, child2] |
michael@0 | 1972 | * |
michael@0 | 1973 | * the children can either be strings (which act like text nodes), other templates or |
michael@0 | 1974 | * functions (see below) |
michael@0 | 1975 | * |
michael@0 | 1976 | * A text node is represented as |
michael@0 | 1977 | * |
michael@0 | 1978 | * ["{text}", value] |
michael@0 | 1979 | * |
michael@0 | 1980 | * String values have a simple substitution syntax; ${foo} represents a variable foo. |
michael@0 | 1981 | * |
michael@0 | 1982 | * It is possible to embed logic in templates by using a function in a place where a |
michael@0 | 1983 | * node would usually go. The function must either return part of a template or null. |
michael@0 | 1984 | * |
michael@0 | 1985 | * In cases where a set of nodes are required as output rather than a single node |
michael@0 | 1986 | * with children it is possible to just use a list |
michael@0 | 1987 | * [node1, node2, node3] |
michael@0 | 1988 | * |
michael@0 | 1989 | * Usage: |
michael@0 | 1990 | * |
michael@0 | 1991 | * render(template, substitutions) - take a template and an object mapping |
michael@0 | 1992 | * variable names to parameters and return either a DOM node or a list of DOM nodes |
michael@0 | 1993 | * |
michael@0 | 1994 | * substitute(template, substitutions) - take a template and variable mapping object, |
michael@0 | 1995 | * make the variable substitutions and return the substituted template |
michael@0 | 1996 | * |
michael@0 | 1997 | */ |
michael@0 | 1998 | |
michael@0 | 1999 | function is_single_node(template) |
michael@0 | 2000 | { |
michael@0 | 2001 | return typeof template[0] === "string"; |
michael@0 | 2002 | } |
michael@0 | 2003 | |
michael@0 | 2004 | function substitute(template, substitutions) |
michael@0 | 2005 | { |
michael@0 | 2006 | if (typeof template === "function") { |
michael@0 | 2007 | var replacement = template(substitutions); |
michael@0 | 2008 | if (replacement) |
michael@0 | 2009 | { |
michael@0 | 2010 | var rv = substitute(replacement, substitutions); |
michael@0 | 2011 | return rv; |
michael@0 | 2012 | } |
michael@0 | 2013 | else |
michael@0 | 2014 | { |
michael@0 | 2015 | return null; |
michael@0 | 2016 | } |
michael@0 | 2017 | } |
michael@0 | 2018 | else if (is_single_node(template)) |
michael@0 | 2019 | { |
michael@0 | 2020 | return substitute_single(template, substitutions); |
michael@0 | 2021 | } |
michael@0 | 2022 | else |
michael@0 | 2023 | { |
michael@0 | 2024 | return filter(map(template, function(x) { |
michael@0 | 2025 | return substitute(x, substitutions); |
michael@0 | 2026 | }), function(x) {return x !== null;}); |
michael@0 | 2027 | } |
michael@0 | 2028 | } |
michael@0 | 2029 | |
michael@0 | 2030 | function substitute_single(template, substitutions) |
michael@0 | 2031 | { |
michael@0 | 2032 | var substitution_re = /\${([^ }]*)}/g; |
michael@0 | 2033 | |
michael@0 | 2034 | function do_substitution(input) { |
michael@0 | 2035 | var components = input.split(substitution_re); |
michael@0 | 2036 | var rv = []; |
michael@0 | 2037 | for (var i=0; i<components.length; i+=2) |
michael@0 | 2038 | { |
michael@0 | 2039 | rv.push(components[i]); |
michael@0 | 2040 | if (components[i+1]) |
michael@0 | 2041 | { |
michael@0 | 2042 | rv.push(String(substitutions[components[i+1]])); |
michael@0 | 2043 | } |
michael@0 | 2044 | } |
michael@0 | 2045 | return rv; |
michael@0 | 2046 | } |
michael@0 | 2047 | |
michael@0 | 2048 | var rv = []; |
michael@0 | 2049 | rv.push(do_substitution(String(template[0])).join("")); |
michael@0 | 2050 | |
michael@0 | 2051 | if (template[0] === "{text}") { |
michael@0 | 2052 | substitute_children(template.slice(1), rv); |
michael@0 | 2053 | } else { |
michael@0 | 2054 | substitute_attrs(template[1], rv); |
michael@0 | 2055 | substitute_children(template.slice(2), rv); |
michael@0 | 2056 | } |
michael@0 | 2057 | |
michael@0 | 2058 | function substitute_attrs(attrs, rv) |
michael@0 | 2059 | { |
michael@0 | 2060 | rv[1] = {}; |
michael@0 | 2061 | for (var name in template[1]) |
michael@0 | 2062 | { |
michael@0 | 2063 | if (attrs.hasOwnProperty(name)) |
michael@0 | 2064 | { |
michael@0 | 2065 | var new_name = do_substitution(name).join(""); |
michael@0 | 2066 | var new_value = do_substitution(attrs[name]).join(""); |
michael@0 | 2067 | rv[1][new_name] = new_value; |
michael@0 | 2068 | }; |
michael@0 | 2069 | } |
michael@0 | 2070 | } |
michael@0 | 2071 | |
michael@0 | 2072 | function substitute_children(children, rv) |
michael@0 | 2073 | { |
michael@0 | 2074 | for (var i=0; i<children.length; i++) |
michael@0 | 2075 | { |
michael@0 | 2076 | if (children[i] instanceof Object) { |
michael@0 | 2077 | var replacement = substitute(children[i], substitutions); |
michael@0 | 2078 | if (replacement !== null) |
michael@0 | 2079 | { |
michael@0 | 2080 | if (is_single_node(replacement)) |
michael@0 | 2081 | { |
michael@0 | 2082 | rv.push(replacement); |
michael@0 | 2083 | } |
michael@0 | 2084 | else |
michael@0 | 2085 | { |
michael@0 | 2086 | extend(rv, replacement); |
michael@0 | 2087 | } |
michael@0 | 2088 | } |
michael@0 | 2089 | } |
michael@0 | 2090 | else |
michael@0 | 2091 | { |
michael@0 | 2092 | extend(rv, do_substitution(String(children[i]))); |
michael@0 | 2093 | } |
michael@0 | 2094 | } |
michael@0 | 2095 | return rv; |
michael@0 | 2096 | } |
michael@0 | 2097 | |
michael@0 | 2098 | return rv; |
michael@0 | 2099 | } |
michael@0 | 2100 | |
michael@0 | 2101 | function make_dom_single(template, doc) |
michael@0 | 2102 | { |
michael@0 | 2103 | var output_document = doc || document; |
michael@0 | 2104 | if (template[0] === "{text}") |
michael@0 | 2105 | { |
michael@0 | 2106 | var element = output_document.createTextNode(""); |
michael@0 | 2107 | for (var i=1; i<template.length; i++) |
michael@0 | 2108 | { |
michael@0 | 2109 | element.data += template[i]; |
michael@0 | 2110 | } |
michael@0 | 2111 | } |
michael@0 | 2112 | else |
michael@0 | 2113 | { |
michael@0 | 2114 | var element = output_document.createElementNS(xhtml_ns, template[0]); |
michael@0 | 2115 | for (var name in template[1]) { |
michael@0 | 2116 | if (template[1].hasOwnProperty(name)) |
michael@0 | 2117 | { |
michael@0 | 2118 | element.setAttribute(name, template[1][name]); |
michael@0 | 2119 | } |
michael@0 | 2120 | } |
michael@0 | 2121 | for (var i=2; i<template.length; i++) |
michael@0 | 2122 | { |
michael@0 | 2123 | if (template[i] instanceof Object) |
michael@0 | 2124 | { |
michael@0 | 2125 | var sub_element = make_dom(template[i]); |
michael@0 | 2126 | element.appendChild(sub_element); |
michael@0 | 2127 | } |
michael@0 | 2128 | else |
michael@0 | 2129 | { |
michael@0 | 2130 | var text_node = output_document.createTextNode(template[i]); |
michael@0 | 2131 | element.appendChild(text_node); |
michael@0 | 2132 | } |
michael@0 | 2133 | } |
michael@0 | 2134 | } |
michael@0 | 2135 | |
michael@0 | 2136 | return element; |
michael@0 | 2137 | } |
michael@0 | 2138 | |
michael@0 | 2139 | |
michael@0 | 2140 | |
michael@0 | 2141 | function make_dom(template, substitutions, output_document) |
michael@0 | 2142 | { |
michael@0 | 2143 | if (is_single_node(template)) |
michael@0 | 2144 | { |
michael@0 | 2145 | return make_dom_single(template, output_document); |
michael@0 | 2146 | } |
michael@0 | 2147 | else |
michael@0 | 2148 | { |
michael@0 | 2149 | return map(template, function(x) { |
michael@0 | 2150 | return make_dom_single(x, output_document); |
michael@0 | 2151 | }); |
michael@0 | 2152 | } |
michael@0 | 2153 | } |
michael@0 | 2154 | |
michael@0 | 2155 | function render(template, substitutions, output_document) |
michael@0 | 2156 | { |
michael@0 | 2157 | return make_dom(substitute(template, substitutions), output_document); |
michael@0 | 2158 | } |
michael@0 | 2159 | |
michael@0 | 2160 | /* |
michael@0 | 2161 | * Utility funcions |
michael@0 | 2162 | */ |
michael@0 | 2163 | function assert(expected_true, function_name, description, error, substitutions) |
michael@0 | 2164 | { |
michael@0 | 2165 | if (expected_true !== true) |
michael@0 | 2166 | { |
michael@0 | 2167 | throw new AssertionError(make_message(function_name, description, |
michael@0 | 2168 | error, substitutions)); |
michael@0 | 2169 | } |
michael@0 | 2170 | } |
michael@0 | 2171 | |
michael@0 | 2172 | function AssertionError(message) |
michael@0 | 2173 | { |
michael@0 | 2174 | this.message = message; |
michael@0 | 2175 | } |
michael@0 | 2176 | |
michael@0 | 2177 | function make_message(function_name, description, error, substitutions) |
michael@0 | 2178 | { |
michael@0 | 2179 | for (var p in substitutions) { |
michael@0 | 2180 | if (substitutions.hasOwnProperty(p)) { |
michael@0 | 2181 | substitutions[p] = format_value(substitutions[p]); |
michael@0 | 2182 | } |
michael@0 | 2183 | } |
michael@0 | 2184 | var node_form = substitute(["{text}", "${function_name}: ${description}" + error], |
michael@0 | 2185 | merge({function_name:function_name, |
michael@0 | 2186 | description:(description?description + " ":"")}, |
michael@0 | 2187 | substitutions)); |
michael@0 | 2188 | return node_form.slice(1).join(""); |
michael@0 | 2189 | } |
michael@0 | 2190 | |
michael@0 | 2191 | function filter(array, callable, thisObj) { |
michael@0 | 2192 | var rv = []; |
michael@0 | 2193 | for (var i=0; i<array.length; i++) |
michael@0 | 2194 | { |
michael@0 | 2195 | if (array.hasOwnProperty(i)) |
michael@0 | 2196 | { |
michael@0 | 2197 | var pass = callable.call(thisObj, array[i], i, array); |
michael@0 | 2198 | if (pass) { |
michael@0 | 2199 | rv.push(array[i]); |
michael@0 | 2200 | } |
michael@0 | 2201 | } |
michael@0 | 2202 | } |
michael@0 | 2203 | return rv; |
michael@0 | 2204 | } |
michael@0 | 2205 | |
michael@0 | 2206 | function map(array, callable, thisObj) |
michael@0 | 2207 | { |
michael@0 | 2208 | var rv = []; |
michael@0 | 2209 | rv.length = array.length; |
michael@0 | 2210 | for (var i=0; i<array.length; i++) |
michael@0 | 2211 | { |
michael@0 | 2212 | if (array.hasOwnProperty(i)) |
michael@0 | 2213 | { |
michael@0 | 2214 | rv[i] = callable.call(thisObj, array[i], i, array); |
michael@0 | 2215 | } |
michael@0 | 2216 | } |
michael@0 | 2217 | return rv; |
michael@0 | 2218 | } |
michael@0 | 2219 | |
michael@0 | 2220 | function extend(array, items) |
michael@0 | 2221 | { |
michael@0 | 2222 | Array.prototype.push.apply(array, items); |
michael@0 | 2223 | } |
michael@0 | 2224 | |
michael@0 | 2225 | function forEach (array, callback, thisObj) |
michael@0 | 2226 | { |
michael@0 | 2227 | for (var i=0; i<array.length; i++) |
michael@0 | 2228 | { |
michael@0 | 2229 | if (array.hasOwnProperty(i)) |
michael@0 | 2230 | { |
michael@0 | 2231 | callback.call(thisObj, array[i], i, array); |
michael@0 | 2232 | } |
michael@0 | 2233 | } |
michael@0 | 2234 | } |
michael@0 | 2235 | |
michael@0 | 2236 | function merge(a,b) |
michael@0 | 2237 | { |
michael@0 | 2238 | var rv = {}; |
michael@0 | 2239 | var p; |
michael@0 | 2240 | for (p in a) |
michael@0 | 2241 | { |
michael@0 | 2242 | rv[p] = a[p]; |
michael@0 | 2243 | } |
michael@0 | 2244 | for (p in b) { |
michael@0 | 2245 | rv[p] = b[p]; |
michael@0 | 2246 | } |
michael@0 | 2247 | return rv; |
michael@0 | 2248 | } |
michael@0 | 2249 | |
michael@0 | 2250 | function expose(object, name) |
michael@0 | 2251 | { |
michael@0 | 2252 | var components = name.split("."); |
michael@0 | 2253 | var target = window; |
michael@0 | 2254 | for (var i=0; i<components.length - 1; i++) |
michael@0 | 2255 | { |
michael@0 | 2256 | if (!(components[i] in target)) |
michael@0 | 2257 | { |
michael@0 | 2258 | target[components[i]] = {}; |
michael@0 | 2259 | } |
michael@0 | 2260 | target = target[components[i]]; |
michael@0 | 2261 | } |
michael@0 | 2262 | target[components[components.length - 1]] = object; |
michael@0 | 2263 | } |
michael@0 | 2264 | |
michael@0 | 2265 | function forEach_windows(callback) { |
michael@0 | 2266 | // Iterate of the the windows [self ... top, opener]. The callback is passed |
michael@0 | 2267 | // two objects, the first one is the windows object itself, the second one |
michael@0 | 2268 | // is a boolean indicating whether or not its on the same origin as the |
michael@0 | 2269 | // current window. |
michael@0 | 2270 | var cache = forEach_windows.result_cache; |
michael@0 | 2271 | if (!cache) { |
michael@0 | 2272 | cache = [[self, true]]; |
michael@0 | 2273 | var w = self; |
michael@0 | 2274 | var i = 0; |
michael@0 | 2275 | var so; |
michael@0 | 2276 | var origins = location.ancestorOrigins; |
michael@0 | 2277 | while (w != w.parent) |
michael@0 | 2278 | { |
michael@0 | 2279 | w = w.parent; |
michael@0 | 2280 | // In WebKit, calls to parent windows' properties that aren't on the same |
michael@0 | 2281 | // origin cause an error message to be displayed in the error console but |
michael@0 | 2282 | // don't throw an exception. This is a deviation from the current HTML5 |
michael@0 | 2283 | // spec. See: https://bugs.webkit.org/show_bug.cgi?id=43504 |
michael@0 | 2284 | // The problem with WebKit's behavior is that it pollutes the error console |
michael@0 | 2285 | // with error messages that can't be caught. |
michael@0 | 2286 | // |
michael@0 | 2287 | // This issue can be mitigated by relying on the (for now) proprietary |
michael@0 | 2288 | // `location.ancestorOrigins` property which returns an ordered list of |
michael@0 | 2289 | // the origins of enclosing windows. See: |
michael@0 | 2290 | // http://trac.webkit.org/changeset/113945. |
michael@0 | 2291 | if(origins) { |
michael@0 | 2292 | so = (location.origin == origins[i]); |
michael@0 | 2293 | } |
michael@0 | 2294 | else |
michael@0 | 2295 | { |
michael@0 | 2296 | so = is_same_origin(w); |
michael@0 | 2297 | } |
michael@0 | 2298 | cache.push([w, so]); |
michael@0 | 2299 | i++; |
michael@0 | 2300 | } |
michael@0 | 2301 | w = window.opener; |
michael@0 | 2302 | if(w) |
michael@0 | 2303 | { |
michael@0 | 2304 | // window.opener isn't included in the `location.ancestorOrigins` prop. |
michael@0 | 2305 | // We'll just have to deal with a simple check and an error msg on WebKit |
michael@0 | 2306 | // browsers in this case. |
michael@0 | 2307 | cache.push([w, is_same_origin(w)]); |
michael@0 | 2308 | } |
michael@0 | 2309 | forEach_windows.result_cache = cache; |
michael@0 | 2310 | } |
michael@0 | 2311 | |
michael@0 | 2312 | forEach(cache, |
michael@0 | 2313 | function(a) |
michael@0 | 2314 | { |
michael@0 | 2315 | callback.apply(null, a); |
michael@0 | 2316 | }); |
michael@0 | 2317 | } |
michael@0 | 2318 | |
michael@0 | 2319 | function is_same_origin(w) { |
michael@0 | 2320 | try { |
michael@0 | 2321 | 'random_prop' in w; |
michael@0 | 2322 | return true; |
michael@0 | 2323 | } catch(e) { |
michael@0 | 2324 | return false; |
michael@0 | 2325 | } |
michael@0 | 2326 | } |
michael@0 | 2327 | |
michael@0 | 2328 | function supports_post_message(w) |
michael@0 | 2329 | { |
michael@0 | 2330 | var supports; |
michael@0 | 2331 | var type; |
michael@0 | 2332 | // Given IE implements postMessage across nested iframes but not across |
michael@0 | 2333 | // windows or tabs, you can't infer cross-origin communication from the presence |
michael@0 | 2334 | // of postMessage on the current window object only. |
michael@0 | 2335 | // |
michael@0 | 2336 | // Touching the postMessage prop on a window can throw if the window is |
michael@0 | 2337 | // not from the same origin AND post message is not supported in that |
michael@0 | 2338 | // browser. So just doing an existence test here won't do, you also need |
michael@0 | 2339 | // to wrap it in a try..cacth block. |
michael@0 | 2340 | try |
michael@0 | 2341 | { |
michael@0 | 2342 | type = typeof w.postMessage; |
michael@0 | 2343 | if (type === "function") |
michael@0 | 2344 | { |
michael@0 | 2345 | supports = true; |
michael@0 | 2346 | } |
michael@0 | 2347 | // IE8 supports postMessage, but implements it as a host object which |
michael@0 | 2348 | // returns "object" as its `typeof`. |
michael@0 | 2349 | else if (type === "object") |
michael@0 | 2350 | { |
michael@0 | 2351 | supports = true; |
michael@0 | 2352 | } |
michael@0 | 2353 | // This is the case where postMessage isn't supported AND accessing a |
michael@0 | 2354 | // window property across origins does NOT throw (e.g. old Safari browser). |
michael@0 | 2355 | else |
michael@0 | 2356 | { |
michael@0 | 2357 | supports = false; |
michael@0 | 2358 | } |
michael@0 | 2359 | } |
michael@0 | 2360 | catch(e) { |
michael@0 | 2361 | // This is the case where postMessage isn't supported AND accessing a |
michael@0 | 2362 | // window property across origins throws (e.g. old Firefox browser). |
michael@0 | 2363 | supports = false; |
michael@0 | 2364 | } |
michael@0 | 2365 | return supports; |
michael@0 | 2366 | } |
michael@0 | 2367 | })(); |
michael@0 | 2368 | // vim: set expandtab shiftwidth=4 tabstop=4: |