1/*global self*/ 2/*jshint latedef: nofunc*/ 3/* 4Distributed under both the W3C Test Suite License [1] and the W3C 53-clause BSD License [2]. To contribute to a W3C Test Suite, see the 6policies 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: https://web-platform-tests.org/writing-tests/testharness-api.html 14 * (../docs/_writing-tests/testharness-api.md) */ 15 16(function (global_scope) 17{ 18 var debug = false; 19 // default timeout is 10 seconds, test can override if needed 20 var settings = { 21 output:true, 22 harness_timeout:{ 23 "normal":10000, 24 "long":60000 25 }, 26 test_timeout:null, 27 message_events: ["start", "test_state", "result", "completion"] 28 }; 29 30 var xhtml_ns = "http://www.w3.org/1999/xhtml"; 31 32 /* 33 * TestEnvironment is an abstraction for the environment in which the test 34 * harness is used. Each implementation of a test environment has to provide 35 * the following interface: 36 * 37 * interface TestEnvironment { 38 * // Invoked after the global 'tests' object has been created and it's 39 * // safe to call add_*_callback() to register event handlers. 40 * void on_tests_ready(); 41 * 42 * // Invoked after setup() has been called to notify the test environment 43 * // of changes to the test harness properties. 44 * void on_new_harness_properties(object properties); 45 * 46 * // Should return a new unique default test name. 47 * DOMString next_default_test_name(); 48 * 49 * // Should return the test harness timeout duration in milliseconds. 50 * float test_timeout(); 51 * }; 52 */ 53 54 /* 55 * A test environment with a DOM. The global object is 'window'. By default 56 * test results are displayed in a table. Any parent windows receive 57 * callbacks or messages via postMessage() when test events occur. See 58 * apisample11.html and apisample12.html. 59 */ 60 function WindowTestEnvironment() { 61 this.name_counter = 0; 62 this.window_cache = null; 63 this.output_handler = null; 64 this.all_loaded = false; 65 var this_obj = this; 66 this.message_events = []; 67 this.dispatched_messages = []; 68 69 this.message_functions = { 70 start: [add_start_callback, remove_start_callback, 71 function (properties) { 72 this_obj._dispatch("start_callback", [properties], 73 {type: "start", properties: properties}); 74 }], 75 76 test_state: [add_test_state_callback, remove_test_state_callback, 77 function(test) { 78 this_obj._dispatch("test_state_callback", [test], 79 {type: "test_state", 80 test: test.structured_clone()}); 81 }], 82 result: [add_result_callback, remove_result_callback, 83 function (test) { 84 this_obj.output_handler.show_status(); 85 this_obj._dispatch("result_callback", [test], 86 {type: "result", 87 test: test.structured_clone()}); 88 }], 89 completion: [add_completion_callback, remove_completion_callback, 90 function (tests, harness_status) { 91 var cloned_tests = map(tests, function(test) { 92 return test.structured_clone(); 93 }); 94 this_obj._dispatch("completion_callback", [tests, harness_status], 95 {type: "complete", 96 tests: cloned_tests, 97 status: harness_status.structured_clone()}); 98 }] 99 } 100 101 on_event(window, 'load', function() { 102 this_obj.all_loaded = true; 103 }); 104 105 on_event(window, 'message', function(event) { 106 if (event.data && event.data.type === "getmessages" && event.source) { 107 // A window can post "getmessages" to receive a duplicate of every 108 // message posted by this environment so far. This allows subscribers 109 // from fetch_tests_from_window to 'catch up' to the current state of 110 // this environment. 111 for (var i = 0; i < this_obj.dispatched_messages.length; ++i) 112 { 113 event.source.postMessage(this_obj.dispatched_messages[i], "*"); 114 } 115 } 116 }); 117 } 118 119 WindowTestEnvironment.prototype._dispatch = function(selector, callback_args, message_arg) { 120 this.dispatched_messages.push(message_arg); 121 this._forEach_windows( 122 function(w, same_origin) { 123 if (same_origin) { 124 try { 125 var has_selector = selector in w; 126 } catch(e) { 127 // If document.domain was set at some point same_origin can be 128 // wrong and the above will fail. 129 has_selector = false; 130 } 131 if (has_selector) { 132 try { 133 w[selector].apply(undefined, callback_args); 134 } catch (e) { 135 if (debug) { 136 throw e; 137 } 138 } 139 } 140 } 141 if (supports_post_message(w) && w !== self) { 142 w.postMessage(message_arg, "*"); 143 } 144 }); 145 }; 146 147 WindowTestEnvironment.prototype._forEach_windows = function(callback) { 148 // Iterate over the windows [self ... top, opener]. The callback is passed 149 // two objects, the first one is the window object itself, the second one 150 // is a boolean indicating whether or not it's on the same origin as the 151 // current window. 152 var cache = this.window_cache; 153 if (!cache) { 154 cache = [[self, true]]; 155 var w = self; 156 var i = 0; 157 var so; 158 while (w != w.parent) { 159 w = w.parent; 160 so = is_same_origin(w); 161 cache.push([w, so]); 162 i++; 163 } 164 w = window.opener; 165 if (w) { 166 cache.push([w, is_same_origin(w)]); 167 } 168 this.window_cache = cache; 169 } 170 171 forEach(cache, 172 function(a) { 173 callback.apply(null, a); 174 }); 175 }; 176 177 WindowTestEnvironment.prototype.on_tests_ready = function() { 178 var output = new Output(); 179 this.output_handler = output; 180 181 var this_obj = this; 182 183 add_start_callback(function (properties) { 184 this_obj.output_handler.init(properties); 185 }); 186 187 add_test_state_callback(function(test) { 188 this_obj.output_handler.show_status(); 189 }); 190 191 add_result_callback(function (test) { 192 this_obj.output_handler.show_status(); 193 }); 194 195 add_completion_callback(function (tests, harness_status) { 196 this_obj.output_handler.show_results(tests, harness_status); 197 }); 198 this.setup_messages(settings.message_events); 199 }; 200 201 WindowTestEnvironment.prototype.setup_messages = function(new_events) { 202 var this_obj = this; 203 forEach(settings.message_events, function(x) { 204 var current_dispatch = this_obj.message_events.indexOf(x) !== -1; 205 var new_dispatch = new_events.indexOf(x) !== -1; 206 if (!current_dispatch && new_dispatch) { 207 this_obj.message_functions[x][0](this_obj.message_functions[x][2]); 208 } else if (current_dispatch && !new_dispatch) { 209 this_obj.message_functions[x][1](this_obj.message_functions[x][2]); 210 } 211 }); 212 this.message_events = new_events; 213 } 214 215 WindowTestEnvironment.prototype.next_default_test_name = function() { 216 var suffix = this.name_counter > 0 ? " " + this.name_counter : ""; 217 this.name_counter++; 218 return get_title() + suffix; 219 }; 220 221 WindowTestEnvironment.prototype.on_new_harness_properties = function(properties) { 222 this.output_handler.setup(properties); 223 if (properties.hasOwnProperty("message_events")) { 224 this.setup_messages(properties.message_events); 225 } 226 }; 227 228 WindowTestEnvironment.prototype.add_on_loaded_callback = function(callback) { 229 on_event(window, 'load', callback); 230 }; 231 232 WindowTestEnvironment.prototype.test_timeout = function() { 233 var metas = document.getElementsByTagName("meta"); 234 for (var i = 0; i < metas.length; i++) { 235 if (metas[i].name == "timeout") { 236 if (metas[i].content == "long") { 237 return settings.harness_timeout.long; 238 } 239 break; 240 } 241 } 242 return settings.harness_timeout.normal; 243 }; 244 245 /* 246 * Base TestEnvironment implementation for a generic web worker. 247 * 248 * Workers accumulate test results. One or more clients can connect and 249 * retrieve results from a worker at any time. 250 * 251 * WorkerTestEnvironment supports communicating with a client via a 252 * MessagePort. The mechanism for determining the appropriate MessagePort 253 * for communicating with a client depends on the type of worker and is 254 * implemented by the various specializations of WorkerTestEnvironment 255 * below. 256 * 257 * A client document using testharness can use fetch_tests_from_worker() to 258 * retrieve results from a worker. See apisample16.html. 259 */ 260 function WorkerTestEnvironment() { 261 this.name_counter = 0; 262 this.all_loaded = true; 263 this.message_list = []; 264 this.message_ports = []; 265 } 266 267 WorkerTestEnvironment.prototype._dispatch = function(message) { 268 this.message_list.push(message); 269 for (var i = 0; i < this.message_ports.length; ++i) 270 { 271 this.message_ports[i].postMessage(message); 272 } 273 }; 274 275 // The only requirement is that port has a postMessage() method. It doesn't 276 // have to be an instance of a MessagePort, and often isn't. 277 WorkerTestEnvironment.prototype._add_message_port = function(port) { 278 this.message_ports.push(port); 279 for (var i = 0; i < this.message_list.length; ++i) 280 { 281 port.postMessage(this.message_list[i]); 282 } 283 }; 284 285 WorkerTestEnvironment.prototype.next_default_test_name = function() { 286 var suffix = this.name_counter > 0 ? " " + this.name_counter : ""; 287 this.name_counter++; 288 return get_title() + suffix; 289 }; 290 291 WorkerTestEnvironment.prototype.on_new_harness_properties = function() {}; 292 293 WorkerTestEnvironment.prototype.on_tests_ready = function() { 294 var this_obj = this; 295 add_start_callback( 296 function(properties) { 297 this_obj._dispatch({ 298 type: "start", 299 properties: properties, 300 }); 301 }); 302 add_test_state_callback( 303 function(test) { 304 this_obj._dispatch({ 305 type: "test_state", 306 test: test.structured_clone() 307 }); 308 }); 309 add_result_callback( 310 function(test) { 311 this_obj._dispatch({ 312 type: "result", 313 test: test.structured_clone() 314 }); 315 }); 316 add_completion_callback( 317 function(tests, harness_status) { 318 this_obj._dispatch({ 319 type: "complete", 320 tests: map(tests, 321 function(test) { 322 return test.structured_clone(); 323 }), 324 status: harness_status.structured_clone() 325 }); 326 }); 327 }; 328 329 WorkerTestEnvironment.prototype.add_on_loaded_callback = function() {}; 330 331 WorkerTestEnvironment.prototype.test_timeout = function() { 332 // Tests running in a worker don't have a default timeout. I.e. all 333 // worker tests behave as if settings.explicit_timeout is true. 334 return null; 335 }; 336 337 /* 338 * Dedicated web workers. 339 * https://html.spec.whatwg.org/multipage/workers.html#dedicatedworkerglobalscope 340 * 341 * This class is used as the test_environment when testharness is running 342 * inside a dedicated worker. 343 */ 344 function DedicatedWorkerTestEnvironment() { 345 WorkerTestEnvironment.call(this); 346 // self is an instance of DedicatedWorkerGlobalScope which exposes 347 // a postMessage() method for communicating via the message channel 348 // established when the worker is created. 349 this._add_message_port(self); 350 } 351 DedicatedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); 352 353 DedicatedWorkerTestEnvironment.prototype.on_tests_ready = function() { 354 WorkerTestEnvironment.prototype.on_tests_ready.call(this); 355 // In the absence of an onload notification, we a require dedicated 356 // workers to explicitly signal when the tests are done. 357 tests.wait_for_finish = true; 358 }; 359 360 /* 361 * Shared web workers. 362 * https://html.spec.whatwg.org/multipage/workers.html#sharedworkerglobalscope 363 * 364 * This class is used as the test_environment when testharness is running 365 * inside a shared web worker. 366 */ 367 function SharedWorkerTestEnvironment() { 368 WorkerTestEnvironment.call(this); 369 var this_obj = this; 370 // Shared workers receive message ports via the 'onconnect' event for 371 // each connection. 372 self.addEventListener("connect", 373 function(message_event) { 374 this_obj._add_message_port(message_event.source); 375 }, false); 376 } 377 SharedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); 378 379 SharedWorkerTestEnvironment.prototype.on_tests_ready = function() { 380 WorkerTestEnvironment.prototype.on_tests_ready.call(this); 381 // In the absence of an onload notification, we a require shared 382 // workers to explicitly signal when the tests are done. 383 tests.wait_for_finish = true; 384 }; 385 386 /* 387 * Service workers. 388 * http://www.w3.org/TR/service-workers/ 389 * 390 * This class is used as the test_environment when testharness is running 391 * inside a service worker. 392 */ 393 function ServiceWorkerTestEnvironment() { 394 WorkerTestEnvironment.call(this); 395 this.all_loaded = false; 396 this.on_loaded_callback = null; 397 var this_obj = this; 398 self.addEventListener("message", 399 function(event) { 400 if (event.data && event.data.type && event.data.type === "connect") { 401 if (event.ports && event.ports[0]) { 402 // If a MessageChannel was passed, then use it to 403 // send results back to the main window. This 404 // allows the tests to work even if the browser 405 // does not fully support MessageEvent.source in 406 // ServiceWorkers yet. 407 this_obj._add_message_port(event.ports[0]); 408 event.ports[0].start(); 409 } else { 410 // If there is no MessageChannel, then attempt to 411 // use the MessageEvent.source to send results 412 // back to the main window. 413 this_obj._add_message_port(event.source); 414 } 415 } 416 }, false); 417 418 // The oninstall event is received after the service worker script and 419 // all imported scripts have been fetched and executed. It's the 420 // equivalent of an onload event for a document. All tests should have 421 // been added by the time this event is received, thus it's not 422 // necessary to wait until the onactivate event. However, tests for 423 // installed service workers need another event which is equivalent to 424 // the onload event because oninstall is fired only on installation. The 425 // onmessage event is used for that purpose since tests using 426 // testharness.js should ask the result to its service worker by 427 // PostMessage. If the onmessage event is triggered on the service 428 // worker's context, that means the worker's script has been evaluated. 429 on_event(self, "install", on_all_loaded); 430 on_event(self, "message", on_all_loaded); 431 function on_all_loaded() { 432 if (this_obj.all_loaded) 433 return; 434 this_obj.all_loaded = true; 435 if (this_obj.on_loaded_callback) { 436 this_obj.on_loaded_callback(); 437 } 438 } 439 } 440 441 ServiceWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); 442 443 ServiceWorkerTestEnvironment.prototype.add_on_loaded_callback = function(callback) { 444 if (this.all_loaded) { 445 callback(); 446 } else { 447 this.on_loaded_callback = callback; 448 } 449 }; 450 451 /* 452 * JavaScript shells. 453 * 454 * This class is used as the test_environment when testharness is running 455 * inside a JavaScript shell. 456 */ 457 function ShellTestEnvironment() { 458 this.name_counter = 0; 459 this.all_loaded = false; 460 this.on_loaded_callback = null; 461 Promise.resolve().then(function() { 462 this.all_loaded = true 463 if (this.on_loaded_callback) { 464 this.on_loaded_callback(); 465 } 466 }.bind(this)); 467 this.message_list = []; 468 this.message_ports = []; 469 } 470 471 ShellTestEnvironment.prototype.next_default_test_name = function() { 472 var suffix = this.name_counter > 0 ? " " + this.name_counter : ""; 473 this.name_counter++; 474 return "Untitled" + suffix; 475 }; 476 477 ShellTestEnvironment.prototype.on_new_harness_properties = function() {}; 478 479 ShellTestEnvironment.prototype.on_tests_ready = function() {}; 480 481 ShellTestEnvironment.prototype.add_on_loaded_callback = function(callback) { 482 if (this.all_loaded) { 483 callback(); 484 } else { 485 this.on_loaded_callback = callback; 486 } 487 }; 488 489 ShellTestEnvironment.prototype.test_timeout = function() { 490 // Tests running in a shell don't have a default timeout, so behave as 491 // if settings.explicit_timeout is true. 492 return null; 493 }; 494 495 function create_test_environment() { 496 if ('document' in global_scope) { 497 return new WindowTestEnvironment(); 498 } 499 if ('DedicatedWorkerGlobalScope' in global_scope && 500 global_scope instanceof DedicatedWorkerGlobalScope) { 501 return new DedicatedWorkerTestEnvironment(); 502 } 503 if ('SharedWorkerGlobalScope' in global_scope && 504 global_scope instanceof SharedWorkerGlobalScope) { 505 return new SharedWorkerTestEnvironment(); 506 } 507 if ('ServiceWorkerGlobalScope' in global_scope && 508 global_scope instanceof ServiceWorkerGlobalScope) { 509 return new ServiceWorkerTestEnvironment(); 510 } 511 if ('WorkerGlobalScope' in global_scope && 512 global_scope instanceof WorkerGlobalScope) { 513 return new DedicatedWorkerTestEnvironment(); 514 } 515 516 if (!('location' in global_scope)) { 517 return new ShellTestEnvironment(); 518 } 519 520 throw new Error("Unsupported test environment"); 521 } 522 523 var test_environment = create_test_environment(); 524 525 function is_shared_worker(worker) { 526 return 'SharedWorker' in global_scope && worker instanceof SharedWorker; 527 } 528 529 function is_service_worker(worker) { 530 // The worker object may be from another execution context, 531 // so do not use instanceof here. 532 return 'ServiceWorker' in global_scope && 533 Object.prototype.toString.call(worker) == '[object ServiceWorker]'; 534 } 535 536 /* 537 * API functions 538 */ 539 function test(func, name, properties) 540 { 541 var test_name = name ? name : test_environment.next_default_test_name(); 542 properties = properties ? properties : {}; 543 var test_obj = new Test(test_name, properties); 544 var value = test_obj.step(func, test_obj, test_obj); 545 546 if (value !== undefined) { 547 var msg = "test named \"" + test_name + 548 "\" inappropriately returned a value"; 549 550 try { 551 if (value && value.hasOwnProperty("then")) { 552 msg += ", consider using `promise_test` instead"; 553 } 554 } catch (err) {} 555 556 tests.status.status = tests.status.ERROR; 557 tests.status.message = msg; 558 } 559 560 if (test_obj.phase === test_obj.phases.STARTED) { 561 test_obj.done(); 562 } 563 } 564 565 function async_test(func, name, properties) 566 { 567 if (typeof func !== "function") { 568 properties = name; 569 name = func; 570 func = null; 571 } 572 var test_name = name ? name : test_environment.next_default_test_name(); 573 properties = properties ? properties : {}; 574 var test_obj = new Test(test_name, properties); 575 if (func) { 576 test_obj.step(func, test_obj, test_obj); 577 } 578 return test_obj; 579 } 580 581 function promise_test(func, name, properties) { 582 var test = async_test(name, properties); 583 test._is_promise_test = true; 584 585 // If there is no promise tests queue make one. 586 if (!tests.promise_tests) { 587 tests.promise_tests = Promise.resolve(); 588 } 589 tests.promise_tests = tests.promise_tests.then(function() { 590 return new Promise(function(resolve) { 591 var promise = test.step(func, test, test); 592 593 test.step(function() { 594 assert(!!promise, "promise_test", null, 595 "test body must return a 'thenable' object (received ${value})", 596 {value:promise}); 597 assert(typeof promise.then === "function", "promise_test", null, 598 "test body must return a 'thenable' object (received an object with no `then` method)", 599 null); 600 }); 601 602 // Test authors may use the `step` method within a 603 // `promise_test` even though this reflects a mixture of 604 // asynchronous control flow paradigms. The "done" callback 605 // should be registered prior to the resolution of the 606 // user-provided Promise to avoid timeouts in cases where the 607 // Promise does not settle but a `step` function has thrown an 608 // error. 609 add_test_done_callback(test, resolve); 610 611 Promise.resolve(promise) 612 .catch(test.step_func( 613 function(value) { 614 if (value instanceof AssertionError) { 615 throw value; 616 } 617 assert(false, "promise_test", null, 618 "Unhandled rejection with value: ${value}", {value:value}); 619 })) 620 .then(function() { 621 test.done(); 622 }); 623 }); 624 }); 625 } 626 627 function promise_rejects(test, expected, promise, description) { 628 return promise.then(test.unreached_func("Should have rejected: " + description)).catch(function(e) { 629 assert_throws(expected, function() { throw e }, description); 630 }); 631 } 632 633 /** 634 * This constructor helper allows DOM events to be handled using Promises, 635 * which can make it a lot easier to test a very specific series of events, 636 * including ensuring that unexpected events are not fired at any point. 637 */ 638 function EventWatcher(test, watchedNode, eventTypes, timeoutPromise) 639 { 640 if (typeof eventTypes == 'string') { 641 eventTypes = [eventTypes]; 642 } 643 644 var waitingFor = null; 645 646 // This is null unless we are recording all events, in which case it 647 // will be an Array object. 648 var recordedEvents = null; 649 650 var eventHandler = test.step_func(function(evt) { 651 assert_true(!!waitingFor, 652 'Not expecting event, but got ' + evt.type + ' event'); 653 assert_equals(evt.type, waitingFor.types[0], 654 'Expected ' + waitingFor.types[0] + ' event, but got ' + 655 evt.type + ' event instead'); 656 657 if (Array.isArray(recordedEvents)) { 658 recordedEvents.push(evt); 659 } 660 661 if (waitingFor.types.length > 1) { 662 // Pop first event from array 663 waitingFor.types.shift(); 664 return; 665 } 666 // We need to null out waitingFor before calling the resolve function 667 // since the Promise's resolve handlers may call wait_for() which will 668 // need to set waitingFor. 669 var resolveFunc = waitingFor.resolve; 670 waitingFor = null; 671 // Likewise, we should reset the state of recordedEvents. 672 var result = recordedEvents || evt; 673 recordedEvents = null; 674 resolveFunc(result); 675 }); 676 677 for (var i = 0; i < eventTypes.length; i++) { 678 watchedNode.addEventListener(eventTypes[i], eventHandler, false); 679 } 680 681 /** 682 * Returns a Promise that will resolve after the specified event or 683 * series of events has occurred. 684 * 685 * @param options An optional options object. If the 'record' property 686 * on this object has the value 'all', when the Promise 687 * returned by this function is resolved, *all* Event 688 * objects that were waited for will be returned as an 689 * array. 690 * 691 * For example, 692 * 693 * ```js 694 * const watcher = new EventWatcher(t, div, [ 'animationstart', 695 * 'animationiteration', 696 * 'animationend' ]); 697 * return watcher.wait_for([ 'animationstart', 'animationend' ], 698 * { record: 'all' }).then(evts => { 699 * assert_equals(evts[0].elapsedTime, 0.0); 700 * assert_equals(evts[1].elapsedTime, 2.0); 701 * }); 702 * ``` 703 */ 704 this.wait_for = function(types, options) { 705 if (waitingFor) { 706 return Promise.reject('Already waiting for an event or events'); 707 } 708 if (typeof types == 'string') { 709 types = [types]; 710 } 711 if (options && options.record && options.record === 'all') { 712 recordedEvents = []; 713 } 714 return new Promise(function(resolve, reject) { 715 var timeout = test.step_func(function() { 716 // If the timeout fires after the events have been received 717 // or during a subsequent call to wait_for, ignore it. 718 if (!waitingFor || waitingFor.resolve !== resolve) 719 return; 720 721 // This should always fail, otherwise we should have 722 // resolved the promise. 723 assert_true(waitingFor.types.length == 0, 724 'Timed out waiting for ' + waitingFor.types.join(', ')); 725 var result = recordedEvents; 726 recordedEvents = null; 727 var resolveFunc = waitingFor.resolve; 728 waitingFor = null; 729 resolveFunc(result); 730 }); 731 732 if (timeoutPromise) { 733 timeoutPromise().then(timeout); 734 } 735 736 waitingFor = { 737 types: types, 738 resolve: resolve, 739 reject: reject 740 }; 741 }); 742 }; 743 744 function stop_watching() { 745 for (var i = 0; i < eventTypes.length; i++) { 746 watchedNode.removeEventListener(eventTypes[i], eventHandler, false); 747 } 748 }; 749 750 test._add_cleanup(stop_watching); 751 752 return this; 753 } 754 expose(EventWatcher, 'EventWatcher'); 755 756 function setup(func_or_properties, maybe_properties) 757 { 758 var func = null; 759 var properties = {}; 760 if (arguments.length === 2) { 761 func = func_or_properties; 762 properties = maybe_properties; 763 } else if (func_or_properties instanceof Function) { 764 func = func_or_properties; 765 } else { 766 properties = func_or_properties; 767 } 768 tests.setup(func, properties); 769 test_environment.on_new_harness_properties(properties); 770 } 771 772 function done() { 773 if (tests.tests.length === 0) { 774 tests.set_file_is_test(); 775 } 776 if (tests.file_is_test) { 777 // file is test files never have asynchronous cleanup logic, 778 // meaning the fully-synchronous `done` function can be used here. 779 tests.tests[0].done(); 780 } 781 tests.end_wait(); 782 } 783 784 function generate_tests(func, args, properties) { 785 forEach(args, function(x, i) 786 { 787 var name = x[0]; 788 test(function() 789 { 790 func.apply(this, x.slice(1)); 791 }, 792 name, 793 Array.isArray(properties) ? properties[i] : properties); 794 }); 795 } 796 797 function on_event(object, event, callback) 798 { 799 object.addEventListener(event, callback, false); 800 } 801 802 function step_timeout(f, t) { 803 var outer_this = this; 804 var args = Array.prototype.slice.call(arguments, 2); 805 return setTimeout(function() { 806 f.apply(outer_this, args); 807 }, t * tests.timeout_multiplier); 808 } 809 810 expose(test, 'test'); 811 expose(async_test, 'async_test'); 812 expose(promise_test, 'promise_test'); 813 expose(promise_rejects, 'promise_rejects'); 814 expose(generate_tests, 'generate_tests'); 815 expose(setup, 'setup'); 816 expose(done, 'done'); 817 expose(on_event, 'on_event'); 818 expose(step_timeout, 'step_timeout'); 819 820 /* 821 * Return a string truncated to the given length, with ... added at the end 822 * if it was longer. 823 */ 824 function truncate(s, len) 825 { 826 if (s.length > len) { 827 return s.substring(0, len - 3) + "..."; 828 } 829 return s; 830 } 831 832 /* 833 * Return true if object is probably a Node object. 834 */ 835 function is_node(object) 836 { 837 // I use duck-typing instead of instanceof, because 838 // instanceof doesn't work if the node is from another window (like an 839 // iframe's contentWindow): 840 // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295 841 try { 842 var has_node_properties = ("nodeType" in object && 843 "nodeName" in object && 844 "nodeValue" in object && 845 "childNodes" in object); 846 } catch (e) { 847 // We're probably cross-origin, which means we aren't a node 848 return false; 849 } 850 851 if (has_node_properties) { 852 try { 853 object.nodeType; 854 } catch (e) { 855 // The object is probably Node.prototype or another prototype 856 // object that inherits from it, and not a Node instance. 857 return false; 858 } 859 return true; 860 } 861 return false; 862 } 863 864 var replacements = { 865 "0": "0", 866 "1": "x01", 867 "2": "x02", 868 "3": "x03", 869 "4": "x04", 870 "5": "x05", 871 "6": "x06", 872 "7": "x07", 873 "8": "b", 874 "9": "t", 875 "10": "n", 876 "11": "v", 877 "12": "f", 878 "13": "r", 879 "14": "x0e", 880 "15": "x0f", 881 "16": "x10", 882 "17": "x11", 883 "18": "x12", 884 "19": "x13", 885 "20": "x14", 886 "21": "x15", 887 "22": "x16", 888 "23": "x17", 889 "24": "x18", 890 "25": "x19", 891 "26": "x1a", 892 "27": "x1b", 893 "28": "x1c", 894 "29": "x1d", 895 "30": "x1e", 896 "31": "x1f", 897 "0xfffd": "ufffd", 898 "0xfffe": "ufffe", 899 "0xffff": "uffff", 900 }; 901 902 /* 903 * Convert a value to a nice, human-readable string 904 */ 905 function format_value(val, seen) 906 { 907 if (!seen) { 908 seen = []; 909 } 910 if (typeof val === "object" && val !== null) { 911 if (seen.indexOf(val) >= 0) { 912 return "[...]"; 913 } 914 seen.push(val); 915 } 916 if (Array.isArray(val)) { 917 return "[" + val.map(function(x) {return format_value(x, seen);}).join(", ") + "]"; 918 } 919 920 switch (typeof val) { 921 case "string": 922 val = val.replace("\\", "\\\\"); 923 for (var p in replacements) { 924 var replace = "\\" + replacements[p]; 925 val = val.replace(RegExp(String.fromCharCode(p), "g"), replace); 926 } 927 return '"' + val.replace(/"/g, '\\"') + '"'; 928 case "boolean": 929 case "undefined": 930 return String(val); 931 case "number": 932 // In JavaScript, -0 === 0 and String(-0) == "0", so we have to 933 // special-case. 934 if (val === -0 && 1/val === -Infinity) { 935 return "-0"; 936 } 937 return String(val); 938 case "object": 939 if (val === null) { 940 return "null"; 941 } 942 943 // Special-case Node objects, since those come up a lot in my tests. I 944 // ignore namespaces. 945 if (is_node(val)) { 946 switch (val.nodeType) { 947 case Node.ELEMENT_NODE: 948 var ret = "<" + val.localName; 949 for (var i = 0; i < val.attributes.length; i++) { 950 ret += " " + val.attributes[i].name + '="' + val.attributes[i].value + '"'; 951 } 952 ret += ">" + val.innerHTML + "</" + val.localName + ">"; 953 return "Element node " + truncate(ret, 60); 954 case Node.TEXT_NODE: 955 return 'Text node "' + truncate(val.data, 60) + '"'; 956 case Node.PROCESSING_INSTRUCTION_NODE: 957 return "ProcessingInstruction node with target " + format_value(truncate(val.target, 60)) + " and data " + format_value(truncate(val.data, 60)); 958 case Node.COMMENT_NODE: 959 return "Comment node <!--" + truncate(val.data, 60) + "-->"; 960 case Node.DOCUMENT_NODE: 961 return "Document node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children"); 962 case Node.DOCUMENT_TYPE_NODE: 963 return "DocumentType node"; 964 case Node.DOCUMENT_FRAGMENT_NODE: 965 return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children"); 966 default: 967 return "Node object of unknown type"; 968 } 969 } 970 971 /* falls through */ 972 default: 973 try { 974 return typeof val + ' "' + truncate(String(val), 1000) + '"'; 975 } catch(e) { 976 return ("[stringifying object threw " + String(e) + 977 " with type " + String(typeof e) + "]"); 978 } 979 } 980 } 981 expose(format_value, "format_value"); 982 983 /* 984 * Assertions 985 */ 986 987 function assert_true(actual, description) 988 { 989 assert(actual === true, "assert_true", description, 990 "expected true got ${actual}", {actual:actual}); 991 } 992 expose(assert_true, "assert_true"); 993 994 function assert_false(actual, description) 995 { 996 assert(actual === false, "assert_false", description, 997 "expected false got ${actual}", {actual:actual}); 998 } 999 expose(assert_false, "assert_false"); 1000 1001 function same_value(x, y) { 1002 if (y !== y) { 1003 //NaN case 1004 return x !== x; 1005 } 1006 if (x === 0 && y === 0) { 1007 //Distinguish +0 and -0 1008 return 1/x === 1/y; 1009 } 1010 return x === y; 1011 } 1012 1013 function assert_equals(actual, expected, description) 1014 { 1015 /* 1016 * Test if two primitives are equal or two objects 1017 * are the same object 1018 */ 1019 if (typeof actual != typeof expected) { 1020 assert(false, "assert_equals", description, 1021 "expected (" + typeof expected + ") ${expected} but got (" + typeof actual + ") ${actual}", 1022 {expected:expected, actual:actual}); 1023 return; 1024 } 1025 assert(same_value(actual, expected), "assert_equals", description, 1026 "expected ${expected} but got ${actual}", 1027 {expected:expected, actual:actual}); 1028 } 1029 expose(assert_equals, "assert_equals"); 1030 1031 function assert_not_equals(actual, expected, description) 1032 { 1033 /* 1034 * Test if two primitives are unequal or two objects 1035 * are different objects 1036 */ 1037 assert(!same_value(actual, expected), "assert_not_equals", description, 1038 "got disallowed value ${actual}", 1039 {actual:actual}); 1040 } 1041 expose(assert_not_equals, "assert_not_equals"); 1042 1043 function assert_in_array(actual, expected, description) 1044 { 1045 assert(expected.indexOf(actual) != -1, "assert_in_array", description, 1046 "value ${actual} not in array ${expected}", 1047 {actual:actual, expected:expected}); 1048 } 1049 expose(assert_in_array, "assert_in_array"); 1050 1051 function assert_object_equals(actual, expected, description) 1052 { 1053 assert(typeof actual === "object" && actual !== null, "assert_object_equals", description, 1054 "value is ${actual}, expected object", 1055 {actual: actual}); 1056 //This needs to be improved a great deal 1057 function check_equal(actual, expected, stack) 1058 { 1059 stack.push(actual); 1060 1061 var p; 1062 for (p in actual) { 1063 assert(expected.hasOwnProperty(p), "assert_object_equals", description, 1064 "unexpected property ${p}", {p:p}); 1065 1066 if (typeof actual[p] === "object" && actual[p] !== null) { 1067 if (stack.indexOf(actual[p]) === -1) { 1068 check_equal(actual[p], expected[p], stack); 1069 } 1070 } else { 1071 assert(same_value(actual[p], expected[p]), "assert_object_equals", description, 1072 "property ${p} expected ${expected} got ${actual}", 1073 {p:p, expected:expected, actual:actual}); 1074 } 1075 } 1076 for (p in expected) { 1077 assert(actual.hasOwnProperty(p), 1078 "assert_object_equals", description, 1079 "expected property ${p} missing", {p:p}); 1080 } 1081 stack.pop(); 1082 } 1083 check_equal(actual, expected, []); 1084 } 1085 expose(assert_object_equals, "assert_object_equals"); 1086 1087 function assert_array_equals(actual, expected, description) 1088 { 1089 assert(typeof actual === "object" && actual !== null && "length" in actual, 1090 "assert_array_equals", description, 1091 "value is ${actual}, expected array", 1092 {actual:actual}); 1093 assert(actual.length === expected.length, 1094 "assert_array_equals", description, 1095 "lengths differ, expected ${expected} got ${actual}", 1096 {expected:expected.length, actual:actual.length}); 1097 1098 for (var i = 0; i < actual.length; i++) { 1099 assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i), 1100 "assert_array_equals", description, 1101 "property ${i}, property expected to be ${expected} but was ${actual}", 1102 {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing", 1103 actual:actual.hasOwnProperty(i) ? "present" : "missing"}); 1104 assert(same_value(expected[i], actual[i]), 1105 "assert_array_equals", description, 1106 "property ${i}, expected ${expected} but got ${actual}", 1107 {i:i, expected:expected[i], actual:actual[i]}); 1108 } 1109 } 1110 expose(assert_array_equals, "assert_array_equals"); 1111 1112 function assert_array_approx_equals(actual, expected, epsilon, description) 1113 { 1114 /* 1115 * Test if two primitive arrays are equal within +/- epsilon 1116 */ 1117 assert(actual.length === expected.length, 1118 "assert_array_approx_equals", description, 1119 "lengths differ, expected ${expected} got ${actual}", 1120 {expected:expected.length, actual:actual.length}); 1121 1122 for (var i = 0; i < actual.length; i++) { 1123 assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i), 1124 "assert_array_approx_equals", description, 1125 "property ${i}, property expected to be ${expected} but was ${actual}", 1126 {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing", 1127 actual:actual.hasOwnProperty(i) ? "present" : "missing"}); 1128 assert(typeof actual[i] === "number", 1129 "assert_array_approx_equals", description, 1130 "property ${i}, expected a number but got a ${type_actual}", 1131 {i:i, type_actual:typeof actual[i]}); 1132 assert(Math.abs(actual[i] - expected[i]) <= epsilon, 1133 "assert_array_approx_equals", description, 1134 "property ${i}, expected ${expected} +/- ${epsilon}, expected ${expected} but got ${actual}", 1135 {i:i, expected:expected[i], actual:actual[i], epsilon:epsilon}); 1136 } 1137 } 1138 expose(assert_array_approx_equals, "assert_array_approx_equals"); 1139 1140 function assert_approx_equals(actual, expected, epsilon, description) 1141 { 1142 /* 1143 * Test if two primitive numbers are equal within +/- epsilon 1144 */ 1145 assert(typeof actual === "number", 1146 "assert_approx_equals", description, 1147 "expected a number but got a ${type_actual}", 1148 {type_actual:typeof actual}); 1149 1150 assert(Math.abs(actual - expected) <= epsilon, 1151 "assert_approx_equals", description, 1152 "expected ${expected} +/- ${epsilon} but got ${actual}", 1153 {expected:expected, actual:actual, epsilon:epsilon}); 1154 } 1155 expose(assert_approx_equals, "assert_approx_equals"); 1156 1157 function assert_less_than(actual, expected, description) 1158 { 1159 /* 1160 * Test if a primitive number is less than another 1161 */ 1162 assert(typeof actual === "number", 1163 "assert_less_than", description, 1164 "expected a number but got a ${type_actual}", 1165 {type_actual:typeof actual}); 1166 1167 assert(actual < expected, 1168 "assert_less_than", description, 1169 "expected a number less than ${expected} but got ${actual}", 1170 {expected:expected, actual:actual}); 1171 } 1172 expose(assert_less_than, "assert_less_than"); 1173 1174 function assert_greater_than(actual, expected, description) 1175 { 1176 /* 1177 * Test if a primitive number is greater than another 1178 */ 1179 assert(typeof actual === "number", 1180 "assert_greater_than", description, 1181 "expected a number but got a ${type_actual}", 1182 {type_actual:typeof actual}); 1183 1184 assert(actual > expected, 1185 "assert_greater_than", description, 1186 "expected a number greater than ${expected} but got ${actual}", 1187 {expected:expected, actual:actual}); 1188 } 1189 expose(assert_greater_than, "assert_greater_than"); 1190 1191 function assert_between_exclusive(actual, lower, upper, description) 1192 { 1193 /* 1194 * Test if a primitive number is between two others 1195 */ 1196 assert(typeof actual === "number", 1197 "assert_between_exclusive", description, 1198 "expected a number but got a ${type_actual}", 1199 {type_actual:typeof actual}); 1200 1201 assert(actual > lower && actual < upper, 1202 "assert_between_exclusive", description, 1203 "expected a number greater than ${lower} " + 1204 "and less than ${upper} but got ${actual}", 1205 {lower:lower, upper:upper, actual:actual}); 1206 } 1207 expose(assert_between_exclusive, "assert_between_exclusive"); 1208 1209 function assert_less_than_equal(actual, expected, description) 1210 { 1211 /* 1212 * Test if a primitive number is less than or equal to another 1213 */ 1214 assert(typeof actual === "number", 1215 "assert_less_than_equal", description, 1216 "expected a number but got a ${type_actual}", 1217 {type_actual:typeof actual}); 1218 1219 assert(actual <= expected, 1220 "assert_less_than_equal", description, 1221 "expected a number less than or equal to ${expected} but got ${actual}", 1222 {expected:expected, actual:actual}); 1223 } 1224 expose(assert_less_than_equal, "assert_less_than_equal"); 1225 1226 function assert_greater_than_equal(actual, expected, description) 1227 { 1228 /* 1229 * Test if a primitive number is greater than or equal to another 1230 */ 1231 assert(typeof actual === "number", 1232 "assert_greater_than_equal", description, 1233 "expected a number but got a ${type_actual}", 1234 {type_actual:typeof actual}); 1235 1236 assert(actual >= expected, 1237 "assert_greater_than_equal", description, 1238 "expected a number greater than or equal to ${expected} but got ${actual}", 1239 {expected:expected, actual:actual}); 1240 } 1241 expose(assert_greater_than_equal, "assert_greater_than_equal"); 1242 1243 function assert_between_inclusive(actual, lower, upper, description) 1244 { 1245 /* 1246 * Test if a primitive number is between to two others or equal to either of them 1247 */ 1248 assert(typeof actual === "number", 1249 "assert_between_inclusive", description, 1250 "expected a number but got a ${type_actual}", 1251 {type_actual:typeof actual}); 1252 1253 assert(actual >= lower && actual <= upper, 1254 "assert_between_inclusive", description, 1255 "expected a number greater than or equal to ${lower} " + 1256 "and less than or equal to ${upper} but got ${actual}", 1257 {lower:lower, upper:upper, actual:actual}); 1258 } 1259 expose(assert_between_inclusive, "assert_between_inclusive"); 1260 1261 function assert_regexp_match(actual, expected, description) { 1262 /* 1263 * Test if a string (actual) matches a regexp (expected) 1264 */ 1265 assert(expected.test(actual), 1266 "assert_regexp_match", description, 1267 "expected ${expected} but got ${actual}", 1268 {expected:expected, actual:actual}); 1269 } 1270 expose(assert_regexp_match, "assert_regexp_match"); 1271 1272 function assert_class_string(object, class_string, description) { 1273 assert_equals({}.toString.call(object), "[object " + class_string + "]", 1274 description); 1275 } 1276 expose(assert_class_string, "assert_class_string"); 1277 1278 1279 function assert_own_property(object, property_name, description) { 1280 assert(object.hasOwnProperty(property_name), 1281 "assert_own_property", description, 1282 "expected property ${p} missing", {p:property_name}); 1283 } 1284 expose(assert_own_property, "assert_own_property"); 1285 1286 function assert_not_own_property(object, property_name, description) { 1287 assert(!object.hasOwnProperty(property_name), 1288 "assert_not_own_property", description, 1289 "unexpected property ${p} is found on object", {p:property_name}); 1290 } 1291 expose(assert_not_own_property, "assert_not_own_property"); 1292 1293 function _assert_inherits(name) { 1294 return function (object, property_name, description) 1295 { 1296 assert(typeof object === "object" || typeof object === "function", 1297 name, description, 1298 "provided value is not an object"); 1299 1300 assert("hasOwnProperty" in object, 1301 name, description, 1302 "provided value is an object but has no hasOwnProperty method"); 1303 1304 assert(!object.hasOwnProperty(property_name), 1305 name, description, 1306 "property ${p} found on object expected in prototype chain", 1307 {p:property_name}); 1308 1309 assert(property_name in object, 1310 name, description, 1311 "property ${p} not found in prototype chain", 1312 {p:property_name}); 1313 }; 1314 } 1315 expose(_assert_inherits("assert_inherits"), "assert_inherits"); 1316 expose(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute"); 1317 1318 function assert_readonly(object, property_name, description) 1319 { 1320 var initial_value = object[property_name]; 1321 try { 1322 //Note that this can have side effects in the case where 1323 //the property has PutForwards 1324 object[property_name] = initial_value + "a"; //XXX use some other value here? 1325 assert(same_value(object[property_name], initial_value), 1326 "assert_readonly", description, 1327 "changing property ${p} succeeded", 1328 {p:property_name}); 1329 } finally { 1330 object[property_name] = initial_value; 1331 } 1332 } 1333 expose(assert_readonly, "assert_readonly"); 1334 1335 /** 1336 * Assert an Exception with the expected code is thrown. 1337 * 1338 * @param {object|number|string} code The expected exception code. 1339 * @param {Function} func Function which should throw. 1340 * @param {string} description Error description for the case that the error is not thrown. 1341 */ 1342 function assert_throws(code, func, description) 1343 { 1344 try { 1345 func.call(this); 1346 assert(false, "assert_throws", description, 1347 "${func} did not throw", {func:func}); 1348 } catch (e) { 1349 if (e instanceof AssertionError) { 1350 throw e; 1351 } 1352 1353 assert(typeof e === "object", 1354 "assert_throws", description, 1355 "${func} threw ${e} with type ${type}, not an object", 1356 {func:func, e:e, type:typeof e}); 1357 1358 assert(e !== null, 1359 "assert_throws", description, 1360 "${func} threw null, not an object", 1361 {func:func}); 1362 1363 if (code === null) { 1364 throw new AssertionError('Test bug: need to pass exception to assert_throws()'); 1365 } 1366 if (typeof code === "object") { 1367 assert("name" in e && e.name == code.name, 1368 "assert_throws", description, 1369 "${func} threw ${actual} (${actual_name}) expected ${expected} (${expected_name})", 1370 {func:func, actual:e, actual_name:e.name, 1371 expected:code, 1372 expected_name:code.name}); 1373 return; 1374 } 1375 1376 var code_name_map = { 1377 INDEX_SIZE_ERR: 'IndexSizeError', 1378 HIERARCHY_REQUEST_ERR: 'HierarchyRequestError', 1379 WRONG_DOCUMENT_ERR: 'WrongDocumentError', 1380 INVALID_CHARACTER_ERR: 'InvalidCharacterError', 1381 NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError', 1382 NOT_FOUND_ERR: 'NotFoundError', 1383 NOT_SUPPORTED_ERR: 'NotSupportedError', 1384 INUSE_ATTRIBUTE_ERR: 'InUseAttributeError', 1385 INVALID_STATE_ERR: 'InvalidStateError', 1386 SYNTAX_ERR: 'SyntaxError', 1387 INVALID_MODIFICATION_ERR: 'InvalidModificationError', 1388 NAMESPACE_ERR: 'NamespaceError', 1389 INVALID_ACCESS_ERR: 'InvalidAccessError', 1390 TYPE_MISMATCH_ERR: 'TypeMismatchError', 1391 SECURITY_ERR: 'SecurityError', 1392 NETWORK_ERR: 'NetworkError', 1393 ABORT_ERR: 'AbortError', 1394 URL_MISMATCH_ERR: 'URLMismatchError', 1395 QUOTA_EXCEEDED_ERR: 'QuotaExceededError', 1396 TIMEOUT_ERR: 'TimeoutError', 1397 INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError', 1398 DATA_CLONE_ERR: 'DataCloneError' 1399 }; 1400 1401 var name = code in code_name_map ? code_name_map[code] : code; 1402 1403 var name_code_map = { 1404 IndexSizeError: 1, 1405 HierarchyRequestError: 3, 1406 WrongDocumentError: 4, 1407 InvalidCharacterError: 5, 1408 NoModificationAllowedError: 7, 1409 NotFoundError: 8, 1410 NotSupportedError: 9, 1411 InUseAttributeError: 10, 1412 InvalidStateError: 11, 1413 SyntaxError: 12, 1414 InvalidModificationError: 13, 1415 NamespaceError: 14, 1416 InvalidAccessError: 15, 1417 TypeMismatchError: 17, 1418 SecurityError: 18, 1419 NetworkError: 19, 1420 AbortError: 20, 1421 URLMismatchError: 21, 1422 QuotaExceededError: 22, 1423 TimeoutError: 23, 1424 InvalidNodeTypeError: 24, 1425 DataCloneError: 25, 1426 1427 EncodingError: 0, 1428 NotReadableError: 0, 1429 UnknownError: 0, 1430 ConstraintError: 0, 1431 DataError: 0, 1432 TransactionInactiveError: 0, 1433 ReadOnlyError: 0, 1434 VersionError: 0, 1435 OperationError: 0, 1436 NotAllowedError: 0 1437 }; 1438 1439 if (!(name in name_code_map)) { 1440 throw new AssertionError('Test bug: unrecognized DOMException code "' + code + '" passed to assert_throws()'); 1441 } 1442 1443 var required_props = { code: name_code_map[name] }; 1444 1445 if (required_props.code === 0 || 1446 ("name" in e && 1447 e.name !== e.name.toUpperCase() && 1448 e.name !== "DOMException")) { 1449 // New style exception: also test the name property. 1450 required_props.name = name; 1451 } 1452 1453 //We'd like to test that e instanceof the appropriate interface, 1454 //but we can't, because we don't know what window it was created 1455 //in. It might be an instanceof the appropriate interface on some 1456 //unknown other window. TODO: Work around this somehow? 1457 1458 for (var prop in required_props) { 1459 assert(prop in e && e[prop] == required_props[prop], 1460 "assert_throws", description, 1461 "${func} threw ${e} that is not a DOMException " + code + ": property ${prop} is equal to ${actual}, expected ${expected}", 1462 {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]}); 1463 } 1464 } 1465 } 1466 expose(assert_throws, "assert_throws"); 1467 1468 function assert_unreached(description) { 1469 assert(false, "assert_unreached", description, 1470 "Reached unreachable code"); 1471 } 1472 expose(assert_unreached, "assert_unreached"); 1473 1474 function assert_any(assert_func, actual, expected_array) 1475 { 1476 var args = [].slice.call(arguments, 3); 1477 var errors = []; 1478 var passed = false; 1479 forEach(expected_array, 1480 function(expected) 1481 { 1482 try { 1483 assert_func.apply(this, [actual, expected].concat(args)); 1484 passed = true; 1485 } catch (e) { 1486 errors.push(e.message); 1487 } 1488 }); 1489 if (!passed) { 1490 throw new AssertionError(errors.join("\n\n")); 1491 } 1492 } 1493 expose(assert_any, "assert_any"); 1494 1495 function Test(name, properties) 1496 { 1497 if (tests.file_is_test && tests.tests.length) { 1498 throw new Error("Tried to create a test with file_is_test"); 1499 } 1500 this.name = name; 1501 1502 this.phase = (tests.is_aborted || tests.phase === tests.phases.COMPLETE) ? 1503 this.phases.COMPLETE : this.phases.INITIAL; 1504 1505 this.status = this.NOTRUN; 1506 this.timeout_id = null; 1507 this.index = null; 1508 1509 this.properties = properties; 1510 this.timeout_length = settings.test_timeout; 1511 if (this.timeout_length !== null) { 1512 this.timeout_length *= tests.timeout_multiplier; 1513 } 1514 1515 this.message = null; 1516 this.stack = null; 1517 1518 this.steps = []; 1519 this._is_promise_test = false; 1520 1521 this.cleanup_callbacks = []; 1522 this._user_defined_cleanup_count = 0; 1523 this._done_callbacks = []; 1524 1525 // Tests declared following harness completion are likely an indication 1526 // of a programming error, but they cannot be reported 1527 // deterministically. 1528 if (tests.phase === tests.phases.COMPLETE) { 1529 return; 1530 } 1531 1532 tests.push(this); 1533 } 1534 1535 Test.statuses = { 1536 PASS:0, 1537 FAIL:1, 1538 TIMEOUT:2, 1539 NOTRUN:3 1540 }; 1541 1542 Test.prototype = merge({}, Test.statuses); 1543 1544 Test.prototype.phases = { 1545 INITIAL:0, 1546 STARTED:1, 1547 HAS_RESULT:2, 1548 CLEANING:3, 1549 COMPLETE:4 1550 }; 1551 1552 Test.prototype.structured_clone = function() 1553 { 1554 if (!this._structured_clone) { 1555 var msg = this.message; 1556 msg = msg ? String(msg) : msg; 1557 this._structured_clone = merge({ 1558 name:String(this.name), 1559 properties:merge({}, this.properties), 1560 phases:merge({}, this.phases) 1561 }, Test.statuses); 1562 } 1563 this._structured_clone.status = this.status; 1564 this._structured_clone.message = this.message; 1565 this._structured_clone.stack = this.stack; 1566 this._structured_clone.index = this.index; 1567 this._structured_clone.phase = this.phase; 1568 return this._structured_clone; 1569 }; 1570 1571 Test.prototype.step = function(func, this_obj) 1572 { 1573 if (this.phase > this.phases.STARTED) { 1574 return; 1575 } 1576 this.phase = this.phases.STARTED; 1577 //If we don't get a result before the harness times out that will be a test timeout 1578 this.set_status(this.TIMEOUT, "Test timed out"); 1579 1580 tests.started = true; 1581 tests.notify_test_state(this); 1582 1583 if (this.timeout_id === null) { 1584 this.set_timeout(); 1585 } 1586 1587 this.steps.push(func); 1588 1589 if (arguments.length === 1) { 1590 this_obj = this; 1591 } 1592 1593 try { 1594 return func.apply(this_obj, Array.prototype.slice.call(arguments, 2)); 1595 } catch (e) { 1596 if (this.phase >= this.phases.HAS_RESULT) { 1597 return; 1598 } 1599 var message = String((typeof e === "object" && e !== null) ? e.message : e); 1600 var stack = e.stack ? e.stack : null; 1601 1602 this.set_status(this.FAIL, message, stack); 1603 this.phase = this.phases.HAS_RESULT; 1604 this.done(); 1605 } 1606 }; 1607 1608 Test.prototype.step_func = function(func, this_obj) 1609 { 1610 var test_this = this; 1611 1612 if (arguments.length === 1) { 1613 this_obj = test_this; 1614 } 1615 1616 return function() 1617 { 1618 return test_this.step.apply(test_this, [func, this_obj].concat( 1619 Array.prototype.slice.call(arguments))); 1620 }; 1621 }; 1622 1623 Test.prototype.step_func_done = function(func, this_obj) 1624 { 1625 var test_this = this; 1626 1627 if (arguments.length === 1) { 1628 this_obj = test_this; 1629 } 1630 1631 return function() 1632 { 1633 if (func) { 1634 test_this.step.apply(test_this, [func, this_obj].concat( 1635 Array.prototype.slice.call(arguments))); 1636 } 1637 test_this.done(); 1638 }; 1639 }; 1640 1641 Test.prototype.unreached_func = function(description) 1642 { 1643 return this.step_func(function() { 1644 assert_unreached(description); 1645 }); 1646 }; 1647 1648 Test.prototype.step_timeout = function(f, timeout) { 1649 var test_this = this; 1650 var args = Array.prototype.slice.call(arguments, 2); 1651 return setTimeout(this.step_func(function() { 1652 return f.apply(test_this, args); 1653 }), timeout * tests.timeout_multiplier); 1654 } 1655 1656 /* 1657 * Private method for registering cleanup functions. `testharness.js` 1658 * internals should use this method instead of the public `add_cleanup` 1659 * method in order to hide implementation details from the harness status 1660 * message in the case errors. 1661 */ 1662 Test.prototype._add_cleanup = function(callback) { 1663 this.cleanup_callbacks.push(callback); 1664 }; 1665 1666 /* 1667 * Schedule a function to be run after the test result is known, regardless 1668 * of passing or failing state. The behavior of this function will not 1669 * influence the result of the test, but if an exception is thrown, the 1670 * test harness will report an error. 1671 */ 1672 Test.prototype.add_cleanup = function(callback) { 1673 this._user_defined_cleanup_count += 1; 1674 this._add_cleanup(callback); 1675 }; 1676 1677 Test.prototype.set_timeout = function() 1678 { 1679 if (this.timeout_length !== null) { 1680 var this_obj = this; 1681 this.timeout_id = setTimeout(function() 1682 { 1683 this_obj.timeout(); 1684 }, this.timeout_length); 1685 } 1686 }; 1687 1688 Test.prototype.set_status = function(status, message, stack) 1689 { 1690 this.status = status; 1691 this.message = message; 1692 this.stack = stack ? stack : null; 1693 }; 1694 1695 Test.prototype.timeout = function() 1696 { 1697 this.timeout_id = null; 1698 this.set_status(this.TIMEOUT, "Test timed out"); 1699 this.phase = this.phases.HAS_RESULT; 1700 this.done(); 1701 }; 1702 1703 Test.prototype.force_timeout = Test.prototype.timeout; 1704 1705 /** 1706 * Update the test status, initiate "cleanup" functions, and signal test 1707 * completion. 1708 */ 1709 Test.prototype.done = function() 1710 { 1711 if (this.phase >= this.phases.CLEANING) { 1712 return; 1713 } 1714 1715 if (this.phase <= this.phases.STARTED) { 1716 this.set_status(this.PASS, null); 1717 } 1718 1719 if (global_scope.clearTimeout) { 1720 clearTimeout(this.timeout_id); 1721 } 1722 1723 this.cleanup(); 1724 }; 1725 1726 function add_test_done_callback(test, callback) 1727 { 1728 if (test.phase === test.phases.COMPLETE) { 1729 callback(); 1730 return; 1731 } 1732 1733 test._done_callbacks.push(callback); 1734 } 1735 1736 /* 1737 * Invoke all specified cleanup functions. If one or more produce an error, 1738 * the context is in an unpredictable state, so all further testing should 1739 * be cancelled. 1740 */ 1741 Test.prototype.cleanup = function() { 1742 var error_count = 0; 1743 var bad_value_count = 0; 1744 function on_error() { 1745 error_count += 1; 1746 // Abort tests immediately so that tests declared within subsequent 1747 // cleanup functions are not run. 1748 tests.abort(); 1749 } 1750 var this_obj = this; 1751 var results = []; 1752 1753 this.phase = this.phases.CLEANING; 1754 1755 forEach(this.cleanup_callbacks, 1756 function(cleanup_callback) { 1757 var result; 1758 1759 try { 1760 result = cleanup_callback(); 1761 } catch (e) { 1762 on_error(); 1763 return; 1764 } 1765 1766 if (!is_valid_cleanup_result(this_obj, result)) { 1767 bad_value_count += 1; 1768 // Abort tests immediately so that tests declared 1769 // within subsequent cleanup functions are not run. 1770 tests.abort(); 1771 } 1772 1773 results.push(result); 1774 }); 1775 1776 if (!this._is_promise_test) { 1777 cleanup_done(this_obj, error_count, bad_value_count); 1778 } else { 1779 all_async(results, 1780 function(result, done) { 1781 if (result && typeof result.then === "function") { 1782 result 1783 .then(null, on_error) 1784 .then(done); 1785 } else { 1786 done(); 1787 } 1788 }, 1789 function() { 1790 cleanup_done(this_obj, error_count, bad_value_count); 1791 }); 1792 } 1793 }; 1794 1795 /** 1796 * Determine if the return value of a cleanup function is valid for a given 1797 * test. Any test may return the value `undefined`. Tests created with 1798 * `promise_test` may alternatively return "thenable" object values. 1799 */ 1800 function is_valid_cleanup_result(test, result) { 1801 if (result === undefined) { 1802 return true; 1803 } 1804 1805 if (test._is_promise_test) { 1806 return result && typeof result.then === "function"; 1807 } 1808 1809 return false; 1810 } 1811 1812 function cleanup_done(test, error_count, bad_value_count) { 1813 if (error_count || bad_value_count) { 1814 var total = test._user_defined_cleanup_count; 1815 1816 tests.status.status = tests.status.ERROR; 1817 tests.status.message = "Test named '" + test.name + 1818 "' specified " + total + 1819 " 'cleanup' function" + (total > 1 ? "s" : ""); 1820 1821 if (error_count) { 1822 tests.status.message += ", and " + error_count + " failed"; 1823 } 1824 1825 if (bad_value_count) { 1826 var type = test._is_promise_test ? 1827 "non-thenable" : "non-undefined"; 1828 tests.status.message += ", and " + bad_value_count + 1829 " returned a " + type + " value"; 1830 } 1831 1832 tests.status.message += "."; 1833 1834 tests.status.stack = null; 1835 } 1836 1837 test.phase = test.phases.COMPLETE; 1838 tests.result(test); 1839 forEach(test._done_callbacks, 1840 function(callback) { 1841 callback(); 1842 }); 1843 test._done_callbacks.length = 0; 1844 } 1845 1846 /* 1847 * A RemoteTest object mirrors a Test object on a remote worker. The 1848 * associated RemoteWorker updates the RemoteTest object in response to 1849 * received events. In turn, the RemoteTest object replicates these events 1850 * on the local document. This allows listeners (test result reporting 1851 * etc..) to transparently handle local and remote events. 1852 */ 1853 function RemoteTest(clone) { 1854 var this_obj = this; 1855 Object.keys(clone).forEach( 1856 function(key) { 1857 this_obj[key] = clone[key]; 1858 }); 1859 this.index = null; 1860 this.phase = this.phases.INITIAL; 1861 this.update_state_from(clone); 1862 this._done_callbacks = []; 1863 tests.push(this); 1864 } 1865 1866 RemoteTest.prototype.structured_clone = function() { 1867 var clone = {}; 1868 Object.keys(this).forEach( 1869 (function(key) { 1870 var value = this[key]; 1871 // `RemoteTest` instances are responsible for managing 1872 // their own "done" callback functions, so those functions 1873 // are not relevant in other execution contexts. Because of 1874 // this (and because Function values cannot be serialized 1875 // for cross-realm transmittance), the property should not 1876 // be considered when cloning instances. 1877 if (key === '_done_callbacks' ) { 1878 return; 1879 } 1880 1881 if (typeof value === "object" && value !== null) { 1882 clone[key] = merge({}, value); 1883 } else { 1884 clone[key] = value; 1885 } 1886 }).bind(this)); 1887 clone.phases = merge({}, this.phases); 1888 return clone; 1889 }; 1890 1891 /** 1892 * `RemoteTest` instances are objects which represent tests running in 1893 * another realm. They do not define "cleanup" functions (if necessary, 1894 * such functions are defined on the associated `Test` instance within the 1895 * external realm). However, `RemoteTests` may have "done" callbacks (e.g. 1896 * as attached by the `Tests` instance responsible for tracking the overall 1897 * test status in the parent realm). The `cleanup` method delegates to 1898 * `done` in order to ensure that such callbacks are invoked following the 1899 * completion of the `RemoteTest`. 1900 */ 1901 RemoteTest.prototype.cleanup = function() { 1902 this.done(); 1903 }; 1904 RemoteTest.prototype.phases = Test.prototype.phases; 1905 RemoteTest.prototype.update_state_from = function(clone) { 1906 this.status = clone.status; 1907 this.message = clone.message; 1908 this.stack = clone.stack; 1909 if (this.phase === this.phases.INITIAL) { 1910 this.phase = this.phases.STARTED; 1911 } 1912 }; 1913 RemoteTest.prototype.done = function() { 1914 this.phase = this.phases.COMPLETE; 1915 1916 forEach(this._done_callbacks, 1917 function(callback) { 1918 callback(); 1919 }); 1920 } 1921 1922 /* 1923 * A RemoteContext listens for test events from a remote test context, such 1924 * as another window or a worker. These events are then used to construct 1925 * and maintain RemoteTest objects that mirror the tests running in the 1926 * remote context. 1927 * 1928 * An optional third parameter can be used as a predicate to filter incoming 1929 * MessageEvents. 1930 */ 1931 function RemoteContext(remote, message_target, message_filter) { 1932 this.running = true; 1933 this.started = false; 1934 this.tests = new Array(); 1935 this.early_exception = null; 1936 1937 var this_obj = this; 1938 // If remote context is cross origin assigning to onerror is not 1939 // possible, so silently catch those errors. 1940 try { 1941 remote.onerror = function(error) { this_obj.remote_error(error); }; 1942 } catch (e) { 1943 // Ignore. 1944 } 1945 1946 // Keeping a reference to the remote object and the message handler until 1947 // remote_done() is seen prevents the remote object and its message channel 1948 // from going away before all the messages are dispatched. 1949 this.remote = remote; 1950 this.message_target = message_target; 1951 this.message_handler = function(message) { 1952 var passesFilter = !message_filter || message_filter(message); 1953 // The reference to the `running` property in the following 1954 // condition is unnecessary because that value is only set to 1955 // `false` after the `message_handler` function has been 1956 // unsubscribed. 1957 // TODO: Simplify the condition by removing the reference. 1958 if (this_obj.running && message.data && passesFilter && 1959 (message.data.type in this_obj.message_handlers)) { 1960 this_obj.message_handlers[message.data.type].call(this_obj, message.data); 1961 } 1962 }; 1963 1964 if (self.Promise) { 1965 this.done = new Promise(function(resolve) { 1966 this_obj.doneResolve = resolve; 1967 }); 1968 } 1969 1970 this.message_target.addEventListener("message", this.message_handler); 1971 } 1972 1973 RemoteContext.prototype.remote_error = function(error) { 1974 if (error.preventDefault) { 1975 error.preventDefault(); 1976 } 1977 1978 // Defer interpretation of errors until the testing protocol has 1979 // started and the remote test's `allow_uncaught_exception` property 1980 // is available. 1981 if (!this.started) { 1982 this.early_exception = error; 1983 } else if (!this.allow_uncaught_exception) { 1984 this.report_uncaught(error); 1985 } 1986 }; 1987 1988 RemoteContext.prototype.report_uncaught = function(error) { 1989 var message = error.message || String(error); 1990 var filename = (error.filename ? " " + error.filename: ""); 1991 // FIXME: Display remote error states separately from main document 1992 // error state. 1993 tests.set_status(tests.status.ERROR, 1994 "Error in remote" + filename + ": " + message, 1995 error.stack); 1996 }; 1997 1998 RemoteContext.prototype.start = function(data) { 1999 this.started = true; 2000 this.allow_uncaught_exception = data.properties.allow_uncaught_exception; 2001 2002 if (this.early_exception && !this.allow_uncaught_exception) { 2003 this.report_uncaught(this.early_exception); 2004 } 2005 }; 2006 2007 RemoteContext.prototype.test_state = function(data) { 2008 var remote_test = this.tests[data.test.index]; 2009 if (!remote_test) { 2010 remote_test = new RemoteTest(data.test); 2011 this.tests[data.test.index] = remote_test; 2012 } 2013 remote_test.update_state_from(data.test); 2014 tests.notify_test_state(remote_test); 2015 }; 2016 2017 RemoteContext.prototype.test_done = function(data) { 2018 var remote_test = this.tests[data.test.index]; 2019 remote_test.update_state_from(data.test); 2020 remote_test.done(); 2021 tests.result(remote_test); 2022 }; 2023 2024 RemoteContext.prototype.remote_done = function(data) { 2025 if (tests.status.status === null && 2026 data.status.status !== data.status.OK) { 2027 tests.set_status(data.status.status, data.status.message, data.status.sack); 2028 } 2029 2030 this.message_target.removeEventListener("message", this.message_handler); 2031 this.running = false; 2032 2033 // If remote context is cross origin assigning to onerror is not 2034 // possible, so silently catch those errors. 2035 try { 2036 this.remote.onerror = null; 2037 } catch (e) { 2038 // Ignore. 2039 } 2040 2041 this.remote = null; 2042 this.message_target = null; 2043 if (this.doneResolve) { 2044 this.doneResolve(); 2045 } 2046 2047 if (tests.all_done()) { 2048 tests.complete(); 2049 } 2050 }; 2051 2052 RemoteContext.prototype.message_handlers = { 2053 start: RemoteContext.prototype.start, 2054 test_state: RemoteContext.prototype.test_state, 2055 result: RemoteContext.prototype.test_done, 2056 complete: RemoteContext.prototype.remote_done 2057 }; 2058 2059 /* 2060 * Harness 2061 */ 2062 2063 function TestsStatus() 2064 { 2065 this.status = null; 2066 this.message = null; 2067 this.stack = null; 2068 } 2069 2070 TestsStatus.statuses = { 2071 OK:0, 2072 ERROR:1, 2073 TIMEOUT:2 2074 }; 2075 2076 TestsStatus.prototype = merge({}, TestsStatus.statuses); 2077 2078 TestsStatus.prototype.structured_clone = function() 2079 { 2080 if (!this._structured_clone) { 2081 var msg = this.message; 2082 msg = msg ? String(msg) : msg; 2083 this._structured_clone = merge({ 2084 status:this.status, 2085 message:msg, 2086 stack:this.stack 2087 }, TestsStatus.statuses); 2088 } 2089 return this._structured_clone; 2090 }; 2091 2092 function Tests() 2093 { 2094 this.tests = []; 2095 this.num_pending = 0; 2096 2097 this.phases = { 2098 INITIAL:0, 2099 SETUP:1, 2100 HAVE_TESTS:2, 2101 HAVE_RESULTS:3, 2102 COMPLETE:4 2103 }; 2104 this.phase = this.phases.INITIAL; 2105 2106 this.properties = {}; 2107 2108 this.wait_for_finish = false; 2109 this.processing_callbacks = false; 2110 2111 this.allow_uncaught_exception = false; 2112 2113 this.file_is_test = false; 2114 2115 this.timeout_multiplier = 1; 2116 this.timeout_length = test_environment.test_timeout(); 2117 this.timeout_id = null; 2118 2119 this.start_callbacks = []; 2120 this.test_state_callbacks = []; 2121 this.test_done_callbacks = []; 2122 this.all_done_callbacks = []; 2123 2124 this.pending_remotes = []; 2125 2126 this.status = new TestsStatus(); 2127 2128 var this_obj = this; 2129 2130 test_environment.add_on_loaded_callback(function() { 2131 if (this_obj.all_done()) { 2132 this_obj.complete(); 2133 } 2134 }); 2135 2136 this.set_timeout(); 2137 } 2138 2139 Tests.prototype.setup = function(func, properties) 2140 { 2141 if (this.phase >= this.phases.HAVE_RESULTS) { 2142 return; 2143 } 2144 2145 if (this.phase < this.phases.SETUP) { 2146 this.phase = this.phases.SETUP; 2147 } 2148 2149 this.properties = properties; 2150 2151 for (var p in properties) { 2152 if (properties.hasOwnProperty(p)) { 2153 var value = properties[p]; 2154 if (p == "allow_uncaught_exception") { 2155 this.allow_uncaught_exception = value; 2156 } else if (p == "explicit_done" && value) { 2157 this.wait_for_finish = true; 2158 } else if (p == "explicit_timeout" && value) { 2159 this.timeout_length = null; 2160 if (this.timeout_id) 2161 { 2162 clearTimeout(this.timeout_id); 2163 } 2164 } else if (p == "timeout_multiplier") { 2165 this.timeout_multiplier = value; 2166 if (this.timeout_length) { 2167 this.timeout_length *= this.timeout_multiplier; 2168 } 2169 } 2170 } 2171 } 2172 2173 if (func) { 2174 try { 2175 func(); 2176 } catch (e) { 2177 this.status.status = this.status.ERROR; 2178 this.status.message = String(e); 2179 this.status.stack = e.stack ? e.stack : null; 2180 } 2181 } 2182 this.set_timeout(); 2183 }; 2184 2185 Tests.prototype.set_file_is_test = function() { 2186 if (this.tests.length > 0) { 2187 throw new Error("Tried to set file as test after creating a test"); 2188 } 2189 this.wait_for_finish = true; 2190 this.file_is_test = true; 2191 // Create the test, which will add it to the list of tests 2192 async_test(); 2193 }; 2194 2195 Tests.prototype.set_status = function(status, message, stack) 2196 { 2197 this.status.status = status; 2198 this.status.message = message; 2199 this.status.stack = stack ? stack : null; 2200 }; 2201 2202 Tests.prototype.set_timeout = function() { 2203 if (global_scope.clearTimeout) { 2204 var this_obj = this; 2205 clearTimeout(this.timeout_id); 2206 if (this.timeout_length !== null) { 2207 this.timeout_id = setTimeout(function() { 2208 this_obj.timeout(); 2209 }, this.timeout_length); 2210 } 2211 } 2212 }; 2213 2214 Tests.prototype.timeout = function() { 2215 var test_in_cleanup = null; 2216 2217 if (this.status.status === null) { 2218 forEach(this.tests, 2219 function(test) { 2220 // No more than one test is expected to be in the 2221 // "CLEANUP" phase at any time 2222 if (test.phase === test.phases.CLEANING) { 2223 test_in_cleanup = test; 2224 } 2225 2226 test.phase = test.phases.COMPLETE; 2227 }); 2228 2229 // Timeouts that occur while a test is in the "cleanup" phase 2230 // indicate that some global state was not properly reverted. This 2231 // invalidates the overall test execution, so the timeout should be 2232 // reported as an error and cancel the execution of any remaining 2233 // tests. 2234 if (test_in_cleanup) { 2235 this.status.status = this.status.ERROR; 2236 this.status.message = "Timeout while running cleanup for " + 2237 "test named \"" + test_in_cleanup.name + "\"."; 2238 tests.status.stack = null; 2239 } else { 2240 this.status.status = this.status.TIMEOUT; 2241 } 2242 } 2243 2244 this.complete(); 2245 }; 2246 2247 Tests.prototype.end_wait = function() 2248 { 2249 this.wait_for_finish = false; 2250 if (this.all_done()) { 2251 this.complete(); 2252 } 2253 }; 2254 2255 Tests.prototype.push = function(test) 2256 { 2257 if (this.phase < this.phases.HAVE_TESTS) { 2258 this.start(); 2259 } 2260 this.num_pending++; 2261 test.index = this.tests.push(test); 2262 this.notify_test_state(test); 2263 }; 2264 2265 Tests.prototype.notify_test_state = function(test) { 2266 var this_obj = this; 2267 forEach(this.test_state_callbacks, 2268 function(callback) { 2269 callback(test, this_obj); 2270 }); 2271 }; 2272 2273 Tests.prototype.all_done = function() { 2274 return this.tests.length > 0 && test_environment.all_loaded && 2275 (this.num_pending === 0 || this.is_aborted) && !this.wait_for_finish && 2276 !this.processing_callbacks && 2277 !this.pending_remotes.some(function(w) { return w.running; }); 2278 }; 2279 2280 Tests.prototype.start = function() { 2281 this.phase = this.phases.HAVE_TESTS; 2282 this.notify_start(); 2283 }; 2284 2285 Tests.prototype.notify_start = function() { 2286 var this_obj = this; 2287 forEach (this.start_callbacks, 2288 function(callback) 2289 { 2290 callback(this_obj.properties); 2291 }); 2292 }; 2293 2294 Tests.prototype.result = function(test) 2295 { 2296 // If the harness has already transitioned beyond the `HAVE_RESULTS` 2297 // phase, subsequent tests should not cause it to revert. 2298 if (this.phase <= this.phases.HAVE_RESULTS) { 2299 this.phase = this.phases.HAVE_RESULTS; 2300 } 2301 this.num_pending--; 2302 this.notify_result(test); 2303 }; 2304 2305 Tests.prototype.notify_result = function(test) { 2306 var this_obj = this; 2307 this.processing_callbacks = true; 2308 forEach(this.test_done_callbacks, 2309 function(callback) 2310 { 2311 callback(test, this_obj); 2312 }); 2313 this.processing_callbacks = false; 2314 if (this_obj.all_done()) { 2315 this_obj.complete(); 2316 } 2317 }; 2318 2319 Tests.prototype.complete = function() { 2320 if (this.phase === this.phases.COMPLETE) { 2321 return; 2322 } 2323 var this_obj = this; 2324 var all_complete = function() { 2325 this_obj.phase = this_obj.phases.COMPLETE; 2326 this_obj.notify_complete(); 2327 }; 2328 var incomplete = filter(this.tests, 2329 function(test) { 2330 return test.phase < test.phases.COMPLETE; 2331 }); 2332 2333 /** 2334 * To preserve legacy behavior, overall test completion must be 2335 * signaled synchronously. 2336 */ 2337 if (incomplete.length === 0) { 2338 all_complete(); 2339 return; 2340 } 2341 2342 all_async(incomplete, 2343 function(test, testDone) 2344 { 2345 if (test.phase === test.phases.INITIAL) { 2346 test.phase = test.phases.COMPLETE; 2347 testDone(); 2348 } else { 2349 add_test_done_callback(test, testDone); 2350 test.cleanup(); 2351 } 2352 }, 2353 all_complete); 2354 }; 2355 2356 /** 2357 * Update the harness status to reflect an unrecoverable harness error that 2358 * should cancel all further testing. Update all previously-defined tests 2359 * which have not yet started to indicate that they will not be executed. 2360 */ 2361 Tests.prototype.abort = function() { 2362 this.status.status = this.status.ERROR; 2363 this.is_aborted = true; 2364 2365 forEach(this.tests, 2366 function(test) { 2367 if (test.phase === test.phases.INITIAL) { 2368 test.phase = test.phases.COMPLETE; 2369 } 2370 }); 2371 }; 2372 2373 /* 2374 * Determine if any tests share the same `name` property. Return an array 2375 * containing the names of any such duplicates. 2376 */ 2377 Tests.prototype.find_duplicates = function() { 2378 var names = Object.create(null); 2379 var duplicates = []; 2380 2381 forEach (this.tests, 2382 function(test) 2383 { 2384 if (test.name in names && duplicates.indexOf(test.name) === -1) { 2385 duplicates.push(test.name); 2386 } 2387 names[test.name] = true; 2388 }); 2389 2390 return duplicates; 2391 }; 2392 2393 function code_unit_str(char) { 2394 return 'U+' + char.charCodeAt(0).toString(16); 2395 } 2396 2397 function sanitize_unpaired_surrogates(str) { 2398 return str.replace(/([\ud800-\udbff])(?![\udc00-\udfff])/g, 2399 function(_, unpaired) 2400 { 2401 return code_unit_str(unpaired); 2402 }) 2403 // This replacement is intentionally implemented without an 2404 // ES2018 negative lookbehind assertion to support runtimes 2405 // which do not yet implement that language feature. 2406 .replace(/(^|[^\ud800-\udbff])([\udc00-\udfff])/g, 2407 function(_, previous, unpaired) { 2408 if (/[\udc00-\udfff]/.test(previous)) { 2409 previous = code_unit_str(previous); 2410 } 2411 2412 return previous + code_unit_str(unpaired); 2413 }); 2414 } 2415 2416 function sanitize_all_unpaired_surrogates(tests) { 2417 forEach (tests, 2418 function (test) 2419 { 2420 var sanitized = sanitize_unpaired_surrogates(test.name); 2421 2422 if (test.name !== sanitized) { 2423 test.name = sanitized; 2424 delete test._structured_clone; 2425 } 2426 }); 2427 } 2428 2429 Tests.prototype.notify_complete = function() { 2430 var this_obj = this; 2431 var duplicates; 2432 2433 if (this.status.status === null) { 2434 duplicates = this.find_duplicates(); 2435 2436 // Some transports adhere to UTF-8's restriction on unpaired 2437 // surrogates. Sanitize the titles so that the results can be 2438 // consistently sent via all transports. 2439 sanitize_all_unpaired_surrogates(this.tests); 2440 2441 // Test names are presumed to be unique within test files--this 2442 // allows consumers to use them for identification purposes. 2443 // Duplicated names violate this expectation and should therefore 2444 // be reported as an error. 2445 if (duplicates.length) { 2446 this.status.status = this.status.ERROR; 2447 this.status.message = 2448 duplicates.length + ' duplicate test name' + 2449 (duplicates.length > 1 ? 's' : '') + ': "' + 2450 duplicates.join('", "') + '"'; 2451 } else { 2452 this.status.status = this.status.OK; 2453 } 2454 } 2455 2456 forEach (this.all_done_callbacks, 2457 function(callback) 2458 { 2459 callback(this_obj.tests, this_obj.status); 2460 }); 2461 }; 2462 2463 /* 2464 * Constructs a RemoteContext that tracks tests from a specific worker. 2465 */ 2466 Tests.prototype.create_remote_worker = function(worker) { 2467 var message_port; 2468 2469 if (is_service_worker(worker)) { 2470 if (window.MessageChannel) { 2471 // The ServiceWorker's implicit MessagePort is currently not 2472 // reliably accessible from the ServiceWorkerGlobalScope due to 2473 // Blink setting MessageEvent.source to null for messages sent 2474 // via ServiceWorker.postMessage(). Until that's resolved, 2475 // create an explicit MessageChannel and pass one end to the 2476 // worker. 2477 var message_channel = new MessageChannel(); 2478 message_port = message_channel.port1; 2479 message_port.start(); 2480 worker.postMessage({type: "connect"}, [message_channel.port2]); 2481 } else { 2482 // If MessageChannel is not available, then try the 2483 // ServiceWorker.postMessage() approach using MessageEvent.source 2484 // on the other end. 2485 message_port = navigator.serviceWorker; 2486 worker.postMessage({type: "connect"}); 2487 } 2488 } else if (is_shared_worker(worker)) { 2489 message_port = worker.port; 2490 message_port.start(); 2491 } else { 2492 message_port = worker; 2493 } 2494 2495 return new RemoteContext(worker, message_port); 2496 }; 2497 2498 /* 2499 * Constructs a RemoteContext that tracks tests from a specific window. 2500 */ 2501 Tests.prototype.create_remote_window = function(remote) { 2502 remote.postMessage({type: "getmessages"}, "*"); 2503 return new RemoteContext( 2504 remote, 2505 window, 2506 function(msg) { 2507 return msg.source === remote; 2508 } 2509 ); 2510 }; 2511 2512 Tests.prototype.fetch_tests_from_worker = function(worker) { 2513 if (this.phase >= this.phases.COMPLETE) { 2514 return; 2515 } 2516 2517 var remoteContext = this.create_remote_worker(worker); 2518 this.pending_remotes.push(remoteContext); 2519 return remoteContext.done; 2520 }; 2521 2522 function fetch_tests_from_worker(port) { 2523 return tests.fetch_tests_from_worker(port); 2524 } 2525 expose(fetch_tests_from_worker, 'fetch_tests_from_worker'); 2526 2527 Tests.prototype.fetch_tests_from_window = function(remote) { 2528 if (this.phase >= this.phases.COMPLETE) { 2529 return; 2530 } 2531 2532 this.pending_remotes.push(this.create_remote_window(remote)); 2533 }; 2534 2535 function fetch_tests_from_window(window) { 2536 tests.fetch_tests_from_window(window); 2537 } 2538 expose(fetch_tests_from_window, 'fetch_tests_from_window'); 2539 2540 function timeout() { 2541 if (tests.timeout_length === null) { 2542 tests.timeout(); 2543 } 2544 } 2545 expose(timeout, 'timeout'); 2546 2547 function add_start_callback(callback) { 2548 tests.start_callbacks.push(callback); 2549 } 2550 2551 function add_test_state_callback(callback) { 2552 tests.test_state_callbacks.push(callback); 2553 } 2554 2555 function add_result_callback(callback) { 2556 tests.test_done_callbacks.push(callback); 2557 } 2558 2559 function add_completion_callback(callback) { 2560 tests.all_done_callbacks.push(callback); 2561 } 2562 2563 expose(add_start_callback, 'add_start_callback'); 2564 expose(add_test_state_callback, 'add_test_state_callback'); 2565 expose(add_result_callback, 'add_result_callback'); 2566 expose(add_completion_callback, 'add_completion_callback'); 2567 2568 function remove(array, item) { 2569 var index = array.indexOf(item); 2570 if (index > -1) { 2571 array.splice(index, 1); 2572 } 2573 } 2574 2575 function remove_start_callback(callback) { 2576 remove(tests.start_callbacks, callback); 2577 } 2578 2579 function remove_test_state_callback(callback) { 2580 remove(tests.test_state_callbacks, callback); 2581 } 2582 2583 function remove_result_callback(callback) { 2584 remove(tests.test_done_callbacks, callback); 2585 } 2586 2587 function remove_completion_callback(callback) { 2588 remove(tests.all_done_callbacks, callback); 2589 } 2590 2591 /* 2592 * Output listener 2593 */ 2594 2595 function Output() { 2596 this.output_document = document; 2597 this.output_node = null; 2598 this.enabled = settings.output; 2599 this.phase = this.INITIAL; 2600 } 2601 2602 Output.prototype.INITIAL = 0; 2603 Output.prototype.STARTED = 1; 2604 Output.prototype.HAVE_RESULTS = 2; 2605 Output.prototype.COMPLETE = 3; 2606 2607 Output.prototype.setup = function(properties) { 2608 if (this.phase > this.INITIAL) { 2609 return; 2610 } 2611 2612 //If output is disabled in testharnessreport.js the test shouldn't be 2613 //able to override that 2614 this.enabled = this.enabled && (properties.hasOwnProperty("output") ? 2615 properties.output : settings.output); 2616 }; 2617 2618 Output.prototype.init = function(properties) { 2619 if (this.phase >= this.STARTED) { 2620 return; 2621 } 2622 if (properties.output_document) { 2623 this.output_document = properties.output_document; 2624 } else { 2625 this.output_document = document; 2626 } 2627 this.phase = this.STARTED; 2628 }; 2629 2630 Output.prototype.resolve_log = function() { 2631 var output_document; 2632 if (this.output_node) { 2633 return; 2634 } 2635 if (typeof this.output_document === "function") { 2636 output_document = this.output_document.apply(undefined); 2637 } else { 2638 output_document = this.output_document; 2639 } 2640 if (!output_document) { 2641 return; 2642 } 2643 var node = output_document.getElementById("log"); 2644 if (!node) { 2645 if (output_document.readyState === "loading") { 2646 return; 2647 } 2648 node = output_document.createElementNS("http://www.w3.org/1999/xhtml", "div"); 2649 node.id = "log"; 2650 if (output_document.body) { 2651 output_document.body.appendChild(node); 2652 } else { 2653 var root = output_document.documentElement; 2654 var is_html = (root && 2655 root.namespaceURI == "http://www.w3.org/1999/xhtml" && 2656 root.localName == "html"); 2657 var is_svg = (output_document.defaultView && 2658 "SVGSVGElement" in output_document.defaultView && 2659 root instanceof output_document.defaultView.SVGSVGElement); 2660 if (is_svg) { 2661 var foreignObject = output_document.createElementNS("http://www.w3.org/2000/svg", "foreignObject"); 2662 foreignObject.setAttribute("width", "100%"); 2663 foreignObject.setAttribute("height", "100%"); 2664 root.appendChild(foreignObject); 2665 foreignObject.appendChild(node); 2666 } else if (is_html) { 2667 root.appendChild(output_document.createElementNS("http://www.w3.org/1999/xhtml", "body")) 2668 .appendChild(node); 2669 } else { 2670 root.appendChild(node); 2671 } 2672 } 2673 } 2674 this.output_document = output_document; 2675 this.output_node = node; 2676 }; 2677 2678 Output.prototype.show_status = function() { 2679 if (this.phase < this.STARTED) { 2680 this.init(); 2681 } 2682 if (!this.enabled || this.phase === this.COMPLETE) { 2683 return; 2684 } 2685 this.resolve_log(); 2686 if (this.phase < this.HAVE_RESULTS) { 2687 this.phase = this.HAVE_RESULTS; 2688 } 2689 var done_count = tests.tests.length - tests.num_pending; 2690 if (this.output_node) { 2691 if (done_count < 100 || 2692 (done_count < 1000 && done_count % 100 === 0) || 2693 done_count % 1000 === 0) { 2694 this.output_node.textContent = "Running, " + 2695 done_count + " complete, " + 2696 tests.num_pending + " remain"; 2697 } 2698 } 2699 }; 2700 2701 Output.prototype.show_results = function (tests, harness_status) { 2702 if (this.phase >= this.COMPLETE) { 2703 return; 2704 } 2705 if (!this.enabled) { 2706 return; 2707 } 2708 if (!this.output_node) { 2709 this.resolve_log(); 2710 } 2711 this.phase = this.COMPLETE; 2712 2713 var log = this.output_node; 2714 if (!log) { 2715 return; 2716 } 2717 var output_document = this.output_document; 2718 2719 while (log.lastChild) { 2720 log.removeChild(log.lastChild); 2721 } 2722 2723 var stylesheet = output_document.createElementNS(xhtml_ns, "style"); 2724 stylesheet.textContent = stylesheetContent; 2725 var heads = output_document.getElementsByTagName("head"); 2726 if (heads.length) { 2727 heads[0].appendChild(stylesheet); 2728 } 2729 2730 var status_text_harness = {}; 2731 status_text_harness[harness_status.OK] = "OK"; 2732 status_text_harness[harness_status.ERROR] = "Error"; 2733 status_text_harness[harness_status.TIMEOUT] = "Timeout"; 2734 2735 var status_text = {}; 2736 status_text[Test.prototype.PASS] = "Pass"; 2737 status_text[Test.prototype.FAIL] = "Fail"; 2738 status_text[Test.prototype.TIMEOUT] = "Timeout"; 2739 status_text[Test.prototype.NOTRUN] = "Not Run"; 2740 2741 var status_number = {}; 2742 forEach(tests, 2743 function(test) { 2744 var status = status_text[test.status]; 2745 if (status_number.hasOwnProperty(status)) { 2746 status_number[status] += 1; 2747 } else { 2748 status_number[status] = 1; 2749 } 2750 }); 2751 2752 function status_class(status) 2753 { 2754 return status.replace(/\s/g, '').toLowerCase(); 2755 } 2756 2757 var summary_template = ["section", {"id":"summary"}, 2758 ["h2", {}, "Summary"], 2759 function() 2760 { 2761 2762 var status = status_text_harness[harness_status.status]; 2763 var rv = [["section", {}, 2764 ["p", {}, 2765 "Harness status: ", 2766 ["span", {"class":status_class(status)}, 2767 status 2768 ], 2769 ] 2770 ]]; 2771 2772 if (harness_status.status === harness_status.ERROR) { 2773 rv[0].push(["pre", {}, harness_status.message]); 2774 if (harness_status.stack) { 2775 rv[0].push(["pre", {}, harness_status.stack]); 2776 } 2777 } 2778 return rv; 2779 }, 2780 ["p", {}, "Found ${num_tests} tests"], 2781 function() { 2782 var rv = [["div", {}]]; 2783 var i = 0; 2784 while (status_text.hasOwnProperty(i)) { 2785 if (status_number.hasOwnProperty(status_text[i])) { 2786 var status = status_text[i]; 2787 rv[0].push(["div", {"class":status_class(status)}, 2788 ["label", {}, 2789 ["input", {type:"checkbox", checked:"checked"}], 2790 status_number[status] + " " + status]]); 2791 } 2792 i++; 2793 } 2794 return rv; 2795 }, 2796 ]; 2797 2798 log.appendChild(render(summary_template, {num_tests:tests.length}, output_document)); 2799 2800 forEach(output_document.querySelectorAll("section#summary label"), 2801 function(element) 2802 { 2803 on_event(element, "click", 2804 function(e) 2805 { 2806 if (output_document.getElementById("results") === null) { 2807 e.preventDefault(); 2808 return; 2809 } 2810 var result_class = element.parentNode.getAttribute("class"); 2811 var style_element = output_document.querySelector("style#hide-" + result_class); 2812 var input_element = element.querySelector("input"); 2813 if (!style_element && !input_element.checked) { 2814 style_element = output_document.createElementNS(xhtml_ns, "style"); 2815 style_element.id = "hide-" + result_class; 2816 style_element.textContent = "table#results > tbody > tr."+result_class+"{display:none}"; 2817 output_document.body.appendChild(style_element); 2818 } else if (style_element && input_element.checked) { 2819 style_element.parentNode.removeChild(style_element); 2820 } 2821 }); 2822 }); 2823 2824 // This use of innerHTML plus manual escaping is not recommended in 2825 // general, but is necessary here for performance. Using textContent 2826 // on each individual <td> adds tens of seconds of execution time for 2827 // large test suites (tens of thousands of tests). 2828 function escape_html(s) 2829 { 2830 return s.replace(/\&/g, "&") 2831 .replace(/</g, "<") 2832 .replace(/"/g, """) 2833 .replace(/'/g, "'"); 2834 } 2835 2836 function has_assertions() 2837 { 2838 for (var i = 0; i < tests.length; i++) { 2839 if (tests[i].properties.hasOwnProperty("assert")) { 2840 return true; 2841 } 2842 } 2843 return false; 2844 } 2845 2846 function get_assertion(test) 2847 { 2848 if (test.properties.hasOwnProperty("assert")) { 2849 if (Array.isArray(test.properties.assert)) { 2850 return test.properties.assert.join(' '); 2851 } 2852 return test.properties.assert; 2853 } 2854 return ''; 2855 } 2856 2857 log.appendChild(document.createElementNS(xhtml_ns, "section")); 2858 var assertions = has_assertions(); 2859 var html = "<h2>Details</h2><table id='results' " + (assertions ? "class='assertions'" : "" ) + ">" + 2860 "<thead><tr><th>Result</th><th>Test Name</th>" + 2861 (assertions ? "<th>Assertion</th>" : "") + 2862 "<th>Message</th></tr></thead>" + 2863 "<tbody>"; 2864 for (var i = 0; i < tests.length; i++) { 2865 html += '<tr class="' + 2866 escape_html(status_class(status_text[tests[i].status])) + 2867 '"><td>' + 2868 escape_html(status_text[tests[i].status]) + 2869 "</td><td>" + 2870 escape_html(tests[i].name) + 2871 "</td><td>" + 2872 (assertions ? escape_html(get_assertion(tests[i])) + "</td><td>" : "") + 2873 escape_html(tests[i].message ? tests[i].message : " ") + 2874 (tests[i].stack ? "<pre>" + 2875 escape_html(tests[i].stack) + 2876 "</pre>": "") + 2877 "</td></tr>"; 2878 } 2879 html += "</tbody></table>"; 2880 try { 2881 log.lastChild.innerHTML = html; 2882 } catch (e) { 2883 log.appendChild(document.createElementNS(xhtml_ns, "p")) 2884 .textContent = "Setting innerHTML for the log threw an exception."; 2885 log.appendChild(document.createElementNS(xhtml_ns, "pre")) 2886 .textContent = html; 2887 } 2888 }; 2889 2890 /* 2891 * Template code 2892 * 2893 * A template is just a JavaScript structure. An element is represented as: 2894 * 2895 * [tag_name, {attr_name:attr_value}, child1, child2] 2896 * 2897 * the children can either be strings (which act like text nodes), other templates or 2898 * functions (see below) 2899 * 2900 * A text node is represented as 2901 * 2902 * ["{text}", value] 2903 * 2904 * String values have a simple substitution syntax; ${foo} represents a variable foo. 2905 * 2906 * It is possible to embed logic in templates by using a function in a place where a 2907 * node would usually go. The function must either return part of a template or null. 2908 * 2909 * In cases where a set of nodes are required as output rather than a single node 2910 * with children it is possible to just use a list 2911 * [node1, node2, node3] 2912 * 2913 * Usage: 2914 * 2915 * render(template, substitutions) - take a template and an object mapping 2916 * variable names to parameters and return either a DOM node or a list of DOM nodes 2917 * 2918 * substitute(template, substitutions) - take a template and variable mapping object, 2919 * make the variable substitutions and return the substituted template 2920 * 2921 */ 2922 2923 function is_single_node(template) 2924 { 2925 return typeof template[0] === "string"; 2926 } 2927 2928 function substitute(template, substitutions) 2929 { 2930 if (typeof template === "function") { 2931 var replacement = template(substitutions); 2932 if (!replacement) { 2933 return null; 2934 } 2935 2936 return substitute(replacement, substitutions); 2937 } 2938 2939 if (is_single_node(template)) { 2940 return substitute_single(template, substitutions); 2941 } 2942 2943 return filter(map(template, function(x) { 2944 return substitute(x, substitutions); 2945 }), function(x) {return x !== null;}); 2946 } 2947 2948 function substitute_single(template, substitutions) 2949 { 2950 var substitution_re = /\$\{([^ }]*)\}/g; 2951 2952 function do_substitution(input) { 2953 var components = input.split(substitution_re); 2954 var rv = []; 2955 for (var i = 0; i < components.length; i += 2) { 2956 rv.push(components[i]); 2957 if (components[i + 1]) { 2958 rv.push(String(substitutions[components[i + 1]])); 2959 } 2960 } 2961 return rv; 2962 } 2963 2964 function substitute_attrs(attrs, rv) 2965 { 2966 rv[1] = {}; 2967 for (var name in template[1]) { 2968 if (attrs.hasOwnProperty(name)) { 2969 var new_name = do_substitution(name).join(""); 2970 var new_value = do_substitution(attrs[name]).join(""); 2971 rv[1][new_name] = new_value; 2972 } 2973 } 2974 } 2975 2976 function substitute_children(children, rv) 2977 { 2978 for (var i = 0; i < children.length; i++) { 2979 if (children[i] instanceof Object) { 2980 var replacement = substitute(children[i], substitutions); 2981 if (replacement !== null) { 2982 if (is_single_node(replacement)) { 2983 rv.push(replacement); 2984 } else { 2985 extend(rv, replacement); 2986 } 2987 } 2988 } else { 2989 extend(rv, do_substitution(String(children[i]))); 2990 } 2991 } 2992 return rv; 2993 } 2994 2995 var rv = []; 2996 rv.push(do_substitution(String(template[0])).join("")); 2997 2998 if (template[0] === "{text}") { 2999 substitute_children(template.slice(1), rv); 3000 } else { 3001 substitute_attrs(template[1], rv); 3002 substitute_children(template.slice(2), rv); 3003 } 3004 3005 return rv; 3006 } 3007 3008 function make_dom_single(template, doc) 3009 { 3010 var output_document = doc || document; 3011 var element; 3012 if (template[0] === "{text}") { 3013 element = output_document.createTextNode(""); 3014 for (var i = 1; i < template.length; i++) { 3015 element.data += template[i]; 3016 } 3017 } else { 3018 element = output_document.createElementNS(xhtml_ns, template[0]); 3019 for (var name in template[1]) { 3020 if (template[1].hasOwnProperty(name)) { 3021 element.setAttribute(name, template[1][name]); 3022 } 3023 } 3024 for (var i = 2; i < template.length; i++) { 3025 if (template[i] instanceof Object) { 3026 var sub_element = make_dom(template[i]); 3027 element.appendChild(sub_element); 3028 } else { 3029 var text_node = output_document.createTextNode(template[i]); 3030 element.appendChild(text_node); 3031 } 3032 } 3033 } 3034 3035 return element; 3036 } 3037 3038 function make_dom(template, substitutions, output_document) 3039 { 3040 if (is_single_node(template)) { 3041 return make_dom_single(template, output_document); 3042 } 3043 3044 return map(template, function(x) { 3045 return make_dom_single(x, output_document); 3046 }); 3047 } 3048 3049 function render(template, substitutions, output_document) 3050 { 3051 return make_dom(substitute(template, substitutions), output_document); 3052 } 3053 3054 /* 3055 * Utility functions 3056 */ 3057 function assert(expected_true, function_name, description, error, substitutions) 3058 { 3059 if (tests.tests.length === 0) { 3060 tests.set_file_is_test(); 3061 } 3062 if (expected_true !== true) { 3063 var msg = make_message(function_name, description, 3064 error, substitutions); 3065 throw new AssertionError(msg); 3066 } 3067 } 3068 3069 function AssertionError(message) 3070 { 3071 this.message = message; 3072 this.stack = this.get_stack(); 3073 } 3074 expose(AssertionError, "AssertionError"); 3075 3076 AssertionError.prototype = Object.create(Error.prototype); 3077 3078 AssertionError.prototype.get_stack = function() { 3079 var stack = new Error().stack; 3080 // IE11 does not initialize 'Error.stack' until the object is thrown. 3081 if (!stack) { 3082 try { 3083 throw new Error(); 3084 } catch (e) { 3085 stack = e.stack; 3086 } 3087 } 3088 3089 // 'Error.stack' is not supported in all browsers/versions 3090 if (!stack) { 3091 return "(Stack trace unavailable)"; 3092 } 3093 3094 var lines = stack.split("\n"); 3095 3096 // Create a pattern to match stack frames originating within testharness.js. These include the 3097 // script URL, followed by the line/col (e.g., '/resources/testharness.js:120:21'). 3098 // Escape the URL per http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript 3099 // in case it contains RegExp characters. 3100 var script_url = get_script_url(); 3101 var re_text = script_url ? script_url.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') : "\\btestharness.js"; 3102 var re = new RegExp(re_text + ":\\d+:\\d+"); 3103 3104 // Some browsers include a preamble that specifies the type of the error object. Skip this by 3105 // advancing until we find the first stack frame originating from testharness.js. 3106 var i = 0; 3107 while (!re.test(lines[i]) && i < lines.length) { 3108 i++; 3109 } 3110 3111 // Then skip the top frames originating from testharness.js to begin the stack at the test code. 3112 while (re.test(lines[i]) && i < lines.length) { 3113 i++; 3114 } 3115 3116 // Paranoid check that we didn't skip all frames. If so, return the original stack unmodified. 3117 if (i >= lines.length) { 3118 return stack; 3119 } 3120 3121 return lines.slice(i).join("\n"); 3122 } 3123 3124 function make_message(function_name, description, error, substitutions) 3125 { 3126 for (var p in substitutions) { 3127 if (substitutions.hasOwnProperty(p)) { 3128 substitutions[p] = format_value(substitutions[p]); 3129 } 3130 } 3131 var node_form = substitute(["{text}", "${function_name}: ${description}" + error], 3132 merge({function_name:function_name, 3133 description:(description?description + " ":"")}, 3134 substitutions)); 3135 return node_form.slice(1).join(""); 3136 } 3137 3138 function filter(array, callable, thisObj) { 3139 var rv = []; 3140 for (var i = 0; i < array.length; i++) { 3141 if (array.hasOwnProperty(i)) { 3142 var pass = callable.call(thisObj, array[i], i, array); 3143 if (pass) { 3144 rv.push(array[i]); 3145 } 3146 } 3147 } 3148 return rv; 3149 } 3150 3151 function map(array, callable, thisObj) 3152 { 3153 var rv = []; 3154 rv.length = array.length; 3155 for (var i = 0; i < array.length; i++) { 3156 if (array.hasOwnProperty(i)) { 3157 rv[i] = callable.call(thisObj, array[i], i, array); 3158 } 3159 } 3160 return rv; 3161 } 3162 3163 function extend(array, items) 3164 { 3165 Array.prototype.push.apply(array, items); 3166 } 3167 3168 function forEach(array, callback, thisObj) 3169 { 3170 for (var i = 0; i < array.length; i++) { 3171 if (array.hasOwnProperty(i)) { 3172 callback.call(thisObj, array[i], i, array); 3173 } 3174 } 3175 } 3176 3177 /** 3178 * Immediately invoke a "iteratee" function with a series of values in 3179 * parallel and invoke a final "done" function when all of the "iteratee" 3180 * invocations have signaled completion. 3181 * 3182 * If all callbacks complete synchronously (or if no callbacks are 3183 * specified), the `done_callback` will be invoked synchronously. It is the 3184 * responsibility of the caller to ensure asynchronicity in cases where 3185 * that is desired. 3186 * 3187 * @param {array} value Zero or more values to use in the invocation of 3188 * `iter_callback` 3189 * @param {function} iter_callback A function that will be invoked once for 3190 * each of the provided `values`. Two 3191 * arguments will be available in each 3192 * invocation: the value from `values` and 3193 * a function that must be invoked to 3194 * signal completion 3195 * @param {function} done_callback A function that will be invoked after 3196 * all operations initiated by the 3197 * `iter_callback` function have signaled 3198 * completion 3199 */ 3200 function all_async(values, iter_callback, done_callback) 3201 { 3202 var remaining = values.length; 3203 3204 if (remaining === 0) { 3205 done_callback(); 3206 } 3207 3208 forEach(values, 3209 function(element) { 3210 var invoked = false; 3211 var elDone = function() { 3212 if (invoked) { 3213 return; 3214 } 3215 3216 invoked = true; 3217 remaining -= 1; 3218 3219 if (remaining === 0) { 3220 done_callback(); 3221 } 3222 }; 3223 3224 iter_callback(element, elDone); 3225 }); 3226 } 3227 3228 function merge(a,b) 3229 { 3230 var rv = {}; 3231 var p; 3232 for (p in a) { 3233 rv[p] = a[p]; 3234 } 3235 for (p in b) { 3236 rv[p] = b[p]; 3237 } 3238 return rv; 3239 } 3240 3241 function expose(object, name) 3242 { 3243 var components = name.split("."); 3244 var target = global_scope; 3245 for (var i = 0; i < components.length - 1; i++) { 3246 if (!(components[i] in target)) { 3247 target[components[i]] = {}; 3248 } 3249 target = target[components[i]]; 3250 } 3251 target[components[components.length - 1]] = object; 3252 } 3253 3254 function is_same_origin(w) { 3255 try { 3256 'random_prop' in w; 3257 return true; 3258 } catch (e) { 3259 return false; 3260 } 3261 } 3262 3263 /** Returns the 'src' URL of the first <script> tag in the page to include the file 'testharness.js'. */ 3264 function get_script_url() 3265 { 3266 if (!('document' in global_scope)) { 3267 return undefined; 3268 } 3269 3270 var scripts = document.getElementsByTagName("script"); 3271 for (var i = 0; i < scripts.length; i++) { 3272 var src; 3273 if (scripts[i].src) { 3274 src = scripts[i].src; 3275 } else if (scripts[i].href) { 3276 //SVG case 3277 src = scripts[i].href.baseVal; 3278 } 3279 3280 var matches = src && src.match(/^(.*\/|)testharness\.js$/); 3281 if (matches) { 3282 return src; 3283 } 3284 } 3285 return undefined; 3286 } 3287 3288 /** Returns the <title> or filename or "Untitled" */ 3289 function get_title() 3290 { 3291 if ('document' in global_scope) { 3292 //Don't use document.title to work around an Opera bug in XHTML documents 3293 var title = document.getElementsByTagName("title")[0]; 3294 if (title && title.firstChild && title.firstChild.data) { 3295 return title.firstChild.data; 3296 } 3297 } 3298 if ('META_TITLE' in global_scope && META_TITLE) { 3299 return META_TITLE; 3300 } 3301 if ('location' in global_scope) { 3302 return location.pathname.substring(location.pathname.lastIndexOf('/') + 1, location.pathname.indexOf('.')); 3303 } 3304 return "Untitled"; 3305 } 3306 3307 function supports_post_message(w) 3308 { 3309 var supports; 3310 var type; 3311 // Given IE implements postMessage across nested iframes but not across 3312 // windows or tabs, you can't infer cross-origin communication from the presence 3313 // of postMessage on the current window object only. 3314 // 3315 // Touching the postMessage prop on a window can throw if the window is 3316 // not from the same origin AND post message is not supported in that 3317 // browser. So just doing an existence test here won't do, you also need 3318 // to wrap it in a try..catch block. 3319 try { 3320 type = typeof w.postMessage; 3321 if (type === "function") { 3322 supports = true; 3323 } 3324 3325 // IE8 supports postMessage, but implements it as a host object which 3326 // returns "object" as its `typeof`. 3327 else if (type === "object") { 3328 supports = true; 3329 } 3330 3331 // This is the case where postMessage isn't supported AND accessing a 3332 // window property across origins does NOT throw (e.g. old Safari browser). 3333 else { 3334 supports = false; 3335 } 3336 } catch (e) { 3337 // This is the case where postMessage isn't supported AND accessing a 3338 // window property across origins throws (e.g. old Firefox browser). 3339 supports = false; 3340 } 3341 return supports; 3342 } 3343 3344 /** 3345 * Setup globals 3346 */ 3347 3348 var tests = new Tests(); 3349 3350 if (global_scope.addEventListener) { 3351 var error_handler = function(e) { 3352 if (tests.tests.length === 0 && !tests.allow_uncaught_exception) { 3353 tests.set_file_is_test(); 3354 } 3355 3356 var stack; 3357 if (e.error && e.error.stack) { 3358 stack = e.error.stack; 3359 } else { 3360 stack = e.filename + ":" + e.lineno + ":" + e.colno; 3361 } 3362 3363 if (tests.file_is_test) { 3364 var test = tests.tests[0]; 3365 if (test.phase >= test.phases.HAS_RESULT) { 3366 return; 3367 } 3368 test.set_status(test.FAIL, e.message, stack); 3369 test.phase = test.phases.HAS_RESULT; 3370 // The following function invocation is superfluous. 3371 // TODO: Remove. 3372 test.done(); 3373 } else if (!tests.allow_uncaught_exception) { 3374 tests.status.status = tests.status.ERROR; 3375 tests.status.message = e.message; 3376 tests.status.stack = stack; 3377 } 3378 done(); 3379 }; 3380 3381 addEventListener("error", error_handler, false); 3382 addEventListener("unhandledrejection", function(e){ error_handler(e.reason); }, false); 3383 } 3384 3385 test_environment.on_tests_ready(); 3386 3387 /** 3388 * Stylesheet 3389 */ 3390 var stylesheetContent = "\ 3391html {\ 3392 font-family:DejaVu Sans, Bitstream Vera Sans, Arial, Sans;\ 3393}\ 3394\ 3395#log .warning,\ 3396#log .warning a {\ 3397 color: black;\ 3398 background: yellow;\ 3399}\ 3400\ 3401#log .error,\ 3402#log .error a {\ 3403 color: white;\ 3404 background: red;\ 3405}\ 3406\ 3407section#summary {\ 3408 margin-bottom:1em;\ 3409}\ 3410\ 3411table#results {\ 3412 border-collapse:collapse;\ 3413 table-layout:fixed;\ 3414 width:100%;\ 3415}\ 3416\ 3417table#results th:first-child,\ 3418table#results td:first-child {\ 3419 width:4em;\ 3420}\ 3421\ 3422table#results th:last-child,\ 3423table#results td:last-child {\ 3424 width:50%;\ 3425}\ 3426\ 3427table#results.assertions th:last-child,\ 3428table#results.assertions td:last-child {\ 3429 width:35%;\ 3430}\ 3431\ 3432table#results th {\ 3433 padding:0;\ 3434 padding-bottom:0.5em;\ 3435 border-bottom:medium solid black;\ 3436}\ 3437\ 3438table#results td {\ 3439 padding:1em;\ 3440 padding-bottom:0.5em;\ 3441 border-bottom:thin solid black;\ 3442}\ 3443\ 3444tr.pass > td:first-child {\ 3445 color:green;\ 3446}\ 3447\ 3448tr.fail > td:first-child {\ 3449 color:red;\ 3450}\ 3451\ 3452tr.timeout > td:first-child {\ 3453 color:red;\ 3454}\ 3455\ 3456tr.notrun > td:first-child {\ 3457 color:blue;\ 3458}\ 3459\ 3460.pass > td:first-child, .fail > td:first-child, .timeout > td:first-child, .notrun > td:first-child {\ 3461 font-variant:small-caps;\ 3462}\ 3463\ 3464table#results span {\ 3465 display:block;\ 3466}\ 3467\ 3468table#results span.expected {\ 3469 font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;\ 3470 white-space:pre;\ 3471}\ 3472\ 3473table#results span.actual {\ 3474 font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;\ 3475 white-space:pre;\ 3476}\ 3477\ 3478span.ok {\ 3479 color:green;\ 3480}\ 3481\ 3482tr.error {\ 3483 color:red;\ 3484}\ 3485\ 3486span.timeout {\ 3487 color:red;\ 3488}\ 3489\ 3490span.ok, span.timeout, span.error {\ 3491 font-variant:small-caps;\ 3492}\ 3493"; 3494 3495})(this); 3496// vim: set expandtab shiftwidth=4 tabstop=4: 3497