| 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) { | |
| 544 return promise.then(test.unreached_func("Should have rejected.")).catch(
function(e) { | |
| 545 assert_throws(expected, function() { throw e }); | |
| 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 if ("nodeType" in object && | |
| 701 "nodeName" in object && | |
| 702 "nodeValue" in object && | |
| 703 "childNodes" in object) { | |
| 704 try { | |
| 705 object.nodeType; | |
| 706 } catch (e) { | |
| 707 // The object is probably Node.prototype or another prototype | |
| 708 // object that inherits from it, and not a Node instance. | |
| 709 return false; | |
| 710 } | |
| 711 return true; | |
| 712 } | |
| 713 return false; | |
| 714 } | |
| 715 | |
| 716 /* | |
| 717 * Convert a value to a nice, human-readable string | |
| 718 */ | |
| 719 function format_value(val, seen) | |
| 720 { | |
| 721 if (!seen) { | |
| 722 seen = []; | |
| 723 } | |
| 724 if (typeof val === "object" && val !== null) { | |
| 725 if (seen.indexOf(val) >= 0) { | |
| 726 return "[...]"; | |
| 727 } | |
| 728 seen.push(val); | |
| 729 } | |
| 730 if (Array.isArray(val)) { | |
| 731 return "[" + val.map(function(x) {return format_value(x, seen);}).jo
in(", ") + "]"; | |
| 732 } | |
| 733 | |
| 734 switch (typeof val) { | |
| 735 case "string": | |
| 736 val = val.replace("\\", "\\\\"); | |
| 737 for (var i = 0; i < 32; i++) { | |
| 738 var replace = "\\"; | |
| 739 switch (i) { | |
| 740 case 0: replace += "0"; break; | |
| 741 case 1: replace += "x01"; break; | |
| 742 case 2: replace += "x02"; break; | |
| 743 case 3: replace += "x03"; break; | |
| 744 case 4: replace += "x04"; break; | |
| 745 case 5: replace += "x05"; break; | |
| 746 case 6: replace += "x06"; break; | |
| 747 case 7: replace += "x07"; break; | |
| 748 case 8: replace += "b"; break; | |
| 749 case 9: replace += "t"; break; | |
| 750 case 10: replace += "n"; break; | |
| 751 case 11: replace += "v"; break; | |
| 752 case 12: replace += "f"; break; | |
| 753 case 13: replace += "r"; break; | |
| 754 case 14: replace += "x0e"; break; | |
| 755 case 15: replace += "x0f"; break; | |
| 756 case 16: replace += "x10"; break; | |
| 757 case 17: replace += "x11"; break; | |
| 758 case 18: replace += "x12"; break; | |
| 759 case 19: replace += "x13"; break; | |
| 760 case 20: replace += "x14"; break; | |
| 761 case 21: replace += "x15"; break; | |
| 762 case 22: replace += "x16"; break; | |
| 763 case 23: replace += "x17"; break; | |
| 764 case 24: replace += "x18"; break; | |
| 765 case 25: replace += "x19"; break; | |
| 766 case 26: replace += "x1a"; break; | |
| 767 case 27: replace += "x1b"; break; | |
| 768 case 28: replace += "x1c"; break; | |
| 769 case 29: replace += "x1d"; break; | |
| 770 case 30: replace += "x1e"; break; | |
| 771 case 31: replace += "x1f"; break; | |
| 772 } | |
| 773 val = val.replace(RegExp(String.fromCharCode(i), "g"), replace); | |
| 774 } | |
| 775 return '"' + val.replace(/"/g, '\\"') + '"'; | |
| 776 case "boolean": | |
| 777 case "undefined": | |
| 778 return String(val); | |
| 779 case "number": | |
| 780 // In JavaScript, -0 === 0 and String(-0) == "0", so we have to | |
| 781 // special-case. | |
| 782 if (val === -0 && 1/val === -Infinity) { | |
| 783 return "-0"; | |
| 784 } | |
| 785 return String(val); | |
| 786 case "object": | |
| 787 if (val === null) { | |
| 788 return "null"; | |
| 789 } | |
| 790 | |
| 791 // Special-case Node objects, since those come up a lot in my tests.
I | |
| 792 // ignore namespaces. | |
| 793 if (is_node(val)) { | |
| 794 switch (val.nodeType) { | |
| 795 case Node.ELEMENT_NODE: | |
| 796 var ret = "<" + val.localName; | |
| 797 for (var i = 0; i < val.attributes.length; i++) { | |
| 798 ret += " " + val.attributes[i].name + '="' + val.attribu
tes[i].value + '"'; | |
| 799 } | |
| 800 ret += ">" + val.innerHTML + "</" + val.localName + ">"; | |
| 801 return "Element node " + truncate(ret, 60); | |
| 802 case Node.TEXT_NODE: | |
| 803 return 'Text node "' + truncate(val.data, 60) + '"'; | |
| 804 case Node.PROCESSING_INSTRUCTION_NODE: | |
| 805 return "ProcessingInstruction node with target " + format_va
lue(truncate(val.target, 60)) + " and data " + format_value(truncate(val.data, 6
0)); | |
| 806 case Node.COMMENT_NODE: | |
| 807 return "Comment node <!--" + truncate(val.data, 60) + "-->"; | |
| 808 case Node.DOCUMENT_NODE: | |
| 809 return "Document node with " + val.childNodes.length + (val.
childNodes.length == 1 ? " child" : " children"); | |
| 810 case Node.DOCUMENT_TYPE_NODE: | |
| 811 return "DocumentType node"; | |
| 812 case Node.DOCUMENT_FRAGMENT_NODE: | |
| 813 return "DocumentFragment node with " + val.childNodes.length
+ (val.childNodes.length == 1 ? " child" : " children"); | |
| 814 default: | |
| 815 return "Node object of unknown type"; | |
| 816 } | |
| 817 } | |
| 818 | |
| 819 /* falls through */ | |
| 820 default: | |
| 821 return typeof val + ' "' + truncate(String(val), 60) + '"'; | |
| 822 } | |
| 823 } | |
| 824 expose(format_value, "format_value"); | |
| 825 | |
| 826 /* | |
| 827 * Assertions | |
| 828 */ | |
| 829 | |
| 830 function assert_true(actual, description) | |
| 831 { | |
| 832 assert(actual === true, "assert_true", description, | |
| 833 "expected true got ${actual}", {actual:actual}); | |
| 834 } | |
| 835 expose(assert_true, "assert_true"); | |
| 836 | |
| 837 function assert_false(actual, description) | |
| 838 { | |
| 839 assert(actual === false, "assert_false", description, | |
| 840 "expected false got ${actual}", {actual:actual}
); | |
| 841 } | |
| 842 expose(assert_false, "assert_false"); | |
| 843 | |
| 844 function same_value(x, y) { | |
| 845 if (y !== y) { | |
| 846 //NaN case | |
| 847 return x !== x; | |
| 848 } | |
| 849 if (x === 0 && y === 0) { | |
| 850 //Distinguish +0 and -0 | |
| 851 return 1/x === 1/y; | |
| 852 } | |
| 853 return x === y; | |
| 854 } | |
| 855 | |
| 856 function assert_equals(actual, expected, description) | |
| 857 { | |
| 858 /* | |
| 859 * Test if two primitives are equal or two objects | |
| 860 * are the same object | |
| 861 */ | |
| 862 if (typeof actual != typeof expected) { | |
| 863 assert(false, "assert_equals", description, | |
| 864 "expected (" + typeof expected + ") ${expected} but go
t (" + typeof actual + ") ${actual}", | |
| 865 {expected:expected, actual:actual}); | |
| 866 return; | |
| 867 } | |
| 868 assert(same_value(actual, expected), "assert_equals", description, | |
| 869 "expected ${expected} but got ${act
ual}", | |
| 870 {expected:expected, actual:actual})
; | |
| 871 } | |
| 872 expose(assert_equals, "assert_equals"); | |
| 873 | |
| 874 function assert_not_equals(actual, expected, description) | |
| 875 { | |
| 876 /* | |
| 877 * Test if two primitives are unequal or two objects | |
| 878 * are different objects | |
| 879 */ | |
| 880 assert(!same_value(actual, expected), "assert_not_equals", description, | |
| 881 "got disallowed value ${actual}", | |
| 882 {actual:actual}); | |
| 883 } | |
| 884 expose(assert_not_equals, "assert_not_equals"); | |
| 885 | |
| 886 function assert_in_array(actual, expected, description) | |
| 887 { | |
| 888 assert(expected.indexOf(actual) != -1, "assert_in_array", description, | |
| 889 "value ${actual} not in array ${e
xpected}", | |
| 890 {actual:actual, expected:expected
}); | |
| 891 } | |
| 892 expose(assert_in_array, "assert_in_array"); | |
| 893 | |
| 894 function assert_object_equals(actual, expected, description) | |
| 895 { | |
| 896 //This needs to be improved a great deal | |
| 897 function check_equal(actual, expected, stack) | |
| 898 { | |
| 899 stack.push(actual); | |
| 900 | |
| 901 var p; | |
| 902 for (p in actual) { | |
| 903 assert(expected.hasOwnProperty(p), "assert_object_equals", desc
ription, | |
| 904 "unexpected property ${p}",
{p:p}); | |
| 905 | |
| 906 if (typeof actual[p] === "object" && actual[p] !== null) { | |
| 907 if (stack.indexOf(actual[p]) === -1) { | |
| 908 check_equal(actual[p], expected[p], stack); | |
| 909 } | |
| 910 } else { | |
| 911 assert(same_value(actual[p], expected[p]), "assert_object_e
quals", description, | |
| 912 "property ${p} expected $
{expected} got ${actual}", | |
| 913 {p:p, expected:expected,
actual:actual}); | |
| 914 } | |
| 915 } | |
| 916 for (p in expected) { | |
| 917 assert(actual.hasOwnProperty(p), | |
| 918 "assert_object_equals", description, | |
| 919 "expected property ${p} missing", {p:p}); | |
| 920 } | |
| 921 stack.pop(); | |
| 922 } | |
| 923 check_equal(actual, expected, []); | |
| 924 } | |
| 925 expose(assert_object_equals, "assert_object_equals"); | |
| 926 | |
| 927 function assert_array_equals(actual, expected, description) | |
| 928 { | |
| 929 assert(actual.length === expected.length, | |
| 930 "assert_array_equals", description, | |
| 931 "lengths differ, expected ${expected} got ${actual}", | |
| 932 {expected:expected.length, actual:actual.length}); | |
| 933 | |
| 934 for (var i = 0; i < actual.length; i++) { | |
| 935 assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i), | |
| 936 "assert_array_equals", description, | |
| 937 "property ${i}, property expected to be ${expected} but was $
{actual}", | |
| 938 {i:i, expected:expected.hasOwnProperty(i) ? "present" : "miss
ing", | |
| 939 actual:actual.hasOwnProperty(i) ? "present" : "missing"}); | |
| 940 assert(same_value(expected[i], actual[i]), | |
| 941 "assert_array_equals", description, | |
| 942 "property ${i}, expected ${expected} but got ${actual}", | |
| 943 {i:i, expected:expected[i], actual:actual[i]}); | |
| 944 } | |
| 945 } | |
| 946 expose(assert_array_equals, "assert_array_equals"); | |
| 947 | |
| 948 function assert_approx_equals(actual, expected, epsilon, description) | |
| 949 { | |
| 950 /* | |
| 951 * Test if two primitive numbers are equal withing +/- epsilon | |
| 952 */ | |
| 953 assert(typeof actual === "number", | |
| 954 "assert_approx_equals", description, | |
| 955 "expected a number but got a ${type_actual}", | |
| 956 {type_actual:typeof actual}); | |
| 957 | |
| 958 assert(Math.abs(actual - expected) <= epsilon, | |
| 959 "assert_approx_equals", description, | |
| 960 "expected ${expected} +/- ${epsilon} but got ${actual}", | |
| 961 {expected:expected, actual:actual, epsilon:epsilon}); | |
| 962 } | |
| 963 expose(assert_approx_equals, "assert_approx_equals"); | |
| 964 | |
| 965 function assert_less_than(actual, expected, description) | |
| 966 { | |
| 967 /* | |
| 968 * Test if a primitive number is less than another | |
| 969 */ | |
| 970 assert(typeof actual === "number", | |
| 971 "assert_less_than", description, | |
| 972 "expected a number but got a ${type_actual}", | |
| 973 {type_actual:typeof actual}); | |
| 974 | |
| 975 assert(actual < expected, | |
| 976 "assert_less_than", description, | |
| 977 "expected a number less than ${expected} but got ${actual}", | |
| 978 {expected:expected, actual:actual}); | |
| 979 } | |
| 980 expose(assert_less_than, "assert_less_than"); | |
| 981 | |
| 982 function assert_greater_than(actual, expected, description) | |
| 983 { | |
| 984 /* | |
| 985 * Test if a primitive number is greater than another | |
| 986 */ | |
| 987 assert(typeof actual === "number", | |
| 988 "assert_greater_than", description, | |
| 989 "expected a number but got a ${type_actual}", | |
| 990 {type_actual:typeof actual}); | |
| 991 | |
| 992 assert(actual > expected, | |
| 993 "assert_greater_than", description, | |
| 994 "expected a number greater than ${expected} but got ${actual}", | |
| 995 {expected:expected, actual:actual}); | |
| 996 } | |
| 997 expose(assert_greater_than, "assert_greater_than"); | |
| 998 | |
| 999 function assert_between_exclusive(actual, lower, upper, description) | |
| 1000 { | |
| 1001 /* | |
| 1002 * Test if a primitive number is between two others | |
| 1003 */ | |
| 1004 assert(typeof actual === "number", | |
| 1005 "assert_between_exclusive", description, | |
| 1006 "expected a number but got a ${type_actual}", | |
| 1007 {type_actual:typeof actual}); | |
| 1008 | |
| 1009 assert(actual > lower && actual < upper, | |
| 1010 "assert_between_exclusive", description, | |
| 1011 "expected a number greater than ${lower} " + | |
| 1012 "and less than ${upper} but got ${actual}", | |
| 1013 {lower:lower, upper:upper, actual:actual}); | |
| 1014 } | |
| 1015 expose(assert_between_exclusive, "assert_between_exclusive"); | |
| 1016 | |
| 1017 function assert_less_than_equal(actual, expected, description) | |
| 1018 { | |
| 1019 /* | |
| 1020 * Test if a primitive number is less than or equal to another | |
| 1021 */ | |
| 1022 assert(typeof actual === "number", | |
| 1023 "assert_less_than_equal", description, | |
| 1024 "expected a number but got a ${type_actual}", | |
| 1025 {type_actual:typeof actual}); | |
| 1026 | |
| 1027 assert(actual <= expected, | |
| 1028 "assert_less_than_equal", description, | |
| 1029 "expected a number less than or equal to ${expected} but got ${ac
tual}", | |
| 1030 {expected:expected, actual:actual}); | |
| 1031 } | |
| 1032 expose(assert_less_than_equal, "assert_less_than_equal"); | |
| 1033 | |
| 1034 function assert_greater_than_equal(actual, expected, description) | |
| 1035 { | |
| 1036 /* | |
| 1037 * Test if a primitive number is greater than or equal to another | |
| 1038 */ | |
| 1039 assert(typeof actual === "number", | |
| 1040 "assert_greater_than_equal", description, | |
| 1041 "expected a number but got a ${type_actual}", | |
| 1042 {type_actual:typeof actual}); | |
| 1043 | |
| 1044 assert(actual >= expected, | |
| 1045 "assert_greater_than_equal", description, | |
| 1046 "expected a number greater than or equal to ${expected} but got $
{actual}", | |
| 1047 {expected:expected, actual:actual}); | |
| 1048 } | |
| 1049 expose(assert_greater_than_equal, "assert_greater_than_equal"); | |
| 1050 | |
| 1051 function assert_between_inclusive(actual, lower, upper, description) | |
| 1052 { | |
| 1053 /* | |
| 1054 * Test if a primitive number is between to two others or equal to eithe
r of them | |
| 1055 */ | |
| 1056 assert(typeof actual === "number", | |
| 1057 "assert_between_inclusive", description, | |
| 1058 "expected a number but got a ${type_actual}", | |
| 1059 {type_actual:typeof actual}); | |
| 1060 | |
| 1061 assert(actual >= lower && actual <= upper, | |
| 1062 "assert_between_inclusive", description, | |
| 1063 "expected a number greater than or equal to ${lower} " + | |
| 1064 "and less than or equal to ${upper} but got ${actual}", | |
| 1065 {lower:lower, upper:upper, actual:actual}); | |
| 1066 } | |
| 1067 expose(assert_between_inclusive, "assert_between_inclusive"); | |
| 1068 | |
| 1069 function assert_regexp_match(actual, expected, description) { | |
| 1070 /* | |
| 1071 * Test if a string (actual) matches a regexp (expected) | |
| 1072 */ | |
| 1073 assert(expected.test(actual), | |
| 1074 "assert_regexp_match", description, | |
| 1075 "expected ${expected} but got ${actual}", | |
| 1076 {expected:expected, actual:actual}); | |
| 1077 } | |
| 1078 expose(assert_regexp_match, "assert_regexp_match"); | |
| 1079 | |
| 1080 function assert_class_string(object, class_string, description) { | |
| 1081 assert_equals({}.toString.call(object), "[object " + class_string + "]", | |
| 1082 description); | |
| 1083 } | |
| 1084 expose(assert_class_string, "assert_class_string"); | |
| 1085 | |
| 1086 | |
| 1087 function _assert_own_property(name) { | |
| 1088 return function(object, property_name, description) | |
| 1089 { | |
| 1090 assert(object.hasOwnProperty(property_name), | |
| 1091 name, description, | |
| 1092 "expected property ${p} missing", {p:property_name}); | |
| 1093 }; | |
| 1094 } | |
| 1095 expose(_assert_own_property("assert_exists"), "assert_exists"); | |
| 1096 expose(_assert_own_property("assert_own_property"), "assert_own_property"); | |
| 1097 | |
| 1098 function assert_not_exists(object, property_name, description) | |
| 1099 { | |
| 1100 assert(!object.hasOwnProperty(property_name), | |
| 1101 "assert_not_exists", description, | |
| 1102 "unexpected property ${p} found", {p:property_name}); | |
| 1103 } | |
| 1104 expose(assert_not_exists, "assert_not_exists"); | |
| 1105 | |
| 1106 function _assert_inherits(name) { | |
| 1107 return function (object, property_name, description) | |
| 1108 { | |
| 1109 assert(typeof object === "object" || typeof object === "function", | |
| 1110 name, description, | |
| 1111 "provided value is not an object"); | |
| 1112 | |
| 1113 assert("hasOwnProperty" in object, | |
| 1114 name, description, | |
| 1115 "provided value is an object but has no hasOwnProperty method
"); | |
| 1116 | |
| 1117 assert(!object.hasOwnProperty(property_name), | |
| 1118 name, description, | |
| 1119 "property ${p} found on object expected in prototype chain", | |
| 1120 {p:property_name}); | |
| 1121 | |
| 1122 assert(property_name in object, | |
| 1123 name, description, | |
| 1124 "property ${p} not found in prototype chain", | |
| 1125 {p:property_name}); | |
| 1126 }; | |
| 1127 } | |
| 1128 expose(_assert_inherits("assert_inherits"), "assert_inherits"); | |
| 1129 expose(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute"); | |
| 1130 | |
| 1131 function assert_readonly(object, property_name, description) | |
| 1132 { | |
| 1133 var initial_value = object[property_name]; | |
| 1134 try { | |
| 1135 //Note that this can have side effects in the case where | |
| 1136 //the property has PutForwards | |
| 1137 object[property_name] = initial_value + "a"; //XXX use some other v
alue here? | |
| 1138 assert(same_value(object[property_name], initial_value), | |
| 1139 "assert_readonly", description, | |
| 1140 "changing property ${p} succeeded", | |
| 1141 {p:property_name}); | |
| 1142 } finally { | |
| 1143 object[property_name] = initial_value; | |
| 1144 } | |
| 1145 } | |
| 1146 expose(assert_readonly, "assert_readonly"); | |
| 1147 | |
| 1148 function assert_throws(code, func, description) | |
| 1149 { | |
| 1150 try { | |
| 1151 func.call(this); | |
| 1152 assert(false, "assert_throws", description, | |
| 1153 "${func} did not throw", {func:func}); | |
| 1154 } catch (e) { | |
| 1155 if (e instanceof AssertionError) { | |
| 1156 throw e; | |
| 1157 } | |
| 1158 if (code === null) { | |
| 1159 return; | |
| 1160 } | |
| 1161 if (typeof code === "object") { | |
| 1162 assert(typeof e == "object" && "name" in e && e.name == code.nam
e, | |
| 1163 "assert_throws", description, | |
| 1164 "${func} threw ${actual} (${actual_name}) expected ${expe
cted} (${expected_name})", | |
| 1165 {func:func, actual:e, actual_name:e.name, | |
| 1166 expected:code, | |
| 1167 expected_name:code.name}); | |
| 1168 return; | |
| 1169 } | |
| 1170 | |
| 1171 var code_name_map = { | |
| 1172 INDEX_SIZE_ERR: 'IndexSizeError', | |
| 1173 HIERARCHY_REQUEST_ERR: 'HierarchyRequestError', | |
| 1174 WRONG_DOCUMENT_ERR: 'WrongDocumentError', | |
| 1175 INVALID_CHARACTER_ERR: 'InvalidCharacterError', | |
| 1176 NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError', | |
| 1177 NOT_FOUND_ERR: 'NotFoundError', | |
| 1178 NOT_SUPPORTED_ERR: 'NotSupportedError', | |
| 1179 INUSE_ATTRIBUTE_ERR: 'InUseAttributeError', | |
| 1180 INVALID_STATE_ERR: 'InvalidStateError', | |
| 1181 SYNTAX_ERR: 'SyntaxError', | |
| 1182 INVALID_MODIFICATION_ERR: 'InvalidModificationError', | |
| 1183 NAMESPACE_ERR: 'NamespaceError', | |
| 1184 INVALID_ACCESS_ERR: 'InvalidAccessError', | |
| 1185 TYPE_MISMATCH_ERR: 'TypeMismatchError', | |
| 1186 SECURITY_ERR: 'SecurityError', | |
| 1187 NETWORK_ERR: 'NetworkError', | |
| 1188 ABORT_ERR: 'AbortError', | |
| 1189 URL_MISMATCH_ERR: 'URLMismatchError', | |
| 1190 QUOTA_EXCEEDED_ERR: 'QuotaExceededError', | |
| 1191 TIMEOUT_ERR: 'TimeoutError', | |
| 1192 INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError', | |
| 1193 DATA_CLONE_ERR: 'DataCloneError' | |
| 1194 }; | |
| 1195 | |
| 1196 var name = code in code_name_map ? code_name_map[code] : code; | |
| 1197 | |
| 1198 var name_code_map = { | |
| 1199 IndexSizeError: 1, | |
| 1200 HierarchyRequestError: 3, | |
| 1201 WrongDocumentError: 4, | |
| 1202 InvalidCharacterError: 5, | |
| 1203 NoModificationAllowedError: 7, | |
| 1204 NotFoundError: 8, | |
| 1205 NotSupportedError: 9, | |
| 1206 InUseAttributeError: 10, | |
| 1207 InvalidStateError: 11, | |
| 1208 SyntaxError: 12, | |
| 1209 InvalidModificationError: 13, | |
| 1210 NamespaceError: 14, | |
| 1211 InvalidAccessError: 15, | |
| 1212 TypeMismatchError: 17, | |
| 1213 SecurityError: 18, | |
| 1214 NetworkError: 19, | |
| 1215 AbortError: 20, | |
| 1216 URLMismatchError: 21, | |
| 1217 QuotaExceededError: 22, | |
| 1218 TimeoutError: 23, | |
| 1219 InvalidNodeTypeError: 24, | |
| 1220 DataCloneError: 25, | |
| 1221 | |
| 1222 EncodingError: 0, | |
| 1223 NotReadableError: 0, | |
| 1224 UnknownError: 0, | |
| 1225 ConstraintError: 0, | |
| 1226 DataError: 0, | |
| 1227 TransactionInactiveError: 0, | |
| 1228 ReadOnlyError: 0, | |
| 1229 VersionError: 0, | |
| 1230 OperationError: 0, | |
| 1231 }; | |
| 1232 | |
| 1233 if (!(name in name_code_map)) { | |
| 1234 throw new AssertionError('Test bug: unrecognized DOMException co
de "' + code + '" passed to assert_throws()'); | |
| 1235 } | |
| 1236 | |
| 1237 var required_props = { code: name_code_map[name] }; | |
| 1238 | |
| 1239 if (required_props.code === 0 || | |
| 1240 (typeof e == "object" && | |
| 1241 "name" in e && | |
| 1242 e.name !== e.name.toUpperCase() && | |
| 1243 e.name !== "DOMException")) { | |
| 1244 // New style exception: also test the name property. | |
| 1245 required_props.name = name; | |
| 1246 } | |
| 1247 | |
| 1248 //We'd like to test that e instanceof the appropriate interface, | |
| 1249 //but we can't, because we don't know what window it was created | |
| 1250 //in. It might be an instanceof the appropriate interface on some | |
| 1251 //unknown other window. TODO: Work around this somehow? | |
| 1252 | |
| 1253 assert(typeof e == "object", | |
| 1254 "assert_throws", description, | |
| 1255 "${func} threw ${e} with type ${type}, not an object", | |
| 1256 {func:func, e:e, type:typeof e}); | |
| 1257 | |
| 1258 for (var prop in required_props) { | |
| 1259 assert(typeof e == "object" && prop in e && e[prop] == required_
props[prop], | |
| 1260 "assert_throws", description, | |
| 1261 "${func} threw ${e} that is not a DOMException " + code +
": property ${prop} is equal to ${actual}, expected ${expected}", | |
| 1262 {func:func, e:e, prop:prop, actual:e[prop], expected:requ
ired_props[prop]}); | |
| 1263 } | |
| 1264 } | |
| 1265 } | |
| 1266 expose(assert_throws, "assert_throws"); | |
| 1267 | |
| 1268 function assert_unreached(description) { | |
| 1269 assert(false, "assert_unreached", description, | |
| 1270 "Reached unreachable code"); | |
| 1271 } | |
| 1272 expose(assert_unreached, "assert_unreached"); | |
| 1273 | |
| 1274 function assert_any(assert_func, actual, expected_array) | |
| 1275 { | |
| 1276 var args = [].slice.call(arguments, 3); | |
| 1277 var errors = []; | |
| 1278 var passed = false; | |
| 1279 forEach(expected_array, | |
| 1280 function(expected) | |
| 1281 { | |
| 1282 try { | |
| 1283 assert_func.apply(this, [actual, expected].concat(args))
; | |
| 1284 passed = true; | |
| 1285 } catch (e) { | |
| 1286 errors.push(e.message); | |
| 1287 } | |
| 1288 }); | |
| 1289 if (!passed) { | |
| 1290 throw new AssertionError(errors.join("\n\n")); | |
| 1291 } | |
| 1292 } | |
| 1293 expose(assert_any, "assert_any"); | |
| 1294 | |
| 1295 function Test(name, properties) | |
| 1296 { | |
| 1297 if (tests.file_is_test && tests.tests.length) { | |
| 1298 throw new Error("Tried to create a test with file_is_test"); | |
| 1299 } | |
| 1300 this.name = name; | |
| 1301 | |
| 1302 this.phase = this.phases.INITIAL; | |
| 1303 | |
| 1304 this.status = this.NOTRUN; | |
| 1305 this.timeout_id = null; | |
| 1306 this.index = null; | |
| 1307 | |
| 1308 this.properties = properties; | |
| 1309 var timeout = properties.timeout ? properties.timeout : settings.test_ti
meout; | |
| 1310 if (timeout !== null) { | |
| 1311 this.timeout_length = timeout * tests.timeout_multiplier; | |
| 1312 } else { | |
| 1313 this.timeout_length = null; | |
| 1314 } | |
| 1315 | |
| 1316 this.message = null; | |
| 1317 this.stack = null; | |
| 1318 | |
| 1319 this.steps = []; | |
| 1320 | |
| 1321 this.cleanup_callbacks = []; | |
| 1322 | |
| 1323 tests.push(this); | |
| 1324 } | |
| 1325 | |
| 1326 Test.statuses = { | |
| 1327 PASS:0, | |
| 1328 FAIL:1, | |
| 1329 TIMEOUT:2, | |
| 1330 NOTRUN:3 | |
| 1331 }; | |
| 1332 | |
| 1333 Test.prototype = merge({}, Test.statuses); | |
| 1334 | |
| 1335 Test.prototype.phases = { | |
| 1336 INITIAL:0, | |
| 1337 STARTED:1, | |
| 1338 HAS_RESULT:2, | |
| 1339 COMPLETE:3 | |
| 1340 }; | |
| 1341 | |
| 1342 Test.prototype.structured_clone = function() | |
| 1343 { | |
| 1344 if (!this._structured_clone) { | |
| 1345 var msg = this.message; | |
| 1346 msg = msg ? String(msg) : msg; | |
| 1347 this._structured_clone = merge({ | |
| 1348 name:String(this.name), | |
| 1349 properties:merge({}, this.properties), | |
| 1350 }, Test.statuses); | |
| 1351 } | |
| 1352 this._structured_clone.status = this.status; | |
| 1353 this._structured_clone.message = this.message; | |
| 1354 this._structured_clone.stack = this.stack; | |
| 1355 this._structured_clone.index = this.index; | |
| 1356 return this._structured_clone; | |
| 1357 }; | |
| 1358 | |
| 1359 Test.prototype.step = function(func, this_obj) | |
| 1360 { | |
| 1361 if (this.phase > this.phases.STARTED) { | |
| 1362 return; | |
| 1363 } | |
| 1364 this.phase = this.phases.STARTED; | |
| 1365 //If we don't get a result before the harness times out that will be a t
est timout | |
| 1366 this.set_status(this.TIMEOUT, "Test timed out"); | |
| 1367 | |
| 1368 tests.started = true; | |
| 1369 tests.notify_test_state(this); | |
| 1370 | |
| 1371 if (this.timeout_id === null) { | |
| 1372 this.set_timeout(); | |
| 1373 } | |
| 1374 | |
| 1375 this.steps.push(func); | |
| 1376 | |
| 1377 if (arguments.length === 1) { | |
| 1378 this_obj = this; | |
| 1379 } | |
| 1380 | |
| 1381 try { | |
| 1382 return func.apply(this_obj, Array.prototype.slice.call(arguments, 2)
); | |
| 1383 } catch (e) { | |
| 1384 if (this.phase >= this.phases.HAS_RESULT) { | |
| 1385 return; | |
| 1386 } | |
| 1387 var message = String((typeof e === "object" && e !== null) ? e.messa
ge : e); | |
| 1388 var stack = e.stack ? e.stack : null; | |
| 1389 | |
| 1390 this.set_status(this.FAIL, message, stack); | |
| 1391 this.phase = this.phases.HAS_RESULT; | |
| 1392 this.done(); | |
| 1393 } | |
| 1394 }; | |
| 1395 | |
| 1396 Test.prototype.step_func = function(func, this_obj) | |
| 1397 { | |
| 1398 var test_this = this; | |
| 1399 | |
| 1400 if (arguments.length === 1) { | |
| 1401 this_obj = test_this; | |
| 1402 } | |
| 1403 | |
| 1404 return function() | |
| 1405 { | |
| 1406 return test_this.step.apply(test_this, [func, this_obj].concat( | |
| 1407 Array.prototype.slice.call(arguments))); | |
| 1408 }; | |
| 1409 }; | |
| 1410 | |
| 1411 Test.prototype.step_func_done = function(func, this_obj) | |
| 1412 { | |
| 1413 var test_this = this; | |
| 1414 | |
| 1415 if (arguments.length === 1) { | |
| 1416 this_obj = test_this; | |
| 1417 } | |
| 1418 | |
| 1419 return function() | |
| 1420 { | |
| 1421 if (func) { | |
| 1422 test_this.step.apply(test_this, [func, this_obj].concat( | |
| 1423 Array.prototype.slice.call(arguments))); | |
| 1424 } | |
| 1425 test_this.done(); | |
| 1426 }; | |
| 1427 }; | |
| 1428 | |
| 1429 Test.prototype.unreached_func = function(description) | |
| 1430 { | |
| 1431 return this.step_func(function() { | |
| 1432 assert_unreached(description); | |
| 1433 }); | |
| 1434 }; | |
| 1435 | |
| 1436 Test.prototype.step_timeout = function(f, timeout) { | |
| 1437 var test_this = this; | |
| 1438 var args = Array.prototype.slice.call(arguments, 2); | |
| 1439 return setTimeout(this.step_func(function() { | |
| 1440 return f.apply(test_this, args); | |
| 1441 }, timeout * tests.timeout_multiplier)); | |
| 1442 } | |
| 1443 | |
| 1444 Test.prototype.add_cleanup = function(callback) { | |
| 1445 this.cleanup_callbacks.push(callback); | |
| 1446 }; | |
| 1447 | |
| 1448 Test.prototype.force_timeout = function() { | |
| 1449 this.set_status(this.TIMEOUT); | |
| 1450 this.phase = this.phases.HAS_RESULT; | |
| 1451 }; | |
| 1452 | |
| 1453 Test.prototype.set_timeout = function() | |
| 1454 { | |
| 1455 if (this.timeout_length !== null) { | |
| 1456 var this_obj = this; | |
| 1457 this.timeout_id = setTimeout(function() | |
| 1458 { | |
| 1459 this_obj.timeout(); | |
| 1460 }, this.timeout_length); | |
| 1461 } | |
| 1462 }; | |
| 1463 | |
| 1464 Test.prototype.set_status = function(status, message, stack) | |
| 1465 { | |
| 1466 this.status = status; | |
| 1467 this.message = message; | |
| 1468 this.stack = stack ? stack : null; | |
| 1469 }; | |
| 1470 | |
| 1471 Test.prototype.timeout = function() | |
| 1472 { | |
| 1473 this.timeout_id = null; | |
| 1474 this.set_status(this.TIMEOUT, "Test timed out"); | |
| 1475 this.phase = this.phases.HAS_RESULT; | |
| 1476 this.done(); | |
| 1477 }; | |
| 1478 | |
| 1479 Test.prototype.done = function() | |
| 1480 { | |
| 1481 if (this.phase == this.phases.COMPLETE) { | |
| 1482 return; | |
| 1483 } | |
| 1484 | |
| 1485 if (this.phase <= this.phases.STARTED) { | |
| 1486 this.set_status(this.PASS, null); | |
| 1487 } | |
| 1488 | |
| 1489 this.phase = this.phases.COMPLETE; | |
| 1490 | |
| 1491 clearTimeout(this.timeout_id); | |
| 1492 tests.result(this); | |
| 1493 this.cleanup(); | |
| 1494 }; | |
| 1495 | |
| 1496 Test.prototype.cleanup = function() { | |
| 1497 forEach(this.cleanup_callbacks, | |
| 1498 function(cleanup_callback) { | |
| 1499 cleanup_callback(); | |
| 1500 }); | |
| 1501 }; | |
| 1502 | |
| 1503 /* | |
| 1504 * A RemoteTest object mirrors a Test object on a remote worker. The | |
| 1505 * associated RemoteWorker updates the RemoteTest object in response to | |
| 1506 * received events. In turn, the RemoteTest object replicates these events | |
| 1507 * on the local document. This allows listeners (test result reporting | |
| 1508 * etc..) to transparently handle local and remote events. | |
| 1509 */ | |
| 1510 function RemoteTest(clone) { | |
| 1511 var this_obj = this; | |
| 1512 Object.keys(clone).forEach( | |
| 1513 function(key) { | |
| 1514 this_obj[key] = clone[key]; | |
| 1515 }); | |
| 1516 this.index = null; | |
| 1517 this.phase = this.phases.INITIAL; | |
| 1518 this.update_state_from(clone); | |
| 1519 tests.push(this); | |
| 1520 } | |
| 1521 | |
| 1522 RemoteTest.prototype.structured_clone = function() { | |
| 1523 var clone = {}; | |
| 1524 Object.keys(this).forEach( | |
| 1525 (function(key) { | |
| 1526 if (typeof(this[key]) === "object") { | |
| 1527 clone[key] = merge({}, this[key]); | |
| 1528 } else { | |
| 1529 clone[key] = this[key]; | |
| 1530 } | |
| 1531 }).bind(this)); | |
| 1532 clone.phases = merge({}, this.phases); | |
| 1533 return clone; | |
| 1534 }; | |
| 1535 | |
| 1536 RemoteTest.prototype.cleanup = function() {}; | |
| 1537 RemoteTest.prototype.phases = Test.prototype.phases; | |
| 1538 RemoteTest.prototype.update_state_from = function(clone) { | |
| 1539 this.status = clone.status; | |
| 1540 this.message = clone.message; | |
| 1541 this.stack = clone.stack; | |
| 1542 if (this.phase === this.phases.INITIAL) { | |
| 1543 this.phase = this.phases.STARTED; | |
| 1544 } | |
| 1545 }; | |
| 1546 RemoteTest.prototype.done = function() { | |
| 1547 this.phase = this.phases.COMPLETE; | |
| 1548 } | |
| 1549 | |
| 1550 /* | |
| 1551 * A RemoteWorker listens for test events from a worker. These events are | |
| 1552 * then used to construct and maintain RemoteTest objects that mirror the | |
| 1553 * tests running on the remote worker. | |
| 1554 */ | |
| 1555 function RemoteWorker(worker) { | |
| 1556 this.running = true; | |
| 1557 this.tests = new Array(); | |
| 1558 | |
| 1559 var this_obj = this; | |
| 1560 worker.onerror = function(error) { this_obj.worker_error(error); }; | |
| 1561 | |
| 1562 var message_port; | |
| 1563 | |
| 1564 if (is_service_worker(worker)) { | |
| 1565 if (window.MessageChannel) { | |
| 1566 // The ServiceWorker's implicit MessagePort is currently not | |
| 1567 // reliably accessible from the ServiceWorkerGlobalScope due to | |
| 1568 // Blink setting MessageEvent.source to null for messages sent | |
| 1569 // via ServiceWorker.postMessage(). Until that's resolved, | |
| 1570 // create an explicit MessageChannel and pass one end to the | |
| 1571 // worker. | |
| 1572 var message_channel = new MessageChannel(); | |
| 1573 message_port = message_channel.port1; | |
| 1574 message_port.start(); | |
| 1575 worker.postMessage({type: "connect"}, [message_channel.port2]); | |
| 1576 } else { | |
| 1577 // If MessageChannel is not available, then try the | |
| 1578 // ServiceWorker.postMessage() approach using MessageEvent.sourc
e | |
| 1579 // on the other end. | |
| 1580 message_port = navigator.serviceWorker; | |
| 1581 worker.postMessage({type: "connect"}); | |
| 1582 } | |
| 1583 } else if (is_shared_worker(worker)) { | |
| 1584 message_port = worker.port; | |
| 1585 } else { | |
| 1586 message_port = worker; | |
| 1587 } | |
| 1588 | |
| 1589 // Keeping a reference to the worker until worker_done() is seen | |
| 1590 // prevents the Worker object and its MessageChannel from going away | |
| 1591 // before all the messages are dispatched. | |
| 1592 this.worker = worker; | |
| 1593 | |
| 1594 message_port.onmessage = | |
| 1595 function(message) { | |
| 1596 if (this_obj.running && (message.data.type in this_obj.message_h
andlers)) { | |
| 1597 this_obj.message_handlers[message.data.type].call(this_obj,
message.data); | |
| 1598 } | |
| 1599 }; | |
| 1600 } | |
| 1601 | |
| 1602 RemoteWorker.prototype.worker_error = function(error) { | |
| 1603 var message = error.message || String(error); | |
| 1604 var filename = (error.filename ? " " + error.filename: ""); | |
| 1605 // FIXME: Display worker error states separately from main document | |
| 1606 // error state. | |
| 1607 this.worker_done({ | |
| 1608 status: { | |
| 1609 status: tests.status.ERROR, | |
| 1610 message: "Error in worker" + filename + ": " + message, | |
| 1611 stack: error.stack | |
| 1612 } | |
| 1613 }); | |
| 1614 error.preventDefault(); | |
| 1615 }; | |
| 1616 | |
| 1617 RemoteWorker.prototype.test_state = function(data) { | |
| 1618 var remote_test = this.tests[data.test.index]; | |
| 1619 if (!remote_test) { | |
| 1620 remote_test = new RemoteTest(data.test); | |
| 1621 this.tests[data.test.index] = remote_test; | |
| 1622 } | |
| 1623 remote_test.update_state_from(data.test); | |
| 1624 tests.notify_test_state(remote_test); | |
| 1625 }; | |
| 1626 | |
| 1627 RemoteWorker.prototype.test_done = function(data) { | |
| 1628 var remote_test = this.tests[data.test.index]; | |
| 1629 remote_test.update_state_from(data.test); | |
| 1630 remote_test.done(); | |
| 1631 tests.result(remote_test); | |
| 1632 }; | |
| 1633 | |
| 1634 RemoteWorker.prototype.worker_done = function(data) { | |
| 1635 if (tests.status.status === null && | |
| 1636 data.status.status !== data.status.OK) { | |
| 1637 tests.status.status = data.status.status; | |
| 1638 tests.status.message = data.status.message; | |
| 1639 tests.status.stack = data.status.stack; | |
| 1640 } | |
| 1641 this.running = false; | |
| 1642 this.worker = null; | |
| 1643 if (tests.all_done()) { | |
| 1644 tests.complete(); | |
| 1645 } | |
| 1646 }; | |
| 1647 | |
| 1648 RemoteWorker.prototype.message_handlers = { | |
| 1649 test_state: RemoteWorker.prototype.test_state, | |
| 1650 result: RemoteWorker.prototype.test_done, | |
| 1651 complete: RemoteWorker.prototype.worker_done | |
| 1652 }; | |
| 1653 | |
| 1654 /* | |
| 1655 * Harness | |
| 1656 */ | |
| 1657 | |
| 1658 function TestsStatus() | |
| 1659 { | |
| 1660 this.status = null; | |
| 1661 this.message = null; | |
| 1662 this.stack = null; | |
| 1663 } | |
| 1664 | |
| 1665 TestsStatus.statuses = { | |
| 1666 OK:0, | |
| 1667 ERROR:1, | |
| 1668 TIMEOUT:2 | |
| 1669 }; | |
| 1670 | |
| 1671 TestsStatus.prototype = merge({}, TestsStatus.statuses); | |
| 1672 | |
| 1673 TestsStatus.prototype.structured_clone = function() | |
| 1674 { | |
| 1675 if (!this._structured_clone) { | |
| 1676 var msg = this.message; | |
| 1677 msg = msg ? String(msg) : msg; | |
| 1678 this._structured_clone = merge({ | |
| 1679 status:this.status, | |
| 1680 message:msg, | |
| 1681 stack:this.stack | |
| 1682 }, TestsStatus.statuses); | |
| 1683 } | |
| 1684 return this._structured_clone; | |
| 1685 }; | |
| 1686 | |
| 1687 function Tests() | |
| 1688 { | |
| 1689 this.tests = []; | |
| 1690 this.num_pending = 0; | |
| 1691 | |
| 1692 this.phases = { | |
| 1693 INITIAL:0, | |
| 1694 SETUP:1, | |
| 1695 HAVE_TESTS:2, | |
| 1696 HAVE_RESULTS:3, | |
| 1697 COMPLETE:4 | |
| 1698 }; | |
| 1699 this.phase = this.phases.INITIAL; | |
| 1700 | |
| 1701 this.properties = {}; | |
| 1702 | |
| 1703 this.wait_for_finish = false; | |
| 1704 this.processing_callbacks = false; | |
| 1705 | |
| 1706 this.allow_uncaught_exception = false; | |
| 1707 | |
| 1708 this.file_is_test = false; | |
| 1709 | |
| 1710 this.timeout_multiplier = 1; | |
| 1711 this.timeout_length = test_environment.test_timeout(); | |
| 1712 this.timeout_id = null; | |
| 1713 | |
| 1714 this.start_callbacks = []; | |
| 1715 this.test_state_callbacks = []; | |
| 1716 this.test_done_callbacks = []; | |
| 1717 this.all_done_callbacks = []; | |
| 1718 | |
| 1719 this.pending_workers = []; | |
| 1720 | |
| 1721 this.status = new TestsStatus(); | |
| 1722 | |
| 1723 var this_obj = this; | |
| 1724 | |
| 1725 test_environment.add_on_loaded_callback(function() { | |
| 1726 if (this_obj.all_done()) { | |
| 1727 this_obj.complete(); | |
| 1728 } | |
| 1729 }); | |
| 1730 | |
| 1731 this.set_timeout(); | |
| 1732 } | |
| 1733 | |
| 1734 Tests.prototype.setup = function(func, properties) | |
| 1735 { | |
| 1736 if (this.phase >= this.phases.HAVE_RESULTS) { | |
| 1737 return; | |
| 1738 } | |
| 1739 | |
| 1740 if (this.phase < this.phases.SETUP) { | |
| 1741 this.phase = this.phases.SETUP; | |
| 1742 } | |
| 1743 | |
| 1744 this.properties = properties; | |
| 1745 | |
| 1746 for (var p in properties) { | |
| 1747 if (properties.hasOwnProperty(p)) { | |
| 1748 var value = properties[p]; | |
| 1749 if (p == "allow_uncaught_exception") { | |
| 1750 this.allow_uncaught_exception = value; | |
| 1751 } else if (p == "explicit_done" && value) { | |
| 1752 this.wait_for_finish = true; | |
| 1753 } else if (p == "explicit_timeout" && value) { | |
| 1754 this.timeout_length = null; | |
| 1755 if (this.timeout_id) | |
| 1756 { | |
| 1757 clearTimeout(this.timeout_id); | |
| 1758 } | |
| 1759 } else if (p == "timeout_multiplier") { | |
| 1760 this.timeout_multiplier = value; | |
| 1761 } | |
| 1762 } | |
| 1763 } | |
| 1764 | |
| 1765 if (func) { | |
| 1766 try { | |
| 1767 func(); | |
| 1768 } catch (e) { | |
| 1769 this.status.status = this.status.ERROR; | |
| 1770 this.status.message = String(e); | |
| 1771 this.status.stack = e.stack ? e.stack : null; | |
| 1772 } | |
| 1773 } | |
| 1774 this.set_timeout(); | |
| 1775 }; | |
| 1776 | |
| 1777 Tests.prototype.set_file_is_test = function() { | |
| 1778 if (this.tests.length > 0) { | |
| 1779 throw new Error("Tried to set file as test after creating a test"); | |
| 1780 } | |
| 1781 this.wait_for_finish = true; | |
| 1782 this.file_is_test = true; | |
| 1783 // Create the test, which will add it to the list of tests | |
| 1784 async_test(); | |
| 1785 }; | |
| 1786 | |
| 1787 Tests.prototype.set_timeout = function() { | |
| 1788 var this_obj = this; | |
| 1789 clearTimeout(this.timeout_id); | |
| 1790 if (this.timeout_length !== null) { | |
| 1791 this.timeout_id = setTimeout(function() { | |
| 1792 this_obj.timeout(); | |
| 1793 }, this.timeout_length); | |
| 1794 } | |
| 1795 }; | |
| 1796 | |
| 1797 Tests.prototype.timeout = function() { | |
| 1798 if (this.status.status === null) { | |
| 1799 this.status.status = this.status.TIMEOUT; | |
| 1800 } | |
| 1801 this.complete(); | |
| 1802 }; | |
| 1803 | |
| 1804 Tests.prototype.end_wait = function() | |
| 1805 { | |
| 1806 this.wait_for_finish = false; | |
| 1807 if (this.all_done()) { | |
| 1808 this.complete(); | |
| 1809 } | |
| 1810 }; | |
| 1811 | |
| 1812 Tests.prototype.push = function(test) | |
| 1813 { | |
| 1814 if (this.phase < this.phases.HAVE_TESTS) { | |
| 1815 this.start(); | |
| 1816 } | |
| 1817 this.num_pending++; | |
| 1818 test.index = this.tests.push(test); | |
| 1819 this.notify_test_state(test); | |
| 1820 }; | |
| 1821 | |
| 1822 Tests.prototype.notify_test_state = function(test) { | |
| 1823 var this_obj = this; | |
| 1824 forEach(this.test_state_callbacks, | |
| 1825 function(callback) { | |
| 1826 callback(test, this_obj); | |
| 1827 }); | |
| 1828 }; | |
| 1829 | |
| 1830 Tests.prototype.all_done = function() { | |
| 1831 return (this.tests.length > 0 && test_environment.all_loaded && | |
| 1832 this.num_pending === 0 && !this.wait_for_finish && | |
| 1833 !this.processing_callbacks && | |
| 1834 !this.pending_workers.some(function(w) { return w.running; })); | |
| 1835 }; | |
| 1836 | |
| 1837 Tests.prototype.start = function() { | |
| 1838 this.phase = this.phases.HAVE_TESTS; | |
| 1839 this.notify_start(); | |
| 1840 }; | |
| 1841 | |
| 1842 Tests.prototype.notify_start = function() { | |
| 1843 var this_obj = this; | |
| 1844 forEach (this.start_callbacks, | |
| 1845 function(callback) | |
| 1846 { | |
| 1847 callback(this_obj.properties); | |
| 1848 }); | |
| 1849 }; | |
| 1850 | |
| 1851 Tests.prototype.result = function(test) | |
| 1852 { | |
| 1853 if (this.phase > this.phases.HAVE_RESULTS) { | |
| 1854 return; | |
| 1855 } | |
| 1856 this.phase = this.phases.HAVE_RESULTS; | |
| 1857 this.num_pending--; | |
| 1858 this.notify_result(test); | |
| 1859 }; | |
| 1860 | |
| 1861 Tests.prototype.notify_result = function(test) { | |
| 1862 var this_obj = this; | |
| 1863 this.processing_callbacks = true; | |
| 1864 forEach(this.test_done_callbacks, | |
| 1865 function(callback) | |
| 1866 { | |
| 1867 callback(test, this_obj); | |
| 1868 }); | |
| 1869 this.processing_callbacks = false; | |
| 1870 if (this_obj.all_done()) { | |
| 1871 this_obj.complete(); | |
| 1872 } | |
| 1873 }; | |
| 1874 | |
| 1875 Tests.prototype.complete = function() { | |
| 1876 if (this.phase === this.phases.COMPLETE) { | |
| 1877 return; | |
| 1878 } | |
| 1879 this.phase = this.phases.COMPLETE; | |
| 1880 var this_obj = this; | |
| 1881 this.tests.forEach( | |
| 1882 function(x) | |
| 1883 { | |
| 1884 if (x.phase < x.phases.COMPLETE) { | |
| 1885 this_obj.notify_result(x); | |
| 1886 x.cleanup(); | |
| 1887 x.phase = x.phases.COMPLETE; | |
| 1888 } | |
| 1889 } | |
| 1890 ); | |
| 1891 this.notify_complete(); | |
| 1892 }; | |
| 1893 | |
| 1894 Tests.prototype.notify_complete = function() { | |
| 1895 var this_obj = this; | |
| 1896 if (this.status.status === null) { | |
| 1897 this.status.status = this.status.OK; | |
| 1898 } | |
| 1899 | |
| 1900 forEach (this.all_done_callbacks, | |
| 1901 function(callback) | |
| 1902 { | |
| 1903 callback(this_obj.tests, this_obj.status); | |
| 1904 }); | |
| 1905 }; | |
| 1906 | |
| 1907 Tests.prototype.fetch_tests_from_worker = function(worker) { | |
| 1908 if (this.phase >= this.phases.COMPLETE) { | |
| 1909 return; | |
| 1910 } | |
| 1911 | |
| 1912 this.pending_workers.push(new RemoteWorker(worker)); | |
| 1913 }; | |
| 1914 | |
| 1915 function fetch_tests_from_worker(port) { | |
| 1916 tests.fetch_tests_from_worker(port); | |
| 1917 } | |
| 1918 expose(fetch_tests_from_worker, 'fetch_tests_from_worker'); | |
| 1919 | |
| 1920 function timeout() { | |
| 1921 if (tests.timeout_length === null) { | |
| 1922 tests.timeout(); | |
| 1923 } | |
| 1924 } | |
| 1925 expose(timeout, 'timeout'); | |
| 1926 | |
| 1927 function add_start_callback(callback) { | |
| 1928 tests.start_callbacks.push(callback); | |
| 1929 } | |
| 1930 | |
| 1931 function add_test_state_callback(callback) { | |
| 1932 tests.test_state_callbacks.push(callback); | |
| 1933 } | |
| 1934 | |
| 1935 function add_result_callback(callback) { | |
| 1936 tests.test_done_callbacks.push(callback); | |
| 1937 } | |
| 1938 | |
| 1939 function add_completion_callback(callback) { | |
| 1940 tests.all_done_callbacks.push(callback); | |
| 1941 } | |
| 1942 | |
| 1943 expose(add_start_callback, 'add_start_callback'); | |
| 1944 expose(add_test_state_callback, 'add_test_state_callback'); | |
| 1945 expose(add_result_callback, 'add_result_callback'); | |
| 1946 expose(add_completion_callback, 'add_completion_callback'); | |
| 1947 | |
| 1948 function remove(array, item) { | |
| 1949 var index = array.indexOf(item); | |
| 1950 if (index > -1) { | |
| 1951 array.splice(index, 1); | |
| 1952 } | |
| 1953 } | |
| 1954 | |
| 1955 function remove_start_callback(callback) { | |
| 1956 remove(tests.start_callbacks, callback); | |
| 1957 } | |
| 1958 | |
| 1959 function remove_test_state_callback(callback) { | |
| 1960 remove(tests.test_state_callbacks, callback); | |
| 1961 } | |
| 1962 | |
| 1963 function remove_result_callback(callback) { | |
| 1964 remove(tests.test_done_callbacks, callback); | |
| 1965 } | |
| 1966 | |
| 1967 function remove_completion_callback(callback) { | |
| 1968 remove(tests.all_done_callbacks, callback); | |
| 1969 } | |
| 1970 | |
| 1971 /* | |
| 1972 * Output listener | |
| 1973 */ | |
| 1974 | |
| 1975 function Output() { | |
| 1976 this.output_document = document; | |
| 1977 this.output_node = null; | |
| 1978 this.enabled = settings.output; | |
| 1979 this.phase = this.INITIAL; | |
| 1980 } | |
| 1981 | |
| 1982 Output.prototype.INITIAL = 0; | |
| 1983 Output.prototype.STARTED = 1; | |
| 1984 Output.prototype.HAVE_RESULTS = 2; | |
| 1985 Output.prototype.COMPLETE = 3; | |
| 1986 | |
| 1987 Output.prototype.setup = function(properties) { | |
| 1988 if (this.phase > this.INITIAL) { | |
| 1989 return; | |
| 1990 } | |
| 1991 | |
| 1992 //If output is disabled in testharnessreport.js the test shouldn't be | |
| 1993 //able to override that | |
| 1994 this.enabled = this.enabled && (properties.hasOwnProperty("output") ? | |
| 1995 properties.output : settings.output); | |
| 1996 }; | |
| 1997 | |
| 1998 Output.prototype.init = function(properties) { | |
| 1999 if (this.phase >= this.STARTED) { | |
| 2000 return; | |
| 2001 } | |
| 2002 if (properties.output_document) { | |
| 2003 this.output_document = properties.output_document; | |
| 2004 } else { | |
| 2005 this.output_document = document; | |
| 2006 } | |
| 2007 this.phase = this.STARTED; | |
| 2008 }; | |
| 2009 | |
| 2010 Output.prototype.resolve_log = function() { | |
| 2011 var output_document; | |
| 2012 if (typeof this.output_document === "function") { | |
| 2013 output_document = this.output_document.apply(undefined); | |
| 2014 } else { | |
| 2015 output_document = this.output_document; | |
| 2016 } | |
| 2017 if (!output_document) { | |
| 2018 return; | |
| 2019 } | |
| 2020 var node = output_document.getElementById("log"); | |
| 2021 if (!node) { | |
| 2022 if (!document.body || document.readyState == "loading") { | |
| 2023 return; | |
| 2024 } | |
| 2025 node = output_document.createElement("div"); | |
| 2026 node.id = "log"; | |
| 2027 output_document.body.appendChild(node); | |
| 2028 } | |
| 2029 this.output_document = output_document; | |
| 2030 this.output_node = node; | |
| 2031 }; | |
| 2032 | |
| 2033 Output.prototype.show_status = function() { | |
| 2034 if (this.phase < this.STARTED) { | |
| 2035 this.init(); | |
| 2036 } | |
| 2037 if (!this.enabled) { | |
| 2038 return; | |
| 2039 } | |
| 2040 if (this.phase < this.HAVE_RESULTS) { | |
| 2041 this.resolve_log(); | |
| 2042 this.phase = this.HAVE_RESULTS; | |
| 2043 } | |
| 2044 var done_count = tests.tests.length - tests.num_pending; | |
| 2045 if (this.output_node) { | |
| 2046 if (done_count < 100 || | |
| 2047 (done_count < 1000 && done_count % 100 === 0) || | |
| 2048 done_count % 1000 === 0) { | |
| 2049 this.output_node.textContent = "Running, " + | |
| 2050 done_count + " complete, " + | |
| 2051 tests.num_pending + " remain"; | |
| 2052 } | |
| 2053 } | |
| 2054 }; | |
| 2055 | |
| 2056 Output.prototype.show_results = function (tests, harness_status) { | |
| 2057 if (this.phase >= this.COMPLETE) { | |
| 2058 return; | |
| 2059 } | |
| 2060 if (!this.enabled) { | |
| 2061 return; | |
| 2062 } | |
| 2063 if (!this.output_node) { | |
| 2064 this.resolve_log(); | |
| 2065 } | |
| 2066 this.phase = this.COMPLETE; | |
| 2067 | |
| 2068 var log = this.output_node; | |
| 2069 if (!log) { | |
| 2070 return; | |
| 2071 } | |
| 2072 var output_document = this.output_document; | |
| 2073 | |
| 2074 while (log.lastChild) { | |
| 2075 log.removeChild(log.lastChild); | |
| 2076 } | |
| 2077 | |
| 2078 var harness_url = get_harness_url(); | |
| 2079 if (harness_url !== null) { | |
| 2080 var stylesheet = output_document.createElementNS(xhtml_ns, "link"); | |
| 2081 stylesheet.setAttribute("rel", "stylesheet"); | |
| 2082 stylesheet.setAttribute("href", harness_url + "testharness.css"); | |
| 2083 var heads = output_document.getElementsByTagName("head"); | |
| 2084 if (heads.length) { | |
| 2085 heads[0].appendChild(stylesheet); | |
| 2086 } | |
| 2087 } | |
| 2088 | |
| 2089 var status_text_harness = {}; | |
| 2090 status_text_harness[harness_status.OK] = "OK"; | |
| 2091 status_text_harness[harness_status.ERROR] = "Error"; | |
| 2092 status_text_harness[harness_status.TIMEOUT] = "Timeout"; | |
| 2093 | |
| 2094 var status_text = {}; | |
| 2095 status_text[Test.prototype.PASS] = "Pass"; | |
| 2096 status_text[Test.prototype.FAIL] = "Fail"; | |
| 2097 status_text[Test.prototype.TIMEOUT] = "Timeout"; | |
| 2098 status_text[Test.prototype.NOTRUN] = "Not Run"; | |
| 2099 | |
| 2100 var status_number = {}; | |
| 2101 forEach(tests, | |
| 2102 function(test) { | |
| 2103 var status = status_text[test.status]; | |
| 2104 if (status_number.hasOwnProperty(status)) { | |
| 2105 status_number[status] += 1; | |
| 2106 } else { | |
| 2107 status_number[status] = 1; | |
| 2108 } | |
| 2109 }); | |
| 2110 | |
| 2111 function status_class(status) | |
| 2112 { | |
| 2113 return status.replace(/\s/g, '').toLowerCase(); | |
| 2114 } | |
| 2115 | |
| 2116 var summary_template = ["section", {"id":"summary"}, | |
| 2117 ["h2", {}, "Summary"], | |
| 2118 function() | |
| 2119 { | |
| 2120 | |
| 2121 var status = status_text_harness[harness_sta
tus.status]; | |
| 2122 var rv = [["section", {}, | |
| 2123 ["p", {}, | |
| 2124 "Harness status: ", | |
| 2125 ["span", {"class":status_class(s
tatus)}, | |
| 2126 status | |
| 2127 ], | |
| 2128 ] | |
| 2129 ]]; | |
| 2130 | |
| 2131 if (harness_status.status === harness_status
.ERROR) { | |
| 2132 rv[0].push(["pre", {}, harness_status.me
ssage]); | |
| 2133 if (harness_status.stack) { | |
| 2134 rv[0].push(["pre", {}, harness_statu
s.stack]); | |
| 2135 } | |
| 2136 } | |
| 2137 return rv; | |
| 2138 }, | |
| 2139 ["p", {}, "Found ${num_tests} tests"], | |
| 2140 function() { | |
| 2141 var rv = [["div", {}]]; | |
| 2142 var i = 0; | |
| 2143 while (status_text.hasOwnProperty(i)) { | |
| 2144 if (status_number.hasOwnProperty(status_
text[i])) { | |
| 2145 var status = status_text[i]; | |
| 2146 rv[0].push(["div", {"class":status_c
lass(status)}, | |
| 2147 ["label", {}, | |
| 2148 ["input", {type:"checkb
ox", checked:"checked"}], | |
| 2149 status_number[status] +
" " + status]]); | |
| 2150 } | |
| 2151 i++; | |
| 2152 } | |
| 2153 return rv; | |
| 2154 }, | |
| 2155 ]; | |
| 2156 | |
| 2157 log.appendChild(render(summary_template, {num_tests:tests.length}, outpu
t_document)); | |
| 2158 | |
| 2159 forEach(output_document.querySelectorAll("section#summary label"), | |
| 2160 function(element) | |
| 2161 { | |
| 2162 on_event(element, "click", | |
| 2163 function(e) | |
| 2164 { | |
| 2165 if (output_document.getElementById("results") =
== null) { | |
| 2166 e.preventDefault(); | |
| 2167 return; | |
| 2168 } | |
| 2169 var result_class = element.parentNode.getAttrib
ute("class"); | |
| 2170 var style_element = output_document.querySelect
or("style#hide-" + result_class); | |
| 2171 var input_element = element.querySelector("inpu
t"); | |
| 2172 if (!style_element && !input_element.checked) { | |
| 2173 style_element = output_document.createEleme
ntNS(xhtml_ns, "style"); | |
| 2174 style_element.id = "hide-" + result_class; | |
| 2175 style_element.textContent = "table#results
> tbody > tr."+result_class+"{display:none}"; | |
| 2176 output_document.body.appendChild(style_elem
ent); | |
| 2177 } else if (style_element && input_element.check
ed) { | |
| 2178 style_element.parentNode.removeChild(style_
element); | |
| 2179 } | |
| 2180 }); | |
| 2181 }); | |
| 2182 | |
| 2183 // This use of innerHTML plus manual escaping is not recommended in | |
| 2184 // general, but is necessary here for performance. Using textContent | |
| 2185 // on each individual <td> adds tens of seconds of execution time for | |
| 2186 // large test suites (tens of thousands of tests). | |
| 2187 function escape_html(s) | |
| 2188 { | |
| 2189 return s.replace(/\&/g, "&") | |
| 2190 .replace(/</g, "<") | |
| 2191 .replace(/"/g, """) | |
| 2192 .replace(/'/g, "'"); | |
| 2193 } | |
| 2194 | |
| 2195 function has_assertions() | |
| 2196 { | |
| 2197 for (var i = 0; i < tests.length; i++) { | |
| 2198 if (tests[i].properties.hasOwnProperty("assert")) { | |
| 2199 return true; | |
| 2200 } | |
| 2201 } | |
| 2202 return false; | |
| 2203 } | |
| 2204 | |
| 2205 function get_assertion(test) | |
| 2206 { | |
| 2207 if (test.properties.hasOwnProperty("assert")) { | |
| 2208 if (Array.isArray(test.properties.assert)) { | |
| 2209 return test.properties.assert.join(' '); | |
| 2210 } | |
| 2211 return test.properties.assert; | |
| 2212 } | |
| 2213 return ''; | |
| 2214 } | |
| 2215 | |
| 2216 log.appendChild(document.createElementNS(xhtml_ns, "section")); | |
| 2217 var assertions = has_assertions(); | |
| 2218 var html = "<h2>Details</h2><table id='results' " + (assertions ? "class
='assertions'" : "" ) + ">" + | |
| 2219 "<thead><tr><th>Result</th><th>Test Name</th>" + | |
| 2220 (assertions ? "<th>Assertion</th>" : "") + | |
| 2221 "<th>Message</th></tr></thead>" + | |
| 2222 "<tbody>"; | |
| 2223 for (var i = 0; i < tests.length; i++) { | |
| 2224 html += '<tr class="' + | |
| 2225 escape_html(status_class(status_text[tests[i].status])) + | |
| 2226 '"><td>' + | |
| 2227 escape_html(status_text[tests[i].status]) + | |
| 2228 "</td><td>" + | |
| 2229 escape_html(tests[i].name) + | |
| 2230 "</td><td>" + | |
| 2231 (assertions ? escape_html(get_assertion(tests[i])) + "</td><td>"
: "") + | |
| 2232 escape_html(tests[i].message ? tests[i].message : " ") + | |
| 2233 (tests[i].stack ? "<pre>" + | |
| 2234 escape_html(tests[i].stack) + | |
| 2235 "</pre>": "") + | |
| 2236 "</td></tr>"; | |
| 2237 } | |
| 2238 html += "</tbody></table>"; | |
| 2239 try { | |
| 2240 log.lastChild.innerHTML = html; | |
| 2241 } catch (e) { | |
| 2242 log.appendChild(document.createElementNS(xhtml_ns, "p")) | |
| 2243 .textContent = "Setting innerHTML for the log threw an exception.
"; | |
| 2244 log.appendChild(document.createElementNS(xhtml_ns, "pre")) | |
| 2245 .textContent = html; | |
| 2246 } | |
| 2247 }; | |
| 2248 | |
| 2249 /* | |
| 2250 * Template code | |
| 2251 * | |
| 2252 * A template is just a javascript structure. An element is represented as: | |
| 2253 * | |
| 2254 * [tag_name, {attr_name:attr_value}, child1, child2] | |
| 2255 * | |
| 2256 * the children can either be strings (which act like text nodes), other tem
plates or | |
| 2257 * functions (see below) | |
| 2258 * | |
| 2259 * A text node is represented as | |
| 2260 * | |
| 2261 * ["{text}", value] | |
| 2262 * | |
| 2263 * String values have a simple substitution syntax; ${foo} represents a vari
able foo. | |
| 2264 * | |
| 2265 * It is possible to embed logic in templates by using a function in a place
where a | |
| 2266 * node would usually go. The function must either return part of a template
or null. | |
| 2267 * | |
| 2268 * In cases where a set of nodes are required as output rather than a single
node | |
| 2269 * with children it is possible to just use a list | |
| 2270 * [node1, node2, node3] | |
| 2271 * | |
| 2272 * Usage: | |
| 2273 * | |
| 2274 * render(template, substitutions) - take a template and an object mapping | |
| 2275 * variable names to parameters and return either a DOM node or a list of DO
M nodes | |
| 2276 * | |
| 2277 * substitute(template, substitutions) - take a template and variable mappin
g object, | |
| 2278 * make the variable substitutions and return the substituted template | |
| 2279 * | |
| 2280 */ | |
| 2281 | |
| 2282 function is_single_node(template) | |
| 2283 { | |
| 2284 return typeof template[0] === "string"; | |
| 2285 } | |
| 2286 | |
| 2287 function substitute(template, substitutions) | |
| 2288 { | |
| 2289 if (typeof template === "function") { | |
| 2290 var replacement = template(substitutions); | |
| 2291 if (!replacement) { | |
| 2292 return null; | |
| 2293 } | |
| 2294 | |
| 2295 return substitute(replacement, substitutions); | |
| 2296 } | |
| 2297 | |
| 2298 if (is_single_node(template)) { | |
| 2299 return substitute_single(template, substitutions); | |
| 2300 } | |
| 2301 | |
| 2302 return filter(map(template, function(x) { | |
| 2303 return substitute(x, substitutions); | |
| 2304 }), function(x) {return x !== null;}); | |
| 2305 } | |
| 2306 | |
| 2307 function substitute_single(template, substitutions) | |
| 2308 { | |
| 2309 var substitution_re = /\$\{([^ }]*)\}/g; | |
| 2310 | |
| 2311 function do_substitution(input) { | |
| 2312 var components = input.split(substitution_re); | |
| 2313 var rv = []; | |
| 2314 for (var i = 0; i < components.length; i += 2) { | |
| 2315 rv.push(components[i]); | |
| 2316 if (components[i + 1]) { | |
| 2317 rv.push(String(substitutions[components[i + 1]])); | |
| 2318 } | |
| 2319 } | |
| 2320 return rv; | |
| 2321 } | |
| 2322 | |
| 2323 function substitute_attrs(attrs, rv) | |
| 2324 { | |
| 2325 rv[1] = {}; | |
| 2326 for (var name in template[1]) { | |
| 2327 if (attrs.hasOwnProperty(name)) { | |
| 2328 var new_name = do_substitution(name).join(""); | |
| 2329 var new_value = do_substitution(attrs[name]).join(""); | |
| 2330 rv[1][new_name] = new_value; | |
| 2331 } | |
| 2332 } | |
| 2333 } | |
| 2334 | |
| 2335 function substitute_children(children, rv) | |
| 2336 { | |
| 2337 for (var i = 0; i < children.length; i++) { | |
| 2338 if (children[i] instanceof Object) { | |
| 2339 var replacement = substitute(children[i], substitutions); | |
| 2340 if (replacement !== null) { | |
| 2341 if (is_single_node(replacement)) { | |
| 2342 rv.push(replacement); | |
| 2343 } else { | |
| 2344 extend(rv, replacement); | |
| 2345 } | |
| 2346 } | |
| 2347 } else { | |
| 2348 extend(rv, do_substitution(String(children[i]))); | |
| 2349 } | |
| 2350 } | |
| 2351 return rv; | |
| 2352 } | |
| 2353 | |
| 2354 var rv = []; | |
| 2355 rv.push(do_substitution(String(template[0])).join("")); | |
| 2356 | |
| 2357 if (template[0] === "{text}") { | |
| 2358 substitute_children(template.slice(1), rv); | |
| 2359 } else { | |
| 2360 substitute_attrs(template[1], rv); | |
| 2361 substitute_children(template.slice(2), rv); | |
| 2362 } | |
| 2363 | |
| 2364 return rv; | |
| 2365 } | |
| 2366 | |
| 2367 function make_dom_single(template, doc) | |
| 2368 { | |
| 2369 var output_document = doc || document; | |
| 2370 var element; | |
| 2371 if (template[0] === "{text}") { | |
| 2372 element = output_document.createTextNode(""); | |
| 2373 for (var i = 1; i < template.length; i++) { | |
| 2374 element.data += template[i]; | |
| 2375 } | |
| 2376 } else { | |
| 2377 element = output_document.createElementNS(xhtml_ns, template[0]); | |
| 2378 for (var name in template[1]) { | |
| 2379 if (template[1].hasOwnProperty(name)) { | |
| 2380 element.setAttribute(name, template[1][name]); | |
| 2381 } | |
| 2382 } | |
| 2383 for (var i = 2; i < template.length; i++) { | |
| 2384 if (template[i] instanceof Object) { | |
| 2385 var sub_element = make_dom(template[i]); | |
| 2386 element.appendChild(sub_element); | |
| 2387 } else { | |
| 2388 var text_node = output_document.createTextNode(template[i]); | |
| 2389 element.appendChild(text_node); | |
| 2390 } | |
| 2391 } | |
| 2392 } | |
| 2393 | |
| 2394 return element; | |
| 2395 } | |
| 2396 | |
| 2397 function make_dom(template, substitutions, output_document) | |
| 2398 { | |
| 2399 if (is_single_node(template)) { | |
| 2400 return make_dom_single(template, output_document); | |
| 2401 } | |
| 2402 | |
| 2403 return map(template, function(x) { | |
| 2404 return make_dom_single(x, output_document); | |
| 2405 }); | |
| 2406 } | |
| 2407 | |
| 2408 function render(template, substitutions, output_document) | |
| 2409 { | |
| 2410 return make_dom(substitute(template, substitutions), output_document); | |
| 2411 } | |
| 2412 | |
| 2413 /* | |
| 2414 * Utility funcions | |
| 2415 */ | |
| 2416 function assert(expected_true, function_name, description, error, substituti
ons) | |
| 2417 { | |
| 2418 if (tests.tests.length === 0) { | |
| 2419 tests.set_file_is_test(); | |
| 2420 } | |
| 2421 if (expected_true !== true) { | |
| 2422 var msg = make_message(function_name, description, | |
| 2423 error, substitutions); | |
| 2424 throw new AssertionError(msg); | |
| 2425 } | |
| 2426 } | |
| 2427 | |
| 2428 function AssertionError(message) | |
| 2429 { | |
| 2430 this.message = message; | |
| 2431 this.stack = this.get_stack(); | |
| 2432 } | |
| 2433 | |
| 2434 AssertionError.prototype = Object.create(Error.prototype); | |
| 2435 | |
| 2436 AssertionError.prototype.get_stack = function() { | |
| 2437 var stack = new Error().stack; | |
| 2438 // IE11 does not initialize 'Error.stack' until the object is thrown. | |
| 2439 if (!stack) { | |
| 2440 try { | |
| 2441 throw new Error(); | |
| 2442 } catch (e) { | |
| 2443 stack = e.stack; | |
| 2444 } | |
| 2445 } | |
| 2446 | |
| 2447 var lines = stack.split("\n"); | |
| 2448 | |
| 2449 // Create a pattern to match stack frames originating within testharness
.js. These include the | |
| 2450 // script URL, followed by the line/col (e.g., '/resources/testharness.j
s:120:21'). | |
| 2451 var re = new RegExp((get_script_url() || "\\btestharness.js") + ":\\d+:\
\d+"); | |
| 2452 | |
| 2453 // Some browsers include a preamble that specifies the type of the error
object. Skip this by | |
| 2454 // advancing until we find the first stack frame originating from testha
rness.js. | |
| 2455 var i = 0; | |
| 2456 while (!re.test(lines[i]) && i < lines.length) { | |
| 2457 i++; | |
| 2458 } | |
| 2459 | |
| 2460 // Then skip the top frames originating from testharness.js to begin the
stack at the test code. | |
| 2461 while (re.test(lines[i]) && i < lines.length) { | |
| 2462 i++; | |
| 2463 } | |
| 2464 | |
| 2465 // Paranoid check that we didn't skip all frames. If so, return the ori
ginal stack unmodified. | |
| 2466 if (i >= lines.length) { | |
| 2467 return stack; | |
| 2468 } | |
| 2469 | |
| 2470 return lines.slice(i).join("\n"); | |
| 2471 } | |
| 2472 | |
| 2473 function make_message(function_name, description, error, substitutions) | |
| 2474 { | |
| 2475 for (var p in substitutions) { | |
| 2476 if (substitutions.hasOwnProperty(p)) { | |
| 2477 substitutions[p] = format_value(substitutions[p]); | |
| 2478 } | |
| 2479 } | |
| 2480 var node_form = substitute(["{text}", "${function_name}: ${description}"
+ error], | |
| 2481 merge({function_name:function_name, | |
| 2482 description:(description?description +
" ":"")}, | |
| 2483 substitutions)); | |
| 2484 return node_form.slice(1).join(""); | |
| 2485 } | |
| 2486 | |
| 2487 function filter(array, callable, thisObj) { | |
| 2488 var rv = []; | |
| 2489 for (var i = 0; i < array.length; i++) { | |
| 2490 if (array.hasOwnProperty(i)) { | |
| 2491 var pass = callable.call(thisObj, array[i], i, array); | |
| 2492 if (pass) { | |
| 2493 rv.push(array[i]); | |
| 2494 } | |
| 2495 } | |
| 2496 } | |
| 2497 return rv; | |
| 2498 } | |
| 2499 | |
| 2500 function map(array, callable, thisObj) | |
| 2501 { | |
| 2502 var rv = []; | |
| 2503 rv.length = array.length; | |
| 2504 for (var i = 0; i < array.length; i++) { | |
| 2505 if (array.hasOwnProperty(i)) { | |
| 2506 rv[i] = callable.call(thisObj, array[i], i, array); | |
| 2507 } | |
| 2508 } | |
| 2509 return rv; | |
| 2510 } | |
| 2511 | |
| 2512 function extend(array, items) | |
| 2513 { | |
| 2514 Array.prototype.push.apply(array, items); | |
| 2515 } | |
| 2516 | |
| 2517 function forEach(array, callback, thisObj) | |
| 2518 { | |
| 2519 for (var i = 0; i < array.length; i++) { | |
| 2520 if (array.hasOwnProperty(i)) { | |
| 2521 callback.call(thisObj, array[i], i, array); | |
| 2522 } | |
| 2523 } | |
| 2524 } | |
| 2525 | |
| 2526 function merge(a,b) | |
| 2527 { | |
| 2528 var rv = {}; | |
| 2529 var p; | |
| 2530 for (p in a) { | |
| 2531 rv[p] = a[p]; | |
| 2532 } | |
| 2533 for (p in b) { | |
| 2534 rv[p] = b[p]; | |
| 2535 } | |
| 2536 return rv; | |
| 2537 } | |
| 2538 | |
| 2539 function expose(object, name) | |
| 2540 { | |
| 2541 var components = name.split("."); | |
| 2542 var target = test_environment.global_scope(); | |
| 2543 for (var i = 0; i < components.length - 1; i++) { | |
| 2544 if (!(components[i] in target)) { | |
| 2545 target[components[i]] = {}; | |
| 2546 } | |
| 2547 target = target[components[i]]; | |
| 2548 } | |
| 2549 target[components[components.length - 1]] = object; | |
| 2550 } | |
| 2551 | |
| 2552 function is_same_origin(w) { | |
| 2553 try { | |
| 2554 'random_prop' in w; | |
| 2555 return true; | |
| 2556 } catch (e) { | |
| 2557 return false; | |
| 2558 } | |
| 2559 } | |
| 2560 | |
| 2561 /** Returns the 'src' URL of the first <script> tag in the page to include t
he file 'testharness.js'. */ | |
| 2562 function get_script_url() | |
| 2563 { | |
| 2564 if (!('document' in self)) { | |
| 2565 return undefined; | |
| 2566 } | |
| 2567 | |
| 2568 var scripts = document.getElementsByTagName("script"); | |
| 2569 for (var i = 0; i < scripts.length; i++) { | |
| 2570 var src; | |
| 2571 if (scripts[i].src) { | |
| 2572 src = scripts[i].src; | |
| 2573 } else if (scripts[i].href) { | |
| 2574 //SVG case | |
| 2575 src = scripts[i].href.baseVal; | |
| 2576 } | |
| 2577 | |
| 2578 var matches = src && src.match(/^(.*\/|)testharness\.js$/); | |
| 2579 if (matches) { | |
| 2580 return src; | |
| 2581 } | |
| 2582 } | |
| 2583 return undefined; | |
| 2584 } | |
| 2585 | |
| 2586 /** Returns the URL path at which the files for testharness.js are assumed t
o reside (e.g., '/resources/'). | |
| 2587 The path is derived from inspecting the 'src' of the <script> tag that i
ncluded 'testharness.js'. */ | |
| 2588 function get_harness_url() | |
| 2589 { | |
| 2590 var script_url = get_script_url(); | |
| 2591 | |
| 2592 // Exclude the 'testharness.js' file from the returned path, but '+ 1' t
o include the trailing slash. | |
| 2593 return script_url ? script_url.slice(0, script_url.lastIndexOf('/') + 1)
: undefined; | |
| 2594 } | |
| 2595 | |
| 2596 function supports_post_message(w) | |
| 2597 { | |
| 2598 var supports; | |
| 2599 var type; | |
| 2600 // Given IE implements postMessage across nested iframes but not across | |
| 2601 // windows or tabs, you can't infer cross-origin communication from the
presence | |
| 2602 // of postMessage on the current window object only. | |
| 2603 // | |
| 2604 // Touching the postMessage prop on a window can throw if the window is | |
| 2605 // not from the same origin AND post message is not supported in that | |
| 2606 // browser. So just doing an existence test here won't do, you also need | |
| 2607 // to wrap it in a try..cacth block. | |
| 2608 try { | |
| 2609 type = typeof w.postMessage; | |
| 2610 if (type === "function") { | |
| 2611 supports = true; | |
| 2612 } | |
| 2613 | |
| 2614 // IE8 supports postMessage, but implements it as a host object whic
h | |
| 2615 // returns "object" as its `typeof`. | |
| 2616 else if (type === "object") { | |
| 2617 supports = true; | |
| 2618 } | |
| 2619 | |
| 2620 // This is the case where postMessage isn't supported AND accessing
a | |
| 2621 // window property across origins does NOT throw (e.g. old Safari br
owser). | |
| 2622 else { | |
| 2623 supports = false; | |
| 2624 } | |
| 2625 } catch (e) { | |
| 2626 // This is the case where postMessage isn't supported AND accessing
a | |
| 2627 // window property across origins throws (e.g. old Firefox browser). | |
| 2628 supports = false; | |
| 2629 } | |
| 2630 return supports; | |
| 2631 } | |
| 2632 | |
| 2633 /** | |
| 2634 * Setup globals | |
| 2635 */ | |
| 2636 | |
| 2637 var tests = new Tests(); | |
| 2638 | |
| 2639 addEventListener("error", function(e) { | |
| 2640 if (tests.file_is_test) { | |
| 2641 var test = tests.tests[0]; | |
| 2642 if (test.phase >= test.phases.HAS_RESULT) { | |
| 2643 return; | |
| 2644 } | |
| 2645 test.set_status(test.FAIL, e.message, e.stack); | |
| 2646 test.phase = test.phases.HAS_RESULT; | |
| 2647 test.done(); | |
| 2648 done(); | |
| 2649 } else if (!tests.allow_uncaught_exception) { | |
| 2650 tests.status.status = tests.status.ERROR; | |
| 2651 tests.status.message = e.message; | |
| 2652 tests.status.stack = e.stack; | |
| 2653 } | |
| 2654 }); | |
| 2655 | |
| 2656 test_environment.on_tests_ready(); | |
| 2657 | |
| 2658 })(); | |
| 2659 // vim: set expandtab shiftwidth=4 tabstop=4: | |
| OLD | NEW |