| OLD | NEW |
| (Empty) |
| 1 /*global self*/ | |
| 2 /*jshint latedef: nofunc*/ | |
| 3 /* | |
| 4 Distributed under both the W3C Test Suite License [1] and the W3C | |
| 5 3-clause BSD License [2]. To contribute to a W3C Test Suite, see the | |
| 6 policies and contribution forms [3]. | |
| 7 | |
| 8 [1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license | |
| 9 [2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license | |
| 10 [3] http://www.w3.org/2004/10/27-testcases | |
| 11 */ | |
| 12 | |
| 13 /* Documentation is in docs/api.md */ | |
| 14 | |
| 15 (function () | |
| 16 { | |
| 17 var debug = false; | |
| 18 // default timeout is 10 seconds, test can override if needed | |
| 19 var settings = { | |
| 20 output:true, | |
| 21 harness_timeout:{ | |
| 22 "normal":10000, | |
| 23 "long":60000 | |
| 24 }, | |
| 25 test_timeout:null, | |
| 26 message_events: ["start", "test_state", "result", "completion"] | |
| 27 }; | |
| 28 | |
| 29 var xhtml_ns = "http://www.w3.org/1999/xhtml"; | |
| 30 | |
| 31 /* | |
| 32 * TestEnvironment is an abstraction for the environment in which the test | |
| 33 * harness is used. Each implementation of a test environment has to provide | |
| 34 * the following interface: | |
| 35 * | |
| 36 * interface TestEnvironment { | |
| 37 * // Invoked after the global 'tests' object has been created and it's | |
| 38 * // safe to call add_*_callback() to register event handlers. | |
| 39 * void on_tests_ready(); | |
| 40 * | |
| 41 * // Invoked after setup() has been called to notify the test environment | |
| 42 * // of changes to the test harness properties. | |
| 43 * void on_new_harness_properties(object properties); | |
| 44 * | |
| 45 * // Should return a new unique default test name. | |
| 46 * DOMString next_default_test_name(); | |
| 47 * | |
| 48 * // Should return the test harness timeout duration in milliseconds. | |
| 49 * float test_timeout(); | |
| 50 * | |
| 51 * // Should return the global scope object. | |
| 52 * object global_scope(); | |
| 53 * }; | |
| 54 */ | |
| 55 | |
| 56 /* | |
| 57 * A test environment with a DOM. The global object is 'window'. By default | |
| 58 * test results are displayed in a table. Any parent windows receive | |
| 59 * callbacks or messages via postMessage() when test events occur. See | |
| 60 * apisample11.html and apisample12.html. | |
| 61 */ | |
| 62 function WindowTestEnvironment() { | |
| 63 this.name_counter = 0; | |
| 64 this.window_cache = null; | |
| 65 this.output_handler = null; | |
| 66 this.all_loaded = false; | |
| 67 var this_obj = this; | |
| 68 this.message_events = []; | |
| 69 | |
| 70 this.message_functions = { | |
| 71 start: [add_start_callback, remove_start_callback, | |
| 72 function (properties) { | |
| 73 this_obj._dispatch("start_callback", [properties], | |
| 74 {type: "start", properties: propertie
s}); | |
| 75 }], | |
| 76 | |
| 77 test_state: [add_test_state_callback, remove_test_state_callback, | |
| 78 function(test) { | |
| 79 this_obj._dispatch("test_state_callback", [test], | |
| 80 {type: "test_state", | |
| 81 test: test.structured_clone()})
; | |
| 82 }], | |
| 83 result: [add_result_callback, remove_result_callback, | |
| 84 function (test) { | |
| 85 this_obj.output_handler.show_status(); | |
| 86 this_obj._dispatch("result_callback", [test], | |
| 87 {type: "result", | |
| 88 test: test.structured_clone()}); | |
| 89 }], | |
| 90 completion: [add_completion_callback, remove_completion_callback, | |
| 91 function (tests, harness_status) { | |
| 92 var cloned_tests = map(tests, function(test) { | |
| 93 return test.structured_clone(); | |
| 94 }); | |
| 95 this_obj._dispatch("completion_callback", [tests, h
arness_status], | |
| 96 {type: "complete", | |
| 97 tests: cloned_tests, | |
| 98 status: harness_status.structur
ed_clone()}); | |
| 99 }] | |
| 100 } | |
| 101 | |
| 102 on_event(window, 'load', function() { | |
| 103 this_obj.all_loaded = true; | |
| 104 }); | |
| 105 } | |
| 106 | |
| 107 WindowTestEnvironment.prototype._dispatch = function(selector, callback_args
, message_arg) { | |
| 108 this._forEach_windows( | |
| 109 function(w, same_origin) { | |
| 110 if (same_origin) { | |
| 111 try { | |
| 112 var has_selector = selector in w; | |
| 113 } catch(e) { | |
| 114 // If document.domain was set at some point same_ori
gin can be | |
| 115 // wrong and the above will fail. | |
| 116 has_selector = false; | |
| 117 } | |
| 118 if (has_selector) { | |
| 119 try { | |
| 120 w[selector].apply(undefined, callback_args); | |
| 121 } catch (e) { | |
| 122 if (debug) { | |
| 123 throw e; | |
| 124 } | |
| 125 } | |
| 126 } | |
| 127 } | |
| 128 if (supports_post_message(w) && w !== self) { | |
| 129 w.postMessage(message_arg, "*"); | |
| 130 } | |
| 131 }); | |
| 132 }; | |
| 133 | |
| 134 WindowTestEnvironment.prototype._forEach_windows = function(callback) { | |
| 135 // Iterate of the the windows [self ... top, opener]. The callback is pa
ssed | |
| 136 // two objects, the first one is the windows object itself, the second o
ne | |
| 137 // is a boolean indicating whether or not its on the same origin as the | |
| 138 // current window. | |
| 139 var cache = this.window_cache; | |
| 140 if (!cache) { | |
| 141 cache = [[self, true]]; | |
| 142 var w = self; | |
| 143 var i = 0; | |
| 144 var so; | |
| 145 var origins = location.ancestorOrigins; | |
| 146 while (w != w.parent) { | |
| 147 w = w.parent; | |
| 148 // In WebKit, calls to parent windows' properties that aren't on
the same | |
| 149 // origin cause an error message to be displayed in the error co
nsole but | |
| 150 // don't throw an exception. This is a deviation from the curren
t HTML5 | |
| 151 // spec. See: https://bugs.webkit.org/show_bug.cgi?id=43504 | |
| 152 // The problem with WebKit's behavior is that it pollutes the er
ror console | |
| 153 // with error messages that can't be caught. | |
| 154 // | |
| 155 // This issue can be mitigated by relying on the (for now) propr
ietary | |
| 156 // `location.ancestorOrigins` property which returns an ordered
list of | |
| 157 // the origins of enclosing windows. See: | |
| 158 // http://trac.webkit.org/changeset/113945. | |
| 159 if (origins) { | |
| 160 so = (location.origin == origins[i]); | |
| 161 } else { | |
| 162 so = is_same_origin(w); | |
| 163 } | |
| 164 cache.push([w, so]); | |
| 165 i++; | |
| 166 } | |
| 167 w = window.opener; | |
| 168 if (w) { | |
| 169 // window.opener isn't included in the `location.ancestorOrigins
` prop. | |
| 170 // We'll just have to deal with a simple check and an error msg
on WebKit | |
| 171 // browsers in this case. | |
| 172 cache.push([w, is_same_origin(w)]); | |
| 173 } | |
| 174 this.window_cache = cache; | |
| 175 } | |
| 176 | |
| 177 forEach(cache, | |
| 178 function(a) { | |
| 179 callback.apply(null, a); | |
| 180 }); | |
| 181 }; | |
| 182 | |
| 183 WindowTestEnvironment.prototype.on_tests_ready = function() { | |
| 184 var output = new Output(); | |
| 185 this.output_handler = output; | |
| 186 | |
| 187 var this_obj = this; | |
| 188 | |
| 189 add_start_callback(function (properties) { | |
| 190 this_obj.output_handler.init(properties); | |
| 191 }); | |
| 192 | |
| 193 add_test_state_callback(function(test) { | |
| 194 this_obj.output_handler.show_status(); | |
| 195 }); | |
| 196 | |
| 197 add_result_callback(function (test) { | |
| 198 this_obj.output_handler.show_status(); | |
| 199 }); | |
| 200 | |
| 201 add_completion_callback(function (tests, harness_status) { | |
| 202 this_obj.output_handler.show_results(tests, harness_status); | |
| 203 }); | |
| 204 this.setup_messages(settings.message_events); | |
| 205 }; | |
| 206 | |
| 207 WindowTestEnvironment.prototype.setup_messages = function(new_events) { | |
| 208 var this_obj = this; | |
| 209 forEach(settings.message_events, function(x) { | |
| 210 var current_dispatch = this_obj.message_events.indexOf(x) !== -1; | |
| 211 var new_dispatch = new_events.indexOf(x) !== -1; | |
| 212 if (!current_dispatch && new_dispatch) { | |
| 213 this_obj.message_functions[x][0](this_obj.message_functions[x][2
]); | |
| 214 } else if (current_dispatch && !new_dispatch) { | |
| 215 this_obj.message_functions[x][1](this_obj.message_functions[x][2
]); | |
| 216 } | |
| 217 }); | |
| 218 this.message_events = new_events; | |
| 219 } | |
| 220 | |
| 221 WindowTestEnvironment.prototype.next_default_test_name = function() { | |
| 222 //Don't use document.title to work around an Opera bug in XHTML document
s | |
| 223 var title = document.getElementsByTagName("title")[0]; | |
| 224 var prefix = (title && title.firstChild && title.firstChild.data) || "Un
titled"; | |
| 225 var suffix = this.name_counter > 0 ? " " + this.name_counter : ""; | |
| 226 this.name_counter++; | |
| 227 return prefix + suffix; | |
| 228 }; | |
| 229 | |
| 230 WindowTestEnvironment.prototype.on_new_harness_properties = function(propert
ies) { | |
| 231 this.output_handler.setup(properties); | |
| 232 if (properties.hasOwnProperty("message_events")) { | |
| 233 this.setup_messages(properties.message_events); | |
| 234 } | |
| 235 }; | |
| 236 | |
| 237 WindowTestEnvironment.prototype.add_on_loaded_callback = function(callback)
{ | |
| 238 on_event(window, 'load', callback); | |
| 239 }; | |
| 240 | |
| 241 WindowTestEnvironment.prototype.test_timeout = function() { | |
| 242 var metas = document.getElementsByTagName("meta"); | |
| 243 for (var i = 0; i < metas.length; i++) { | |
| 244 if (metas[i].name == "timeout") { | |
| 245 if (metas[i].content == "long") { | |
| 246 return settings.harness_timeout.long; | |
| 247 } | |
| 248 break; | |
| 249 } | |
| 250 } | |
| 251 return settings.harness_timeout.normal; | |
| 252 }; | |
| 253 | |
| 254 WindowTestEnvironment.prototype.global_scope = function() { | |
| 255 return window; | |
| 256 }; | |
| 257 | |
| 258 /* | |
| 259 * Base TestEnvironment implementation for a generic web worker. | |
| 260 * | |
| 261 * Workers accumulate test results. One or more clients can connect and | |
| 262 * retrieve results from a worker at any time. | |
| 263 * | |
| 264 * WorkerTestEnvironment supports communicating with a client via a | |
| 265 * MessagePort. The mechanism for determining the appropriate MessagePort | |
| 266 * for communicating with a client depends on the type of worker and is | |
| 267 * implemented by the various specializations of WorkerTestEnvironment | |
| 268 * below. | |
| 269 * | |
| 270 * A client document using testharness can use fetch_tests_from_worker() to | |
| 271 * retrieve results from a worker. See apisample16.html. | |
| 272 */ | |
| 273 function WorkerTestEnvironment() { | |
| 274 this.name_counter = 0; | |
| 275 this.all_loaded = true; | |
| 276 this.message_list = []; | |
| 277 this.message_ports = []; | |
| 278 } | |
| 279 | |
| 280 WorkerTestEnvironment.prototype._dispatch = function(message) { | |
| 281 this.message_list.push(message); | |
| 282 for (var i = 0; i < this.message_ports.length; ++i) | |
| 283 { | |
| 284 this.message_ports[i].postMessage(message); | |
| 285 } | |
| 286 }; | |
| 287 | |
| 288 // The only requirement is that port has a postMessage() method. It doesn't | |
| 289 // have to be an instance of a MessagePort, and often isn't. | |
| 290 WorkerTestEnvironment.prototype._add_message_port = function(port) { | |
| 291 this.message_ports.push(port); | |
| 292 for (var i = 0; i < this.message_list.length; ++i) | |
| 293 { | |
| 294 port.postMessage(this.message_list[i]); | |
| 295 } | |
| 296 }; | |
| 297 | |
| 298 WorkerTestEnvironment.prototype.next_default_test_name = function() { | |
| 299 var suffix = this.name_counter > 0 ? " " + this.name_counter : ""; | |
| 300 this.name_counter++; | |
| 301 return "Untitled" + suffix; | |
| 302 }; | |
| 303 | |
| 304 WorkerTestEnvironment.prototype.on_new_harness_properties = function() {}; | |
| 305 | |
| 306 WorkerTestEnvironment.prototype.on_tests_ready = function() { | |
| 307 var this_obj = this; | |
| 308 add_start_callback( | |
| 309 function(properties) { | |
| 310 this_obj._dispatch({ | |
| 311 type: "start", | |
| 312 properties: properties, | |
| 313 }); | |
| 314 }); | |
| 315 add_test_state_callback( | |
| 316 function(test) { | |
| 317 this_obj._dispatch({ | |
| 318 type: "test_state", | |
| 319 test: test.structured_clone() | |
| 320 }); | |
| 321 }); | |
| 322 add_result_callback( | |
| 323 function(test) { | |
| 324 this_obj._dispatch({ | |
| 325 type: "result", | |
| 326 test: test.structured_clone() | |
| 327 }); | |
| 328 }); | |
| 329 add_completion_callback( | |
| 330 function(tests, harness_status) { | |
| 331 this_obj._dispatch({ | |
| 332 type: "complete", | |
| 333 tests: map(tests, | |
| 334 function(test) { | |
| 335 return test.structured_clone(); | |
| 336 }), | |
| 337 status: harness_status.structured_clone() | |
| 338 }); | |
| 339 }); | |
| 340 }; | |
| 341 | |
| 342 WorkerTestEnvironment.prototype.add_on_loaded_callback = function() {}; | |
| 343 | |
| 344 WorkerTestEnvironment.prototype.test_timeout = function() { | |
| 345 // Tests running in a worker don't have a default timeout. I.e. all | |
| 346 // worker tests behave as if settings.explicit_timeout is true. | |
| 347 return null; | |
| 348 }; | |
| 349 | |
| 350 WorkerTestEnvironment.prototype.global_scope = function() { | |
| 351 return self; | |
| 352 }; | |
| 353 | |
| 354 /* | |
| 355 * Dedicated web workers. | |
| 356 * https://html.spec.whatwg.org/multipage/workers.html#dedicatedworkerglobal
scope | |
| 357 * | |
| 358 * This class is used as the test_environment when testharness is running | |
| 359 * inside a dedicated worker. | |
| 360 */ | |
| 361 function DedicatedWorkerTestEnvironment() { | |
| 362 WorkerTestEnvironment.call(this); | |
| 363 // self is an instance of DedicatedWorkerGlobalScope which exposes | |
| 364 // a postMessage() method for communicating via the message channel | |
| 365 // established when the worker is created. | |
| 366 this._add_message_port(self); | |
| 367 } | |
| 368 DedicatedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironme
nt.prototype); | |
| 369 | |
| 370 DedicatedWorkerTestEnvironment.prototype.on_tests_ready = function() { | |
| 371 WorkerTestEnvironment.prototype.on_tests_ready.call(this); | |
| 372 // In the absence of an onload notification, we a require dedicated | |
| 373 // workers to explicitly signal when the tests are done. | |
| 374 tests.wait_for_finish = true; | |
| 375 }; | |
| 376 | |
| 377 /* | |
| 378 * Shared web workers. | |
| 379 * https://html.spec.whatwg.org/multipage/workers.html#sharedworkerglobalsco
pe | |
| 380 * | |
| 381 * This class is used as the test_environment when testharness is running | |
| 382 * inside a shared web worker. | |
| 383 */ | |
| 384 function SharedWorkerTestEnvironment() { | |
| 385 WorkerTestEnvironment.call(this); | |
| 386 var this_obj = this; | |
| 387 // Shared workers receive message ports via the 'onconnect' event for | |
| 388 // each connection. | |
| 389 self.addEventListener("connect", | |
| 390 function(message_event) { | |
| 391 this_obj._add_message_port(message_event.source); | |
| 392 }); | |
| 393 } | |
| 394 SharedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.
prototype); | |
| 395 | |
| 396 SharedWorkerTestEnvironment.prototype.on_tests_ready = function() { | |
| 397 WorkerTestEnvironment.prototype.on_tests_ready.call(this); | |
| 398 // In the absence of an onload notification, we a require shared | |
| 399 // workers to explicitly signal when the tests are done. | |
| 400 tests.wait_for_finish = true; | |
| 401 }; | |
| 402 | |
| 403 /* | |
| 404 * Service workers. | |
| 405 * http://www.w3.org/TR/service-workers/ | |
| 406 * | |
| 407 * This class is used as the test_environment when testharness is running | |
| 408 * inside a service worker. | |
| 409 */ | |
| 410 function ServiceWorkerTestEnvironment() { | |
| 411 WorkerTestEnvironment.call(this); | |
| 412 this.all_loaded = false; | |
| 413 this.on_loaded_callback = null; | |
| 414 var this_obj = this; | |
| 415 self.addEventListener("message", | |
| 416 function(event) { | |
| 417 if (event.data.type && event.data.type === "connect") { | |
| 418 if (event.ports && event.ports[0]) { | |
| 419 // If a MessageChannel was passed, then use it to | |
| 420 // send results back to the main window. This | |
| 421 // allows the tests to work even if the browser | |
| 422 // does not fully support MessageEvent.source in | |
| 423 // ServiceWorkers yet. | |
| 424 this_obj._add_message_port(event.ports[0]); | |
| 425 event.ports[0].start(); | |
| 426 } else { | |
| 427 // If there is no MessageChannel, then attempt to | |
| 428 // use the MessageEvent.source to send results | |
| 429 // back to the main window. | |
| 430 this_obj._add_message_port(event.source); | |
| 431 } | |
| 432 } | |
| 433 }); | |
| 434 | |
| 435 // The oninstall event is received after the service worker script and | |
| 436 // all imported scripts have been fetched and executed. It's the | |
| 437 // equivalent of an onload event for a document. All tests should have | |
| 438 // been added by the time this event is received, thus it's not | |
| 439 // necessary to wait until the onactivate event. | |
| 440 on_event(self, "install", | |
| 441 function(event) { | |
| 442 this_obj.all_loaded = true; | |
| 443 if (this_obj.on_loaded_callback) { | |
| 444 this_obj.on_loaded_callback(); | |
| 445 } | |
| 446 }); | |
| 447 } | |
| 448 ServiceWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment
.prototype); | |
| 449 | |
| 450 ServiceWorkerTestEnvironment.prototype.add_on_loaded_callback = function(cal
lback) { | |
| 451 if (this.all_loaded) { | |
| 452 callback(); | |
| 453 } else { | |
| 454 this.on_loaded_callback = callback; | |
| 455 } | |
| 456 }; | |
| 457 | |
| 458 function create_test_environment() { | |
| 459 if ('document' in self) { | |
| 460 return new WindowTestEnvironment(); | |
| 461 } | |
| 462 if ('DedicatedWorkerGlobalScope' in self && | |
| 463 self instanceof DedicatedWorkerGlobalScope) { | |
| 464 return new DedicatedWorkerTestEnvironment(); | |
| 465 } | |
| 466 if ('SharedWorkerGlobalScope' in self && | |
| 467 self instanceof SharedWorkerGlobalScope) { | |
| 468 return new SharedWorkerTestEnvironment(); | |
| 469 } | |
| 470 if ('ServiceWorkerGlobalScope' in self && | |
| 471 self instanceof ServiceWorkerGlobalScope) { | |
| 472 return new ServiceWorkerTestEnvironment(); | |
| 473 } | |
| 474 throw new Error("Unsupported test environment"); | |
| 475 } | |
| 476 | |
| 477 var test_environment = create_test_environment(); | |
| 478 | |
| 479 function is_shared_worker(worker) { | |
| 480 return 'SharedWorker' in self && worker instanceof SharedWorker; | |
| 481 } | |
| 482 | |
| 483 function is_service_worker(worker) { | |
| 484 return 'ServiceWorker' in self && worker instanceof ServiceWorker; | |
| 485 } | |
| 486 | |
| 487 /* | |
| 488 * API functions | |
| 489 */ | |
| 490 | |
| 491 function test(func, name, properties) | |
| 492 { | |
| 493 var test_name = name ? name : test_environment.next_default_test_name(); | |
| 494 properties = properties ? properties : {}; | |
| 495 var test_obj = new Test(test_name, properties); | |
| 496 test_obj.step(func, test_obj, test_obj); | |
| 497 if (test_obj.phase === test_obj.phases.STARTED) { | |
| 498 test_obj.done(); | |
| 499 } | |
| 500 } | |
| 501 | |
| 502 function async_test(func, name, properties) | |
| 503 { | |
| 504 if (typeof func !== "function") { | |
| 505 properties = name; | |
| 506 name = func; | |
| 507 func = null; | |
| 508 } | |
| 509 var test_name = name ? name : test_environment.next_default_test_name(); | |
| 510 properties = properties ? properties : {}; | |
| 511 var test_obj = new Test(test_name, properties); | |
| 512 if (func) { | |
| 513 test_obj.step(func, test_obj, test_obj); | |
| 514 } | |
| 515 return test_obj; | |
| 516 } | |
| 517 | |
| 518 function promise_test(func, name, properties) { | |
| 519 var test = async_test(name, properties); | |
| 520 // If there is no promise tests queue make one. | |
| 521 test.step(function() { | |
| 522 if (!tests.promise_tests) { | |
| 523 tests.promise_tests = Promise.resolve(); | |
| 524 } | |
| 525 }); | |
| 526 tests.promise_tests = tests.promise_tests.then(function() { | |
| 527 return Promise.resolve(test.step(func, test, test)) | |
| 528 .then( | |
| 529 function() { | |
| 530 test.done(); | |
| 531 }) | |
| 532 .catch(test.step_func( | |
| 533 function(value) { | |
| 534 if (value instanceof AssertionError) { | |
| 535 throw value; | |
| 536 } | |
| 537 assert(false, "promise_test", null, | |
| 538 "Unhandled rejection with value: ${value}", {valu
e:value}); | |
| 539 })); | |
| 540 }); | |
| 541 } | |
| 542 | |
| 543 function promise_rejects(test, expected, promise, description) { | |
| 544 return promise.then(test.unreached_func("Should have rejected: " + descr
iption)).catch(function(e) { | |
| 545 assert_throws(expected, function() { throw e }, description); | |
| 546 }); | |
| 547 } | |
| 548 | |
| 549 /** | |
| 550 * This constructor helper allows DOM events to be handled using Promises, | |
| 551 * which can make it a lot easier to test a very specific series of events, | |
| 552 * including ensuring that unexpected events are not fired at any point. | |
| 553 */ | |
| 554 function EventWatcher(test, watchedNode, eventTypes) | |
| 555 { | |
| 556 if (typeof eventTypes == 'string') { | |
| 557 eventTypes = [eventTypes]; | |
| 558 } | |
| 559 | |
| 560 var waitingFor = null; | |
| 561 | |
| 562 var eventHandler = test.step_func(function(evt) { | |
| 563 assert_true(!!waitingFor, | |
| 564 'Not expecting event, but got ' + evt.type + ' event'); | |
| 565 assert_equals(evt.type, waitingFor.types[0], | |
| 566 'Expected ' + waitingFor.types[0] + ' event, but got '
+ | |
| 567 evt.type + ' event instead'); | |
| 568 if (waitingFor.types.length > 1) { | |
| 569 // Pop first event from array | |
| 570 waitingFor.types.shift(); | |
| 571 return; | |
| 572 } | |
| 573 // We need to null out waitingFor before calling the resolve functio
n | |
| 574 // since the Promise's resolve handlers may call wait_for() which wi
ll | |
| 575 // need to set waitingFor. | |
| 576 var resolveFunc = waitingFor.resolve; | |
| 577 waitingFor = null; | |
| 578 resolveFunc(evt); | |
| 579 }); | |
| 580 | |
| 581 for (var i = 0; i < eventTypes.length; i++) { | |
| 582 watchedNode.addEventListener(eventTypes[i], eventHandler); | |
| 583 } | |
| 584 | |
| 585 /** | |
| 586 * Returns a Promise that will resolve after the specified event or | |
| 587 * series of events has occured. | |
| 588 */ | |
| 589 this.wait_for = function(types) { | |
| 590 if (waitingFor) { | |
| 591 return Promise.reject('Already waiting for an event or events'); | |
| 592 } | |
| 593 if (typeof types == 'string') { | |
| 594 types = [types]; | |
| 595 } | |
| 596 return new Promise(function(resolve, reject) { | |
| 597 waitingFor = { | |
| 598 types: types, | |
| 599 resolve: resolve, | |
| 600 reject: reject | |
| 601 }; | |
| 602 }); | |
| 603 }; | |
| 604 | |
| 605 function stop_watching() { | |
| 606 for (var i = 0; i < eventTypes.length; i++) { | |
| 607 watchedNode.removeEventListener(eventTypes[i], eventHandler); | |
| 608 } | |
| 609 }; | |
| 610 | |
| 611 test.add_cleanup(stop_watching); | |
| 612 | |
| 613 return this; | |
| 614 } | |
| 615 expose(EventWatcher, 'EventWatcher'); | |
| 616 | |
| 617 function setup(func_or_properties, maybe_properties) | |
| 618 { | |
| 619 var func = null; | |
| 620 var properties = {}; | |
| 621 if (arguments.length === 2) { | |
| 622 func = func_or_properties; | |
| 623 properties = maybe_properties; | |
| 624 } else if (func_or_properties instanceof Function) { | |
| 625 func = func_or_properties; | |
| 626 } else { | |
| 627 properties = func_or_properties; | |
| 628 } | |
| 629 tests.setup(func, properties); | |
| 630 test_environment.on_new_harness_properties(properties); | |
| 631 } | |
| 632 | |
| 633 function done() { | |
| 634 if (tests.tests.length === 0) { | |
| 635 tests.set_file_is_test(); | |
| 636 } | |
| 637 if (tests.file_is_test) { | |
| 638 tests.tests[0].done(); | |
| 639 } | |
| 640 tests.end_wait(); | |
| 641 } | |
| 642 | |
| 643 function generate_tests(func, args, properties) { | |
| 644 forEach(args, function(x, i) | |
| 645 { | |
| 646 var name = x[0]; | |
| 647 test(function() | |
| 648 { | |
| 649 func.apply(this, x.slice(1)); | |
| 650 }, | |
| 651 name, | |
| 652 Array.isArray(properties) ? properties[i] : properties)
; | |
| 653 }); | |
| 654 } | |
| 655 | |
| 656 function on_event(object, event, callback) | |
| 657 { | |
| 658 object.addEventListener(event, callback, false); | |
| 659 } | |
| 660 | |
| 661 function step_timeout(f, t) { | |
| 662 var outer_this = this; | |
| 663 var args = Array.prototype.slice.call(arguments, 2); | |
| 664 return setTimeout(function() { | |
| 665 f.apply(outer_this, args); | |
| 666 }, t * tests.timeout_multiplier); | |
| 667 } | |
| 668 | |
| 669 expose(test, 'test'); | |
| 670 expose(async_test, 'async_test'); | |
| 671 expose(promise_test, 'promise_test'); | |
| 672 expose(promise_rejects, 'promise_rejects'); | |
| 673 expose(generate_tests, 'generate_tests'); | |
| 674 expose(setup, 'setup'); | |
| 675 expose(done, 'done'); | |
| 676 expose(on_event, 'on_event'); | |
| 677 expose(step_timeout, 'step_timeout'); | |
| 678 | |
| 679 /* | |
| 680 * Return a string truncated to the given length, with ... added at the end | |
| 681 * if it was longer. | |
| 682 */ | |
| 683 function truncate(s, len) | |
| 684 { | |
| 685 if (s.length > len) { | |
| 686 return s.substring(0, len - 3) + "..."; | |
| 687 } | |
| 688 return s; | |
| 689 } | |
| 690 | |
| 691 /* | |
| 692 * Return true if object is probably a Node object. | |
| 693 */ | |
| 694 function is_node(object) | |
| 695 { | |
| 696 // I use duck-typing instead of instanceof, because | |
| 697 // instanceof doesn't work if the node is from another window (like an | |
| 698 // iframe's contentWindow): | |
| 699 // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295 | |
| 700 try { | |
| 701 var has_node_properties = ("nodeType" in object && | |
| 702 "nodeName" in object && | |
| 703 "nodeValue" in object && | |
| 704 "childNodes" in object); | |
| 705 } catch (e) { | |
| 706 // We're probably cross-origin, which means we aren't a node | |
| 707 return false; | |
| 708 } | |
| 709 | |
| 710 if (has_node_properties) { | |
| 711 try { | |
| 712 object.nodeType; | |
| 713 } catch (e) { | |
| 714 // The object is probably Node.prototype or another prototype | |
| 715 // object that inherits from it, and not a Node instance. | |
| 716 return false; | |
| 717 } | |
| 718 return true; | |
| 719 } | |
| 720 return false; | |
| 721 } | |
| 722 | |
| 723 var replacements = { | |
| 724 "0": "0", | |
| 725 "1": "x01", | |
| 726 "2": "x02", | |
| 727 "3": "x03", | |
| 728 "4": "x04", | |
| 729 "5": "x05", | |
| 730 "6": "x06", | |
| 731 "7": "x07", | |
| 732 "8": "b", | |
| 733 "9": "t", | |
| 734 "10": "n", | |
| 735 "11": "v", | |
| 736 "12": "f", | |
| 737 "13": "r", | |
| 738 "14": "x0e", | |
| 739 "15": "x0f", | |
| 740 "16": "x10", | |
| 741 "17": "x11", | |
| 742 "18": "x12", | |
| 743 "19": "x13", | |
| 744 "20": "x14", | |
| 745 "21": "x15", | |
| 746 "22": "x16", | |
| 747 "23": "x17", | |
| 748 "24": "x18", | |
| 749 "25": "x19", | |
| 750 "26": "x1a", | |
| 751 "27": "x1b", | |
| 752 "28": "x1c", | |
| 753 "29": "x1d", | |
| 754 "30": "x1e", | |
| 755 "31": "x1f", | |
| 756 "0xfffd": "ufffd", | |
| 757 "0xfffe": "ufffe", | |
| 758 "0xffff": "uffff", | |
| 759 }; | |
| 760 | |
| 761 /* | |
| 762 * Convert a value to a nice, human-readable string | |
| 763 */ | |
| 764 function format_value(val, seen) | |
| 765 { | |
| 766 if (!seen) { | |
| 767 seen = []; | |
| 768 } | |
| 769 if (typeof val === "object" && val !== null) { | |
| 770 if (seen.indexOf(val) >= 0) { | |
| 771 return "[...]"; | |
| 772 } | |
| 773 seen.push(val); | |
| 774 } | |
| 775 if (Array.isArray(val)) { | |
| 776 return "[" + val.map(function(x) {return format_value(x, seen);}).jo
in(", ") + "]"; | |
| 777 } | |
| 778 | |
| 779 switch (typeof val) { | |
| 780 case "string": | |
| 781 val = val.replace("\\", "\\\\"); | |
| 782 for (var p in replacements) { | |
| 783 var replace = "\\" + replacements[p]; | |
| 784 val = val.replace(RegExp(String.fromCharCode(p), "g"), replace); | |
| 785 } | |
| 786 return '"' + val.replace(/"/g, '\\"') + '"'; | |
| 787 case "boolean": | |
| 788 case "undefined": | |
| 789 return String(val); | |
| 790 case "number": | |
| 791 // In JavaScript, -0 === 0 and String(-0) == "0", so we have to | |
| 792 // special-case. | |
| 793 if (val === -0 && 1/val === -Infinity) { | |
| 794 return "-0"; | |
| 795 } | |
| 796 return String(val); | |
| 797 case "object": | |
| 798 if (val === null) { | |
| 799 return "null"; | |
| 800 } | |
| 801 | |
| 802 // Special-case Node objects, since those come up a lot in my tests.
I | |
| 803 // ignore namespaces. | |
| 804 if (is_node(val)) { | |
| 805 switch (val.nodeType) { | |
| 806 case Node.ELEMENT_NODE: | |
| 807 var ret = "<" + val.localName; | |
| 808 for (var i = 0; i < val.attributes.length; i++) { | |
| 809 ret += " " + val.attributes[i].name + '="' + val.attribu
tes[i].value + '"'; | |
| 810 } | |
| 811 ret += ">" + val.innerHTML + "</" + val.localName + ">"; | |
| 812 return "Element node " + truncate(ret, 60); | |
| 813 case Node.TEXT_NODE: | |
| 814 return 'Text node "' + truncate(val.data, 60) + '"'; | |
| 815 case Node.PROCESSING_INSTRUCTION_NODE: | |
| 816 return "ProcessingInstruction node with target " + format_va
lue(truncate(val.target, 60)) + " and data " + format_value(truncate(val.data, 6
0)); | |
| 817 case Node.COMMENT_NODE: | |
| 818 return "Comment node <!--" + truncate(val.data, 60) + "-->"; | |
| 819 case Node.DOCUMENT_NODE: | |
| 820 return "Document node with " + val.childNodes.length + (val.
childNodes.length == 1 ? " child" : " children"); | |
| 821 case Node.DOCUMENT_TYPE_NODE: | |
| 822 return "DocumentType node"; | |
| 823 case Node.DOCUMENT_FRAGMENT_NODE: | |
| 824 return "DocumentFragment node with " + val.childNodes.length
+ (val.childNodes.length == 1 ? " child" : " children"); | |
| 825 default: | |
| 826 return "Node object of unknown type"; | |
| 827 } | |
| 828 } | |
| 829 | |
| 830 /* falls through */ | |
| 831 default: | |
| 832 try { | |
| 833 return typeof val + ' "' + truncate(String(val), 60) + '"'; | |
| 834 } catch(e) { | |
| 835 return ("[stringifying object threw " + String(e) + | |
| 836 " with type " + String(typeof e) + "]"); | |
| 837 } | |
| 838 } | |
| 839 } | |
| 840 expose(format_value, "format_value"); | |
| 841 | |
| 842 /* | |
| 843 * Assertions | |
| 844 */ | |
| 845 | |
| 846 function assert_true(actual, description) | |
| 847 { | |
| 848 assert(actual === true, "assert_true", description, | |
| 849 "expected true got ${actual}", {actual:actual}); | |
| 850 } | |
| 851 expose(assert_true, "assert_true"); | |
| 852 | |
| 853 function assert_false(actual, description) | |
| 854 { | |
| 855 assert(actual === false, "assert_false", description, | |
| 856 "expected false got ${actual}", {actual:actual}
); | |
| 857 } | |
| 858 expose(assert_false, "assert_false"); | |
| 859 | |
| 860 function same_value(x, y) { | |
| 861 if (y !== y) { | |
| 862 //NaN case | |
| 863 return x !== x; | |
| 864 } | |
| 865 if (x === 0 && y === 0) { | |
| 866 //Distinguish +0 and -0 | |
| 867 return 1/x === 1/y; | |
| 868 } | |
| 869 return x === y; | |
| 870 } | |
| 871 | |
| 872 function assert_equals(actual, expected, description) | |
| 873 { | |
| 874 /* | |
| 875 * Test if two primitives are equal or two objects | |
| 876 * are the same object | |
| 877 */ | |
| 878 if (typeof actual != typeof expected) { | |
| 879 assert(false, "assert_equals", description, | |
| 880 "expected (" + typeof expected + ") ${expected} but go
t (" + typeof actual + ") ${actual}", | |
| 881 {expected:expected, actual:actual}); | |
| 882 return; | |
| 883 } | |
| 884 assert(same_value(actual, expected), "assert_equals", description, | |
| 885 "expected ${expected} but got ${act
ual}", | |
| 886 {expected:expected, actual:actual})
; | |
| 887 } | |
| 888 expose(assert_equals, "assert_equals"); | |
| 889 | |
| 890 function assert_not_equals(actual, expected, description) | |
| 891 { | |
| 892 /* | |
| 893 * Test if two primitives are unequal or two objects | |
| 894 * are different objects | |
| 895 */ | |
| 896 assert(!same_value(actual, expected), "assert_not_equals", description, | |
| 897 "got disallowed value ${actual}", | |
| 898 {actual:actual}); | |
| 899 } | |
| 900 expose(assert_not_equals, "assert_not_equals"); | |
| 901 | |
| 902 function assert_in_array(actual, expected, description) | |
| 903 { | |
| 904 assert(expected.indexOf(actual) != -1, "assert_in_array", description, | |
| 905 "value ${actual} not in array ${e
xpected}", | |
| 906 {actual:actual, expected:expected
}); | |
| 907 } | |
| 908 expose(assert_in_array, "assert_in_array"); | |
| 909 | |
| 910 function assert_object_equals(actual, expected, description) | |
| 911 { | |
| 912 //This needs to be improved a great deal | |
| 913 function check_equal(actual, expected, stack) | |
| 914 { | |
| 915 stack.push(actual); | |
| 916 | |
| 917 var p; | |
| 918 for (p in actual) { | |
| 919 assert(expected.hasOwnProperty(p), "assert_object_equals", desc
ription, | |
| 920 "unexpected property ${p}",
{p:p}); | |
| 921 | |
| 922 if (typeof actual[p] === "object" && actual[p] !== null) { | |
| 923 if (stack.indexOf(actual[p]) === -1) { | |
| 924 check_equal(actual[p], expected[p], stack); | |
| 925 } | |
| 926 } else { | |
| 927 assert(same_value(actual[p], expected[p]), "assert_object_e
quals", description, | |
| 928 "property ${p} expected $
{expected} got ${actual}", | |
| 929 {p:p, expected:expected,
actual:actual}); | |
| 930 } | |
| 931 } | |
| 932 for (p in expected) { | |
| 933 assert(actual.hasOwnProperty(p), | |
| 934 "assert_object_equals", description, | |
| 935 "expected property ${p} missing", {p:p}); | |
| 936 } | |
| 937 stack.pop(); | |
| 938 } | |
| 939 check_equal(actual, expected, []); | |
| 940 } | |
| 941 expose(assert_object_equals, "assert_object_equals"); | |
| 942 | |
| 943 function assert_array_equals(actual, expected, description) | |
| 944 { | |
| 945 assert(actual.length === expected.length, | |
| 946 "assert_array_equals", description, | |
| 947 "lengths differ, expected ${expected} got ${actual}", | |
| 948 {expected:expected.length, actual:actual.length}); | |
| 949 | |
| 950 for (var i = 0; i < actual.length; i++) { | |
| 951 assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i), | |
| 952 "assert_array_equals", description, | |
| 953 "property ${i}, property expected to be ${expected} but was $
{actual}", | |
| 954 {i:i, expected:expected.hasOwnProperty(i) ? "present" : "miss
ing", | |
| 955 actual:actual.hasOwnProperty(i) ? "present" : "missing"}); | |
| 956 assert(same_value(expected[i], actual[i]), | |
| 957 "assert_array_equals", description, | |
| 958 "property ${i}, expected ${expected} but got ${actual}", | |
| 959 {i:i, expected:expected[i], actual:actual[i]}); | |
| 960 } | |
| 961 } | |
| 962 expose(assert_array_equals, "assert_array_equals"); | |
| 963 | |
| 964 function assert_approx_equals(actual, expected, epsilon, description) | |
| 965 { | |
| 966 /* | |
| 967 * Test if two primitive numbers are equal withing +/- epsilon | |
| 968 */ | |
| 969 assert(typeof actual === "number", | |
| 970 "assert_approx_equals", description, | |
| 971 "expected a number but got a ${type_actual}", | |
| 972 {type_actual:typeof actual}); | |
| 973 | |
| 974 assert(Math.abs(actual - expected) <= epsilon, | |
| 975 "assert_approx_equals", description, | |
| 976 "expected ${expected} +/- ${epsilon} but got ${actual}", | |
| 977 {expected:expected, actual:actual, epsilon:epsilon}); | |
| 978 } | |
| 979 expose(assert_approx_equals, "assert_approx_equals"); | |
| 980 | |
| 981 function assert_less_than(actual, expected, description) | |
| 982 { | |
| 983 /* | |
| 984 * Test if a primitive number is less than another | |
| 985 */ | |
| 986 assert(typeof actual === "number", | |
| 987 "assert_less_than", description, | |
| 988 "expected a number but got a ${type_actual}", | |
| 989 {type_actual:typeof actual}); | |
| 990 | |
| 991 assert(actual < expected, | |
| 992 "assert_less_than", description, | |
| 993 "expected a number less than ${expected} but got ${actual}", | |
| 994 {expected:expected, actual:actual}); | |
| 995 } | |
| 996 expose(assert_less_than, "assert_less_than"); | |
| 997 | |
| 998 function assert_greater_than(actual, expected, description) | |
| 999 { | |
| 1000 /* | |
| 1001 * Test if a primitive number is greater than another | |
| 1002 */ | |
| 1003 assert(typeof actual === "number", | |
| 1004 "assert_greater_than", description, | |
| 1005 "expected a number but got a ${type_actual}", | |
| 1006 {type_actual:typeof actual}); | |
| 1007 | |
| 1008 assert(actual > expected, | |
| 1009 "assert_greater_than", description, | |
| 1010 "expected a number greater than ${expected} but got ${actual}", | |
| 1011 {expected:expected, actual:actual}); | |
| 1012 } | |
| 1013 expose(assert_greater_than, "assert_greater_than"); | |
| 1014 | |
| 1015 function assert_between_exclusive(actual, lower, upper, description) | |
| 1016 { | |
| 1017 /* | |
| 1018 * Test if a primitive number is between two others | |
| 1019 */ | |
| 1020 assert(typeof actual === "number", | |
| 1021 "assert_between_exclusive", description, | |
| 1022 "expected a number but got a ${type_actual}", | |
| 1023 {type_actual:typeof actual}); | |
| 1024 | |
| 1025 assert(actual > lower && actual < upper, | |
| 1026 "assert_between_exclusive", description, | |
| 1027 "expected a number greater than ${lower} " + | |
| 1028 "and less than ${upper} but got ${actual}", | |
| 1029 {lower:lower, upper:upper, actual:actual}); | |
| 1030 } | |
| 1031 expose(assert_between_exclusive, "assert_between_exclusive"); | |
| 1032 | |
| 1033 function assert_less_than_equal(actual, expected, description) | |
| 1034 { | |
| 1035 /* | |
| 1036 * Test if a primitive number is less than or equal to another | |
| 1037 */ | |
| 1038 assert(typeof actual === "number", | |
| 1039 "assert_less_than_equal", description, | |
| 1040 "expected a number but got a ${type_actual}", | |
| 1041 {type_actual:typeof actual}); | |
| 1042 | |
| 1043 assert(actual <= expected, | |
| 1044 "assert_less_than_equal", description, | |
| 1045 "expected a number less than or equal to ${expected} but got ${ac
tual}", | |
| 1046 {expected:expected, actual:actual}); | |
| 1047 } | |
| 1048 expose(assert_less_than_equal, "assert_less_than_equal"); | |
| 1049 | |
| 1050 function assert_greater_than_equal(actual, expected, description) | |
| 1051 { | |
| 1052 /* | |
| 1053 * Test if a primitive number is greater than or equal to another | |
| 1054 */ | |
| 1055 assert(typeof actual === "number", | |
| 1056 "assert_greater_than_equal", description, | |
| 1057 "expected a number but got a ${type_actual}", | |
| 1058 {type_actual:typeof actual}); | |
| 1059 | |
| 1060 assert(actual >= expected, | |
| 1061 "assert_greater_than_equal", description, | |
| 1062 "expected a number greater than or equal to ${expected} but got $
{actual}", | |
| 1063 {expected:expected, actual:actual}); | |
| 1064 } | |
| 1065 expose(assert_greater_than_equal, "assert_greater_than_equal"); | |
| 1066 | |
| 1067 function assert_between_inclusive(actual, lower, upper, description) | |
| 1068 { | |
| 1069 /* | |
| 1070 * Test if a primitive number is between to two others or equal to eithe
r of them | |
| 1071 */ | |
| 1072 assert(typeof actual === "number", | |
| 1073 "assert_between_inclusive", description, | |
| 1074 "expected a number but got a ${type_actual}", | |
| 1075 {type_actual:typeof actual}); | |
| 1076 | |
| 1077 assert(actual >= lower && actual <= upper, | |
| 1078 "assert_between_inclusive", description, | |
| 1079 "expected a number greater than or equal to ${lower} " + | |
| 1080 "and less than or equal to ${upper} but got ${actual}", | |
| 1081 {lower:lower, upper:upper, actual:actual}); | |
| 1082 } | |
| 1083 expose(assert_between_inclusive, "assert_between_inclusive"); | |
| 1084 | |
| 1085 function assert_regexp_match(actual, expected, description) { | |
| 1086 /* | |
| 1087 * Test if a string (actual) matches a regexp (expected) | |
| 1088 */ | |
| 1089 assert(expected.test(actual), | |
| 1090 "assert_regexp_match", description, | |
| 1091 "expected ${expected} but got ${actual}", | |
| 1092 {expected:expected, actual:actual}); | |
| 1093 } | |
| 1094 expose(assert_regexp_match, "assert_regexp_match"); | |
| 1095 | |
| 1096 function assert_class_string(object, class_string, description) { | |
| 1097 assert_equals({}.toString.call(object), "[object " + class_string + "]", | |
| 1098 description); | |
| 1099 } | |
| 1100 expose(assert_class_string, "assert_class_string"); | |
| 1101 | |
| 1102 | |
| 1103 function _assert_own_property(name) { | |
| 1104 return function(object, property_name, description) | |
| 1105 { | |
| 1106 assert(object.hasOwnProperty(property_name), | |
| 1107 name, description, | |
| 1108 "expected property ${p} missing", {p:property_name}); | |
| 1109 }; | |
| 1110 } | |
| 1111 expose(_assert_own_property("assert_exists"), "assert_exists"); | |
| 1112 expose(_assert_own_property("assert_own_property"), "assert_own_property"); | |
| 1113 | |
| 1114 function assert_not_exists(object, property_name, description) | |
| 1115 { | |
| 1116 assert(!object.hasOwnProperty(property_name), | |
| 1117 "assert_not_exists", description, | |
| 1118 "unexpected property ${p} found", {p:property_name}); | |
| 1119 } | |
| 1120 expose(assert_not_exists, "assert_not_exists"); | |
| 1121 | |
| 1122 function _assert_inherits(name) { | |
| 1123 return function (object, property_name, description) | |
| 1124 { | |
| 1125 assert(typeof object === "object" || typeof object === "function", | |
| 1126 name, description, | |
| 1127 "provided value is not an object"); | |
| 1128 | |
| 1129 assert("hasOwnProperty" in object, | |
| 1130 name, description, | |
| 1131 "provided value is an object but has no hasOwnProperty method
"); | |
| 1132 | |
| 1133 assert(!object.hasOwnProperty(property_name), | |
| 1134 name, description, | |
| 1135 "property ${p} found on object expected in prototype chain", | |
| 1136 {p:property_name}); | |
| 1137 | |
| 1138 assert(property_name in object, | |
| 1139 name, description, | |
| 1140 "property ${p} not found in prototype chain", | |
| 1141 {p:property_name}); | |
| 1142 }; | |
| 1143 } | |
| 1144 expose(_assert_inherits("assert_inherits"), "assert_inherits"); | |
| 1145 expose(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute"); | |
| 1146 | |
| 1147 function assert_readonly(object, property_name, description) | |
| 1148 { | |
| 1149 var initial_value = object[property_name]; | |
| 1150 try { | |
| 1151 //Note that this can have side effects in the case where | |
| 1152 //the property has PutForwards | |
| 1153 object[property_name] = initial_value + "a"; //XXX use some other v
alue here? | |
| 1154 assert(same_value(object[property_name], initial_value), | |
| 1155 "assert_readonly", description, | |
| 1156 "changing property ${p} succeeded", | |
| 1157 {p:property_name}); | |
| 1158 } finally { | |
| 1159 object[property_name] = initial_value; | |
| 1160 } | |
| 1161 } | |
| 1162 expose(assert_readonly, "assert_readonly"); | |
| 1163 | |
| 1164 function assert_throws(code, func, description) | |
| 1165 { | |
| 1166 try { | |
| 1167 func.call(this); | |
| 1168 assert(false, "assert_throws", description, | |
| 1169 "${func} did not throw", {func:func}); | |
| 1170 } catch (e) { | |
| 1171 if (e instanceof AssertionError) { | |
| 1172 throw e; | |
| 1173 } | |
| 1174 if (code === null) { | |
| 1175 return; | |
| 1176 } | |
| 1177 if (typeof code === "object") { | |
| 1178 assert(typeof e == "object" && "name" in e && e.name == code.nam
e, | |
| 1179 "assert_throws", description, | |
| 1180 "${func} threw ${actual} (${actual_name}) expected ${expe
cted} (${expected_name})", | |
| 1181 {func:func, actual:e, actual_name:e.name, | |
| 1182 expected:code, | |
| 1183 expected_name:code.name}); | |
| 1184 return; | |
| 1185 } | |
| 1186 | |
| 1187 var code_name_map = { | |
| 1188 INDEX_SIZE_ERR: 'IndexSizeError', | |
| 1189 HIERARCHY_REQUEST_ERR: 'HierarchyRequestError', | |
| 1190 WRONG_DOCUMENT_ERR: 'WrongDocumentError', | |
| 1191 INVALID_CHARACTER_ERR: 'InvalidCharacterError', | |
| 1192 NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError', | |
| 1193 NOT_FOUND_ERR: 'NotFoundError', | |
| 1194 NOT_SUPPORTED_ERR: 'NotSupportedError', | |
| 1195 INUSE_ATTRIBUTE_ERR: 'InUseAttributeError', | |
| 1196 INVALID_STATE_ERR: 'InvalidStateError', | |
| 1197 SYNTAX_ERR: 'SyntaxError', | |
| 1198 INVALID_MODIFICATION_ERR: 'InvalidModificationError', | |
| 1199 NAMESPACE_ERR: 'NamespaceError', | |
| 1200 INVALID_ACCESS_ERR: 'InvalidAccessError', | |
| 1201 TYPE_MISMATCH_ERR: 'TypeMismatchError', | |
| 1202 SECURITY_ERR: 'SecurityError', | |
| 1203 NETWORK_ERR: 'NetworkError', | |
| 1204 ABORT_ERR: 'AbortError', | |
| 1205 URL_MISMATCH_ERR: 'URLMismatchError', | |
| 1206 QUOTA_EXCEEDED_ERR: 'QuotaExceededError', | |
| 1207 TIMEOUT_ERR: 'TimeoutError', | |
| 1208 INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError', | |
| 1209 DATA_CLONE_ERR: 'DataCloneError' | |
| 1210 }; | |
| 1211 | |
| 1212 var name = code in code_name_map ? code_name_map[code] : code; | |
| 1213 | |
| 1214 var name_code_map = { | |
| 1215 IndexSizeError: 1, | |
| 1216 HierarchyRequestError: 3, | |
| 1217 WrongDocumentError: 4, | |
| 1218 InvalidCharacterError: 5, | |
| 1219 NoModificationAllowedError: 7, | |
| 1220 NotFoundError: 8, | |
| 1221 NotSupportedError: 9, | |
| 1222 InUseAttributeError: 10, | |
| 1223 InvalidStateError: 11, | |
| 1224 SyntaxError: 12, | |
| 1225 InvalidModificationError: 13, | |
| 1226 NamespaceError: 14, | |
| 1227 InvalidAccessError: 15, | |
| 1228 TypeMismatchError: 17, | |
| 1229 SecurityError: 18, | |
| 1230 NetworkError: 19, | |
| 1231 AbortError: 20, | |
| 1232 URLMismatchError: 21, | |
| 1233 QuotaExceededError: 22, | |
| 1234 TimeoutError: 23, | |
| 1235 InvalidNodeTypeError: 24, | |
| 1236 DataCloneError: 25, | |
| 1237 | |
| 1238 EncodingError: 0, | |
| 1239 NotReadableError: 0, | |
| 1240 UnknownError: 0, | |
| 1241 ConstraintError: 0, | |
| 1242 DataError: 0, | |
| 1243 TransactionInactiveError: 0, | |
| 1244 ReadOnlyError: 0, | |
| 1245 VersionError: 0, | |
| 1246 OperationError: 0, | |
| 1247 }; | |
| 1248 | |
| 1249 if (!(name in name_code_map)) { | |
| 1250 throw new AssertionError('Test bug: unrecognized DOMException co
de "' + code + '" passed to assert_throws()'); | |
| 1251 } | |
| 1252 | |
| 1253 var required_props = { code: name_code_map[name] }; | |
| 1254 | |
| 1255 if (required_props.code === 0 || | |
| 1256 (typeof e == "object" && | |
| 1257 "name" in e && | |
| 1258 e.name !== e.name.toUpperCase() && | |
| 1259 e.name !== "DOMException")) { | |
| 1260 // New style exception: also test the name property. | |
| 1261 required_props.name = name; | |
| 1262 } | |
| 1263 | |
| 1264 //We'd like to test that e instanceof the appropriate interface, | |
| 1265 //but we can't, because we don't know what window it was created | |
| 1266 //in. It might be an instanceof the appropriate interface on some | |
| 1267 //unknown other window. TODO: Work around this somehow? | |
| 1268 | |
| 1269 assert(typeof e == "object", | |
| 1270 "assert_throws", description, | |
| 1271 "${func} threw ${e} with type ${type}, not an object", | |
| 1272 {func:func, e:e, type:typeof e}); | |
| 1273 | |
| 1274 for (var prop in required_props) { | |
| 1275 assert(typeof e == "object" && prop in e && e[prop] == required_
props[prop], | |
| 1276 "assert_throws", description, | |
| 1277 "${func} threw ${e} that is not a DOMException " + code +
": property ${prop} is equal to ${actual}, expected ${expected}", | |
| 1278 {func:func, e:e, prop:prop, actual:e[prop], expected:requ
ired_props[prop]}); | |
| 1279 } | |
| 1280 } | |
| 1281 } | |
| 1282 expose(assert_throws, "assert_throws"); | |
| 1283 | |
| 1284 function assert_unreached(description) { | |
| 1285 assert(false, "assert_unreached", description, | |
| 1286 "Reached unreachable code"); | |
| 1287 } | |
| 1288 expose(assert_unreached, "assert_unreached"); | |
| 1289 | |
| 1290 function assert_any(assert_func, actual, expected_array) | |
| 1291 { | |
| 1292 var args = [].slice.call(arguments, 3); | |
| 1293 var errors = []; | |
| 1294 var passed = false; | |
| 1295 forEach(expected_array, | |
| 1296 function(expected) | |
| 1297 { | |
| 1298 try { | |
| 1299 assert_func.apply(this, [actual, expected].concat(args))
; | |
| 1300 passed = true; | |
| 1301 } catch (e) { | |
| 1302 errors.push(e.message); | |
| 1303 } | |
| 1304 }); | |
| 1305 if (!passed) { | |
| 1306 throw new AssertionError(errors.join("\n\n")); | |
| 1307 } | |
| 1308 } | |
| 1309 expose(assert_any, "assert_any"); | |
| 1310 | |
| 1311 function Test(name, properties) | |
| 1312 { | |
| 1313 if (tests.file_is_test && tests.tests.length) { | |
| 1314 throw new Error("Tried to create a test with file_is_test"); | |
| 1315 } | |
| 1316 this.name = name; | |
| 1317 | |
| 1318 this.phase = this.phases.INITIAL; | |
| 1319 | |
| 1320 this.status = this.NOTRUN; | |
| 1321 this.timeout_id = null; | |
| 1322 this.index = null; | |
| 1323 | |
| 1324 this.properties = properties; | |
| 1325 var timeout = properties.timeout ? properties.timeout : settings.test_ti
meout; | |
| 1326 if (timeout !== null) { | |
| 1327 this.timeout_length = timeout * tests.timeout_multiplier; | |
| 1328 } else { | |
| 1329 this.timeout_length = null; | |
| 1330 } | |
| 1331 | |
| 1332 this.message = null; | |
| 1333 this.stack = null; | |
| 1334 | |
| 1335 this.steps = []; | |
| 1336 | |
| 1337 this.cleanup_callbacks = []; | |
| 1338 | |
| 1339 tests.push(this); | |
| 1340 } | |
| 1341 | |
| 1342 Test.statuses = { | |
| 1343 PASS:0, | |
| 1344 FAIL:1, | |
| 1345 TIMEOUT:2, | |
| 1346 NOTRUN:3 | |
| 1347 }; | |
| 1348 | |
| 1349 Test.prototype = merge({}, Test.statuses); | |
| 1350 | |
| 1351 Test.prototype.phases = { | |
| 1352 INITIAL:0, | |
| 1353 STARTED:1, | |
| 1354 HAS_RESULT:2, | |
| 1355 COMPLETE:3 | |
| 1356 }; | |
| 1357 | |
| 1358 Test.prototype.structured_clone = function() | |
| 1359 { | |
| 1360 if (!this._structured_clone) { | |
| 1361 var msg = this.message; | |
| 1362 msg = msg ? String(msg) : msg; | |
| 1363 this._structured_clone = merge({ | |
| 1364 name:String(this.name), | |
| 1365 properties:merge({}, this.properties), | |
| 1366 }, Test.statuses); | |
| 1367 } | |
| 1368 this._structured_clone.status = this.status; | |
| 1369 this._structured_clone.message = this.message; | |
| 1370 this._structured_clone.stack = this.stack; | |
| 1371 this._structured_clone.index = this.index; | |
| 1372 return this._structured_clone; | |
| 1373 }; | |
| 1374 | |
| 1375 Test.prototype.step = function(func, this_obj) | |
| 1376 { | |
| 1377 if (this.phase > this.phases.STARTED) { | |
| 1378 return; | |
| 1379 } | |
| 1380 this.phase = this.phases.STARTED; | |
| 1381 //If we don't get a result before the harness times out that will be a t
est timout | |
| 1382 this.set_status(this.TIMEOUT, "Test timed out"); | |
| 1383 | |
| 1384 tests.started = true; | |
| 1385 tests.notify_test_state(this); | |
| 1386 | |
| 1387 if (this.timeout_id === null) { | |
| 1388 this.set_timeout(); | |
| 1389 } | |
| 1390 | |
| 1391 this.steps.push(func); | |
| 1392 | |
| 1393 if (arguments.length === 1) { | |
| 1394 this_obj = this; | |
| 1395 } | |
| 1396 | |
| 1397 try { | |
| 1398 return func.apply(this_obj, Array.prototype.slice.call(arguments, 2)
); | |
| 1399 } catch (e) { | |
| 1400 if (this.phase >= this.phases.HAS_RESULT) { | |
| 1401 return; | |
| 1402 } | |
| 1403 var message = String((typeof e === "object" && e !== null) ? e.messa
ge : e); | |
| 1404 var stack = e.stack ? e.stack : null; | |
| 1405 | |
| 1406 this.set_status(this.FAIL, message, stack); | |
| 1407 this.phase = this.phases.HAS_RESULT; | |
| 1408 this.done(); | |
| 1409 } | |
| 1410 }; | |
| 1411 | |
| 1412 Test.prototype.step_func = function(func, this_obj) | |
| 1413 { | |
| 1414 var test_this = this; | |
| 1415 | |
| 1416 if (arguments.length === 1) { | |
| 1417 this_obj = test_this; | |
| 1418 } | |
| 1419 | |
| 1420 return function() | |
| 1421 { | |
| 1422 return test_this.step.apply(test_this, [func, this_obj].concat( | |
| 1423 Array.prototype.slice.call(arguments))); | |
| 1424 }; | |
| 1425 }; | |
| 1426 | |
| 1427 Test.prototype.step_func_done = function(func, this_obj) | |
| 1428 { | |
| 1429 var test_this = this; | |
| 1430 | |
| 1431 if (arguments.length === 1) { | |
| 1432 this_obj = test_this; | |
| 1433 } | |
| 1434 | |
| 1435 return function() | |
| 1436 { | |
| 1437 if (func) { | |
| 1438 test_this.step.apply(test_this, [func, this_obj].concat( | |
| 1439 Array.prototype.slice.call(arguments))); | |
| 1440 } | |
| 1441 test_this.done(); | |
| 1442 }; | |
| 1443 }; | |
| 1444 | |
| 1445 Test.prototype.unreached_func = function(description) | |
| 1446 { | |
| 1447 return this.step_func(function() { | |
| 1448 assert_unreached(description); | |
| 1449 }); | |
| 1450 }; | |
| 1451 | |
| 1452 Test.prototype.step_timeout = function(f, timeout) { | |
| 1453 var test_this = this; | |
| 1454 var args = Array.prototype.slice.call(arguments, 2); | |
| 1455 return setTimeout(this.step_func(function() { | |
| 1456 return f.apply(test_this, args); | |
| 1457 }), timeout * tests.timeout_multiplier); | |
| 1458 } | |
| 1459 | |
| 1460 Test.prototype.add_cleanup = function(callback) { | |
| 1461 this.cleanup_callbacks.push(callback); | |
| 1462 }; | |
| 1463 | |
| 1464 Test.prototype.force_timeout = function() { | |
| 1465 this.set_status(this.TIMEOUT); | |
| 1466 this.phase = this.phases.HAS_RESULT; | |
| 1467 }; | |
| 1468 | |
| 1469 Test.prototype.set_timeout = function() | |
| 1470 { | |
| 1471 if (this.timeout_length !== null) { | |
| 1472 var this_obj = this; | |
| 1473 this.timeout_id = setTimeout(function() | |
| 1474 { | |
| 1475 this_obj.timeout(); | |
| 1476 }, this.timeout_length); | |
| 1477 } | |
| 1478 }; | |
| 1479 | |
| 1480 Test.prototype.set_status = function(status, message, stack) | |
| 1481 { | |
| 1482 this.status = status; | |
| 1483 this.message = message; | |
| 1484 this.stack = stack ? stack : null; | |
| 1485 }; | |
| 1486 | |
| 1487 Test.prototype.timeout = function() | |
| 1488 { | |
| 1489 this.timeout_id = null; | |
| 1490 this.set_status(this.TIMEOUT, "Test timed out"); | |
| 1491 this.phase = this.phases.HAS_RESULT; | |
| 1492 this.done(); | |
| 1493 }; | |
| 1494 | |
| 1495 Test.prototype.done = function() | |
| 1496 { | |
| 1497 if (this.phase == this.phases.COMPLETE) { | |
| 1498 return; | |
| 1499 } | |
| 1500 | |
| 1501 if (this.phase <= this.phases.STARTED) { | |
| 1502 this.set_status(this.PASS, null); | |
| 1503 } | |
| 1504 | |
| 1505 this.phase = this.phases.COMPLETE; | |
| 1506 | |
| 1507 clearTimeout(this.timeout_id); | |
| 1508 tests.result(this); | |
| 1509 this.cleanup(); | |
| 1510 }; | |
| 1511 | |
| 1512 Test.prototype.cleanup = function() { | |
| 1513 forEach(this.cleanup_callbacks, | |
| 1514 function(cleanup_callback) { | |
| 1515 cleanup_callback(); | |
| 1516 }); | |
| 1517 }; | |
| 1518 | |
| 1519 /* | |
| 1520 * A RemoteTest object mirrors a Test object on a remote worker. The | |
| 1521 * associated RemoteWorker updates the RemoteTest object in response to | |
| 1522 * received events. In turn, the RemoteTest object replicates these events | |
| 1523 * on the local document. This allows listeners (test result reporting | |
| 1524 * etc..) to transparently handle local and remote events. | |
| 1525 */ | |
| 1526 function RemoteTest(clone) { | |
| 1527 var this_obj = this; | |
| 1528 Object.keys(clone).forEach( | |
| 1529 function(key) { | |
| 1530 this_obj[key] = clone[key]; | |
| 1531 }); | |
| 1532 this.index = null; | |
| 1533 this.phase = this.phases.INITIAL; | |
| 1534 this.update_state_from(clone); | |
| 1535 tests.push(this); | |
| 1536 } | |
| 1537 | |
| 1538 RemoteTest.prototype.structured_clone = function() { | |
| 1539 var clone = {}; | |
| 1540 Object.keys(this).forEach( | |
| 1541 (function(key) { | |
| 1542 if (typeof(this[key]) === "object") { | |
| 1543 clone[key] = merge({}, this[key]); | |
| 1544 } else { | |
| 1545 clone[key] = this[key]; | |
| 1546 } | |
| 1547 }).bind(this)); | |
| 1548 clone.phases = merge({}, this.phases); | |
| 1549 return clone; | |
| 1550 }; | |
| 1551 | |
| 1552 RemoteTest.prototype.cleanup = function() {}; | |
| 1553 RemoteTest.prototype.phases = Test.prototype.phases; | |
| 1554 RemoteTest.prototype.update_state_from = function(clone) { | |
| 1555 this.status = clone.status; | |
| 1556 this.message = clone.message; | |
| 1557 this.stack = clone.stack; | |
| 1558 if (this.phase === this.phases.INITIAL) { | |
| 1559 this.phase = this.phases.STARTED; | |
| 1560 } | |
| 1561 }; | |
| 1562 RemoteTest.prototype.done = function() { | |
| 1563 this.phase = this.phases.COMPLETE; | |
| 1564 } | |
| 1565 | |
| 1566 /* | |
| 1567 * A RemoteWorker listens for test events from a worker. These events are | |
| 1568 * then used to construct and maintain RemoteTest objects that mirror the | |
| 1569 * tests running on the remote worker. | |
| 1570 */ | |
| 1571 function RemoteWorker(worker) { | |
| 1572 this.running = true; | |
| 1573 this.tests = new Array(); | |
| 1574 | |
| 1575 var this_obj = this; | |
| 1576 worker.onerror = function(error) { this_obj.worker_error(error); }; | |
| 1577 | |
| 1578 var message_port; | |
| 1579 | |
| 1580 if (is_service_worker(worker)) { | |
| 1581 if (window.MessageChannel) { | |
| 1582 // The ServiceWorker's implicit MessagePort is currently not | |
| 1583 // reliably accessible from the ServiceWorkerGlobalScope due to | |
| 1584 // Blink setting MessageEvent.source to null for messages sent | |
| 1585 // via ServiceWorker.postMessage(). Until that's resolved, | |
| 1586 // create an explicit MessageChannel and pass one end to the | |
| 1587 // worker. | |
| 1588 var message_channel = new MessageChannel(); | |
| 1589 message_port = message_channel.port1; | |
| 1590 message_port.start(); | |
| 1591 worker.postMessage({type: "connect"}, [message_channel.port2]); | |
| 1592 } else { | |
| 1593 // If MessageChannel is not available, then try the | |
| 1594 // ServiceWorker.postMessage() approach using MessageEvent.sourc
e | |
| 1595 // on the other end. | |
| 1596 message_port = navigator.serviceWorker; | |
| 1597 worker.postMessage({type: "connect"}); | |
| 1598 } | |
| 1599 } else if (is_shared_worker(worker)) { | |
| 1600 message_port = worker.port; | |
| 1601 } else { | |
| 1602 message_port = worker; | |
| 1603 } | |
| 1604 | |
| 1605 // Keeping a reference to the worker until worker_done() is seen | |
| 1606 // prevents the Worker object and its MessageChannel from going away | |
| 1607 // before all the messages are dispatched. | |
| 1608 this.worker = worker; | |
| 1609 | |
| 1610 message_port.onmessage = | |
| 1611 function(message) { | |
| 1612 if (this_obj.running && (message.data.type in this_obj.message_h
andlers)) { | |
| 1613 this_obj.message_handlers[message.data.type].call(this_obj,
message.data); | |
| 1614 } | |
| 1615 }; | |
| 1616 } | |
| 1617 | |
| 1618 RemoteWorker.prototype.worker_error = function(error) { | |
| 1619 var message = error.message || String(error); | |
| 1620 var filename = (error.filename ? " " + error.filename: ""); | |
| 1621 // FIXME: Display worker error states separately from main document | |
| 1622 // error state. | |
| 1623 this.worker_done({ | |
| 1624 status: { | |
| 1625 status: tests.status.ERROR, | |
| 1626 message: "Error in worker" + filename + ": " + message, | |
| 1627 stack: error.stack | |
| 1628 } | |
| 1629 }); | |
| 1630 error.preventDefault(); | |
| 1631 }; | |
| 1632 | |
| 1633 RemoteWorker.prototype.test_state = function(data) { | |
| 1634 var remote_test = this.tests[data.test.index]; | |
| 1635 if (!remote_test) { | |
| 1636 remote_test = new RemoteTest(data.test); | |
| 1637 this.tests[data.test.index] = remote_test; | |
| 1638 } | |
| 1639 remote_test.update_state_from(data.test); | |
| 1640 tests.notify_test_state(remote_test); | |
| 1641 }; | |
| 1642 | |
| 1643 RemoteWorker.prototype.test_done = function(data) { | |
| 1644 var remote_test = this.tests[data.test.index]; | |
| 1645 remote_test.update_state_from(data.test); | |
| 1646 remote_test.done(); | |
| 1647 tests.result(remote_test); | |
| 1648 }; | |
| 1649 | |
| 1650 RemoteWorker.prototype.worker_done = function(data) { | |
| 1651 if (tests.status.status === null && | |
| 1652 data.status.status !== data.status.OK) { | |
| 1653 tests.status.status = data.status.status; | |
| 1654 tests.status.message = data.status.message; | |
| 1655 tests.status.stack = data.status.stack; | |
| 1656 } | |
| 1657 this.running = false; | |
| 1658 this.worker = null; | |
| 1659 if (tests.all_done()) { | |
| 1660 tests.complete(); | |
| 1661 } | |
| 1662 }; | |
| 1663 | |
| 1664 RemoteWorker.prototype.message_handlers = { | |
| 1665 test_state: RemoteWorker.prototype.test_state, | |
| 1666 result: RemoteWorker.prototype.test_done, | |
| 1667 complete: RemoteWorker.prototype.worker_done | |
| 1668 }; | |
| 1669 | |
| 1670 /* | |
| 1671 * Harness | |
| 1672 */ | |
| 1673 | |
| 1674 function TestsStatus() | |
| 1675 { | |
| 1676 this.status = null; | |
| 1677 this.message = null; | |
| 1678 this.stack = null; | |
| 1679 } | |
| 1680 | |
| 1681 TestsStatus.statuses = { | |
| 1682 OK:0, | |
| 1683 ERROR:1, | |
| 1684 TIMEOUT:2 | |
| 1685 }; | |
| 1686 | |
| 1687 TestsStatus.prototype = merge({}, TestsStatus.statuses); | |
| 1688 | |
| 1689 TestsStatus.prototype.structured_clone = function() | |
| 1690 { | |
| 1691 if (!this._structured_clone) { | |
| 1692 var msg = this.message; | |
| 1693 msg = msg ? String(msg) : msg; | |
| 1694 this._structured_clone = merge({ | |
| 1695 status:this.status, | |
| 1696 message:msg, | |
| 1697 stack:this.stack | |
| 1698 }, TestsStatus.statuses); | |
| 1699 } | |
| 1700 return this._structured_clone; | |
| 1701 }; | |
| 1702 | |
| 1703 function Tests() | |
| 1704 { | |
| 1705 this.tests = []; | |
| 1706 this.num_pending = 0; | |
| 1707 | |
| 1708 this.phases = { | |
| 1709 INITIAL:0, | |
| 1710 SETUP:1, | |
| 1711 HAVE_TESTS:2, | |
| 1712 HAVE_RESULTS:3, | |
| 1713 COMPLETE:4 | |
| 1714 }; | |
| 1715 this.phase = this.phases.INITIAL; | |
| 1716 | |
| 1717 this.properties = {}; | |
| 1718 | |
| 1719 this.wait_for_finish = false; | |
| 1720 this.processing_callbacks = false; | |
| 1721 | |
| 1722 this.allow_uncaught_exception = false; | |
| 1723 | |
| 1724 this.file_is_test = false; | |
| 1725 | |
| 1726 this.timeout_multiplier = 1; | |
| 1727 this.timeout_length = test_environment.test_timeout(); | |
| 1728 this.timeout_id = null; | |
| 1729 | |
| 1730 this.start_callbacks = []; | |
| 1731 this.test_state_callbacks = []; | |
| 1732 this.test_done_callbacks = []; | |
| 1733 this.all_done_callbacks = []; | |
| 1734 | |
| 1735 this.pending_workers = []; | |
| 1736 | |
| 1737 this.status = new TestsStatus(); | |
| 1738 | |
| 1739 var this_obj = this; | |
| 1740 | |
| 1741 test_environment.add_on_loaded_callback(function() { | |
| 1742 if (this_obj.all_done()) { | |
| 1743 this_obj.complete(); | |
| 1744 } | |
| 1745 }); | |
| 1746 | |
| 1747 this.set_timeout(); | |
| 1748 } | |
| 1749 | |
| 1750 Tests.prototype.setup = function(func, properties) | |
| 1751 { | |
| 1752 if (this.phase >= this.phases.HAVE_RESULTS) { | |
| 1753 return; | |
| 1754 } | |
| 1755 | |
| 1756 if (this.phase < this.phases.SETUP) { | |
| 1757 this.phase = this.phases.SETUP; | |
| 1758 } | |
| 1759 | |
| 1760 this.properties = properties; | |
| 1761 | |
| 1762 for (var p in properties) { | |
| 1763 if (properties.hasOwnProperty(p)) { | |
| 1764 var value = properties[p]; | |
| 1765 if (p == "allow_uncaught_exception") { | |
| 1766 this.allow_uncaught_exception = value; | |
| 1767 } else if (p == "explicit_done" && value) { | |
| 1768 this.wait_for_finish = true; | |
| 1769 } else if (p == "explicit_timeout" && value) { | |
| 1770 this.timeout_length = null; | |
| 1771 if (this.timeout_id) | |
| 1772 { | |
| 1773 clearTimeout(this.timeout_id); | |
| 1774 } | |
| 1775 } else if (p == "timeout_multiplier") { | |
| 1776 this.timeout_multiplier = value; | |
| 1777 } | |
| 1778 } | |
| 1779 } | |
| 1780 | |
| 1781 if (func) { | |
| 1782 try { | |
| 1783 func(); | |
| 1784 } catch (e) { | |
| 1785 this.status.status = this.status.ERROR; | |
| 1786 this.status.message = String(e); | |
| 1787 this.status.stack = e.stack ? e.stack : null; | |
| 1788 } | |
| 1789 } | |
| 1790 this.set_timeout(); | |
| 1791 }; | |
| 1792 | |
| 1793 Tests.prototype.set_file_is_test = function() { | |
| 1794 if (this.tests.length > 0) { | |
| 1795 throw new Error("Tried to set file as test after creating a test"); | |
| 1796 } | |
| 1797 this.wait_for_finish = true; | |
| 1798 this.file_is_test = true; | |
| 1799 // Create the test, which will add it to the list of tests | |
| 1800 async_test(); | |
| 1801 }; | |
| 1802 | |
| 1803 Tests.prototype.set_timeout = function() { | |
| 1804 var this_obj = this; | |
| 1805 clearTimeout(this.timeout_id); | |
| 1806 if (this.timeout_length !== null) { | |
| 1807 this.timeout_id = setTimeout(function() { | |
| 1808 this_obj.timeout(); | |
| 1809 }, this.timeout_length); | |
| 1810 } | |
| 1811 }; | |
| 1812 | |
| 1813 Tests.prototype.timeout = function() { | |
| 1814 if (this.status.status === null) { | |
| 1815 this.status.status = this.status.TIMEOUT; | |
| 1816 } | |
| 1817 this.complete(); | |
| 1818 }; | |
| 1819 | |
| 1820 Tests.prototype.end_wait = function() | |
| 1821 { | |
| 1822 this.wait_for_finish = false; | |
| 1823 if (this.all_done()) { | |
| 1824 this.complete(); | |
| 1825 } | |
| 1826 }; | |
| 1827 | |
| 1828 Tests.prototype.push = function(test) | |
| 1829 { | |
| 1830 if (this.phase < this.phases.HAVE_TESTS) { | |
| 1831 this.start(); | |
| 1832 } | |
| 1833 this.num_pending++; | |
| 1834 test.index = this.tests.push(test); | |
| 1835 this.notify_test_state(test); | |
| 1836 }; | |
| 1837 | |
| 1838 Tests.prototype.notify_test_state = function(test) { | |
| 1839 var this_obj = this; | |
| 1840 forEach(this.test_state_callbacks, | |
| 1841 function(callback) { | |
| 1842 callback(test, this_obj); | |
| 1843 }); | |
| 1844 }; | |
| 1845 | |
| 1846 Tests.prototype.all_done = function() { | |
| 1847 return (this.tests.length > 0 && test_environment.all_loaded && | |
| 1848 this.num_pending === 0 && !this.wait_for_finish && | |
| 1849 !this.processing_callbacks && | |
| 1850 !this.pending_workers.some(function(w) { return w.running; })); | |
| 1851 }; | |
| 1852 | |
| 1853 Tests.prototype.start = function() { | |
| 1854 this.phase = this.phases.HAVE_TESTS; | |
| 1855 this.notify_start(); | |
| 1856 }; | |
| 1857 | |
| 1858 Tests.prototype.notify_start = function() { | |
| 1859 var this_obj = this; | |
| 1860 forEach (this.start_callbacks, | |
| 1861 function(callback) | |
| 1862 { | |
| 1863 callback(this_obj.properties); | |
| 1864 }); | |
| 1865 }; | |
| 1866 | |
| 1867 Tests.prototype.result = function(test) | |
| 1868 { | |
| 1869 if (this.phase > this.phases.HAVE_RESULTS) { | |
| 1870 return; | |
| 1871 } | |
| 1872 this.phase = this.phases.HAVE_RESULTS; | |
| 1873 this.num_pending--; | |
| 1874 this.notify_result(test); | |
| 1875 }; | |
| 1876 | |
| 1877 Tests.prototype.notify_result = function(test) { | |
| 1878 var this_obj = this; | |
| 1879 this.processing_callbacks = true; | |
| 1880 forEach(this.test_done_callbacks, | |
| 1881 function(callback) | |
| 1882 { | |
| 1883 callback(test, this_obj); | |
| 1884 }); | |
| 1885 this.processing_callbacks = false; | |
| 1886 if (this_obj.all_done()) { | |
| 1887 this_obj.complete(); | |
| 1888 } | |
| 1889 }; | |
| 1890 | |
| 1891 Tests.prototype.complete = function() { | |
| 1892 if (this.phase === this.phases.COMPLETE) { | |
| 1893 return; | |
| 1894 } | |
| 1895 this.phase = this.phases.COMPLETE; | |
| 1896 var this_obj = this; | |
| 1897 this.tests.forEach( | |
| 1898 function(x) | |
| 1899 { | |
| 1900 if (x.phase < x.phases.COMPLETE) { | |
| 1901 this_obj.notify_result(x); | |
| 1902 x.cleanup(); | |
| 1903 x.phase = x.phases.COMPLETE; | |
| 1904 } | |
| 1905 } | |
| 1906 ); | |
| 1907 this.notify_complete(); | |
| 1908 }; | |
| 1909 | |
| 1910 Tests.prototype.notify_complete = function() { | |
| 1911 var this_obj = this; | |
| 1912 if (this.status.status === null) { | |
| 1913 this.status.status = this.status.OK; | |
| 1914 } | |
| 1915 | |
| 1916 forEach (this.all_done_callbacks, | |
| 1917 function(callback) | |
| 1918 { | |
| 1919 callback(this_obj.tests, this_obj.status); | |
| 1920 }); | |
| 1921 }; | |
| 1922 | |
| 1923 Tests.prototype.fetch_tests_from_worker = function(worker) { | |
| 1924 if (this.phase >= this.phases.COMPLETE) { | |
| 1925 return; | |
| 1926 } | |
| 1927 | |
| 1928 this.pending_workers.push(new RemoteWorker(worker)); | |
| 1929 }; | |
| 1930 | |
| 1931 function fetch_tests_from_worker(port) { | |
| 1932 tests.fetch_tests_from_worker(port); | |
| 1933 } | |
| 1934 expose(fetch_tests_from_worker, 'fetch_tests_from_worker'); | |
| 1935 | |
| 1936 function timeout() { | |
| 1937 if (tests.timeout_length === null) { | |
| 1938 tests.timeout(); | |
| 1939 } | |
| 1940 } | |
| 1941 expose(timeout, 'timeout'); | |
| 1942 | |
| 1943 function add_start_callback(callback) { | |
| 1944 tests.start_callbacks.push(callback); | |
| 1945 } | |
| 1946 | |
| 1947 function add_test_state_callback(callback) { | |
| 1948 tests.test_state_callbacks.push(callback); | |
| 1949 } | |
| 1950 | |
| 1951 function add_result_callback(callback) { | |
| 1952 tests.test_done_callbacks.push(callback); | |
| 1953 } | |
| 1954 | |
| 1955 function add_completion_callback(callback) { | |
| 1956 tests.all_done_callbacks.push(callback); | |
| 1957 } | |
| 1958 | |
| 1959 expose(add_start_callback, 'add_start_callback'); | |
| 1960 expose(add_test_state_callback, 'add_test_state_callback'); | |
| 1961 expose(add_result_callback, 'add_result_callback'); | |
| 1962 expose(add_completion_callback, 'add_completion_callback'); | |
| 1963 | |
| 1964 function remove(array, item) { | |
| 1965 var index = array.indexOf(item); | |
| 1966 if (index > -1) { | |
| 1967 array.splice(index, 1); | |
| 1968 } | |
| 1969 } | |
| 1970 | |
| 1971 function remove_start_callback(callback) { | |
| 1972 remove(tests.start_callbacks, callback); | |
| 1973 } | |
| 1974 | |
| 1975 function remove_test_state_callback(callback) { | |
| 1976 remove(tests.test_state_callbacks, callback); | |
| 1977 } | |
| 1978 | |
| 1979 function remove_result_callback(callback) { | |
| 1980 remove(tests.test_done_callbacks, callback); | |
| 1981 } | |
| 1982 | |
| 1983 function remove_completion_callback(callback) { | |
| 1984 remove(tests.all_done_callbacks, callback); | |
| 1985 } | |
| 1986 | |
| 1987 /* | |
| 1988 * Output listener | |
| 1989 */ | |
| 1990 | |
| 1991 function Output() { | |
| 1992 this.output_document = document; | |
| 1993 this.output_node = null; | |
| 1994 this.enabled = settings.output; | |
| 1995 this.phase = this.INITIAL; | |
| 1996 } | |
| 1997 | |
| 1998 Output.prototype.INITIAL = 0; | |
| 1999 Output.prototype.STARTED = 1; | |
| 2000 Output.prototype.HAVE_RESULTS = 2; | |
| 2001 Output.prototype.COMPLETE = 3; | |
| 2002 | |
| 2003 Output.prototype.setup = function(properties) { | |
| 2004 if (this.phase > this.INITIAL) { | |
| 2005 return; | |
| 2006 } | |
| 2007 | |
| 2008 //If output is disabled in testharnessreport.js the test shouldn't be | |
| 2009 //able to override that | |
| 2010 this.enabled = this.enabled && (properties.hasOwnProperty("output") ? | |
| 2011 properties.output : settings.output); | |
| 2012 }; | |
| 2013 | |
| 2014 Output.prototype.init = function(properties) { | |
| 2015 if (this.phase >= this.STARTED) { | |
| 2016 return; | |
| 2017 } | |
| 2018 if (properties.output_document) { | |
| 2019 this.output_document = properties.output_document; | |
| 2020 } else { | |
| 2021 this.output_document = document; | |
| 2022 } | |
| 2023 this.phase = this.STARTED; | |
| 2024 }; | |
| 2025 | |
| 2026 Output.prototype.resolve_log = function() { | |
| 2027 var output_document; | |
| 2028 if (typeof this.output_document === "function") { | |
| 2029 output_document = this.output_document.apply(undefined); | |
| 2030 } else { | |
| 2031 output_document = this.output_document; | |
| 2032 } | |
| 2033 if (!output_document) { | |
| 2034 return; | |
| 2035 } | |
| 2036 var node = output_document.getElementById("log"); | |
| 2037 if (!node) { | |
| 2038 if (!document.body || document.readyState == "loading") { | |
| 2039 return; | |
| 2040 } | |
| 2041 node = output_document.createElement("div"); | |
| 2042 node.id = "log"; | |
| 2043 output_document.body.appendChild(node); | |
| 2044 } | |
| 2045 this.output_document = output_document; | |
| 2046 this.output_node = node; | |
| 2047 }; | |
| 2048 | |
| 2049 Output.prototype.show_status = function() { | |
| 2050 if (this.phase < this.STARTED) { | |
| 2051 this.init(); | |
| 2052 } | |
| 2053 if (!this.enabled) { | |
| 2054 return; | |
| 2055 } | |
| 2056 if (this.phase < this.HAVE_RESULTS) { | |
| 2057 this.resolve_log(); | |
| 2058 this.phase = this.HAVE_RESULTS; | |
| 2059 } | |
| 2060 var done_count = tests.tests.length - tests.num_pending; | |
| 2061 if (this.output_node) { | |
| 2062 if (done_count < 100 || | |
| 2063 (done_count < 1000 && done_count % 100 === 0) || | |
| 2064 done_count % 1000 === 0) { | |
| 2065 this.output_node.textContent = "Running, " + | |
| 2066 done_count + " complete, " + | |
| 2067 tests.num_pending + " remain"; | |
| 2068 } | |
| 2069 } | |
| 2070 }; | |
| 2071 | |
| 2072 Output.prototype.show_results = function (tests, harness_status) { | |
| 2073 if (this.phase >= this.COMPLETE) { | |
| 2074 return; | |
| 2075 } | |
| 2076 if (!this.enabled) { | |
| 2077 return; | |
| 2078 } | |
| 2079 if (!this.output_node) { | |
| 2080 this.resolve_log(); | |
| 2081 } | |
| 2082 this.phase = this.COMPLETE; | |
| 2083 | |
| 2084 var log = this.output_node; | |
| 2085 if (!log) { | |
| 2086 return; | |
| 2087 } | |
| 2088 var output_document = this.output_document; | |
| 2089 | |
| 2090 while (log.lastChild) { | |
| 2091 log.removeChild(log.lastChild); | |
| 2092 } | |
| 2093 | |
| 2094 var harness_url = get_harness_url(); | |
| 2095 if (harness_url !== null) { | |
| 2096 var stylesheet = output_document.createElementNS(xhtml_ns, "link"); | |
| 2097 stylesheet.setAttribute("rel", "stylesheet"); | |
| 2098 stylesheet.setAttribute("href", harness_url + "testharness.css"); | |
| 2099 var heads = output_document.getElementsByTagName("head"); | |
| 2100 if (heads.length) { | |
| 2101 heads[0].appendChild(stylesheet); | |
| 2102 } | |
| 2103 } | |
| 2104 | |
| 2105 var status_text_harness = {}; | |
| 2106 status_text_harness[harness_status.OK] = "OK"; | |
| 2107 status_text_harness[harness_status.ERROR] = "Error"; | |
| 2108 status_text_harness[harness_status.TIMEOUT] = "Timeout"; | |
| 2109 | |
| 2110 var status_text = {}; | |
| 2111 status_text[Test.prototype.PASS] = "Pass"; | |
| 2112 status_text[Test.prototype.FAIL] = "Fail"; | |
| 2113 status_text[Test.prototype.TIMEOUT] = "Timeout"; | |
| 2114 status_text[Test.prototype.NOTRUN] = "Not Run"; | |
| 2115 | |
| 2116 var status_number = {}; | |
| 2117 forEach(tests, | |
| 2118 function(test) { | |
| 2119 var status = status_text[test.status]; | |
| 2120 if (status_number.hasOwnProperty(status)) { | |
| 2121 status_number[status] += 1; | |
| 2122 } else { | |
| 2123 status_number[status] = 1; | |
| 2124 } | |
| 2125 }); | |
| 2126 | |
| 2127 function status_class(status) | |
| 2128 { | |
| 2129 return status.replace(/\s/g, '').toLowerCase(); | |
| 2130 } | |
| 2131 | |
| 2132 var summary_template = ["section", {"id":"summary"}, | |
| 2133 ["h2", {}, "Summary"], | |
| 2134 function() | |
| 2135 { | |
| 2136 | |
| 2137 var status = status_text_harness[harness_sta
tus.status]; | |
| 2138 var rv = [["section", {}, | |
| 2139 ["p", {}, | |
| 2140 "Harness status: ", | |
| 2141 ["span", {"class":status_class(s
tatus)}, | |
| 2142 status | |
| 2143 ], | |
| 2144 ] | |
| 2145 ]]; | |
| 2146 | |
| 2147 if (harness_status.status === harness_status
.ERROR) { | |
| 2148 rv[0].push(["pre", {}, harness_status.me
ssage]); | |
| 2149 if (harness_status.stack) { | |
| 2150 rv[0].push(["pre", {}, harness_statu
s.stack]); | |
| 2151 } | |
| 2152 } | |
| 2153 return rv; | |
| 2154 }, | |
| 2155 ["p", {}, "Found ${num_tests} tests"], | |
| 2156 function() { | |
| 2157 var rv = [["div", {}]]; | |
| 2158 var i = 0; | |
| 2159 while (status_text.hasOwnProperty(i)) { | |
| 2160 if (status_number.hasOwnProperty(status_
text[i])) { | |
| 2161 var status = status_text[i]; | |
| 2162 rv[0].push(["div", {"class":status_c
lass(status)}, | |
| 2163 ["label", {}, | |
| 2164 ["input", {type:"checkb
ox", checked:"checked"}], | |
| 2165 status_number[status] +
" " + status]]); | |
| 2166 } | |
| 2167 i++; | |
| 2168 } | |
| 2169 return rv; | |
| 2170 }, | |
| 2171 ]; | |
| 2172 | |
| 2173 log.appendChild(render(summary_template, {num_tests:tests.length}, outpu
t_document)); | |
| 2174 | |
| 2175 forEach(output_document.querySelectorAll("section#summary label"), | |
| 2176 function(element) | |
| 2177 { | |
| 2178 on_event(element, "click", | |
| 2179 function(e) | |
| 2180 { | |
| 2181 if (output_document.getElementById("results") =
== null) { | |
| 2182 e.preventDefault(); | |
| 2183 return; | |
| 2184 } | |
| 2185 var result_class = element.parentNode.getAttrib
ute("class"); | |
| 2186 var style_element = output_document.querySelect
or("style#hide-" + result_class); | |
| 2187 var input_element = element.querySelector("inpu
t"); | |
| 2188 if (!style_element && !input_element.checked) { | |
| 2189 style_element = output_document.createEleme
ntNS(xhtml_ns, "style"); | |
| 2190 style_element.id = "hide-" + result_class; | |
| 2191 style_element.textContent = "table#results
> tbody > tr."+result_class+"{display:none}"; | |
| 2192 output_document.body.appendChild(style_elem
ent); | |
| 2193 } else if (style_element && input_element.check
ed) { | |
| 2194 style_element.parentNode.removeChild(style_
element); | |
| 2195 } | |
| 2196 }); | |
| 2197 }); | |
| 2198 | |
| 2199 // This use of innerHTML plus manual escaping is not recommended in | |
| 2200 // general, but is necessary here for performance. Using textContent | |
| 2201 // on each individual <td> adds tens of seconds of execution time for | |
| 2202 // large test suites (tens of thousands of tests). | |
| 2203 function escape_html(s) | |
| 2204 { | |
| 2205 return s.replace(/\&/g, "&") | |
| 2206 .replace(/</g, "<") | |
| 2207 .replace(/"/g, """) | |
| 2208 .replace(/'/g, "'"); | |
| 2209 } | |
| 2210 | |
| 2211 function has_assertions() | |
| 2212 { | |
| 2213 for (var i = 0; i < tests.length; i++) { | |
| 2214 if (tests[i].properties.hasOwnProperty("assert")) { | |
| 2215 return true; | |
| 2216 } | |
| 2217 } | |
| 2218 return false; | |
| 2219 } | |
| 2220 | |
| 2221 function get_assertion(test) | |
| 2222 { | |
| 2223 if (test.properties.hasOwnProperty("assert")) { | |
| 2224 if (Array.isArray(test.properties.assert)) { | |
| 2225 return test.properties.assert.join(' '); | |
| 2226 } | |
| 2227 return test.properties.assert; | |
| 2228 } | |
| 2229 return ''; | |
| 2230 } | |
| 2231 | |
| 2232 log.appendChild(document.createElementNS(xhtml_ns, "section")); | |
| 2233 var assertions = has_assertions(); | |
| 2234 var html = "<h2>Details</h2><table id='results' " + (assertions ? "class
='assertions'" : "" ) + ">" + | |
| 2235 "<thead><tr><th>Result</th><th>Test Name</th>" + | |
| 2236 (assertions ? "<th>Assertion</th>" : "") + | |
| 2237 "<th>Message</th></tr></thead>" + | |
| 2238 "<tbody>"; | |
| 2239 for (var i = 0; i < tests.length; i++) { | |
| 2240 html += '<tr class="' + | |
| 2241 escape_html(status_class(status_text[tests[i].status])) + | |
| 2242 '"><td>' + | |
| 2243 escape_html(status_text[tests[i].status]) + | |
| 2244 "</td><td>" + | |
| 2245 escape_html(tests[i].name) + | |
| 2246 "</td><td>" + | |
| 2247 (assertions ? escape_html(get_assertion(tests[i])) + "</td><td>"
: "") + | |
| 2248 escape_html(tests[i].message ? tests[i].message : " ") + | |
| 2249 (tests[i].stack ? "<pre>" + | |
| 2250 escape_html(tests[i].stack) + | |
| 2251 "</pre>": "") + | |
| 2252 "</td></tr>"; | |
| 2253 } | |
| 2254 html += "</tbody></table>"; | |
| 2255 try { | |
| 2256 log.lastChild.innerHTML = html; | |
| 2257 } catch (e) { | |
| 2258 log.appendChild(document.createElementNS(xhtml_ns, "p")) | |
| 2259 .textContent = "Setting innerHTML for the log threw an exception.
"; | |
| 2260 log.appendChild(document.createElementNS(xhtml_ns, "pre")) | |
| 2261 .textContent = html; | |
| 2262 } | |
| 2263 }; | |
| 2264 | |
| 2265 /* | |
| 2266 * Template code | |
| 2267 * | |
| 2268 * A template is just a javascript structure. An element is represented as: | |
| 2269 * | |
| 2270 * [tag_name, {attr_name:attr_value}, child1, child2] | |
| 2271 * | |
| 2272 * the children can either be strings (which act like text nodes), other tem
plates or | |
| 2273 * functions (see below) | |
| 2274 * | |
| 2275 * A text node is represented as | |
| 2276 * | |
| 2277 * ["{text}", value] | |
| 2278 * | |
| 2279 * String values have a simple substitution syntax; ${foo} represents a vari
able foo. | |
| 2280 * | |
| 2281 * It is possible to embed logic in templates by using a function in a place
where a | |
| 2282 * node would usually go. The function must either return part of a template
or null. | |
| 2283 * | |
| 2284 * In cases where a set of nodes are required as output rather than a single
node | |
| 2285 * with children it is possible to just use a list | |
| 2286 * [node1, node2, node3] | |
| 2287 * | |
| 2288 * Usage: | |
| 2289 * | |
| 2290 * render(template, substitutions) - take a template and an object mapping | |
| 2291 * variable names to parameters and return either a DOM node or a list of DO
M nodes | |
| 2292 * | |
| 2293 * substitute(template, substitutions) - take a template and variable mappin
g object, | |
| 2294 * make the variable substitutions and return the substituted template | |
| 2295 * | |
| 2296 */ | |
| 2297 | |
| 2298 function is_single_node(template) | |
| 2299 { | |
| 2300 return typeof template[0] === "string"; | |
| 2301 } | |
| 2302 | |
| 2303 function substitute(template, substitutions) | |
| 2304 { | |
| 2305 if (typeof template === "function") { | |
| 2306 var replacement = template(substitutions); | |
| 2307 if (!replacement) { | |
| 2308 return null; | |
| 2309 } | |
| 2310 | |
| 2311 return substitute(replacement, substitutions); | |
| 2312 } | |
| 2313 | |
| 2314 if (is_single_node(template)) { | |
| 2315 return substitute_single(template, substitutions); | |
| 2316 } | |
| 2317 | |
| 2318 return filter(map(template, function(x) { | |
| 2319 return substitute(x, substitutions); | |
| 2320 }), function(x) {return x !== null;}); | |
| 2321 } | |
| 2322 | |
| 2323 function substitute_single(template, substitutions) | |
| 2324 { | |
| 2325 var substitution_re = /\$\{([^ }]*)\}/g; | |
| 2326 | |
| 2327 function do_substitution(input) { | |
| 2328 var components = input.split(substitution_re); | |
| 2329 var rv = []; | |
| 2330 for (var i = 0; i < components.length; i += 2) { | |
| 2331 rv.push(components[i]); | |
| 2332 if (components[i + 1]) { | |
| 2333 rv.push(String(substitutions[components[i + 1]])); | |
| 2334 } | |
| 2335 } | |
| 2336 return rv; | |
| 2337 } | |
| 2338 | |
| 2339 function substitute_attrs(attrs, rv) | |
| 2340 { | |
| 2341 rv[1] = {}; | |
| 2342 for (var name in template[1]) { | |
| 2343 if (attrs.hasOwnProperty(name)) { | |
| 2344 var new_name = do_substitution(name).join(""); | |
| 2345 var new_value = do_substitution(attrs[name]).join(""); | |
| 2346 rv[1][new_name] = new_value; | |
| 2347 } | |
| 2348 } | |
| 2349 } | |
| 2350 | |
| 2351 function substitute_children(children, rv) | |
| 2352 { | |
| 2353 for (var i = 0; i < children.length; i++) { | |
| 2354 if (children[i] instanceof Object) { | |
| 2355 var replacement = substitute(children[i], substitutions); | |
| 2356 if (replacement !== null) { | |
| 2357 if (is_single_node(replacement)) { | |
| 2358 rv.push(replacement); | |
| 2359 } else { | |
| 2360 extend(rv, replacement); | |
| 2361 } | |
| 2362 } | |
| 2363 } else { | |
| 2364 extend(rv, do_substitution(String(children[i]))); | |
| 2365 } | |
| 2366 } | |
| 2367 return rv; | |
| 2368 } | |
| 2369 | |
| 2370 var rv = []; | |
| 2371 rv.push(do_substitution(String(template[0])).join("")); | |
| 2372 | |
| 2373 if (template[0] === "{text}") { | |
| 2374 substitute_children(template.slice(1), rv); | |
| 2375 } else { | |
| 2376 substitute_attrs(template[1], rv); | |
| 2377 substitute_children(template.slice(2), rv); | |
| 2378 } | |
| 2379 | |
| 2380 return rv; | |
| 2381 } | |
| 2382 | |
| 2383 function make_dom_single(template, doc) | |
| 2384 { | |
| 2385 var output_document = doc || document; | |
| 2386 var element; | |
| 2387 if (template[0] === "{text}") { | |
| 2388 element = output_document.createTextNode(""); | |
| 2389 for (var i = 1; i < template.length; i++) { | |
| 2390 element.data += template[i]; | |
| 2391 } | |
| 2392 } else { | |
| 2393 element = output_document.createElementNS(xhtml_ns, template[0]); | |
| 2394 for (var name in template[1]) { | |
| 2395 if (template[1].hasOwnProperty(name)) { | |
| 2396 element.setAttribute(name, template[1][name]); | |
| 2397 } | |
| 2398 } | |
| 2399 for (var i = 2; i < template.length; i++) { | |
| 2400 if (template[i] instanceof Object) { | |
| 2401 var sub_element = make_dom(template[i]); | |
| 2402 element.appendChild(sub_element); | |
| 2403 } else { | |
| 2404 var text_node = output_document.createTextNode(template[i]); | |
| 2405 element.appendChild(text_node); | |
| 2406 } | |
| 2407 } | |
| 2408 } | |
| 2409 | |
| 2410 return element; | |
| 2411 } | |
| 2412 | |
| 2413 function make_dom(template, substitutions, output_document) | |
| 2414 { | |
| 2415 if (is_single_node(template)) { | |
| 2416 return make_dom_single(template, output_document); | |
| 2417 } | |
| 2418 | |
| 2419 return map(template, function(x) { | |
| 2420 return make_dom_single(x, output_document); | |
| 2421 }); | |
| 2422 } | |
| 2423 | |
| 2424 function render(template, substitutions, output_document) | |
| 2425 { | |
| 2426 return make_dom(substitute(template, substitutions), output_document); | |
| 2427 } | |
| 2428 | |
| 2429 /* | |
| 2430 * Utility funcions | |
| 2431 */ | |
| 2432 function assert(expected_true, function_name, description, error, substituti
ons) | |
| 2433 { | |
| 2434 if (tests.tests.length === 0) { | |
| 2435 tests.set_file_is_test(); | |
| 2436 } | |
| 2437 if (expected_true !== true) { | |
| 2438 var msg = make_message(function_name, description, | |
| 2439 error, substitutions); | |
| 2440 throw new AssertionError(msg); | |
| 2441 } | |
| 2442 } | |
| 2443 | |
| 2444 function AssertionError(message) | |
| 2445 { | |
| 2446 this.message = message; | |
| 2447 this.stack = this.get_stack(); | |
| 2448 } | |
| 2449 | |
| 2450 AssertionError.prototype = Object.create(Error.prototype); | |
| 2451 | |
| 2452 AssertionError.prototype.get_stack = function() { | |
| 2453 var stack = new Error().stack; | |
| 2454 // IE11 does not initialize 'Error.stack' until the object is thrown. | |
| 2455 if (!stack) { | |
| 2456 try { | |
| 2457 throw new Error(); | |
| 2458 } catch (e) { | |
| 2459 stack = e.stack; | |
| 2460 } | |
| 2461 } | |
| 2462 | |
| 2463 var lines = stack.split("\n"); | |
| 2464 | |
| 2465 // Create a pattern to match stack frames originating within testharness
.js. These include the | |
| 2466 // script URL, followed by the line/col (e.g., '/resources/testharness.j
s:120:21'). | |
| 2467 var re = new RegExp((get_script_url() || "\\btestharness.js") + ":\\d+:\
\d+"); | |
| 2468 | |
| 2469 // Some browsers include a preamble that specifies the type of the error
object. Skip this by | |
| 2470 // advancing until we find the first stack frame originating from testha
rness.js. | |
| 2471 var i = 0; | |
| 2472 while (!re.test(lines[i]) && i < lines.length) { | |
| 2473 i++; | |
| 2474 } | |
| 2475 | |
| 2476 // Then skip the top frames originating from testharness.js to begin the
stack at the test code. | |
| 2477 while (re.test(lines[i]) && i < lines.length) { | |
| 2478 i++; | |
| 2479 } | |
| 2480 | |
| 2481 // Paranoid check that we didn't skip all frames. If so, return the ori
ginal stack unmodified. | |
| 2482 if (i >= lines.length) { | |
| 2483 return stack; | |
| 2484 } | |
| 2485 | |
| 2486 return lines.slice(i).join("\n"); | |
| 2487 } | |
| 2488 | |
| 2489 function make_message(function_name, description, error, substitutions) | |
| 2490 { | |
| 2491 for (var p in substitutions) { | |
| 2492 if (substitutions.hasOwnProperty(p)) { | |
| 2493 substitutions[p] = format_value(substitutions[p]); | |
| 2494 } | |
| 2495 } | |
| 2496 var node_form = substitute(["{text}", "${function_name}: ${description}"
+ error], | |
| 2497 merge({function_name:function_name, | |
| 2498 description:(description?description +
" ":"")}, | |
| 2499 substitutions)); | |
| 2500 return node_form.slice(1).join(""); | |
| 2501 } | |
| 2502 | |
| 2503 function filter(array, callable, thisObj) { | |
| 2504 var rv = []; | |
| 2505 for (var i = 0; i < array.length; i++) { | |
| 2506 if (array.hasOwnProperty(i)) { | |
| 2507 var pass = callable.call(thisObj, array[i], i, array); | |
| 2508 if (pass) { | |
| 2509 rv.push(array[i]); | |
| 2510 } | |
| 2511 } | |
| 2512 } | |
| 2513 return rv; | |
| 2514 } | |
| 2515 | |
| 2516 function map(array, callable, thisObj) | |
| 2517 { | |
| 2518 var rv = []; | |
| 2519 rv.length = array.length; | |
| 2520 for (var i = 0; i < array.length; i++) { | |
| 2521 if (array.hasOwnProperty(i)) { | |
| 2522 rv[i] = callable.call(thisObj, array[i], i, array); | |
| 2523 } | |
| 2524 } | |
| 2525 return rv; | |
| 2526 } | |
| 2527 | |
| 2528 function extend(array, items) | |
| 2529 { | |
| 2530 Array.prototype.push.apply(array, items); | |
| 2531 } | |
| 2532 | |
| 2533 function forEach(array, callback, thisObj) | |
| 2534 { | |
| 2535 for (var i = 0; i < array.length; i++) { | |
| 2536 if (array.hasOwnProperty(i)) { | |
| 2537 callback.call(thisObj, array[i], i, array); | |
| 2538 } | |
| 2539 } | |
| 2540 } | |
| 2541 | |
| 2542 function merge(a,b) | |
| 2543 { | |
| 2544 var rv = {}; | |
| 2545 var p; | |
| 2546 for (p in a) { | |
| 2547 rv[p] = a[p]; | |
| 2548 } | |
| 2549 for (p in b) { | |
| 2550 rv[p] = b[p]; | |
| 2551 } | |
| 2552 return rv; | |
| 2553 } | |
| 2554 | |
| 2555 function expose(object, name) | |
| 2556 { | |
| 2557 var components = name.split("."); | |
| 2558 var target = test_environment.global_scope(); | |
| 2559 for (var i = 0; i < components.length - 1; i++) { | |
| 2560 if (!(components[i] in target)) { | |
| 2561 target[components[i]] = {}; | |
| 2562 } | |
| 2563 target = target[components[i]]; | |
| 2564 } | |
| 2565 target[components[components.length - 1]] = object; | |
| 2566 } | |
| 2567 | |
| 2568 function is_same_origin(w) { | |
| 2569 try { | |
| 2570 'random_prop' in w; | |
| 2571 return true; | |
| 2572 } catch (e) { | |
| 2573 return false; | |
| 2574 } | |
| 2575 } | |
| 2576 | |
| 2577 /** Returns the 'src' URL of the first <script> tag in the page to include t
he file 'testharness.js'. */ | |
| 2578 function get_script_url() | |
| 2579 { | |
| 2580 if (!('document' in self)) { | |
| 2581 return undefined; | |
| 2582 } | |
| 2583 | |
| 2584 var scripts = document.getElementsByTagName("script"); | |
| 2585 for (var i = 0; i < scripts.length; i++) { | |
| 2586 var src; | |
| 2587 if (scripts[i].src) { | |
| 2588 src = scripts[i].src; | |
| 2589 } else if (scripts[i].href) { | |
| 2590 //SVG case | |
| 2591 src = scripts[i].href.baseVal; | |
| 2592 } | |
| 2593 | |
| 2594 var matches = src && src.match(/^(.*\/|)testharness\.js$/); | |
| 2595 if (matches) { | |
| 2596 return src; | |
| 2597 } | |
| 2598 } | |
| 2599 return undefined; | |
| 2600 } | |
| 2601 | |
| 2602 /** Returns the URL path at which the files for testharness.js are assumed t
o reside (e.g., '/resources/'). | |
| 2603 The path is derived from inspecting the 'src' of the <script> tag that i
ncluded 'testharness.js'. */ | |
| 2604 function get_harness_url() | |
| 2605 { | |
| 2606 var script_url = get_script_url(); | |
| 2607 | |
| 2608 // Exclude the 'testharness.js' file from the returned path, but '+ 1' t
o include the trailing slash. | |
| 2609 return script_url ? script_url.slice(0, script_url.lastIndexOf('/') + 1)
: undefined; | |
| 2610 } | |
| 2611 | |
| 2612 function supports_post_message(w) | |
| 2613 { | |
| 2614 var supports; | |
| 2615 var type; | |
| 2616 // Given IE implements postMessage across nested iframes but not across | |
| 2617 // windows or tabs, you can't infer cross-origin communication from the
presence | |
| 2618 // of postMessage on the current window object only. | |
| 2619 // | |
| 2620 // Touching the postMessage prop on a window can throw if the window is | |
| 2621 // not from the same origin AND post message is not supported in that | |
| 2622 // browser. So just doing an existence test here won't do, you also need | |
| 2623 // to wrap it in a try..cacth block. | |
| 2624 try { | |
| 2625 type = typeof w.postMessage; | |
| 2626 if (type === "function") { | |
| 2627 supports = true; | |
| 2628 } | |
| 2629 | |
| 2630 // IE8 supports postMessage, but implements it as a host object whic
h | |
| 2631 // returns "object" as its `typeof`. | |
| 2632 else if (type === "object") { | |
| 2633 supports = true; | |
| 2634 } | |
| 2635 | |
| 2636 // This is the case where postMessage isn't supported AND accessing
a | |
| 2637 // window property across origins does NOT throw (e.g. old Safari br
owser). | |
| 2638 else { | |
| 2639 supports = false; | |
| 2640 } | |
| 2641 } catch (e) { | |
| 2642 // This is the case where postMessage isn't supported AND accessing
a | |
| 2643 // window property across origins throws (e.g. old Firefox browser). | |
| 2644 supports = false; | |
| 2645 } | |
| 2646 return supports; | |
| 2647 } | |
| 2648 | |
| 2649 /** | |
| 2650 * Setup globals | |
| 2651 */ | |
| 2652 | |
| 2653 var tests = new Tests(); | |
| 2654 | |
| 2655 addEventListener("error", function(e) { | |
| 2656 if (tests.file_is_test) { | |
| 2657 var test = tests.tests[0]; | |
| 2658 if (test.phase >= test.phases.HAS_RESULT) { | |
| 2659 return; | |
| 2660 } | |
| 2661 test.set_status(test.FAIL, e.message, e.stack); | |
| 2662 test.phase = test.phases.HAS_RESULT; | |
| 2663 test.done(); | |
| 2664 done(); | |
| 2665 } else if (!tests.allow_uncaught_exception) { | |
| 2666 tests.status.status = tests.status.ERROR; | |
| 2667 tests.status.message = e.message; | |
| 2668 tests.status.stack = e.stack; | |
| 2669 } | |
| 2670 }); | |
| 2671 | |
| 2672 test_environment.on_tests_ready(); | |
| 2673 | |
| 2674 })(); | |
| 2675 // vim: set expandtab shiftwidth=4 tabstop=4: | |
| OLD | NEW |